sessionpipe-conformance 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAeA,gFAAgF;AAChF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE;IAAE,MAAM,EAAE;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAanH"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAeA,gFAAgF;AAChF,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAE;IAAE,MAAM,EAAE;QAAE,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;CAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BnH"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.13.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.13.2 · Apache-2.0 · https://sessionpipe.org
3
3
 
4
4
  // src/cli.ts
5
5
  import { readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync5 } from "node:fs";
@@ -16,7 +16,7 @@ var homeDir = () => os.homedir();
16
16
  // ../core/src/adapters/claude-code.ts
17
17
  import { execFileSync as execFileSync2 } from "node:child_process";
18
18
  import { createHash } from "node:crypto";
19
- import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "node:fs";
19
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
20
20
  import { userInfo } from "node:os";
21
21
  import path3 from "node:path";
22
22
 
@@ -374,6 +374,41 @@ function readJson(file) {
374
374
  const f = readJsonFile(file);
375
375
  return f.ok ? f.value : {};
376
376
  }
377
+ function refused(file, e, what, existed) {
378
+ const { code, syscall } = e;
379
+ const why = `can't ${what} it (${code ?? String(e)})`;
380
+ if (what === "remove" || syscall === "open" || syscall === "mkdir")
381
+ return { file, changed: false, skipped: true, note: `${why}; not touched` };
382
+ return {
383
+ file,
384
+ changed: true,
385
+ skipped: true,
386
+ ...existed ? {} : { created: true },
387
+ note: `${why} part way; check the file before the harness reads it`
388
+ };
389
+ }
390
+ function writeBack(file, text, existed = true) {
391
+ try {
392
+ mkdirSync(path2.dirname(file), { recursive: true });
393
+ writeFileSync(file, text);
394
+ return null;
395
+ } catch (e) {
396
+ return refused(file, e, "write", existed);
397
+ }
398
+ }
399
+ function removeEmptied(file, empty) {
400
+ try {
401
+ unlinkSync(file);
402
+ return { file, changed: true, note: "removed (sessionpipe created it)" };
403
+ } catch (e) {
404
+ const bad = refused(file, e, "remove", true);
405
+ return writeBack(file, empty) ?? {
406
+ file,
407
+ changed: true,
408
+ note: `emptied; ${bad.note.replace(/; not touched$/, "")}, so the empty file stays`
409
+ };
410
+ }
411
+ }
377
412
  function entryFor(event, harness, cmd, o) {
378
413
  const matcher = o.matcherFor(event);
379
414
  return {
@@ -415,8 +450,8 @@ function installClaudeShaped(file, harness, events, cmd, o) {
415
450
  }
416
451
  if (changed) {
417
452
  settings2.hooks = hooks;
418
- mkdirSync(path2.dirname(file), { recursive: true });
419
- writeFileSync(file, formatJson(settings2, f));
453
+ const bad = writeBack(file, formatJson(settings2, f), f.existed);
454
+ if (bad) return bad;
420
455
  }
421
456
  return { file, changed, created: !f.existed && changed };
422
457
  }
@@ -439,12 +474,8 @@ function uninstallClaudeShaped(file, events, opts = {}) {
439
474
  }
440
475
  if (!changed) return { file, changed: false };
441
476
  if (!Object.keys(hooks).length) delete settings2.hooks;
442
- if (opts.created && !Object.keys(settings2).length) {
443
- unlinkSync(file);
444
- return { file, changed: true, note: "removed (sessionpipe created it)" };
445
- }
446
- writeFileSync(file, formatJson(settings2, f));
447
- return { file, changed: true };
477
+ if (opts.created && !Object.keys(settings2).length) return removeEmptied(file, formatJson(settings2, f));
478
+ return writeBack(file, formatJson(settings2, f)) ?? { file, changed: true };
448
479
  }
449
480
  function installedClaudeShaped(file, harness, events, cmd, o) {
450
481
  const f = readJsonFile(file);
@@ -523,7 +554,7 @@ var claudeCode = {
523
554
  configFiles: (env = process.env) => claudeDirs(env).map(settingsIn),
524
555
  install: (cmd, env = process.env, opts = {}) => claudeDirs(env).flatMap((d) => {
525
556
  const hooks = installClaudeShaped(settingsIn(d), NAME, eventsFor(opts.lean), cmd, OPTS);
526
- const line = opts.statusLine === void 0 ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
557
+ const line = opts.statusLine === void 0 || hooks.skipped ? null : statusLine(settingsIn(d), opts.statusLine, cmd);
527
558
  return line ? [hooks, line] : [hooks];
528
559
  }),
529
560
  uninstall: (env = process.env, opts = {}) => claudeDirs(env).map((d) => {
@@ -531,7 +562,9 @@ var claudeCode = {
531
562
  const hooks = uninstallClaudeShaped(settingsIn(d), CLAUDE_EVENTS, {
532
563
  created: opts.created?.includes(settingsIn(d))
533
564
  });
534
- return line && !hooks.changed ? line : hooks;
565
+ if (line?.changed && !line.skipped && hooks.skipped)
566
+ return { ...hooks, note: `status line removed, hooks not: ${hooks.note?.replace(/; not touched$/, "")}` };
567
+ return line && !hooks.changed && !hooks.skipped ? line : hooks;
535
568
  }),
536
569
  installed: (cmd, env = process.env, opts = {}) => claudeDirs(env).map((d) => ({
537
570
  file: settingsIn(d),
@@ -626,7 +659,8 @@ var claudeCode = {
626
659
  attention_id: call_id ?? permissionId(),
627
660
  kind: "permission",
628
661
  tool,
629
- message: tool ? `${tool}${describe(s.tool_input)}` : void 0
662
+ message: tool ? `${tool}${describe(s.tool_input)}` : void 0,
663
+ agent_id
630
664
  })
631
665
  );
632
666
  break;
@@ -635,7 +669,8 @@ var claudeCode = {
635
669
  ev("attention.needed", {
636
670
  attention_id: `notif-${Date.now()}`,
637
671
  kind: notifKind(s.notification_type),
638
- message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0
672
+ message: typeof s.message === "string" ? s.message.slice(0, 200) : void 0,
673
+ agent_id
639
674
  })
640
675
  );
641
676
  break;
@@ -760,8 +795,11 @@ function statusLine(file, want, cmd) {
760
795
  if (want ? ours && cur?.command === command && cur?.refreshInterval === LINE_REFRESH_S : !ours) return null;
761
796
  if (want) f.value.statusLine = { type: "command", command, refreshInterval: LINE_REFRESH_S };
762
797
  else delete f.value.statusLine;
763
- writeFileSync2(file, formatJson(f.value, f));
764
- return { file, changed: true, note: want ? "status line written (account limits)" : "status line removed" };
798
+ return writeBack(file, formatJson(f.value, f), f.existed) ?? {
799
+ file,
800
+ changed: true,
801
+ note: want ? "status line written (account limits)" : "status line removed"
802
+ };
765
803
  }
766
804
  var epochIso = (t) => typeof t === "number" && t > 0 && t < 1e11 ? new Date(t * 1e3).toISOString() : void 0;
767
805
  var WINDOW_MINUTES = { five_hour: 300, seven_day: 10080 };
@@ -1253,16 +1291,7 @@ function probe(bin, args) {
1253
1291
  }
1254
1292
 
1255
1293
  // ../core/src/adapters/antigravity.ts
1256
- import {
1257
- existsSync as existsSync3,
1258
- mkdirSync as mkdirSync2,
1259
- readdirSync as readdirSync3,
1260
- readFileSync as readFileSync4,
1261
- realpathSync,
1262
- statSync as statSync4,
1263
- unlinkSync as unlinkSync2,
1264
- writeFileSync as writeFileSync3
1265
- } from "node:fs";
1294
+ import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync4, realpathSync, statSync as statSync4 } from "node:fs";
1266
1295
  import path5 from "node:path";
1267
1296
  var NAME2 = "antigravity";
1268
1297
  var HOOK_NAME = "sessionpipe";
@@ -1294,10 +1323,8 @@ var antigravity = {
1294
1323
  const e = entry(cmd);
1295
1324
  if (JSON.stringify(hooks[HOOK_NAME]) === JSON.stringify(e)) return [{ file, changed: false }];
1296
1325
  hooks[HOOK_NAME] = e;
1297
- mkdirSync2(path5.dirname(file), { recursive: true });
1298
- writeFileSync3(file, formatJson(hooks, f));
1299
1326
  return [
1300
- {
1327
+ writeBack(file, formatJson(hooks, f), f.existed) ?? {
1301
1328
  file,
1302
1329
  changed: true,
1303
1330
  created: !f.existed,
@@ -1313,12 +1340,8 @@ var antigravity = {
1313
1340
  const hooks = f.value;
1314
1341
  if (!(HOOK_NAME in hooks)) return [{ file, changed: false }];
1315
1342
  delete hooks[HOOK_NAME];
1316
- if (opts.created?.includes(file) && !Object.keys(hooks).length) {
1317
- unlinkSync2(file);
1318
- return [{ file, changed: true, note: "removed (sessionpipe created it)" }];
1319
- }
1320
- writeFileSync3(file, formatJson(hooks, f));
1321
- return [{ file, changed: true }];
1343
+ if (opts.created?.includes(file) && !Object.keys(hooks).length) return [removeEmptied(file, formatJson(hooks, f))];
1344
+ return [writeBack(file, formatJson(hooks, f)) ?? { file, changed: true }];
1322
1345
  },
1323
1346
  installed(cmd, env = process.env) {
1324
1347
  const file = hooksFile(env);
@@ -1541,7 +1564,7 @@ function antigravityFacts(file, opts) {
1541
1564
  }
1542
1565
 
1543
1566
  // ../core/src/adapters/codex.ts
1544
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1567
+ import { existsSync as existsSync4, readdirSync as readdirSync4, readFileSync as readFileSync5 } from "node:fs";
1545
1568
  import path6 from "node:path";
1546
1569
 
1547
1570
  // ../core/src/adapters/codex-config.ts
@@ -1694,29 +1717,30 @@ var configFile = (env) => path6.join(codexHome(env), "config.toml");
1694
1717
  var sessionsDir = (env) => path6.join(codexHome(env), "sessions");
1695
1718
  var validTool3 = (n) => typeof n === "string" && /^[A-Za-z0-9_.:-]{1,120}$/.test(n);
1696
1719
  var endReason2 = (r) => r === "clear" || r === "logout" || r === "exit" ? r : "other";
1720
+ var UNTIL_APPROVED = "Codex will report nothing until you approve the hooks: start `codex` and accept the hook review";
1697
1721
  function installNotify(cmd, env) {
1698
1722
  const file = configFile(env);
1699
1723
  let text = "";
1724
+ let existed = true;
1700
1725
  try {
1701
1726
  text = readFileSync5(file, "utf8");
1702
- } catch {
1727
+ } catch (e) {
1728
+ existed = e.code !== "ENOENT";
1703
1729
  }
1704
1730
  const argv = cmd([NAME3, "notify"]).argv;
1705
1731
  const line = `notify = ${JSON.stringify(argv)}`;
1706
1732
  const n = readNotify(text);
1707
- const write = (next) => {
1708
- mkdirSync3(path6.dirname(file), { recursive: true });
1709
- writeFileSync4(file, next);
1733
+ const write = (next, note) => {
1734
+ const bad = writeBack(file, next, existed);
1735
+ return bad ? { ...bad, note: `${bad.note}. ${UNTIL_APPROVED}` } : { file, changed: true, note };
1710
1736
  };
1711
1737
  if (n && !n.other) {
1712
1738
  if (!n.misplaced && JSON.stringify(n.argv) === JSON.stringify(argv))
1713
1739
  return { file, changed: false, note: "notify current" };
1714
- write(withNotify(text, line));
1715
- return {
1716
- file,
1717
- changed: true,
1718
- note: n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1719
- };
1740
+ return write(
1741
+ withNotify(text, line),
1742
+ n.misplaced ? "notify moved to the top level (it sat inside a [table], where Codex never reads it)" : "notify updated"
1743
+ );
1720
1744
  }
1721
1745
  if (n?.other) {
1722
1746
  const prev = previousNotify(n.argv);
@@ -1725,20 +1749,20 @@ function installNotify(cmd, env) {
1725
1749
  if (JSON.stringify(prev) === JSON.stringify(argv))
1726
1750
  return { file, changed: false, note: "notify current (after Codex Computer Use)" };
1727
1751
  }
1728
- if (!prev || OURS2.test(JSON.stringify(prev))) {
1729
- write(replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`));
1730
- return { file, changed: true, note: "chained after Codex Computer Use (--previous-notify)" };
1731
- }
1752
+ if (!prev || OURS2.test(JSON.stringify(prev)))
1753
+ return write(
1754
+ replaceNotify(text, n, `notify = ${withPreviousRaw(n.raw, argv)}`),
1755
+ "chained after Codex Computer Use (--previous-notify)"
1756
+ );
1732
1757
  }
1733
1758
  return {
1734
1759
  file,
1735
1760
  changed: false,
1736
1761
  skipped: true,
1737
- 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`
1762
+ note: `notify belongs to ${(n.argv?.[0] ?? n.raw).replace(/^.*\//, "").slice(0, 40)}; left alone. ${UNTIL_APPROVED}`
1738
1763
  };
1739
1764
  }
1740
- write(withNotify(text, line));
1741
- return { file, changed: true, note: "notify set at the top level" };
1765
+ return write(withNotify(text, line), "notify set at the top level");
1742
1766
  }
1743
1767
  function uninstallNotify(env) {
1744
1768
  const file = configFile(env);
@@ -1750,15 +1774,13 @@ function uninstallNotify(env) {
1750
1774
  }
1751
1775
  const n = readNotify(text);
1752
1776
  if (!n) return { file, changed: false };
1753
- if (!n.other) {
1754
- writeFileSync4(file, withoutNotify(text));
1755
- return { file, changed: true };
1756
- }
1777
+ if (!n.other) return writeBack(file, withoutNotify(text)) ?? { file, changed: true };
1757
1778
  const prev = previousNotify(n.argv);
1758
- if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev))) {
1759
- writeFileSync4(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`));
1760
- return { file, changed: true };
1761
- }
1779
+ if (isComputerUse(n.argv) && prev && OURS2.test(JSON.stringify(prev)))
1780
+ return writeBack(file, replaceNotify(text, n, `notify = ${stripPreviousRaw(n.raw)}`)) ?? {
1781
+ file,
1782
+ changed: true
1783
+ };
1762
1784
  return { file, changed: false };
1763
1785
  }
1764
1786
  function findRollout(threadId, env = process.env) {
@@ -1790,8 +1812,12 @@ function rolloutMeta(file) {
1790
1812
  return scanLines(file, (line) => parseLines([line])[0] ?? {});
1791
1813
  }
1792
1814
  function ofAnotherThread(file, id) {
1815
+ const other = threadOf(file);
1816
+ return !!other && other !== id.toLowerCase();
1817
+ }
1818
+ function threadOf(file) {
1793
1819
  const m = /-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i.exec(file);
1794
- return !!m?.[1] && m[1].toLowerCase() !== id.toLowerCase();
1820
+ return m?.[1] ? m[1].toLowerCase() : null;
1795
1821
  }
1796
1822
  var codex = {
1797
1823
  name: NAME3,
@@ -1870,6 +1896,8 @@ var codex = {
1870
1896
  const turn_id = typeof s.turn_id === "string" ? s.turn_id : void 0;
1871
1897
  const tool = validTool3(s.tool_name) ? s.tool_name : void 0;
1872
1898
  const call_id = typeof s.tool_use_id === "string" ? s.tool_use_id : void 0;
1899
+ const thread = typeof s.transcript_path === "string" ? threadOf(s.transcript_path) : null;
1900
+ const agent_id = typeof s.agent_id === "string" && s.agent_id ? s.agent_id : typeof s.subagent_id === "string" && s.subagent_id ? s.subagent_id : thread && thread !== String(id).toLowerCase() ? thread : void 0;
1873
1901
  const ev = (type, data) => ({ type, data: prune3(data), harnessEvent: event });
1874
1902
  const events = [];
1875
1903
  switch (event) {
@@ -1889,17 +1917,30 @@ var codex = {
1889
1917
  );
1890
1918
  break;
1891
1919
  case "PreToolUse":
1892
- if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, input: s.tool_input }));
1920
+ if (tool) events.push(ev("tool.started", { tool, call_id, turn_id, agent_id, input: s.tool_input }));
1893
1921
  break;
1894
1922
  case "PostToolUse":
1895
1923
  if (tool)
1896
1924
  events.push(
1897
- ev("tool.ended", { tool, call_id, turn_id, ok: true, input: s.tool_input, output: s.tool_response })
1925
+ ev("tool.ended", {
1926
+ tool,
1927
+ call_id,
1928
+ turn_id,
1929
+ agent_id,
1930
+ ok: true,
1931
+ input: s.tool_input,
1932
+ output: s.tool_response
1933
+ })
1898
1934
  );
1899
1935
  break;
1900
1936
  case "PermissionRequest":
1901
1937
  events.push(
1902
- ev("attention.needed", { attention_id: call_id ?? `perm-${Date.now()}`, kind: "permission", tool })
1938
+ ev("attention.needed", {
1939
+ attention_id: call_id ?? `perm-${Date.now()}`,
1940
+ kind: "permission",
1941
+ tool,
1942
+ agent_id
1943
+ })
1903
1944
  );
1904
1945
  break;
1905
1946
  case "Stop":
@@ -2389,7 +2430,7 @@ var CHAIN_LIMIT = 4e3;
2389
2430
  var CHAIN_SQL = `WITH RECURSIVE chain(node_id, parent_node_id, chat_message, created_at) AS (
2390
2431
  SELECT node_id, parent_node_id, chat_message, created_at FROM message_nodes
2391
2432
  WHERE session_id = ? AND node_id = (SELECT main_chain_id FROM sessions WHERE id = ?)
2392
- UNION ALL
2433
+ UNION
2393
2434
  SELECT m.node_id, m.parent_node_id, m.chat_message, m.created_at FROM message_nodes m
2394
2435
  JOIN chain c ON m.node_id = c.parent_node_id AND m.session_id = ? AND m.node_id >= ?
2395
2436
  )
@@ -2681,7 +2722,18 @@ function matches(expect, got, pathStr = "") {
2681
2722
  async function runAdapterFixtures(files) {
2682
2723
  const results = [];
2683
2724
  for (const file of files) {
2684
- const fx = JSON.parse(readFileSync7(file, "utf8"));
2725
+ let fx;
2726
+ try {
2727
+ fx = JSON.parse(readFileSync7(file, "utf8"));
2728
+ } catch (e) {
2729
+ const code = e.code;
2730
+ results.push({ file, ok: false, why: code ? `not readable: ${code}` : `not JSON: ${e.message}` });
2731
+ continue;
2732
+ }
2733
+ if (typeof fx?.harness !== "string" || !fx.input || !Array.isArray(fx.expect)) {
2734
+ results.push({ file, ok: false, why: "not an adapter fixture: it needs harness, input and expect" });
2735
+ continue;
2736
+ }
2685
2737
  const adapter = adapterByName(fx.harness);
2686
2738
  if (!adapter) {
2687
2739
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });
@@ -2723,14 +2775,27 @@ var walk = (d) => readdirSync5(d).flatMap((n) => {
2723
2775
  async function main(argv, io = process) {
2724
2776
  const target = argv[0];
2725
2777
  if (!target) {
2726
- io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | https://receiver>\n");
2778
+ io.stdout.write("usage: sessionpipe-conformance <conformance/harness dir | fixture file | https://receiver>\n");
2727
2779
  return 2;
2728
2780
  }
2729
- if (/^https?:\/\//.test(target)) {
2781
+ if (/^https?:\/\//i.test(target)) {
2730
2782
  io.stdout.write("receiver scenarios land in M3\n");
2731
2783
  return 2;
2732
2784
  }
2733
- const r = await runAdapterFixtures(walk(target));
2785
+ let files;
2786
+ try {
2787
+ files = statSync5(target).isDirectory() ? walk(target) : [target];
2788
+ } catch (e) {
2789
+ io.stdout.write(`sessionpipe-conformance: ${e.message}
2790
+ `);
2791
+ return 2;
2792
+ }
2793
+ if (!files.length) {
2794
+ io.stdout.write(`sessionpipe-conformance: no .json fixtures under ${target}
2795
+ `);
2796
+ return 2;
2797
+ }
2798
+ const r = await runAdapterFixtures(files);
2734
2799
  io.stdout.write(`${r.table}
2735
2800
  ${r.results.length - r.failed}/${r.results.length} passed
2736
2801
  `);