kimi-plugin-cc 2.0.7

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 (190) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/.claude-plugin/marketplace.json +16 -0
  3. package/.claude-plugin/plugin.json +32 -0
  4. package/CHANGELOG.md +1096 -0
  5. package/LICENSE +201 -0
  6. package/README.md +772 -0
  7. package/agents/kimi-ask.md +82 -0
  8. package/agents/kimi-challenge.md +82 -0
  9. package/agents/kimi-pursue.md +91 -0
  10. package/agents/kimi-rescue.md +85 -0
  11. package/agents/kimi-review.md +82 -0
  12. package/agents/kimi-swarm-write.md +92 -0
  13. package/agents/kimi-swarm.md +83 -0
  14. package/commands/README.md +16 -0
  15. package/commands/ask.md +32 -0
  16. package/commands/cancel.md +17 -0
  17. package/commands/challenge.md +29 -0
  18. package/commands/pursue.md +37 -0
  19. package/commands/replay.md +13 -0
  20. package/commands/rescue.md +32 -0
  21. package/commands/result.md +13 -0
  22. package/commands/review.md +29 -0
  23. package/commands/setup.md +23 -0
  24. package/commands/status.md +13 -0
  25. package/commands/swarm.md +38 -0
  26. package/dist/background-spawn.js +224 -0
  27. package/dist/cli-cancellation.js +43 -0
  28. package/dist/cli-client.js +845 -0
  29. package/dist/commands/ask.js +327 -0
  30. package/dist/commands/cancel.js +178 -0
  31. package/dist/commands/cli-helpers.js +112 -0
  32. package/dist/commands/models.js +140 -0
  33. package/dist/commands/not-implemented.js +4 -0
  34. package/dist/commands/pursue.js +311 -0
  35. package/dist/commands/registry.js +87 -0
  36. package/dist/commands/repair-sessions.js +159 -0
  37. package/dist/commands/replay.js +123 -0
  38. package/dist/commands/rescue.js +333 -0
  39. package/dist/commands/result.js +58 -0
  40. package/dist/commands/review-gate.js +393 -0
  41. package/dist/commands/review.js +218 -0
  42. package/dist/commands/setup.js +1132 -0
  43. package/dist/commands/status.js +27 -0
  44. package/dist/commands/swarm.js +576 -0
  45. package/dist/commands/worker.js +31 -0
  46. package/dist/companion.js +114 -0
  47. package/dist/config.js +22 -0
  48. package/dist/errors.js +40 -0
  49. package/dist/git.js +215 -0
  50. package/dist/hooks/approval-hook.js +112 -0
  51. package/dist/hooks/approval-policy.js +254 -0
  52. package/dist/hooks/config-safety.js +637 -0
  53. package/dist/hooks/install-paths.js +504 -0
  54. package/dist/hooks/install.js +281 -0
  55. package/dist/hooks/managed-block.js +745 -0
  56. package/dist/hooks/review-gate-stop.js +19 -0
  57. package/dist/job-store.js +559 -0
  58. package/dist/jobs.js +202 -0
  59. package/dist/kimi-command.js +72 -0
  60. package/dist/kimi-engine.js +624 -0
  61. package/dist/kimi-errors.js +220 -0
  62. package/dist/kimi-home.js +9 -0
  63. package/dist/kimi-timeouts.js +68 -0
  64. package/dist/kimi-version-probe.js +1410 -0
  65. package/dist/logging.js +22 -0
  66. package/dist/native-v2-preflight.js +417 -0
  67. package/dist/package.json +1 -0
  68. package/dist/parsing.js +553 -0
  69. package/dist/paths.js +27 -0
  70. package/dist/plugin-data.js +98 -0
  71. package/dist/process-tree.js +448 -0
  72. package/dist/render.js +159 -0
  73. package/dist/rescue-approval.js +951 -0
  74. package/dist/schemas/review-gate-output.js +43 -0
  75. package/dist/session-title.js +293 -0
  76. package/dist/stream-json.js +498 -0
  77. package/dist/types.js +1 -0
  78. package/dist/vendor/shell-quote/LICENSE +24 -0
  79. package/dist/vendor/shell-quote/index.js +1 -0
  80. package/dist/vendor/shell-quote/parse.js +229 -0
  81. package/dist/vendor/smol-toml/LICENSE +24 -0
  82. package/dist/vendor/smol-toml/date.js +127 -0
  83. package/dist/vendor/smol-toml/error.js +63 -0
  84. package/dist/vendor/smol-toml/extract.js +100 -0
  85. package/dist/vendor/smol-toml/parse.js +148 -0
  86. package/dist/vendor/smol-toml/primitive.js +179 -0
  87. package/dist/vendor/smol-toml/struct.js +184 -0
  88. package/dist/vendor/smol-toml/util.js +113 -0
  89. package/dist/version.js +7 -0
  90. package/docs/migration.md +215 -0
  91. package/docs/models.md +82 -0
  92. package/docs/registry-releases.md +70 -0
  93. package/docs/safety.md +318 -0
  94. package/docs/session-visibility.md +32 -0
  95. package/hooks/README.md +9 -0
  96. package/hooks/hooks.json +16 -0
  97. package/package.json +67 -0
  98. package/plugins/kimi-codex/.codex-plugin/plugin.json +39 -0
  99. package/plugins/kimi-codex/dist/background-spawn.js +224 -0
  100. package/plugins/kimi-codex/dist/cli-cancellation.js +43 -0
  101. package/plugins/kimi-codex/dist/cli-client.js +845 -0
  102. package/plugins/kimi-codex/dist/commands/ask.js +327 -0
  103. package/plugins/kimi-codex/dist/commands/cancel.js +178 -0
  104. package/plugins/kimi-codex/dist/commands/cli-helpers.js +112 -0
  105. package/plugins/kimi-codex/dist/commands/models.js +140 -0
  106. package/plugins/kimi-codex/dist/commands/not-implemented.js +4 -0
  107. package/plugins/kimi-codex/dist/commands/pursue.js +311 -0
  108. package/plugins/kimi-codex/dist/commands/registry.js +87 -0
  109. package/plugins/kimi-codex/dist/commands/repair-sessions.js +159 -0
  110. package/plugins/kimi-codex/dist/commands/replay.js +123 -0
  111. package/plugins/kimi-codex/dist/commands/rescue.js +333 -0
  112. package/plugins/kimi-codex/dist/commands/result.js +58 -0
  113. package/plugins/kimi-codex/dist/commands/review-gate.js +393 -0
  114. package/plugins/kimi-codex/dist/commands/review.js +218 -0
  115. package/plugins/kimi-codex/dist/commands/setup.js +1132 -0
  116. package/plugins/kimi-codex/dist/commands/status.js +27 -0
  117. package/plugins/kimi-codex/dist/commands/swarm.js +576 -0
  118. package/plugins/kimi-codex/dist/commands/worker.js +31 -0
  119. package/plugins/kimi-codex/dist/companion.js +114 -0
  120. package/plugins/kimi-codex/dist/config.js +22 -0
  121. package/plugins/kimi-codex/dist/errors.js +40 -0
  122. package/plugins/kimi-codex/dist/git.js +215 -0
  123. package/plugins/kimi-codex/dist/hooks/approval-hook.js +112 -0
  124. package/plugins/kimi-codex/dist/hooks/approval-policy.js +254 -0
  125. package/plugins/kimi-codex/dist/hooks/config-safety.js +637 -0
  126. package/plugins/kimi-codex/dist/hooks/install-paths.js +504 -0
  127. package/plugins/kimi-codex/dist/hooks/install.js +281 -0
  128. package/plugins/kimi-codex/dist/hooks/managed-block.js +745 -0
  129. package/plugins/kimi-codex/dist/hooks/review-gate-stop.js +19 -0
  130. package/plugins/kimi-codex/dist/job-store.js +559 -0
  131. package/plugins/kimi-codex/dist/jobs.js +202 -0
  132. package/plugins/kimi-codex/dist/kimi-command.js +72 -0
  133. package/plugins/kimi-codex/dist/kimi-engine.js +624 -0
  134. package/plugins/kimi-codex/dist/kimi-errors.js +220 -0
  135. package/plugins/kimi-codex/dist/kimi-home.js +9 -0
  136. package/plugins/kimi-codex/dist/kimi-timeouts.js +68 -0
  137. package/plugins/kimi-codex/dist/kimi-version-probe.js +1410 -0
  138. package/plugins/kimi-codex/dist/logging.js +22 -0
  139. package/plugins/kimi-codex/dist/native-v2-preflight.js +417 -0
  140. package/plugins/kimi-codex/dist/package.json +1 -0
  141. package/plugins/kimi-codex/dist/parsing.js +553 -0
  142. package/plugins/kimi-codex/dist/paths.js +27 -0
  143. package/plugins/kimi-codex/dist/plugin-data.js +98 -0
  144. package/plugins/kimi-codex/dist/process-tree.js +448 -0
  145. package/plugins/kimi-codex/dist/render.js +159 -0
  146. package/plugins/kimi-codex/dist/rescue-approval.js +951 -0
  147. package/plugins/kimi-codex/dist/schemas/review-gate-output.js +43 -0
  148. package/plugins/kimi-codex/dist/session-title.js +293 -0
  149. package/plugins/kimi-codex/dist/stream-json.js +498 -0
  150. package/plugins/kimi-codex/dist/types.js +1 -0
  151. package/plugins/kimi-codex/dist/vendor/shell-quote/LICENSE +24 -0
  152. package/plugins/kimi-codex/dist/vendor/shell-quote/index.js +1 -0
  153. package/plugins/kimi-codex/dist/vendor/shell-quote/parse.js +229 -0
  154. package/plugins/kimi-codex/dist/vendor/smol-toml/LICENSE +24 -0
  155. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +127 -0
  156. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +63 -0
  157. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +100 -0
  158. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +148 -0
  159. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +179 -0
  160. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +184 -0
  161. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +113 -0
  162. package/plugins/kimi-codex/dist/version.js +7 -0
  163. package/plugins/kimi-codex/scripts/companion.sh +37 -0
  164. package/plugins/kimi-codex/scripts/review-gate-hook.sh +32 -0
  165. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +31 -0
  166. package/plugins/kimi-codex/skills/kimi-ask/agents/openai.yaml +6 -0
  167. package/plugins/kimi-codex/skills/kimi-cancel/SKILL.md +25 -0
  168. package/plugins/kimi-codex/skills/kimi-cancel/agents/openai.yaml +6 -0
  169. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +31 -0
  170. package/plugins/kimi-codex/skills/kimi-challenge/agents/openai.yaml +6 -0
  171. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +30 -0
  172. package/plugins/kimi-codex/skills/kimi-pursue/agents/openai.yaml +6 -0
  173. package/plugins/kimi-codex/skills/kimi-replay/SKILL.md +25 -0
  174. package/plugins/kimi-codex/skills/kimi-replay/agents/openai.yaml +6 -0
  175. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +30 -0
  176. package/plugins/kimi-codex/skills/kimi-rescue/agents/openai.yaml +6 -0
  177. package/plugins/kimi-codex/skills/kimi-result/SKILL.md +25 -0
  178. package/plugins/kimi-codex/skills/kimi-result/agents/openai.yaml +6 -0
  179. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +31 -0
  180. package/plugins/kimi-codex/skills/kimi-review/agents/openai.yaml +6 -0
  181. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +32 -0
  182. package/plugins/kimi-codex/skills/kimi-setup/agents/openai.yaml +6 -0
  183. package/plugins/kimi-codex/skills/kimi-status/SKILL.md +25 -0
  184. package/plugins/kimi-codex/skills/kimi-status/agents/openai.yaml +6 -0
  185. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +31 -0
  186. package/plugins/kimi-codex/skills/kimi-swarm/agents/openai.yaml +6 -0
  187. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +32 -0
  188. package/plugins/kimi-codex/skills/kimi-swarm-write/agents/openai.yaml +6 -0
  189. package/scripts/companion.sh +37 -0
  190. package/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,504 @@
1
+ // Shared canonical-path helpers for the kimi-plugin-cc PreToolUse hook.
2
+ //
3
+ // Why a separate module:
4
+ //
5
+ // `runtime/commands/setup.ts` is the installer; `runtime/hooks/install.ts`
6
+ // is the per-call verifier. Audit (reports 27 + 28) found two
7
+ // convergent issues:
8
+ //
9
+ // 1. The verifier's drift gate was opt-in (callers had to pass
10
+ // `expectedHookPath`). rescue.ts called it WITHOUT the path, so
11
+ // a managed block referencing a stale hook script silently passed.
12
+ // kimi-code's spawn of the stale path exited 127 (or
13
+ // MODULE_NOT_FOUND), which the hook runner treats as ALLOW —
14
+ // rescue's workspace-bound allowlist bypassed in production.
15
+ //
16
+ // 2. Even when callers DID pass `expectedHookPath`, the verifier
17
+ // used `commandPath.includes(expectedHookPath)` (substring), so a
18
+ // crafted command like `true # /path/to/approval-hook.js` would
19
+ // pass: `/bin/sh -c "true # ..."` runs only `true` (exit 0,
20
+ // no-op allow), then kimi-code treats exit 0 as ALLOW.
21
+ //
22
+ // Fix: every verifier path now reconstructs the canonical expected
23
+ // shell command from the resolved Node binary + hook script path, and
24
+ // does EXACT equality. This module owns the single source of truth for
25
+ // how that command is built.
26
+ //
27
+ // Both setup.ts (write side) and install.ts (verify side) import from
28
+ // here. The probe in setup.ts also uses `buildHookShellCommand` so the
29
+ // shell probe runs the exact byte string the managed block writes.
30
+ // These three call sites cannot drift without a compile error.
31
+ import { createHash } from "node:crypto";
32
+ import { realpathSync } from "node:fs";
33
+ import path from "node:path";
34
+ import { fileURLToPath } from "node:url";
35
+ import { RuntimeError } from "../errors.js";
36
+ /**
37
+ * Prefer a non-version-stamped path to the SAME Node binary.
38
+ *
39
+ * `process.execPath` is fully symlink-resolved, so under Homebrew it is
40
+ * `/opt/homebrew/Cellar/node/<version>/bin/node` — a path that changes on
41
+ * every `brew upgrade node`. The managed block is verified by byte-exact
42
+ * equality, so that churn silently invalidates EVERY host's hook at once and
43
+ * forces a re-run of `/kimi:setup` with no user-visible cause (diagnosed
44
+ * 2026-07-25; a June config backup held `node/26.0.0` and `node/26.3.0`
45
+ * side by side, i.e. this had already fired before).
46
+ *
47
+ * `process.argv0` preserves the path the interpreter was actually invoked
48
+ * with, and `scripts/companion.sh` execs `$(command -v node)` — the stable
49
+ * `/opt/homebrew/bin/node` symlink. Prefer it, but ONLY when it provably
50
+ * denotes the same executable: absolute, and `realpath()`-identical to
51
+ * `process.execPath`. That guard is the whole safety argument — it can never
52
+ * select a DIFFERENT binary, only a more stable NAME for the one already
53
+ * running. Any doubt falls back to `execPath`, so the result is always an
54
+ * absolute path to a real interpreter.
55
+ *
56
+ * Side benefit: this keeps the hook and the companion on the same interpreter
57
+ * by construction, since companion.sh resolves `node` through that same
58
+ * stable name. The old behavior could diverge — companion running the new
59
+ * node while the hook stayed pinned to a Cellar path brew had already
60
+ * deleted, which spawns nothing and (exit 127) reads as ALLOW.
61
+ *
62
+ * Scope: helps symlink-based installs (Homebrew, and any manager exposing a
63
+ * stable `node`). nvm/asdf/mise expose version-stamped paths directly, so
64
+ * argv0 == execPath there and behavior is unchanged, not worse.
65
+ */
66
+ export function preferStableNodePath(execPath, argv0 = process.argv0, realpath = realpathSync) {
67
+ if (argv0 === undefined || argv0.length === 0 || !path.isAbsolute(argv0)) {
68
+ return execPath;
69
+ }
70
+ try {
71
+ return realpath(argv0) === realpath(execPath) ? argv0 : execPath;
72
+ }
73
+ catch {
74
+ return execPath;
75
+ }
76
+ }
77
+ /**
78
+ * Resolve the absolute path to the Node binary used in the PreToolUse
79
+ * hook command. kimi-code spawns hooks via `/bin/sh -c "<command>"`; a
80
+ * bare `node` would rely on the shell's PATH at execution time, which
81
+ * fails under GUI/LaunchAgent launches with sanitized PATH. Require an
82
+ * absolute path — either a stable alias of the in-process interpreter
83
+ * (see `preferStableNodePath`) or an explicit `KIMI_PLUGIN_CC_NODE_BIN`
84
+ * override.
85
+ */
86
+ export function resolveNodeBinary(env) {
87
+ const override = env.KIMI_PLUGIN_CC_NODE_BIN;
88
+ if (override === undefined || override.length === 0) {
89
+ return preferStableNodePath(process.execPath);
90
+ }
91
+ if (!path.isAbsolute(override)) {
92
+ throw new RuntimeError("SETUP_NODE_BIN_NOT_ABSOLUTE", [
93
+ `KIMI_PLUGIN_CC_NODE_BIN must be an absolute path; got ${JSON.stringify(override)}.`,
94
+ `kimi-code spawns hooks via /bin/sh -c, where a bare command relies on the shell's PATH at hook execution time.`,
95
+ `Use an absolute path so the hook keeps firing under sanitized-PATH launches.`,
96
+ ].join(" "), "setup.node-bin", { details: { override } });
97
+ }
98
+ return override;
99
+ }
100
+ /**
101
+ * Build the exact shell command string that `/bin/sh -c "<command>"`
102
+ * needs to spawn the hook. Single source of truth for:
103
+ *
104
+ * - what `/kimi:setup` writes into kimi-code's config
105
+ * (`command = "..."` inside [[hooks]])
106
+ * - what the shell probe runs via `spawn("/bin/sh", ["-c", ...])`
107
+ * - what the verifier (`evaluateInstalled`) equality-checks the
108
+ * installed `command = "..."` against on every command spawn
109
+ *
110
+ * Single-quoting both tokens means a path containing spaces or
111
+ * apostrophes round-trips cleanly. The probe and managed block ARE the
112
+ * same byte string — drift between them would break safety.
113
+ */
114
+ export function buildHookShellCommand(hookScriptPath, env) {
115
+ const nodeBin = resolveNodeBinary(env);
116
+ return `${shellSingleQuote(nodeBin)} ${shellSingleQuote(hookScriptPath)}`;
117
+ }
118
+ /**
119
+ * POSIX shell single-quote a string. Inner `'` are escaped as `'\''`
120
+ * (close-quote, escaped quote, re-open-quote). Always safe — no shell
121
+ * metacharacters survive the encoding.
122
+ */
123
+ export function shellSingleQuote(value) {
124
+ return `'${value.replaceAll("'", "'\\''")}'`;
125
+ }
126
+ /**
127
+ * Resolve the absolute path to the compiled hook script.
128
+ *
129
+ * Resolution order:
130
+ *
131
+ * 1. `KIMI_PLUGIN_CC_HOOK_SCRIPT` override — tests / advanced users.
132
+ * 2. Sibling resolution from this file's URL. This module lives at
133
+ * `<root>/{runtime,dist}/hooks/install-paths.{ts,js}`. The hook
134
+ * artifact lives at `<root>/dist/hooks/approval-hook.js`. Walk up
135
+ * to `<root>` and append the canonical hook artifact path.
136
+ */
137
+ export function resolveHookScriptPath(env) {
138
+ const override = env.KIMI_PLUGIN_CC_HOOK_SCRIPT;
139
+ if (override !== undefined && override.length > 0) {
140
+ if (!path.isAbsolute(override)) {
141
+ // kimi-code spawns hooks via `/bin/sh -c "<command>"` with a
142
+ // cwd that may not match the companion's. A relative path here
143
+ // would resolve against the kimi-code shell's working dir at
144
+ // hook execution time — different from the path resolved at
145
+ // install time. The mismatch would let the verifier bless a
146
+ // path that doesn't actually run. Match the NODE_BIN_NOT_ABSOLUTE
147
+ // contract by requiring an absolute override. Audit re-review
148
+ // (report 34 Codex MEDIUM) flagged this.
149
+ throw new RuntimeError("SETUP_HOOK_SCRIPT_NOT_ABSOLUTE", [
150
+ `KIMI_PLUGIN_CC_HOOK_SCRIPT must be an absolute path; got ${JSON.stringify(override)}.`,
151
+ `kimi-code spawns hooks via /bin/sh -c with a cwd that may differ from the companion's.`,
152
+ `Use an absolute path so the verifier and the runtime spawn refer to the same file.`,
153
+ ].join(" "), "setup.hook-script-path", { details: { override } });
154
+ }
155
+ return override;
156
+ }
157
+ const here = fileURLToPath(import.meta.url);
158
+ const parts = here.split(path.sep);
159
+ // Pin to the canonical suffix `{runtime|dist}/hooks/install-paths.{ts,js}`
160
+ // — anchoring to a specific tail keeps ancestor directories named
161
+ // "runtime" or "dist" from confusing the lookup.
162
+ if (parts.length < 3) {
163
+ throw resolveHookFailure(here);
164
+ }
165
+ const tailParent = parts[parts.length - 2];
166
+ const tailGrandparent = parts[parts.length - 3];
167
+ if (tailParent !== "hooks" || (tailGrandparent !== "runtime" && tailGrandparent !== "dist")) {
168
+ throw resolveHookFailure(here);
169
+ }
170
+ const pluginRoot = parts.slice(0, parts.length - 3).join(path.sep) || path.sep;
171
+ return path.join(pluginRoot, "dist", "hooks", "approval-hook.js");
172
+ }
173
+ function resolveHookFailure(here) {
174
+ return new RuntimeError("SETUP_RESOLVE_HOOK_FAILED", `Could not infer plugin root from install-paths module path ${here}. Set KIMI_PLUGIN_CC_HOOK_SCRIPT to the absolute path of dist/hooks/approval-hook.js.`, "setup.resolve-hook", { details: { here } });
175
+ }
176
+ /**
177
+ * Parse a hook shell command of the canonical `'<nodeBin>' '<hookScript>'`
178
+ * shape (two POSIX single-quoted tokens, space-separated) back into its two
179
+ * tokens. The exact inverse of `buildHookShellCommand` →
180
+ * `shellSingleQuote(nodeBin) + " " + shellSingleQuote(hookScript)`.
181
+ *
182
+ * Returns `null` for any command that isn't exactly two single-quoted tokens
183
+ * (e.g. a legacy bare-`node` form, a crafted/garbage command, or anything with
184
+ * unquoted bare words) — callers treat `null` as "can't classify; use the
185
+ * generic mismatch message". Strict by design: it only recognizes the canonical
186
+ * single-quoted form this module emits, so it never mis-attributes a hand-rolled
187
+ * command's tokens. Tokens with embedded spaces or apostrophes round-trip
188
+ * (spaces live inside the quotes; `'` is encoded as `'\''`).
189
+ */
190
+ export function parseHookShellCommand(command) {
191
+ const tokens = parseSingleQuotedTokens(command);
192
+ if (tokens === null || tokens.length !== 2) {
193
+ return null;
194
+ }
195
+ return { nodeBin: tokens[0], hookScript: tokens[1] };
196
+ }
197
+ /**
198
+ * Tokenize a string of POSIX single-quoted tokens as produced by
199
+ * `shellSingleQuote` (`'...'` with inner `'` encoded as `'\''`). Returns the
200
+ * decoded token list, or `null` if the input contains any unquoted bare
201
+ * character or an unterminated quote — i.e. anything outside the grammar this
202
+ * module emits.
203
+ */
204
+ function parseSingleQuotedTokens(input) {
205
+ const tokens = [];
206
+ let i = 0;
207
+ const n = input.length;
208
+ while (i < n) {
209
+ if (input[i] === " ") {
210
+ i += 1;
211
+ continue;
212
+ }
213
+ let token = "";
214
+ let consumedAny = false;
215
+ while (i < n && input[i] !== " ") {
216
+ const ch = input[i];
217
+ if (ch === "'") {
218
+ // A '...' quoted segment. Single-quoted content can never contain a
219
+ // literal "'", so the next "'" is always the real close.
220
+ const close = input.indexOf("'", i + 1);
221
+ if (close === -1)
222
+ return null; // unterminated quote
223
+ token += input.slice(i + 1, close);
224
+ i = close + 1;
225
+ consumedAny = true;
226
+ }
227
+ else if (ch === "\\") {
228
+ // The `\'` half of a `'\''` escape (an apostrophe in the value).
229
+ if (i + 1 >= n)
230
+ return null;
231
+ token += input[i + 1];
232
+ i += 2;
233
+ consumedAny = true;
234
+ }
235
+ else {
236
+ // A bare unquoted character — not part of the canonical grammar.
237
+ return null;
238
+ }
239
+ }
240
+ if (!consumedAny)
241
+ return null;
242
+ tokens.push(token);
243
+ }
244
+ return tokens;
245
+ }
246
+ /**
247
+ * H4 — classify a hook-command MISMATCH into an actionable diagnosis. When the
248
+ * managed block is structurally valid but its `command` differs from what this
249
+ * companion would write, the raw "expected X; got Y" dump is hard to act on. The
250
+ * common real cause is environment drift: a Node upgrade or version-manager
251
+ * (nvm/asdf/mise/fnm/Homebrew) switch moved the pinned interpreter, or a plugin
252
+ * update changed the version-stamped hook-script path. This names which token
253
+ * drifted and, for Node, whether the old binary still exists on disk (a gone
254
+ * binary is the unambiguous "your Node moved" signal).
255
+ *
256
+ * Returns `undefined` when it can't classify (either command isn't the canonical
257
+ * two-single-quoted-token shape, or the tokens are somehow equal) — the caller
258
+ * then falls back to the generic mismatch message. Pure except for the injected
259
+ * `nodeExists` predicate (so the fs probe stays at the call site). Does NOT alter
260
+ * the verifier's exact-equality decision — only the human/LLM-facing reason.
261
+ */
262
+ /**
263
+ * Machine-readable sibling of `describeHookCommandDrift`. Same inputs, same
264
+ * parse, but returns WHICH token moved instead of prose — so an LLM caller can
265
+ * branch on `axis` rather than pattern-matching a human sentence (LLM-caller
266
+ * discipline: load-bearing context goes in structured state, never in prose).
267
+ *
268
+ * `"hook-script"` is the recoverable case: install paths are version-stamped,
269
+ * so a plugin update moves it and re-running setup on THAT host re-pins it.
270
+ * `"node-bin"`/`"both"` are reported but deliberately NOT auto-recoverable by
271
+ * a caller retry — a moved interpreter can mean the pinned one is gone, and a
272
+ * hook that fails to spawn exits 127, which kimi-code reads as ALLOW.
273
+ *
274
+ * Returns `undefined` when either command isn't the canonical two-token shape
275
+ * or the commands are equal. Never influences the installed decision.
276
+ */
277
+ /**
278
+ * Do two paths name the same file? Used ONLY to sharpen a drift *message*
279
+ * (distinguishing the v1.9.0 stable-path re-pin from a real interpreter move).
280
+ * Never load-bearing: any throw means "can't tell", and the caller falls back
281
+ * to the more conservative wording.
282
+ */
283
+ function defaultSameFile(a, b) {
284
+ try {
285
+ return realpathSync(a) === realpathSync(b);
286
+ }
287
+ catch {
288
+ return false;
289
+ }
290
+ }
291
+ export function classifyHookCommandDrift(installedCommand, expectedCommand, sameFile = defaultSameFile) {
292
+ const installed = parseHookShellCommand(installedCommand);
293
+ const expected = parseHookShellCommand(expectedCommand);
294
+ if (installed === null || expected === null) {
295
+ return undefined;
296
+ }
297
+ const nodeDrift = installed.nodeBin !== expected.nodeBin;
298
+ const hookDrift = installed.hookScript !== expected.hookScript;
299
+ if (!nodeDrift && !hookDrift) {
300
+ return undefined;
301
+ }
302
+ const axis = nodeDrift && hookDrift ? "both" : nodeDrift ? "node-bin" : "hook-script";
303
+ // A node token that changed SPELLING but still names the same file is not an
304
+ // interpreter move. Distinguishing this matters because it is precisely the
305
+ // v1.9.0 upgrade shape (the pin moves off the version-stamped path, and the
306
+ // plugin's own version-stamped script path moves at the same time → `both`),
307
+ // which every existing install hits exactly once. Without this the caller
308
+ // would be told "do not retry, the interpreter may be gone" while the human
309
+ // message says "your Node did not move" — a contradiction where the
310
+ // structured field, being the load-bearing one, was the wrong half.
311
+ return {
312
+ axis,
313
+ installedCommand,
314
+ expectedCommand,
315
+ ...(nodeDrift ? { nodeInterpreterUnchanged: sameFile(installed.nodeBin, expected.nodeBin) } : {}),
316
+ };
317
+ }
318
+ export function describeHookCommandDrift(installedCommand, expectedCommand, nodeExists, sameFile = defaultSameFile) {
319
+ const installed = parseHookShellCommand(installedCommand);
320
+ const expected = parseHookShellCommand(expectedCommand);
321
+ if (installed === null || expected === null) {
322
+ return undefined;
323
+ }
324
+ const nodeDrift = installed.nodeBin !== expected.nodeBin;
325
+ const hookDrift = installed.hookScript !== expected.hookScript;
326
+ if (!nodeDrift && !hookDrift) {
327
+ return undefined;
328
+ }
329
+ const parts = [];
330
+ if (nodeDrift) {
331
+ if (!nodeExists(installed.nodeBin)) {
332
+ parts.push(`Node binary drift: the installed hook pins ${installed.nodeBin}, which no longer exists on disk ` +
333
+ `(this companion runs ${expected.nodeBin}). This is the classic Node-upgrade / version-manager ` +
334
+ `(nvm, asdf, mise, fnm, Homebrew) drift — the pinned interpreter moved, so kimi-code can no longer ` +
335
+ `spawn the hook and read-only enforcement silently degrades.`);
336
+ }
337
+ else if (sameFile(installed.nodeBin, expected.nodeBin)) {
338
+ // Both paths name the SAME interpreter — a spelling change, not a moved
339
+ // binary. This is the v1.9.0 migration: the pin moved off the
340
+ // symlink-resolved (version-stamped) path onto the stable one, so every
341
+ // pre-1.9.0 install drifts exactly once. Blaming a "version-manager
342
+ // switch between runs" here would be actively misleading — the user
343
+ // changed nothing about their Node.
344
+ parts.push(`Node path spelling changed: the installed hook pins ${installed.nodeBin} and this companion runs ` +
345
+ `${expected.nodeBin} — these are the SAME interpreter reached by different paths. Since v1.9.0 the ` +
346
+ `hook pins the stable path instead of the version-stamped one, so a Node upgrade no longer breaks it. ` +
347
+ `This is the expected one-time re-pin after upgrading the plugin; your Node did not move.`);
348
+ }
349
+ else {
350
+ parts.push(`Node binary changed: the installed hook pins ${installed.nodeBin}, but this companion runs ` +
351
+ `${expected.nodeBin} (both exist on disk — likely a Node version-manager switch between runs).`);
352
+ }
353
+ }
354
+ if (hookDrift) {
355
+ parts.push(`Hook script path drift: the installed hook points at ${installed.hookScript}, but this companion's ` +
356
+ `hook is ${expected.hookScript} (likely a plugin update or move — the install path is version-stamped).`);
357
+ }
358
+ parts.push("Run /kimi:setup to re-pin the managed block to this companion's current paths.");
359
+ return parts.join(" ");
360
+ }
361
+ /**
362
+ * Best-effort: compute the canonical expected shell command for the
363
+ * current env. Returns `undefined` if either path can't be resolved
364
+ * (caller treats this as "managed block is unverifiable; do not assume
365
+ * installed"). Never throws.
366
+ *
367
+ * This is the helper the verifier uses on every plugin command spawn.
368
+ */
369
+ export function tryBuildExpectedHookCommand(env) {
370
+ try {
371
+ const hookScriptPath = resolveHookScriptPath(env);
372
+ const command = buildHookShellCommand(hookScriptPath, env);
373
+ // Surfaced so the verifier can probe the pinned interpreter for
374
+ // executability. DERIVED FROM `command` BY PARSING IT BACK, never by calling
375
+ // `resolveNodeBinary` a second time.
376
+ //
377
+ // Re-resolving would be a latent bypass: `resolveNodeBinary`'s no-override
378
+ // path runs `preferStableNodePath`, which calls `realpathSync` and falls
379
+ // back to `execPath` on ANY throw. So it is NOT a pure function of `env` —
380
+ // it reads live filesystem state. If the pinned symlink were unlinked
381
+ // between the two calls (exactly what `brew upgrade node` does), call #1
382
+ // would embed the symlink in `command` while call #2 returned the old
383
+ // realpath. The verifier would then compare the config against the symlink
384
+ // (match) but probe the OLD Cellar path for executability (may still exist)
385
+ // — reporting installed=true for a hook whose actual pinned interpreter is
386
+ // gone. `/bin/sh -c` would exit 127, which kimi-code reads as ALLOW.
387
+ //
388
+ // Parsing the command back is the exact inverse of how it was built, so
389
+ // `nodeBin` is byte-identical to the token in `command` BY CONSTRUCTION —
390
+ // the guarantee is structural, not probabilistic. The `??` is unreachable
391
+ // (we just built this string with the very quoting `parseHookShellCommand`
392
+ // decodes) and exists only so a future change to either side degrades to
393
+ // the old behavior instead of throwing.
394
+ const nodeBin = parseHookShellCommand(command)?.nodeBin ?? resolveNodeBinary(env);
395
+ return { command, hookScriptPath, nodeBin };
396
+ }
397
+ catch (err) {
398
+ if (err instanceof RuntimeError) {
399
+ return { error: err };
400
+ }
401
+ return {
402
+ error: new RuntimeError("SETUP_RESOLVE_HOOK_FAILED", `Unexpected error resolving hook path: ${err.message}`, "setup.resolve-hook", err instanceof Error
403
+ ? { cause: err, details: {} }
404
+ : { details: {} }),
405
+ };
406
+ }
407
+ }
408
+ // ----- Host identity -----------------------------------------------------
409
+ //
410
+ // Claude Code and Codex install this plugin to DIFFERENT, version-stamped,
411
+ // host-specific paths but SHARE one `~/.kimi-code/config.toml`:
412
+ //
413
+ // Claude: ~/.claude/plugins/cache/kimi-marketplace/kimi/<ver>/dist/hooks/approval-hook.js
414
+ // Codex: ~/.codex/plugins/cache/kimi-marketplace/kimi/<ver>/dist/hooks/approval-hook.js
415
+ //
416
+ // The managed block is host-scoped (marker suffix `:<host-id>`) so each host
417
+ // owns and verifies its OWN PreToolUse block without clobbering the other's.
418
+ // The host id must be VERSION-INDEPENDENT so a plugin upgrade REFRESHES the
419
+ // same host's block instead of accumulating one block per version.
420
+ /**
421
+ * Resolve a stable, version-independent host id for the managed-block marker.
422
+ *
423
+ * Order: an explicit `KIMI_PLUGIN_CC_HOST_ID` override (slugified) wins — used
424
+ * by tests and the live-repair path. Otherwise derive from the resolved hook
425
+ * script path. Pass the already-resolved `hookScriptPath` when you have it
426
+ * (the verifier + setup do) so we don't re-resolve and risk a second throw.
427
+ */
428
+ export function resolveHostId(env, hookScriptPath) {
429
+ const override = env.KIMI_PLUGIN_CC_HOST_ID;
430
+ if (override !== undefined && override.trim().length > 0) {
431
+ return slugifyHostId(override);
432
+ }
433
+ const resolved = hookScriptPath ?? resolveHookScriptPath(env);
434
+ return hostIdFromHookScript(resolved);
435
+ }
436
+ /**
437
+ * Derive a host id from a hook-script path. `~/.claude/...` → `claude-code`,
438
+ * `~/.codex/...` → `codex` (both literal + version-independent), else a stable
439
+ * `host-<sha1(hookDir)[:8]>` for dev checkouts / unrecognized layouts.
440
+ */
441
+ export function hostIdFromHookScript(hookScriptPath) {
442
+ const norm = hookScriptPath.split(path.sep).join("/");
443
+ if (norm.includes("/.claude/"))
444
+ return "claude-code";
445
+ if (norm.includes("/.codex/"))
446
+ return "codex";
447
+ // Dev checkouts are not version-stamped, so hashing the hook's directory is
448
+ // stable across runs (and across plugin upgrades, which don't move it). Use
449
+ // 16 hex chars (64 bits): an 8-char prefix is only 32 bits, where two
450
+ // distinct dev roots can collide (Codex review found a concrete pair) and the
451
+ // second setup would treat the first host's block as its own and clobber it.
452
+ const digest = createHash("sha1").update(path.dirname(hookScriptPath)).digest("hex");
453
+ return `host-${digest.slice(0, 16)}`;
454
+ }
455
+ /**
456
+ * Normalize an arbitrary host-id override into the `[a-z0-9-]+` slug the
457
+ * marker regex accepts. Empty results fall back to `host` so the marker is
458
+ * always well-formed.
459
+ */
460
+ export function slugifyHostId(value) {
461
+ const slug = value
462
+ .trim()
463
+ .toLowerCase()
464
+ .replace(/[^a-z0-9]+/g, "-")
465
+ .replace(/^-+|-+$/g, "")
466
+ .slice(0, 40);
467
+ return slug.length > 0 ? slug : "host";
468
+ }
469
+ /**
470
+ * True when a (decoded) hook `command` string is unambiguously THIS plugin's
471
+ * approval hook — a canonical two-single-quoted-token command whose script is
472
+ * `approval-hook.js` living under a `kimi-plugin-cc` / `kimi-marketplace`
473
+ * install tree. Used to prune orphaned, marker-less `[[hooks]]` entries left by
474
+ * older installs. Deliberately strict: a hand-rolled or non-canonical command
475
+ * returns false, so we never remove a user's own hook.
476
+ */
477
+ /**
478
+ * Derive the host id that OWNS a hook command, from its script path — the
479
+ * host-scoped counterpart of `hostIdFromHookScript`. Returns `null` when the
480
+ * command isn't the canonical two-single-quoted-token shape (a stale/bare
481
+ * legacy command whose owner can't be determined). Callers treat `null` as
482
+ * "claimable by the current host." Lets a legacy (un-suffixed) block be
483
+ * attributed to whichever host actually wrote it, so one host's `/kimi:setup`
484
+ * never adopts or removes another host's block.
485
+ */
486
+ export function hostIdFromHookCommand(command) {
487
+ const parsed = parseHookShellCommand(command);
488
+ if (parsed === null)
489
+ return null;
490
+ return hostIdFromHookScript(parsed.hookScript);
491
+ }
492
+ export function isOurApprovalHookCommand(decodedCommand) {
493
+ const parsed = parseHookShellCommand(decodedCommand);
494
+ if (parsed === null)
495
+ return false;
496
+ const script = parsed.hookScript.split(/[\\/]/);
497
+ if (script[script.length - 1] !== "approval-hook.js")
498
+ return false;
499
+ const normalized = parsed.hookScript.replace(/\\/g, "/");
500
+ // Require a real path SEGMENT, not an arbitrary substring — otherwise a
501
+ // user hook at `/opt/acme/kimi-plugin-cc-wrapper/approval-hook.js` would be
502
+ // misclassified as ours and pruned. (Codex review.)
503
+ return normalized.includes("/kimi-plugin-cc/") || normalized.includes("/kimi-marketplace/");
504
+ }