@kontextmind/kxm 0.7.86 → 0.7.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/docs/configuration.md +2 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +895 -498
- package/plugins/kxm/dist/extension.js +70 -61
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +44 -26
- package/plugins/kxm/dist/server.js +34 -23
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/src/cli/context-skills.ts +51 -6
- package/plugins/kxm/src/cli/hub.ts +39 -14
- package/plugins/kxm/src/cli/project.ts +78 -23
- package/plugins/kxm/src/cli/roles.ts +48 -4
- package/plugins/kxm/src/cli/system.ts +18 -1
- package/plugins/kxm/src/cli/tasks.ts +34 -5
- package/plugins/kxm/src/cli/types.ts +30 -0
- package/plugins/kxm/src/cli/workflows.ts +20 -4
- package/plugins/kxm/src/cli.ts +57 -0
- package/plugins/kxm/src/config.ts +4 -3
- package/plugins/kxm/src/database.ts +72 -33
- package/plugins/kxm/src/local-snapshot.ts +4 -4
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/memory.ts +10 -7
- package/plugins/kxm/src/role.ts +18 -6
- package/plugins/kxm/src/session-work.ts +4 -1
- package/plugins/kxm/src/skills.ts +29 -15
- package/plugins/kxm/src/sqlite.ts +18 -0
- package/plugins/kxm/src/task-manager.ts +20 -13
- package/plugins/kxm/src/workflow-manager.ts +12 -4
|
@@ -14783,7 +14783,7 @@ import { createHash as createHash5, createHmac } from "node:crypto";
|
|
|
14783
14783
|
import { existsSync as existsSync6 } from "node:fs";
|
|
14784
14784
|
import { createServer } from "node:http";
|
|
14785
14785
|
import { isIP } from "node:net";
|
|
14786
|
-
import { dirname as
|
|
14786
|
+
import { dirname as dirname6, join as join6, resolve as resolve6 } from "node:path";
|
|
14787
14787
|
|
|
14788
14788
|
// plugins/kxm/src/protocol.ts
|
|
14789
14789
|
import { randomUUID } from "node:crypto";
|
|
@@ -18213,7 +18213,7 @@ function boundedRefs(value, field) {
|
|
|
18213
18213
|
var import_yaml4 = __toESM(require_dist(), 1);
|
|
18214
18214
|
import { createHash as createHash4 } from "node:crypto";
|
|
18215
18215
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
18216
|
-
import { join as join4 } from "node:path";
|
|
18216
|
+
import { dirname as dirname4, join as join4 } from "node:path";
|
|
18217
18217
|
var SKILL_CANDIDATE_SCHEMA = "kxm.skill-candidate.v1";
|
|
18218
18218
|
var SKILL_EVALUATION_SCHEMA = "kxm.skill-evaluation.v1";
|
|
18219
18219
|
var SKILL_DECISION_SCHEMA = "kxm.skill-decision.v1";
|
|
@@ -18301,10 +18301,22 @@ var SkillLifecycle = class {
|
|
|
18301
18301
|
root;
|
|
18302
18302
|
now;
|
|
18303
18303
|
allowOptimizationEvals;
|
|
18304
|
+
dryRun;
|
|
18305
|
+
/** What a `dryRun` lifecycle would have written or moved, in order. */
|
|
18306
|
+
planned = [];
|
|
18304
18307
|
constructor(root, options = {}) {
|
|
18305
18308
|
this.root = root;
|
|
18306
18309
|
this.now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
18307
18310
|
this.allowOptimizationEvals = options.allowOptimizationEvals === true;
|
|
18311
|
+
this.dryRun = options.dryRun === true;
|
|
18312
|
+
}
|
|
18313
|
+
write(file, content) {
|
|
18314
|
+
if (this.dryRun) {
|
|
18315
|
+
this.planned.push({ action: "write", target: file });
|
|
18316
|
+
return;
|
|
18317
|
+
}
|
|
18318
|
+
mkdirSync2(dirname4(file), { recursive: true });
|
|
18319
|
+
writeFileSync2(file, content);
|
|
18308
18320
|
}
|
|
18309
18321
|
dir(state) {
|
|
18310
18322
|
return join4(this.root, state === "candidate" ? "candidates" : `${state}s`.replace("rejecteds", "rejected").replace("promoteds", "promoted"));
|
|
@@ -18317,15 +18329,14 @@ var SkillLifecycle = class {
|
|
|
18317
18329
|
return { dir, metadata: join4(dir, "metadata.json"), skill: join4(dir, "SKILL.md") };
|
|
18318
18330
|
}
|
|
18319
18331
|
appendHistory(id, record) {
|
|
18320
|
-
mkdirSync2(join4(this.root, "history"), { recursive: true });
|
|
18321
18332
|
const line = `${JSON.stringify(record)}
|
|
18322
18333
|
`;
|
|
18323
18334
|
if (existsSync4(this.historyFile(id))) {
|
|
18324
18335
|
const existing = readFileSync3(this.historyFile(id), "utf8");
|
|
18325
18336
|
const lines = existing.split("\n").filter((entry) => entry.trim());
|
|
18326
|
-
|
|
18337
|
+
this.write(this.historyFile(id), [...lines.slice(-499), line.trim()].join("\n") + "\n");
|
|
18327
18338
|
} else {
|
|
18328
|
-
|
|
18339
|
+
this.write(this.historyFile(id), line);
|
|
18329
18340
|
}
|
|
18330
18341
|
}
|
|
18331
18342
|
history(id) {
|
|
@@ -18346,6 +18357,10 @@ var SkillLifecycle = class {
|
|
|
18346
18357
|
if (!existsSync4(fromDir)) {
|
|
18347
18358
|
throw new SkillLifecycleError("skill_not_found", `skill ${id} not found in ${from}`);
|
|
18348
18359
|
}
|
|
18360
|
+
if (this.dryRun) {
|
|
18361
|
+
this.planned.push({ action: "move", target: `${fromDir} -> ${toDir}` });
|
|
18362
|
+
return;
|
|
18363
|
+
}
|
|
18349
18364
|
mkdirSync2(this.dir(to), { recursive: true });
|
|
18350
18365
|
if (existsSync4(toDir)) rmSync(toDir, { recursive: true, force: true });
|
|
18351
18366
|
renameSync(fromDir, toDir);
|
|
@@ -18389,7 +18404,7 @@ var SkillLifecycle = class {
|
|
|
18389
18404
|
if (!harness) throw new SkillLifecycleError("invalid_skill_compatibility", "compatibility.harness is required");
|
|
18390
18405
|
const contentSha256 = skillContentSha256(content);
|
|
18391
18406
|
const id = skillIdFor(name, contentSha256);
|
|
18392
|
-
const {
|
|
18407
|
+
const { metadata, skill } = this.paths("candidate", id);
|
|
18393
18408
|
if (existsSync4(metadata)) {
|
|
18394
18409
|
throw new SkillLifecycleError(
|
|
18395
18410
|
"skill_candidate_exists",
|
|
@@ -18409,9 +18424,8 @@ var SkillLifecycle = class {
|
|
|
18409
18424
|
createdAt: this.now(),
|
|
18410
18425
|
...input.supersedes ? { supersedes: input.supersedes } : {}
|
|
18411
18426
|
};
|
|
18412
|
-
|
|
18413
|
-
|
|
18414
|
-
writeFileSync2(metadata, `${JSON.stringify(record, null, 2)}
|
|
18427
|
+
this.write(skill, content);
|
|
18428
|
+
this.write(metadata, `${JSON.stringify(record, null, 2)}
|
|
18415
18429
|
`);
|
|
18416
18430
|
this.appendHistory(id, { schema: "kxm.skill-history-event.v1", event: "candidate_created", by: createdBy, supersedes: input.supersedes, at: record.createdAt });
|
|
18417
18431
|
return record;
|
|
@@ -18500,18 +18514,15 @@ var SkillLifecycle = class {
|
|
|
18500
18514
|
};
|
|
18501
18515
|
const candidatePaths = this.paths("candidate", candidateId);
|
|
18502
18516
|
const promotedPaths = this.paths("promoted", candidateId);
|
|
18503
|
-
mkdirSync2(promotedPaths.dir, { recursive: true });
|
|
18504
18517
|
const skillContent = readFileSync3(candidatePaths.skill, "utf8");
|
|
18505
18518
|
const metadataContent = readFileSync3(candidatePaths.metadata, "utf8");
|
|
18506
|
-
|
|
18507
|
-
|
|
18508
|
-
const
|
|
18509
|
-
mkdirSync2(patchesDir, { recursive: true });
|
|
18510
|
-
const patchPath = join4(patchesDir, `${candidateId}.patch`);
|
|
18519
|
+
this.write(promotedPaths.skill, skillContent);
|
|
18520
|
+
this.write(promotedPaths.metadata, metadataContent);
|
|
18521
|
+
const patchPath = join4(this.root, "patches", `${candidateId}.patch`);
|
|
18511
18522
|
const relSkillPath = `.kxm/skills/promoted/${candidateId}/SKILL.md`;
|
|
18512
18523
|
const relMetaPath = `.kxm/skills/promoted/${candidateId}/metadata.json`;
|
|
18513
18524
|
const patch = `${createUnifiedPatch(relSkillPath, skillContent)}${createUnifiedPatch(relMetaPath, metadataContent)}`;
|
|
18514
|
-
|
|
18525
|
+
this.write(patchPath, patch);
|
|
18515
18526
|
this.appendHistory(candidateId, record);
|
|
18516
18527
|
return { ...metadata, patch, patchPath };
|
|
18517
18528
|
}
|
|
@@ -19031,7 +19042,7 @@ import {
|
|
|
19031
19042
|
unlinkSync,
|
|
19032
19043
|
writeFileSync as writeFileSync4
|
|
19033
19044
|
} from "node:fs";
|
|
19034
|
-
import { basename as basename3, dirname as
|
|
19045
|
+
import { basename as basename3, dirname as dirname5, join as join5, resolve as resolve4 } from "node:path";
|
|
19035
19046
|
|
|
19036
19047
|
// plugins/kxm/src/sqlite.ts
|
|
19037
19048
|
import { createRequire } from "node:module";
|
|
@@ -19078,7 +19089,7 @@ function databaseError(code, file, message) {
|
|
|
19078
19089
|
return new KxmConfigError([issue2]);
|
|
19079
19090
|
}
|
|
19080
19091
|
function checkedParent(path, description) {
|
|
19081
|
-
const parent =
|
|
19092
|
+
const parent = dirname5(path);
|
|
19082
19093
|
if (!existsSync5(parent)) mkdirSync4(parent, { recursive: true, mode: 448 });
|
|
19083
19094
|
const stat = lstatSync2(parent, { throwIfNoEntry: false });
|
|
19084
19095
|
if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
@@ -20359,7 +20370,7 @@ function createMeshHub(options = {}) {
|
|
|
20359
20370
|
const logger = options.logger ?? (() => void 0);
|
|
20360
20371
|
const hubNow = options.now ?? (() => Date.now());
|
|
20361
20372
|
const assetsDir2 = options.assetsDir;
|
|
20362
|
-
const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(
|
|
20373
|
+
const hubRepoRoot = options.repoRoot ?? (options.dataPath && options.dataPath !== ":memory:" ? resolve6(dirname6(dirname6(options.dataPath))) : process.cwd());
|
|
20363
20374
|
const store = new MeshStore(options.dataPath);
|
|
20364
20375
|
const agents = store.agents;
|
|
20365
20376
|
const messages = store.messages;
|
|
@@ -22556,11 +22567,11 @@ data: ${JSON.stringify({ agent: publicAgent(current, staleAfterMs) })}
|
|
|
22556
22567
|
|
|
22557
22568
|
// plugins/kxm/src/server.ts
|
|
22558
22569
|
import { mkdirSync as mkdirSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
22559
|
-
import { dirname as
|
|
22570
|
+
import { dirname as dirname8, join as join7, resolve as resolve7 } from "node:path";
|
|
22560
22571
|
|
|
22561
22572
|
// plugins/kxm/src/logger.ts
|
|
22562
22573
|
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, renameSync as renameSync3, statSync as statSync2, unlinkSync as unlinkSync2 } from "node:fs";
|
|
22563
|
-
import { dirname as
|
|
22574
|
+
import { dirname as dirname7 } from "node:path";
|
|
22564
22575
|
var LOG_LEVEL_PRIORITY = {
|
|
22565
22576
|
debug: 10,
|
|
22566
22577
|
info: 20,
|
|
@@ -22666,7 +22677,7 @@ function createLogger(options) {
|
|
|
22666
22677
|
currentSize = 0;
|
|
22667
22678
|
}
|
|
22668
22679
|
try {
|
|
22669
|
-
mkdirSync5(
|
|
22680
|
+
mkdirSync5(dirname7(filePath), { recursive: true });
|
|
22670
22681
|
appendFileSync(filePath, line, { encoding: "utf8", mode: 384 });
|
|
22671
22682
|
currentSize += lineBytes;
|
|
22672
22683
|
} catch {
|
|
@@ -22729,7 +22740,7 @@ var rateLimitWindowMs = Number.parseInt(
|
|
|
22729
22740
|
process.env.KXM_RATE_LIMIT_WINDOW_MS ?? String(DEFAULT_RATE_LIMIT_WINDOW_MS),
|
|
22730
22741
|
10
|
|
22731
22742
|
);
|
|
22732
|
-
for (const directory of [configDir, logsDir, assetsDir, stateDir,
|
|
22743
|
+
for (const directory of [configDir, logsDir, assetsDir, stateDir, dirname8(logPath)]) {
|
|
22733
22744
|
mkdirSync6(directory, { recursive: true });
|
|
22734
22745
|
}
|
|
22735
22746
|
var structuredLog = createLogger({
|
package/plugins/kxm/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join, relative } from "node:path";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import { redactSecrets } from "../redact.ts";
|
|
4
4
|
import {
|
|
5
5
|
createMemoryNote,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "../memory.ts";
|
|
11
11
|
import { SkillLifecycle, type SkillEvaluationKind, type SkillState } from "../skills.ts";
|
|
12
12
|
import { writeCompiledWiki } from "../wiki.ts";
|
|
13
|
-
import { print, type Runtime } from "./types.ts";
|
|
13
|
+
import { print, printPlan, type Runtime } from "./types.ts";
|
|
14
14
|
|
|
15
15
|
/** Authenticated hub POST for context operations. The CLI operates as the
|
|
16
16
|
* control plane: the administrative token scopes one project per request. */
|
|
@@ -122,6 +122,15 @@ export async function cmdContextPromote(runtime: Runtime, project: string, propo
|
|
|
122
122
|
runtime.io.stderr("context promote --evidence must contain at least one durable evidence reference\n");
|
|
123
123
|
return 2;
|
|
124
124
|
}
|
|
125
|
+
if (runtime.dryRun) {
|
|
126
|
+
printPlan(
|
|
127
|
+
runtime,
|
|
128
|
+
{ command: "context promote", project, proposalId, evidence },
|
|
129
|
+
[{ action: "request", target: `POST ${runtime.serverUrl.replace(/\/$/, "")}/v1/context/state/promote` }],
|
|
130
|
+
`promote proposal ${proposalId} in ${project}`,
|
|
131
|
+
);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
125
134
|
const response = await hubContextPost({
|
|
126
135
|
serverUrl: runtime.serverUrl,
|
|
127
136
|
path: "/v1/context/state/promote",
|
|
@@ -158,6 +167,16 @@ export async function cmdContextWikiCompile(runtime: Runtime, project: string, o
|
|
|
158
167
|
return 1;
|
|
159
168
|
}
|
|
160
169
|
const compiled = response.body as { audit: { pages: string[]; contradictions: number }; pages: { path: string; content: string }[] };
|
|
170
|
+
if (options.out && runtime.dryRun) {
|
|
171
|
+
const outDir = options.out;
|
|
172
|
+
printPlan(
|
|
173
|
+
runtime,
|
|
174
|
+
{ command: "context wiki-compile", project, pages: compiled.audit.pages, openContradictions: compiled.audit.contradictions, outDir },
|
|
175
|
+
compiled.pages.map((page) => ({ action: "write", target: resolve(outDir, page.path) })),
|
|
176
|
+
`write ${compiled.pages.length} compiled wiki page(s) to ${outDir}`,
|
|
177
|
+
);
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
161
180
|
let written: string[] = [];
|
|
162
181
|
if (options.out) {
|
|
163
182
|
const pages = new Map(compiled.pages.map((page) => [page.path, page.content]));
|
|
@@ -233,7 +252,7 @@ export async function cmdSkillsCreate(runtime: Runtime, options: { file: string;
|
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, options: { kind: string; evaluator: string; fail?: boolean | undefined; score?: string | undefined; details?: string | undefined }): Promise<number> {
|
|
236
|
-
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
255
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
|
|
237
256
|
try {
|
|
238
257
|
const outcome = lifecycle.evaluate(skillId, {
|
|
239
258
|
kind: options.kind as SkillEvaluationKind,
|
|
@@ -242,6 +261,10 @@ export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, optio
|
|
|
242
261
|
...(options.score !== undefined ? { score: Number(options.score) } : {}),
|
|
243
262
|
...(options.details ? { details: options.details } : {}),
|
|
244
263
|
});
|
|
264
|
+
if (runtime.dryRun) {
|
|
265
|
+
printPlan(runtime, { command: "skills evaluate", skillId, quarantined: outcome.quarantined, evaluation: outcome.evaluation }, lifecycle.planned, `record ${options.kind} evaluation${outcome.quarantined ? " (candidate quarantined)" : ""}`);
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
245
268
|
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)" : ""}`);
|
|
246
269
|
return 0;
|
|
247
270
|
} catch (error) {
|
|
@@ -252,13 +275,17 @@ export async function cmdSkillsEvaluate(runtime: Runtime, skillId: string, optio
|
|
|
252
275
|
}
|
|
253
276
|
|
|
254
277
|
export async function cmdSkillsPromote(runtime: Runtime, skillId: string, options: { decidedBy: string; evidence: string; reason?: string | undefined }): Promise<number> {
|
|
255
|
-
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
278
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
|
|
256
279
|
try {
|
|
257
280
|
const promoted = lifecycle.promote(skillId, {
|
|
258
281
|
decidedBy: options.decidedBy,
|
|
259
282
|
reason: options.reason ?? "passed protected evaluation",
|
|
260
283
|
evidenceRefs: csv(options.evidence) ?? [],
|
|
261
284
|
});
|
|
285
|
+
if (runtime.dryRun) {
|
|
286
|
+
printPlan(runtime, { command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, lifecycle.planned, `promote skill ${skillId}`);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
262
289
|
print(runtime.io, runtime.json, { ok: true, command: "skills promote", skillId, metadata: promoted, patchPath: promoted.patchPath }, `promoted skill ${skillId} (patch: ${promoted.patchPath})`);
|
|
263
290
|
return 0;
|
|
264
291
|
} catch (error) {
|
|
@@ -269,9 +296,13 @@ export async function cmdSkillsPromote(runtime: Runtime, skillId: string, option
|
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
export async function cmdSkillsReject(runtime: Runtime, skillId: string, options: { decidedBy: string; reason?: string | undefined }): Promise<number> {
|
|
272
|
-
const lifecycle = new SkillLifecycle(skillsRoot(runtime));
|
|
299
|
+
const lifecycle = new SkillLifecycle(skillsRoot(runtime), { dryRun: runtime.dryRun });
|
|
273
300
|
try {
|
|
274
301
|
const metadata = lifecycle.reject(skillId, { decidedBy: options.decidedBy, reason: options.reason ?? "rejected" });
|
|
302
|
+
if (runtime.dryRun) {
|
|
303
|
+
printPlan(runtime, { command: "skills reject", skillId, metadata }, lifecycle.planned, `reject skill ${skillId} (history retained)`);
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
275
306
|
print(runtime.io, runtime.json, { ok: true, command: "skills reject", skillId, metadata }, `rejected skill ${skillId} (history retained)`);
|
|
276
307
|
return 0;
|
|
277
308
|
} catch (error) {
|
|
@@ -339,8 +370,13 @@ export async function cmdMemoryNote(
|
|
|
339
370
|
scope: (options.scope ?? "project") as MemoryScope,
|
|
340
371
|
...(options.kind !== undefined ? { kind: options.kind } : {}),
|
|
341
372
|
...(options.body !== undefined ? { body: options.body } : {}),
|
|
373
|
+
dryRun: runtime.dryRun,
|
|
342
374
|
});
|
|
343
375
|
const relPath = relative(runtime.cwd, path);
|
|
376
|
+
if (runtime.dryRun) {
|
|
377
|
+
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)`);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
344
380
|
print(
|
|
345
381
|
runtime.io,
|
|
346
382
|
runtime.json,
|
|
@@ -357,7 +393,16 @@ export async function cmdMemoryNote(
|
|
|
357
393
|
|
|
358
394
|
export async function cmdMemorySync(runtime: Runtime): Promise<number> {
|
|
359
395
|
try {
|
|
360
|
-
const result = syncHarnessMemory(runtime.cwd);
|
|
396
|
+
const result = syncHarnessMemory(runtime.cwd, { dryRun: runtime.dryRun });
|
|
397
|
+
if (runtime.dryRun) {
|
|
398
|
+
printPlan(
|
|
399
|
+
runtime,
|
|
400
|
+
{ command: "memory sync", ...result },
|
|
401
|
+
[...result.created, ...result.updated].map((file) => ({ action: "write", target: join(runtime.cwd, file) })),
|
|
402
|
+
"regenerate project memory blocks in AGENTS.md, CLAUDE.md, and GEMINI.md",
|
|
403
|
+
);
|
|
404
|
+
return 0;
|
|
405
|
+
}
|
|
361
406
|
print(
|
|
362
407
|
runtime.io,
|
|
363
408
|
runtime.json,
|
|
@@ -6,7 +6,7 @@ import { defaultProjectName } from "../project-name.ts";
|
|
|
6
6
|
import { hasClientHubCredential, resolveClientHubAuthToken } from "../hub-env.ts";
|
|
7
7
|
import { agentWorker, type Worker } from "../envelope.ts";
|
|
8
8
|
import { MESH_TUI_PANELS, runMeshTui, type MeshTuiPanel } from "../tui.ts";
|
|
9
|
-
import { formatSessionBriefText, loadSessionBriefAsync, type SessionHubStatus } from "../session-work.ts";
|
|
9
|
+
import { formatSessionBriefText, loadSessionBriefAsync, readCachedSessionBrief, type SessionHubStatus } from "../session-work.ts";
|
|
10
10
|
import {
|
|
11
11
|
HUB_BINDING_SCHEMA,
|
|
12
12
|
HubBindingError,
|
|
@@ -47,11 +47,13 @@ import {
|
|
|
47
47
|
import { createSession, loadNamedWorkers, rosterNames, sessionAssetDirs, workflowAssetDirs, writeSession } from "../session.ts";
|
|
48
48
|
import {
|
|
49
49
|
print,
|
|
50
|
+
printPlan,
|
|
50
51
|
printWorker,
|
|
51
52
|
hostMode,
|
|
52
53
|
workspaceEnv,
|
|
53
54
|
spawnScript,
|
|
54
55
|
processExists,
|
|
56
|
+
type PlannedChange,
|
|
55
57
|
type Runtime,
|
|
56
58
|
} from "./types.ts";
|
|
57
59
|
|
|
@@ -140,7 +142,7 @@ export async function refreshKxmUpdateNotice(
|
|
|
140
142
|
}
|
|
141
143
|
const fetched = await fetchLatestKxmVersion(resolved.source, runtime.env, runtime.fetchImpl);
|
|
142
144
|
const notice = noticeFromVersions(installed, fetched.latest, resolved, fetched.error, fetched.asset);
|
|
143
|
-
writeUpdateCache(runtime.dirs.state, notice);
|
|
145
|
+
if (!runtime.dryRun) writeUpdateCache(runtime.dirs.state, notice);
|
|
144
146
|
return notice;
|
|
145
147
|
}
|
|
146
148
|
|
|
@@ -556,6 +558,12 @@ export async function cmdSessionStatus(runtime: Runtime): Promise<number> {
|
|
|
556
558
|
}
|
|
557
559
|
|
|
558
560
|
export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean | undefined; clear?: boolean | undefined; issue?: boolean | undefined } = {}): Promise<number> {
|
|
561
|
+
const tokenFile = sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR);
|
|
562
|
+
if (options.clear && runtime.dryRun) {
|
|
563
|
+
const present = existsSync(tokenFile);
|
|
564
|
+
printPlan(runtime, { command: "auth token", cleared: present }, present ? [{ action: "delete", target: tokenFile }] : [], present ? "clear the session token from disk" : "no session token file to clear");
|
|
565
|
+
return 0;
|
|
566
|
+
}
|
|
559
567
|
if (options.clear) {
|
|
560
568
|
const cleared = clearSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
561
569
|
print(
|
|
@@ -615,18 +623,19 @@ export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean
|
|
|
615
623
|
return 1;
|
|
616
624
|
}
|
|
617
625
|
|
|
626
|
+
const existingToken = options.issue ? undefined : readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
627
|
+
if (runtime.dryRun && !existingToken) {
|
|
628
|
+
// The token is only worth printing once it is on disk; a dry run never is.
|
|
629
|
+
printPlan(runtime, { command: "auth token" }, [{ action: "write", target: tokenFile }], "issue an operator session token and persist it");
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
|
|
618
633
|
let token: string;
|
|
619
|
-
if (
|
|
634
|
+
if (existingToken) {
|
|
635
|
+
token = existingToken.token;
|
|
636
|
+
} else {
|
|
620
637
|
token = mintSessionToken({ preset: "operator" });
|
|
621
638
|
persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
622
|
-
} else {
|
|
623
|
-
const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
624
|
-
if (existing) {
|
|
625
|
-
token = existing.token;
|
|
626
|
-
} else {
|
|
627
|
-
token = mintSessionToken({ preset: "operator" });
|
|
628
|
-
persistSessionTokenToDisk(token, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
629
|
-
}
|
|
630
639
|
}
|
|
631
640
|
|
|
632
641
|
print(runtime.io, runtime.json, { ok: true, command: "auth token", token }, token);
|
|
@@ -635,11 +644,17 @@ export async function cmdAuthToken(runtime: Runtime, options: { status?: boolean
|
|
|
635
644
|
|
|
636
645
|
export async function cmdSessionBrief(runtime: Runtime, options: { status?: boolean | undefined; token?: boolean | undefined } = {}): Promise<number> {
|
|
637
646
|
const existing = readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
638
|
-
|
|
639
|
-
|
|
647
|
+
// A dry run neither mints nor persists a token; it names the file it would write.
|
|
648
|
+
const planned: PlannedChange[] = existing ? [] : [{ action: "write", target: sessionTokenPath(runtime.env.KXM_USER_CONFIG_DIR) }];
|
|
649
|
+
const sessionToken = existing?.token ?? (runtime.dryRun ? undefined : mintSessionToken({ preset: "operator" }));
|
|
650
|
+
if (!existing && sessionToken) {
|
|
640
651
|
persistSessionTokenToDisk(sessionToken, { userConfigDir: runtime.env.KXM_USER_CONFIG_DIR });
|
|
641
652
|
}
|
|
642
653
|
if (options.token) {
|
|
654
|
+
if (!sessionToken) {
|
|
655
|
+
printPlan(runtime, { command: "session brief" }, planned, "issue an operator session token and persist it");
|
|
656
|
+
return 0;
|
|
657
|
+
}
|
|
643
658
|
print(runtime.io, runtime.json, { ok: true, command: "session brief", sessionToken }, sessionToken);
|
|
644
659
|
return 0;
|
|
645
660
|
}
|
|
@@ -668,9 +683,19 @@ export async function cmdSessionBrief(runtime: Runtime, options: { status?: bool
|
|
|
668
683
|
|
|
669
684
|
const brief = await loadSessionBriefAsync(runtime.dirs.workdir, env, undefined, hub, {
|
|
670
685
|
fetchImpl: runtime.fetchImpl,
|
|
671
|
-
sessionToken,
|
|
686
|
+
...(sessionToken ? { sessionToken } : {}),
|
|
687
|
+
writeCache: !runtime.dryRun,
|
|
672
688
|
});
|
|
673
689
|
|
|
690
|
+
if (runtime.dryRun) {
|
|
691
|
+
if (!readCachedSessionBrief(runtime.dirs.state)) planned.push({ action: "write", target: join(runtime.dirs.state, "session-brief.json") });
|
|
692
|
+
print(runtime.io, runtime.json, { ...brief, dryRun: true, planned }, [
|
|
693
|
+
options.status ? brief.statusLine : formatSessionBriefText(brief),
|
|
694
|
+
...planned.map((change) => `dry run: would ${change.action} ${change.target}`),
|
|
695
|
+
].join("\n"));
|
|
696
|
+
return 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
674
699
|
if (options.status) {
|
|
675
700
|
print(runtime.io, runtime.json, brief, brief.statusLine);
|
|
676
701
|
return 0;
|
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "node:f
|
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
|
-
import { createBackup, restoreBackup } from "../database.ts";
|
|
6
|
+
import { createBackup, planBackup, planRestore, restoreBackup } from "../database.ts";
|
|
7
7
|
import { refreshModelInventory } from "../model-inventory.ts";
|
|
8
8
|
import { listInventoryModels, listRoleBindings, loadRoutePolicy, setRouteState, updateRouteState } from "../routes.ts";
|
|
9
9
|
import {
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
} from "../kxm-update.ts";
|
|
44
44
|
import { loadKxmUpdateConfig } from "../kxm-update-config.ts";
|
|
45
45
|
import type { InstallKindReport, InstallProbe } from "../kxm-install-kind.ts";
|
|
46
|
-
import { print, type CliIo, type CliSpawnResult, type Runtime } from "./types.ts";
|
|
46
|
+
import { print, printPlan, refuseDryRun, type CliIo, type CliSpawnResult, type Runtime } from "./types.ts";
|
|
47
47
|
|
|
48
48
|
export const kxmDriveCliSeams: {
|
|
49
49
|
ensureSupervisor?: typeof ensureKxmSupervisor;
|
|
@@ -181,10 +181,24 @@ export async function cmdKxmInit(
|
|
|
181
181
|
|
|
182
182
|
export async function cmdBackup(runtime: Runtime, options: { out?: string | undefined }): Promise<number> {
|
|
183
183
|
try {
|
|
184
|
-
const
|
|
184
|
+
const backupOptions = {
|
|
185
185
|
projectRoot: runtime.cwd,
|
|
186
186
|
...(options.out ? { outDir: resolve(runtime.cwd, options.out) } : {}),
|
|
187
|
-
}
|
|
187
|
+
};
|
|
188
|
+
if (runtime.dryRun) {
|
|
189
|
+
const plan = planBackup(backupOptions);
|
|
190
|
+
printPlan(
|
|
191
|
+
runtime,
|
|
192
|
+
{ command: "backup", outDir: plan.outDir, stores: plan.stores },
|
|
193
|
+
[
|
|
194
|
+
...plan.stores.map((store) => ({ action: "write" as const, target: join(plan.outDir, store.backupFile) })),
|
|
195
|
+
{ action: "write", target: join(plan.outDir, "manifest.json") },
|
|
196
|
+
],
|
|
197
|
+
`back up ${plan.stores.length} store(s) to ${plan.outDir} (sources are not opened, so their WAL is not checkpointed)`,
|
|
198
|
+
);
|
|
199
|
+
return 0;
|
|
200
|
+
}
|
|
201
|
+
const { manifest, outDir } = createBackup(backupOptions);
|
|
188
202
|
const payload = {
|
|
189
203
|
ok: true,
|
|
190
204
|
command: "backup",
|
|
@@ -211,6 +225,26 @@ export async function cmdBackup(runtime: Runtime, options: { out?: string | unde
|
|
|
211
225
|
|
|
212
226
|
export async function cmdRestore(runtime: Runtime, manifestArg: string): Promise<number> {
|
|
213
227
|
try {
|
|
228
|
+
if (runtime.dryRun) {
|
|
229
|
+
const plan = planRestore(resolve(runtime.cwd, manifestArg), { projectRoot: runtime.cwd });
|
|
230
|
+
printPlan(
|
|
231
|
+
runtime,
|
|
232
|
+
{
|
|
233
|
+
command: "restore",
|
|
234
|
+
backupId: plan.backupId,
|
|
235
|
+
manifestPath: plan.manifestPath,
|
|
236
|
+
stores: plan.stores.map(({ storeId, targetPath, schemaVersion }) => ({ storeId, targetPath, schemaVersion })),
|
|
237
|
+
},
|
|
238
|
+
plan.stores.flatMap((store) => [
|
|
239
|
+
{ action: "write" as const, target: store.targetPath },
|
|
240
|
+
...[`${store.targetPath}-wal`, `${store.targetPath}-shm`]
|
|
241
|
+
.filter((file) => existsSync(file))
|
|
242
|
+
.map((file) => ({ action: "delete" as const, target: file })),
|
|
243
|
+
]),
|
|
244
|
+
`restore ${plan.stores.length} store(s) from ${plan.manifestPath}; digests verified against the manifest`,
|
|
245
|
+
);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
214
248
|
const result = restoreBackup(resolve(runtime.cwd, manifestArg), {
|
|
215
249
|
projectRoot: runtime.cwd,
|
|
216
250
|
});
|
|
@@ -281,10 +315,23 @@ export async function cmdKxmTrust(runtime: Runtime, check: boolean, options: { b
|
|
|
281
315
|
}
|
|
282
316
|
}
|
|
283
317
|
|
|
318
|
+
/** The supervisor a run read talks to. Normally it is started on demand; under
|
|
319
|
+
* `--dry-run` a read may only attach, because starting the Runtime is a change. */
|
|
320
|
+
async function readSupervisor(runtime: Runtime, command: string): Promise<Awaited<ReturnType<typeof ensureKxmSupervisor>> | number> {
|
|
321
|
+
if (!runtime.dryRun) return await (kxmDriveCliSeams.ensureSupervisor ?? ensureKxmSupervisor)({ env: runtime.env });
|
|
322
|
+
const attached = await attachKxmSupervisor({ env: runtime.env });
|
|
323
|
+
return attached ?? refuseDryRun(runtime.io, runtime.json, command, "the Runtime supervisor is not running and --dry-run will not start it");
|
|
324
|
+
}
|
|
325
|
+
|
|
284
326
|
export const RUN_ENGINE_PHASE = "pre-3a";
|
|
285
327
|
export const RUN_ENGINE_NOTICE = "runs remain created until the run engine lands; no steps execute yet";
|
|
286
328
|
|
|
287
|
-
|
|
329
|
+
/** Where `kxm run <workflow>` would create its run, or the exit code of the
|
|
330
|
+
* refusal it already printed. Reads only; shared with `task run --dry-run`. */
|
|
331
|
+
export function resolveKxmRunTarget(
|
|
332
|
+
runtime: Runtime,
|
|
333
|
+
workflow: string | undefined,
|
|
334
|
+
): { projectRoot: string; workflowId: string; configRevision: string } | number {
|
|
288
335
|
if (runtime.workspaceFlag !== undefined) {
|
|
289
336
|
print(runtime.io, runtime.json, {
|
|
290
337
|
ok: false,
|
|
@@ -297,33 +344,38 @@ export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined,
|
|
|
297
344
|
print(runtime.io, runtime.json, { ok: false, command: "run", error: "workflow_required" }, "usage: kxm run <workflow> [prompt]");
|
|
298
345
|
return 2;
|
|
299
346
|
}
|
|
347
|
+
const projectRoot = discoverKxmProjectRoot(runtime.cwd);
|
|
348
|
+
if (!projectRoot) {
|
|
349
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "project_required" }, "kxm run requires a KXM project (run kxm init first)");
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
const bundle = loadKxmProject(projectRoot, {});
|
|
353
|
+
if (!bundle.workflows.has(workflow)) {
|
|
354
|
+
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
return { projectRoot, workflowId: workflow, configRevision: bundle.configRevision };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function cmdKxmRun(runtime: Runtime, workflow: string | undefined, promptParts: string[]): Promise<number> {
|
|
300
361
|
try {
|
|
301
|
-
const
|
|
302
|
-
if (
|
|
303
|
-
|
|
304
|
-
return 1;
|
|
305
|
-
}
|
|
306
|
-
const bundle = loadKxmProject(projectRoot, {});
|
|
307
|
-
if (!bundle.workflows.has(workflow)) {
|
|
308
|
-
print(runtime.io, runtime.json, { ok: false, command: "run", error: "run_workflow_unknown", workflow }, `workflow ${workflow} does not exist in this project`);
|
|
309
|
-
return 1;
|
|
310
|
-
}
|
|
362
|
+
const target = resolveKxmRunTarget(runtime, workflow);
|
|
363
|
+
if (typeof target === "number") return target;
|
|
364
|
+
const { projectRoot } = target;
|
|
311
365
|
if (runtime.dryRun) {
|
|
312
366
|
print(runtime.io, runtime.json, {
|
|
313
367
|
ok: true,
|
|
314
368
|
command: "run",
|
|
315
369
|
dryRun: true,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
configRevision: bundle.configRevision,
|
|
319
|
-
}, `run plan: workflow ${workflow} at ${bundle.configRevision.slice(0, 19)}… (no run created)`);
|
|
370
|
+
...target,
|
|
371
|
+
}, `run plan: workflow ${target.workflowId} at ${target.configRevision.slice(0, 19)}… (no run created)`);
|
|
320
372
|
return 0;
|
|
321
373
|
}
|
|
322
374
|
const supervisor = await ensureKxmSupervisor({ env: runtime.env });
|
|
323
375
|
const prompt = promptParts.join(" ").trim();
|
|
324
376
|
const acceptance = await kxmRuntimeRequest(supervisor, "POST", "/v1/runs", {
|
|
325
377
|
projectRoot,
|
|
326
|
-
workflowId:
|
|
378
|
+
workflowId: target.workflowId,
|
|
327
379
|
prompt,
|
|
328
380
|
});
|
|
329
381
|
const run = acceptance.run as { runId: string; homeRuntimeId: string; status: string; configRevision: string };
|
|
@@ -408,7 +460,8 @@ export async function cmdKxmRunStatus(runtime: Runtime, runId: string): Promise<
|
|
|
408
460
|
print(runtime.io, runtime.json, { ok: false, command: "runs status", error: "project_required" }, "kxm runs status requires a KXM project (run kxm init first)");
|
|
409
461
|
return 1;
|
|
410
462
|
}
|
|
411
|
-
const supervisor = await (
|
|
463
|
+
const supervisor = await readSupervisor(runtime, "runs status");
|
|
464
|
+
if (typeof supervisor === "number") return supervisor;
|
|
412
465
|
const result = await (kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest)(supervisor, "GET", `/v1/runs/${encodeURIComponent(runId)}?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
413
466
|
const run = result.run as { runId: string; status: string; workflowId: string; configRevision: string; updatedAt: string };
|
|
414
467
|
const drive = result.drive as KxmStatusDrive | undefined;
|
|
@@ -570,7 +623,8 @@ export async function cmdKxmRunList(runtime: Runtime): Promise<number> {
|
|
|
570
623
|
}
|
|
571
624
|
const bundle = loadKxmProject(projectRoot, {});
|
|
572
625
|
const projectId = String(bundle.project.value.id);
|
|
573
|
-
const supervisor = await
|
|
626
|
+
const supervisor = await readSupervisor(runtime, "runs list");
|
|
627
|
+
if (typeof supervisor === "number") return supervisor;
|
|
574
628
|
const result = await kxmRuntimeRequest(supervisor, "GET", `/v1/projects/${encodeURIComponent(projectId)}/runs?projectRoot=${encodeURIComponent(projectRoot)}`);
|
|
575
629
|
const runs = (result.runs ?? []) as Array<{ runId: string; status: string; workflowId: string; createdAt: string; projectionError?: string }>;
|
|
576
630
|
print(
|
|
@@ -851,7 +905,8 @@ export async function cmdKxmRunReceipt(runtime: Runtime, runId: string, options:
|
|
|
851
905
|
print(runtime.io, runtime.json, { ok: false, command: "runs receipt", error: "project_required" }, "kxm runs receipt requires a KXM project (run kxm init first)");
|
|
852
906
|
return 1;
|
|
853
907
|
}
|
|
854
|
-
const supervisor = await (
|
|
908
|
+
const supervisor = await readSupervisor(runtime, "runs receipt");
|
|
909
|
+
if (typeof supervisor === "number") return supervisor;
|
|
855
910
|
const result = await (kxmDriveCliSeams.runtimeRequest ?? kxmRuntimeRequest)(
|
|
856
911
|
supervisor,
|
|
857
912
|
"GET",
|