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