@kontextmind/kxm 0.7.0 → 0.7.10
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/.kxm/roles/writer.yaml +2 -0
- package/docs/README.md +3 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +19 -2
- package/docs/browser-automation.md +116 -0
- package/docs/configuration.md +10 -1
- package/docs/getting-started.md +21 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/operations.md +24 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/docs/skills/repo-work-delivery.md +5 -0
- package/docs/skills.md +2 -0
- package/docs/troubleshooting.md +22 -1
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/cli.js +41203 -38364
- package/plugins/kxm/dist/core.js +57 -0
- package/plugins/kxm/dist/extension.js +40 -3
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +1582 -81
- package/plugins/kxm/dist/server.js +129 -4
- package/plugins/kxm/dist/vnext-runtime-supervisor.js +221 -41
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +30 -0
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/skills/kxm-hub-ops/SKILL.md +9 -0
- package/plugins/kxm/skills/kxm-project-setup/SKILL.md +9 -2
- package/plugins/kxm/src/autocomplete.ts +1 -1
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/cli/context-skills.ts +373 -0
- package/plugins/kxm/src/cli/hub.ts +614 -0
- package/plugins/kxm/src/cli/roles.ts +615 -0
- package/plugins/kxm/src/cli/system.ts +906 -0
- package/plugins/kxm/src/cli/tasks.ts +364 -0
- package/plugins/kxm/src/cli/types.ts +270 -0
- package/plugins/kxm/src/cli/vnext.ts +698 -0
- package/plugins/kxm/src/cli/workflows.ts +699 -0
- package/plugins/kxm/src/cli.ts +238 -3791
- package/plugins/kxm/src/completion-install.ts +223 -0
- package/plugins/kxm/src/database.ts +1 -1
- package/plugins/kxm/src/external-effects.ts +1 -1
- package/plugins/kxm/src/hub-env.ts +193 -0
- package/plugins/kxm/src/init-guide-setup.ts +547 -0
- package/plugins/kxm/src/local-snapshot.ts +1 -1
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/model-inventory.ts +8 -8
- package/plugins/kxm/src/modes.ts +348 -0
- package/plugins/kxm/src/protocol.ts +111 -0
- package/plugins/kxm/src/role.ts +335 -0
- package/plugins/kxm/src/runtime.ts +4 -0
- package/plugins/kxm/src/safety-integrity.ts +76 -0
- package/plugins/kxm/src/sqlite.ts +76 -0
- package/plugins/kxm/src/ssh-remote.ts +560 -0
- package/plugins/kxm/src/store.ts +1 -1
- package/plugins/kxm/src/subagent-control.ts +312 -0
- package/plugins/kxm/src/vnext-bindings.ts +1 -1
- package/plugins/kxm/src/vnext-config.ts +38 -1
- package/plugins/kxm/src/vnext-engine-command.ts +2 -0
- package/plugins/kxm/src/vnext-engine.ts +16 -0
- package/plugins/kxm/src/vnext-harness.ts +92 -22
- package/plugins/kxm/src/vnext-oneshot-evidence.ts +39 -7
- package/plugins/kxm/src/vnext-oneshot-process.ts +46 -8
- package/plugins/kxm/src/vnext-oneshot-producer.ts +22 -1
- package/plugins/kxm/src/vnext-pi-producer.ts +11 -7
- package/plugins/kxm/src/vnext-runtime-store.ts +1 -1
- package/plugins/kxm/src/vnext-runtime-supervisor.ts +5 -3
- package/plugins/kxm/src/workflow-tui.ts +1 -1
- package/plugins/kxm/src/workflow.ts +144 -0
- package/schemas/vnext/modes.schema.json +56 -0
- package/scripts/kxm-bump-version.mjs +146 -0
- package/scripts/kxm-hub.mjs +145 -3
- package/scripts/kxm-publish-npm.mjs +3 -1
- package/scripts/kxm-release-github.mjs +3 -1
- package/scripts/kxm.mjs +0 -0
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { probeHarnessesAsync } from "../vnext-harness.ts";
|
|
5
|
+
import { suggestWorkflowAndRoles } from "../suggest.ts";
|
|
6
|
+
import {
|
|
7
|
+
createGoal,
|
|
8
|
+
createTask,
|
|
9
|
+
listGoals,
|
|
10
|
+
listTasks,
|
|
11
|
+
getTask,
|
|
12
|
+
updateTaskStatus,
|
|
13
|
+
syncTaskWithTracker,
|
|
14
|
+
type TaskStatus,
|
|
15
|
+
type TrackerType,
|
|
16
|
+
} from "../task-manager.ts";
|
|
17
|
+
import { discoverVnextProjectRoot } from "../vnext-config.ts";
|
|
18
|
+
import { compileVnextWorkflow } from "../vnext-engine-compile.ts";
|
|
19
|
+
import {
|
|
20
|
+
createStudioServer,
|
|
21
|
+
DEFAULT_STUDIO_PORT,
|
|
22
|
+
generateStudioLayout,
|
|
23
|
+
} from "../studio-layout.ts";
|
|
24
|
+
import { readSessionTokenFromDisk } from "../commands.ts";
|
|
25
|
+
import { print, type Runtime } from "./types.ts";
|
|
26
|
+
import { cmdVnextRun } from "./vnext.ts";
|
|
27
|
+
|
|
28
|
+
export async function cmdSuggest(runtime: Runtime, promptParts: string[]): Promise<number> {
|
|
29
|
+
try {
|
|
30
|
+
const prompt = promptParts.join(" ").trim();
|
|
31
|
+
if (!prompt) {
|
|
32
|
+
runtime.io.stderr("prompt must be non-empty\n");
|
|
33
|
+
return 2;
|
|
34
|
+
}
|
|
35
|
+
const inventory = await probeHarnessesAsync({ env: runtime.env });
|
|
36
|
+
const availableHarnesses = inventory.harnesses.map((h) => ({
|
|
37
|
+
harness: h.id,
|
|
38
|
+
auth: h.authenticated === true ? "authenticated" : "unauthenticated",
|
|
39
|
+
}));
|
|
40
|
+
const suggestion = suggestWorkflowAndRoles(prompt, { availableHarnesses });
|
|
41
|
+
|
|
42
|
+
const text = [
|
|
43
|
+
`Suggested Workflow: ${suggestion.workflowId} (${suggestion.area})`,
|
|
44
|
+
`Confidence: ${(suggestion.confidence * 100).toFixed(0)}%`,
|
|
45
|
+
`Reasons: ${suggestion.reasons.join("; ")}`,
|
|
46
|
+
`Suggested Skills: ${suggestion.suggestedSkills.join(", ") || "none"}`,
|
|
47
|
+
`Roles:`,
|
|
48
|
+
` Planner: ${suggestion.roles.planner.harness} (${suggestion.roles.planner.model})`,
|
|
49
|
+
` Writer: ${suggestion.roles.writer.harness} (${suggestion.roles.writer.model})`,
|
|
50
|
+
` Critics: ${suggestion.roles.critics.map((c) => `${c.harness}:${c.model}`).join(", ")}`,
|
|
51
|
+
` Verifier: ${suggestion.roles.verifier.command}`,
|
|
52
|
+
``,
|
|
53
|
+
`Execute with:`,
|
|
54
|
+
` ${suggestion.suggestedCommand}`,
|
|
55
|
+
].join("\n");
|
|
56
|
+
|
|
57
|
+
print(runtime.io, runtime.json, { ok: true, command: "suggest", prompt, ...suggestion }, text);
|
|
58
|
+
return 0;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
runtime.io.stderr(`suggest failed: ${message}\n`);
|
|
62
|
+
return 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function cmdGoalCreate(
|
|
67
|
+
runtime: Runtime,
|
|
68
|
+
title: string,
|
|
69
|
+
options: { area?: string | undefined; metric?: string[] | undefined; targetDate?: string | undefined },
|
|
70
|
+
): Promise<number> {
|
|
71
|
+
try {
|
|
72
|
+
const goal = createGoal(runtime.cwd, {
|
|
73
|
+
title,
|
|
74
|
+
area: options.area,
|
|
75
|
+
successMetrics: options.metric,
|
|
76
|
+
targetDate: options.targetDate,
|
|
77
|
+
});
|
|
78
|
+
print(
|
|
79
|
+
runtime.io,
|
|
80
|
+
runtime.json,
|
|
81
|
+
{ ok: true, command: "goal create", goal },
|
|
82
|
+
`Created goal ${goal.id}: ${goal.title}`,
|
|
83
|
+
);
|
|
84
|
+
return 0;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
runtime.io.stderr(`goal create failed: ${message}\n`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function cmdGoalList(runtime: Runtime): Promise<number> {
|
|
93
|
+
try {
|
|
94
|
+
const goals = listGoals(runtime.cwd);
|
|
95
|
+
const text = goals.length === 0
|
|
96
|
+
? "No goals recorded in .kxm/goals/"
|
|
97
|
+
: goals.map((g) => `[${g.status}] ${g.id}: ${g.title} (${g.area})`).join("\n");
|
|
98
|
+
print(runtime.io, runtime.json, { ok: true, command: "goal list", count: goals.length, goals }, text);
|
|
99
|
+
return 0;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
+
runtime.io.stderr(`goal list failed: ${message}\n`);
|
|
103
|
+
return 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function cmdTaskCreate(
|
|
108
|
+
runtime: Runtime,
|
|
109
|
+
title: string,
|
|
110
|
+
options: { goal?: string | undefined; objective?: string | undefined; workflow?: string | undefined; tracker?: string | undefined; issue?: string | undefined },
|
|
111
|
+
): Promise<number> {
|
|
112
|
+
try {
|
|
113
|
+
const task = createTask(runtime.cwd, {
|
|
114
|
+
title,
|
|
115
|
+
goalId: options.goal,
|
|
116
|
+
objective: options.objective ?? title,
|
|
117
|
+
assignedWorkflow: options.workflow,
|
|
118
|
+
trackerSync: options.tracker && options.issue
|
|
119
|
+
? { tracker: options.tracker as TrackerType, issueKey: options.issue }
|
|
120
|
+
: undefined,
|
|
121
|
+
});
|
|
122
|
+
print(
|
|
123
|
+
runtime.io,
|
|
124
|
+
runtime.json,
|
|
125
|
+
{ ok: true, command: "task create", task },
|
|
126
|
+
`Created task ${task.id}: ${task.title} [${task.status}]`,
|
|
127
|
+
);
|
|
128
|
+
return 0;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
131
|
+
runtime.io.stderr(`task create failed: ${message}\n`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function cmdTaskList(
|
|
137
|
+
runtime: Runtime,
|
|
138
|
+
options: { goal?: string | undefined; status?: string | undefined },
|
|
139
|
+
): Promise<number> {
|
|
140
|
+
try {
|
|
141
|
+
const tasks = listTasks(runtime.cwd, {
|
|
142
|
+
goalId: options.goal,
|
|
143
|
+
status: options.status as TaskStatus | undefined,
|
|
144
|
+
});
|
|
145
|
+
const text = tasks.length === 0
|
|
146
|
+
? "No tasks recorded in .kxm/tasks/"
|
|
147
|
+
: tasks.map((t) => `[${t.status}] ${t.id}: ${t.title}${t.assignedWorkflow ? ` -> ${t.assignedWorkflow}` : ""}`).join("\n");
|
|
148
|
+
print(runtime.io, runtime.json, { ok: true, command: "task list", count: tasks.length, tasks }, text);
|
|
149
|
+
return 0;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
152
|
+
runtime.io.stderr(`task list failed: ${message}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function cmdTaskGet(runtime: Runtime, taskId: string): Promise<number> {
|
|
158
|
+
try {
|
|
159
|
+
const task = getTask(runtime.cwd, taskId);
|
|
160
|
+
if (!task) {
|
|
161
|
+
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
162
|
+
return 1;
|
|
163
|
+
}
|
|
164
|
+
const text = [
|
|
165
|
+
`Task: ${task.id}`,
|
|
166
|
+
`Title: ${task.title}`,
|
|
167
|
+
`Status: ${task.status}`,
|
|
168
|
+
`Objective: ${task.objective}`,
|
|
169
|
+
task.assignedWorkflow ? `Workflow: ${task.assignedWorkflow}` : "",
|
|
170
|
+
task.workflowRunId ? `Active Run: ${task.workflowRunId}` : "",
|
|
171
|
+
task.trackerSync ? `Tracker: ${task.trackerSync.tracker} (#${task.trackerSync.issueKey}) [${task.trackerSync.syncStatus}]` : "",
|
|
172
|
+
].filter(Boolean).join("\n");
|
|
173
|
+
print(runtime.io, runtime.json, { ok: true, command: "task get", task }, text);
|
|
174
|
+
return 0;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
runtime.io.stderr(`task get failed: ${message}\n`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function cmdTaskRun(runtime: Runtime, taskId: string): Promise<number> {
|
|
183
|
+
try {
|
|
184
|
+
const task = getTask(runtime.cwd, taskId);
|
|
185
|
+
if (!task) {
|
|
186
|
+
runtime.io.stderr(`Task ${taskId} not found\n`);
|
|
187
|
+
return 1;
|
|
188
|
+
}
|
|
189
|
+
const workflow = task.assignedWorkflow ?? "default";
|
|
190
|
+
const exitCode = await cmdVnextRun(runtime, workflow, [task.objective]);
|
|
191
|
+
if (exitCode === 0) {
|
|
192
|
+
updateTaskStatus(runtime.cwd, taskId, "in_progress");
|
|
193
|
+
}
|
|
194
|
+
return exitCode;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
runtime.io.stderr(`task run failed: ${message}\n`);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function cmdTaskSync(runtime: Runtime, taskId: string): Promise<number> {
|
|
203
|
+
try {
|
|
204
|
+
const synced = syncTaskWithTracker(runtime.cwd, taskId);
|
|
205
|
+
print(
|
|
206
|
+
runtime.io,
|
|
207
|
+
runtime.json,
|
|
208
|
+
{ ok: true, command: "task sync", task: synced },
|
|
209
|
+
`Synced task ${taskId} with ${synced.trackerSync?.tracker} #${synced.trackerSync?.issueKey}`,
|
|
210
|
+
);
|
|
211
|
+
return 0;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
runtime.io.stderr(`task sync failed: ${message}\n`);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function cmdStudioLayout(runtime: Runtime, workflowPath?: string | undefined): Promise<number> {
|
|
220
|
+
try {
|
|
221
|
+
const projectRoot = discoverVnextProjectRoot(runtime.cwd) ?? runtime.cwd;
|
|
222
|
+
let filePath = workflowPath;
|
|
223
|
+
if (!filePath) {
|
|
224
|
+
filePath = resolve(projectRoot, ".kxm", "workflows", "default.yaml");
|
|
225
|
+
}
|
|
226
|
+
let yamlContent: string;
|
|
227
|
+
let workflowId = "default";
|
|
228
|
+
if (existsSync(filePath)) {
|
|
229
|
+
yamlContent = readFileSync(filePath, "utf8");
|
|
230
|
+
} else {
|
|
231
|
+
yamlContent = `schema: kxm.workflow.v1
|
|
232
|
+
description: Feature implementation workflow
|
|
233
|
+
coordinator: coordinator
|
|
234
|
+
limits:
|
|
235
|
+
maxTransitions: 12
|
|
236
|
+
steps:
|
|
237
|
+
- id: plan
|
|
238
|
+
kind: agent
|
|
239
|
+
agent: planner
|
|
240
|
+
maxAttempts: 2
|
|
241
|
+
on:
|
|
242
|
+
passed: implement
|
|
243
|
+
failed:
|
|
244
|
+
target: $terminal
|
|
245
|
+
terminalStatus: failed
|
|
246
|
+
- id: implement
|
|
247
|
+
kind: agent
|
|
248
|
+
agent: writer
|
|
249
|
+
maxAttempts: 3
|
|
250
|
+
on:
|
|
251
|
+
passed: verify
|
|
252
|
+
failed:
|
|
253
|
+
target: $terminal
|
|
254
|
+
terminalStatus: failed
|
|
255
|
+
- id: verify
|
|
256
|
+
kind: gate
|
|
257
|
+
gate: verify-gate
|
|
258
|
+
expect: pass
|
|
259
|
+
maxAttempts: 2
|
|
260
|
+
on:
|
|
261
|
+
passed:
|
|
262
|
+
target: $terminal
|
|
263
|
+
terminalStatus: completed
|
|
264
|
+
failed:
|
|
265
|
+
target: implement
|
|
266
|
+
maxTransitions: 2
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
const parsedYaml = parseYaml(yamlContent) as any;
|
|
270
|
+
const plan = compileVnextWorkflow({ id: workflowId, value: parsedYaml });
|
|
271
|
+
const layout = generateStudioLayout(plan);
|
|
272
|
+
print(runtime.io, runtime.json, { ok: true, command: "studio layout", layout }, JSON.stringify(layout, null, 2));
|
|
273
|
+
return 0;
|
|
274
|
+
} catch (error) {
|
|
275
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
276
|
+
runtime.io.stderr(`studio layout failed: ${message}\n`);
|
|
277
|
+
return 1;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function cmdStudioServe(
|
|
282
|
+
runtime: Runtime,
|
|
283
|
+
options: { port?: string | undefined; host?: string | undefined; token?: string | undefined },
|
|
284
|
+
): Promise<number> {
|
|
285
|
+
const port = options.port ? parseInt(options.port, 10) : DEFAULT_STUDIO_PORT;
|
|
286
|
+
const host = options.host ?? "127.0.0.1";
|
|
287
|
+
const sessionToken = options.token
|
|
288
|
+
?? runtime.env.KXM_SESSION_TOKEN
|
|
289
|
+
?? readSessionTokenFromDisk({ userConfigDir: runtime.env.KXM_USER_CONFIG_DIR })?.token;
|
|
290
|
+
|
|
291
|
+
if (runtime.dryRun) {
|
|
292
|
+
print(
|
|
293
|
+
runtime.io,
|
|
294
|
+
runtime.json,
|
|
295
|
+
{ ok: true, command: "studio serve", dryRun: true, port, host },
|
|
296
|
+
`would start studio server on http://${host}:${port}`,
|
|
297
|
+
);
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const serverHandle = createStudioServer({
|
|
303
|
+
port,
|
|
304
|
+
host,
|
|
305
|
+
projectRoot: runtime.cwd,
|
|
306
|
+
sessionToken,
|
|
307
|
+
planProvider: () => {
|
|
308
|
+
try {
|
|
309
|
+
const wfDir = join(runtime.cwd, ".kxm", "workflows");
|
|
310
|
+
const defaultPath = join(wfDir, "default.yaml");
|
|
311
|
+
let targetPath = existsSync(defaultPath) ? defaultPath : undefined;
|
|
312
|
+
if (!targetPath && existsSync(wfDir)) {
|
|
313
|
+
const yml = readdirSync(wfDir).find((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
|
|
314
|
+
if (yml) targetPath = join(wfDir, yml);
|
|
315
|
+
}
|
|
316
|
+
if (targetPath && existsSync(targetPath)) {
|
|
317
|
+
const raw = readFileSync(targetPath, "utf8");
|
|
318
|
+
const parsed = parseYaml(raw) as any;
|
|
319
|
+
const wfId = basename(targetPath).replace(/\.(yaml|yml)$/, "");
|
|
320
|
+
return compileVnextWorkflow({ id: wfId, value: parsed });
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
// fallback
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
const actualPort = await serverHandle.listen();
|
|
329
|
+
const info = {
|
|
330
|
+
ok: true,
|
|
331
|
+
command: "studio serve",
|
|
332
|
+
port: actualPort,
|
|
333
|
+
host,
|
|
334
|
+
url: `http://${host}:${actualPort}`,
|
|
335
|
+
};
|
|
336
|
+
print(
|
|
337
|
+
runtime.io,
|
|
338
|
+
runtime.json,
|
|
339
|
+
info,
|
|
340
|
+
`KXM Web Studio listening on http://${host}:${actualPort} (Decision Q8 & D14)\nPress Ctrl+C to stop.\n`,
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (runtime.env.KXM_STUDIO_ONCE) {
|
|
344
|
+
await serverHandle.close();
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
await new Promise<void>((resolveClose) => {
|
|
349
|
+
const shutdown = async () => {
|
|
350
|
+
process.off("SIGINT", shutdown);
|
|
351
|
+
process.off("SIGTERM", shutdown);
|
|
352
|
+
await serverHandle.close();
|
|
353
|
+
resolveClose();
|
|
354
|
+
};
|
|
355
|
+
process.once("SIGINT", shutdown);
|
|
356
|
+
process.once("SIGTERM", shutdown);
|
|
357
|
+
});
|
|
358
|
+
return 0;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
361
|
+
runtime.io.stderr(`studio serve failed: ${message}\n`);
|
|
362
|
+
return 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import { redactSecrets } from "../redact.ts";
|
|
6
|
+
import { appendTelemetry, inferImprovementTarget, makeTelemetryEvent, telemetryPath } from "../telemetry.ts";
|
|
7
|
+
import { gateWorker, workerResult, type Worker, type WorkerOutcome } from "../envelope.ts";
|
|
8
|
+
import { canonicalWorkflowEvidenceKey, type WorkflowEvidenceInput } from "../workflow.ts";
|
|
9
|
+
import {
|
|
10
|
+
HubBindingError,
|
|
11
|
+
hubBindingFile,
|
|
12
|
+
readHubBinding,
|
|
13
|
+
} from "../hub-binding.ts";
|
|
14
|
+
import type { InstallProbe } from "../kxm-install-kind.ts";
|
|
15
|
+
|
|
16
|
+
export interface CliSpawnResult {
|
|
17
|
+
status: number | null;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
error?: Error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CliIo {
|
|
24
|
+
stdout: (text: string) => void;
|
|
25
|
+
stderr: (text: string) => void;
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
now?: () => number;
|
|
28
|
+
sleep?: (ms: number) => Promise<void>;
|
|
29
|
+
spawnHub?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
|
|
30
|
+
spawnWorker?: (extraEnv: NodeJS.ProcessEnv) => number | Promise<number>;
|
|
31
|
+
installProbe?: Partial<InstallProbe>;
|
|
32
|
+
spawnSync?: (command: string, args: readonly string[]) => CliSpawnResult;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CliContext {
|
|
36
|
+
env: NodeJS.ProcessEnv;
|
|
37
|
+
io: CliIo;
|
|
38
|
+
cwd: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface GlobalOpts {
|
|
42
|
+
json?: boolean;
|
|
43
|
+
dryRun?: boolean;
|
|
44
|
+
workspace?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface WorkspaceDirs {
|
|
48
|
+
workdir: string;
|
|
49
|
+
workspace: string;
|
|
50
|
+
config: string;
|
|
51
|
+
logs: string;
|
|
52
|
+
assets: string;
|
|
53
|
+
state: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface Runtime extends CliContext, Required<Pick<GlobalOpts, "json" | "dryRun">> {
|
|
57
|
+
workspaceFlag?: string;
|
|
58
|
+
dirs: WorkspaceDirs;
|
|
59
|
+
serverUrl: string;
|
|
60
|
+
boundHubUrl?: string;
|
|
61
|
+
fetchImpl: typeof fetch;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const repoRoot = resolve(fileURLToPath(new URL("../../../../", import.meta.url)));
|
|
65
|
+
|
|
66
|
+
export function spawnScript(scriptName: string, extraEnv: NodeJS.ProcessEnv = {}): Promise<number> {
|
|
67
|
+
return new Promise((resolveExit) => {
|
|
68
|
+
const child = spawn(process.execPath, [join(repoRoot, "scripts", scriptName)], {
|
|
69
|
+
stdio: "inherit",
|
|
70
|
+
env: { ...process.env, ...extraEnv },
|
|
71
|
+
});
|
|
72
|
+
child.once("error", () => resolveExit(1));
|
|
73
|
+
child.once("exit", (code) => resolveExit(code ?? 1));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseEvidencePairs(values: string[]): WorkflowEvidenceInput {
|
|
78
|
+
const evidence = new Map<string, string>();
|
|
79
|
+
for (const value of values) {
|
|
80
|
+
const separator = value.indexOf("=");
|
|
81
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
82
|
+
throw new Error("evidence must use <required-key>=<evidence> syntax");
|
|
83
|
+
}
|
|
84
|
+
const requirement = canonicalWorkflowEvidenceKey(value.slice(0, separator));
|
|
85
|
+
const proof = value.slice(separator + 1).trim();
|
|
86
|
+
if (!requirement || !proof) throw new Error("evidence must use <required-key>=<evidence> syntax");
|
|
87
|
+
if (evidence.has(requirement)) throw new Error(`duplicate normalized evidence key: ${requirement}`);
|
|
88
|
+
evidence.set(requirement, proof);
|
|
89
|
+
}
|
|
90
|
+
return Object.fromEntries(evidence);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const CLI_RESULT_SCHEMA = "kxm.cli-result.v1";
|
|
94
|
+
|
|
95
|
+
/** Every CLI payload carries a schema tag; payloads that already declare one
|
|
96
|
+
* (worker envelopes) keep theirs. `ok:false` goes to stderr in both modes so
|
|
97
|
+
* agents can split results from errors without parsing. */
|
|
98
|
+
export function print(io: CliIo, jsonMode: boolean, payload: object, text: string): void {
|
|
99
|
+
const tagged = { schema: CLI_RESULT_SCHEMA, ...payload } as Record<string, unknown>;
|
|
100
|
+
const safePayload = JSON.stringify(redactCliValue(tagged));
|
|
101
|
+
const line = jsonMode ? `${safePayload}\n` : `${redactSecrets(text)}\n`;
|
|
102
|
+
if (tagged.ok === false) io.stderr(line);
|
|
103
|
+
else io.stdout(line);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function printWorker(
|
|
107
|
+
runtime: Runtime,
|
|
108
|
+
worker: Worker,
|
|
109
|
+
payload: Record<string, unknown> & { command: string; ok: boolean },
|
|
110
|
+
text: string,
|
|
111
|
+
outcome?: WorkerOutcome,
|
|
112
|
+
): void {
|
|
113
|
+
const sessionId = runtime.env.KXM_SESSION_ID?.trim();
|
|
114
|
+
const envelope = workerResult(worker, {
|
|
115
|
+
...payload,
|
|
116
|
+
summary: text,
|
|
117
|
+
...(outcome ? { outcome } : {}),
|
|
118
|
+
...(sessionId ? { sessionId } : {}),
|
|
119
|
+
});
|
|
120
|
+
if (!runtime.dryRun) {
|
|
121
|
+
try {
|
|
122
|
+
const safeEnvelope = redactCliValue(envelope) as typeof envelope;
|
|
123
|
+
appendTelemetry(telemetryPath(runtime.dirs.logs), makeTelemetryEvent({
|
|
124
|
+
envelope: safeEnvelope,
|
|
125
|
+
...(sessionId ? { sessionId } : {}),
|
|
126
|
+
host: hostMode(runtime),
|
|
127
|
+
target: inferImprovementTarget({
|
|
128
|
+
...(worker.project ? { project: worker.project } : {}),
|
|
129
|
+
env: runtime.env,
|
|
130
|
+
}),
|
|
131
|
+
}));
|
|
132
|
+
} catch {
|
|
133
|
+
// Telemetry must never fail the operator command.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
print(runtime.io, runtime.json, envelope, text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function hostMode(runtime: Runtime): "local" | "hub" {
|
|
140
|
+
try {
|
|
141
|
+
const hostname = new URL(runtime.serverUrl).hostname;
|
|
142
|
+
if (hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1") return "local";
|
|
143
|
+
} catch {
|
|
144
|
+
// Default local when the hub URL is not a URL.
|
|
145
|
+
}
|
|
146
|
+
return "hub";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function gateOf(runtime: Runtime, name: string): ReturnType<typeof gateWorker> {
|
|
150
|
+
const project = runtime.env.KXM_PROJECT?.trim();
|
|
151
|
+
return gateWorker({ name, ...(project ? { project } : {}) });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function redactCliValue(value: unknown, field = ""): unknown {
|
|
155
|
+
if (typeof value === "string") {
|
|
156
|
+
// These fields are public audit digests, not credentials. Preserve
|
|
157
|
+
// them only by exact field name and shape; every other 64-hex value keeps
|
|
158
|
+
// the conservative generic redaction behavior.
|
|
159
|
+
if (
|
|
160
|
+
(field === "requestSha256"
|
|
161
|
+
|| field === "replySha256"
|
|
162
|
+
|| field === "behavioralSha256"
|
|
163
|
+
|| field === "workflowDefinitionSha256"
|
|
164
|
+
|| field === "verifierConfigSha256"
|
|
165
|
+
|| field === "rolePromptSha256"
|
|
166
|
+
|| field === "contentSha256"
|
|
167
|
+
|| field === "configRevision"
|
|
168
|
+
|| field === "baseSha256"
|
|
169
|
+
|| field === "localSha256"
|
|
170
|
+
|| field === "targetSha256"
|
|
171
|
+
|| field === "sourceTemplateRevision"
|
|
172
|
+
|| field === "targetTemplateRevision"
|
|
173
|
+
|| field === "sourceDigest"
|
|
174
|
+
|| field === "decisionDigest"
|
|
175
|
+
|| field === "receiptSha256"
|
|
176
|
+
|| field === "sha256"
|
|
177
|
+
|| field === "valueSha256"
|
|
178
|
+
|| field === "baseRevision"
|
|
179
|
+
|| field === "candidateRevision"
|
|
180
|
+
|| field === "baseValueSha256"
|
|
181
|
+
|| field === "candidateValueSha256")
|
|
182
|
+
&& /^(?:sha256:)?[a-f0-9]{64}$/.test(value)
|
|
183
|
+
) return value;
|
|
184
|
+
return redactSecrets(value);
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) return value.map((candidate) => redactCliValue(candidate));
|
|
187
|
+
if (value && typeof value === "object") {
|
|
188
|
+
return Object.fromEntries(
|
|
189
|
+
Object.entries(value as Record<string, unknown>)
|
|
190
|
+
.map(([key, candidate]) => [key, redactCliValue(candidate, key)]),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function workspaceDirs(cwd: string, workspaceFlag: string | undefined, env: NodeJS.ProcessEnv): WorkspaceDirs {
|
|
197
|
+
const workdir = resolve(env.KXM_WORKDIR?.trim() || cwd);
|
|
198
|
+
const workspace = resolve(workdir, workspaceFlag || env.KXM_WORKSPACE_DIR?.trim() || ".kxm");
|
|
199
|
+
const derive = workspaceFlag !== undefined;
|
|
200
|
+
return {
|
|
201
|
+
workdir,
|
|
202
|
+
workspace,
|
|
203
|
+
config: derive ? join(workspace, "config") : resolve(workdir, env.KXM_CONFIG_DIR?.trim() || join(workspace, "config")),
|
|
204
|
+
logs: derive ? join(workspace, "logs") : resolve(workdir, env.KXM_LOGS_DIR?.trim() || join(workspace, "logs")),
|
|
205
|
+
assets: derive ? join(workspace, "assets") : resolve(workdir, env.KXM_ASSETS_DIR?.trim() || join(workspace, "assets")),
|
|
206
|
+
state: derive ? join(workspace, "state") : resolve(workdir, env.KXM_STATE_DIR?.trim() || join(workspace, "state")),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function maskEnvName(name: string): boolean {
|
|
211
|
+
return /TOKEN|SECRET|KEY|PASSWORD/i.test(name);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function redactConfiguredValues(text: string, env: NodeJS.ProcessEnv): string {
|
|
215
|
+
let safe = text;
|
|
216
|
+
for (const [name, value] of Object.entries(env)) {
|
|
217
|
+
if (!maskEnvName(name) || !value || value.length < 4) continue;
|
|
218
|
+
safe = safe.replaceAll(value, "[redacted]");
|
|
219
|
+
}
|
|
220
|
+
const trailingNewline = safe.endsWith("\n") ? "\n" : "";
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(safe) as unknown;
|
|
223
|
+
return `${JSON.stringify(redactCliValue(parsed))}${trailingNewline}`;
|
|
224
|
+
} catch {
|
|
225
|
+
// Human-readable output and diagnostics keep conservative generic
|
|
226
|
+
// redaction, including opaque 64-hex values.
|
|
227
|
+
}
|
|
228
|
+
return redactSecrets(safe);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function processExists(pid: number): boolean {
|
|
232
|
+
try { process.kill(pid, 0); return true; } catch (error) { return (error as NodeJS.ErrnoException).code === "EPERM"; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function runtimeFrom(ctx: CliContext, command: Command): Runtime {
|
|
236
|
+
const opts = command.optsWithGlobals() as GlobalOpts;
|
|
237
|
+
const envServerUrl = ctx.env.KXM_SERVER_URL?.trim();
|
|
238
|
+
let boundHubUrl: string | undefined;
|
|
239
|
+
try {
|
|
240
|
+
boundHubUrl = readHubBinding(ctx.env)?.url;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error instanceof HubBindingError) {
|
|
243
|
+
ctx.io.stderr(`kxm: ignoring malformed hub binding at ${hubBindingFile(ctx.env)}; run kxm hub bind <url> again\n`);
|
|
244
|
+
} else {
|
|
245
|
+
throw error;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...ctx,
|
|
250
|
+
json: Boolean(opts.json),
|
|
251
|
+
dryRun: Boolean(opts.dryRun),
|
|
252
|
+
...(opts.workspace === undefined ? {} : { workspaceFlag: opts.workspace }),
|
|
253
|
+
dirs: workspaceDirs(ctx.cwd, opts.workspace, ctx.env),
|
|
254
|
+
serverUrl: envServerUrl || boundHubUrl || "http://127.0.0.1:7331",
|
|
255
|
+
...(boundHubUrl ? { boundHubUrl } : {}),
|
|
256
|
+
fetchImpl: ctx.io.fetchImpl ?? fetch,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function workspaceEnv(runtime: Runtime): NodeJS.ProcessEnv {
|
|
261
|
+
return {
|
|
262
|
+
KXM_WORKDIR: runtime.dirs.workdir,
|
|
263
|
+
KXM_WORKSPACE_DIR: runtime.dirs.workspace,
|
|
264
|
+
KXM_CONFIG_DIR: runtime.dirs.config,
|
|
265
|
+
KXM_LOGS_DIR: runtime.dirs.logs,
|
|
266
|
+
KXM_ASSETS_DIR: runtime.dirs.assets,
|
|
267
|
+
KXM_STATE_DIR: runtime.dirs.state,
|
|
268
|
+
KXM_SERVER_URL: runtime.serverUrl,
|
|
269
|
+
};
|
|
270
|
+
}
|