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,745 @@
1
+ // Shared parser for the kimi-plugin-cc managed block in
2
+ // ~/.kimi-code/config.toml.
3
+ //
4
+ // Why a separate module:
5
+ //
6
+ // PR 2 added `runtime/hooks/install.ts::verifyHookInstalled` as a
7
+ // pre-call gate. PR 4 added `runtime/commands/setup.ts` as the
8
+ // installer. PR 4 reviewers found that the two were validating
9
+ // different shapes — the verifier was a substring check (could be
10
+ // bypassed by stray comments containing `kimi-plugin-cc-managed`),
11
+ // and `setup --check` only confirmed `blockText.includes(hookPath)`
12
+ // without checking for matcher/event/command exactness. A block with
13
+ // `matcher = "*"` (which throws inside kimi-code and silently
14
+ // disables the hook) would pass both gates.
15
+ //
16
+ // This module owns the single source of truth for what a valid
17
+ // managed block looks like. Both call sites import from here.
18
+ //
19
+ // (The drift classifier `describeHookCommandDrift` lives in install-paths.ts —
20
+ // the single source of truth for the hook command's byte shape — and is used
21
+ // here to turn a command MISMATCH into an actionable diagnosis when the caller
22
+ // supplies an `nodeExists` fs predicate.)
23
+ //
24
+ // HOST SCOPING (v1.7.0):
25
+ //
26
+ // Claude Code and Codex install this plugin to DIFFERENT, version-stamped
27
+ // paths but SHARE one `~/.kimi-code/config.toml`. Before v1.7.0 there was a
28
+ // single managed block whose `command` was exact-matched to the RUNNING
29
+ // host's path, so `/kimi:setup` (Claude) and `$kimi-setup` (Codex) overwrote
30
+ // each other. Now the block is HOST-SCOPED — the marker carries a `:<host-id>`
31
+ // suffix and each host owns/verifies its OWN block. Un-suffixed markers are
32
+ // treated as LEGACY (from pre-v1.7.0 single-host installs); the current host
33
+ // adopts a lone legacy block on its next install (see setup.ts).
34
+ //
35
+ // What a valid managed block looks like (line-by-line, post-trim):
36
+ //
37
+ // # === BEGIN kimi-plugin-cc-managed:<host-id> (vX.Y.Z) ===
38
+ // ... optional comment lines (any number, any content) ...
39
+ // [[hooks]]
40
+ // event = "PreToolUse"
41
+ // command = "node 'absolute/path/to/approval-hook.js'" (or process.execPath form)
42
+ // timeout = <integer>
43
+ // # === END kimi-plugin-cc-managed:<host-id> ===
44
+ //
45
+ // Critical rules:
46
+ //
47
+ // - NO `matcher = ...` line. kimi-code compiles matchers as JS regex;
48
+ // `new RegExp("*")` throws and silently disables the hook. Omitting
49
+ // matcher means "fire for every tool".
50
+ // - `event = "PreToolUse"` is required. Any other event would not
51
+ // enforce our safety contract.
52
+ // - Exactly ONE `[[hooks]]` declaration inside the block. Two would
53
+ // register two hooks, which is fine functionally but breaks
54
+ // idempotency checks.
55
+ // - The `command` line must contain the hook script path we expect.
56
+ import { parse as parseToml } from "../vendor/smol-toml/parse.js";
57
+ import { classifyHookCommandDrift, describeHookCommandDrift, hostIdFromHookCommand, isOurApprovalHookCommand, } from "./install-paths.js";
58
+ const MARKER_TAG = "kimi-plugin-cc-managed";
59
+ /**
60
+ * Strict BEGIN matcher. Accepts `# === BEGIN kimi-plugin-cc-managed`
61
+ * with an optional `:<host-id>` suffix, an optional ` (vX.Y.Z)` suffix,
62
+ * and trailing ` ===`. Group 1 captures the host id (undefined = legacy).
63
+ * Anything else (random comment containing the tag, mid-line embedded
64
+ * marker) is not a managed-block marker.
65
+ */
66
+ const BEGIN_LINE_RE = /^#\s*===\s*BEGIN\s+kimi-plugin-cc-managed(?::([A-Za-z0-9._-]+))?(?:\s+\([^)]+\))?\s*===\s*$/;
67
+ const END_LINE_RE = /^#\s*===\s*END\s+kimi-plugin-cc-managed(?::([A-Za-z0-9._-]+))?\s*===\s*$/;
68
+ /** TOML basic string for `command = "..."`. Captures the inner value. */
69
+ const COMMAND_LINE_RE = /^command\s*=\s*"((?:[^"\\]|\\.)*)"\s*$/;
70
+ /** TOML literal string for `command = '...'` (no escapes inside). */
71
+ const COMMAND_LITERAL_LINE_RE = /^command\s*=\s*'([^']*)'\s*$/;
72
+ const EVENT_LINE_RE = /^event\s*=\s*"PreToolUse"\s*$/;
73
+ /** Anything matcher-shaped is a critical safety failure — block disabled. */
74
+ const MATCHER_LINE_RE = /^matcher\s*=/;
75
+ /** `timeout = <int>` — the only other key our managed [[hooks]] table emits. */
76
+ const TIMEOUT_LINE_RE = /^timeout\s*=\s*\d+\s*$/;
77
+ const HOOKS_TABLE_RE = /^\[\[hooks\]\]\s*$/;
78
+ /** Locate every BEGIN/END marker line, in order, with its parsed host id. */
79
+ function findMarkers(lines) {
80
+ const hits = [];
81
+ for (let i = 0; i < lines.length; i += 1) {
82
+ const trimmed = lines[i].trim();
83
+ const begin = BEGIN_LINE_RE.exec(trimmed);
84
+ if (begin !== null) {
85
+ // Normalize marker host ids to lowercase so a hand-edited `:Claude-Code`
86
+ // isn't treated as a different host from the lowercase `claude-code` the
87
+ // installer emits (Kimi review — avoids confusing duplicate blocks).
88
+ hits.push({ index: i, kind: "begin", hostId: begin[1]?.toLowerCase() ?? null });
89
+ continue;
90
+ }
91
+ const end = END_LINE_RE.exec(trimmed);
92
+ if (end !== null) {
93
+ hits.push({ index: i, kind: "end", hostId: end[1]?.toLowerCase() ?? null });
94
+ }
95
+ }
96
+ return hits;
97
+ }
98
+ /**
99
+ * Pair BEGIN/END markers into well-formed blocks. Returns the first orphan
100
+ * (BEGIN-without-END or END-without-BEGIN) encountered, mirroring the
101
+ * pre-v1.7.0 single-block behavior — an orphan is a hard error for install.
102
+ */
103
+ function pairBlocks(lines) {
104
+ const markers = findMarkers(lines);
105
+ const blocks = [];
106
+ let open = null;
107
+ for (const marker of markers) {
108
+ if (marker.kind === "begin") {
109
+ // A second BEGIN before the previous one closed → the previous is orphaned.
110
+ if (open !== null)
111
+ return { orphan: "BEGIN-without-END" };
112
+ open = marker;
113
+ continue;
114
+ }
115
+ // end
116
+ if (open === null)
117
+ return { orphan: "END-without-BEGIN" };
118
+ const body = validateBlockBody(lines.slice(open.index + 1, marker.index));
119
+ blocks.push({
120
+ hostId: open.hostId,
121
+ beginLine: open.index,
122
+ endLine: marker.index,
123
+ commandPath: body.commandPath,
124
+ valid: body.valid,
125
+ invalidReason: body.invalidReason,
126
+ });
127
+ open = null;
128
+ }
129
+ if (open !== null)
130
+ return { orphan: "BEGIN-without-END" };
131
+ return { blocks };
132
+ }
133
+ /** Validate the lines BETWEEN a BEGIN/END pair against the safety grammar. */
134
+ function validateBlockBody(rawBody) {
135
+ const blockLines = rawBody.map((line) => line.trim());
136
+ let foundHooksTable = false;
137
+ let foundEvent = false;
138
+ let commandPath = "";
139
+ let invalidReason;
140
+ for (const line of blockLines) {
141
+ if (line.length === 0 || line.startsWith("#"))
142
+ continue;
143
+ if (HOOKS_TABLE_RE.test(line)) {
144
+ if (foundHooksTable) {
145
+ invalidReason = "block contains more than one [[hooks]] table";
146
+ break;
147
+ }
148
+ foundHooksTable = true;
149
+ continue;
150
+ }
151
+ if (line.startsWith("[")) {
152
+ // Any TOML table header other than the single [[hooks]] table means the
153
+ // `event`/`command` lines below could belong to a DIFFERENT table (e.g.
154
+ // `[[hooks]]` with only an event, then `[not_hooks]` carrying the
155
+ // canonical command). Binding the block to exactly one [[hooks]] table
156
+ // keeps the verifier from blessing a command-less hook. (Codex review.)
157
+ invalidReason =
158
+ "block contains an unexpected TOML table — only a single [[hooks]] table is allowed. Reinstall with /kimi:setup to repair.";
159
+ break;
160
+ }
161
+ if (MATCHER_LINE_RE.test(line)) {
162
+ invalidReason =
163
+ 'block contains a `matcher = ...` line — kimi-code compiles matchers as JS regex (`new RegExp("*")` throws and disables the hook). Reinstall with /kimi:setup to repair.';
164
+ break;
165
+ }
166
+ if (EVENT_LINE_RE.test(line)) {
167
+ foundEvent = true;
168
+ continue;
169
+ }
170
+ const basic = COMMAND_LINE_RE.exec(line);
171
+ const literal = COMMAND_LITERAL_LINE_RE.exec(line);
172
+ if (basic !== null || literal !== null) {
173
+ // TOML basic strings escape `\`, `"`, and the control chars.
174
+ // Setup writes the escaped form via `tomlBasicString`; the
175
+ // verifier compares against the JS-string canonical command, so
176
+ // we must decode the escapes here before equality. Without this,
177
+ // a hook script path containing `'` round-trips through
178
+ // `shellSingleQuote` -> backslash -> TOML basic-string escape ->
179
+ // captured raw -> mismatch on verify. Audit re-review (reports
180
+ // 33/34) flagged this as a UX false-fail. Literal strings ('...')
181
+ // have no escapes — pass through as-is.
182
+ commandPath = basic !== null ? decodeTomlBasicString(basic[1]) : literal[1];
183
+ continue;
184
+ }
185
+ // Other TOML lines (timeout = N, etc.) are accepted as opaque
186
+ // pass-through. We don't validate them; if kimi-code rejects them
187
+ // the probe will catch it.
188
+ }
189
+ if (invalidReason === undefined) {
190
+ if (!foundHooksTable) {
191
+ invalidReason = "block is missing a `[[hooks]]` table";
192
+ }
193
+ else if (!foundEvent) {
194
+ invalidReason = 'block is missing `event = "PreToolUse"`';
195
+ }
196
+ else if (commandPath.length === 0) {
197
+ invalidReason = "block is missing a `command = \"...\"` line";
198
+ }
199
+ }
200
+ // Parser-based matcher rejection — defense in depth over the line-based
201
+ // MATCHER_LINE_RE above. smol-toml normalizes a quoted `"matcher"`, a literal
202
+ // `'matcher'`, or a dotted key to the `matcher` key that the bare-line regex
203
+ // (`/^matcher\s*=/`) MISSES, while kimi-code's real parser honors it and
204
+ // DISABLES the hook (`new RegExp("*")` throws → fire-for-nothing → auto-
205
+ // approve). The absent-state fallback (`hasCleanEnforcingHookEntry`) already
206
+ // parses and is immune; the primary marked-block path must be too, or a
207
+ // quoted matcher slips a disabled hook past the verifier as `installed:true`.
208
+ // Fail closed on any parse error — a body that won't parse in isolation is
209
+ // anomalous and kimi-code would reject the whole config anyway. (kimi
210
+ // whole-repo audit 2026-07-17.)
211
+ if (invalidReason === undefined) {
212
+ try {
213
+ const parsed = parseToml(rawBody.join("\n"));
214
+ const hooks = parsed !== null && typeof parsed === "object"
215
+ ? parsed.hooks
216
+ : undefined;
217
+ const hasMatcher = Array.isArray(hooks) &&
218
+ hooks.some((entry) => entry !== null &&
219
+ typeof entry === "object" &&
220
+ Object.prototype.hasOwnProperty.call(entry, "matcher"));
221
+ if (hasMatcher) {
222
+ invalidReason =
223
+ "block contains a `matcher` key (in a quoted or dotted spelling the line scanner misses) — kimi-code compiles matchers as JS regex and disables the hook. Reinstall with /kimi:setup to repair.";
224
+ }
225
+ }
226
+ catch {
227
+ invalidReason =
228
+ "block body is not valid TOML in isolation — cannot confirm it carries no hook-disabling matcher. Reinstall with /kimi:setup to repair.";
229
+ }
230
+ }
231
+ return { valid: invalidReason === undefined, invalidReason, commandPath };
232
+ }
233
+ /**
234
+ * Parse the supplied kimi-code config text and resolve the managed-block
235
+ * state FOR A SPECIFIC HOST.
236
+ *
237
+ * Host resolution: the current host's own suffixed block is authoritative if
238
+ * present; otherwise a lone LEGACY (un-suffixed) block is adoptable and is
239
+ * returned as the found block (the current host will convert it on install).
240
+ * Other hosts' suffixed blocks are ignored for this host's state (that is the
241
+ * whole point of host scoping — they coexist).
242
+ *
243
+ * Stripping line endings before the per-line matchers means this is
244
+ * CRLF-safe — the trailing `\r` on each line is removed before regex
245
+ * comparison.
246
+ */
247
+ export function parseManagedBlock(contents, hostId) {
248
+ const rawLines = contents.split("\n");
249
+ const lines = rawLines.map((line) => line.replace(/\r$/, ""));
250
+ const paired = pairBlocks(lines);
251
+ if ("orphan" in paired) {
252
+ return { state: { kind: "orphan", detail: paired.orphan }, blocks: [], lines };
253
+ }
254
+ const blocks = paired.blocks;
255
+ // A block "belongs to" the current host when its marker suffix is this host,
256
+ // OR (for a legacy un-suffixed block) its command path derives to this host —
257
+ // an un-parseable/stale legacy command is claimable by whoever installs next.
258
+ // This is what stops one host from adopting/clobbering another host's legacy
259
+ // block during migration (Kimi review): a `~/.codex/…` legacy block is NOT
260
+ // relevant to the `claude-code` host, so Claude appends its own block and
261
+ // leaves Codex's intact.
262
+ const relevant = blocks.filter((b) => effectiveHost(b, hostId) === hostId);
263
+ if (relevant.length === 0) {
264
+ return { state: { kind: "absent" }, blocks, lines };
265
+ }
266
+ if (relevant.length > 1) {
267
+ return {
268
+ state: { kind: "duplicate", beginLines: relevant.map((b) => b.beginLine) },
269
+ blocks,
270
+ lines,
271
+ };
272
+ }
273
+ const block = relevant[0];
274
+ return {
275
+ state: {
276
+ kind: "found",
277
+ beginLine: block.beginLine,
278
+ endLine: block.endLine,
279
+ hostId: block.hostId,
280
+ commandPath: block.commandPath,
281
+ valid: block.valid,
282
+ invalidReason: block.invalidReason,
283
+ },
284
+ blocks,
285
+ lines,
286
+ };
287
+ }
288
+ /**
289
+ * The host a managed block belongs to: its marker suffix when present,
290
+ * otherwise the host derived from its command path (a legacy un-suffixed
291
+ * block), falling back to `currentHost` when the command can't be attributed
292
+ * (a stale/bare legacy command — claimable by whoever installs next).
293
+ */
294
+ export function effectiveHost(block, currentHost) {
295
+ if (block.hostId !== null)
296
+ return block.hostId;
297
+ return hostIdFromHookCommand(block.commandPath) ?? currentHost;
298
+ }
299
+ /**
300
+ * Decode the `command = "..."` (or `'...'`) value from a single trimmed TOML
301
+ * line, or `null` if the line isn't a command assignment. Exported so the
302
+ * uninstall path can attribute a legacy block to its host without re-deriving
303
+ * the TOML decode.
304
+ */
305
+ export function decodeManagedCommandLine(trimmedLine) {
306
+ const basic = COMMAND_LINE_RE.exec(trimmedLine);
307
+ if (basic !== null)
308
+ return decodeTomlBasicString(basic[1]);
309
+ const literal = COMMAND_LITERAL_LINE_RE.exec(trimmedLine);
310
+ if (literal !== null)
311
+ return literal[1];
312
+ return null;
313
+ }
314
+ /**
315
+ * Robust, PARSER-based "is our clean enforcing hook present?" check for the
316
+ * marker-less fallback. Parses the whole config with the same vendored
317
+ * `smol-toml` kimi-code uses, then looks for a `hooks[]` entry that is EXACTLY
318
+ * our clean hook:
319
+ *
320
+ * - `event === "PreToolUse"`,
321
+ * - `command === expectedCommand` (byte-exact — the security boundary),
322
+ * - NO `matcher` key in ANY TOML spelling (bare `matcher`, quoted
323
+ * `"matcher"`, after a blank line, past a multiline array) — a matcher can
324
+ * disable the hook (`new RegExp("*")` throws → hook never fires) or narrow
325
+ * it to a subset of tools, so its presence means "not a clean install",
326
+ * - no keys beyond `{event, command, timeout}` — an unknown key trips
327
+ * kimi-code's `.strict()` hook schema and disables the whole hooks array.
328
+ *
329
+ * Using the real parser (not the line-oriented `findBareApprovalHookTables`,
330
+ * which stays the conservative PRUNE scanner) closes the entire class of
331
+ * line-scanner blind spots a three-model review panel demonstrated (Opus +
332
+ * Codex + kimi all independently: parse the table, don't lex it). Returns
333
+ * `false` on any parse error — fail-closed. The real callers
334
+ * (`verifyHookInstalled`, `runCheck`) run whole-file schema validation first,
335
+ * so a malformed or foreign-invalid file never reaches an installed verdict.
336
+ *
337
+ * NOTE: this deliberately does NOT range-check `timeout` — that is the config
338
+ * schema's job (run upstream by both callers). A clean entry with an
339
+ * out-of-range timeout is refused there, not here.
340
+ */
341
+ /**
342
+ * Return `contents` with every well-formed managed BEGIN/END block removed
343
+ * (inclusive), so what remains is only the bare/root TOML. Used by
344
+ * `hasCleanEnforcingHookEntry` to enforce host isolation. If the markers are
345
+ * unbalanced (orphan), returns the input unchanged — the caller is only reached
346
+ * in the `absent` state, where a well-formed foreign block is the norm.
347
+ */
348
+ function stripManagedBlockLines(contents) {
349
+ const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
350
+ const paired = pairBlocks(lines);
351
+ if (!("blocks" in paired))
352
+ return contents;
353
+ const inBlock = new Set();
354
+ for (const block of paired.blocks) {
355
+ for (let i = block.beginLine; i <= block.endLine; i += 1)
356
+ inBlock.add(i);
357
+ }
358
+ if (inBlock.size === 0)
359
+ return contents;
360
+ return lines.filter((_, i) => !inBlock.has(i)).join("\n");
361
+ }
362
+ /**
363
+ * True if a parsed `hooks[]` array contains a clean enforcing entry for
364
+ * `expectedCommand`: `event="PreToolUse"`, byte-exact `command`, NO `matcher`
365
+ * (any spelling the parser normalized to the `matcher` key can disable/narrow
366
+ * the hook), and no keys beyond {event,command,timeout} (a stray key is fatal to
367
+ * kimi-code's strict schema). Each host's hook path is distinct, so the command
368
+ * uniquely identifies this host's entry.
369
+ */
370
+ function hooksArrayHasCleanEntry(hooks, expectedCommand) {
371
+ if (!Array.isArray(hooks))
372
+ return false;
373
+ const allowedKeys = new Set(["event", "command", "timeout"]);
374
+ for (const entry of hooks) {
375
+ if (entry === null || typeof entry !== "object")
376
+ continue;
377
+ const rec = entry;
378
+ if (rec.event !== "PreToolUse")
379
+ continue;
380
+ if (rec.command !== expectedCommand)
381
+ continue;
382
+ if (Object.prototype.hasOwnProperty.call(rec, "matcher"))
383
+ continue;
384
+ if (!Object.keys(rec).every((k) => allowedKeys.has(k)))
385
+ continue;
386
+ return true;
387
+ }
388
+ return false;
389
+ }
390
+ export function hasCleanEnforcingHookEntry(contents, expectedCommand) {
391
+ // Consider only hooks that live OUTSIDE every managed BEGIN/END block. In the
392
+ // `absent` state this host has no block of its own, so any surviving marked
393
+ // block belongs to ANOTHER host — its entries are that host's responsibility,
394
+ // not evidence that THIS host is installed (host isolation). The fallback's
395
+ // whole purpose is to recognize a MARKER-STRIPPED (bare) copy of our own hook,
396
+ // so we strip all marked-block lines, then parse the remainder with the real
397
+ // TOML parser.
398
+ let parsed;
399
+ try {
400
+ parsed = parseToml(stripManagedBlockLines(contents));
401
+ }
402
+ catch {
403
+ return false;
404
+ }
405
+ if (parsed === null || typeof parsed !== "object")
406
+ return false;
407
+ return hooksArrayHasCleanEntry(parsed.hooks, expectedCommand);
408
+ }
409
+ /**
410
+ * Whole-file parser check for the FOUND (marked-block present) path. The line
411
+ * grammar in `validateBlockBody` only inspects the lines BETWEEN the BEGIN/END
412
+ * markers — but a `matcher = "*"` (or a stray key) placed AFTER the END comment
413
+ * is still part of the SAME `[[hooks]]` TOML table (comments don't terminate a
414
+ * table), so kimi-code loads it and DISABLES the hook (`new RegExp("*")` throws)
415
+ * while the marked body looks clean → a fail-open `installed:true`. Parsing the
416
+ * whole file exactly as kimi-code does attributes that trailing key to our entry
417
+ * and rejects it. Fail-closed on any parse error. (Fable/kimi audit 2026-07-17.)
418
+ */
419
+ function foundHookEntryIsClean(contents, expectedCommand) {
420
+ let parsed;
421
+ try {
422
+ parsed = parseToml(contents);
423
+ }
424
+ catch {
425
+ return false;
426
+ }
427
+ if (parsed === null || typeof parsed !== "object")
428
+ return false;
429
+ return hooksArrayHasCleanEntry(parsed.hooks, expectedCommand);
430
+ }
431
+ export function evaluateInstalled(contents, expectedCommand, opts) {
432
+ const { state } = parseManagedBlock(contents, opts.hostId);
433
+ if (state.kind === "absent") {
434
+ // kimi-code re-serializes config.toml on login/settings writes via a
435
+ // comment-dropping TOML stringifier (`stringifyToml(configToTomlData(...))`
436
+ // in packages/agent-core/src/config/toml.ts — smol-toml has no comment
437
+ // support), which deletes our BEGIN/END markers while leaving the
438
+ // `[[hooks]]` TABLE (data) intact and enforcing. Markers were never the
439
+ // security boundary — the byte-exact canonical command is — so a
440
+ // marker-less table that is EXACTLY our clean enforcing hook still counts
441
+ // as installed. This decision is made against the REAL TOML parser (not a
442
+ // line scanner): a line scanner has TOML-semantic blind spots that a
443
+ // reviewer panel demonstrated let a hook-DISABLING `matcher` hide from it —
444
+ // after a blank line, behind a quoted `"matcher"` key, or past a multiline
445
+ // array value — while kimi-code still honors it (`new RegExp("*")` throws →
446
+ // hook disabled → auto-approve). The parser sees the whole table regardless
447
+ // of layout, so `hasCleanEnforcingHookEntry` is immune to all three.
448
+ if (hasCleanEnforcingHookEntry(contents, expectedCommand)) {
449
+ return {
450
+ installed: true,
451
+ via: "bare-table",
452
+ note: "this host's managed-block markers are missing (kimi-code rewrites its config on login/settings changes and strips all comments), but the hook table with the exact canonical command is present and enforcing. Run /kimi:setup to re-adorn the markers.",
453
+ state,
454
+ };
455
+ }
456
+ // Markers absent AND no byte-exact bare table. The common real-world cause
457
+ // is BOTH failures at once: kimi-code strips markers on every login/settings
458
+ // write, and a plugin update then moves the version-stamped hook path. The
459
+ // 2026-07-25 Codex incident was exactly this pair. Classify the drift from
460
+ // this host's own stale bare table so callers still get an actionable axis
461
+ // instead of a bare "not present".
462
+ const staleOwn = findBareApprovalHookTables(contents).find((table) => table.command !== expectedCommand &&
463
+ isOurApprovalHookCommand(table.command) &&
464
+ hostIdFromHookCommand(table.command) === opts.hostId);
465
+ return {
466
+ installed: false,
467
+ reason: "managed block is not present",
468
+ state,
469
+ ...(staleOwn !== undefined
470
+ ? { drift: classifyHookCommandDrift(staleOwn.command, expectedCommand) }
471
+ : {}),
472
+ };
473
+ }
474
+ if (state.kind === "duplicate") {
475
+ return {
476
+ installed: false,
477
+ reason: `duplicate managed blocks detected at lines ${state.beginLines
478
+ .map((line) => line + 1)
479
+ .join(", ")}. Run /kimi:setup --uninstall, then /kimi:setup.`,
480
+ state,
481
+ };
482
+ }
483
+ if (state.kind === "orphan") {
484
+ return {
485
+ installed: false,
486
+ reason: `${state.detail} marker. Run /kimi:setup --uninstall to clear.`,
487
+ state,
488
+ };
489
+ }
490
+ if (!state.valid) {
491
+ return {
492
+ installed: false,
493
+ reason: state.invalidReason ?? "managed block failed validation",
494
+ state,
495
+ };
496
+ }
497
+ if (state.commandPath !== expectedCommand) {
498
+ const classified = opts.nodeExists !== undefined
499
+ ? describeHookCommandDrift(state.commandPath, expectedCommand, opts.nodeExists)
500
+ : undefined;
501
+ return {
502
+ installed: false,
503
+ reason: classified ??
504
+ `installed block's command does not match the canonical command this companion would write. Run /kimi:setup to refresh. expected ${expectedCommand}; got ${state.commandPath}.`,
505
+ state,
506
+ drift: classifyHookCommandDrift(state.commandPath, expectedCommand),
507
+ };
508
+ }
509
+ // The marked body (lines between BEGIN/END) passed the line grammar and the
510
+ // command matches — but confirm with the REAL whole-file parse that no matcher
511
+ // or stray key rides our `[[hooks]]` table from OUTSIDE the markers (e.g. after
512
+ // the END comment, which does not terminate the TOML table). Such a key would
513
+ // load in kimi-code and disable/reject the hook while the body looks clean.
514
+ if (!foundHookEntryIsClean(contents, expectedCommand)) {
515
+ return {
516
+ installed: false,
517
+ reason: "the managed block's hook table carries a matcher or an unexpected key OUTSIDE the marked body (e.g. after the END marker) — kimi-code would load it and disable the hook. Run /kimi:setup --uninstall, then /kimi:setup to repair.",
518
+ state,
519
+ };
520
+ }
521
+ return { installed: true, via: "managed-block", state };
522
+ }
523
+ /**
524
+ * The line-oriented PRUNE scanner: find bare (marker-less) `[[hooks]]` tables
525
+ * matching our grammar so `findUnmanagedApprovalHookBlocks` can remove them.
526
+ *
527
+ * NOTE: the INSTALLED decision does NOT use this — it uses the real TOML parser
528
+ * (`hasCleanEnforcingHookEntry`), because a line scanner has TOML-semantic
529
+ * blind spots (a `matcher` hidden after a blank line / behind a quoted key /
530
+ * past a multiline array). This scanner is only for computing physical line
531
+ * RANGES to delete, where a conservative, layout-aware view is what we want.
532
+ *
533
+ * `[start, end)` (the range to delete) stays CONSERVATIVE: it covers only the
534
+ * contiguous non-blank body after the `[[hooks]]` header (stopping at the first
535
+ * blank line, next table header, or managed marker), so a prune never swallows
536
+ * adjacent user content past a blank separator. Separately, to avoid leaving a
537
+ * DANGLING `matcher` when a table has one after a blank line, the GRAMMAR check
538
+ * scans the table's full TOML span (to the next header/marker/EOF) for a
539
+ * `matcher` line and, if found, does not report the table at all (so it is
540
+ * never half-cut). `command` is the decoded shell command.
541
+ */
542
+ export function findBareApprovalHookTables(contents) {
543
+ const lines = contents.split("\n").map((line) => line.replace(/\r$/, ""));
544
+ const managed = new Set();
545
+ const paired = pairBlocks(lines);
546
+ if ("blocks" in paired) {
547
+ for (const block of paired.blocks) {
548
+ for (let i = block.beginLine; i <= block.endLine; i += 1)
549
+ managed.add(i);
550
+ }
551
+ }
552
+ const ranges = [];
553
+ for (let i = 0; i < lines.length; i += 1) {
554
+ if (managed.has(i))
555
+ continue;
556
+ if (!HOOKS_TABLE_RE.test(lines[i].trim()))
557
+ continue;
558
+ // Collect the contiguous table body.
559
+ let j = i + 1;
560
+ let commandDecoded = "";
561
+ let hasPreToolUseEvent = false;
562
+ let hasMatcher = false;
563
+ // A table we wrote contains ONLY event/command/timeout (+ comments). If any
564
+ // OTHER key appears — e.g. a multi-line `metadata = [ … ]` array, whose
565
+ // continuation lines would otherwise fool the `[`-boundary check and leave
566
+ // dangling TOML after a partial prune — treat the table as NOT ours and
567
+ // skip it. Conservative by design (Codex review): never partially cut a
568
+ // structure we don't fully recognize.
569
+ let simpleGrammar = true;
570
+ while (j < lines.length) {
571
+ const trimmed = lines[j].trim();
572
+ if (trimmed.length === 0)
573
+ break;
574
+ // Any TOML table header bounds the table — including one with a trailing
575
+ // comment (`[[permission.rules]] # note`). A bare leading `[` is a table
576
+ // header in TOML (value arrays are `key = [...]`), so stop there and
577
+ // never swallow the next table. (Codex review.)
578
+ if (trimmed.startsWith("["))
579
+ break;
580
+ if (BEGIN_LINE_RE.test(trimmed) || END_LINE_RE.test(trimmed))
581
+ break;
582
+ if (trimmed.startsWith("#")) {
583
+ j += 1;
584
+ continue;
585
+ }
586
+ if (EVENT_LINE_RE.test(trimmed)) {
587
+ hasPreToolUseEvent = true;
588
+ }
589
+ else if (MATCHER_LINE_RE.test(trimmed)) {
590
+ hasMatcher = true;
591
+ }
592
+ else {
593
+ const decoded = decodeManagedCommandLine(trimmed);
594
+ if (decoded !== null) {
595
+ commandDecoded = decoded;
596
+ }
597
+ else if (!TIMEOUT_LINE_RE.test(trimmed)) {
598
+ // Some key other than event/command/timeout/matcher — not our shape.
599
+ simpleGrammar = false;
600
+ }
601
+ }
602
+ j += 1;
603
+ }
604
+ // CRITICAL (Opus review, HIGH): a blank line does NOT end a TOML table —
605
+ // table membership runs from `[[hooks]]` to the NEXT table header (`[…`) or
606
+ // EOF, across any blank lines. The contiguous loop above stops at the first
607
+ // blank, so a `matcher = "*"` placed AFTER a blank line is still part of
608
+ // this hook table for kimi-code's real parser: it loads (matcher is a
609
+ // schema-valid string, so whole-file validation passes), then throws at
610
+ // `new RegExp("*")` at hook-execution time and SILENTLY DISABLES the hook.
611
+ // If we only checked the contiguous body we'd report `installed: true` for
612
+ // a hook that is actually off — the exact auto-approve bypass this plugin
613
+ // exists to prevent. So scan the REST of the table's TOML span for a
614
+ // matcher and reject the table if one appears anywhere. (The prune line
615
+ // range stays `[start, j)` — conservative, never eating past the blank —
616
+ // because a rejected table is never pruned anyway.) Foreign keys after a
617
+ // blank are NOT re-checked here: an unknown key fails kimi-code's `.strict()`
618
+ // hook schema, so `validateKimiHookSetForEnvironment` rejects the whole file
619
+ // upstream; only `matcher` is schema-valid-but-hook-disabling.
620
+ let matcherBeyondBody = false;
621
+ for (let k = j; k < lines.length; k += 1) {
622
+ const t = lines[k].trim();
623
+ // The current table's TOML span ends at the next table header or a
624
+ // managed marker; a matcher past that point belongs to a different table.
625
+ if (t.startsWith("[") || BEGIN_LINE_RE.test(t) || END_LINE_RE.test(t))
626
+ break;
627
+ if (MATCHER_LINE_RE.test(t)) {
628
+ matcherBeyondBody = true;
629
+ break;
630
+ }
631
+ }
632
+ // Only report a table that matches our managed-block grammar EXACTLY:
633
+ // `event = "PreToolUse"`, no matcher anywhere in its TOML span, and no keys
634
+ // beyond event/command/timeout in the contiguous body. (Codex + Kimi review
635
+ // — reduces false positives and avoids corrupting a multi-line-array table.)
636
+ // Command-identity filtering (ours? whose host?) is the caller's concern —
637
+ // see `findUnmanagedApprovalHookBlocks` and `evaluateInstalled`.
638
+ if (simpleGrammar &&
639
+ commandDecoded.length > 0 &&
640
+ hasPreToolUseEvent &&
641
+ !hasMatcher &&
642
+ !matcherBeyondBody) {
643
+ ranges.push({ start: i, end: j, command: commandDecoded });
644
+ }
645
+ }
646
+ return ranges;
647
+ }
648
+ /**
649
+ * Find orphaned, marker-less `[[hooks]]` tables that are unambiguously THIS
650
+ * plugin's approval hook (canonical `'<node>' '<...>/approval-hook.js'` command
651
+ * under a kimi-marketplace / kimi-plugin-cc tree) and live OUTSIDE any managed
652
+ * BEGIN/END block. These arise when a kimi-code config rewrite strips the
653
+ * marker comments off a live block (kimi-code's TOML stringifier drops all
654
+ * comments), or from pre-v1.7.0 installs / host-path churn.
655
+ *
656
+ * HOST SCOPING (v1.8.2): pass `ownedBy` (a host id from `resolveHostId`) to
657
+ * restrict the result to tables whose command path derives to THAT host via
658
+ * `hostIdFromHookCommand`. Pruning MUST pass it — after a kimi-code comment
659
+ * strip, another host's marker-less table is that host's LIVE hook, and a
660
+ * host-blind prune silently disarms it (the pre-v1.8.2 seesaw: each host's
661
+ * setup deleted the other's enforcement). Omitting `ownedBy` is the explicit
662
+ * every-host sweep reserved for `/kimi:setup --uninstall --all`.
663
+ */
664
+ export function findUnmanagedApprovalHookBlocks(contents, ownedBy, alsoMatchCommand) {
665
+ return findBareApprovalHookTables(contents).filter((table) => {
666
+ // A table whose command byte-equals what THIS host would write is
667
+ // unambiguously this host's own (marker-stripped) hook — the tightest
668
+ // possible ownership signal, stronger than any path derivation. Match it
669
+ // regardless of `ownedBy`, so the install re-adorn works even when a
670
+ // `KIMI_PLUGIN_CC_HOST_ID` override disagrees with the path-derived host
671
+ // (`hostIdFromHookCommand`) — otherwise setup would append a duplicate
672
+ // block beside the identical bare table (Kimi review F4). Never widens the
673
+ // prune: another host's table and a hand-rolled hook carry different
674
+ // commands, so they never equal this host's canonical command.
675
+ if (alsoMatchCommand !== undefined && table.command === alsoMatchCommand)
676
+ return true;
677
+ if (!isOurApprovalHookCommand(table.command))
678
+ return false;
679
+ if (ownedBy === undefined)
680
+ return true;
681
+ return hostIdFromHookCommand(table.command) === ownedBy;
682
+ });
683
+ }
684
+ export const MARKERS = {
685
+ TAG: MARKER_TAG,
686
+ BEGIN_LINE_RE,
687
+ END_LINE_RE,
688
+ };
689
+ /**
690
+ * Decode a TOML 1.0 basic-string body (the bytes BETWEEN the quotes —
691
+ * the surrounding `"..."` is stripped by the capture group). Handles
692
+ * the six standard escapes plus `\\` and `\"`. Unknown escapes fall
693
+ * through as-is, mirroring permissive parsers; the canonical command
694
+ * we compare against never contains them, so a mismatch surfaces as
695
+ * "installed: false" rather than a parse error.
696
+ *
697
+ * The inverse of `tomlBasicString` in `runtime/hooks/install-paths.ts`'s
698
+ * sibling helper in setup.ts. Audit re-review (reports 33/34) flagged
699
+ * the missing decode as a verifier false-fail for apostrophe paths.
700
+ */
701
+ function decodeTomlBasicString(raw) {
702
+ let out = "";
703
+ for (let i = 0; i < raw.length; i += 1) {
704
+ const ch = raw[i];
705
+ if (ch !== "\\") {
706
+ out += ch;
707
+ continue;
708
+ }
709
+ const next = raw[i + 1];
710
+ if (next === undefined) {
711
+ out += ch;
712
+ continue;
713
+ }
714
+ i += 1;
715
+ switch (next) {
716
+ case "\\":
717
+ out += "\\";
718
+ break;
719
+ case "\"":
720
+ out += "\"";
721
+ break;
722
+ case "b":
723
+ out += "\b";
724
+ break;
725
+ case "t":
726
+ out += "\t";
727
+ break;
728
+ case "n":
729
+ out += "\n";
730
+ break;
731
+ case "f":
732
+ out += "\f";
733
+ break;
734
+ case "r":
735
+ out += "\r";
736
+ break;
737
+ default:
738
+ // Pass unknown escapes through verbatim. The canonical command
739
+ // never produces them, so an inequality surfaces as a benign
740
+ // "installed: false" rather than a parse exception.
741
+ out += `\\${next}`;
742
+ }
743
+ }
744
+ return out;
745
+ }