sessionpipe-conformance 0.11.0 → 0.13.0

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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // sessionpipe-conformance 0.11.0 · Apache-2.0 · https://sessionpipe.org
2
+ // sessionpipe-conformance 0.13.0 · 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";
@@ -7,7 +7,7 @@ import path9 from "node:path";
7
7
  import { pathToFileURL } from "node:url";
8
8
 
9
9
  // src/index.ts
10
- import { readFileSync as readFileSync6 } from "node:fs";
10
+ import { readFileSync as readFileSync7 } from "node:fs";
11
11
 
12
12
  // ../core/src/paths.ts
13
13
  import os from "node:os";
@@ -168,7 +168,14 @@ ${m.text}`;
168
168
  continue;
169
169
  }
170
170
  if (cur) turns.push(cur);
171
- cur = { user: m.text, assistant: "", at: m.at, startLine: m.line, endLine: m.line };
171
+ cur = {
172
+ user: m.text,
173
+ assistant: "",
174
+ at: m.at,
175
+ startLine: m.line,
176
+ endLine: m.line,
177
+ ...m.injected ? { injected: true } : {}
178
+ };
172
179
  } else {
173
180
  if (!cur) cur = { user: "", assistant: "", at: m.at, startLine: m.line, endLine: m.line };
174
181
  cur.assistant += (cur.assistant ? "\n\n" : "") + m.text;
@@ -178,10 +185,17 @@ ${m.text}`;
178
185
  if (cur?.assistant) turns.push(cur);
179
186
  const line = cur && !cur.assistant ? cur.startLine : endLine;
180
187
  const out = {
181
- turns: turns.map((t) => ({ user: t.user, assistant: t.assistant, at: t.at, endLine: t.endLine })),
188
+ turns: turns.map((t) => ({
189
+ user: t.user,
190
+ assistant: t.assistant,
191
+ at: t.at,
192
+ endLine: t.endLine,
193
+ ...t.injected ? { injected: true } : {}
194
+ })),
182
195
  line
183
196
  };
184
- if (cur && !cur.assistant && cur.user) out.open = { user: cur.user, at: cur.at };
197
+ if (cur && !cur.assistant && cur.user)
198
+ out.open = { user: cur.user, at: cur.at, ...cur.injected ? { injected: true } : {} };
185
199
  return out;
186
200
  }
187
201
  var isInjected = (text) => /^<[a-z_-]+(?:\s[^>]*)?>/i.test(text) && /<\/[a-z_-]+>\s*$/i.test(text) || /^<!--\s/.test(text) || /^<command-/.test(text);
@@ -204,27 +218,51 @@ var textParts = (c, kinds) => {
204
218
  (b) => b && typeof b === "object" && kinds.includes(b.type ?? "") && typeof b.text === "string"
205
219
  ).map((b) => b.text).join("\n");
206
220
  };
221
+ var SPOKEN = /* @__PURE__ */ new Set(["human", "peer"]);
222
+ var PEER_INTRO = /sent a message[^\n]*:$/;
223
+ var PEER_OUTRO = "\n\nThis came from another ";
224
+ var ENVELOPE = /^<cross-session-message\b[^>]*>\n([\s\S]*)\n<\/cross-session-message>$/;
225
+ function peerAsk(origin, text) {
226
+ const body = origin?.body;
227
+ let t = typeof body === "string" && body.trim() ? body : text;
228
+ if (t !== body) {
229
+ const nl = t.indexOf("\n");
230
+ if (nl > 0 && PEER_INTRO.test(t.slice(0, nl))) t = t.slice(nl + 1);
231
+ const outro = t.indexOf(PEER_OUTRO);
232
+ if (outro > 0) t = t.slice(0, outro);
233
+ }
234
+ return (ENVELOPE.exec(t.trim())?.[1] ?? t).trim();
235
+ }
207
236
  var queuedPrompt = (a) => {
208
237
  if (!a || typeof a !== "object") return "";
209
238
  const q = a;
210
239
  if (q.type !== "queued_command" || (q.commandMode ?? "prompt") !== "prompt") return "";
211
- if (q.origin?.kind && q.origin.kind !== "human") return "";
212
- return textParts(q.prompt, ["text"]).trim();
240
+ const kind = q.origin?.kind ?? "human";
241
+ if (!SPOKEN.has(kind)) return "";
242
+ const text = textParts(q.prompt, ["text"]).trim();
243
+ return kind === "peer" ? peerAsk(q.origin, text) : text;
213
244
  };
245
+ var channelText = (text) => /^<channel\b[^>]*>([\s\S]*)<\/channel>$/.exec(text)?.[1]?.trim() ?? "";
214
246
  function readClaude(lines, from) {
215
247
  const msgs = [];
216
248
  for (let i = from; i < lines.length; i++) {
217
249
  const j = lines[i] ? parse(lines[i]) : null;
218
250
  if (!j || j.isSidechain) continue;
219
251
  const origin = j.origin;
220
- if (j.type === "user" && (j.isMeta || j.isCompactSummary || j.repeatsCompactSummary || origin?.kind && origin.kind !== "human"))
221
- continue;
252
+ const channel = j.type === "user" && origin?.kind === "channel";
253
+ const peer = origin?.kind === "peer";
254
+ if (j.type === "user" && (j.isCompactSummary || j.repeatsCompactSummary)) continue;
255
+ if (j.type === "user" && !channel && !peer && (j.isMeta || origin?.kind && origin.kind !== "human")) continue;
222
256
  const at = Date.parse(String(j.timestamp ?? "")) || Date.now();
223
257
  const c = j.message?.content;
224
- if (j.type === "user") {
225
- const text = textParts(c, ["text"]).trim();
258
+ if (channel) {
259
+ const text = channelText(textParts(c, ["text"]).trim());
260
+ if (text) msgs.push({ role: "user", text, at, line: i, injected: true });
261
+ } else if (j.type === "user") {
262
+ const raw = textParts(c, ["text"]).trim();
263
+ const text = peer ? peerAsk(origin, raw) : raw;
226
264
  if (!text || isInjected(text)) continue;
227
- msgs.push({ role: "user", text, at, line: i });
265
+ msgs.push({ role: "user", text, at, line: i, ...peer ? { injected: true } : {} });
228
266
  } else if (j.type === "assistant" && Array.isArray(c)) {
229
267
  const text = textParts(c, ["text"]).trim();
230
268
  if (text) msgs.push({ role: "assistant", text, at, line: i });
@@ -602,13 +640,7 @@ var claudeCode = {
602
640
  );
603
641
  break;
604
642
  case "Stop":
605
- events.push(
606
- ev("turn.ended", {
607
- turn_id,
608
- reason: "stop",
609
- background: Array.isArray(s.background_tasks) ? s.background_tasks.length : void 0
610
- })
611
- );
643
+ events.push(ev("turn.ended", { turn_id, reason: "stop", background: backgroundCount(s.background_tasks) }));
612
644
  break;
613
645
  case "StopFailure":
614
646
  events.push(
@@ -631,7 +663,8 @@ var claudeCode = {
631
663
  events.push(
632
664
  ev("subagent.ended", {
633
665
  agent_id: String(s.agent_id ?? ""),
634
- agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0
666
+ agent_type: typeof s.agent_type === "string" ? s.agent_type : void 0,
667
+ background: backgroundCount(s.background_tasks, String(s.agent_id ?? ""))
635
668
  })
636
669
  );
637
670
  break;
@@ -706,6 +739,15 @@ var claudeCode = {
706
739
  return rows2;
707
740
  }
708
741
  };
742
+ var WAITER_RE = /(?:^|[\s"'`/\\])sessionpipe(?:\.\w+)?["']?\s+wait(?:\s|$)/;
743
+ function backgroundCount(tasks, ending) {
744
+ if (!Array.isArray(tasks)) return void 0;
745
+ return tasks.filter((t) => {
746
+ const task = t;
747
+ if (ending !== void 0 && task?.id === ending) return false;
748
+ return !(typeof task?.command === "string" && WAITER_RE.test(task.command));
749
+ }).length;
750
+ }
709
751
  var LINE_REFRESH_S = 60;
710
752
  function statusLine(file, want, cmd) {
711
753
  const f = readJsonFile(file);
@@ -1134,8 +1176,36 @@ async function claudeAiTitle(bridge, token) {
1134
1176
  }
1135
1177
 
1136
1178
  // ../core/src/adapters/headless.ts
1137
- import { accessSync, constants, statSync as statSync3 } from "node:fs";
1179
+ import { accessSync, constants, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1138
1180
  import path4 from "node:path";
1181
+
1182
+ // ../core/src/launched-by.ts
1183
+ var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
1184
+ var DELEGATED_BY_ENV = "SESSIONPIPE_ASKED_BY";
1185
+ var SESSION_REF = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
1186
+ function refIn(env, name) {
1187
+ const v = env[name]?.trim();
1188
+ return v && SESSION_REF.test(v) ? v : void 0;
1189
+ }
1190
+ function launchedBy(env) {
1191
+ return refIn(env, LAUNCHED_BY_ENV);
1192
+ }
1193
+ function delegatedBy(env) {
1194
+ return refIn(env, DELEGATED_BY_ENV);
1195
+ }
1196
+ function readHook(adapter, input) {
1197
+ const r = adapter.fromHook(input);
1198
+ if (r && !r.session.parent_id) {
1199
+ const self = `${adapter.name}:${r.session.id}`;
1200
+ const launcher = launchedBy(input.env);
1201
+ if (launcher && launcher !== self) r.session.launched_by = launcher;
1202
+ const asker = delegatedBy(input.env);
1203
+ if (asker && asker !== self) r.session.delegated_by = asker;
1204
+ }
1205
+ return r;
1206
+ }
1207
+
1208
+ // ../core/src/adapters/headless.ts
1139
1209
  var JOB_TIMEOUT_MS = 30 * 6e4;
1140
1210
  var OUT_CAP = 1024 * 1024;
1141
1211
  function isExe(p) {
@@ -1156,13 +1226,38 @@ function findBinary(o) {
1156
1226
  for (const d of o.dirs) for (const n of o.names) if (isExe(path4.join(d, n))) return path4.join(d, n);
1157
1227
  return null;
1158
1228
  }
1229
+ function shimTarget(shim) {
1230
+ let text = "";
1231
+ try {
1232
+ text = readFileSync3(shim, "utf8");
1233
+ } catch {
1234
+ return null;
1235
+ }
1236
+ const m = /"%(?:~dp0|dp0%)\\([^"]+)"\s+%\*/.exec(text);
1237
+ if (!m) return null;
1238
+ const target = path4.resolve(path4.dirname(shim), m[1].split("\\").join(path4.sep));
1239
+ try {
1240
+ return statSync3(target).isFile() ? target : null;
1241
+ } catch {
1242
+ return null;
1243
+ }
1244
+ }
1245
+ function spawnable(bin, args) {
1246
+ if (!/\.(cmd|bat)$/i.test(bin)) return { file: bin, args };
1247
+ const target = shimTarget(bin);
1248
+ return target ? { file: process.execPath, args: [target, ...args] } : null;
1249
+ }
1250
+ function probe(bin, args) {
1251
+ const s = spawnable(bin, args);
1252
+ return s ? { ...s, shell: false } : { file: [`"${bin}"`, ...args].join(" "), args: [], shell: true };
1253
+ }
1159
1254
 
1160
1255
  // ../core/src/adapters/antigravity.ts
1161
1256
  import {
1162
1257
  existsSync as existsSync3,
1163
1258
  mkdirSync as mkdirSync2,
1164
1259
  readdirSync as readdirSync3,
1165
- readFileSync as readFileSync3,
1260
+ readFileSync as readFileSync4,
1166
1261
  realpathSync,
1167
1262
  statSync as statSync4,
1168
1263
  unlinkSync as unlinkSync2,
@@ -1291,7 +1386,7 @@ var antigravity = {
1291
1386
  readTranscript(file, fromLine) {
1292
1387
  let text = "";
1293
1388
  try {
1294
- text = readFileSync3(file, "utf8");
1389
+ text = readFileSync4(file, "utf8");
1295
1390
  } catch {
1296
1391
  return { turns: [], line: fromLine };
1297
1392
  }
@@ -1370,7 +1465,7 @@ function agProjects(dir) {
1370
1465
  if (!ID_RE.test(id)) continue;
1371
1466
  let j;
1372
1467
  try {
1373
- j = JSON.parse(readFileSync3(path5.join(dir, n), "utf8"));
1468
+ j = JSON.parse(readFileSync4(path5.join(dir, n), "utf8"));
1374
1469
  } catch {
1375
1470
  continue;
1376
1471
  }
@@ -1407,7 +1502,7 @@ function antigravityRemoteUrl(transcript, opts) {
1407
1502
  for (const d of dirs) {
1408
1503
  try {
1409
1504
  const m = /installation_uuid:\s*"([0-9a-f-]{36})"/i.exec(
1410
- readFileSync3(path5.join(d, "antigravity_state.pbtxt"), "utf8")
1505
+ readFileSync4(path5.join(d, "antigravity_state.pbtxt"), "utf8")
1411
1506
  );
1412
1507
  if (!m) continue;
1413
1508
  const base = `https://antigravity.google.com/r/${m[1]}-v2`;
@@ -1446,7 +1541,7 @@ function antigravityFacts(file, opts) {
1446
1541
  }
1447
1542
 
1448
1543
  // ../core/src/adapters/codex.ts
1449
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
1544
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
1450
1545
  import path6 from "node:path";
1451
1546
 
1452
1547
  // ../core/src/adapters/codex-config.ts
@@ -1603,7 +1698,7 @@ function installNotify(cmd, env) {
1603
1698
  const file = configFile(env);
1604
1699
  let text = "";
1605
1700
  try {
1606
- text = readFileSync4(file, "utf8");
1701
+ text = readFileSync5(file, "utf8");
1607
1702
  } catch {
1608
1703
  }
1609
1704
  const argv = cmd([NAME3, "notify"]).argv;
@@ -1649,7 +1744,7 @@ function uninstallNotify(env) {
1649
1744
  const file = configFile(env);
1650
1745
  let text;
1651
1746
  try {
1652
- text = readFileSync4(file, "utf8");
1747
+ text = readFileSync5(file, "utf8");
1653
1748
  } catch {
1654
1749
  return { file, changed: false };
1655
1750
  }
@@ -1721,7 +1816,7 @@ var codex = {
1721
1816
  ];
1722
1817
  let text = "";
1723
1818
  try {
1724
- text = readFileSync4(configFile(env), "utf8");
1819
+ text = readFileSync5(configFile(env), "utf8");
1725
1820
  } catch {
1726
1821
  }
1727
1822
  const n = readNotify(text);
@@ -1848,7 +1943,7 @@ var codex = {
1848
1943
  readTranscript(file, fromLine) {
1849
1944
  let text = "";
1850
1945
  try {
1851
- text = readFileSync4(file, "utf8");
1946
+ text = readFileSync5(file, "utf8");
1852
1947
  } catch {
1853
1948
  return { turns: [], line: fromLine };
1854
1949
  }
@@ -2369,7 +2464,7 @@ function devinAccount(env = process.env) {
2369
2464
 
2370
2465
  // ../core/src/adapters/gemini-cli.ts
2371
2466
  import { execFileSync as execFileSync4 } from "node:child_process";
2372
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
2467
+ import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
2373
2468
  import path8 from "node:path";
2374
2469
  var NAME5 = "gemini-cli";
2375
2470
  var GEMINI_EVENTS = [
@@ -2391,14 +2486,14 @@ function geminiVersion(platform = process.platform, env = process.env) {
2391
2486
  const win = platform === "win32";
2392
2487
  const bin = win ? findBinary({ names: ["gemini.exe", "gemini.cmd"], dirs: [path8.dirname(process.execPath)], env }) : "gemini";
2393
2488
  if (!bin) throw new Error("no gemini on PATH");
2394
- const o = {
2489
+ const c = probe(bin, ["--version"]);
2490
+ return execFileSync4(c.file, c.args, {
2395
2491
  encoding: "utf8",
2396
2492
  timeout: 4e3,
2397
2493
  stdio: ["ignore", "pipe", "ignore"],
2398
- env
2399
- };
2400
- const out = /\.cmd$/i.test(bin) ? execFileSync4(`"${bin}" --version`, { ...o, shell: true }) : execFileSync4(bin, ["--version"], o);
2401
- return out.trim();
2494
+ env,
2495
+ shell: c.shell
2496
+ }).trim();
2402
2497
  }
2403
2498
  function geminiHooksActive(settingsFile) {
2404
2499
  const tools = readJson(settingsFile).tools;
@@ -2523,7 +2618,7 @@ var geminiCli = {
2523
2618
  readTranscript(file, fromLine) {
2524
2619
  let text = "";
2525
2620
  try {
2526
- text = readFileSync5(file, "utf8");
2621
+ text = readFileSync6(file, "utf8");
2527
2622
  } catch {
2528
2623
  return { turns: [], line: fromLine };
2529
2624
  }
@@ -2562,22 +2657,6 @@ function prune5(o) {
2562
2657
  var ADAPTERS = [claudeCode, codex, geminiCli, antigravity, devin];
2563
2658
  var adapterByName = (name) => ADAPTERS.find((a) => a.name === name);
2564
2659
 
2565
- // ../core/src/launched-by.ts
2566
- var LAUNCHED_BY_ENV = "SESSIONPIPE_LAUNCHED_BY";
2567
- var LAUNCHED_BY = /^[a-z][a-z0-9-]{0,39}:[A-Za-z0-9._:-]{1,128}$/;
2568
- function launchedBy(env) {
2569
- const v = env[LAUNCHED_BY_ENV]?.trim();
2570
- return v && LAUNCHED_BY.test(v) ? v : void 0;
2571
- }
2572
- function readHook(adapter, input) {
2573
- const r = adapter.fromHook(input);
2574
- if (r && !r.session.parent_id) {
2575
- const launcher = launchedBy(input.env);
2576
- if (launcher && launcher !== `${adapter.name}:${r.session.id}`) r.session.launched_by = launcher;
2577
- }
2578
- return r;
2579
- }
2580
-
2581
2660
  // src/index.ts
2582
2661
  function matches(expect, got, pathStr = "") {
2583
2662
  if (expect === "*") return null;
@@ -2602,7 +2681,7 @@ function matches(expect, got, pathStr = "") {
2602
2681
  async function runAdapterFixtures(files) {
2603
2682
  const results = [];
2604
2683
  for (const file of files) {
2605
- const fx = JSON.parse(readFileSync6(file, "utf8"));
2684
+ const fx = JSON.parse(readFileSync7(file, "utf8"));
2606
2685
  const adapter = adapterByName(fx.harness);
2607
2686
  if (!adapter) {
2608
2687
  results.push({ file, ok: false, why: `no adapter ${fx.harness}` });