UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

478 lines (455 loc) • 21.5 kB
import { readFileSync } from "node:fs"; import { join, relative, resolve } from "node:path"; import { redactSecrets } from "../redact.ts"; import { createMemoryNote, formatMemoryBriefText, generateMemoryBrief, syncHarnessMemory, type MemoryScope, } from "../memory.ts"; import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "../skills.ts"; import { writeCompiledWiki } from "../wiki.ts"; import { CloudTokenError } from "../hub-binding.ts"; import { resolveControlPlaneAuthToken } from "../hub-env.ts"; import { print, printPlan, type Runtime } from "./types.ts"; /** Authenticated hub POST for context operations. The CLI operates as the * control plane: the administrative token scopes one project per request. */ export async function hubContextPost(input: { serverUrl: string; path: string; body: Record<string, unknown>; authToken?: string | undefined; /** When set, a cloud binding's token source replaces the explicit env token. */ env?: NodeJS.ProcessEnv | undefined; command?: string | undefined; io?: Runtime["io"] | undefined; json?: boolean | undefined; fetchImpl: typeof fetch; }): Promise<{ ok: boolean; status: number; body: unknown; exitCode?: number }> { let authToken = input.authToken; if (input.env) { try { authToken = resolveControlPlaneAuthToken(input.env); } catch (error) { if (error instanceof CloudTokenError) { const command = input.command ?? "context"; if (input.io) { print(input.io, Boolean(input.json), { ok: false, command, error: error.code, detail: error.message }, error.message); } return { ok: false, status: 0, body: { error: error.code, detail: error.message }, exitCode: 2 }; } throw error; } } const response = await input.fetchImpl(`${input.serverUrl.replace(/\/$/, "")}${input.path}`, { method: "POST", headers: { "content-type": "application/json", ...(authToken ? { authorization: `Bearer ${authToken}` } : {}), }, body: JSON.stringify(input.body), }); const text = redactSecrets((await response.text()).slice(0, 64_000)); let body: unknown = text; try { body = JSON.parse(text); } catch { // Keep text for diagnostics without treating it as a secret. } return { ok: response.ok, status: response.status, body }; } export function parseContextKinds(value: string | undefined): string[] | undefined { if (!value) return undefined; return value.split(",").map((kind) => kind.trim()).filter((kind) => kind.length > 0); } export async function cmdContextGet(runtime: Runtime, project: string, options: { role: string; task: string; run?: string | undefined; stage?: string | undefined; budget?: string | undefined; kinds?: string | undefined }): Promise<number> { const budget = options.budget === undefined ? undefined : Number(options.budget); if (options.budget !== undefined && (!Number.isInteger(budget) || (budget as number) < 512 || (budget as number) > 200_000)) { runtime.io.stderr("context get --budget must be an integer between 512 and 200000\n"); return 2; } const body: Record<string, unknown> = { project, role: options.role, task: options.task, }; if (options.run) body.workflowRunId = options.run; if (options.stage) body.stageId = options.stage; if (budget !== undefined) body.budgetTokens = budget; const kinds = parseContextKinds(options.kinds); if (kinds) body.includeKinds = kinds; const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/get", body, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context get", status: response.status, ...(response.body as object) }, `context get ${response.ok ? "assembled" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextRecall(runtime: Runtime, project: string, options: { query?: string | undefined; kinds?: string | undefined; limit?: string | undefined }): Promise<number> { const body: Record<string, unknown> = { project }; if (options.query) body.query = options.query; const kinds = parseContextKinds(options.kinds); if (kinds) body.kinds = kinds; if (options.limit) body.limit = Number(options.limit); const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/recall", body, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context recall", status: response.status, ...(response.body as object) }, `context recall ${response.ok ? "complete" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextState(runtime: Runtime, project: string, key: string, options: { asOf?: string | undefined }): Promise<number> { const body: Record<string, unknown> = { project, key }; if (options.asOf) body.asOf = options.asOf; const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/state", body, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context state", status: response.status, ...(response.body as object) }, `context state ${response.ok ? "resolved" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextEpisode(runtime: Runtime, project: string, options: { run?: string | undefined }): Promise<number> { const body: Record<string, unknown> = { project }; if (options.run) body.workflowRunId = options.run; const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/episode", body, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context episode", status: response.status, ...(response.body as object) }, `context episode ${response.ok ? "complete" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextPromote(runtime: Runtime, project: string, proposalId: string, options: { evidence: string }): Promise<number> { const evidence = options.evidence.split(",").map((ref) => ref.trim()).filter((ref) => ref.length > 0); if (evidence.length === 0) { runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n"); return 2; } if (runtime.dryRun) { printPlan( runtime, { command: "context promote", project, proposalId, evidence }, [{ action: "request", target: `POST ${runtime.serverUrl.replace(/\/$/, "")}/v1/context/state/promote` }], `promote proposal ${proposalId} in ${project}`, ); return 0; } const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/state/promote", body: { project, proposalId, evidence }, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context promote", status: response.status, ...(response.body as object) }, `context promote ${response.ok ? "recorded" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextExplain(runtime: Runtime, project: string, itemId: string): Promise<number> { const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/explain", body: { project, id: itemId }, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; print(runtime.io, runtime.json, { ok: response.ok, command: "context explain", status: response.status, ...(response.body as object) }, `context explain ${response.ok ? "complete" : `failed (${response.status})`}`); return response.ok ? 0 : 1; } export async function cmdContextWikiCompile(runtime: Runtime, project: string, options: { out?: string | undefined }): Promise<number> { const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/wiki/compile", body: { project }, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; if (!response.ok) { print(runtime.io, runtime.json, { ok: false, command: "context wiki-compile", status: response.status, body: response.body }, `wiki compile failed (${response.status})`); return 1; } const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] }; if (options.out && runtime.dryRun) { const outDir = options.out; printPlan( runtime, { command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, outDir }, compiled.pages.map((page) => ({ action: "write", target: resolve(outDir, page.path) })), `write ${compiled.pages.length} compiled wiki page(s) to ${outDir}`, ); return 0; } let written: string[] = []; if (options.out) { const pages = new Map(compiled.pages.map((page) => [page.path, page.content])); written = writeCompiledWiki(options.out, { pages, index: pages.get(".kxm/knowledge/wiki/index.md") ?? "", audit: { project, pages: compiled.audit.pages, stateItems: 0, contextItems: 0, contradictions: compiled.audit.contradictions, compiledAt: "" } }); } print(runtime.io, runtime.json, { ok: true, command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, ...(written.length > 0 ? { written: written.length, outDir: options.out } : { dryRun: true }) }, `compiled ${compiled.audit.pages.length} wiki page(s)${written.length > 0 ? ` to ${options.out}` : " (dry-run)"}`); return 0; } export async function cmdContextWikiLint(runtime: Runtime, project: string): Promise<number> { const response = await hubContextPost({ serverUrl: runtime.serverUrl, path: "/v1/context/wiki/compile", body: { project }, env: runtime.env, command: "context", io: runtime.io, json: runtime.json, fetchImpl: runtime.fetchImpl, }); if (response.exitCode) return response.exitCode; if (!response.ok) { print(runtime.io, runtime.json, { ok: false, command: "context wiki-lint", status: response.status, body: response.body }, `wiki lint failed (${response.status})`); return 1; } const compiled = response.body as { audit: { stateItems: number; contextItems: number; contradictions: number; compiledAt: string }; lint: { severity: string; rule: string; path: string; message: string }[]; }; const issues = compiled.lint; print(runtime.io, runtime.json, { ok: issues.length === 0, command: "context wiki-lint", project, issues, audit: compiled.audit }, issues.length === 0 ? "wiki lint clean" : `wiki lint found ${issues.length} issue(s)`); return issues.every((issue) => issue.severity !== "error") ? 0 : 1; } export function skillStateFromFlag(value: string): SkillState { if (value === "candidate" || value === "promoted" || value === "quarantined" || value === "rejected") return value; throw new Error(`invalid skill state ${value}`); } export function skillsRoot(runtime: Runtime): string { return join(runtime.dirs.workdir, ".kxm", "skills"); } export function csv(value: string | undefined): string[] | undefined { if (!value) return undefined; const items = value.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0); return items.length > 0 ? items : undefined; } export async function cmdSkillsCreate(runtime: Runtime, options: { file: string; name: string; description?: string | undefined; createdBy: string; run?: string | undefined; journal?: string | undefined; receipt?: string | undefined; harness: string; models: string; supersedes?: string | undefined }): Promise<number> { if (runtime.dryRun) { print(runtime.io, runtime.json, { ok: true, command: "skills create", dryRun: true, name: options.name }, "would create skill candidate"); return 0; } const lifecycle = new SkillLifecycle(skillsRoot(runtime)); try { const metadata = lifecycle.create({ name: options.name, description: options.description ?? "", content: readFileSync(options.file, "utf8"), createdBy: options.createdBy, sources: { runIds: csv(options.run) ?? [], journalEntryIds: csv(options.journal) ?? [], evidenceReceipts: csv(options.receipt) ?? [], }, compatibility: { harness: options.harness, models: csv(options.models) ?? [] }, ...(options.supersedes ? { supersedes: options.supersedes } : {}), }); print(runtime.io, runtime.json, { ok: true, command: "skills create", metadata }, `created skill candidate ${metadata.id}`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills create failed: ${message}\n`); return 1; } } export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean | undefined; score?: string | undefined; details?: string | undefined }): Promise<number> { const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun }); try { const outcome = lifecycle.evaluate(skillId, { kind: options.kind as SkillEvaluationKind, evaluatorVersion: options.evaluator, passed: options.fail !== true, ...(options.score !== undefined ? { score: Number(options.score) } : {}), ...(options.details ? { details: options.details } : {}), }); if (runtime.dryRun) { printPlan(runtime, { command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, lifecycle.planned, `record ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`); return 0; } print(runtime.io, runtime.json, { ok: true, command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, `recorded ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills evaluate failed: ${message}\n`); return 1; } } export async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string | undefined }): Promise<number> { const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun }); try { const promoted = lifecycle.promote(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "passed protected evaluation", evidenceRefs: csv(options.evidence) ?? [], }); if (runtime.dryRun) { printPlan(runtime, { command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, lifecycle.planned, `promote skill ${skillId}`); return 0; } print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills promote failed: ${message}\n`); return 1; } } export async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string | undefined }): Promise<number> { const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun }); try { const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" }); if (runtime.dryRun) { printPlan(runtime, { command: "skills reject", skillId, metadata }, lifecycle.planned, `reject skill ${skillId} (history retained)`); return 0; } print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills reject failed: ${message}\n`); return 1; } } export async function cmdSkillsList(runtime: Runtime, options: { state: string }): Promise<number> { try { const lifecycle = new SkillLifecycle(skillsRoot(runtime)); const state = skillStateFromFlag(options.state); const items = lifecycle.list(state).map((metadata) => ({ id: metadata.id, name: metadata.name, version: metadata.version, createdBy: metadata.createdBy, createdAt: metadata.createdAt, models: metadata.compatibility.models, })); print(runtime.io, runtime.json, { ok: true, command: "skills list", state: options.state, skills: items }, `${items.length} ${state} skill(s)`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills list failed: ${message}\n`); return 1; } } export async function cmdSkillsVerify(runtime: Runtime, skillId: string, options: { state: string }): Promise<number> { try { const lifecycle = new SkillLifecycle(skillsRoot(runtime)); const state = skillStateFromFlag(options.state); const metadata = lifecycle.verify(state, skillId); print(runtime.io, runtime.json, { ok: true, command: "skills verify", skillId, state: options.state, contentSha256: metadata.contentSha256 }, `skill ${skillId} integrity verified`); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`skills verify failed: ${message}\n`); return 1; } } export async function cmdMemoryBrief(runtime: Runtime): Promise<number> { try { const brief = generateMemoryBrief(runtime.cwd); const text = formatMemoryBriefText(brief); print(runtime.io, runtime.json, { ok: true, command: "memory brief", brief }, text); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`memory brief failed: ${message}\n`); return 1; } } export async function cmdMemoryNote( runtime: Runtime, fact: string, options: { scope?: string | undefined; kind?: string | undefined; body?: string | undefined }, ): Promise<number> { try { const { record, path } = createMemoryNote(runtime.cwd, fact, { scope: (options.scope ?? "project") as MemoryScope, ...(options.kind !== undefined ? { kind: options.kind } : {}), ...(options.body !== undefined ? { body: options.body } : {}), dryRun: runtime.dryRun, }); const relPath = relative(runtime.cwd, path); if (runtime.dryRun) { printPlan(runtime, { command: "memory note", candidate: record, path: relPath }, [{ action: "write", target: path }], `record memory candidate in ${relPath} (the id is assigned when it is recorded)`); return 0; } print( runtime.io, runtime.json, { ok: true, command: "memory note", candidate: record, path: relPath }, `Recorded memory candidate ${record.id} in ${relPath} (promoted via PR)`, ); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`memory note failed: ${message}\n`); return 1; } } export async function cmdMemorySync(runtime: Runtime): Promise<number> { try { const result = syncHarnessMemory(runtime.cwd, { dryRun: runtime.dryRun }); if (runtime.dryRun) { printPlan( runtime, { command: "memory sync", ...result }, result.updated.map((file) => ({ action: "write", target: join(runtime.cwd, file) })), "regenerate the project memory block in the instruction files this project already has", ); return 0; } const lines = [ ...(result.updated.length > 0 ? [`updated: ${result.updated.join(", ")}`] : []), ...(result.unchanged.length > 0 ? [`unchanged: ${result.unchanged.join(", ")}`] : []), ...(result.missing.length > 0 ? [`not present, not created: ${result.missing.join(", ")}`] : []), ]; print( runtime.io, runtime.json, { ok: true, command: "memory sync", ...result }, `Synced project memory\n${lines.join("\n")}`, ); return 0; } catch (error) { const message = error instanceof Error ? error.message : String(error); runtime.io.stderr(`memory sync failed: ${message}\n`); return 1; } }