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.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +5 -3
  3. package/.mcp.json +13 -0
  4. package/CHANGELOG.md +10 -0
  5. package/README.md +57 -5
  6. package/dist/cli-client.js +11 -1
  7. package/dist/commands/ask.js +4 -2
  8. package/dist/commands/cancel.js +43 -4
  9. package/dist/commands/pursue.js +3 -2
  10. package/dist/commands/rescue.js +3 -2
  11. package/dist/commands/review-gate.js +2 -4
  12. package/dist/commands/review.js +4 -2
  13. package/dist/commands/swarm.js +4 -3
  14. package/dist/companion.js +13 -40
  15. package/dist/help.js +2 -0
  16. package/dist/job-id.js +12 -0
  17. package/dist/job-store.js +59 -0
  18. package/dist/jobs.js +9 -1
  19. package/dist/mcp/binding.js +58 -0
  20. package/dist/mcp/job-control.js +345 -0
  21. package/dist/mcp/launcher.js +257 -0
  22. package/dist/mcp/operations.js +301 -0
  23. package/dist/mcp/protocol.js +275 -0
  24. package/dist/mcp/server.js +8 -0
  25. package/dist/mcp/stdio.js +78 -0
  26. package/dist/mcp/supervised-child.js +15 -0
  27. package/dist/mcp/tool-result.js +7 -0
  28. package/dist/mcp/workspace.js +34 -0
  29. package/dist/model-command.js +32 -0
  30. package/dist/version.js +1 -1
  31. package/docs/migration.md +2 -2
  32. package/docs/registry-releases.md +6 -8
  33. package/docs/safety.md +16 -2
  34. package/package.json +7 -3
  35. package/plugins/kimi-codex/.codex-plugin/plugin.json +7 -4
  36. package/plugins/kimi-codex/.mcp.json +28 -0
  37. package/plugins/kimi-codex/dist/cli-client.js +11 -1
  38. package/plugins/kimi-codex/dist/commands/ask.js +4 -2
  39. package/plugins/kimi-codex/dist/commands/cancel.js +43 -4
  40. package/plugins/kimi-codex/dist/commands/pursue.js +3 -2
  41. package/plugins/kimi-codex/dist/commands/rescue.js +3 -2
  42. package/plugins/kimi-codex/dist/commands/review-gate.js +2 -4
  43. package/plugins/kimi-codex/dist/commands/review.js +4 -2
  44. package/plugins/kimi-codex/dist/commands/swarm.js +4 -3
  45. package/plugins/kimi-codex/dist/companion.js +13 -40
  46. package/plugins/kimi-codex/dist/help.js +2 -0
  47. package/plugins/kimi-codex/dist/job-id.js +12 -0
  48. package/plugins/kimi-codex/dist/job-store.js +59 -0
  49. package/plugins/kimi-codex/dist/jobs.js +9 -1
  50. package/plugins/kimi-codex/dist/mcp/binding.js +58 -0
  51. package/plugins/kimi-codex/dist/mcp/job-control.js +345 -0
  52. package/plugins/kimi-codex/dist/mcp/launcher.js +257 -0
  53. package/plugins/kimi-codex/dist/mcp/operations.js +301 -0
  54. package/plugins/kimi-codex/dist/mcp/protocol.js +275 -0
  55. package/plugins/kimi-codex/dist/mcp/server.js +8 -0
  56. package/plugins/kimi-codex/dist/mcp/stdio.js +78 -0
  57. package/plugins/kimi-codex/dist/mcp/supervised-child.js +15 -0
  58. package/plugins/kimi-codex/dist/mcp/tool-result.js +7 -0
  59. package/plugins/kimi-codex/dist/mcp/workspace.js +34 -0
  60. package/plugins/kimi-codex/dist/model-command.js +32 -0
  61. package/plugins/kimi-codex/dist/version.js +1 -1
@@ -0,0 +1,257 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { access, open, stat } from "node:fs/promises";
4
+ import { constants as fsConstants } from "node:fs";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { RuntimeError } from "../errors.js";
8
+ import { preferStableNodePath } from "../hooks/install-paths.js";
9
+ import { JobStore } from "../job-store.js";
10
+ import { markJobFailed } from "../jobs.js";
11
+ import { resolveKimiHome } from "../kimi-home.js";
12
+ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
13
+ import { readProcessIdentity, revalidateProcessIdentities, } from "../process-tree.js";
14
+ import { assertMcpJobBinding, mcpCompanionIdentity } from "./binding.js";
15
+ import { resolveMcpWorkspace } from "./workspace.js";
16
+ const compiled = import.meta.url.endsWith(".js");
17
+ const companionEntrypoint = fileURLToPath(new URL(compiled ? "../companion.js" : "../companion.ts", import.meta.url));
18
+ const companionProjectRoot = path.resolve(path.dirname(companionEntrypoint), "..");
19
+ export async function launchMcpOperation(invocation, env, options) {
20
+ const attachTimeoutMs = options?.attachTimeoutMs ?? 60_000;
21
+ const readIdentity = options?.identityReader ?? readProcessIdentity;
22
+ let preserveStartingTask = false;
23
+ const attachDeadline = Date.now() + attachTimeoutMs;
24
+ const paths = resolvePluginPaths(env);
25
+ await ensurePluginPaths(paths);
26
+ const workspace = await resolveMcpWorkspace(env, invocation.workspace);
27
+ const taskId = randomUUID();
28
+ const stdoutPath = path.join(paths.logsDir, `mcp-${taskId}.stdout.log`);
29
+ const stderrPath = path.join(paths.logsDir, `mcp-${taskId}.stderr.log`);
30
+ const store = new JobStore(paths);
31
+ let child;
32
+ let stdoutHandle;
33
+ let stderrHandle;
34
+ let exited;
35
+ let spawnError;
36
+ try {
37
+ const task = store.createMcpTask({
38
+ task_id: taskId,
39
+ operation_kind: invocation.operationKind,
40
+ repo_id: workspace.repo.repoId,
41
+ workspace_cwd: workspace.cwd,
42
+ stdout_path: stdoutPath,
43
+ stderr_path: stderrPath,
44
+ });
45
+ stdoutHandle = await open(stdoutPath, "wx", 0o600);
46
+ stderrHandle = await open(stderrPath, "wx", 0o600);
47
+ const nodeBinary = await resolveNodeBinary(env);
48
+ const args = compiled
49
+ ? [companionEntrypoint, "mcp-child", taskId, ...invocation.argv]
50
+ : ["--import", "tsx", companionEntrypoint, "mcp-child", taskId, ...invocation.argv];
51
+ const childEnv = sanitizedChildEnv(env, paths, workspace.cwd);
52
+ child = spawn(nodeBinary, args, {
53
+ cwd: companionProjectRoot,
54
+ detached: process.platform !== "win32",
55
+ stdio: ["ignore", stdoutHandle.fd, stderrHandle.fd],
56
+ env: childEnv,
57
+ });
58
+ child.once("error", (error) => { spawnError = error; });
59
+ child.once("close", (code, signal) => { exited = { code, signal }; });
60
+ if (child.pid !== undefined) {
61
+ const identity = await readIdentity(child.pid);
62
+ store.updateMcpTask(taskId, {
63
+ companion_pid: child.pid,
64
+ ...(identity ? { companion_identity: JSON.stringify(identity) } : {}),
65
+ });
66
+ }
67
+ await stdoutHandle.close();
68
+ await stderrHandle.close();
69
+ stdoutHandle = undefined;
70
+ stderrHandle = undefined;
71
+ for (;;) {
72
+ if (spawnError)
73
+ throw await markLaunchFailure(store, task, spawnError);
74
+ const job = store.getJob(taskId);
75
+ if (job && (job.status !== "running" || job.phase === "turn-running" || job.phase === "worker-running")) {
76
+ const currentTask = store.getMcpTask(taskId) ?? task;
77
+ await assertMcpJobBinding(currentTask, job);
78
+ let identity;
79
+ if (job.status === "running") {
80
+ const expected = mcpCompanionIdentity(currentTask);
81
+ if (expected) {
82
+ const validation = await revalidateProcessIdentities([expected], {
83
+ identityReader: options?.identityReader,
84
+ });
85
+ if (!validation.complete) {
86
+ preserveStartingTask = true;
87
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The attachment identity probe was inconclusive; the launch record was preserved.", "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind } });
88
+ }
89
+ if (validation.identities.length === 0) {
90
+ preserveStartingTask = true;
91
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", "The recorded companion process identity changed before attachment; no process was adopted and the launch record was preserved.", "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind } });
92
+ }
93
+ identity = validation.identities[0];
94
+ }
95
+ else if (child.pid !== undefined) {
96
+ identity = await readIdentity(child.pid);
97
+ }
98
+ }
99
+ const attached = store.updateMcpTask(taskId, {
100
+ launch_state: "attached",
101
+ attached_at: new Date().toISOString(),
102
+ ...(identity ? { companion_identity: JSON.stringify(identity) } : {}),
103
+ });
104
+ child.unref();
105
+ return { task: attached, job };
106
+ }
107
+ if (exited) {
108
+ const diagnostic = await readTail(stderrPath);
109
+ const error = new RuntimeError("MCP_CHILD_LAUNCH_FAILED", `The companion exited before creating its job (code ${exited.code ?? "unknown"}${exited.signal ? `, signal ${exited.signal}` : ""}).${diagnostic ? `\n${diagnostic}` : ""}`, "mcp.launch", { details: { job_id: taskId, operation_kind: invocation.operationKind, exit_code: exited.code, signal: exited.signal } });
110
+ throw await markLaunchFailure(store, task, error, diagnostic);
111
+ }
112
+ if (Date.now() >= attachDeadline) {
113
+ const aborted = await abortMcpLaunch(store, paths, task, job, attachTimeoutMs, options?.identityReader);
114
+ if ("result" in aborted) {
115
+ child.unref();
116
+ return aborted.result;
117
+ }
118
+ preserveStartingTask = aborted.preserveStarting;
119
+ throw aborted.error;
120
+ }
121
+ await sleep(25);
122
+ }
123
+ }
124
+ catch (error) {
125
+ if (!preserveStartingTask && store.getMcpTask(taskId)?.launch_state === "starting") {
126
+ store.updateMcpTask(taskId, {
127
+ launch_state: "exited",
128
+ launch_error: (error instanceof Error ? error.message : String(error)).slice(-8_000),
129
+ });
130
+ }
131
+ child?.unref();
132
+ throw error;
133
+ }
134
+ finally {
135
+ store.close();
136
+ await stdoutHandle?.close().catch(() => undefined);
137
+ await stderrHandle?.close().catch(() => undefined);
138
+ }
139
+ }
140
+ async function abortMcpLaunch(store, paths, task, job, timeoutMs, identityReader) {
141
+ let signalDelivered = false;
142
+ let preserveStarting = true;
143
+ try {
144
+ const current = store.getMcpTask(task.task_id) ?? task;
145
+ const identity = mcpCompanionIdentity(current);
146
+ if (identity) {
147
+ const validation = await revalidateProcessIdentities([identity], { identityReader });
148
+ if (!validation.complete) {
149
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The companion identity probe was inconclusive; no signal was sent and the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
150
+ }
151
+ if (validation.identities.length === 0) {
152
+ throw new RuntimeError("MCP_PROCESS_IDENTITY_MISMATCH", "The recorded companion process identity no longer matches; no signal was sent and the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
153
+ }
154
+ signalVerifiedIdentity(identity, "SIGTERM");
155
+ signalDelivered = true;
156
+ await sleep(1_500);
157
+ const remaining = await revalidateProcessIdentities([identity], { identityReader });
158
+ if (!remaining.complete) {
159
+ const error = new RuntimeError("MCP_PROCESS_IDENTITY_UNAVAILABLE", "The post-signal companion identity probe was inconclusive; the launch record was preserved.", "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind } });
160
+ if (job) {
161
+ await assertMcpJobBinding(current, job);
162
+ return { result: { task: current, job } };
163
+ }
164
+ return { error, preserveStarting };
165
+ }
166
+ if (remaining.identities.length > 0) {
167
+ signalVerifiedIdentity(identity, "SIGKILL");
168
+ }
169
+ preserveStarting = false;
170
+ }
171
+ const error = new RuntimeError("MCP_LAUNCH_TIMEOUT", `The companion did not become cancellation-ready within ${timeoutMs}ms.${signalDelivered ? " The companion was signalled." : " No verified process identity was available, so no signal was sent."}`, "mcp.launch", { details: { job_id: task.task_id, operation_kind: task.operation_kind, timeout_ms: timeoutMs, signal_delivered: signalDelivered } });
172
+ if (signalDelivered) {
173
+ if (job) {
174
+ await markJobFailed(store, paths, job, error, "MCP launch timed out before cancellation was ready.", { phase: "failed" });
175
+ }
176
+ return { error: await markLaunchFailure(store, current, error), preserveStarting };
177
+ }
178
+ if (job) {
179
+ await assertMcpJobBinding(current, job);
180
+ return { result: { task: current, job } };
181
+ }
182
+ return { error, preserveStarting };
183
+ }
184
+ catch (error) {
185
+ return { error: error instanceof Error ? error : new Error(String(error)), preserveStarting };
186
+ }
187
+ }
188
+ function signalVerifiedIdentity(identity, signal) {
189
+ try {
190
+ process.kill(identity.processGroupId === identity.pid ? -identity.processGroupId : identity.pid, signal);
191
+ }
192
+ catch (error) {
193
+ const code = error.code;
194
+ if (code !== "ESRCH")
195
+ throw error;
196
+ }
197
+ }
198
+ async function resolveNodeBinary(env) {
199
+ const override = env.KIMI_PLUGIN_CC_NODE_BIN;
200
+ const binary = override ? override : preferStableNodePath(process.execPath);
201
+ if (binary.includes("/") || binary.includes("\\")) {
202
+ try {
203
+ await access(binary, fsConstants.X_OK);
204
+ }
205
+ catch (error) {
206
+ throw new RuntimeError("MCP_NODE_BIN_INVALID", `Configured Node binary is not executable: ${binary}`, "mcp.launch", {
207
+ cause: error instanceof Error ? error : undefined,
208
+ });
209
+ }
210
+ }
211
+ return binary;
212
+ }
213
+ function sanitizedChildEnv(env, paths, workspace) {
214
+ return {
215
+ ...env,
216
+ KIMI_PLUGIN_CC_WORKSPACE_CWD: workspace,
217
+ KIMI_PLUGIN_CC_MCP_WORKSPACE: workspace,
218
+ KIMI_PLUGIN_CC_DATA: path.resolve(paths.claudePluginData),
219
+ KIMI_CODE_HOME: resolveKimiHome(env, workspace),
220
+ KIMI_PLUGIN_CC_WORKSPACE_ROOT: undefined,
221
+ KIMI_PLUGIN_CC_CMD: undefined,
222
+ KIMI_PLUGIN_CC_OPERATION: undefined,
223
+ KIMI_PLUGIN_CC_SKIP_HOOK_CHECK: undefined,
224
+ KIMI_PLUGIN_CC_SKIP_VERSION_PROBE: undefined,
225
+ KIMI_CODE_TRUST_WORKSPACE: undefined,
226
+ KIMI_CODE_EXPERIMENTAL_FLAG: undefined,
227
+ };
228
+ }
229
+ async function markLaunchFailure(store, task, error, diagnostic) {
230
+ const failure = error instanceof Error ? error : new Error(String(error));
231
+ store.updateMcpTask(task.task_id, {
232
+ launch_state: "exited",
233
+ launch_error: (diagnostic || failure.message).slice(-8_000),
234
+ });
235
+ return failure;
236
+ }
237
+ async function readTail(filePath, maximum = 8_000) {
238
+ try {
239
+ const info = await stat(filePath);
240
+ const handle = await open(filePath, "r");
241
+ try {
242
+ const length = Math.min(maximum, info.size);
243
+ const buffer = Buffer.alloc(length);
244
+ await handle.read(buffer, 0, length, Math.max(0, info.size - length));
245
+ return buffer.toString("utf8").trim();
246
+ }
247
+ finally {
248
+ await handle.close();
249
+ }
250
+ }
251
+ catch {
252
+ return "";
253
+ }
254
+ }
255
+ function sleep(ms) {
256
+ return new Promise((resolve) => setTimeout(resolve, ms));
257
+ }
@@ -0,0 +1,301 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ import { parseDurationMs, validateThinkingEffort } from "../parsing.js";
3
+ const WORKSPACE_PROPERTY = {
4
+ type: "string",
5
+ description: "Absolute workspace directory. Required when the host does not configure a default; Codex normally supplies it explicitly.",
6
+ };
7
+ const MODEL_PROPERTY = {
8
+ type: "string",
9
+ description: "One configured Kimi model alias. Omit to preserve the normal session default; never guess an alias.",
10
+ };
11
+ const THINKING_PROPERTY = {
12
+ type: "string",
13
+ description: "Per-run thinking-effort request such as off, low, medium, or high. Provider/model support varies and is not observable by the plugin.",
14
+ };
15
+ const BUDGET_PROPERTY = {
16
+ type: "string",
17
+ description: "Finite duration such as 30m, 1h, or 90s; bare numbers mean minutes. Maximum 24h. Background operation does not extend it.",
18
+ };
19
+ const COMMON_PROPERTIES = {
20
+ workspace: WORKSPACE_PROPERTY,
21
+ model: MODEL_PROPERTY,
22
+ thinking: THINKING_PROPERTY,
23
+ budget: BUDGET_PROPERTY,
24
+ };
25
+ const RESUME_PROPERTIES = {
26
+ fresh: {
27
+ type: "boolean",
28
+ description: "Start a fresh Kimi session. Incompatible with resume and resume_latest.",
29
+ },
30
+ resume: {
31
+ type: "string",
32
+ description: "Resume this job ID or Kimi session ID with the standard continuation instruction. Do not also supply prompt.",
33
+ },
34
+ resume_latest: {
35
+ type: "boolean",
36
+ description: "Resume the latest matching session for this repository. May be combined with a new prompt.",
37
+ },
38
+ };
39
+ const PROMPT_PROPERTY = {
40
+ type: "string",
41
+ description: "The complete user request, preserved verbatim. Required except when resume or resume_latest supplies the continuation context.",
42
+ };
43
+ const OBJECTIVE_PROPERTY = {
44
+ type: "string",
45
+ description: "The complete objective, preserved verbatim.",
46
+ };
47
+ const FOCUS_PROPERTY = {
48
+ type: "string",
49
+ description: "Optional review focus or adversarial framing, preserved verbatim.",
50
+ };
51
+ const BASE_PROPERTY = {
52
+ type: "string",
53
+ description: "Optional Git base ref. The committed branch diff is authoritative; dirty current files are not treated as committed snapshots.",
54
+ };
55
+ const TURNS_PROPERTY = {
56
+ type: "integer",
57
+ minimum: 1,
58
+ description: "Soft turn-count hint included in the objective. The finite budget is the hard bound.",
59
+ };
60
+ const CAP_PROPERTY = {
61
+ type: "integer",
62
+ minimum: 1,
63
+ description: "Soft hint for total subagent count, not a hook-enforced limit.",
64
+ };
65
+ const MAX_CONCURRENCY_PROPERTY = {
66
+ type: "integer",
67
+ minimum: 1,
68
+ description: "Hard peak subagent concurrency on supported Kimi Code versions. Distinct from the soft total cap.",
69
+ };
70
+ const START_RESULT_GUIDANCE = "Task-capable clients receive an MCP task. Other clients receive a job ID immediately after launch; use kimi_job_status and kimi_job_result until terminal, and kimi_job_cancel to stop. Do not present a launch handle as the finished answer.";
71
+ function schema(properties, required = []) {
72
+ return { type: "object", properties, required, additionalProperties: false };
73
+ }
74
+ const OPERATION_SPECS = {
75
+ kimi_ask: {
76
+ kind: "ask",
77
+ command: ["ask"],
78
+ write: false,
79
+ summary: "Start an independent read-only Kimi repository explanation.",
80
+ inputSchema: schema({ ...COMMON_PROPERTIES, prompt: PROMPT_PROPERTY, ...RESUME_PROPERTIES }),
81
+ },
82
+ kimi_review: {
83
+ kind: "review",
84
+ command: ["review"],
85
+ write: false,
86
+ summary: "Start an independent read-only Kimi review of working-tree changes, including untracked files, or a committed base diff.",
87
+ inputSchema: schema({ ...COMMON_PROPERTIES, base: BASE_PROPERTY, focus: FOCUS_PROPERTY }),
88
+ },
89
+ kimi_challenge: {
90
+ kind: "challenge",
91
+ command: ["task", "challenge"],
92
+ write: false,
93
+ summary: "Start a read-only Kimi challenge of assumptions, design choices, and tradeoffs rather than a defect-only review.",
94
+ inputSchema: schema({ ...COMMON_PROPERTIES, base: BASE_PROPERTY, focus: FOCUS_PROPERTY }),
95
+ },
96
+ kimi_rescue: {
97
+ kind: "rescue",
98
+ command: ["task", "rescue"],
99
+ write: true,
100
+ summary: "Start a bounded write-capable Kimi investigation or fix inside the validated workspace. Review the resulting changes; no commit is made.",
101
+ inputSchema: schema({ ...COMMON_PROPERTIES, prompt: PROMPT_PROPERTY, ...RESUME_PROPERTIES }),
102
+ },
103
+ kimi_pursue: {
104
+ kind: "pursue",
105
+ command: ["task", "pursue"],
106
+ write: true,
107
+ summary: "Start explicitly autonomous Kimi goal mode with a finite hard budget and workspace write limits. Use only for hands-off objectives, not a single bounded fix.",
108
+ inputSchema: schema({ ...COMMON_PROPERTIES, objective: OBJECTIVE_PROPERTY, turns: TURNS_PROPERTY }, ["objective"]),
109
+ },
110
+ kimi_swarm: {
111
+ kind: "swarm",
112
+ command: ["task", "swarm"],
113
+ write: false,
114
+ summary: "Start a read-only parallel Kimi fan-out across many independent review targets.",
115
+ inputSchema: schema({ ...COMMON_PROPERTIES, objective: OBJECTIVE_PROPERTY, cap: CAP_PROPERTY, max_concurrency: MAX_CONCURRENCY_PROPERTY }, ["objective"]),
116
+ },
117
+ kimi_swarm_write: {
118
+ kind: "swarm-write",
119
+ command: ["task", "swarm"],
120
+ write: true,
121
+ summary: "Start parallel Kimi implementation in a disposable worktree off HEAD. The real tree is not edited; the result is a patch to review and apply separately.",
122
+ inputSchema: schema({ ...COMMON_PROPERTIES, objective: OBJECTIVE_PROPERTY, cap: CAP_PROPERTY, max_concurrency: MAX_CONCURRENCY_PROPERTY }, ["objective"]),
123
+ },
124
+ };
125
+ const JOB_CONTROL_SCHEMA = schema({
126
+ workspace: WORKSPACE_PROPERTY,
127
+ job_id: { type: "string", description: "The UUID returned by an MCP-launched Kimi operation." },
128
+ }, ["job_id"]);
129
+ export const MCP_TOOL_DEFINITIONS = [
130
+ ...Object.entries(OPERATION_SPECS).map(([name, spec]) => ({
131
+ name: name,
132
+ description: `${spec.summary} ${START_RESULT_GUIDANCE}`,
133
+ inputSchema: spec.inputSchema,
134
+ annotations: {
135
+ title: name.replaceAll("_", " "),
136
+ readOnlyHint: !spec.write,
137
+ destructiveHint: spec.write,
138
+ idempotentHint: false,
139
+ openWorldHint: true,
140
+ },
141
+ })),
142
+ {
143
+ name: "kimi_job_status",
144
+ description: "Read one MCP-launched job's durable state and phase in the same workspace. This is always an ordinary MCP result, never a task.",
145
+ inputSchema: JOB_CONTROL_SCHEMA,
146
+ annotations: { title: "kimi job status", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
147
+ },
148
+ {
149
+ name: "kimi_job_result",
150
+ description: "Read the terminal artifact for one MCP-launched job in the same workspace. Failed jobs return isError:true with their artifact; running jobs return JOB_NOT_TERMINAL without changing state.",
151
+ inputSchema: JOB_CONTROL_SCHEMA,
152
+ annotations: { title: "kimi job result", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
153
+ },
154
+ {
155
+ name: "kimi_job_cancel",
156
+ description: "Cooperatively cancel one running MCP-launched job in the same workspace. Terminal jobs are a no-op. Cancellation does not roll back edits, and write-swarm still captures its patch before cleanup.",
157
+ inputSchema: JOB_CONTROL_SCHEMA,
158
+ annotations: { title: "kimi job cancel", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
159
+ },
160
+ ];
161
+ export function isMcpModelToolName(name) {
162
+ return Object.hasOwn(OPERATION_SPECS, name);
163
+ }
164
+ export function buildMcpOperationInvocation(name, input) {
165
+ const spec = OPERATION_SPECS[name];
166
+ const properties = spec.inputSchema.properties;
167
+ const args = readArguments(input, Object.keys(properties));
168
+ const workspace = optionalString(args, "workspace");
169
+ const argv = [...spec.command];
170
+ appendCommonOptions(argv, args, spec.kind === "pursue" || spec.kind === "swarm" || spec.kind === "swarm-write" ? "safety" : "response");
171
+ switch (spec.kind) {
172
+ case "ask":
173
+ case "rescue": {
174
+ const prompt = args.prompt === undefined ? undefined : requiredNonEmptyString(args, "prompt");
175
+ const fresh = optionalBoolean(args, "fresh") ?? false;
176
+ const resumeLatest = optionalBoolean(args, "resume_latest") ?? false;
177
+ const resume = args.resume === undefined ? undefined : requiredNonEmptyString(args, "resume");
178
+ if (fresh && (resumeLatest || resume !== undefined)) {
179
+ throw invalidArguments("fresh cannot be combined with resume or resume_latest.");
180
+ }
181
+ if (resumeLatest && resume !== undefined) {
182
+ throw invalidArguments("resume and resume_latest are alternatives; choose one.");
183
+ }
184
+ if (resume !== undefined && prompt !== undefined) {
185
+ throw invalidArguments("resume uses the standard continuation instruction and cannot include prompt. Use resume_latest with a new prompt instead.");
186
+ }
187
+ if (prompt === undefined && !resumeLatest && resume === undefined) {
188
+ throw invalidArguments(`${spec.kind} requires prompt unless resume or resume_latest is selected.`);
189
+ }
190
+ if (fresh)
191
+ argv.push("--fresh");
192
+ if (resumeLatest)
193
+ argv.push("-r");
194
+ if (resume !== undefined)
195
+ argv.push("--resume", resume);
196
+ if (prompt !== undefined)
197
+ argv.push("--", prompt);
198
+ break;
199
+ }
200
+ case "review":
201
+ case "challenge": {
202
+ const base = args.base === undefined ? undefined : requiredNonEmptyString(args, "base");
203
+ const focus = optionalString(args, "focus");
204
+ if (base !== undefined)
205
+ argv.push("--base", base);
206
+ if (focus !== undefined)
207
+ argv.push("--", focus);
208
+ break;
209
+ }
210
+ case "pursue": {
211
+ const turns = optionalPositiveInteger(args, "turns");
212
+ if (turns !== undefined)
213
+ argv.push("--turns", String(turns));
214
+ argv.push("--", requiredNonEmptyString(args, "objective"));
215
+ break;
216
+ }
217
+ case "swarm":
218
+ case "swarm-write": {
219
+ const cap = optionalPositiveInteger(args, "cap");
220
+ const maxConcurrency = optionalPositiveInteger(args, "max_concurrency");
221
+ if (cap !== undefined)
222
+ argv.push("--cap", String(cap));
223
+ if (maxConcurrency !== undefined)
224
+ argv.push("--max-concurrency", String(maxConcurrency));
225
+ if (spec.kind === "swarm-write")
226
+ argv.push("--write");
227
+ argv.push("--", requiredNonEmptyString(args, "objective"));
228
+ break;
229
+ }
230
+ }
231
+ return { operationKind: spec.kind, ...(workspace !== undefined ? { workspace } : {}), argv };
232
+ }
233
+ export function readJobControlArguments(input) {
234
+ const args = readArguments(input, ["workspace", "job_id"]);
235
+ return {
236
+ ...(optionalString(args, "workspace") !== undefined ? { workspace: optionalString(args, "workspace") } : {}),
237
+ jobId: requiredNonEmptyString(args, "job_id"),
238
+ };
239
+ }
240
+ function appendCommonOptions(argv, args, budgetKind) {
241
+ const budget = optionalString(args, "budget");
242
+ const model = optionalString(args, "model");
243
+ const thinking = optionalString(args, "thinking");
244
+ if (budget !== undefined) {
245
+ parseDurationMs(budget, "--budget", "mcp.args", budgetKind);
246
+ argv.push("--budget", budget);
247
+ }
248
+ if (model !== undefined)
249
+ argv.push("-m", requiredNonEmptyString(args, "model"));
250
+ if (thinking !== undefined) {
251
+ validateThinkingEffort(thinking, "mcp.args");
252
+ argv.push("--thinking", thinking);
253
+ }
254
+ }
255
+ function readArguments(input, allowedKeys) {
256
+ if (input === undefined)
257
+ return {};
258
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
259
+ throw invalidArguments("Tool arguments must be a JSON object.");
260
+ }
261
+ const args = input;
262
+ const allowed = new Set(allowedKeys);
263
+ for (const key of Object.keys(args)) {
264
+ if (!allowed.has(key))
265
+ throw invalidArguments(`Unknown argument ${key}. Allowed arguments: ${allowedKeys.join(", ")}.`);
266
+ }
267
+ return args;
268
+ }
269
+ function optionalString(args, key) {
270
+ const value = args[key];
271
+ if (value === undefined)
272
+ return undefined;
273
+ if (typeof value !== "string")
274
+ throw invalidArguments(`${key} must be a string.`);
275
+ return value;
276
+ }
277
+ function requiredNonEmptyString(args, key) {
278
+ const value = optionalString(args, key);
279
+ if (value === undefined || value.trim().length === 0)
280
+ throw invalidArguments(`${key} must be a non-empty string.`);
281
+ return value;
282
+ }
283
+ function optionalBoolean(args, key) {
284
+ const value = args[key];
285
+ if (value === undefined)
286
+ return undefined;
287
+ if (typeof value !== "boolean")
288
+ throw invalidArguments(`${key} must be a boolean.`);
289
+ return value;
290
+ }
291
+ function optionalPositiveInteger(args, key) {
292
+ const value = args[key];
293
+ if (value === undefined)
294
+ return undefined;
295
+ if (!Number.isSafeInteger(value) || value <= 0)
296
+ throw invalidArguments(`${key} must be a positive safe integer.`);
297
+ return value;
298
+ }
299
+ function invalidArguments(message) {
300
+ return new RuntimeError("MCP_INVALID_ARGUMENTS", message, "mcp.args");
301
+ }