skalpel 4.0.26 → 4.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/install.mjs CHANGED
@@ -252,46 +252,80 @@ function tomlBlock(event, marker, command) {
252
252
  return `\n${tomlMark(event, marker)}\n[[hooks.${event}]]\n[[hooks.${event}.hooks]]\ntype = "command"\ncommand = ${tomlString(command)}\ntimeout = 8\n`;
253
253
  }
254
254
 
255
+ // Remove ONLY skalpel's own hook blocks from a Codex config.toml, leaving every foreign block
256
+ // (someone else's `[[hooks.UserPromptSubmit]]`, a `[model]` table, comments, blank lines) byte-for-byte
257
+ // intact. Two identifiers mark a block as ours: the managed marker comment we write above every block,
258
+ // and — for legacy pre-marker installs — a `command` whose basename isOurs. The strip must be robust
259
+ // to a foreign hook block appearing BEFORE or AFTER ours: the previous fixed-width `i += 6` walk and
260
+ // the "stop only at the next `[[hooks.<event>]]` header" bound both mis-scanned that layout, so a
261
+ // foreign block sitting ahead of ours swallowed our marker line (defeating the marker fast-path) and
262
+ // our own block — whose `command = "node \"…\""` line can't be parsed by the command regex — was then
263
+ // never recognized, leaving uninstall a no-op and re-install appending DUPLICATES.
255
264
  function stripManagedTomlBlocks(txt) {
256
265
  const lines = txt.split("\n");
257
266
  const out = [];
258
267
  let removed = 0;
259
268
 
269
+ const HOOK_HEADER = /^\s*\[\[hooks\.(UserPromptSubmit|SessionStart)\]\]\s*$/;
270
+ const isMarker = (line) => TOML_MARKERS.some((marker) => line.includes(marker));
271
+ const isBlank = (line) => line.trim() === "";
272
+ // Any TOML table header — `[table]` or `[[array-of-tables]]`, dotted keys, optional trailing comment.
273
+ const isHeader = (line) => /^\s*\[\[?[^\][]*\]\]?\s*(#.*)?$/.test(line);
274
+
275
+ // Extent of ONE hook block that begins at `start` (a `[[hooks.<event>]]` header). The block owns its
276
+ // `[[hooks.<event>.hooks]]` sub-tables and their keys; it ENDS at the first line that opens a NEW
277
+ // section — a blank line, a skalpel marker, or any table header that is NOT this block's own
278
+ // sub-table (so a following `[model]`, `[[hooks.SessionStart]]`, etc. is never absorbed). Returns the
279
+ // index one past the block.
280
+ const scanBlock = (start, event) => {
281
+ const ownSub = new RegExp(`^\\s*\\[\\[hooks\\.${event}\\.hooks\\]\\]\\s*$`);
282
+ let j = start + 1;
283
+ while (j < lines.length) {
284
+ const line = lines[j];
285
+ if (isBlank(line) || isMarker(line)) break;
286
+ if (isHeader(line) && !ownSub.test(line)) break;
287
+ j += 1;
288
+ }
289
+ return j;
290
+ };
291
+
260
292
  for (let i = 0; i < lines.length; ) {
261
- if (TOML_MARKERS.some((marker) => lines[i].includes(marker))) {
262
- i += 6;
293
+ // 1) A managed marker begins OUR block. Drop the marker and the `[[hooks.<event>]]` block that
294
+ // follows it, bounded by scanBlock — never relying on the command line, which is
295
+ // quoted-within-quotes and unparseable. A stray marker with no block after it is dropped alone.
296
+ if (isMarker(lines[i])) {
297
+ let j = i + 1;
298
+ const header = j < lines.length ? lines[j].match(HOOK_HEADER) : null;
299
+ if (header) j = scanBlock(j, header[1]);
263
300
  removed += 1;
301
+ i = j;
264
302
  continue;
265
303
  }
266
304
 
267
- const section = lines[i].match(/^\[\[hooks\.(UserPromptSubmit|SessionStart)\]\]\s*$/);
268
- if (!section) {
269
- out.push(lines[i]);
270
- i += 1;
305
+ // 2) A bare `[[hooks.<event>]]` header with no marker: a legacy pre-marker skalpel block OR a
306
+ // foreign hook. Gather the whole block and remove it ONLY if its command isOurs; foreign blocks
307
+ // pass through untouched.
308
+ const section = lines[i].match(HOOK_HEADER);
309
+ if (section) {
310
+ const event = section[1];
311
+ const marker = HOOKS.find(([hookEvent]) => hookEvent === event)?.[1];
312
+ const end = scanBlock(i, event);
313
+ const block = lines.slice(i, end);
314
+ const command = block
315
+ .map((line) => line.match(/^\s*command\s*=\s*"([^"]*)"\s*$/)?.[1])
316
+ .find(Boolean);
317
+ if (marker && isOurs(command, marker)) {
318
+ removed += 1;
319
+ } else {
320
+ out.push(...block);
321
+ }
322
+ i = end;
271
323
  continue;
272
324
  }
273
325
 
274
- const event = section[1];
275
- const marker = HOOKS.find(([hookEvent]) => hookEvent === event)?.[1];
276
- const block = [lines[i]];
277
- let j = i + 1;
278
- while (
279
- j < lines.length &&
280
- !/^\[\[hooks\.(UserPromptSubmit|SessionStart)\]\]\s*$/.test(lines[j])
281
- ) {
282
- j += 1;
283
- block.push(lines[j - 1]);
284
- }
285
-
286
- const command = block
287
- .map((line) => line.match(/^\s*command\s*=\s*"([^"]*)"\s*$/)?.[1])
288
- .find(Boolean);
289
- if (marker && isOurs(command, marker)) {
290
- removed += 1;
291
- } else {
292
- out.push(...block);
293
- }
294
- i = j;
326
+ // 3) Everything else — foreign tables, comments, blank lines — kept verbatim.
327
+ out.push(lines[i]);
328
+ i += 1;
295
329
  }
296
330
 
297
331
  return { text: out.join("\n"), removed };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "4.0.26",
3
+ "version": "4.0.27",
4
4
  "description": "Behavioral graph for AI-assisted coding — learns how you work and steers Claude Code + Codex in real time.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -116,7 +116,7 @@ function revealNote() {
116
116
  }
117
117
  }
118
118
 
119
- function main() {
119
+ async function main() {
120
120
  const payload = readPayload();
121
121
 
122
122
  // AHA CATCH REVEAL (opt-in). The single highest-priority line: the agent claimed done but its OWN proof,
@@ -149,4 +149,8 @@ function main() {
149
149
  // Clean session → COUNT-OR-NOTHING resolves to nothing. Emit no bytes.
150
150
  }
151
151
 
152
- main();
152
+ // Fail-open, matching the other three hooks (skalpel-hook / -session / -session-end): any error →
153
+ // print nothing → exit 0, so a thrown error can never spill a stack trace into the live status bar.
154
+ main()
155
+ .then(() => process.exit(0))
156
+ .catch(() => process.exit(0));