@phnx-labs/agents-cli 1.21.2 → 1.22.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.
Files changed (102) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +32 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/computer-actions.d.ts +4 -0
  5. package/dist/commands/computer-actions.js +35 -0
  6. package/dist/commands/computer.js +4 -2
  7. package/dist/commands/exec.d.ts +27 -0
  8. package/dist/commands/exec.js +123 -6
  9. package/dist/commands/models.js +36 -1
  10. package/dist/commands/perf.d.ts +16 -0
  11. package/dist/commands/perf.js +11 -1
  12. package/dist/commands/projects.d.ts +11 -1
  13. package/dist/commands/projects.js +38 -4
  14. package/dist/commands/sessions-backfill.d.ts +32 -0
  15. package/dist/commands/sessions-backfill.js +186 -0
  16. package/dist/commands/sessions-picker.js +17 -2
  17. package/dist/commands/sessions.d.ts +22 -1
  18. package/dist/commands/sessions.js +331 -18
  19. package/dist/commands/teams.js +1 -1
  20. package/dist/commands/worktree.d.ts +3 -3
  21. package/dist/commands/worktree.js +35 -4
  22. package/dist/index.js +8 -0
  23. package/dist/lib/browser/service.js +13 -0
  24. package/dist/lib/computer/dispatch.d.ts +3 -1
  25. package/dist/lib/computer/dispatch.js +10 -2
  26. package/dist/lib/daemon.d.ts +5 -1
  27. package/dist/lib/daemon.js +63 -14
  28. package/dist/lib/devices/resolve-target.d.ts +6 -0
  29. package/dist/lib/devices/resolve-target.js +9 -3
  30. package/dist/lib/event-stream.d.ts +2 -0
  31. package/dist/lib/event-stream.js +3 -0
  32. package/dist/lib/events.d.ts +3 -1
  33. package/dist/lib/events.js +4 -2
  34. package/dist/lib/exec.js +39 -8
  35. package/dist/lib/git.d.ts +14 -0
  36. package/dist/lib/git.js +36 -0
  37. package/dist/lib/hooks/profile.js +1 -14
  38. package/dist/lib/hosts/dispatch.d.ts +12 -0
  39. package/dist/lib/hosts/dispatch.js +23 -6
  40. package/dist/lib/hosts/reconnect.d.ts +38 -0
  41. package/dist/lib/hosts/reconnect.js +85 -4
  42. package/dist/lib/hosts/run-target.js +14 -2
  43. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  44. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  45. package/dist/lib/model-tiers.d.ts +54 -0
  46. package/dist/lib/model-tiers.js +229 -0
  47. package/dist/lib/models.d.ts +3 -0
  48. package/dist/lib/models.js +44 -7
  49. package/dist/lib/percentile.d.ts +12 -0
  50. package/dist/lib/percentile.js +24 -0
  51. package/dist/lib/perf/db.d.ts +1 -2
  52. package/dist/lib/perf/db.js +2 -14
  53. package/dist/lib/plugins.js +12 -1
  54. package/dist/lib/pricing/prices.json +16 -1
  55. package/dist/lib/project-focus.d.ts +42 -0
  56. package/dist/lib/project-focus.js +80 -0
  57. package/dist/lib/project-schedule.d.ts +75 -0
  58. package/dist/lib/project-schedule.js +110 -0
  59. package/dist/lib/redact.d.ts +2 -0
  60. package/dist/lib/redact.js +22 -0
  61. package/dist/lib/remote-agents-json.d.ts +2 -0
  62. package/dist/lib/remote-agents-json.js +3 -3
  63. package/dist/lib/resources.d.ts +16 -0
  64. package/dist/lib/resources.js +25 -14
  65. package/dist/lib/rotate.d.ts +84 -1
  66. package/dist/lib/rotate.js +155 -5
  67. package/dist/lib/routines.js +1 -14
  68. package/dist/lib/runner.d.ts +4 -2
  69. package/dist/lib/runner.js +21 -5
  70. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  71. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  72. package/dist/lib/session/bash-command.js +60 -9
  73. package/dist/lib/session/db.d.ts +22 -1
  74. package/dist/lib/session/db.js +516 -24
  75. package/dist/lib/session/discover.d.ts +68 -7
  76. package/dist/lib/session/discover.js +186 -84
  77. package/dist/lib/session/highlights.d.ts +24 -4
  78. package/dist/lib/session/highlights.js +52 -7
  79. package/dist/lib/session/parse.d.ts +8 -1
  80. package/dist/lib/session/parse.js +102 -35
  81. package/dist/lib/session/prompt.d.ts +19 -0
  82. package/dist/lib/session/prompt.js +43 -0
  83. package/dist/lib/session/remote-list.d.ts +71 -0
  84. package/dist/lib/session/remote-list.js +410 -2
  85. package/dist/lib/session/shell-programs.d.ts +15 -0
  86. package/dist/lib/session/shell-programs.js +359 -0
  87. package/dist/lib/session/tool-calls.d.ts +88 -0
  88. package/dist/lib/session/tool-calls.js +612 -0
  89. package/dist/lib/session/tool-index.d.ts +100 -0
  90. package/dist/lib/session/tool-index.js +773 -0
  91. package/dist/lib/session/tool-store.d.ts +15 -0
  92. package/dist/lib/session/tool-store.js +198 -0
  93. package/dist/lib/session/types.d.ts +49 -0
  94. package/dist/lib/state.d.ts +10 -1
  95. package/dist/lib/state.js +11 -2
  96. package/dist/lib/teams/remoteWorktree.d.ts +3 -4
  97. package/dist/lib/teams/remoteWorktree.js +3 -4
  98. package/dist/lib/teams/worktree.d.ts +11 -1
  99. package/dist/lib/teams/worktree.js +42 -4
  100. package/dist/lib/types.d.ts +31 -0
  101. package/dist/lib/types.js +17 -0
  102. package/package.json +3 -1
@@ -7,9 +7,11 @@
7
7
  */
8
8
  import * as fs from 'fs';
9
9
  import { truncate } from '../format.js';
10
+ import { sanitizeForTerminal } from '../redact.js';
10
11
  import * as path from 'path';
11
12
  import Database from '../sqlite.js';
12
- import { isSyntheticUserMessage } from './prompt.js';
13
+ import { isSyntheticUserMessage, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
14
+ import { structuredToolResult } from './tool-calls.js';
13
15
  /**
14
16
  * Largest session file we will load into memory. Above this we throw a clean
15
17
  * error instead of OOMing or hitting V8's ERR_STRING_TOO_LONG. Aligns with
@@ -23,12 +25,7 @@ export const SESSION_FILE_MAX_BYTES = 200_000_000;
23
25
  * carriage return (0x0d). Everything else in the C0/C1 range and every CSI/OSC
24
26
  * escape is dropped.
25
27
  */
26
- const TERMINAL_ESCAPE_REGEX = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-_]|[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/g;
27
- export function sanitizeForTerminal(s) {
28
- if (typeof s !== 'string' || !s)
29
- return s;
30
- return s.replace(TERMINAL_ESCAPE_REGEX, '');
31
- }
28
+ export { sanitizeForTerminal } from '../redact.js';
32
29
  /** Recursively sanitize every string value within a tool-args object. */
33
30
  function sanitizeArgsDeep(value) {
34
31
  if (typeof value === 'string')
@@ -373,12 +370,21 @@ export function parseClaudeContent(content) {
373
370
  agent: 'claude',
374
371
  timestamp,
375
372
  tool: toolName,
373
+ callId: toolId,
376
374
  args: toolInput,
377
375
  path: toolInput.file_path || undefined,
378
376
  command: toolName === 'Bash' ? toolInput.command : undefined,
379
377
  };
380
378
  if (isLocal)
381
379
  event._local = true;
380
+ // SlashCommand: the MODEL invoking a slash command programmatically
381
+ // (distinct from the <command-name> wrapper below, which is the
382
+ // USER typing one) — see prompt.ts's extractSlashCommandFromToolInput.
383
+ if (toolName === 'SlashCommand') {
384
+ const slashCommand = extractSlashCommandFromToolInput(toolInput);
385
+ if (slashCommand)
386
+ event.slashCommand = slashCommand;
387
+ }
382
388
  events.push(event);
383
389
  }
384
390
  }
@@ -403,13 +409,21 @@ export function parseClaudeContent(content) {
403
409
  // Simple user text
404
410
  const text = contentBlocks.trim();
405
411
  if (text) {
406
- events.push({
412
+ const event = {
407
413
  type: 'message',
408
414
  agent: 'claude',
409
415
  timestamp,
410
416
  role: 'user',
411
417
  content: text,
412
- });
418
+ };
419
+ // The USER typing a slash command — Claude injects a <command-name>
420
+ // wrapper as the message content (see prompt.ts's
421
+ // extractSlashCommandName; distinct from the SlashCommand tool-use
422
+ // above, which is the model invoking one programmatically).
423
+ const slashCommand = extractSlashCommandName(text);
424
+ if (slashCommand)
425
+ event.slashCommand = slashCommand;
426
+ events.push(event);
413
427
  }
414
428
  }
415
429
  else if (Array.isArray(contentBlocks)) {
@@ -444,6 +458,7 @@ export function parseClaudeContent(content) {
444
458
  const toolId = block.tool_use_id;
445
459
  const toolInfo = toolId ? toolUseMap.get(toolId) : undefined;
446
460
  const isError = block.is_error === true;
461
+ const structured = structuredToolResult(block);
447
462
  // Extract output text from tool result
448
463
  let output = '';
449
464
  if (typeof block.content === 'string') {
@@ -461,6 +476,11 @@ export function parseClaudeContent(content) {
461
476
  agent: 'claude',
462
477
  timestamp,
463
478
  tool: toolInfo?.tool,
479
+ callId: toolId,
480
+ outcome: 'error',
481
+ exitCode: structured.exitCode,
482
+ statusCode: structured.statusCode,
483
+ errorCode: structured.errorCode,
464
484
  content: output || 'Tool execution failed',
465
485
  });
466
486
  }
@@ -470,7 +490,12 @@ export function parseClaudeContent(content) {
470
490
  agent: 'claude',
471
491
  timestamp,
472
492
  tool: toolInfo?.tool,
493
+ callId: toolId,
473
494
  success: true,
495
+ outcome: 'ok',
496
+ exitCode: structured.exitCode,
497
+ statusCode: structured.statusCode,
498
+ errorCode: structured.errorCode,
474
499
  output: output.length > 500 ? output.slice(0, 497) + '...' : output,
475
500
  });
476
501
  }
@@ -640,6 +665,7 @@ export function parseCodexContent(content) {
640
665
  agent: 'codex',
641
666
  timestamp,
642
667
  tool: name,
668
+ callId,
643
669
  args,
644
670
  command: name === 'exec_command' ? (args.command || args.cmd) : undefined,
645
671
  path: args.file_path || args.path || undefined,
@@ -648,14 +674,19 @@ export function parseCodexContent(content) {
648
674
  else if (ptype === 'function_call_output') {
649
675
  const callId = payload.call_id || payload.id;
650
676
  const callInfo = callId ? callMap.get(callId) : undefined;
651
- const output = String(payload.output || '');
677
+ const result = structuredToolResult(payload.output);
652
678
  events.push({
653
679
  type: 'tool_result',
654
680
  agent: 'codex',
655
681
  timestamp,
656
682
  tool: callInfo?.name,
657
- success: true,
658
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
683
+ callId,
684
+ success: result.outcome === 'error' ? false : true,
685
+ outcome: result.outcome,
686
+ exitCode: result.exitCode,
687
+ statusCode: result.statusCode,
688
+ errorCode: result.errorCode,
689
+ output: result.text.length > 500 ? result.text.slice(0, 497) + '...' : result.text,
659
690
  });
660
691
  if (callId)
661
692
  callMap.delete(callId);
@@ -682,6 +713,7 @@ export function parseCodexContent(content) {
682
713
  agent: 'codex',
683
714
  timestamp,
684
715
  tool,
716
+ callId,
685
717
  args,
686
718
  path: patchPath,
687
719
  });
@@ -697,14 +729,19 @@ export function parseCodexContent(content) {
697
729
  else if (ptype === 'custom_tool_call_output') {
698
730
  const callId = payload.call_id || payload.id;
699
731
  const callInfo = callId ? callMap.get(callId) : undefined;
700
- const output = String(payload.output || '');
732
+ const result = structuredToolResult(payload.output);
701
733
  events.push({
702
734
  type: 'tool_result',
703
735
  agent: 'codex',
704
736
  timestamp,
705
737
  tool: callInfo?.name,
706
- success: true,
707
- output: output.length > 500 ? output.slice(0, 497) + '...' : output,
738
+ callId,
739
+ success: result.outcome === 'error' ? false : true,
740
+ outcome: result.outcome,
741
+ exitCode: result.exitCode,
742
+ statusCode: result.statusCode,
743
+ errorCode: result.errorCode,
744
+ output: result.text.length > 500 ? result.text.slice(0, 497) + '...' : result.text,
708
745
  });
709
746
  if (callId)
710
747
  callMap.delete(callId);
@@ -802,6 +839,7 @@ export function parseGemini(filePath) {
802
839
  agent: 'gemini',
803
840
  timestamp: tc.timestamp || timestamp,
804
841
  tool: toolName,
842
+ callId: typeof tc.id === 'string' ? tc.id : undefined,
805
843
  args,
806
844
  command: ['run_shell_command', 'shell', 'bash'].includes(toolName) ? args.command : undefined,
807
845
  path: args.file_path || args.path || undefined,
@@ -825,6 +863,7 @@ export function parseGemini(filePath) {
825
863
  agent: 'gemini',
826
864
  timestamp: tc.timestamp || timestamp,
827
865
  tool: toolName,
866
+ callId: typeof tc.id === 'string' ? tc.id : undefined,
828
867
  success: tc.status === 'success',
829
868
  output: output.length > 500 ? output.slice(0, 497) + '...' : output,
830
869
  });
@@ -1044,6 +1083,7 @@ export function parseAntigravity(dbPath) {
1044
1083
  agent: 'antigravity',
1045
1084
  timestamp,
1046
1085
  tool: norm,
1086
+ callId: call.id,
1047
1087
  args: a,
1048
1088
  command: norm === 'Bash' ? a.CommandLine : undefined,
1049
1089
  // Antigravity uses PascalCase arg keys; probe the known path-bearing ones.
@@ -1175,6 +1215,7 @@ export function parseGrok(filePath) {
1175
1215
  agent: 'grok',
1176
1216
  timestamp,
1177
1217
  tool: toolName,
1218
+ callId: typeof call?.id === 'string' ? call.id : undefined,
1178
1219
  args,
1179
1220
  path: args.path || args.file_path || undefined,
1180
1221
  command: typeof args.command === 'string' ? args.command : undefined,
@@ -1190,13 +1231,17 @@ export function parseGrok(filePath) {
1190
1231
  const callId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id : undefined;
1191
1232
  const toolName = (callId && toolCallMap.get(callId)) || 'unknown';
1192
1233
  const output = typeof msg.content === 'string' ? msg.content : extractText(msg.content);
1193
- const isError = typeof output === 'string' && output.startsWith('Error:');
1234
+ const structuredError = msg.is_error === true;
1235
+ const structuredSuccess = msg.is_error === false;
1236
+ const displayAsError = structuredError || (typeof output === 'string' && output.startsWith('Error:'));
1194
1237
  events.push({
1195
- type: isError ? 'error' : 'tool_result',
1238
+ type: displayAsError ? 'error' : 'tool_result',
1196
1239
  agent: 'grok',
1197
1240
  timestamp,
1198
1241
  tool: toolName,
1199
- success: !isError,
1242
+ callId,
1243
+ success: structuredError ? false : structuredSuccess ? true : undefined,
1244
+ outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
1200
1245
  output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1201
1246
  });
1202
1247
  if (callId)
@@ -1295,11 +1340,13 @@ export function parseOpenCode(filePath) {
1295
1340
  const state = partData.state || {};
1296
1341
  const input = state.input || {};
1297
1342
  const output = state.output || '';
1343
+ const callId = typeof partData.callID === 'string' ? partData.callID : undefined;
1298
1344
  events.push({
1299
1345
  type: 'tool_use',
1300
1346
  agent: 'opencode',
1301
1347
  timestamp,
1302
1348
  tool: toolName,
1349
+ callId,
1303
1350
  args: input,
1304
1351
  command: toolName === 'shell' ? input.command : undefined,
1305
1352
  path: input.filePath || input.path || undefined,
@@ -1311,6 +1358,7 @@ export function parseOpenCode(filePath) {
1311
1358
  agent: 'opencode',
1312
1359
  timestamp,
1313
1360
  tool: toolName,
1361
+ callId,
1314
1362
  success: state.status === 'completed',
1315
1363
  output: outputStr.length > 500 ? outputStr.slice(0, 497) + '...' : outputStr,
1316
1364
  });
@@ -1354,8 +1402,10 @@ export function parseRush(filePath) {
1354
1402
  continue;
1355
1403
  }
1356
1404
  const type = raw.type;
1357
- const timestamp = raw.created_at || new Date().toISOString();
1358
- const content = raw.content || {};
1405
+ const timestamp = typeof raw.created_at === 'string' ? raw.created_at : new Date().toISOString();
1406
+ const content = raw.content && typeof raw.content === 'object' && !Array.isArray(raw.content)
1407
+ ? raw.content
1408
+ : {};
1359
1409
  if (type === 'message') {
1360
1410
  const text = typeof content.text === 'string' ? content.text.trim() : '';
1361
1411
  if (!text)
@@ -1378,9 +1428,11 @@ export function parseRush(filePath) {
1378
1428
  });
1379
1429
  }
1380
1430
  else if (type === 'tool_call') {
1381
- const toolName = raw.name || 'unknown';
1382
- const args = content.input || {};
1383
- const callId = raw.tool_call_id;
1431
+ const toolName = typeof raw.name === 'string' && raw.name.length > 0 ? raw.name : 'unknown';
1432
+ const args = content.input && typeof content.input === 'object' && !Array.isArray(content.input)
1433
+ ? content.input
1434
+ : {};
1435
+ const callId = typeof raw.tool_call_id === 'string' ? raw.tool_call_id : undefined;
1384
1436
  if (callId)
1385
1437
  toolCallMap.set(callId, { tool: toolName, args });
1386
1438
  events.push({
@@ -1388,13 +1440,14 @@ export function parseRush(filePath) {
1388
1440
  agent: 'rush',
1389
1441
  timestamp,
1390
1442
  tool: toolName,
1443
+ callId,
1391
1444
  args,
1392
1445
  path: args.file_path || args.path || undefined,
1393
1446
  command: (toolName === 'Bash' || toolName === 'shell') ? args.command : undefined,
1394
1447
  });
1395
1448
  }
1396
1449
  else if (type === 'tool_result') {
1397
- const callId = raw.tool_call_id;
1450
+ const callId = typeof raw.tool_call_id === 'string' ? raw.tool_call_id : undefined;
1398
1451
  const info = callId ? toolCallMap.get(callId) : undefined;
1399
1452
  const output = content.output;
1400
1453
  let outputStr = '';
@@ -1414,7 +1467,8 @@ export function parseRush(filePath) {
1414
1467
  type: success ? 'tool_result' : 'error',
1415
1468
  agent: 'rush',
1416
1469
  timestamp,
1417
- tool: info?.tool ?? raw.name,
1470
+ tool: info?.tool ?? (typeof raw.name === 'string' ? raw.name : 'unknown'),
1471
+ callId,
1418
1472
  success,
1419
1473
  output: outputStr.length > 500 ? outputStr.slice(0, 497) + '...' : outputStr,
1420
1474
  });
@@ -1575,9 +1629,11 @@ export function parseKimi(filePath) {
1575
1629
  }
1576
1630
  else if (eventType === 'tool.call') {
1577
1631
  const fn = event.function || {};
1578
- const toolName = typeof event.name === 'string' ? event.name : (fn.name || 'unknown');
1632
+ const toolName = typeof event.name === 'string'
1633
+ ? event.name
1634
+ : typeof fn.name === 'string' ? fn.name : 'unknown';
1579
1635
  let args = {};
1580
- if (event.args && typeof event.args === 'object') {
1636
+ if (event.args && typeof event.args === 'object' && !Array.isArray(event.args)) {
1581
1637
  args = event.args;
1582
1638
  }
1583
1639
  else if (typeof fn.arguments === 'string') {
@@ -1588,10 +1644,11 @@ export function parseKimi(filePath) {
1588
1644
  args = { _raw: fn.arguments };
1589
1645
  }
1590
1646
  }
1591
- else if (fn.arguments && typeof fn.arguments === 'object') {
1647
+ else if (fn.arguments && typeof fn.arguments === 'object' && !Array.isArray(fn.arguments)) {
1592
1648
  args = fn.arguments;
1593
1649
  }
1594
- const callId = event.toolCallId || event.uuid;
1650
+ const rawCallId = event.toolCallId || event.uuid;
1651
+ const callId = typeof rawCallId === 'string' ? rawCallId : undefined;
1595
1652
  if (callId) {
1596
1653
  toolCallMap.set(callId, toolName);
1597
1654
  }
@@ -1600,23 +1657,29 @@ export function parseKimi(filePath) {
1600
1657
  agent: 'kimi',
1601
1658
  timestamp,
1602
1659
  tool: toolName,
1660
+ callId,
1603
1661
  args,
1604
1662
  path: args.path || args.file_path || undefined,
1605
1663
  command: toolName === 'Bash' ? args.command : undefined,
1606
1664
  });
1607
1665
  }
1608
1666
  else if (eventType === 'tool.result') {
1609
- const callId = event.toolCallId || event.parentUuid;
1667
+ const rawCallId = event.toolCallId || event.parentUuid;
1668
+ const callId = typeof rawCallId === 'string' ? rawCallId : undefined;
1610
1669
  const toolName = (callId && toolCallMap.get(callId)) || 'unknown';
1611
1670
  const result = event.result || {};
1612
1671
  const output = typeof result.output === 'string' ? result.output : '';
1613
- const isError = result.isError === true || (output && output.startsWith('Error:'));
1672
+ const structuredError = result.isError === true;
1673
+ const structuredSuccess = result.isError === false;
1674
+ const displayAsError = structuredError || (output && output.startsWith('Error:'));
1614
1675
  events.push({
1615
- type: isError ? 'error' : 'tool_result',
1676
+ type: displayAsError ? 'error' : 'tool_result',
1616
1677
  agent: 'kimi',
1617
1678
  timestamp,
1618
1679
  tool: toolName,
1619
- success: !isError,
1680
+ callId,
1681
+ success: structuredError ? false : structuredSuccess ? true : undefined,
1682
+ outcome: structuredError ? 'error' : structuredSuccess ? 'ok' : 'unknown',
1620
1683
  output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1621
1684
  });
1622
1685
  if (callId) {
@@ -1738,6 +1801,7 @@ function parseAnthropicMessageJsonl(filePath, agent) {
1738
1801
  agent,
1739
1802
  timestamp,
1740
1803
  tool: toolName,
1804
+ callId: typeof block.id === 'string' ? block.id : undefined,
1741
1805
  args: toolInput,
1742
1806
  path: toolInput.file_path || toolInput.path || undefined,
1743
1807
  command: (toolName === 'Bash' || toolName === 'Execute' || toolName === 'Shell') ? toolInput.command : undefined,
@@ -1753,11 +1817,14 @@ function parseAnthropicMessageJsonl(filePath, agent) {
1753
1817
  ? block.content.filter((c) => c.type === 'text').map((c) => c.text || '').join('\n')
1754
1818
  : '';
1755
1819
  if (isError) {
1756
- events.push({ type: 'error', agent, timestamp, tool: toolInfo?.tool, content: output || 'Tool execution failed' });
1820
+ events.push({
1821
+ type: 'error', agent, timestamp, tool: toolInfo?.tool,
1822
+ callId: toolId, outcome: 'error', content: output || 'Tool execution failed',
1823
+ });
1757
1824
  }
1758
1825
  else {
1759
1826
  events.push({
1760
- type: 'tool_result', agent, timestamp, tool: toolInfo?.tool, success: true,
1827
+ type: 'tool_result', agent, timestamp, tool: toolInfo?.tool, callId: toolId, success: true,
1761
1828
  output: output.length > 500 ? output.slice(0, 497) + '...' : output,
1762
1829
  });
1763
1830
  }
@@ -12,6 +12,25 @@
12
12
  * own scaffolding at parse time simply never reach here.
13
13
  */
14
14
  export declare function isSyntheticUserMessage(raw: string | undefined): boolean;
15
+ /**
16
+ * Extract the invoked slash-command name from a raw `role=user` message, or
17
+ * undefined when the message carries no `<command-name>` wrapper. Used by
18
+ * `parseClaudeContent` to populate `SessionEvent.slashCommand`.
19
+ *
20
+ * Normalizes a leading `/` onto the result: current Claude Code wraps the
21
+ * name WITH the slash already (`/recap`), but the wrapper predates that and
22
+ * older transcripts carry the bare name (`recap`) — normalizing here means
23
+ * every consumer sees one consistent `/name` shape regardless of vintage.
24
+ */
25
+ export declare function extractSlashCommandName(raw: string | undefined): string | undefined;
26
+ /**
27
+ * Extract the invoked slash-command name from the `SlashCommand` tool's input
28
+ * — the MODEL invoking a command programmatically, not the user typing it.
29
+ * The tool's `command` field carries the full invocation text (name plus any
30
+ * args, e.g. `/code:commit fix the bug`); only the leading command token is
31
+ * the name, matching {@link extractSlashCommandName}'s shape.
32
+ */
33
+ export declare function extractSlashCommandFromToolInput(toolInput: Record<string, any> | undefined): string | undefined;
15
34
  export declare const HEADLESS_PLAN_MODE_PREFIX = "You are running in HEADLESS PLAN MODE.";
16
35
  /** Strip framework noise from a raw session prompt, returning only meaningful user text. */
17
36
  export declare function cleanSessionPrompt(raw: string): string;
@@ -63,6 +63,49 @@ export function isSyntheticUserMessage(raw) {
63
63
  return false;
64
64
  return SYNTHETIC_USER_MESSAGE_PATTERNS.some(pattern => pattern.test(raw));
65
65
  }
66
+ /**
67
+ * The `<command-name>` wrapper Claude injects into a `role=user` message when
68
+ * a typed slash command is invoked, e.g.:
69
+ * <command-message>recap</command-message>
70
+ * <command-name>/recap</command-name>
71
+ * Captures the name INCLUDING its leading slash (`/recap`), matching how the
72
+ * `SlashCommand` tool's own `command` input is formatted (see
73
+ * `extractSlashCommandFromToolInput`) so both sources land on the same shape.
74
+ */
75
+ const COMMAND_NAME_WRAPPER_RE = /<command-name>\s*([^<]*?)\s*<\/command-name>/i;
76
+ /**
77
+ * Extract the invoked slash-command name from a raw `role=user` message, or
78
+ * undefined when the message carries no `<command-name>` wrapper. Used by
79
+ * `parseClaudeContent` to populate `SessionEvent.slashCommand`.
80
+ *
81
+ * Normalizes a leading `/` onto the result: current Claude Code wraps the
82
+ * name WITH the slash already (`/recap`), but the wrapper predates that and
83
+ * older transcripts carry the bare name (`recap`) — normalizing here means
84
+ * every consumer sees one consistent `/name` shape regardless of vintage.
85
+ */
86
+ export function extractSlashCommandName(raw) {
87
+ if (!raw)
88
+ return undefined;
89
+ const m = raw.match(COMMAND_NAME_WRAPPER_RE);
90
+ const name = m?.[1]?.trim();
91
+ if (!name)
92
+ return undefined;
93
+ return name.startsWith('/') ? name : `/${name}`;
94
+ }
95
+ /**
96
+ * Extract the invoked slash-command name from the `SlashCommand` tool's input
97
+ * — the MODEL invoking a command programmatically, not the user typing it.
98
+ * The tool's `command` field carries the full invocation text (name plus any
99
+ * args, e.g. `/code:commit fix the bug`); only the leading command token is
100
+ * the name, matching {@link extractSlashCommandName}'s shape.
101
+ */
102
+ export function extractSlashCommandFromToolInput(toolInput) {
103
+ const command = toolInput?.command;
104
+ if (typeof command !== 'string')
105
+ return undefined;
106
+ const name = command.trim().split(/\s+/)[0];
107
+ return name || undefined;
108
+ }
66
109
  // Prefix prepended to every Claude-in-plan-mode team spawn prompt.
67
110
  // Ends at a blank line before the real user task.
68
111
  export const HEADLESS_PLAN_MODE_PREFIX = 'You are running in HEADLESS PLAN MODE.';
@@ -1,4 +1,33 @@
1
+ import { type DeviceProfile } from '../devices/registry.js';
2
+ import { type RemoteAgentsJsonParseResult } from '../remote-agents-json.js';
1
3
  import type { SessionMeta } from './types.js';
4
+ import { type ToolProgramCountEnvelope, type ToolSearchEnvelope } from './tool-index.js';
5
+ export declare const REMOTE_STDOUT_MAX_BYTES: number;
6
+ export declare const REMOTE_TOOL_AGGREGATE_MAX_BYTES: number;
7
+ export interface RemoteToolByteBudget {
8
+ remainingBytes: number;
9
+ exhausted: boolean;
10
+ }
11
+ /** Preserve UTF-8 code points when SSH splits them across stdout chunks. */
12
+ export declare class RemoteUtf8Accumulator {
13
+ private readonly decoder;
14
+ private value;
15
+ write(chunk: Buffer): void;
16
+ end(): string;
17
+ current(): string;
18
+ }
19
+ /** Claim received bytes against one fleet-query budget before retaining them. */
20
+ export declare function consumeRemoteToolByteBudget(budget: RemoteToolByteBudget, bytes: number): boolean;
21
+ /** Charge sanitized, machine-stamped evidence because redaction may expand it. */
22
+ export declare function consumeParsedRemoteToolSearchBudget(budget: RemoteToolByteBudget, envelope: ToolSearchEnvelope): boolean;
23
+ /**
24
+ * The command run on each peer: answer for itself, as JSON, without recursing.
25
+ * `forwardedArgs` carry the caller's own query/filters (already including the
26
+ * leading `sessions` and a `--json`) so each peer returns a comparable slice.
27
+ * A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
28
+ * there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
29
+ */
30
+ export declare function remoteListCommand(forwardedArgs: string[], os?: string): string;
2
31
  /**
3
32
  * Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
4
33
  * with `machine`. Defensive against version skew / partial output: non-JSON or
@@ -12,6 +41,17 @@ export declare function parseRemoteListPayload(stdout: string, machine: string,
12
41
  items: SessionMeta[];
13
42
  valid: boolean;
14
43
  };
44
+ /** Run one remote `agents sessions … --json` and capture stdout. Resolves
45
+ * `{ code: null }` on spawn error or timeout (host treated as dead). */
46
+ export declare function sshCapture(target: string, remoteCmd: string, timeoutMs: number, aggregateBudget?: RemoteToolByteBudget, options?: {
47
+ multiplex?: boolean;
48
+ port?: number;
49
+ hostKeyOpts?: string[];
50
+ }): Promise<{
51
+ code: number | null;
52
+ stdout: string;
53
+ aggregateBudgetExceeded?: boolean;
54
+ }>;
15
55
  export interface RemoteListResult {
16
56
  sessions: SessionMeta[];
17
57
  /** How many peer machines we attempted to reach (drives the empty-fleet tip). */
@@ -24,6 +64,8 @@ export interface RemoteListResult {
24
64
  */
25
65
  unreachable: string[];
26
66
  }
67
+ /** Keep browse and tool-search fan-out on the same automatic peer set. */
68
+ export declare function isAutomaticSessionPeer(d: DeviceProfile, self: string): boolean;
27
69
  /**
28
70
  * Gather listing sessions from other machines. With an explicit `hosts` list
29
71
  * (from `--host`), fan out to exactly those. Otherwise sweep the registered,
@@ -32,6 +74,35 @@ export interface RemoteListResult {
32
74
  * already `--json`) so every peer returns the same slice this machine asked for.
33
75
  */
34
76
  export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[]): Promise<RemoteListResult>;
77
+ export interface RemoteToolSearchResult {
78
+ envelopes: Array<{
79
+ machine: string;
80
+ envelope: ToolSearchEnvelope;
81
+ }>;
82
+ deviceCount: number;
83
+ unreachable: string[];
84
+ truncated: string[];
85
+ }
86
+ export interface RemoteToolProgramCountResult {
87
+ envelopes: Array<{
88
+ machine: string;
89
+ envelope: ToolProgramCountEnvelope;
90
+ }>;
91
+ deviceCount: number;
92
+ unreachable: string[];
93
+ }
94
+ export declare function parseRemoteToolProgramCount(stdout: string, machine: string, expectedProgram: string): RemoteAgentsJsonParseResult<{
95
+ machine: string;
96
+ envelope: ToolProgramCountEnvelope;
97
+ }>;
98
+ export declare function gatherRemoteToolProgramCounts(forwardedArgs: string[], hosts: string[] | undefined, expectedProgram: string): Promise<RemoteToolProgramCountResult>;
99
+ export declare function parseRemoteToolSearch(stdout: string, machine: string, expectedClauses?: string[]): ToolSearchEnvelope | undefined;
100
+ /**
101
+ * Fleet sibling of {@link gatherRemoteList} for the versioned tool-search
102
+ * envelope. Each peer executes the same local-only query against its own
103
+ * SQLite index; only compact matches cross SSH.
104
+ */
105
+ export declare function gatherRemoteToolSearch(forwardedArgs: string[], hosts?: string[], maxAggregateBytes?: number, expectedClauses?: string[]): Promise<RemoteToolSearchResult>;
35
106
  /** Resolve a peer's SSH target (and OS) from the device registry by its
36
107
  * normalized machine id — the same id the fan-out tags rows with. Returns
37
108
  * undefined when no registered device with an address matches. */