Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/assets/prompts/konteks-warm-up.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ Optional focus: {{focus}}

If the optional focus is non-empty, pass it to `konteks_warm_up` as a single item in the `focus` array. If it is empty, omit `focus`.

If the warm-up result includes an `update` object, mention the available Konteks update and its command before the ready message. If no `update` object is present, do not mention updates.

After context is loaded, do not summarize or re-explain what you found unless the user explicitly asks. Reply only: `Konteks is warmed up and ready for the task.` no further action is required.
20 changes: 20 additions & 0 deletions src/entrypoints/cli/commands/_base-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Command as CommanderCommand } from 'commander'
import consoleOutput from '@/support/console-output'
import getVersion from '@/support/get-version'
import { checkForKonteksUpdate } from '@/support/update-check'

export type Command = CommanderCommand

Expand Down Expand Up @@ -78,6 +79,10 @@ export default abstract class BaseCommand<
args,
options: commandOptions,
})

if (this.name !== 'mcp') {
await printUpdateNotice()
}
})
}

Expand Down Expand Up @@ -108,6 +113,21 @@ export default abstract class BaseCommand<
}
}

async function printUpdateNotice(): Promise<void> {
const update = await checkForKonteksUpdate()
if (!update) {
return
}

consoleOutput.print(color =>
[
'',
`${color.warning('Update available')}: konteks-cli ${update.current} -> ${update.latest}`,
`${color.dim('Run')}: ${color.primary(update.command)}`,
].join('\n'),
)
}

function stripCommanderActionValues<Options extends object>(
values: unknown[],
options: Options,
Expand Down
22 changes: 20 additions & 2 deletions src/entrypoints/mcp/tools/warm-up-mcp-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
updateChangedProjectMemorySilently,
} from '@/modules/memory/runtime'
import { estimateCharacterTokens } from '@/support/format/tokens'
import { checkForKonteksUpdate } from '@/support/update-check'
import type {
MemorySearchResult,
RecallPackage,
Expand Down Expand Up @@ -62,22 +63,39 @@ export default class WarmUpMcpTool extends BaseMcpTool<Input> {
)
}

return toWarmUpOutput({ recall, warmUp })
return toWarmUpOutput({
recall,
update: await checkForKonteksUpdate(),
warmUp,
})
}
}

function toWarmUpOutput(input: {
warmUp: WarmUpContext
recall?: RecallPackage
update?: {
command: string
current: string
latest: string
}
}): object {
const { warmUp, recall } = input
const { warmUp, recall, update } = input
return {
description: warmUp.description,
entryPoints: warmUp.entryPoints,
guidance: warmUp.guidance.map(toGuidanceOutput),
highlights: warmUp.highlights.map(toWarmUpHighlightOutput),
recall: recall ? toFocusedRecallOutput(recall) : undefined,
technologies: warmUp.technologies,
update: update
? {
available: true,
command: update.command,
current: update.current,
latest: update.latest,
}
: undefined,
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,14 @@ declare module '*.wasm' {
const path: string
export default path
}

declare var __konteksCheckForUpdateForTests:
| (() => Promise<
| {
command: string
current: string
latest: string
}
| undefined
>)
| undefined
207 changes: 207 additions & 0 deletions src/support/update-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { join } from 'node:path'
import getVersion from './get-version'

const PACKAGE_NAME = 'konteks-cli'
const REGISTRY_LATEST_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
const FETCH_TIMEOUT_MS = 750

export type UpdateNotice = {
command: string
current: string
latest: string
}

type UpdateCache = {
checkedAt: string
latest?: string
}

type UpdateCheckOptions = {
cacheDir?: string
currentVersion?: string
fetchLatestVersion?: () => Promise<string | undefined>
now?: Date
ttlMs?: number
}

export async function checkForKonteksUpdate(
options: UpdateCheckOptions = {},
): Promise<UpdateNotice | undefined> {
if (globalThis.__konteksCheckForUpdateForTests) {
return globalThis.__konteksCheckForUpdateForTests()
}
if (
process.env.KONTEKS_SQLITE_TEST_DATABASE &&
!options.fetchLatestVersion
) {
return undefined
}

try {
const current = options.currentVersion ?? getVersion()
const now = options.now ?? new Date()
const ttlMs = options.ttlMs ?? CACHE_TTL_MS
const cacheDir = options.cacheDir ?? updateCacheDir()
const cached = await readCachedUpdate(cacheDir)

if (cached && now.getTime() - Date.parse(cached.checkedAt) < ttlMs) {
return cached.latest
? noticeFromVersions(current, cached.latest)
: undefined
}

const latest = await (
options.fetchLatestVersion ?? fetchLatestVersion
)().catch(() => undefined)
if (!latest) {
await writeCachedUpdate(cacheDir, {
checkedAt: now.toISOString(),
})
return undefined
}

await writeCachedUpdate(cacheDir, {
checkedAt: now.toISOString(),
latest,
})

return noticeFromVersions(current, latest)
} catch {
return undefined
}
}

function noticeFromVersions(
current: string,
latest: string,
): UpdateNotice | undefined {
return isVersionNewer(latest, current)
? {
command: updateCommand(),
current,
latest,
}
: undefined
}

async function fetchLatestVersion(): Promise<string | undefined> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)

try {
const response = await fetch(REGISTRY_LATEST_URL, {
headers: { accept: 'application/json' },
signal: controller.signal,
})
if (!response.ok) {
return undefined
}

const payload = (await response.json()) as { version?: unknown }
return typeof payload.version === 'string' ? payload.version : undefined
} catch {
return undefined
} finally {
clearTimeout(timeout)
}
}

async function readCachedUpdate(
cacheDir: string,
): Promise<UpdateCache | undefined> {
try {
const parsed = JSON.parse(
await readFile(updateCachePath(cacheDir), 'utf8'),
) as {
checkedAt?: unknown
latest?: unknown
}

if (typeof parsed.checkedAt === 'string') {
return {
checkedAt: parsed.checkedAt,
latest:
typeof parsed.latest === 'string'
? parsed.latest
: undefined,
}
}
} catch {
return undefined
}

return undefined
}

async function writeCachedUpdate(
cacheDir: string,
cache: UpdateCache,
): Promise<void> {
try {
await mkdir(cacheDir, { recursive: true })
await writeFile(
updateCachePath(cacheDir),
JSON.stringify(cache),
'utf8',
)
} catch {
// Cache writes are best-effort.
}
}

function updateCachePath(cacheDir: string): string {
return join(cacheDir, 'update-check.json')
}

function updateCacheDir(): string {
const xdgCacheHome = process.env.XDG_CACHE_HOME
if (xdgCacheHome) {
return join(xdgCacheHome, 'konteks')
}

const localAppData = process.env.LOCALAPPDATA
if (localAppData) {
return join(localAppData, 'konteks', 'cache')
}

return join(homedir(), '.cache', 'konteks')
}

function updateCommand(): string {
const userAgent = process.env.npm_config_user_agent?.toLowerCase() ?? ''
return userAgent.includes('bun') || process.versions.bun
? 'bun add -g konteks-cli'
: 'npm install -g konteks-cli'
}

function isVersionNewer(candidate: string, current: string): boolean {
const candidateParts = parseVersion(candidate)
const currentParts = parseVersion(current)

if (!candidateParts || !currentParts) {
return false
}

for (let index = 0; index < candidateParts.length; index += 1) {
if (candidateParts[index] > currentParts[index]) {
return true
}
if (candidateParts[index] < currentParts[index]) {
return false
}
}

return false
}

function parseVersion(version: string): [number, number, number] | undefined {
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u.exec(version)
if (!match) {
return undefined
}

return [Number(match[1]), Number(match[2]), Number(match[3])]
}
Loading