kimi-plugin-cc 2.4.5 → 2.5.2

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 (114) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +103 -29
  5. package/dist/commands/setup.js +78 -51
  6. package/dist/dsh/companion.js +177 -0
  7. package/dist/dsh/index.js +169 -0
  8. package/dist/hooks/install-paths.js +2 -2
  9. package/dist/hooks/managed-block.js +86 -20
  10. package/dist/version.js +1 -1
  11. package/docs/migration.md +4 -2
  12. package/docs/models.md +5 -1
  13. package/docs/registry-releases.md +84 -20
  14. package/docs/safety.md +25 -10
  15. package/docs/upstream-2.4.1-certification.md +6 -0
  16. package/package.json +75 -8
  17. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  18. package/plugins/kimi-codex/dist/commands/setup.js +78 -51
  19. package/plugins/kimi-codex/dist/dsh/companion.js +177 -0
  20. package/plugins/kimi-codex/dist/dsh/index.js +169 -0
  21. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  22. package/plugins/kimi-codex/dist/hooks/managed-block.js +86 -20
  23. package/plugins/kimi-codex/dist/version.js +1 -1
  24. package/plugins/kimi-dsh/LICENSE +201 -0
  25. package/plugins/kimi-dsh/README.md +83 -0
  26. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  27. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  28. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  29. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  30. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  31. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  32. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  33. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  34. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  35. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  36. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  37. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  38. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  39. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  40. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  41. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  42. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  43. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  44. package/plugins/kimi-dsh/dist/commands/setup.js +1074 -0
  45. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  46. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  47. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  48. package/plugins/kimi-dsh/dist/companion.js +97 -0
  49. package/plugins/kimi-dsh/dist/config.js +22 -0
  50. package/plugins/kimi-dsh/dist/dsh/companion.js +177 -0
  51. package/plugins/kimi-dsh/dist/dsh/index.js +169 -0
  52. package/plugins/kimi-dsh/dist/errors.js +40 -0
  53. package/plugins/kimi-dsh/dist/git.js +224 -0
  54. package/plugins/kimi-dsh/dist/help.js +43 -0
  55. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  56. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  57. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  58. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  59. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  60. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  61. package/plugins/kimi-dsh/dist/hooks/managed-block.js +685 -0
  62. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  63. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  64. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  65. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  66. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  67. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  68. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  69. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  70. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  71. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  72. package/plugins/kimi-dsh/dist/logging.js +22 -0
  73. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  74. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  75. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  76. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  77. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  78. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  79. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  80. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  81. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  82. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  83. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  84. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  85. package/plugins/kimi-dsh/dist/package.json +1 -0
  86. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  87. package/plugins/kimi-dsh/dist/paths.js +27 -0
  88. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  89. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  90. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  91. package/plugins/kimi-dsh/dist/render.js +177 -0
  92. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  93. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  94. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  95. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  96. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  97. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  98. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  99. package/plugins/kimi-dsh/dist/types.js +1 -0
  100. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  101. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  102. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  103. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  104. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  105. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  106. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  107. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  111. package/plugins/kimi-dsh/dist/version.js +7 -0
  112. package/plugins/kimi-dsh/package.json +78 -0
  113. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  114. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,177 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { brandString } from "@deepseek-ai/dsh-brand";
3
+ import { assertUsableCwd, settleRunResult, subprocessRunHandle, } from "@deepseek-ai/dsh-subagent";
4
+ import { preferStableNodePath } from "../hooks/install-paths.js";
5
+ export const DSH_COMPANION_OPERATIONS = [
6
+ "ask",
7
+ "review",
8
+ "challenge",
9
+ "rescue",
10
+ "pursue",
11
+ "swarm",
12
+ "swarm-write",
13
+ ];
14
+ export function companionTextTask(prompt) {
15
+ if (prompt.length === 0 || prompt.some((block) => block.type !== "text")) {
16
+ throw new Error("kimi-plugin-cc: a delegation must contain only text blocks");
17
+ }
18
+ const text = prompt.map((block) => block.type === "text" ? block.text : "").join("\n\n");
19
+ if (text.trim().length === 0) {
20
+ throw new Error("kimi-plugin-cc: a delegation must not be empty");
21
+ }
22
+ return text;
23
+ }
24
+ export function companionArgv(operation, prompt, companionPath, nodeBin = preferStableNodePath(process.execPath), options = {}) {
25
+ const command = operation === "ask" || operation === "review"
26
+ ? [operation]
27
+ : operation === "swarm-write"
28
+ ? ["task", "swarm", "--write"]
29
+ : ["task", operation];
30
+ return [
31
+ nodeBin,
32
+ companionPath,
33
+ ...command,
34
+ ...(operation === "ask" || operation === "rescue" ? ["--fresh"] : []),
35
+ ...(options.model === undefined ? [] : ["-m", options.model]),
36
+ ...(options.thinkingEffort === undefined ? [] : ["--thinking", options.thinkingEffort]),
37
+ ...(options.budget === undefined ? [] : ["--budget", options.budget]),
38
+ ...((operation === "review" || operation === "challenge") && options.reviewBase !== undefined
39
+ ? ["--base", options.reviewBase]
40
+ : []),
41
+ "--",
42
+ prompt,
43
+ ];
44
+ }
45
+ export function companionEnvironment(dataRoot, cwd, env, nodeBin = preferStableNodePath(process.execPath)) {
46
+ return {
47
+ ...env,
48
+ KIMI_PLUGIN_CC_DATA: dataRoot,
49
+ KIMI_PLUGIN_CC_HOST_ID: "dsh",
50
+ KIMI_PLUGIN_CC_NODE_BIN: nodeBin,
51
+ KIMI_PLUGIN_CC_WORKSPACE_CWD: cwd,
52
+ CLAUDE_PLUGIN_DATA: undefined,
53
+ PLUGIN_DATA: undefined,
54
+ CLAUDE_PLUGIN_ROOT: undefined,
55
+ PLUGIN_ROOT: undefined,
56
+ KIMI_PLUGIN_CC_SHELL_LAUNCH: undefined,
57
+ KIMI_PLUGIN_CC_MCP_WORKSPACE: undefined,
58
+ KIMI_PLUGIN_CC_WORKSPACE_ROOT: undefined,
59
+ KIMI_PLUGIN_CC_HOOK_SCRIPT: undefined,
60
+ KIMI_PLUGIN_CC_SKIP_HOOK_CHECK: undefined,
61
+ KIMI_PLUGIN_CC_SKIP_VERSION_PROBE: undefined,
62
+ KIMI_CODE_TRUST_WORKSPACE: undefined,
63
+ };
64
+ }
65
+ export async function startCompanionRun(request, operation, spec) {
66
+ const prompt = companionTextTask(request.prompt);
67
+ if (request.signal.aborted) {
68
+ throw new Error("kimi-plugin-cc: delegation was aborted before companion startup");
69
+ }
70
+ const cwd = assertUsableCwd("kimi-plugin-cc", "child cwd", request.cwd ?? process.cwd());
71
+ const termination = new AbortController();
72
+ let cancelled = false;
73
+ const requestCancel = () => {
74
+ cancelled = true;
75
+ if (!termination.signal.aborted) {
76
+ termination.abort(new Error("kimi-plugin-cc: delegation cancelled"));
77
+ }
78
+ };
79
+ const onAbort = () => requestCancel();
80
+ request.signal.addEventListener("abort", onAbort, { once: true });
81
+ let child;
82
+ try {
83
+ const nodeBin = preferStableNodePath(process.execPath);
84
+ child = spec.spawn({
85
+ argv: companionArgv(operation, prompt, spec.companionPath, nodeBin, spec),
86
+ cwd,
87
+ stdio: {
88
+ stdin: "ignore",
89
+ stdout: { maxBytes: spec.maxOutputBytes },
90
+ stderr: "inherit",
91
+ },
92
+ graceMs: spec.disposeGraceMs,
93
+ signal: termination.signal,
94
+ env: companionEnvironment(spec.dataRoot, cwd, spec.env, nodeBin),
95
+ });
96
+ }
97
+ catch (error) {
98
+ request.signal.removeEventListener("abort", onAbort);
99
+ throw error;
100
+ }
101
+ let diagnostic;
102
+ const result = settleRunResult({
103
+ attempt: async () => {
104
+ let outcome;
105
+ try {
106
+ outcome = await child.done;
107
+ }
108
+ catch (error) {
109
+ diagnostic = `Kimi ${operation} companion failed before producing a terminal exit`;
110
+ throw error;
111
+ }
112
+ const collected = child.collected.stdout?.readFrom(0);
113
+ const text = collected?.text.trimEnd() ?? "";
114
+ if (!cancelled && outcome.exitCode === 0 && collected && !collected.lossy && text.trim().length > 0) {
115
+ return { output: [{ type: "text", text }], stopReason: "completed" };
116
+ }
117
+ const facts = [
118
+ `exit code: ${outcome.exitCode ?? "none"}`,
119
+ `signal: ${outcome.signal ?? "none"}`,
120
+ ...(collected?.lossy ? ["stdout truncated"] : []),
121
+ ...(text.trim().length === 0 ? ["stdout empty"] : []),
122
+ ];
123
+ diagnostic = `Kimi ${operation} companion failed (${facts.join("; ")})`;
124
+ return { output: [], diagnostic, stopReason: cancelled ? "aborted" : "error" };
125
+ },
126
+ collectOutput: () => [],
127
+ collectDiagnostic: () => diagnostic,
128
+ cancelled: () => cancelled,
129
+ signal: request.signal,
130
+ onAbort,
131
+ });
132
+ return subprocessRunHandle({
133
+ id: brandString(randomUUID()),
134
+ result,
135
+ signal: request.signal,
136
+ onAbort,
137
+ requestCancel,
138
+ teardown: async () => {
139
+ child.terminate();
140
+ await child.waitForExit();
141
+ await child.done.catch(() => { });
142
+ },
143
+ });
144
+ }
145
+ export async function runCompanionCommand(argv, cwd, spec, signal) {
146
+ const nodeBin = preferStableNodePath(process.execPath);
147
+ const child = spec.spawn({
148
+ argv: [nodeBin, spec.companionPath, ...argv],
149
+ cwd,
150
+ stdio: {
151
+ stdin: "ignore",
152
+ stdout: { maxBytes: spec.maxOutputBytes },
153
+ stderr: { maxBytes: spec.maxOutputBytes },
154
+ },
155
+ graceMs: spec.disposeGraceMs,
156
+ signal,
157
+ env: companionEnvironment(spec.dataRoot, cwd, spec.env, nodeBin),
158
+ });
159
+ try {
160
+ const outcome = await child.done;
161
+ await child.waitForExit();
162
+ const stdout = child.collected.stdout?.readFrom(0);
163
+ const stderr = child.collected.stderr?.readFrom(0);
164
+ return {
165
+ exitCode: outcome.exitCode,
166
+ signal: outcome.signal,
167
+ stdout: stdout?.text ?? "",
168
+ stderr: stderr?.text ?? "",
169
+ truncated: Boolean(stdout?.lossy || stderr?.lossy),
170
+ };
171
+ }
172
+ catch (error) {
173
+ child.terminate();
174
+ await child.waitForExit().catch(() => false);
175
+ throw error;
176
+ }
177
+ }
@@ -0,0 +1,169 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { CommandDefinitionId } from "@deepseek-ai/dsh-commands/brand";
5
+ import z from "@deepseek-ai/schemastery";
6
+ import { NO_START_CAPABILITIES, } from "@deepseek-ai/dsh-subagent";
7
+ import { DSH_COMPANION_OPERATIONS, runCompanionCommand, startCompanionRun, } from "./companion.js";
8
+ import { parseDurationMs } from "../parsing.js";
9
+ export const name = "kimi-plugin-cc";
10
+ export const inject = ["subagents", "subprocess"];
11
+ export const DSH_TOOL_GUIDANCE = [
12
+ "Kimi delegation tools have different write boundaries:",
13
+ "kimi_ask, kimi_review, and kimi_challenge are read-only; kimi_swarm is a read-only parallel review.",
14
+ "kimi_rescue and kimi_pursue can edit the selected workspace; kimi_pursue continues toward a bounded goal.",
15
+ "kimi_swarm_write edits a temporary worktree and returns a patch without applying it.",
16
+ ].join(" ");
17
+ const DEFAULT_DISPOSE_GRACE_MS = 6_000;
18
+ const DEFAULT_MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
19
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
20
+ const COMPANION_PATH = fileURLToPath(new URL("../../dist/companion.js", import.meta.url));
21
+ export const Config = z.object({
22
+ dataRoot: z.string().min(1),
23
+ env: z.dict(z.string()).default({}),
24
+ disposeGraceMs: z.number().default(DEFAULT_DISPOSE_GRACE_MS),
25
+ maxOutputBytes: z.number().default(DEFAULT_MAX_OUTPUT_BYTES),
26
+ model: z.string().min(1),
27
+ thinkingEffort: z.string().min(1),
28
+ budget: z.string().min(1),
29
+ reviewBase: z.string().min(1),
30
+ });
31
+ export function defaultDshDataRoot(env = process.env) {
32
+ const explicit = env.KIMI_PLUGIN_CC_DATA;
33
+ if (explicit !== undefined) {
34
+ if (!path.isAbsolute(explicit)) {
35
+ throw new Error("kimi-plugin-cc: KIMI_PLUGIN_CC_DATA must be absolute");
36
+ }
37
+ return path.normalize(explicit);
38
+ }
39
+ const dshHome = env.DSH_HOME ?? path.join(env.HOME || homedir(), ".dsh");
40
+ if (!path.isAbsolute(dshHome)) {
41
+ throw new Error("kimi-plugin-cc: DSH_HOME must be absolute");
42
+ }
43
+ return path.join(dshHome, "plugin-data", "kimi-plugin-cc");
44
+ }
45
+ function resolveConfig(config) {
46
+ const resolved = {
47
+ dataRoot: config.dataRoot ?? defaultDshDataRoot(),
48
+ env: config.env ?? {},
49
+ disposeGraceMs: config.disposeGraceMs ?? DEFAULT_DISPOSE_GRACE_MS,
50
+ maxOutputBytes: config.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES,
51
+ model: config.model,
52
+ thinkingEffort: config.thinkingEffort,
53
+ budget: config.budget,
54
+ reviewBase: config.reviewBase,
55
+ };
56
+ if (!path.isAbsolute(resolved.dataRoot)) {
57
+ throw new Error("kimi-plugin-cc: dataRoot must be absolute");
58
+ }
59
+ if (!Number.isFinite(resolved.disposeGraceMs) || resolved.disposeGraceMs < DEFAULT_DISPOSE_GRACE_MS || resolved.disposeGraceMs > MAX_TIMER_DELAY_MS) {
60
+ throw new Error(`kimi-plugin-cc: disposeGraceMs must be at least ${DEFAULT_DISPOSE_GRACE_MS} and no greater than ${MAX_TIMER_DELAY_MS}`);
61
+ }
62
+ if (!Number.isSafeInteger(resolved.maxOutputBytes) || resolved.maxOutputBytes <= 0) {
63
+ throw new Error("kimi-plugin-cc: maxOutputBytes must be a positive safe integer");
64
+ }
65
+ for (const [field, value] of [["model", resolved.model], ["thinkingEffort", resolved.thinkingEffort], ["reviewBase", resolved.reviewBase]]) {
66
+ if (value?.startsWith("-"))
67
+ throw new Error(`kimi-plugin-cc: ${field} must not start with '-'`);
68
+ }
69
+ if (resolved.budget !== undefined)
70
+ parseDurationMs(resolved.budget, "--budget", "dsh.config", "safety");
71
+ return resolved;
72
+ }
73
+ class KimiCompanionProvider {
74
+ name;
75
+ operation;
76
+ spec;
77
+ capabilities = NO_START_CAPABILITIES;
78
+ inheritsParentContext = false;
79
+ constructor(name, operation, spec) {
80
+ this.name = name;
81
+ this.operation = operation;
82
+ this.spec = spec;
83
+ }
84
+ start(request) {
85
+ return startCompanionRun(request, this.operation, this.spec);
86
+ }
87
+ }
88
+ function commandArguments(rawInput) {
89
+ const trimmed = rawInput.trim();
90
+ return trimmed.length === 0 ? [] : trimmed.split(/\s+/);
91
+ }
92
+ function commandText(result) {
93
+ return [result.stdout.trimEnd(), result.stderr.trimEnd()]
94
+ .filter((text) => text.length > 0)
95
+ .join("\n");
96
+ }
97
+ export function apply(ctx, config) {
98
+ const resolved = resolveConfig(config);
99
+ const spec = {
100
+ companionPath: COMPANION_PATH,
101
+ dataRoot: resolved.dataRoot,
102
+ env: resolved.env,
103
+ disposeGraceMs: resolved.disposeGraceMs,
104
+ maxOutputBytes: resolved.maxOutputBytes,
105
+ model: resolved.model,
106
+ thinkingEffort: resolved.thinkingEffort,
107
+ budget: resolved.budget,
108
+ reviewBase: resolved.reviewBase,
109
+ spawn: (spawnSpec) => ctx.subprocess.spawn(spawnSpec),
110
+ };
111
+ for (const operation of DSH_COMPANION_OPERATIONS) {
112
+ ctx.subagents.registerProvider(new KimiCompanionProvider(`kimi-${operation}`, operation, spec));
113
+ }
114
+ ctx.inject(["systemPrompt"], (promptCtx) => {
115
+ promptCtx.systemPrompt.section({
116
+ name: "kimi-plugin-cc:tool-boundaries",
117
+ order: promptCtx.systemPrompt.getSectionOrder("TOOL_SUBAGENT"),
118
+ text: () => DSH_TOOL_GUIDANCE,
119
+ });
120
+ });
121
+ ctx.inject(["commands"], (commandCtx) => {
122
+ const execute = async (invocation, argv) => {
123
+ const directories = commandCtx.get("workingDirectory");
124
+ if (directories === undefined) {
125
+ return { kind: "error", text: "Kimi commands require the DSH working-directory service." };
126
+ }
127
+ const cwd = await directories.ensure(invocation.agent, invocation.signal);
128
+ const result = await runCompanionCommand(argv, cwd, spec, invocation.signal);
129
+ const text = commandText(result);
130
+ if (result.truncated) {
131
+ return { kind: "error", text: `${text}\nKimi command output exceeded the configured byte limit.`.trim() };
132
+ }
133
+ if (result.exitCode !== 0) {
134
+ return {
135
+ kind: "error",
136
+ text: `${text}\nKimi command failed (exit code: ${result.exitCode ?? "none"}; signal: ${result.signal ?? "none"}).`.trim(),
137
+ };
138
+ }
139
+ return { kind: "success", text: text.length > 0 ? text : "Kimi command completed." };
140
+ };
141
+ commandCtx.commands.register({
142
+ definitionId: CommandDefinitionId("kimi-plugin-cc/dsh/setup"),
143
+ name: "kimi-setup",
144
+ description: "Install, check, or remove the DSH-owned Kimi PreToolUse hook; list configured models",
145
+ input: { hint: "[--check|--uninstall|--models [--json]]" },
146
+ recordInput: false,
147
+ handler: (invocation) => {
148
+ const args = commandArguments(invocation.rawInput);
149
+ const valid = args.length === 0
150
+ || (args.length === 1 && ["--check", "--uninstall", "--models"].includes(args[0]))
151
+ || (args.length === 2 && args[0] === "--models" && args[1] === "--json");
152
+ if (!valid) {
153
+ return { kind: "error", text: "Usage: /kimi-setup [--check|--uninstall|--models [--json]]" };
154
+ }
155
+ return execute(invocation, ["setup", ...args]);
156
+ },
157
+ });
158
+ for (const command of ["status", "result", "cancel"]) {
159
+ commandCtx.commands.register({
160
+ definitionId: CommandDefinitionId(`kimi-plugin-cc/dsh/${command}`),
161
+ name: `kimi-${command}`,
162
+ description: `${command === "cancel" ? "Cancel" : command === "status" ? "Inspect" : "Return"} a companion Kimi job`,
163
+ input: { hint: "[job-id]" },
164
+ recordInput: false,
165
+ handler: (invocation) => execute(invocation, [command, ...commandArguments(invocation.rawInput)]),
166
+ });
167
+ }
168
+ });
169
+ }
@@ -410,7 +410,7 @@ export function hostIdFromHookCommand(command) {
410
410
  }
411
411
  /**
412
412
  * True only for a canonical two-single-quoted-token command whose script is
413
- * `approval-hook.js` under a kimi-plugin-cc/kimi-marketplace install tree.
413
+ * `approval-hook.js` under a kimi-plugin-cc/dsh-kimi-plugin-cc/kimi-marketplace install tree.
414
414
  * This strict ownership test protects hand-rolled and non-canonical user hooks
415
415
  * from orphan pruning.
416
416
  */
@@ -424,5 +424,5 @@ export function isOurApprovalHookCommand(decodedCommand) {
424
424
  const normalized = parsed.hookScript.replace(/\\/g, "/");
425
425
  // Require a real path segment, not an arbitrary substring, so a user hook in
426
426
  // a similarly named wrapper directory is never misclassified and pruned.
427
- return normalized.includes("/kimi-plugin-cc/") || normalized.includes("/kimi-marketplace/");
427
+ return normalized.includes("/kimi-plugin-cc/") || normalized.includes("/dsh-kimi-plugin-cc/") || normalized.includes("/kimi-marketplace/");
428
428
  }
@@ -45,10 +45,80 @@ const MATCHER_LINE_RE = /^matcher\s*=/;
45
45
  /** `timeout = <int>` — the only other key our managed [[hooks]] table emits. */
46
46
  const TIMEOUT_LINE_RE = /^timeout\s*=\s*\d+\s*$/;
47
47
  const HOOKS_TABLE_RE = /^\[\[hooks\]\]\s*$/;
48
+ // Recognize comment lines lexically, not text inside TOML string values.
49
+ // Whole-document parsing remains the authority for TOML validity.
50
+ function scanTomlLines(contents) {
51
+ const markers = new Set();
52
+ const tables = new Set();
53
+ let state = "plain";
54
+ let line = 0;
55
+ let blankPrefix = true;
56
+ for (let i = 0; i < contents.length; i += 1) {
57
+ const c = contents[i];
58
+ if (c === "\n") {
59
+ line += 1;
60
+ blankPrefix = true;
61
+ continue;
62
+ }
63
+ const onlyWhitespaceBefore = blankPrefix;
64
+ if (c !== " " && c !== "\t" && c !== "\r")
65
+ blankPrefix = false;
66
+ if (state === "plain") {
67
+ if (c === "[" && onlyWhitespaceBefore)
68
+ tables.add(line);
69
+ if (c === "#") {
70
+ if (onlyWhitespaceBefore) {
71
+ const end = contents.indexOf("\n", i);
72
+ const comment = contents.slice(i, end < 0 ? contents.length : end).trim();
73
+ if (BEGIN_LINE_RE.test(comment) || END_LINE_RE.test(comment))
74
+ markers.add(line);
75
+ }
76
+ const end = contents.indexOf("\n", i);
77
+ if (end < 0)
78
+ break;
79
+ i = end - 1;
80
+ }
81
+ else if (c === '"' || c === "'") {
82
+ const triple = contents.slice(i, i + 3) === c.repeat(3);
83
+ state = c === '"' ? (triple ? "multibasic" : "basic") : (triple ? "multiliteral" : "literal");
84
+ if (triple)
85
+ i += 2;
86
+ }
87
+ }
88
+ else {
89
+ const basic = state === "basic" || state === "multibasic";
90
+ const quote = basic ? '"' : "'";
91
+ if (basic && c === "\\") {
92
+ if (contents[i + 1] === "\n") {
93
+ line += 1;
94
+ blankPrefix = true;
95
+ }
96
+ i += 1;
97
+ continue;
98
+ }
99
+ if (c !== quote)
100
+ continue;
101
+ if (state === "basic" || state === "literal")
102
+ state = "plain";
103
+ else if (contents.slice(i, i + 3) === quote.repeat(3)) {
104
+ while (contents[i + 1] === quote)
105
+ i += 1;
106
+ state = "plain";
107
+ }
108
+ }
109
+ }
110
+ return { markers, tables };
111
+ }
112
+ export function markerCommentLines(contents) {
113
+ return scanTomlLines(contents).markers;
114
+ }
48
115
  /** Locate every BEGIN/END marker line, in order, with its parsed host id. */
49
116
  function findMarkers(lines) {
50
117
  const hits = [];
118
+ const comments = markerCommentLines(lines.join("\n"));
51
119
  for (let i = 0; i < lines.length; i += 1) {
120
+ if (!comments.has(i))
121
+ continue;
52
122
  const trimmed = lines[i].trim();
53
123
  const begin = BEGIN_LINE_RE.exec(trimmed);
54
124
  if (begin !== null) {
@@ -66,7 +136,7 @@ function findMarkers(lines) {
66
136
  }
67
137
  /**
68
138
  * Pair BEGIN/END markers into well-formed blocks. Return the first orphan
69
- * (BEGIN-without-END or END-without-BEGIN); either is a hard error for install.
139
+ * (BEGIN-without-END or END-without-BEGIN); verification fails closed on either.
70
140
  */
71
141
  function pairBlocks(lines) {
72
142
  const markers = findMarkers(lines);
@@ -83,6 +153,8 @@ function pairBlocks(lines) {
83
153
  // end
84
154
  if (open === null)
85
155
  return { orphan: "END-without-BEGIN" };
156
+ if (open.hostId !== marker.hostId)
157
+ return { orphan: "BEGIN-without-END" };
86
158
  const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
87
159
  blocks.push({
88
160
  hostId: open.hostId,
@@ -436,13 +508,13 @@ export function evaluateInstalled(contents, expectedCommand, opts) {
436
508
  * The deletion range `[start, end)` is conservative: only the contiguous
437
509
  * non-blank body after `[[hooks]]`, stopping at a blank line, table header, or
438
510
  * managed marker so adjacent user content is preserved. A separate full-span
439
- * scan rejects matcher lines recognized by MATCHER_LINE_RE to avoid half-cutting
440
- * a table. That scan is deliberately heuristic and does not claim to recognize
441
- * every quoted or dotted TOML matcher spelling. `command` is decoded for the
442
- * caller's ownership checks.
511
+ * scan refuses any further TOML field before the next table, including quoted
512
+ * and dotted keys, so pruning cannot attach those fields to another table.
513
+ * `command` is decoded for the caller's ownership checks.
443
514
  */
444
515
  export function findBareApprovalHookTables(contents) {
445
516
  const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
517
+ const tableLines = scanTomlLines(contents).tables;
446
518
  const managed = new Set();
447
519
  const paired = pairBlocks(lines);
448
520
  if ("blocks" in paired) {
@@ -455,7 +527,7 @@ export function findBareApprovalHookTables(contents) {
455
527
  for (let i = 0; i < lines.length; i += 1) {
456
528
  if (managed.has(i))
457
529
  continue;
458
- if (!HOOKS_TABLE_RE.test(lines[i].trim()))
530
+ if (!tableLines.has(i) || !HOOKS_TABLE_RE.test(lines[i].trim()))
459
531
  continue;
460
532
  // Collect the contiguous table body.
461
533
  let j = i + 1;
@@ -500,22 +572,16 @@ export function findBareApprovalHookTables(contents) {
500
572
  }
501
573
  j += 1;
502
574
  }
503
- // A blank line does NOT end a TOML table; membership continues to the next
504
- // header or EOF. The deletion range nevertheless stops at the blank to
505
- // preserve adjacent user content. Scan the remaining span for matcher lines
506
- // this scanner recognizes and reject the table rather than half-cutting it:
507
- // matcher is schema-valid but can disable the hook at runtime. Foreign keys
508
- // and other TOML spellings are handled by whole-file schema validation and
509
- // the parser-based installed checks, not by this prune heuristic.
510
- let matcherBeyondBody = false;
575
+ // Blank lines and marker comments do not end a TOML table. Never leave any
576
+ // field behind to be rebound to a preceding table after pruning, regardless
577
+ // of the field's spelling or whether it is valid for a hook.
578
+ let fieldBeyondBody = false;
511
579
  for (let k = j; k < lines.length; k += 1) {
512
580
  const t = lines[k].trim();
513
- // The current table's TOML span ends at the next table header or a
514
- // managed marker; a matcher past that point belongs to a different table.
515
- if (t.startsWith("[") || BEGIN_LINE_RE.test(t) || END_LINE_RE.test(t))
581
+ if (tableLines.has(k))
516
582
  break;
517
- if (MATCHER_LINE_RE.test(t)) {
518
- matcherBeyondBody = true;
583
+ if (t.length > 0 && !t.startsWith("#")) {
584
+ fieldBeyondBody = true;
519
585
  break;
520
586
  }
521
587
  }
@@ -527,7 +593,7 @@ export function findBareApprovalHookTables(contents) {
527
593
  commandDecoded.length > 0 &&
528
594
  hasPreToolUseEvent &&
529
595
  !hasMatcher &&
530
- !matcherBeyondBody) {
596
+ !fieldBeyondBody) {
531
597
  ranges.push({ start: i, end: j, command: commandDecoded });
532
598
  }
533
599
  }
@@ -4,4 +4,4 @@
4
4
  // and AGENTS.md on every release. A future improvement would read this from
5
5
  // package.json at build time, but that adds a build-step dependency we
6
6
  // don't want yet.
7
- export const KIMI_PLUGIN_CC_VERSION = "2.4.5";
7
+ export const KIMI_PLUGIN_CC_VERSION = "2.5.2";