kimi-plugin-cc 2.4.5 → 2.5.1
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +42 -26
- package/dist/dsh/companion.js +174 -0
- package/dist/dsh/index.js +156 -0
- package/dist/hooks/install-paths.js +2 -2
- package/dist/version.js +1 -1
- package/docs/models.md +5 -1
- package/docs/registry-releases.md +20 -17
- package/docs/safety.md +22 -9
- package/docs/upstream-2.4.1-certification.md +6 -0
- package/package.json +40 -6
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
- package/plugins/kimi-codex/dist/dsh/index.js +156 -0
- package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-dsh/LICENSE +201 -0
- package/plugins/kimi-dsh/README.md +80 -0
- package/plugins/kimi-dsh/cordis.patch.yml +45 -0
- package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
- package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
- package/plugins/kimi-dsh/dist/cli-client.js +876 -0
- package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
- package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
- package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
- package/plugins/kimi-dsh/dist/commands/models.js +140 -0
- package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
- package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
- package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
- package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
- package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
- package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
- package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
- package/plugins/kimi-dsh/dist/commands/result.js +66 -0
- package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
- package/plugins/kimi-dsh/dist/commands/review.js +220 -0
- package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
- package/plugins/kimi-dsh/dist/commands/status.js +27 -0
- package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
- package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
- package/plugins/kimi-dsh/dist/companion.js +97 -0
- package/plugins/kimi-dsh/dist/config.js +22 -0
- package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
- package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
- package/plugins/kimi-dsh/dist/errors.js +40 -0
- package/plugins/kimi-dsh/dist/git.js +224 -0
- package/plugins/kimi-dsh/dist/help.js +43 -0
- package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
- package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
- package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
- package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
- package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
- package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
- package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
- package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
- package/plugins/kimi-dsh/dist/job-id.js +12 -0
- package/plugins/kimi-dsh/dist/job-store.js +659 -0
- package/plugins/kimi-dsh/dist/jobs.js +217 -0
- package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
- package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
- package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
- package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
- package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
- package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
- package/plugins/kimi-dsh/dist/logging.js +22 -0
- package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
- package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
- package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
- package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
- package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
- package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
- package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
- package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
- package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
- package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
- package/plugins/kimi-dsh/dist/model-command.js +32 -0
- package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
- package/plugins/kimi-dsh/dist/package.json +1 -0
- package/plugins/kimi-dsh/dist/parsing.js +620 -0
- package/plugins/kimi-dsh/dist/paths.js +27 -0
- package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
- package/plugins/kimi-dsh/dist/process-tree.js +479 -0
- package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
- package/plugins/kimi-dsh/dist/render.js +177 -0
- package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
- package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
- package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
- package/plugins/kimi-dsh/dist/session-title.js +293 -0
- package/plugins/kimi-dsh/dist/stream-json.js +346 -0
- package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
- package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
- package/plugins/kimi-dsh/dist/types.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
- package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
- package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
- package/plugins/kimi-dsh/dist/version.js +7 -0
- package/plugins/kimi-dsh/package.json +48 -0
- package/plugins/kimi-dsh/scripts/companion.sh +37 -0
- package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { RuntimeError, formatError } from "./errors.js";
|
|
2
|
+
const BUDGET_CEILING_MS = 24 * 60 * 60 * 1_000;
|
|
3
|
+
function formatBudgetMs(ms) {
|
|
4
|
+
if (ms % 3_600_000 === 0)
|
|
5
|
+
return `${ms / 3_600_000}h`;
|
|
6
|
+
if (ms % 60_000 === 0)
|
|
7
|
+
return `${ms / 60_000}m`;
|
|
8
|
+
return `${Math.ceil(ms / 1_000)}s`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Suggest a concrete next budget from the one that just expired: double it,
|
|
12
|
+
* capped at the 24h ceiling. At the ceiling there is nothing left to raise,
|
|
13
|
+
* so the only honest advice is to narrow the prompt.
|
|
14
|
+
*/
|
|
15
|
+
function budgetAdvice(budgetMs) {
|
|
16
|
+
if (budgetMs === undefined || !Number.isFinite(budgetMs) || budgetMs <= 0)
|
|
17
|
+
return "raise the budget (`--budget <dur>`, at most 24h)";
|
|
18
|
+
if (budgetMs >= BUDGET_CEILING_MS)
|
|
19
|
+
return "note the budget is already at the 24h ceiling, so narrowing the prompt is the only remaining lever";
|
|
20
|
+
return `raise the budget (e.g. \`--budget ${formatBudgetMs(Math.min(budgetMs * 2, BUDGET_CEILING_MS))}\`)`;
|
|
21
|
+
}
|
|
22
|
+
export function classifyManagedCommandFailure(error, commandType, jobId, options) {
|
|
23
|
+
const classification = classifyKimiAvailability(error);
|
|
24
|
+
if (!classification) {
|
|
25
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
26
|
+
}
|
|
27
|
+
if (classification.kind === "response_timeout" && commandType !== "review_gate" && options?.background !== undefined) {
|
|
28
|
+
const foregroundAdvice = !options?.background && (commandType === "ask" || commandType === "rescue")
|
|
29
|
+
? " Or run with --background so the caller's shell timeout is not the limit."
|
|
30
|
+
: "";
|
|
31
|
+
classification.nextStep = `Narrow the prompt or ${budgetAdvice(options.budgetMs)}. Partial output, if any, is in the artifact (\`result ${jobId}\`).${foregroundAdvice}`;
|
|
32
|
+
}
|
|
33
|
+
const label = formatCommandLabel(commandType);
|
|
34
|
+
const stage = options?.preserveStage && error instanceof RuntimeError
|
|
35
|
+
? error.stage
|
|
36
|
+
: `${commandType}.runtime`;
|
|
37
|
+
// Thread the cause's code and message into `details` so the underlying
|
|
38
|
+
// failure survives the wrap on every channel (formatError renders details
|
|
39
|
+
// as a JSON line; normalizeJobError persists them in the job row). Without
|
|
40
|
+
// this, a CLI_NONZERO_EXIT whose message embeds kimi's stderr tail (e.g.
|
|
41
|
+
// `auth.login_required` on 2026-08-08) was reduced to the generic
|
|
42
|
+
// "run /kimi:setup" advice with the real cause reachable only as `cause`,
|
|
43
|
+
// which no output channel serializes — an LLM caller could not diagnose it.
|
|
44
|
+
const details = { availability: classification.kind };
|
|
45
|
+
if (error instanceof RuntimeError) {
|
|
46
|
+
details.cause_code = error.code;
|
|
47
|
+
for (const key of ["spawn_code", "spawn_errno", "spawn_message"]) {
|
|
48
|
+
if (error.details[key] !== undefined)
|
|
49
|
+
details[key] = key === "spawn_message" && typeof error.details[key] === "string"
|
|
50
|
+
? truncateCauseMessageForDetails(error.details[key]) : error.details[key];
|
|
51
|
+
}
|
|
52
|
+
if (error.code === "RESPONSE_TIMEOUT") {
|
|
53
|
+
for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
|
|
54
|
+
if (error.details[key] !== undefined)
|
|
55
|
+
details[key] = error.details[key];
|
|
56
|
+
}
|
|
57
|
+
if (details.budget_ms === undefined && options?.budgetMs !== undefined)
|
|
58
|
+
details.budget_ms = options.budgetMs;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (error instanceof Error && error.message.length > 0) {
|
|
62
|
+
details.cause_message = truncateCauseMessageForDetails(error.message);
|
|
63
|
+
}
|
|
64
|
+
return new RuntimeError(mapAvailabilityCode(classification.kind, commandType), `${label} could not run because ${classification.summary} ${classification.nextStep} Job ${jobId} was persisted as failed.`, stage, error instanceof Error ? { cause: error, details } : { details });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Cap the cause message carried in `details` so a large stderr tail cannot
|
|
68
|
+
* bloat the SQLite job row or the rendered details JSON line. Keep the TAIL
|
|
69
|
+
* end: for a crashed subprocess the informative lines (kimi's own `error:`
|
|
70
|
+
* output) are the trailing bytes of stderr, which cli-helpers appends last.
|
|
71
|
+
*/
|
|
72
|
+
const CAUSE_MESSAGE_DETAILS_MAX = 2000;
|
|
73
|
+
function truncateCauseMessageForDetails(message) {
|
|
74
|
+
if (message.length <= CAUSE_MESSAGE_DETAILS_MAX) {
|
|
75
|
+
return message;
|
|
76
|
+
}
|
|
77
|
+
return `…${message.slice(-CAUSE_MESSAGE_DETAILS_MAX)}`;
|
|
78
|
+
}
|
|
79
|
+
export function summarizeKimiAvailabilityWarning(error, commandType) {
|
|
80
|
+
const classification = classifyKimiAvailability(error);
|
|
81
|
+
if (!classification) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (classification.kind === "auth_unavailable") {
|
|
85
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} is not configured for model access; allowing stop.`;
|
|
86
|
+
}
|
|
87
|
+
if (classification.kind === "binary_unavailable") {
|
|
88
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} could not find the Kimi CLI; allowing stop.`;
|
|
89
|
+
}
|
|
90
|
+
if (classification.kind === "startup_failed") {
|
|
91
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} could not start a usable Kimi subprocess; allowing stop.`;
|
|
92
|
+
}
|
|
93
|
+
switch (classification.kind) {
|
|
94
|
+
case "startup_timeout":
|
|
95
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} did not respond during startup; allowing stop.`;
|
|
96
|
+
case "initialize_timeout":
|
|
97
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} did not complete session initialization; allowing stop.`;
|
|
98
|
+
case "response_timeout":
|
|
99
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} did not return a final response; allowing stop.`;
|
|
100
|
+
case "max_steps_reached":
|
|
101
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} exhausted its step budget; allowing stop.`;
|
|
102
|
+
case "timeout":
|
|
103
|
+
return `Kimi ${formatCommandLabel(commandType).toLowerCase()} timed out; allowing stop.`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function classifySetupFailure(error) {
|
|
107
|
+
return classifyKimiAvailability(error);
|
|
108
|
+
}
|
|
109
|
+
function classifyKimiAvailability(error) {
|
|
110
|
+
const message = formatError(error);
|
|
111
|
+
// kimi-code's managed OAuth store emits `auth.login_required: OAuth
|
|
112
|
+
// provider "managed:kimi-code" requires login…` on stderr when the token
|
|
113
|
+
// is missing or expired (observed on 0.34.0 after an out-of-band binary
|
|
114
|
+
// upgrade invalidated the token). The line reaches us inside the
|
|
115
|
+
// CLI_NONZERO_EXIT message because cli-helpers embeds the stderr tail.
|
|
116
|
+
// This MUST precede the code-based CLI_NONZERO_EXIT branch below, which
|
|
117
|
+
// would otherwise misclassify a logged-out CLI as startup_failed and
|
|
118
|
+
// point the user at /kimi:setup — which cannot fix auth.
|
|
119
|
+
if (message.includes("auth.login_required")) {
|
|
120
|
+
return {
|
|
121
|
+
kind: "auth_unavailable",
|
|
122
|
+
summary: "the local Kimi CLI is logged out (its OAuth login is missing or expired).",
|
|
123
|
+
nextStep: "Run `kimi login` to re-authenticate, then retry. `/kimi:setup` does not repair auth.",
|
|
124
|
+
runtimeProbe: "ok",
|
|
125
|
+
authProbe: "failed",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (message.includes("LLM is not set") || message.includes("LLM service error")) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "auth_unavailable",
|
|
131
|
+
summary: "local Kimi authentication or model configuration is not usable.",
|
|
132
|
+
nextStep: "Run `/kimi:setup`, then `kimi login` or fix the local Kimi model configuration and retry.",
|
|
133
|
+
runtimeProbe: "ok",
|
|
134
|
+
authProbe: "failed",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (error instanceof RuntimeError &&
|
|
138
|
+
(error.code === "CLI_SPAWN_FAILED" || error.code === "CLI_PROCESS_ERROR")) {
|
|
139
|
+
const spawnCode = error.details.spawn_code;
|
|
140
|
+
const binaryUnavailable = typeof spawnCode === "string" && ["ENOENT", "EACCES", "ENOEXEC"].includes(spawnCode);
|
|
141
|
+
if (typeof spawnCode === "string" && !binaryUnavailable) {
|
|
142
|
+
return {
|
|
143
|
+
kind: "startup_failed",
|
|
144
|
+
summary: "the Kimi subprocess could not be started or used.",
|
|
145
|
+
nextStep: "Check the underlying process error; shorten the prompt or environment if an argument-size limit was exceeded, then retry.",
|
|
146
|
+
runtimeProbe: "failed",
|
|
147
|
+
authProbe: "failed",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
// Keep opaque synchronous failures and legacy async ENOENT diagnostics compatible.
|
|
151
|
+
if (binaryUnavailable || error.code === "CLI_SPAWN_FAILED" || /\bENOENT\b/.test(message)) {
|
|
152
|
+
return {
|
|
153
|
+
kind: "binary_unavailable",
|
|
154
|
+
summary: "the Kimi CLI is missing from PATH or not executable in this environment.",
|
|
155
|
+
nextStep: "Run `/kimi:setup` to verify the install, then expose `kimi` on PATH and retry.",
|
|
156
|
+
runtimeProbe: "failed",
|
|
157
|
+
authProbe: "failed",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// v1.0 cli-client surfaces non-ENOENT process errors and non-zero
|
|
162
|
+
// exits as CLI_PROCESS_ERROR / CLI_NONZERO_EXIT. Treat both as
|
|
163
|
+
// startup-failed for classifier purposes — the next step is the same
|
|
164
|
+
// (run /kimi:setup) and the distinction between "kimi crashed during
|
|
165
|
+
// init" vs "kimi exited with status 1" is post-hoc.
|
|
166
|
+
if (error instanceof RuntimeError &&
|
|
167
|
+
(error.code === "CLI_PROCESS_ERROR" || error.code === "CLI_NONZERO_EXIT")) {
|
|
168
|
+
return {
|
|
169
|
+
kind: "startup_failed",
|
|
170
|
+
summary: "the Kimi CLI exited before completing the requested operation.",
|
|
171
|
+
nextStep: "Run `/kimi:setup` to verify local Kimi health, then retry.",
|
|
172
|
+
runtimeProbe: "failed",
|
|
173
|
+
authProbe: "failed",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
if (error instanceof RuntimeError && error.code === "MAX_STEPS_REACHED") {
|
|
177
|
+
return {
|
|
178
|
+
kind: "max_steps_reached",
|
|
179
|
+
summary: "Kimi exhausted its step budget before finalizing the turn.",
|
|
180
|
+
nextStep: "Retry with a more focused prompt or a higher step budget.",
|
|
181
|
+
runtimeProbe: "ok",
|
|
182
|
+
authProbe: "ok",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (error instanceof RuntimeError) {
|
|
186
|
+
// STARTUP_TIMEOUT and INITIALIZE_TIMEOUT predate the v1.0 subprocess
|
|
187
|
+
// transport (the v0.4 Wire client had a three-phase startup). The
|
|
188
|
+
// v1.0 cli-client only emits RESPONSE_TIMEOUT, so these branches are
|
|
189
|
+
// defensive — callers that synthesize the old codes (or load v0.4
|
|
190
|
+
// job rows from SQLite) still get a useful classification.
|
|
191
|
+
if (error.code === "STARTUP_TIMEOUT") {
|
|
192
|
+
return {
|
|
193
|
+
kind: "startup_timeout",
|
|
194
|
+
summary: "the Kimi CLI did not respond during startup.",
|
|
195
|
+
nextStep: "Run `/kimi:setup` to verify local Kimi health, then retry.",
|
|
196
|
+
runtimeProbe: "failed",
|
|
197
|
+
authProbe: "failed",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (error.code === "INITIALIZE_TIMEOUT") {
|
|
201
|
+
return {
|
|
202
|
+
kind: "initialize_timeout",
|
|
203
|
+
summary: "the Kimi session started but did not finish initializing in time.",
|
|
204
|
+
nextStep: "Run `/kimi:setup` to verify local Kimi configuration and retry.",
|
|
205
|
+
runtimeProbe: "failed",
|
|
206
|
+
authProbe: "failed",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (error.code === "RESPONSE_TIMEOUT") {
|
|
210
|
+
return {
|
|
211
|
+
kind: "response_timeout",
|
|
212
|
+
summary: "Kimi started and accepted the prompt but never returned a final response.",
|
|
213
|
+
nextStep: "Narrow the prompt or raise the budget (`--budget <dur>`, at most 24h). Check the job artifact (`result <jobId>`) for any captured partial output. Backgrounding does not extend the runtime budget.",
|
|
214
|
+
runtimeProbe: "ok",
|
|
215
|
+
authProbe: "ok",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (message.includes("timed out")) {
|
|
220
|
+
return {
|
|
221
|
+
kind: "timeout",
|
|
222
|
+
summary: "the Kimi runtime did not become ready within the expected time budget.",
|
|
223
|
+
nextStep: "Run `/kimi:setup` to check local Kimi auth and network health, then retry.",
|
|
224
|
+
runtimeProbe: "failed",
|
|
225
|
+
authProbe: "failed",
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
function mapAvailabilityCode(kind, commandType) {
|
|
231
|
+
const prefix = commandType.toUpperCase();
|
|
232
|
+
switch (kind) {
|
|
233
|
+
case "auth_unavailable":
|
|
234
|
+
return `${prefix}_KIMI_AUTH_UNAVAILABLE`;
|
|
235
|
+
case "binary_unavailable":
|
|
236
|
+
return `${prefix}_KIMI_BINARY_UNAVAILABLE`;
|
|
237
|
+
case "startup_failed":
|
|
238
|
+
return `${prefix}_KIMI_STARTUP_FAILED`;
|
|
239
|
+
case "startup_timeout":
|
|
240
|
+
return `${prefix}_KIMI_STARTUP_TIMEOUT`;
|
|
241
|
+
case "initialize_timeout":
|
|
242
|
+
return `${prefix}_KIMI_INITIALIZE_TIMEOUT`;
|
|
243
|
+
case "response_timeout":
|
|
244
|
+
return `${prefix}_KIMI_RESPONSE_TIMEOUT`;
|
|
245
|
+
case "timeout":
|
|
246
|
+
return `${prefix}_KIMI_TIMEOUT`;
|
|
247
|
+
case "max_steps_reached":
|
|
248
|
+
return `${prefix}_KIMI_MAX_STEPS_REACHED`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function formatCommandLabel(commandType) {
|
|
252
|
+
switch (commandType) {
|
|
253
|
+
case "challenge":
|
|
254
|
+
return "challenge";
|
|
255
|
+
case "review_gate":
|
|
256
|
+
return "review gate";
|
|
257
|
+
default:
|
|
258
|
+
return commandType.replaceAll("_", " ");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function resolveKimiHome(env, baseCwd = process.cwd()) {
|
|
4
|
+
const override = env.KIMI_CODE_HOME?.trim();
|
|
5
|
+
if (override) {
|
|
6
|
+
return path.resolve(baseCwd, override);
|
|
7
|
+
}
|
|
8
|
+
return path.join(os.homedir(), ".kimi-code");
|
|
9
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Per-command response budgets leave room for thinking-capable workflows;
|
|
2
|
+
// per-run effort is only a request. Ask/review default to 30 minutes and rescue
|
|
3
|
+
// to 45 minutes for repo-wide work. Review-gate stays at 8 seconds because it
|
|
4
|
+
// runs inside Claude Code's Stop hook and requests thinking off by default;
|
|
5
|
+
// KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can override that request.
|
|
6
|
+
/** Ask budget. Conversational; thinking-on free-form reasoning. */
|
|
7
|
+
export const KIMI_ASK_PROMPT_TIMEOUT_MS = 1_800_000; // 30 minutes
|
|
8
|
+
/** Review/challenge budget. Thinking-on single-turn analysis over the workspace. */
|
|
9
|
+
export const KIMI_REVIEW_PROMPT_TIMEOUT_MS = 1_800_000;
|
|
10
|
+
/** Rescue budget. Thinking-on multi-step apply/test/verify loops. */
|
|
11
|
+
export const KIMI_RESCUE_PROMPT_TIMEOUT_MS = 2_700_000; // 45 minutes
|
|
12
|
+
/**
|
|
13
|
+
* Pursue (autonomous goal mode) DEFAULT wall-clock ceiling when the user
|
|
14
|
+
* passes no `--budget`. This is the ONLY hard bound on an autonomous goal —
|
|
15
|
+
* headless goal create sets no token/turn budget, and the managed hook denies
|
|
16
|
+
* SetGoalBudget. The same duration as
|
|
17
|
+
* the single-turn rescue default, but a safety ceiling rather than a patience
|
|
18
|
+
* knob. Deliberately finite. Overridable per-job via `--budget` (parsed to ms in
|
|
19
|
+
* parsePursueArgs). See runtime/commands/pursue.ts and docs/safety.md.
|
|
20
|
+
*/
|
|
21
|
+
export const KIMI_PURSUE_DEFAULT_BUDGET_MS = 2_700_000; // 45 minutes
|
|
22
|
+
/**
|
|
23
|
+
* Swarm (read-only parallel fan-out) DEFAULT wall-clock ceiling when the user
|
|
24
|
+
* passes no `--budget`. Read-only swarm opens no write surface, so the only new
|
|
25
|
+
* risk is COST/runaway from N parallel model runs (plus possible nested
|
|
26
|
+
* AgentSwarm). This AbortController is the hard bound on that; the `--cap`
|
|
27
|
+
* subagent count is a soft prompt-injected hint (not hook-enforceable —
|
|
28
|
+
* the hook is stateless and can't count subagents). Sized like the single-turn
|
|
29
|
+
* review budget — a swarm fans review work out in parallel within one process,
|
|
30
|
+
* so 30m of wall-clock is generous headroom. Overridable per-job via `--budget`
|
|
31
|
+
* (parsed to ms in parseSwarmArgs). See runtime/commands/swarm.ts and
|
|
32
|
+
* docs/safety.md.
|
|
33
|
+
*/
|
|
34
|
+
export const KIMI_SWARM_DEFAULT_BUDGET_MS = 1_800_000; // 30 minutes
|
|
35
|
+
/**
|
|
36
|
+
* Review-gate budget. Fires inside Claude Code's Stop hook so any value
|
|
37
|
+
* above the user's perceptible wait makes the gate feel broken.
|
|
38
|
+
*
|
|
39
|
+
* The gate requests off through KIMI_MODEL_THINKING_EFFORT by default;
|
|
40
|
+
* KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can inherit or request another effort.
|
|
41
|
+
* BYO/non-trait-driven providers and always-thinking models can ignore it.
|
|
42
|
+
* The 8s timeout remains fail-open; a request never guarantees completion.
|
|
43
|
+
* Hook-escape findings annotate the result and never block or change job outcome.
|
|
44
|
+
* See docs/safety.md.
|
|
45
|
+
*/
|
|
46
|
+
export const KIMI_REVIEW_GATE_TIMEOUT_MS = 8_000;
|
|
47
|
+
export function isTimeoutCode(code) {
|
|
48
|
+
return (code === "TIMEOUT" ||
|
|
49
|
+
code === "STARTUP_TIMEOUT" ||
|
|
50
|
+
code === "INITIALIZE_TIMEOUT" ||
|
|
51
|
+
code === "RESPONSE_TIMEOUT");
|
|
52
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// Probe the installed kimi-code CLI version and check it against the
|
|
2
|
+
// range kimi-plugin-cc has been tested against.
|
|
3
|
+
//
|
|
4
|
+
// Setup probes the installed command and warns outside the tested range;
|
|
5
|
+
// model jobs probe the exact command + prefix tuple to build a fail-closed
|
|
6
|
+
// execution plan before spawn. The latter is the durable engine-provenance
|
|
7
|
+
// gate: an operation outside its certified engine/version range is refused
|
|
8
|
+
// rather than guessed.
|
|
9
|
+
//
|
|
10
|
+
// What this module is NOT:
|
|
11
|
+
//
|
|
12
|
+
// - Not, by itself, the policy decision. setup still warns; the execution-
|
|
13
|
+
// plan layer (`kimi-engine.ts`) decides whether an exact result is certified.
|
|
14
|
+
// - Not a license to infer historical versions. Old job rows remain unknown
|
|
15
|
+
// unless their saved logs contain positive evidence.
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
const KIMI_VERSION_PROBE_TIMEOUT_MS = 5_000;
|
|
18
|
+
/**
|
|
19
|
+
* Legacy-v1 kimi-code minors tested for plugin use. Membership is by
|
|
20
|
+
* `{major, minor}`; every patch within a listed minor is accepted.
|
|
21
|
+
*
|
|
22
|
+
* This table intentionally never gains 0.42, which removed legacy v1.
|
|
23
|
+
* Exact per-operation native-v2 certification lives in `kimi-engine.ts`.
|
|
24
|
+
*/
|
|
25
|
+
export const KIMI_TESTED_MINORS = [
|
|
26
|
+
{ major: 0, minor: 1 },
|
|
27
|
+
{ major: 0, minor: 2 },
|
|
28
|
+
{ major: 0, minor: 3 },
|
|
29
|
+
{ major: 0, minor: 4 },
|
|
30
|
+
{ major: 0, minor: 5 },
|
|
31
|
+
{ major: 0, minor: 6 },
|
|
32
|
+
{ major: 0, minor: 7 },
|
|
33
|
+
{ major: 0, minor: 8 },
|
|
34
|
+
{ major: 0, minor: 9 },
|
|
35
|
+
{ major: 0, minor: 10 },
|
|
36
|
+
{ major: 0, minor: 11 },
|
|
37
|
+
{ major: 0, minor: 12 },
|
|
38
|
+
{ major: 0, minor: 13 },
|
|
39
|
+
{ major: 0, minor: 14 },
|
|
40
|
+
{ major: 0, minor: 15 },
|
|
41
|
+
{ major: 0, minor: 16 },
|
|
42
|
+
{ major: 0, minor: 17 },
|
|
43
|
+
{ major: 0, minor: 18 },
|
|
44
|
+
{ major: 0, minor: 19 },
|
|
45
|
+
{ major: 0, minor: 20 },
|
|
46
|
+
{ major: 0, minor: 21 },
|
|
47
|
+
{ major: 0, minor: 22 },
|
|
48
|
+
{ major: 0, minor: 23 },
|
|
49
|
+
{ major: 0, minor: 24 },
|
|
50
|
+
{ major: 0, minor: 25 },
|
|
51
|
+
{ major: 0, minor: 26 },
|
|
52
|
+
{ major: 0, minor: 27 },
|
|
53
|
+
{ major: 0, minor: 28 },
|
|
54
|
+
{ major: 0, minor: 29 },
|
|
55
|
+
{ major: 0, minor: 30 },
|
|
56
|
+
{ major: 0, minor: 31 },
|
|
57
|
+
{ major: 0, minor: 32 },
|
|
58
|
+
{ major: 0, minor: 33 },
|
|
59
|
+
{ major: 0, minor: 34 },
|
|
60
|
+
{ major: 0, minor: 35 },
|
|
61
|
+
{ major: 0, minor: 36 },
|
|
62
|
+
{ major: 0, minor: 37 },
|
|
63
|
+
{ major: 0, minor: 38 },
|
|
64
|
+
{ major: 0, minor: 39 },
|
|
65
|
+
{ major: 0, minor: 40 },
|
|
66
|
+
{ major: 0, minor: 41 },
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Spawn `<kimi-bin> --version` and parse the output. Never throws;
|
|
70
|
+
* failures resolve to `{kind: "failed", reason}` so callers can decide
|
|
71
|
+
* the policy. The kimi binary path defaults to bare `kimi` (PATH
|
|
72
|
+
* lookup); callers that need an explicit absolute path should pass it.
|
|
73
|
+
*/
|
|
74
|
+
export async function probeKimiVersion(options) {
|
|
75
|
+
const bin = options.kimiBin ?? "kimi";
|
|
76
|
+
const args = [...(options.prefixArgs ?? []), "--version"];
|
|
77
|
+
const timeoutMs = options.timeoutMs ?? KIMI_VERSION_PROBE_TIMEOUT_MS;
|
|
78
|
+
return await new Promise((resolve) => {
|
|
79
|
+
let settled = false;
|
|
80
|
+
const settle = (result) => {
|
|
81
|
+
if (settled)
|
|
82
|
+
return;
|
|
83
|
+
settled = true;
|
|
84
|
+
resolve(result);
|
|
85
|
+
};
|
|
86
|
+
let child;
|
|
87
|
+
try {
|
|
88
|
+
child = spawn(bin, args, {
|
|
89
|
+
cwd: options.cwd,
|
|
90
|
+
env: options.env,
|
|
91
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
settle({
|
|
96
|
+
kind: "failed",
|
|
97
|
+
reason: `spawn failed: ${err.message}`,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
let stdout = "";
|
|
102
|
+
let stderr = "";
|
|
103
|
+
child.stdout.setEncoding("utf8");
|
|
104
|
+
child.stderr.setEncoding("utf8");
|
|
105
|
+
child.stdout.on("data", (chunk) => {
|
|
106
|
+
stdout += chunk;
|
|
107
|
+
});
|
|
108
|
+
child.stderr.on("data", (chunk) => {
|
|
109
|
+
stderr += chunk;
|
|
110
|
+
});
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
try {
|
|
113
|
+
child.kill("SIGTERM");
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
}
|
|
117
|
+
settle({
|
|
118
|
+
kind: "failed",
|
|
119
|
+
reason: `\`${[bin, ...args].join(" ")}\` timed out after ${timeoutMs}ms`,
|
|
120
|
+
});
|
|
121
|
+
}, timeoutMs);
|
|
122
|
+
child.on("error", (err) => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
settle({
|
|
125
|
+
kind: "failed",
|
|
126
|
+
reason: `spawn error: ${err.message}`,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
child.on("close", (code) => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
if (code !== 0) {
|
|
132
|
+
const detail = stderr.trim() !== "" ? stderr.trim() : `exit ${code}`;
|
|
133
|
+
settle({
|
|
134
|
+
kind: "failed",
|
|
135
|
+
reason: `\`${[bin, ...args].join(" ")}\` failed: ${detail}`,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseVersionLine(stdout);
|
|
140
|
+
if (parsed === undefined) {
|
|
141
|
+
settle({
|
|
142
|
+
kind: "failed",
|
|
143
|
+
reason: `could not parse \`${[bin, ...args].join(" ")}\` output: ${JSON.stringify(stdout.slice(0, 80))}`,
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
settle({
|
|
148
|
+
kind: "ok",
|
|
149
|
+
version: parsed.raw,
|
|
150
|
+
major: parsed.major,
|
|
151
|
+
minor: parsed.minor,
|
|
152
|
+
patch: parsed.patch,
|
|
153
|
+
inTestedRange: isInTestedRange(parsed.major, parsed.minor),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Parse a `kimi --version` stdout into a structured version. Accepts
|
|
160
|
+
* bare semver (`0.2.0`), `v`-prefixed (`v0.2.0`), and trailing
|
|
161
|
+
* pre-release / build metadata (`0.2.0-beta.1`, `0.2.0+sha.abc`). The
|
|
162
|
+
* major/minor/patch numbers are the leading three components only —
|
|
163
|
+
* pre-release / build metadata is preserved in `raw` but doesn't
|
|
164
|
+
* affect the tested-range check.
|
|
165
|
+
*
|
|
166
|
+
* Returns undefined when the first nonempty line doesn't look like a version.
|
|
167
|
+
*/
|
|
168
|
+
export function parseVersionLine(stdout) {
|
|
169
|
+
const firstLine = stdout.split(/\r?\n/).find((line) => line.trim().length > 0);
|
|
170
|
+
if (firstLine === undefined)
|
|
171
|
+
return undefined;
|
|
172
|
+
const trimmed = firstLine.trim().replace(/^v/, "");
|
|
173
|
+
const match = trimmed.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/);
|
|
174
|
+
if (match === null)
|
|
175
|
+
return undefined;
|
|
176
|
+
return {
|
|
177
|
+
raw: trimmed,
|
|
178
|
+
major: Number.parseInt(match[1], 10),
|
|
179
|
+
minor: Number.parseInt(match[2], 10),
|
|
180
|
+
patch: Number.parseInt(match[3], 10),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function isInTestedRange(major, minor) {
|
|
184
|
+
return KIMI_TESTED_MINORS.some((entry) => entry.major === major && entry.minor === minor);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The newest `{major, minor}` in the legacy-v1 tested set. Native-v2-certified
|
|
188
|
+
* versions lie outside this table. Set membership already warns for anything
|
|
189
|
+
* outside the legacy range; this upper bound lets the warning distinguish a
|
|
190
|
+
* newer, unverified legacy release from a below/gap version.
|
|
191
|
+
*/
|
|
192
|
+
export function maxTestedMinor() {
|
|
193
|
+
return KIMI_TESTED_MINORS.reduce((max, entry) => entry.major > max.major || (entry.major === max.major && entry.minor > max.minor)
|
|
194
|
+
? entry
|
|
195
|
+
: max);
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Format a user-facing warning line for an out-of-range version probe.
|
|
199
|
+
* Setup itself may complete, but the execution-plan gate will refuse model
|
|
200
|
+
* spawns until the operator selects a certified binary or updates the plugin.
|
|
201
|
+
*/
|
|
202
|
+
export function formatVersionOutOfRangeWarning(probe, pluginVersion) {
|
|
203
|
+
const tested = KIMI_TESTED_MINORS.map((entry) => `${entry.major}.${entry.minor}.x`).join(", ");
|
|
204
|
+
const max = maxTestedMinor();
|
|
205
|
+
const aboveMax = probe.major > max.major || (probe.major === max.major && probe.minor > max.minor);
|
|
206
|
+
const lines = [
|
|
207
|
+
`WARNING: kimi-code version ${probe.version} is outside the range kimi-plugin-cc ${pluginVersion} was tested against (${tested}).`,
|
|
208
|
+
];
|
|
209
|
+
if (aboveMax) {
|
|
210
|
+
// Above the legacy-v1 tested upper bound is a release newer than the last
|
|
211
|
+
// legacy compatibility audit, commonly from out-of-band auto-upgrade drift.
|
|
212
|
+
lines.push(` This is NEWER than the newest version we have tested (${max.major}.${max.minor}.x) and is not yet certified for model execution.`);
|
|
213
|
+
}
|
|
214
|
+
lines.push(` Setup can complete, but model-spawning commands will refuse before Kimi starts rather than run an unreviewed engine/version route.`, ` Update kimi-plugin-cc to a release that certifies this minor, or point KIMI_PLUGIN_CC_KIMI_BIN at a certified kimi-code binary and retry setup.`, ` KIMI_PLUGIN_CC_SKIP_VERSION_PROBE is a test/smoke seam, not a production compatibility override.`);
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export async function writeInvocationLogHeader(logPath, metadata) {
|
|
4
|
+
const line = `${JSON.stringify({
|
|
5
|
+
ts: new Date().toISOString(),
|
|
6
|
+
direction: "meta",
|
|
7
|
+
message: metadata,
|
|
8
|
+
})}\n`;
|
|
9
|
+
await mkdir(path.dirname(logPath), { recursive: true });
|
|
10
|
+
try {
|
|
11
|
+
await appendFile(logPath, line, "utf8");
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code === "ENOENT") {
|
|
15
|
+
// Directory vanished between mkdir and appendFile — recreate and retry once.
|
|
16
|
+
await mkdir(path.dirname(logPath), { recursive: true });
|
|
17
|
+
await appendFile(logPath, line, "utf8");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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
|
+
}
|