kimi-plugin-cc 2.3.1 → 2.4.0
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 +2 -2
- package/.claude-plugin/plugin.json +5 -3
- package/.mcp.json +13 -0
- package/CHANGELOG.md +10 -0
- package/README.md +57 -5
- package/dist/cli-client.js +11 -1
- package/dist/commands/ask.js +4 -2
- package/dist/commands/cancel.js +43 -4
- package/dist/commands/pursue.js +3 -2
- package/dist/commands/rescue.js +3 -2
- package/dist/commands/review-gate.js +2 -4
- package/dist/commands/review.js +4 -2
- package/dist/commands/swarm.js +4 -3
- package/dist/companion.js +13 -40
- package/dist/help.js +2 -0
- package/dist/job-id.js +12 -0
- package/dist/job-store.js +59 -0
- package/dist/jobs.js +9 -1
- package/dist/mcp/binding.js +58 -0
- package/dist/mcp/job-control.js +345 -0
- package/dist/mcp/launcher.js +257 -0
- package/dist/mcp/operations.js +301 -0
- package/dist/mcp/protocol.js +275 -0
- package/dist/mcp/server.js +8 -0
- package/dist/mcp/stdio.js +78 -0
- package/dist/mcp/supervised-child.js +15 -0
- package/dist/mcp/tool-result.js +7 -0
- package/dist/mcp/workspace.js +34 -0
- package/dist/model-command.js +32 -0
- package/dist/version.js +1 -1
- package/docs/migration.md +2 -2
- package/docs/registry-releases.md +6 -8
- package/docs/safety.md +16 -2
- package/package.json +7 -3
- package/plugins/kimi-codex/.codex-plugin/plugin.json +7 -4
- package/plugins/kimi-codex/.mcp.json +28 -0
- package/plugins/kimi-codex/dist/cli-client.js +11 -1
- package/plugins/kimi-codex/dist/commands/ask.js +4 -2
- package/plugins/kimi-codex/dist/commands/cancel.js +43 -4
- package/plugins/kimi-codex/dist/commands/pursue.js +3 -2
- package/plugins/kimi-codex/dist/commands/rescue.js +3 -2
- package/plugins/kimi-codex/dist/commands/review-gate.js +2 -4
- package/plugins/kimi-codex/dist/commands/review.js +4 -2
- package/plugins/kimi-codex/dist/commands/swarm.js +4 -3
- package/plugins/kimi-codex/dist/companion.js +13 -40
- package/plugins/kimi-codex/dist/help.js +2 -0
- package/plugins/kimi-codex/dist/job-id.js +12 -0
- package/plugins/kimi-codex/dist/job-store.js +59 -0
- package/plugins/kimi-codex/dist/jobs.js +9 -1
- package/plugins/kimi-codex/dist/mcp/binding.js +58 -0
- package/plugins/kimi-codex/dist/mcp/job-control.js +345 -0
- package/plugins/kimi-codex/dist/mcp/launcher.js +257 -0
- package/plugins/kimi-codex/dist/mcp/operations.js +301 -0
- package/plugins/kimi-codex/dist/mcp/protocol.js +275 -0
- package/plugins/kimi-codex/dist/mcp/server.js +8 -0
- package/plugins/kimi-codex/dist/mcp/stdio.js +78 -0
- package/plugins/kimi-codex/dist/mcp/supervised-child.js +15 -0
- package/plugins/kimi-codex/dist/mcp/tool-result.js +7 -0
- package/plugins/kimi-codex/dist/mcp/workspace.js +34 -0
- package/plugins/kimi-codex/dist/model-command.js +32 -0
- package/plugins/kimi-codex/dist/version.js +1 -1
|
@@ -305,7 +305,7 @@ export async function runCliPrompt(opts) {
|
|
|
305
305
|
args,
|
|
306
306
|
command_label: opts.commandLabel ?? null,
|
|
307
307
|
});
|
|
308
|
-
return await new Promise((resolve, reject) => {
|
|
308
|
+
return await new Promise(async (resolve, reject) => {
|
|
309
309
|
let aborted = false;
|
|
310
310
|
let cancellationTeardown;
|
|
311
311
|
let settlementStarted = false;
|
|
@@ -536,6 +536,16 @@ export async function runCliPrompt(opts) {
|
|
|
536
536
|
if (opts.signal?.aborted === true) {
|
|
537
537
|
onAbort();
|
|
538
538
|
}
|
|
539
|
+
if (child.pid !== undefined && opts.onSpawn) {
|
|
540
|
+
try {
|
|
541
|
+
await opts.onSpawn(child.pid);
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
onAbort();
|
|
545
|
+
settle("reject", new RuntimeError("CLI_SPAWN_CALLBACK_FAILED", `Failed to record the spawned kimi process: ${formatError(error)}`, "cli-client.spawn", error instanceof Error ? { cause: error, details: errorDetails() } : { details: errorDetails() }));
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
539
549
|
earlyErrorHandled = true;
|
|
540
550
|
child.on("error", (err) => {
|
|
541
551
|
appendLogLine({ event: "process_error", message: formatError(err) });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { companionHelp } from "../help.js";
|
|
2
|
-
import {
|
|
2
|
+
import { allocateJobId } from "../job-id.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
5
|
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
@@ -55,7 +55,7 @@ export async function runAsk(argv, context) {
|
|
|
55
55
|
const store = new JobStore(paths);
|
|
56
56
|
try {
|
|
57
57
|
await sweepStaleJobs(store, paths);
|
|
58
|
-
const jobId =
|
|
58
|
+
const jobId = allocateJobId(context);
|
|
59
59
|
const sessionResolution = await resolveAskSession(store, repoIdentity.repoId, parsed.fresh, parsed.resume, parsed.resumeTarget);
|
|
60
60
|
const askPrompt = buildAskPrompt(parsed.prompt, sessionResolution.reusedSession);
|
|
61
61
|
const executionPlan = await prepareKimiExecutionPlan({
|
|
@@ -152,6 +152,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
|
|
|
152
152
|
}
|
|
153
153
|
handlers = createCliCancellationHandlers();
|
|
154
154
|
const executionPlan = executionPlanFromPersisted(job);
|
|
155
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
155
156
|
// Re-validate resume lineage from the store before spawning: a detached
|
|
156
157
|
// worker (or a forged queued row) reloads the persisted plan and would
|
|
157
158
|
// otherwise hand kimi_session_id to the spawn without the dispatch-time
|
|
@@ -174,6 +175,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
|
|
|
174
175
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
175
176
|
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
176
177
|
logPath: job.stream_log_path,
|
|
178
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
177
179
|
signal: handlers.signal,
|
|
178
180
|
}, job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
|
|
179
181
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
@@ -5,6 +5,7 @@ import { markJobCancelled, sweepStaleJobs } from "../jobs.js";
|
|
|
5
5
|
import { JobStore, withJobStore } from "../job-store.js";
|
|
6
6
|
import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
|
|
7
7
|
import { parseJobLookupArgs } from "../parsing.js";
|
|
8
|
+
import { revalidateProcessIdentities } from "../process-tree.js";
|
|
8
9
|
import { getManagedCommandConfig } from "./registry.js";
|
|
9
10
|
export async function runCancel(argv, context) {
|
|
10
11
|
const parsed = parseJobLookupArgs(argv);
|
|
@@ -31,6 +32,10 @@ export async function runCancel(argv, context) {
|
|
|
31
32
|
message: "cancel is a no-op for terminal jobs.",
|
|
32
33
|
}, null, 2)}\n`;
|
|
33
34
|
}
|
|
35
|
+
if (store.getMcpTask(job.job_id) &&
|
|
36
|
+
(context.mcpCompanionIdentity === undefined || context.mcpKimiIdentity === undefined)) {
|
|
37
|
+
throw new RuntimeError("MCP_CANCEL_REQUIRES_BINDING", `MCP job ${job.job_id} requires MCP job-control cancellation with recorded identities; use tasks/cancel or kimi_job_cancel.`, "mcp.cancel");
|
|
38
|
+
}
|
|
34
39
|
if (job.pid === null && job.kimi_pid === null) {
|
|
35
40
|
throw new RuntimeError("CANCEL_NOT_SUPPORTED", `Job ${job.job_id} does not have a recorded process id to cancel.`, "cancel.runtime");
|
|
36
41
|
}
|
|
@@ -53,6 +58,7 @@ export async function runCancel(argv, context) {
|
|
|
53
58
|
catch (error) {
|
|
54
59
|
preMarkError = error;
|
|
55
60
|
}
|
|
61
|
+
await assertMcpCompanionIdentity(job, context.mcpCompanionIdentity);
|
|
56
62
|
signalJobProcesses(job, "SIGTERM");
|
|
57
63
|
await sleep(1_000);
|
|
58
64
|
if (hasLiveRecordedProcess(job)) {
|
|
@@ -67,6 +73,8 @@ export async function runCancel(argv, context) {
|
|
|
67
73
|
message: "review_gate cancelled; SIGTERM→SIGKILL escalation completed.",
|
|
68
74
|
}, null, 2)}\n`;
|
|
69
75
|
}
|
|
76
|
+
await assertMcpCompanionIdentity(job, context.mcpCompanionIdentity);
|
|
77
|
+
await assertMcpKimiIdentity(job, context.mcpKimiIdentity);
|
|
70
78
|
signalJobProcesses(job, "SIGTERM");
|
|
71
79
|
const cancelled = await waitForCancellation(paths, job.job_id);
|
|
72
80
|
if (cancelled) {
|
|
@@ -76,7 +84,11 @@ export async function runCancel(argv, context) {
|
|
|
76
84
|
message: "Cancellation completed.",
|
|
77
85
|
}, null, 2)}\n`;
|
|
78
86
|
}
|
|
79
|
-
|
|
87
|
+
const companionMatches = await assertMcpCompanionIdentity(job, context.mcpCompanionIdentity, true);
|
|
88
|
+
const kimiMatches = await assertMcpKimiIdentity(job, context.mcpKimiIdentity, true);
|
|
89
|
+
if (companionMatches || kimiMatches) {
|
|
90
|
+
signalJobProcesses(job, "SIGKILL", { companion: companionMatches, kimi: kimiMatches });
|
|
91
|
+
}
|
|
80
92
|
await withJobStore(paths, async (forcedStore) => {
|
|
81
93
|
const current = forcedStore.getJob(job.job_id);
|
|
82
94
|
if (current?.status === "running") {
|
|
@@ -93,11 +105,38 @@ export async function runCancel(argv, context) {
|
|
|
93
105
|
store.close();
|
|
94
106
|
}
|
|
95
107
|
}
|
|
96
|
-
function
|
|
108
|
+
async function assertMcpCompanionIdentity(job, expected, allowMissing = false) {
|
|
109
|
+
return assertMcpProcessIdentity(job.pid, expected, "companion", allowMissing);
|
|
110
|
+
}
|
|
111
|
+
async function assertMcpKimiIdentity(job, expected, allowMissing = false) {
|
|
112
|
+
return assertMcpProcessIdentity(job.kimi_pid, expected, "Kimi", allowMissing);
|
|
113
|
+
}
|
|
114
|
+
async function assertMcpProcessIdentity(actualPid, expected, label, allowMissing) {
|
|
115
|
+
if (expected === undefined)
|
|
116
|
+
return true;
|
|
117
|
+
if (expected === null) {
|
|
118
|
+
if (actualPid === null)
|
|
119
|
+
return false;
|
|
120
|
+
throw new RuntimeError("CANCEL_NOT_SUPPORTED", `MCP job ${actualPid} has no verified ${label} identity; refusing PID-only cancellation.`, "mcp.cancel");
|
|
121
|
+
}
|
|
122
|
+
if (actualPid !== expected.pid) {
|
|
123
|
+
throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", `The recorded ${label} process identity no longer matches; no signal was sent.`, "mcp.cancel");
|
|
124
|
+
}
|
|
125
|
+
const validation = await revalidateProcessIdentities([expected]);
|
|
126
|
+
if (!validation.complete) {
|
|
127
|
+
throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", `The ${label} identity probe was inconclusive; no further signal was sent and the job was not changed.`, "mcp.cancel");
|
|
128
|
+
}
|
|
129
|
+
const matches = validation.identities.length > 0;
|
|
130
|
+
if (!matches && !allowMissing) {
|
|
131
|
+
throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", `The recorded ${label} process identity no longer matches; no signal was sent.`, "mcp.cancel");
|
|
132
|
+
}
|
|
133
|
+
return matches;
|
|
134
|
+
}
|
|
135
|
+
function signalJobProcesses(job, signal, include = { companion: true, kimi: true }) {
|
|
97
136
|
const pids = new Set();
|
|
98
|
-
if (job.pid !== null)
|
|
137
|
+
if (include.companion && job.pid !== null)
|
|
99
138
|
pids.add(job.pid);
|
|
100
|
-
if (job.kimi_pid !== null)
|
|
139
|
+
if (include.kimi && job.kimi_pid !== null)
|
|
101
140
|
pids.add(job.kimi_pid);
|
|
102
141
|
for (const pid of pids) {
|
|
103
142
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { allocateJobId } from "../job-id.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
4
4
|
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
@@ -122,7 +122,7 @@ export async function runPursue(argv, context) {
|
|
|
122
122
|
env: context.env,
|
|
123
123
|
});
|
|
124
124
|
const prompt = buildGoalPrompt(objective, parsed.turns);
|
|
125
|
-
const jobId =
|
|
125
|
+
const jobId = allocateJobId(context);
|
|
126
126
|
const logPath = path.join(paths.logsDir, `pursue-${jobId}.jsonl`);
|
|
127
127
|
const job = store.createJob({
|
|
128
128
|
job_id: jobId,
|
|
@@ -226,6 +226,7 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
|
|
|
226
226
|
model: job.model ?? undefined,
|
|
227
227
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
228
228
|
logPath: job.stream_log_path,
|
|
229
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
229
230
|
signal: handlers.signal,
|
|
230
231
|
}, budgetMs, "pursue.prompt");
|
|
231
232
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { companionHelp } from "../help.js";
|
|
2
|
-
import {
|
|
2
|
+
import { allocateJobId } from "../job-id.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
5
5
|
import { runCliPromptWithBudget } from "../cli-client.js";
|
|
@@ -61,7 +61,7 @@ export async function runRescue(argv, context) {
|
|
|
61
61
|
resumeSessionId: sessionResolution.kimiSessionId ?? undefined,
|
|
62
62
|
resumeSource: sessionResolution.sourceJob,
|
|
63
63
|
});
|
|
64
|
-
const jobId =
|
|
64
|
+
const jobId = allocateJobId(context);
|
|
65
65
|
const logPath = path.join(paths.logsDir, `rescue-${jobId}.jsonl`);
|
|
66
66
|
const job = store.createJob({
|
|
67
67
|
job_id: jobId,
|
|
@@ -192,6 +192,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
|
|
|
192
192
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
193
193
|
resumeSessionId: job.kimi_session_id ?? undefined,
|
|
194
194
|
logPath: job.stream_log_path,
|
|
195
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
195
196
|
signal: handlers.signal,
|
|
196
197
|
}, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
|
|
197
198
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
@@ -166,10 +166,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
|
|
|
166
166
|
try {
|
|
167
167
|
const activeStore = store;
|
|
168
168
|
// runCliPromptWithBudget ties the 8 s timeout to an AbortController
|
|
169
|
-
// that kills the kimi child on expiry
|
|
170
|
-
// Claude Code's Stop hook, so a runaway kimi after timeout would
|
|
171
|
-
// hold model tokens with no way for /kimi:cancel to reach it
|
|
172
|
-
// (the SQLite row's kimi_pid is null). See reports/17 and 18.
|
|
169
|
+
// that kills the kimi child on expiry and records its PID at spawn.
|
|
173
170
|
const result = await runCliPromptWithBudget({
|
|
174
171
|
cwd: payload.cwd,
|
|
175
172
|
env: context.env,
|
|
@@ -182,6 +179,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
|
|
|
182
179
|
// Requested via env; provider/model may ignore it. The 8s budget still applies.
|
|
183
180
|
thinkingEffort,
|
|
184
181
|
logPath,
|
|
182
|
+
onSpawn: (pid) => { activeStore.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
185
183
|
}, KIMI_REVIEW_GATE_TIMEOUT_MS, "review_gate.runtime");
|
|
186
184
|
activeStore.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
187
185
|
assertCliResultSuccess(result, "review_gate.runtime");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { allocateJobId } from "../job-id.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
@@ -38,7 +38,7 @@ export async function runReview(argv, context, commandType) {
|
|
|
38
38
|
env: context.env,
|
|
39
39
|
});
|
|
40
40
|
const store = new JobStore(paths);
|
|
41
|
-
const jobId =
|
|
41
|
+
const jobId = allocateJobId(context);
|
|
42
42
|
// Leave kimi_session_id NULL until kimi announces an actual ID; never invent
|
|
43
43
|
// one that no on-disk session matches.
|
|
44
44
|
const reviewSessionId = null;
|
|
@@ -75,6 +75,7 @@ export async function runReview(argv, context, commandType) {
|
|
|
75
75
|
const reviewConfig = getManagedCommandConfig(commandType);
|
|
76
76
|
const cancel = reviewConfig.cancellation;
|
|
77
77
|
const handlers = createCliCancellationHandlers();
|
|
78
|
+
store.updateRunningJob(job.job_id, { phase: "turn-running" });
|
|
78
79
|
try {
|
|
79
80
|
const result = await runCliPromptWithBudget({
|
|
80
81
|
cwd: context.cwd,
|
|
@@ -87,6 +88,7 @@ export async function runReview(argv, context, commandType) {
|
|
|
87
88
|
model: parsed.model,
|
|
88
89
|
thinkingEffort: parsed.thinkingEffort,
|
|
89
90
|
logPath,
|
|
91
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
90
92
|
signal: handlers.signal,
|
|
91
93
|
}, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
|
|
92
94
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { allocateJobId } from "../job-id.js";
|
|
2
2
|
import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createCliCancellationHandlers } from "../cli-cancellation.js";
|
|
@@ -180,7 +180,7 @@ async function runReadSwarm(parsed, objective, context) {
|
|
|
180
180
|
env: context.env,
|
|
181
181
|
});
|
|
182
182
|
const prompt = buildSwarmPrompt(objective, parsed.cap);
|
|
183
|
-
const jobId =
|
|
183
|
+
const jobId = allocateJobId(context);
|
|
184
184
|
const logPath = path.join(paths.logsDir, `swarm-${jobId}.jsonl`);
|
|
185
185
|
const job = store.createJob({
|
|
186
186
|
job_id: jobId,
|
|
@@ -257,7 +257,7 @@ async function runWriteSwarm(parsed, objective, context) {
|
|
|
257
257
|
context.stderr.write("[kimi-plugin-cc] /kimi:swarm --write bases its worktree on HEAD — your uncommitted changes are NOT visible to the swarm. Commit or stash them first if the swarm needs them.\n");
|
|
258
258
|
}
|
|
259
259
|
const prompt = buildSwarmWritePrompt(objective, parsed.cap);
|
|
260
|
-
const jobId =
|
|
260
|
+
const jobId = allocateJobId(context);
|
|
261
261
|
const logPath = path.join(paths.logsDir, `swarm-write-${jobId}.jsonl`);
|
|
262
262
|
// Namespace worktrees per repo so the cross-repo sweep below only ever
|
|
263
263
|
// touches THIS repo's subtree (the worktreesDir is a single global dir shared
|
|
@@ -493,6 +493,7 @@ writeMode) {
|
|
|
493
493
|
model: job.model ?? undefined,
|
|
494
494
|
thinkingEffort: job.thinking_effort ?? undefined,
|
|
495
495
|
logPath: job.stream_log_path,
|
|
496
|
+
onSpawn: (pid) => { store.updateRunningJob(job.job_id, { kimi_pid: pid }); },
|
|
496
497
|
signal: handlers.signal,
|
|
497
498
|
}, budgetMs, "swarm.prompt");
|
|
498
499
|
store.updateRunningJob(job.job_id, observedExecutionFields(result));
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { companionHelp } from "./help.js";
|
|
2
|
-
import { runAsk } from "./commands/ask.js";
|
|
3
2
|
import { runModelInventory } from "./commands/models.js";
|
|
4
3
|
import { runCancel } from "./commands/cancel.js";
|
|
5
|
-
import { notImplementedCompanionCommand } from "./commands/not-implemented.js";
|
|
6
4
|
import { runReplay } from "./commands/replay.js";
|
|
7
5
|
import { runRepairSessions } from "./commands/repair-sessions.js";
|
|
8
6
|
import { runResult } from "./commands/result.js";
|
|
9
|
-
import { runReview } from "./commands/review.js";
|
|
10
|
-
import { runRescue } from "./commands/rescue.js";
|
|
11
|
-
import { runPursue } from "./commands/pursue.js";
|
|
12
|
-
import { runSwarm } from "./commands/swarm.js";
|
|
13
7
|
import { renderSetupResult, runSetup } from "./commands/setup.js";
|
|
14
8
|
import { runStatus } from "./commands/status.js";
|
|
15
9
|
import { runWorker } from "./commands/worker.js";
|
|
16
10
|
import { formatError, RuntimeError } from "./errors.js";
|
|
17
11
|
import { resolveKimiHome } from "./kimi-home.js";
|
|
12
|
+
import { runModelCommand } from "./model-command.js";
|
|
13
|
+
import { runMcpChild } from "./mcp/supervised-child.js";
|
|
14
|
+
import { runMcpServer } from "./mcp/server.js";
|
|
18
15
|
async function main(argv) {
|
|
19
16
|
const help = companionHelp(argv);
|
|
20
17
|
if (help !== undefined) {
|
|
@@ -27,11 +24,16 @@ async function main(argv) {
|
|
|
27
24
|
cwd,
|
|
28
25
|
env: {
|
|
29
26
|
...process.env,
|
|
30
|
-
KIMI_CODE_HOME: resolveKimiHome(process.env, cwd),
|
|
27
|
+
KIMI_CODE_HOME: command === "mcp" ? process.env.KIMI_CODE_HOME : resolveKimiHome(process.env, cwd),
|
|
31
28
|
},
|
|
32
29
|
stdout: process.stdout,
|
|
33
30
|
stderr: process.stderr,
|
|
34
31
|
};
|
|
32
|
+
const modelResult = await runModelCommand(argv, context);
|
|
33
|
+
if (modelResult !== undefined) {
|
|
34
|
+
context.stdout.write(modelResult);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
35
37
|
switch (command) {
|
|
36
38
|
case "setup": {
|
|
37
39
|
if (rest.includes("--models")) {
|
|
@@ -57,41 +59,12 @@ async function main(argv) {
|
|
|
57
59
|
}
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
|
-
case "
|
|
61
|
-
|
|
62
|
-
context.stdout.write(`${result}\n`);
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
case "task": {
|
|
66
|
-
const [taskType, ...taskArgs] = rest;
|
|
67
|
-
if (taskType === "rescue") {
|
|
68
|
-
const result = await runRescue(taskArgs, context);
|
|
69
|
-
context.stdout.write(result);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (taskType === "challenge") {
|
|
73
|
-
const result = await runReview(taskArgs, context, "challenge");
|
|
74
|
-
context.stdout.write(`${result}\n`);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
if (taskType === "pursue") {
|
|
78
|
-
const result = await runPursue(taskArgs, context);
|
|
79
|
-
context.stdout.write(result);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (taskType === "swarm") {
|
|
83
|
-
const result = await runSwarm(taskArgs, context);
|
|
84
|
-
context.stdout.write(result);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
notImplementedCompanionCommand(taskType ? `task ${taskType}` : "task");
|
|
62
|
+
case "mcp":
|
|
63
|
+
await runMcpServer(context);
|
|
88
64
|
return;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const result = await runAsk(rest, context);
|
|
92
|
-
context.stdout.write(`${result}\n`);
|
|
65
|
+
case "mcp-child":
|
|
66
|
+
context.stdout.write(await runMcpChild(rest, context));
|
|
93
67
|
return;
|
|
94
|
-
}
|
|
95
68
|
case "status":
|
|
96
69
|
context.stdout.write(await runStatus(rest, context));
|
|
97
70
|
return;
|
|
@@ -35,6 +35,8 @@ export function companionHelp(argv) {
|
|
|
35
35
|
case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
|
|
36
36
|
case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
|
|
37
37
|
case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
|
|
38
|
+
case "mcp": return "Usage: companion.sh mcp. Run the Kimi MCP stdio server; normally launched by Claude Code or Codex plugin configuration.";
|
|
39
|
+
case "mcp-child": return "Usage: companion.sh mcp-child <job-uuid> <model-operation> [arguments]. Internal supervised MCP child; launched by the MCP server, not manually.";
|
|
38
40
|
default: return undefined;
|
|
39
41
|
}
|
|
40
42
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { RuntimeError } from "./errors.js";
|
|
3
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
|
+
export function allocateJobId(context) {
|
|
5
|
+
const fixed = context.mcpJobId;
|
|
6
|
+
if (fixed === undefined)
|
|
7
|
+
return randomUUID();
|
|
8
|
+
if (!UUID_PATTERN.test(fixed)) {
|
|
9
|
+
throw new RuntimeError("MCP_INVALID_JOB_ID", "The supervised job ID must be a valid UUID.", "mcp.child");
|
|
10
|
+
}
|
|
11
|
+
return fixed;
|
|
12
|
+
}
|
|
@@ -99,6 +99,25 @@ export class JobStore {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
this.db.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS mcp_tasks (
|
|
103
|
+
task_id TEXT PRIMARY KEY,
|
|
104
|
+
operation_kind TEXT NOT NULL,
|
|
105
|
+
repo_id TEXT NOT NULL,
|
|
106
|
+
workspace_cwd TEXT NOT NULL,
|
|
107
|
+
companion_pid INTEGER,
|
|
108
|
+
companion_identity TEXT,
|
|
109
|
+
kimi_identity TEXT,
|
|
110
|
+
stdout_path TEXT NOT NULL,
|
|
111
|
+
stderr_path TEXT NOT NULL,
|
|
112
|
+
launch_state TEXT NOT NULL,
|
|
113
|
+
launch_error TEXT,
|
|
114
|
+
created_at TEXT NOT NULL,
|
|
115
|
+
attached_at TEXT,
|
|
116
|
+
updated_at TEXT NOT NULL
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
CREATE INDEX IF NOT EXISTS mcp_tasks_workspace_updated_idx
|
|
120
|
+
ON mcp_tasks (workspace_cwd, updated_at DESC);
|
|
102
121
|
|
|
103
122
|
CREATE INDEX IF NOT EXISTS jobs_repo_updated_idx
|
|
104
123
|
ON jobs (repo_id, updated_at DESC);
|
|
@@ -150,6 +169,9 @@ export class JobStore {
|
|
|
150
169
|
-- 0.1.6 migration: rename adversarial_review to challenge
|
|
151
170
|
UPDATE jobs SET command_type = 'challenge' WHERE command_type = 'adversarial_review';
|
|
152
171
|
`);
|
|
172
|
+
if (!mcpTableHasColumn(this.db, "kimi_identity")) {
|
|
173
|
+
this.db.exec("ALTER TABLE mcp_tasks ADD COLUMN kimi_identity TEXT;");
|
|
174
|
+
}
|
|
153
175
|
}
|
|
154
176
|
catch (error) {
|
|
155
177
|
// Migration failed — close the adapter so the OS handle is released
|
|
@@ -258,6 +280,40 @@ export class JobStore {
|
|
|
258
280
|
`, repoId, sessionId);
|
|
259
281
|
return row ? hydrateRow(row) : null;
|
|
260
282
|
}
|
|
283
|
+
createMcpTask(input) {
|
|
284
|
+
const now = new Date().toISOString();
|
|
285
|
+
try {
|
|
286
|
+
this.db.run(`
|
|
287
|
+
INSERT INTO mcp_tasks (
|
|
288
|
+
task_id, operation_kind, repo_id, workspace_cwd, companion_pid, companion_identity, kimi_identity,
|
|
289
|
+
stdout_path, stderr_path, launch_state, launch_error, created_at, attached_at, updated_at
|
|
290
|
+
) VALUES (
|
|
291
|
+
@task_id, @operation_kind, @repo_id, @workspace_cwd, NULL, NULL, NULL,
|
|
292
|
+
@stdout_path, @stderr_path, 'starting', NULL, @created_at, NULL, @updated_at
|
|
293
|
+
)
|
|
294
|
+
`, { ...input, created_at: now, updated_at: now });
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
throw translateSqliteError(error);
|
|
298
|
+
}
|
|
299
|
+
return this.getMcpTask(input.task_id);
|
|
300
|
+
}
|
|
301
|
+
getMcpTask(taskId) {
|
|
302
|
+
return this.db.get("SELECT * FROM mcp_tasks WHERE task_id = ?", taskId) ?? null;
|
|
303
|
+
}
|
|
304
|
+
updateMcpTask(taskId, patch) {
|
|
305
|
+
const updates = ["updated_at = @updated_at"];
|
|
306
|
+
const params = { task_id: taskId, updated_at: new Date().toISOString() };
|
|
307
|
+
for (const key of ["companion_pid", "companion_identity", "kimi_identity", "launch_state", "launch_error", "attached_at"]) {
|
|
308
|
+
const value = patch[key];
|
|
309
|
+
if (value === undefined)
|
|
310
|
+
continue;
|
|
311
|
+
updates.push(`${key} = @${key}`);
|
|
312
|
+
params[key] = value;
|
|
313
|
+
}
|
|
314
|
+
this.db.run(`UPDATE mcp_tasks SET ${updates.join(", ")} WHERE task_id = @task_id`, params);
|
|
315
|
+
return this.getMcpTask(taskId);
|
|
316
|
+
}
|
|
261
317
|
updateRunningJob(jobId, patch) {
|
|
262
318
|
return this.updateWhere(jobId, patch, "status = 'running'");
|
|
263
319
|
}
|
|
@@ -524,6 +580,9 @@ function tableHasColumn(db, columnName) {
|
|
|
524
580
|
const columns = db.all(`PRAGMA table_info(jobs)`);
|
|
525
581
|
return columns.some((column) => column.name === columnName);
|
|
526
582
|
}
|
|
583
|
+
function mcpTableHasColumn(db, columnName) {
|
|
584
|
+
return db.all("PRAGMA table_info(mcp_tasks)").some((column) => column.name === columnName);
|
|
585
|
+
}
|
|
527
586
|
function isSqliteConstraintError(error) {
|
|
528
587
|
const message = formatError(error).toLowerCase();
|
|
529
588
|
return message.includes("unique constraint") || message.includes("constraint failed");
|
|
@@ -30,6 +30,9 @@ export function normalizeJobError(error) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
export async function markJobFailed(store, paths, job, error, summary = "Job failed.", options) {
|
|
33
|
+
const current = store.getJob(job.job_id) ?? job;
|
|
34
|
+
if (current.status !== "running")
|
|
35
|
+
return current;
|
|
33
36
|
if (options?.partialOutput) {
|
|
34
37
|
const label = job.command_type[0].toUpperCase() + job.command_type.slice(1);
|
|
35
38
|
summary = `${label} timed out after ${formatBudgetDuration(options.partialOutput.budgetMs)}; partial output captured (${options.partialOutput.text.trim().length} chars).`;
|
|
@@ -55,6 +58,9 @@ export async function markJobFailed(store, paths, job, error, summary = "Job fai
|
|
|
55
58
|
}) ?? artifactJob);
|
|
56
59
|
}
|
|
57
60
|
export async function markJobCancelled(store, paths, job, summary, error, options) {
|
|
61
|
+
const current = store.getJob(job.job_id) ?? job;
|
|
62
|
+
if (current.status !== "running")
|
|
63
|
+
return current;
|
|
58
64
|
const normalized = error ? normalizeJobError(error) : null;
|
|
59
65
|
const phase = options?.phase;
|
|
60
66
|
const artifactJob = {
|
|
@@ -77,6 +83,8 @@ export async function markJobCancelled(store, paths, job, summary, error, option
|
|
|
77
83
|
}
|
|
78
84
|
export async function sweepStaleJobs(store, paths) {
|
|
79
85
|
for (const job of store.listRunningJobsWithProcessHints()) {
|
|
86
|
+
if (store.getMcpTask(job.job_id))
|
|
87
|
+
continue;
|
|
80
88
|
if (!isStaleEnoughToSweep(job)) {
|
|
81
89
|
continue;
|
|
82
90
|
}
|
|
@@ -109,7 +117,7 @@ export async function sweepStaleJobs(store, paths) {
|
|
|
109
117
|
await markJobFailed(store, paths, job, new RuntimeError("FOREGROUND_PROCESS_DISAPPEARED", describeMissingForegroundProcess(job, companionMissing, kimiMissing), "jobs.sweep"), "Foreground Kimi job disappeared before reporting a terminal state.", job.command_type === "ask" || job.command_type === "rescue" ? { phase: "failed" } : undefined);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
112
|
-
const STALE_SWEEP_GRACE_MS = 15_000;
|
|
120
|
+
export const STALE_SWEEP_GRACE_MS = 15_000;
|
|
113
121
|
function isStaleEnoughToSweep(job) {
|
|
114
122
|
const updatedAt = Date.parse(job.updated_at);
|
|
115
123
|
return Number.isFinite(updatedAt) && Date.now() - updatedAt >= STALE_SWEEP_GRACE_MS;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { RuntimeError } from "../errors.js";
|
|
3
|
+
export async function assertMcpJobBinding(task, job) {
|
|
4
|
+
if (!job)
|
|
5
|
+
throw bindingError(task, "The supervised job row does not exist.");
|
|
6
|
+
if (job.job_id !== task.task_id)
|
|
7
|
+
throw bindingError(task, "Task and job IDs differ.");
|
|
8
|
+
if (job.operation_kind !== task.operation_kind)
|
|
9
|
+
throw bindingError(task, "Task and job operation kinds differ.");
|
|
10
|
+
if (job.repo_id !== task.repo_id)
|
|
11
|
+
throw bindingError(task, "Task and job repositories differ.");
|
|
12
|
+
if (job.background)
|
|
13
|
+
throw bindingError(task, "Supervised MCP jobs must retain foreground companion ownership.");
|
|
14
|
+
if (job.status === "running" && (task.companion_pid === null || job.pid !== task.companion_pid)) {
|
|
15
|
+
throw bindingError(task, "Task and job companion process IDs differ or are missing.");
|
|
16
|
+
}
|
|
17
|
+
let jobCwd;
|
|
18
|
+
try {
|
|
19
|
+
jobCwd = await realpath(job.cwd);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw bindingError(task, "The job workspace cannot be resolved.", error);
|
|
23
|
+
}
|
|
24
|
+
if (jobCwd !== task.workspace_cwd)
|
|
25
|
+
throw bindingError(task, "Task and job workspaces differ.");
|
|
26
|
+
return job;
|
|
27
|
+
}
|
|
28
|
+
export function mcpCompanionIdentity(task) {
|
|
29
|
+
return parseIdentity(task.companion_identity, task.companion_pid, task, "companion");
|
|
30
|
+
}
|
|
31
|
+
export function mcpKimiIdentity(task, job) {
|
|
32
|
+
return parseIdentity(task.kimi_identity, job.kimi_pid, task, "Kimi");
|
|
33
|
+
}
|
|
34
|
+
function parseIdentity(serialized, expectedPid, task, label) {
|
|
35
|
+
if (serialized === null)
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const value = JSON.parse(serialized);
|
|
39
|
+
if (!Number.isInteger(value.pid) || (value.pid ?? 0) <= 0 ||
|
|
40
|
+
!Number.isInteger(value.processGroupId) ||
|
|
41
|
+
typeof value.startToken !== "string" || value.startToken.length === 0 ||
|
|
42
|
+
typeof value.state !== "string") {
|
|
43
|
+
throw new Error("invalid identity shape");
|
|
44
|
+
}
|
|
45
|
+
if (expectedPid !== null && expectedPid !== value.pid)
|
|
46
|
+
throw new Error("identity and recorded PID differ");
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw bindingError(task, `The ${label} process identity is invalid.`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function bindingError(task, message, cause) {
|
|
54
|
+
return new RuntimeError("MCP_JOB_BINDING_FAILED", message, "mcp.binding", {
|
|
55
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
56
|
+
details: { job_id: task.task_id, operation_kind: task.operation_kind },
|
|
57
|
+
});
|
|
58
|
+
}
|