agent-theatre 1.2.3 → 1.3.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/cli.js CHANGED
@@ -300,9 +300,81 @@ function claimsSuccess(text) {
300
300
  if (HEDGE_LEXICON.some((r) => r.test(tail))) return false;
301
301
  return SUCCESS_LEXICON.some((r) => r.test(tail));
302
302
  }
303
- var ROW_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system"]);
304
303
  var obj2 = (x) => x && typeof x === "object" ? x : {};
305
304
  var str2 = (x) => typeof x === "string" ? x : void 0;
305
+ var TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
306
+ function endReason(reason) {
307
+ switch (reason) {
308
+ case "clear":
309
+ case "logout":
310
+ case "prompt_input_exit":
311
+ return "user";
312
+ default:
313
+ return "stop";
314
+ }
315
+ }
316
+ function hookPayloadToEvents(payload, opts = {}) {
317
+ const p = obj2(payload);
318
+ const name = str2(p.hook_event_name);
319
+ const sessionId = str2(p.session_id) ?? "";
320
+ const ts = opts.now ?? Date.now();
321
+ if (!name) return { events: [], warnings: [{ code: "missing_field", detail: "hook_event_name" }] };
322
+ const cwd = str2(p.cwd) ?? "";
323
+ const ctx = { cwd, ...opts.home ? { home: opts.home } : {} };
324
+ const agentId = TOOL_EVENTS.has(name) ? str2(p.agent_id) : void 0;
325
+ const extra = agentId ? { agentId } : {};
326
+ let seq = 0;
327
+ const mk = (body, id = mintId(ts)) => ({ id, seq: seq++, ts, sessionId, source: "hook", ...extra, ...body });
328
+ const one = (body) => ({ events: [mk(body)], warnings: [] });
329
+ switch (name) {
330
+ case "SessionStart":
331
+ return one({ kind: "session_start", repoId: basename(cwd) || "repo" });
332
+ case "SessionEnd":
333
+ return one({ kind: "session_end", reason: endReason(str2(p.reason)), claimedSuccess: false });
334
+ case "Stop":
335
+ return one({ kind: "turn_end", claimedSuccess: claimsSuccess(str2(p.last_assistant_message)), ...noteOf(str2(p.last_assistant_message)) });
336
+ case "SubagentStart": {
337
+ const id = str2(p.agent_id) ?? "";
338
+ const label = str2(p.agent_type);
339
+ return { events: [mk({ kind: "subagent_start", childId: id, ...label ? { label } : {} })], warnings: [] };
340
+ }
341
+ case "SubagentStop":
342
+ return one({ kind: "subagent_stop", childId: str2(p.agent_id) ?? "", ok: true });
343
+ case "Notification": {
344
+ const t = str2(p.notification_type);
345
+ if (t === "permission_prompt") return one({ kind: "waiting_for_user", reason: "permission" });
346
+ if (t === "idle_prompt") return one({ kind: "waiting_for_user", reason: "idle" });
347
+ if (t === "elicitation_dialog") return one({ kind: "waiting_for_user", reason: "question" });
348
+ return { events: [], warnings: [] };
349
+ }
350
+ case "PostToolUseFailure":
351
+ return one({ kind: "error", where: str2(p.tool_name) ?? "tool", retrying: false });
352
+ case "PostToolUse": {
353
+ const tool = str2(p.tool_name);
354
+ if (!tool) return { events: [], warnings: [{ code: "missing_field", detail: "tool_name" }] };
355
+ const dur = typeof p.duration_ms === "number" ? { durationMs: p.duration_ms } : {};
356
+ const out = toolCallToOutcome({ name: tool, input: p.tool_input, response: p.tool_response, ...dur }, ctx);
357
+ const baseId = str2(p.tool_use_id) ?? mintId(ts);
358
+ const events = out.bodies.map((body, i) => mk(body, i === 0 ? baseId : `${baseId}:${i}`));
359
+ return { events, warnings: out.warning ? [out.warning] : [] };
360
+ }
361
+ case "PreToolUse":
362
+ case "UserPromptSubmit":
363
+ case "PreCompact":
364
+ case "PostCompact":
365
+ return { events: [], warnings: [] };
366
+ }
367
+ return { events: [], warnings: [{ code: "unknown_row_type", detail: name }] };
368
+ }
369
+ function noteOf(text) {
370
+ if (!text) return {};
371
+ const para = text.trim().split(/\n{2,}/).filter((x) => x.trim()).pop()?.replace(/\s+/g, " ").trim() ?? "";
372
+ if (!para) return {};
373
+ return { note: para.length > 240 ? "\u2026" + para.slice(-239) : para };
374
+ }
375
+ var ROW_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system"]);
376
+ var obj3 = (x) => x && typeof x === "object" ? x : {};
377
+ var str3 = (x) => typeof x === "string" ? x : void 0;
306
378
  function parseTranscript(text, opts = {}) {
307
379
  const rows = [];
308
380
  const warnings = [];
@@ -338,21 +410,21 @@ function parseTranscriptRows(rows, opts = {}) {
338
410
  events.push({ id, seq: seq++, ts, sessionId, source: "transcript", ...extra, ...body });
339
411
  };
340
412
  const tsOf = (row) => {
341
- const t = str2(row.timestamp);
413
+ const t = str3(row.timestamp);
342
414
  const n = t ? Date.parse(t) : Number.NaN;
343
415
  return Number.isFinite(n) ? n : Number.isFinite(lastTs) ? lastTs : 0;
344
416
  };
345
- const agentOf = (row) => str2(row.agentId) ?? (row.isSidechain === true ? opts.sidechainAgentId ?? "sidechain" : void 0);
417
+ const agentOf = (row) => str3(row.agentId) ?? (row.isSidechain === true ? opts.sidechainAgentId ?? "sidechain" : void 0);
346
418
  for (const raw of rows) {
347
- const row = obj2(raw);
348
- const type = str2(row.type) ?? "";
419
+ const row = obj3(raw);
420
+ const type = str3(row.type) ?? "";
349
421
  if (!ROW_TYPES.has(type)) {
350
422
  if (type) unknownTypes.add(type);
351
423
  continue;
352
424
  }
353
- if (!sessionId && str2(row.sessionId)) sessionId = str2(row.sessionId);
354
- if (!cwd && str2(row.cwd)) cwd = str2(row.cwd);
355
- if (!branch && str2(row.gitBranch)) branch = str2(row.gitBranch);
425
+ if (!sessionId && str3(row.sessionId)) sessionId = str3(row.sessionId);
426
+ if (!cwd && str3(row.cwd)) cwd = str3(row.cwd);
427
+ if (!branch && str3(row.gitBranch)) branch = str3(row.gitBranch);
356
428
  const ts = tsOf(row);
357
429
  if (!Number.isFinite(firstTs)) {
358
430
  firstTs = ts;
@@ -360,39 +432,39 @@ function parseTranscriptRows(rows, opts = {}) {
360
432
  }
361
433
  lastTs = ts;
362
434
  const agentId = agentOf(row);
363
- const message = obj2(row.message);
435
+ const message = obj3(row.message);
364
436
  const content = message.content;
365
437
  if (type === "system") {
366
438
  if (row.subtype === "api_error") {
367
439
  const attempt = typeof row.retryAttempt === "number" ? row.retryAttempt : 0;
368
440
  const max = typeof row.maxRetries === "number" ? row.maxRetries : 0;
369
- push(ts, str2(row.uuid) ?? `sys:${seq}`, { kind: "error", where: "api", retrying: attempt < max }, agentId ? { agentId } : {});
441
+ push(ts, str3(row.uuid) ?? `sys:${seq}`, { kind: "error", where: "api", retrying: attempt < max }, agentId ? { agentId } : {});
370
442
  }
371
443
  continue;
372
444
  }
373
445
  if (type === "assistant") {
374
446
  const blocks2 = Array.isArray(content) ? content : [];
375
447
  for (const b of blocks2) {
376
- const block = obj2(b);
377
- if (block.type === "tool_use" && str2(block.id) && str2(block.name)) {
378
- pending.set(str2(block.id), { name: str2(block.name), input: block.input, ts, ...agentId ? { agentId } : {} });
448
+ const block = obj3(b);
449
+ if (block.type === "tool_use" && str3(block.id) && str3(block.name)) {
450
+ pending.set(str3(block.id), { name: str3(block.name), input: block.input, ts, ...agentId ? { agentId } : {} });
379
451
  } else if (block.type === "text" && !agentId) {
380
- const t = str2(block.text);
452
+ const t = str3(block.text);
381
453
  if (t) lastAssistantText = t;
382
454
  }
383
455
  }
384
456
  if (typeof content === "string" && !agentId) lastAssistantText = content;
385
457
  if (message.stop_reason === "end_turn" && !agentId) {
386
- push(ts, `${str2(row.uuid) ?? seq}:turn`, { kind: "turn_end", claimedSuccess: claimsSuccess(lastAssistantText) });
458
+ push(ts, `${str3(row.uuid) ?? seq}:turn`, { kind: "turn_end", claimedSuccess: claimsSuccess(lastAssistantText), ...noteOf(lastAssistantText) });
387
459
  }
388
460
  continue;
389
461
  }
390
- if (row.toolDenialKind !== void 0) push(ts, `${str2(row.uuid) ?? seq}:deny`, { kind: "waiting_for_user", reason: "permission" });
462
+ if (row.toolDenialKind !== void 0) push(ts, `${str3(row.uuid) ?? seq}:deny`, { kind: "waiting_for_user", reason: "permission" });
391
463
  const blocks = Array.isArray(content) ? content : [];
392
464
  for (const b of blocks) {
393
- const block = obj2(b);
465
+ const block = obj3(b);
394
466
  if (block.type !== "tool_result") continue;
395
- const useId = str2(block.tool_use_id);
467
+ const useId = str3(block.tool_use_id);
396
468
  if (!useId) continue;
397
469
  const p = pending.get(useId);
398
470
  if (!p) {
@@ -413,7 +485,7 @@ function parseTranscriptRows(rows, opts = {}) {
413
485
  if (out.pendingSubagent) openSubagents.set(out.pendingSubagent.childId, useId);
414
486
  }
415
487
  if (blocks.length === 0 && typeof content === "string" && /\[Request interrupted by user/i.test(content)) {
416
- push(ts, `${str2(row.uuid) ?? seq}:interrupt`, { kind: "waiting_for_user", reason: "idle" });
488
+ push(ts, `${str3(row.uuid) ?? seq}:interrupt`, { kind: "waiting_for_user", reason: "idle" });
417
489
  }
418
490
  }
419
491
  for (const [useId, p] of pending) {
@@ -432,72 +504,6 @@ function parseTranscriptRows(rows, opts = {}) {
432
504
  }
433
505
  return { events, warnings };
434
506
  }
435
- var obj3 = (x) => x && typeof x === "object" ? x : {};
436
- var str3 = (x) => typeof x === "string" ? x : void 0;
437
- var TOOL_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse", "PostToolUseFailure"]);
438
- function endReason(reason) {
439
- switch (reason) {
440
- case "clear":
441
- case "logout":
442
- case "prompt_input_exit":
443
- return "user";
444
- default:
445
- return "stop";
446
- }
447
- }
448
- function hookPayloadToEvents(payload, opts = {}) {
449
- const p = obj3(payload);
450
- const name = str3(p.hook_event_name);
451
- const sessionId = str3(p.session_id) ?? "";
452
- const ts = opts.now ?? Date.now();
453
- if (!name) return { events: [], warnings: [{ code: "missing_field", detail: "hook_event_name" }] };
454
- const cwd = str3(p.cwd) ?? "";
455
- const ctx = { cwd, ...opts.home ? { home: opts.home } : {} };
456
- const agentId = TOOL_EVENTS.has(name) ? str3(p.agent_id) : void 0;
457
- const extra = agentId ? { agentId } : {};
458
- let seq = 0;
459
- const mk = (body, id = mintId(ts)) => ({ id, seq: seq++, ts, sessionId, source: "hook", ...extra, ...body });
460
- const one = (body) => ({ events: [mk(body)], warnings: [] });
461
- switch (name) {
462
- case "SessionStart":
463
- return one({ kind: "session_start", repoId: basename(cwd) || "repo" });
464
- case "SessionEnd":
465
- return one({ kind: "session_end", reason: endReason(str3(p.reason)), claimedSuccess: false });
466
- case "Stop":
467
- return one({ kind: "turn_end", claimedSuccess: claimsSuccess(str3(p.last_assistant_message)) });
468
- case "SubagentStart": {
469
- const id = str3(p.agent_id) ?? "";
470
- const label = str3(p.agent_type);
471
- return { events: [mk({ kind: "subagent_start", childId: id, ...label ? { label } : {} })], warnings: [] };
472
- }
473
- case "SubagentStop":
474
- return one({ kind: "subagent_stop", childId: str3(p.agent_id) ?? "", ok: true });
475
- case "Notification": {
476
- const t = str3(p.notification_type);
477
- if (t === "permission_prompt") return one({ kind: "waiting_for_user", reason: "permission" });
478
- if (t === "idle_prompt") return one({ kind: "waiting_for_user", reason: "idle" });
479
- if (t === "elicitation_dialog") return one({ kind: "waiting_for_user", reason: "question" });
480
- return { events: [], warnings: [] };
481
- }
482
- case "PostToolUseFailure":
483
- return one({ kind: "error", where: str3(p.tool_name) ?? "tool", retrying: false });
484
- case "PostToolUse": {
485
- const tool = str3(p.tool_name);
486
- if (!tool) return { events: [], warnings: [{ code: "missing_field", detail: "tool_name" }] };
487
- const dur = typeof p.duration_ms === "number" ? { durationMs: p.duration_ms } : {};
488
- const out = toolCallToOutcome({ name: tool, input: p.tool_input, response: p.tool_response, ...dur }, ctx);
489
- const baseId = str3(p.tool_use_id) ?? mintId(ts);
490
- const events = out.bodies.map((body, i) => mk(body, i === 0 ? baseId : `${baseId}:${i}`));
491
- return { events, warnings: out.warning ? [out.warning] : [] };
492
- }
493
- case "PreToolUse":
494
- case "UserPromptSubmit":
495
- case "PreCompact":
496
- case "PostCompact":
497
- return { events: [], warnings: [] };
498
- }
499
- return { events: [], warnings: [{ code: "unknown_row_type", detail: name }] };
500
- }
501
507
  function hashString(s) {
502
508
  let h = 2166136261;
503
509
  for (let i = 0; i < s.length; i++) {
@@ -1039,7 +1045,7 @@ var Director = class {
1039
1045
  this.emit(e, actor, { kind: "unverified_claim", gremlinId: gid }, 1400, this.mood.onClaimUnverified());
1040
1046
  }
1041
1047
  this.editedSinceTurn = false;
1042
- this.emit(e, actor, { kind: "your_turn" }, 1e3, this.mood.mood);
1048
+ this.emit(e, actor, { kind: "your_turn", ...e.note ? { note: e.note } : {} }, 1e3, this.mood.mood);
1043
1049
  break;
1044
1050
  }
1045
1051
  case "session_end":
@@ -1309,7 +1315,26 @@ var Theatre = class {
1309
1315
  this.port = typeof addr === "object" && addr ? addr.port : port;
1310
1316
  const home = this.opts.home ?? join2(homedir2(), ".agent-theatre");
1311
1317
  mkdirSync(home, { recursive: true });
1318
+ try {
1319
+ const old = parseInt(readFileSync2(join2(home, "port"), "utf8").trim(), 10);
1320
+ if (old > 0 && old !== this.port && portAlive(old) && !existsSync(join2(home, "ports", String(old)))) {
1321
+ mkdirSync(join2(home, "ports"), { recursive: true });
1322
+ writeFileSync(join2(home, "ports", String(old)), "(started by an older build)");
1323
+ }
1324
+ } catch {
1325
+ }
1312
1326
  writeFileSync(join2(home, "port"), String(this.port));
1327
+ mkdirSync(join2(home, "ports"), { recursive: true });
1328
+ for (const f of readdirSync2(join2(home, "ports"))) {
1329
+ const n = parseInt(f, 10);
1330
+ if (n > 0 && n !== this.port && !portAlive(n)) {
1331
+ try {
1332
+ rmSync(join2(home, "ports", f));
1333
+ } catch {
1334
+ }
1335
+ }
1336
+ }
1337
+ writeFileSync(join2(home, "ports", String(this.port)), this.opts.cwd);
1313
1338
  const lostAfter = this.opts.lostAfterMs ?? 9e4;
1314
1339
  this.timer = setInterval(() => {
1315
1340
  const l = this.live;
@@ -1328,6 +1353,10 @@ var Theatre = class {
1328
1353
  if (readFileSync2(join2(home, "port"), "utf8").trim() === String(this.port)) rmSync(join2(home, "port"));
1329
1354
  } catch {
1330
1355
  }
1356
+ try {
1357
+ rmSync(join2(home, "ports", String(this.port)));
1358
+ } catch {
1359
+ }
1331
1360
  for (const ws of this.wss?.clients ?? []) ws.close();
1332
1361
  this.wss?.close();
1333
1362
  await new Promise((res) => this.server ? this.server.close(() => res()) : res());
@@ -1371,6 +1400,12 @@ function claudeBinary() {
1371
1400
  }
1372
1401
  return null;
1373
1402
  }
1403
+ function portAlive(port) {
1404
+ const r = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], { encoding: "utf8", timeout: 2e3 });
1405
+ if (r.status === 0 && r.stdout.trim()) return true;
1406
+ if (r.error) return true;
1407
+ return false;
1408
+ }
1374
1409
 
1375
1410
  // src/scan.ts
1376
1411
  import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
@@ -1505,20 +1540,22 @@ function hasOurHooks(existing) {
1505
1540
  // src/hooks/script.ts
1506
1541
  var HOOK_SCRIPT = `#!/usr/bin/env node
1507
1542
  // Agent Theatre hook \u2014 forwards Claude Code events to the local theatre. Never blocks the agent.
1508
- import { readFileSync } from "node:fs";
1543
+ import { readFileSync, readdirSync } from "node:fs";
1509
1544
  import { homedir } from "node:os";
1510
1545
  import { join } from "node:path";
1511
- const portFile = join(process.env.AGENT_THEATRE_HOME || join(homedir(), ".agent-theatre"), "port");
1512
- let port = 0;
1513
- try { port = parseInt(readFileSync(portFile, "utf8").trim(), 10); } catch { process.exit(0); }
1514
- if (!port) process.exit(0);
1546
+ // Every running stage registers itself in ports/<port>; the event goes to all of them (browser, VS Code, a second repo\u2026).
1547
+ const home = process.env.AGENT_THEATRE_HOME || join(homedir(), ".agent-theatre");
1548
+ const ports = new Set();
1549
+ try { for (const f of readdirSync(join(home, "ports"))) { const n = parseInt(f, 10); if (n > 0) ports.add(n); } } catch { /* no registry yet */ }
1550
+ try { const n = parseInt(readFileSync(join(home, "port"), "utf8").trim(), 10); if (n > 0) ports.add(n); } catch { /* no port file */ }
1551
+ if (!ports.size) process.exit(0);
1515
1552
  let body = "";
1516
1553
  process.stdin.setEncoding("utf8");
1517
1554
  process.stdin.on("data", (c) => { body += c; if (body.length > 2_000_000) body = body.slice(0, 2_000_000); });
1518
1555
  process.stdin.on("end", async () => {
1519
1556
  try {
1520
- await fetch(\`http://127.0.0.1:\${port}/hook\`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(300) });
1521
- } catch { /* theatre not running or slow: that is fine */ }
1557
+ await Promise.allSettled([...ports].map((port) => fetch(\`http://127.0.0.1:\${port}/hook\`, { method: "POST", headers: { "content-type": "application/json" }, body, signal: AbortSignal.timeout(300) })));
1558
+ } catch { /* a theatre not running or slow: that is fine */ }
1522
1559
  process.exit(0);
1523
1560
  });
1524
1561
  setTimeout(() => process.exit(0), 600).unref();