sessionpipe-conformance 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,10 +1,19 @@
1
- // sessionpipe-conformance 0.1.0 · Apache-2.0 · https://sessionpipe.org
1
+ // sessionpipe-conformance 0.1.1 · Apache-2.0 · https://sessionpipe.org
2
2
 
3
3
  // src/index.ts
4
4
  import { readFileSync as readFileSync6 } from "node:fs";
5
5
 
6
6
  // ../core/src/adapters/antigravity.ts
7
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
7
+ import {
8
+ existsSync as existsSync2,
9
+ mkdirSync as mkdirSync2,
10
+ readdirSync as readdirSync2,
11
+ readFileSync as readFileSync2,
12
+ realpathSync,
13
+ statSync as statSync2,
14
+ unlinkSync as unlinkSync2,
15
+ writeFileSync as writeFileSync2
16
+ } from "node:fs";
8
17
  import path3 from "node:path";
9
18
 
10
19
  // ../core/src/paths.ts
@@ -212,16 +221,42 @@ function readAntigravity(lines, from) {
212
221
  }
213
222
 
214
223
  // ../core/src/adapters/claude-shaped.ts
215
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
224
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
216
225
  import path2 from "node:path";
217
226
  var OURS = /sessionpipe[^"\n]*[\\/]hook\.js"?\s+[a-z][a-z-]*\s+[A-Za-z]+\s*$/;
218
227
  var isOurs = (h) => Array.isArray(h?.hooks) && h.hooks.some((x) => typeof x.command === "string" && OURS.test(x.command));
219
- function readJson(file) {
228
+ function readJsonFile(file) {
229
+ let raw;
220
230
  try {
221
- return JSON.parse(readFileSync(file, "utf8"));
231
+ raw = readFileSync(file, "utf8");
222
232
  } catch {
223
- return {};
233
+ return { ok: true, existed: false, value: {}, raw: "", indent: " ", eol: "\n", finalNewline: true };
224
234
  }
235
+ let value;
236
+ try {
237
+ value = JSON.parse(raw);
238
+ } catch {
239
+ const hint = /^\s*\/\/|\/\*/m.test(raw) ? "not plain JSON (it has comments)" : "not plain JSON";
240
+ return { ok: false, existed: true, reason: hint };
241
+ }
242
+ if (!value || typeof value !== "object" || Array.isArray(value))
243
+ return { ok: false, existed: true, reason: "not a JSON object" };
244
+ const eol = raw.includes("\r\n") ? "\r\n" : "\n";
245
+ const m = /^( +|\t+)\S/m.exec(raw);
246
+ const indent = m?.[1] ?? " ";
247
+ return {
248
+ ok: true,
249
+ existed: true,
250
+ value,
251
+ raw,
252
+ indent,
253
+ eol,
254
+ finalNewline: /\r?\n$/.test(raw)
255
+ };
256
+ }
257
+ function formatJson(value, f) {
258
+ const text = JSON.stringify(value, null, f.indent).replace(/\n/g, f.eol);
259
+ return f.finalNewline ? text + f.eol : text;
225
260
  }
226
261
  function entryFor(event, harness, cmd, o) {
227
262
  const matcher = o.matcherFor(event);
@@ -231,9 +266,22 @@ function entryFor(event, harness, cmd, o) {
231
266
  };
232
267
  }
233
268
  function installClaudeShaped(file, harness, events, cmd, o) {
234
- const settings2 = readJson(file);
269
+ const f = readJsonFile(file);
270
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
271
+ const settings2 = f.value;
235
272
  const hooks = settings2.hooks && typeof settings2.hooks === "object" ? settings2.hooks : {};
236
273
  let changed = false;
274
+ const want = new Set(events);
275
+ for (const ev of Object.keys(hooks)) {
276
+ if (want.has(ev)) continue;
277
+ const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
278
+ const kept = list.filter((h) => !isOurs(h));
279
+ if (kept.length !== list.length) {
280
+ changed = true;
281
+ if (kept.length) hooks[ev] = kept;
282
+ else delete hooks[ev];
283
+ }
284
+ }
237
285
  for (const ev of events) {
238
286
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
239
287
  const entry2 = entryFor(ev, harness, cmd, o);
@@ -252,19 +300,19 @@ function installClaudeShaped(file, harness, events, cmd, o) {
252
300
  if (changed) {
253
301
  settings2.hooks = hooks;
254
302
  mkdirSync(path2.dirname(file), { recursive: true });
255
- writeFileSync(file, `${JSON.stringify(settings2, null, 2)}
256
- `);
303
+ writeFileSync(file, formatJson(settings2, f));
257
304
  }
258
- return { file, changed };
305
+ return { file, changed, created: !f.existed && changed };
259
306
  }
260
- function uninstallClaudeShaped(file, events) {
307
+ function uninstallClaudeShaped(file, events, opts = {}) {
261
308
  if (!existsSync(file)) return { file, changed: false };
262
- const raw = readFileSync(file, "utf8");
263
- const settings2 = readJson(file);
309
+ const f = readJsonFile(file);
310
+ if (!f.ok) return { file, changed: false, skipped: true, note: `${f.reason}; not touched` };
311
+ const settings2 = f.value;
264
312
  const hooks = settings2.hooks;
265
313
  if (!hooks || typeof hooks !== "object") return { file, changed: false };
266
314
  let changed = false;
267
- for (const ev of events) {
315
+ for (const ev of [...events, ...Object.keys(hooks)]) {
268
316
  const list = Array.isArray(hooks[ev]) ? hooks[ev] : [];
269
317
  const kept = list.filter((h) => !isOurs(h));
270
318
  if (kept.length !== list.length) {
@@ -275,13 +323,17 @@ function uninstallClaudeShaped(file, events) {
275
323
  }
276
324
  if (!changed) return { file, changed: false };
277
325
  if (!Object.keys(hooks).length) delete settings2.hooks;
278
- const next = `${JSON.stringify(settings2, null, 2)}
279
- `;
280
- if (next !== raw) writeFileSync(file, next);
326
+ if (opts.created && !Object.keys(settings2).length) {
327
+ unlinkSync(file);
328
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
329
+ }
330
+ const next = formatJson(settings2, f);
331
+ if (next !== f.raw) writeFileSync(file, next);
281
332
  return { file, changed: true };
282
333
  }
283
334
  function installedClaudeShaped(file, harness, events, cmd, o) {
284
- const hooks = readJson(file).hooks ?? {};
335
+ const f = readJsonFile(file);
336
+ const hooks = f.ok ? f.value.hooks ?? {} : {};
285
337
  let present = 0;
286
338
  let current = 0;
287
339
  for (const ev of events) {
@@ -290,6 +342,9 @@ function installedClaudeShaped(file, harness, events, cmd, o) {
290
342
  if (mine.length) present++;
291
343
  if (mine.length === 1 && JSON.stringify(mine[0]) === JSON.stringify(entryFor(ev, harness, cmd, o))) current++;
292
344
  }
345
+ for (const ev of Object.keys(hooks))
346
+ if (!events.includes(ev) && (Array.isArray(hooks[ev]) ? hooks[ev] : []).some((h) => isOurs(h)))
347
+ return "stale";
293
348
  if (!present) return "missing";
294
349
  return current === events.length ? "current" : "stale";
295
350
  }
@@ -304,10 +359,13 @@ var projectsDir = (env) => path3.join(gemini(env), "config", "projects");
304
359
  var dataDirs = (env) => ["antigravity", "antigravity-cli", "antigravity-ide"].map((d) => path3.join(gemini(env), d));
305
360
  var ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
306
361
  var validTool = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
307
- function entry(cmd) {
362
+ function entry(cmd, lean) {
308
363
  const handler = (e) => ({ type: "command", command: cmd([NAME, e]).command, timeout: 5 });
309
364
  const out = { enabled: true };
310
- for (const e of AG_EVENTS) out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
365
+ for (const e of AG_EVENTS) {
366
+ if (lean && e === "PreToolUse") continue;
367
+ out[e] = /Tool/.test(e) ? [{ matcher: "*", hooks: [handler(e)] }] : [handler(e)];
368
+ }
311
369
  return out;
312
370
  }
313
371
  var antigravity = {
@@ -315,34 +373,49 @@ var antigravity = {
315
373
  events: AG_EVENTS,
316
374
  detect: (env = process.env) => existsSync2(path3.join(gemini(env), "config")) || dataDirs(env).some((d) => existsSync2(d)),
317
375
  configFiles: (env = process.env) => [hooksFile(env)],
318
- install(cmd, env = process.env) {
376
+ install(cmd, env = process.env, opts = {}) {
319
377
  const file = hooksFile(env);
320
- const hooks = readJson(file);
321
- const e = entry(cmd);
378
+ const f = readJsonFile(file);
379
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
380
+ const hooks = f.value;
381
+ const e = entry(cmd, opts.lean);
322
382
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
323
383
  hooks[HOOK_NAME] = e;
324
384
  mkdirSync2(path3.dirname(file), { recursive: true });
325
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
326
- `);
385
+ writeFileSync2(file, formatJson(hooks, f));
327
386
  return [
328
- { file, changed: true, note: "hooks.json is read when a conversation starts; open ones report after a restart" }
387
+ {
388
+ file,
389
+ changed: true,
390
+ created: !f.existed,
391
+ note: "hooks.json is read when a conversation starts; open ones report after a restart"
392
+ }
329
393
  ];
330
394
  },
331
- uninstall(env = process.env) {
395
+ uninstall(env = process.env, opts = {}) {
332
396
  const file = hooksFile(env);
333
397
  if (!existsSync2(file)) return [{ file, changed: false }];
334
- const hooks = readJson(file);
398
+ const f = readJsonFile(file);
399
+ if (!f.ok) return [{ file, changed: false, skipped: true, note: `${f.reason}; not touched` }];
400
+ const hooks = f.value;
335
401
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
336
402
  delete hooks[HOOK_NAME];
337
- writeFileSync2(file, `${JSON.stringify(hooks, null, 2)}
338
- `);
403
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) {
404
+ unlinkSync2(file);
405
+ return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
406
+ }
407
+ writeFileSync2(file, formatJson(hooks, f));
339
408
  return [{ file, changed: true }];
340
409
  },
341
- installed(cmd, env = process.env) {
410
+ installed(cmd, env = process.env, opts = {}) {
342
411
  const file = hooksFile(env);
343
- const have = readJson(file)[HOOK_NAME];
412
+ const f = readJsonFile(file);
413
+ const have = f.ok ? f.value[HOOK_NAME] : void 0;
344
414
  return [
345
- { file, state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd)) ? "current" : "stale" }
415
+ {
416
+ file,
417
+ state: !have ? "missing" : JSON.stringify(have) === JSON.stringify(entry(cmd, opts.lean)) ? "current" : "stale"
418
+ }
346
419
  ];
347
420
  },
348
421
  fromHook(input) {
@@ -608,8 +681,11 @@ var CLAUDE_EVENTS = [
608
681
  "SubagentStop",
609
682
  "PreCompact",
610
683
  "PostCompact",
611
- "SessionEnd"
684
+ "SessionEnd",
685
+ "StopFailure"
612
686
  ];
687
+ var LEAN_SKIP = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUseFailure"]);
688
+ var eventsFor = (lean) => lean ? CLAUDE_EVENTS.filter((e) => !LEAN_SKIP.has(e)) : [...CLAUDE_EVENTS];
613
689
  var OPTS = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
614
690
  var DESKTOP_ID = /^local_[A-Za-z0-9-]{1,64}$/;
615
691
  function claudeDirs(env = process.env, home = HOME) {
@@ -641,11 +717,13 @@ var claudeCode = {
641
717
  events: CLAUDE_EVENTS,
642
718
  detect: (env = process.env) => claudeDirs(env).some((d) => existsSync3(d)),
643
719
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
644
- install: (cmd, env = process.env) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)),
645
- uninstall: (env = process.env) => claudeDirs(env).map((d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS)),
646
- installed: (cmd, env = process.env) => claudeDirs(env).map((d) => ({
720
+ install: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => installClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)),
721
+ uninstall: (env = process.env, opts = {}) => claudeDirs(env).map(
722
+ (d) => uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, { created: opts.created?.includes(settingsIn(d)) })
723
+ ),
724
+ installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
647
725
  file: settingsIn(d),
648
- state: installedClaudeShaped(settingsIn(d), NAME2, CLAUDE_EVENTS, cmd, OPTS)
726
+ state: installedClaudeShaped(settingsIn(d), NAME2, eventsFor(opts.lean), cmd, OPTS)
649
727
  })),
650
728
  fromHook(input) {
651
729
  const event = input.argv[1] ?? "";
@@ -736,6 +814,15 @@ var claudeCode = {
736
814
  case "Stop":
737
815
  events.push(ev("turn.ended", { turn_id, reason: "stop" }));
738
816
  break;
817
+ case "StopFailure":
818
+ events.push(
819
+ ev("turn.ended", {
820
+ turn_id,
821
+ reason: "error",
822
+ error: typeof s.error === "string" ? s.error.slice(0, 200) : void 0
823
+ })
824
+ );
825
+ break;
739
826
  case "SubagentStart":
740
827
  events.push(
741
828
  ev("subagent.started", {
@@ -1247,6 +1334,7 @@ var CODEX_EVENTS = [
1247
1334
  "SessionEnd"
1248
1335
  ];
1249
1336
  var OPTS2 = { matcherFor: (e) => /Tool|Permission/.test(e) ? "" : void 0, timeout: 5 };
1337
+ var eventsFor2 = (lean) => lean ? CODEX_EVENTS.filter((e) => e !== "PreToolUse") : [...CODEX_EVENTS];
1250
1338
  var codexHome = (env) => env.CODEX_HOME || path5.join(HOME, ".codex");
1251
1339
  var hooksFile2 = (env) => path5.join(codexHome(env), "hooks.json");
1252
1340
  var configFile = (env) => path5.join(codexHome(env), "config.toml");
@@ -1295,7 +1383,8 @@ function installNotify(cmd, env) {
1295
1383
  return {
1296
1384
  file,
1297
1385
  changed: false,
1298
- note: `left alone: notify runs ${n.raw.slice(0, 80)} (hooks.json carries the events; the notify fallback is off)`
1386
+ skipped: true,
1387
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. Codex will report nothing until you approve the hooks: start \`codex\` and accept the hook review`
1299
1388
  };
1300
1389
  }
1301
1390
  write(withNotify(text, line));
@@ -1352,14 +1441,21 @@ var codex = {
1352
1441
  events: CODEX_EVENTS,
1353
1442
  detect: (env = process.env) => existsSync4(codexHome(env)),
1354
1443
  configFiles: (env = process.env) => [hooksFile2(env), configFile(env)],
1355
- install: (cmd, env = process.env) => [
1356
- installClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2),
1444
+ install: (cmd, env = process.env, opts = {}) => [
1445
+ installClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1357
1446
  installNotify(cmd, env)
1358
1447
  ],
1359
- uninstall: (env = process.env) => [uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS), uninstallNotify(env)],
1360
- installed: (cmd, env = process.env) => {
1448
+ uninstall: (env = process.env, opts = {}) => [
1449
+ uninstallClaudeShaped(hooksFile2(env), CODEX_EVENTS, { created: opts.created?.includes(hooksFile2(env)) }),
1450
+ uninstallNotify(env)
1451
+ ],
1452
+ installed: (cmd, env = process.env, opts = {}) => {
1361
1453
  const out = [
1362
- { file: hooksFile2(env), state: installedClaudeShaped(hooksFile2(env), NAME3, CODEX_EVENTS, cmd, OPTS2) }
1454
+ {
1455
+ file: hooksFile2(env),
1456
+ state: installedClaudeShaped(hooksFile2(env), NAME3, eventsFor2(opts.lean), cmd, OPTS2),
1457
+ note: "Codex runs non-managed hooks only after the review in `codex` (/hooks); until then nothing is reported unless the notify fallback is ours"
1458
+ }
1363
1459
  ];
1364
1460
  let text = "";
1365
1461
  try {
@@ -1547,6 +1643,7 @@ function codexFacts(file) {
1547
1643
  }
1548
1644
 
1549
1645
  // ../core/src/adapters/gemini-cli.ts
1646
+ import { execFileSync as execFileSync3 } from "node:child_process";
1550
1647
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
1551
1648
  import path6 from "node:path";
1552
1649
  var NAME4 = "gemini-cli";
@@ -1564,17 +1661,61 @@ var OPTS3 = { matcherFor: (e) => /Tool/.test(e) ? ".*" : void 0, timeout: 5e3 };
1564
1661
  var geminiDir = (env) => env.GEMINI_CLI_HOME || path6.join(HOME, ".gemini");
1565
1662
  var settings = (env) => path6.join(geminiDir(env), "settings.json");
1566
1663
  var validTool4 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1664
+ var eventsFor3 = (lean) => lean ? GEMINI_EVENTS.filter((e) => e !== "BeforeTool") : [...GEMINI_EVENTS];
1665
+ function geminiHooksActive(settingsFile) {
1666
+ let tools;
1667
+ try {
1668
+ tools = JSON.parse(readFileSync5(settingsFile, "utf8")).tools;
1669
+ } catch {
1670
+ }
1671
+ if (tools?.enableHooks === true) return { active: true };
1672
+ if (tools?.enableHooks === false)
1673
+ return { active: false, note: "inactive: tools.enableHooks is false in settings.json" };
1674
+ let version = "";
1675
+ try {
1676
+ version = execFileSync3("gemini", ["--version"], {
1677
+ encoding: "utf8",
1678
+ timeout: 4e3,
1679
+ stdio: ["ignore", "pipe", "ignore"]
1680
+ }).trim();
1681
+ } catch {
1682
+ return {
1683
+ active: true,
1684
+ note: "could not run `gemini --version`; hooks need tools.enableHooks on versions before 0.46"
1685
+ };
1686
+ }
1687
+ const m = /(\d+)\.(\d+)/.exec(version);
1688
+ if (m && (Number(m[1]) > 0 || Number(m[2]) >= 46)) return { active: true };
1689
+ return {
1690
+ active: false,
1691
+ note: `inactive: Gemini CLI ${version || "?"} runs hooks only with "tools": {"enableHooks": true} in settings.json`
1692
+ };
1693
+ }
1567
1694
  var geminiCli = {
1568
1695
  name: NAME4,
1569
1696
  events: GEMINI_EVENTS,
1570
1697
  // ~/.gemini also belongs to Antigravity; Gemini CLI is here when its settings or its chat logs exist.
1571
1698
  detect: (env = process.env) => existsSync5(settings(env)) || existsSync5(path6.join(geminiDir(env), "tmp")),
1572
1699
  configFiles: (env = process.env) => [settings(env)],
1573
- install: (cmd, env = process.env) => [installClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3)],
1574
- uninstall: (env = process.env) => [uninstallClaudeShaped(settings(env), GEMINI_EVENTS)],
1575
- installed: (cmd, env = process.env) => [
1576
- { file: settings(env), state: installedClaudeShaped(settings(env), NAME4, GEMINI_EVENTS, cmd, OPTS3) }
1700
+ install: (cmd, env = process.env, opts = {}) => {
1701
+ const r = installClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1702
+ const a = geminiHooksActive(settings(env));
1703
+ return [a.note && !r.skipped ? { ...r, note: [r.note, a.note].filter(Boolean).join("; ") } : r];
1704
+ },
1705
+ uninstall: (env = process.env, opts = {}) => [
1706
+ uninstallClaudeShaped(settings(env), GEMINI_EVENTS, { created: opts.created?.includes(settings(env)) })
1577
1707
  ],
1708
+ installed: (cmd, env = process.env, opts = {}) => {
1709
+ const state = installedClaudeShaped(settings(env), NAME4, eventsFor3(opts.lean), cmd, OPTS3);
1710
+ const a = geminiHooksActive(settings(env));
1711
+ return [
1712
+ {
1713
+ file: settings(env),
1714
+ state: state === "current" && !a.active ? "inactive" : state,
1715
+ ...a.note ? { note: a.note } : {}
1716
+ }
1717
+ ];
1718
+ },
1578
1719
  fromHook(input) {
1579
1720
  const event = input.argv[1] ?? "";
1580
1721
  let s = {};