@phnx-labs/agents-cli 1.20.4 → 1.20.6

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 (207) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +49 -18
  3. package/dist/commands/browser.js +31 -4
  4. package/dist/commands/cli.js +1 -1
  5. package/dist/commands/cloud.js +1 -1
  6. package/dist/commands/commands.js +2 -0
  7. package/dist/commands/computer.js +10 -2
  8. package/dist/commands/defaults.d.ts +7 -0
  9. package/dist/commands/defaults.js +89 -0
  10. package/dist/commands/doctor.js +1 -1
  11. package/dist/commands/exec.js +73 -19
  12. package/dist/commands/hooks.js +6 -6
  13. package/dist/commands/inspect.d.ts +26 -0
  14. package/dist/commands/inspect.js +590 -0
  15. package/dist/commands/mcp.js +17 -16
  16. package/dist/commands/models.js +1 -1
  17. package/dist/commands/packages.js +6 -4
  18. package/dist/commands/permissions.js +13 -12
  19. package/dist/commands/plugins.d.ts +13 -0
  20. package/dist/commands/plugins.js +100 -11
  21. package/dist/commands/prune.js +3 -2
  22. package/dist/commands/pull.d.ts +12 -5
  23. package/dist/commands/pull.js +26 -422
  24. package/dist/commands/push.d.ts +14 -0
  25. package/dist/commands/push.js +30 -0
  26. package/dist/commands/repo.d.ts +1 -1
  27. package/dist/commands/repo.js +155 -112
  28. package/dist/commands/resource-view.d.ts +2 -0
  29. package/dist/commands/resource-view.js +12 -3
  30. package/dist/commands/routines.js +32 -7
  31. package/dist/commands/rules.js +4 -4
  32. package/dist/commands/secrets.js +46 -9
  33. package/dist/commands/sessions.js +1 -0
  34. package/dist/commands/setup.d.ts +3 -3
  35. package/dist/commands/setup.js +17 -17
  36. package/dist/commands/skills.js +6 -5
  37. package/dist/commands/subagents.js +5 -4
  38. package/dist/commands/sync.d.ts +18 -5
  39. package/dist/commands/sync.js +251 -65
  40. package/dist/commands/teams.js +109 -11
  41. package/dist/commands/tmux.d.ts +25 -0
  42. package/dist/commands/tmux.js +415 -0
  43. package/dist/commands/trash.d.ts +2 -2
  44. package/dist/commands/trash.js +1 -1
  45. package/dist/commands/versions.js +2 -2
  46. package/dist/commands/view.d.ts +12 -1
  47. package/dist/commands/view.js +128 -40
  48. package/dist/commands/workflows.js +4 -3
  49. package/dist/commands/worktree.d.ts +4 -5
  50. package/dist/commands/worktree.js +4 -4
  51. package/dist/index.js +106 -41
  52. package/dist/lib/agents.d.ts +23 -10
  53. package/dist/lib/agents.js +88 -25
  54. package/dist/lib/auto-pull-worker.d.ts +1 -1
  55. package/dist/lib/auto-pull-worker.js +2 -2
  56. package/dist/lib/auto-pull.d.ts +1 -1
  57. package/dist/lib/auto-pull.js +1 -1
  58. package/dist/lib/beta.d.ts +1 -1
  59. package/dist/lib/beta.js +1 -1
  60. package/dist/lib/browser/chrome.d.ts +10 -0
  61. package/dist/lib/browser/chrome.js +84 -3
  62. package/dist/lib/capabilities.js +2 -0
  63. package/dist/lib/commands.d.ts +28 -1
  64. package/dist/lib/commands.js +125 -20
  65. package/dist/lib/doctor-diff.js +2 -2
  66. package/dist/lib/exec.d.ts +14 -0
  67. package/dist/lib/exec.js +59 -5
  68. package/dist/lib/fuzzy.d.ts +12 -2
  69. package/dist/lib/fuzzy.js +29 -4
  70. package/dist/lib/git.js +8 -1
  71. package/dist/lib/hooks.d.ts +2 -2
  72. package/dist/lib/hooks.js +97 -10
  73. package/dist/lib/mcp.js +32 -2
  74. package/dist/lib/migrate.d.ts +51 -0
  75. package/dist/lib/migrate.js +233 -5
  76. package/dist/lib/models.js +62 -15
  77. package/dist/lib/permissions.d.ts +59 -2
  78. package/dist/lib/permissions.js +299 -7
  79. package/dist/lib/plugin-marketplace.d.ts +98 -40
  80. package/dist/lib/plugin-marketplace.js +196 -93
  81. package/dist/lib/plugins.d.ts +21 -4
  82. package/dist/lib/plugins.js +130 -49
  83. package/dist/lib/profiles-presets.js +12 -12
  84. package/dist/lib/project-launch.d.ts +70 -0
  85. package/dist/lib/project-launch.js +404 -0
  86. package/dist/lib/pty-client.js +1 -1
  87. package/dist/lib/pty-server.d.ts +1 -1
  88. package/dist/lib/pty-server.js +8 -5
  89. package/dist/lib/refresh.d.ts +26 -0
  90. package/dist/lib/refresh.js +315 -0
  91. package/dist/lib/resource-patterns.d.ts +1 -1
  92. package/dist/lib/resource-patterns.js +1 -1
  93. package/dist/lib/resources/commands.js +2 -2
  94. package/dist/lib/resources/hooks.d.ts +1 -1
  95. package/dist/lib/resources/hooks.js +1 -1
  96. package/dist/lib/resources/mcp.d.ts +1 -1
  97. package/dist/lib/resources/mcp.js +5 -6
  98. package/dist/lib/resources/permissions.js +5 -2
  99. package/dist/lib/resources/rules.js +3 -2
  100. package/dist/lib/resources/skills.js +3 -2
  101. package/dist/lib/resources/types.d.ts +1 -1
  102. package/dist/lib/resources.d.ts +2 -0
  103. package/dist/lib/resources.js +4 -3
  104. package/dist/lib/rotate.d.ts +1 -1
  105. package/dist/lib/rotate.js +7 -19
  106. package/dist/lib/routines.d.ts +16 -4
  107. package/dist/lib/routines.js +67 -17
  108. package/dist/lib/rules/compile.js +22 -10
  109. package/dist/lib/rules/rules.js +3 -3
  110. package/dist/lib/run-config.d.ts +9 -0
  111. package/dist/lib/run-config.js +35 -0
  112. package/dist/lib/run-defaults.d.ts +42 -0
  113. package/dist/lib/run-defaults.js +180 -0
  114. package/dist/lib/runner.js +16 -3
  115. package/dist/lib/scheduler.js +15 -1
  116. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +9 -1
  119. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  120. package/dist/lib/secrets/install-helper.d.ts +11 -3
  121. package/dist/lib/secrets/install-helper.js +48 -6
  122. package/dist/lib/secrets/linux.d.ts +56 -9
  123. package/dist/lib/secrets/linux.js +327 -59
  124. package/dist/lib/session/db.js +15 -2
  125. package/dist/lib/session/discover.js +118 -3
  126. package/dist/lib/session/parse.js +3 -0
  127. package/dist/lib/session/types.d.ts +1 -1
  128. package/dist/lib/session/types.js +1 -1
  129. package/dist/lib/shims.d.ts +18 -9
  130. package/dist/lib/shims.js +133 -50
  131. package/dist/lib/skills.d.ts +1 -1
  132. package/dist/lib/skills.js +10 -9
  133. package/dist/lib/staleness/detectors/commands.d.ts +3 -0
  134. package/dist/lib/staleness/detectors/commands.js +46 -0
  135. package/dist/lib/staleness/detectors/hooks.d.ts +3 -0
  136. package/dist/lib/staleness/detectors/hooks.js +44 -0
  137. package/dist/lib/staleness/detectors/mcp.d.ts +3 -0
  138. package/dist/lib/staleness/detectors/mcp.js +31 -0
  139. package/dist/lib/staleness/detectors/permissions.d.ts +3 -0
  140. package/dist/lib/staleness/detectors/permissions.js +201 -0
  141. package/dist/lib/staleness/detectors/plugins.d.ts +8 -0
  142. package/dist/lib/staleness/detectors/plugins.js +23 -0
  143. package/dist/lib/staleness/detectors/rules.d.ts +3 -0
  144. package/dist/lib/staleness/detectors/rules.js +34 -0
  145. package/dist/lib/staleness/detectors/skills.d.ts +3 -0
  146. package/dist/lib/staleness/detectors/skills.js +71 -0
  147. package/dist/lib/staleness/detectors/subagents.d.ts +3 -0
  148. package/dist/lib/staleness/detectors/subagents.js +50 -0
  149. package/dist/lib/staleness/detectors/types.d.ts +22 -0
  150. package/dist/lib/staleness/detectors/types.js +1 -0
  151. package/dist/lib/staleness/detectors/workflows.d.ts +3 -0
  152. package/dist/lib/staleness/detectors/workflows.js +28 -0
  153. package/dist/lib/staleness/registry.d.ts +26 -0
  154. package/dist/lib/staleness/registry.js +123 -0
  155. package/dist/lib/staleness/writers/commands.d.ts +3 -0
  156. package/dist/lib/staleness/writers/commands.js +111 -0
  157. package/dist/lib/staleness/writers/hooks.d.ts +3 -0
  158. package/dist/lib/staleness/writers/hooks.js +47 -0
  159. package/dist/lib/staleness/writers/kinds.d.ts +10 -0
  160. package/dist/lib/staleness/writers/kinds.js +15 -0
  161. package/dist/lib/staleness/writers/lazy-map.d.ts +13 -0
  162. package/dist/lib/staleness/writers/lazy-map.js +19 -0
  163. package/dist/lib/staleness/writers/mcp.d.ts +10 -0
  164. package/dist/lib/staleness/writers/mcp.js +19 -0
  165. package/dist/lib/staleness/writers/permissions.d.ts +13 -0
  166. package/dist/lib/staleness/writers/permissions.js +26 -0
  167. package/dist/lib/staleness/writers/plugins.d.ts +7 -0
  168. package/dist/lib/staleness/writers/plugins.js +31 -0
  169. package/dist/lib/staleness/writers/rules.d.ts +7 -0
  170. package/dist/lib/staleness/writers/rules.js +55 -0
  171. package/dist/lib/staleness/writers/skills.d.ts +3 -0
  172. package/dist/lib/staleness/writers/skills.js +81 -0
  173. package/dist/lib/staleness/writers/sources.d.ts +16 -0
  174. package/dist/lib/staleness/writers/sources.js +72 -0
  175. package/dist/lib/staleness/writers/subagents.d.ts +3 -0
  176. package/dist/lib/staleness/writers/subagents.js +53 -0
  177. package/dist/lib/staleness/writers/types.d.ts +36 -0
  178. package/dist/lib/staleness/writers/types.js +1 -0
  179. package/dist/lib/staleness/writers/workflows.d.ts +7 -0
  180. package/dist/lib/staleness/writers/workflows.js +31 -0
  181. package/dist/lib/state.d.ts +34 -11
  182. package/dist/lib/state.js +58 -13
  183. package/dist/lib/subagents.d.ts +0 -2
  184. package/dist/lib/subagents.js +6 -6
  185. package/dist/lib/teams/agents.js +1 -1
  186. package/dist/lib/teams/api.d.ts +67 -0
  187. package/dist/lib/teams/api.js +78 -0
  188. package/dist/lib/teams/parsers.d.ts +1 -1
  189. package/dist/lib/tmux/binary.d.ts +67 -0
  190. package/dist/lib/tmux/binary.js +141 -0
  191. package/dist/lib/tmux/index.d.ts +8 -0
  192. package/dist/lib/tmux/index.js +8 -0
  193. package/dist/lib/tmux/paths.d.ts +17 -0
  194. package/dist/lib/tmux/paths.js +30 -0
  195. package/dist/lib/tmux/session.d.ts +122 -0
  196. package/dist/lib/tmux/session.js +305 -0
  197. package/dist/lib/types.d.ts +73 -13
  198. package/dist/lib/types.js +1 -1
  199. package/dist/lib/usage.js +1 -1
  200. package/dist/lib/versions.d.ts +4 -4
  201. package/dist/lib/versions.js +138 -496
  202. package/dist/lib/workflows.d.ts +2 -4
  203. package/dist/lib/workflows.js +3 -4
  204. package/package.json +6 -3
  205. package/scripts/postinstall.js +16 -63
  206. package/dist/commands/status.d.ts +0 -9
  207. package/dist/commands/status.js +0 -25
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
5
  import { resolveProvider } from '../lib/cloud/registry.js';
6
6
  import { runSupervisor } from '../lib/teams/supervisor.js';
7
- import { handleSpawn, handleStatus, handleStop, handleTasks, } from '../lib/teams/api.js';
7
+ import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary, } from '../lib/teams/api.js';
8
8
  import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
9
9
  import { setHelpSections } from '../lib/help.js';
10
10
  import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
@@ -24,6 +24,7 @@ const AGENT_NAMES = {
24
24
  opencode: 'OpenCode',
25
25
  grok: 'Grok',
26
26
  antigravity: 'Antigravity',
27
+ kimi: 'Kimi',
27
28
  };
28
29
  const VALID_AGENTS = Object.keys(AGENT_NAMES);
29
30
  // 'full' kept as historical alias for 'skip'; normalized to 'skip' downstream.
@@ -381,6 +382,55 @@ async function resolveTeammateSessions(agents) {
381
382
  }
382
383
  return map;
383
384
  }
385
+ // Default compact renderer — one block per teammate, optimized for the
386
+ // orchestrator scanning "what state, what did you touch, what did you say
387
+ // last." Caller passes the projected AgentStatusSummary; for the full
388
+ // verbose layout use printAgentDetail above.
389
+ function printAgentSummary(s) {
390
+ const label = statusColor(s.status)(s.status.toUpperCase());
391
+ const handle = s.name ?? shortId(s.agent_id);
392
+ const ident = s.name ? chalk.gray(`(${shortId(s.agent_id)})`) : '';
393
+ const duration = s.duration ? `${chalk.gray(' · ')}${chalk.white(s.duration)}` : '';
394
+ const errBadge = s.has_errors ? chalk.red(' !') : '';
395
+ const tools = chalk.gray(` · ${s.tool_count} tools`);
396
+ console.log(` ${chalk.cyan(handle.padEnd(14))} ${ident.padEnd(11)} ${label}${duration}${tools}${errBadge}`);
397
+ // Files: counts + basenames. Read is count only.
398
+ const fileLines = [];
399
+ const renderCat = (label, cat) => {
400
+ if (cat.count === 0)
401
+ return;
402
+ const more = cat.count > cat.names.length ? ` +${cat.count - cat.names.length}` : '';
403
+ const names = cat.names.length ? ` ${cat.names.join(', ')}${more}` : '';
404
+ fileLines.push(`${label} ${cat.count}${names}`);
405
+ };
406
+ renderCat('modified', s.files.modified);
407
+ renderCat('created', s.files.created);
408
+ renderCat('deleted', s.files.deleted);
409
+ if (s.files.read.count > 0)
410
+ fileLines.push(`read ${s.files.read.count}`);
411
+ if (fileLines.length) {
412
+ console.log(` ${chalk.gray('files ')} ${fileLines.join(chalk.gray(' · '))}`);
413
+ }
414
+ // Last 3 bash commands.
415
+ const recentBash = s.bash_commands.slice(-3);
416
+ if (recentBash.length) {
417
+ console.log(` ${chalk.gray('bash ')}`);
418
+ for (const cmd of recentBash) {
419
+ console.log(` ${chalk.gray('$')} ${truncate(cmd, 96)}`);
420
+ }
421
+ }
422
+ // Last messages — first non-empty line of each, truncated.
423
+ if (s.last_messages.length) {
424
+ console.log(` ${chalk.gray('messages')}`);
425
+ for (const msg of s.last_messages) {
426
+ const firstLine = msg.split(/\r?\n/).find((l) => l.trim()) || '';
427
+ if (firstLine)
428
+ console.log(` ${chalk.gray('>')} ${truncate(firstLine, 96)}`);
429
+ }
430
+ }
431
+ if (s.pr_url)
432
+ console.log(` ${chalk.gray('PR ')} ${chalk.cyan(s.pr_url)}`);
433
+ }
384
434
  // Render a team's status in the same format the `status` subcommand uses, so
385
435
  // the interactive picker's Enter action drops the user into a familiar view.
386
436
  async function printTeamStatus(team, result) {
@@ -408,6 +458,36 @@ async function printTeamStatus(team, result) {
408
458
  console.log();
409
459
  console.log(chalk.gray(`cursor: ${result.cursor}`));
410
460
  }
461
+ // Compact default renderer — no session-file dive, no per-teammate
462
+ // 15-line preview. One block per teammate, suitable for the orchestrator
463
+ // scanning what each agent did. Use `printTeamStatus` (above) for the
464
+ // verbose/legacy layout.
465
+ function printTeamSummary(team, result) {
466
+ const { summary, agents } = result;
467
+ console.log(chalk.bold(`Team ${chalk.cyan(team)} `) +
468
+ chalk.gray(summary.pending > 0
469
+ ? `(${summary.pending} pending, ${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`
470
+ : `(${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`));
471
+ if (agents.length === 0) {
472
+ console.log(chalk.gray(' (no teammates yet — add one with `agents teams add`)'));
473
+ }
474
+ else {
475
+ const width = Math.min(process.stdout.columns || 80, 80);
476
+ const divider = chalk.gray('┈'.repeat(width));
477
+ for (let i = 0; i < agents.length; i++) {
478
+ console.log();
479
+ if (i > 0) {
480
+ console.log(divider);
481
+ console.log();
482
+ }
483
+ printAgentSummary(agents[i]);
484
+ }
485
+ }
486
+ console.log();
487
+ console.log(chalk.gray(`cursor: ${result.cursor}`));
488
+ console.log(chalk.gray('Full detail: agents teams status ' + team + ' --verbose'));
489
+ console.log(chalk.gray('Raw log: agents teams logs --team ' + team + ' --teammate <name>'));
490
+ }
411
491
  // Classify a team into a single bucket for --status filtering.
412
492
  // - empty: no teammates (created but nobody added yet)
413
493
  // - waiting: only staged teammates — call `teams start` to kick them off
@@ -963,10 +1043,11 @@ export function registerTeamsCommands(program) {
963
1043
  teams
964
1044
  .command('status [team]')
965
1045
  .aliases(['s', 'st', 'check'])
966
- .description("Check in on a team: who's working, what files they touched, recent commands, last output. Pass --since for efficient delta polling.")
1046
+ .description("Check in on a team: status, files touched, recent commands, last messages. Pass --verbose for the full per-teammate dump; --since for delta polling.")
967
1047
  .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
968
1048
  .option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
969
1049
  .option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
1050
+ .option('-v, --verbose', 'Emit the full per-teammate detail (prompt, all file paths, all messages). Default is a compact summary.')
970
1051
  .option('--json', 'Output machine-readable JSON')
971
1052
  .action(async (team, opts) => {
972
1053
  const filter = opts.filter;
@@ -983,8 +1064,15 @@ export function registerTeamsCommands(program) {
983
1064
  const agents = opts.agentId
984
1065
  ? result.agents.filter((a) => a.agent_id.startsWith(opts.agentId))
985
1066
  : result.agents;
1067
+ const filtered = { ...result, agents };
986
1068
  if (isJsonMode(opts)) {
987
- console.log(JSON.stringify({ ...result, agents }, null, 2));
1069
+ // JSON output also respects --verbose. Default = compact summary
1070
+ // shape; --verbose = full AgentStatusDetail. Same toggle covers
1071
+ // both text and JSON so MCP-style consumers can opt into detail.
1072
+ const payload = opts.verbose
1073
+ ? filtered
1074
+ : toTaskStatusSummary(filtered);
1075
+ console.log(JSON.stringify(payload, null, 2));
988
1076
  return;
989
1077
  }
990
1078
  const exists = await teamExists(team);
@@ -992,7 +1080,12 @@ export function registerTeamsCommands(program) {
992
1080
  console.log(chalk.yellow(`No team called '${team}'. Create it with: agents teams create ${team}`));
993
1081
  return;
994
1082
  }
995
- await printTeamStatus(team, { ...result, agents });
1083
+ if (opts.verbose) {
1084
+ await printTeamStatus(team, filtered);
1085
+ }
1086
+ else {
1087
+ printTeamSummary(team, toTaskStatusSummary(filtered));
1088
+ }
996
1089
  }
997
1090
  catch (err) {
998
1091
  die(`Could not check on team ${team}: ${err.message}`);
@@ -1307,14 +1400,19 @@ export function registerTeamsCommands(program) {
1307
1400
  teams
1308
1401
  .command('logs [teammate]')
1309
1402
  .alias('log')
1310
- .description("Read a teammate's raw log output. Accepts name, UUID, or UUID prefix.")
1403
+ .description("Read a teammate's raw log output. Accepts positional name, --teammate <name>, UUID, or UUID prefix.")
1311
1404
  .option('-n, --tail <n>', 'Show only the last N lines instead of the full log')
1312
1405
  .option('--team <team>', 'Disambiguate when the same name appears in multiple teams')
1406
+ .option('--teammate <name>', 'Teammate name (alias for the positional arg; useful for scripts)')
1313
1407
  .action(async (ref, opts) => {
1314
1408
  const base = await getAgentsDir();
1315
- // No teammate picker in TTY, hard fail outside.
1409
+ // Resolve teammate identity. Precedence:
1410
+ // 1. positional `[teammate]` arg (back-compat, most common)
1411
+ // 2. --teammate <name> flag (script-friendly alias)
1412
+ // 3. interactive picker (TTY only)
1413
+ const teammateRef = ref ?? opts.teammate;
1316
1414
  let agentId;
1317
- if (!ref) {
1415
+ if (!teammateRef) {
1318
1416
  const mgr = mkManager();
1319
1417
  const picked = await pickTeammateOr(mgr, 'agents teams logs');
1320
1418
  if (!picked)
@@ -1322,13 +1420,13 @@ export function registerTeamsCommands(program) {
1322
1420
  agentId = picked.agentId;
1323
1421
  }
1324
1422
  else {
1325
- const resolved = await resolveTeammateAcrossTeams(base, ref, opts.team);
1423
+ const resolved = await resolveTeammateAcrossTeams(base, teammateRef, opts.team);
1326
1424
  if (resolved.kind === 'none') {
1327
- die(`No notes on record for teammate '${ref}'`, 2);
1425
+ die(`No notes on record for teammate '${teammateRef}'`, 2);
1328
1426
  }
1329
1427
  if (resolved.kind === 'ambiguous') {
1330
1428
  const hints = resolved.candidates.map((c) => `${c.team}/${c.display}`).join(', ');
1331
- die(`'${ref}' matches multiple teammates: ${hints}.\n` +
1429
+ die(`'${teammateRef}' matches multiple teammates: ${hints}.\n` +
1332
1430
  ` Narrow it with --team <team>, or pass a UUID prefix.`, 2);
1333
1431
  }
1334
1432
  agentId = resolved.agentId;
@@ -1345,7 +1443,7 @@ export function registerTeamsCommands(program) {
1345
1443
  process.stdout.write(lines.slice(-n).join('\n'));
1346
1444
  }
1347
1445
  catch {
1348
- die(`No notes on record for teammate '${ref ?? agentId}' (looked in ${logPath})`, 2);
1446
+ die(`No notes on record for teammate '${teammateRef ?? agentId}' (looked in ${logPath})`, 2);
1349
1447
  }
1350
1448
  });
1351
1449
  // doctor
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `agents tmux` — terminal multiplexer integration.
3
+ *
4
+ * Why this exists: the swarmify VS Code extension was hand-rolling tmux
5
+ * commands with brittle shell escaping (`extension/src/vscode/tmux.ts`).
6
+ * Lifting the orchestration into the CLI gives one source of truth that the
7
+ * extension, raw shells, `agents teams`, routines, and the Swarm MCP can all
8
+ * call into.
9
+ *
10
+ * Surface mirrors `agents pty`:
11
+ * agents tmux check
12
+ * agents tmux new <name> [--cmd ...] [--cwd DIR] [--replace] [--attach-existing] [--source S]
13
+ * agents tmux attach <name>
14
+ * agents tmux list [--json]
15
+ * agents tmux has <name>
16
+ * agents tmux split <name> <h|v> [--cmd ...] [--cwd DIR]
17
+ * agents tmux send <name>[:pane] <keys> [--no-enter] [--raw]
18
+ * agents tmux capture <name>[:pane] [--lines N] [--ansi]
19
+ * agents tmux info <name> [--json]
20
+ * agents tmux kill <name>
21
+ * agents tmux kill-all [--yes]
22
+ */
23
+ import type { Command } from 'commander';
24
+ /** Register the `agents tmux` command tree. */
25
+ export declare function registerTmuxCommands(program: Command): void;
@@ -0,0 +1,415 @@
1
+ /**
2
+ * `agents tmux` — terminal multiplexer integration.
3
+ *
4
+ * Why this exists: the swarmify VS Code extension was hand-rolling tmux
5
+ * commands with brittle shell escaping (`extension/src/vscode/tmux.ts`).
6
+ * Lifting the orchestration into the CLI gives one source of truth that the
7
+ * extension, raw shells, `agents teams`, routines, and the Swarm MCP can all
8
+ * call into.
9
+ *
10
+ * Surface mirrors `agents pty`:
11
+ * agents tmux check
12
+ * agents tmux new <name> [--cmd ...] [--cwd DIR] [--replace] [--attach-existing] [--source S]
13
+ * agents tmux attach <name>
14
+ * agents tmux list [--json]
15
+ * agents tmux has <name>
16
+ * agents tmux split <name> <h|v> [--cmd ...] [--cwd DIR]
17
+ * agents tmux send <name>[:pane] <keys> [--no-enter] [--raw]
18
+ * agents tmux capture <name>[:pane] [--lines N] [--ansi]
19
+ * agents tmux info <name> [--json]
20
+ * agents tmux kill <name>
21
+ * agents tmux kill-all [--yes]
22
+ */
23
+ import chalk from 'chalk';
24
+ import * as path from 'path';
25
+ import { setHelpSections } from '../lib/help.js';
26
+ import { assertTmuxAvailable, attachTmux, capturePane, createSession, getDefaultSocketPath, getTmuxVersion, hasSession, isTmuxInstalled, killAll, killSession, listSessions, readSessionMeta, sendKeys, splitPane, TmuxCommandError, TmuxSessionError, TmuxUnavailableError, } from '../lib/tmux/index.js';
27
+ /** Register the `agents tmux` command tree. */
28
+ export function registerTmuxCommands(program) {
29
+ const tmux = program
30
+ .command('tmux')
31
+ .description('Persistent terminal-multiplexer sessions for agents. Survive editor restarts, share with other tools.');
32
+ setHelpSections(tmux, {
33
+ examples: `
34
+ # Verify tmux is installed
35
+ agents tmux check
36
+
37
+ # Start a detached agent session
38
+ agents tmux new claude-debug --cmd "agents run claude" --cwd ~/code/myrepo
39
+
40
+ # Attach (replaces this shell with the tmux client)
41
+ agents tmux attach claude-debug
42
+
43
+ # Inspect remotely without attaching
44
+ agents tmux capture claude-debug --lines 200
45
+
46
+ # Send a slash command from a script
47
+ agents tmux send claude-debug "/clear"
48
+
49
+ # Clean up
50
+ agents tmux kill claude-debug
51
+ `,
52
+ notes: `
53
+ Storage:
54
+ Shared server socket: ~/.agents/.cache/helpers/tmux/server.sock
55
+ Per-session meta: ~/.agents/.cache/helpers/tmux/<name>.json
56
+
57
+ Session names accept [A-Za-z0-9_-] up to 64 characters. tmux disallows
58
+ '.' and ':' in names since those address windows and panes.
59
+
60
+ Existing tmux sessions you started outside of agents-cli are NOT
61
+ visible here unless you point them at the same socket via --socket.
62
+ `,
63
+ });
64
+ // ─── check ──────────────────────────────────────────────────────────────────
65
+ const checkCmd = tmux
66
+ .command('check')
67
+ .description('Check whether tmux is installed and report its version.')
68
+ .option('--json', 'Output as JSON');
69
+ checkCmd.action((opts) => {
70
+ const installed = isTmuxInstalled();
71
+ const version = installed ? getTmuxVersion() : null;
72
+ if (opts.json) {
73
+ console.log(JSON.stringify({ installed, version, socket: getDefaultSocketPath() }));
74
+ return;
75
+ }
76
+ if (installed) {
77
+ console.log(chalk.green('tmux:'), version ?? '(version unknown)');
78
+ console.log(chalk.gray(`socket: ${getDefaultSocketPath()}`));
79
+ }
80
+ else {
81
+ console.log(chalk.yellow('tmux is not installed.'));
82
+ console.log(chalk.gray(process.platform === 'darwin'
83
+ ? ' Install with: brew install tmux'
84
+ : ' Install with: apt install tmux (or your distro equivalent)'));
85
+ process.exit(1);
86
+ }
87
+ });
88
+ // ─── new ────────────────────────────────────────────────────────────────────
89
+ const newCmd = tmux
90
+ .command('new <name>')
91
+ .description('Start a detached tmux session running a command. The session persists until killed.')
92
+ .option('-c, --cmd <command>', 'Command to launch in the first pane (sh -c). Omit for an empty shell.')
93
+ .option('-d, --cwd <dir>', 'Working directory for the first pane')
94
+ .option('-w, --width <n>', 'Initial window width in columns (tmux clamps to client size on attach)')
95
+ .option('-h, --height <n>', 'Initial window height in rows')
96
+ .option('-s, --source <source>', 'Provenance label: cli|extension|teams|external', 'cli')
97
+ .option('--label <k=v...>', 'Free-form labels (repeatable)', collectLabel, {})
98
+ .option('--socket <path>', 'Use a custom socket (default: shared server)')
99
+ .option('--replace', 'Kill any existing session with the same name first')
100
+ .option('--attach-existing', 'Return the existing session if one with this name already exists')
101
+ .option('--json', 'Output session meta as JSON');
102
+ setHelpSections(newCmd, {
103
+ examples: `
104
+ # Detached agent session
105
+ agents tmux new claude-debug --cmd "agents run claude"
106
+
107
+ # Reuse the session if it already exists (idempotent)
108
+ agents tmux new pair --cmd "agents run claude" --attach-existing
109
+
110
+ # Replace a stale session
111
+ agents tmux new pair --cmd "agents run claude" --replace
112
+ `,
113
+ });
114
+ newCmd.action(async (name, opts) => {
115
+ await guardTmux(async () => {
116
+ const meta = await createSession({
117
+ name,
118
+ cmd: opts.cmd,
119
+ cwd: opts.cwd ? path.resolve(opts.cwd) : undefined,
120
+ width: opts.width ? parseInt(opts.width, 10) : undefined,
121
+ height: opts.height ? parseInt(opts.height, 10) : undefined,
122
+ source: validateSource(opts.source),
123
+ labels: Object.keys(opts.label).length > 0 ? opts.label : undefined,
124
+ socket: opts.socket,
125
+ replace: !!opts.replace,
126
+ attachExisting: !!opts.attachExisting,
127
+ });
128
+ if (opts.json) {
129
+ console.log(JSON.stringify(meta));
130
+ }
131
+ else {
132
+ console.log(meta.name);
133
+ }
134
+ });
135
+ });
136
+ // ─── attach ─────────────────────────────────────────────────────────────────
137
+ const attachCmd = tmux
138
+ .command('attach <name>')
139
+ .description('Attach to a running session. Replaces this shell with the tmux client until you detach (Ctrl-b d).')
140
+ .option('--socket <path>', 'Use a custom socket (default: shared server)');
141
+ attachCmd.action(async (name, opts) => {
142
+ await guardTmux(async () => {
143
+ const socket = opts.socket ?? getDefaultSocketPath();
144
+ if (!(await hasSession(name, socket))) {
145
+ console.error(chalk.red(`No tmux session named "${name}".`));
146
+ process.exit(1);
147
+ }
148
+ if (!process.stdout.isTTY) {
149
+ console.error(chalk.red('attach requires a TTY. Run this from an interactive shell.'));
150
+ process.exit(1);
151
+ }
152
+ const code = await attachTmux({ socket, args: ['attach-session', '-t', `=${name}`] });
153
+ process.exit(code);
154
+ });
155
+ });
156
+ // ─── list ───────────────────────────────────────────────────────────────────
157
+ const listCmd = tmux
158
+ .command('list')
159
+ .alias('ls')
160
+ .description('List live tmux sessions on the shared server. Prunes stale meta files as a side effect.')
161
+ .option('--socket <path>', 'Use a custom socket (default: shared server)')
162
+ .option('--json', 'Output as JSON');
163
+ listCmd.action(async (opts) => {
164
+ await guardTmux(async () => {
165
+ const sessions = await listSessions({ socket: opts.socket });
166
+ if (opts.json) {
167
+ console.log(JSON.stringify(sessions));
168
+ return;
169
+ }
170
+ if (sessions.length === 0) {
171
+ console.log(chalk.gray('No tmux sessions.'));
172
+ return;
173
+ }
174
+ for (const s of sessions) {
175
+ const age = formatAge(Date.now() / 1000 - s.createdAtTmux);
176
+ const cmd = s.meta?.cmd ? chalk.gray(` ${truncate(s.meta.cmd, 60)}`) : '';
177
+ const attached = s.attached ? chalk.green(' [attached]') : '';
178
+ console.log(` ${chalk.bold(s.name)} ${s.windows}w ${age}${attached}${cmd}`);
179
+ }
180
+ });
181
+ });
182
+ // ─── has ────────────────────────────────────────────────────────────────────
183
+ tmux
184
+ .command('has <name>')
185
+ .description('Exit 0 if a session with this name exists, 1 otherwise. Useful in shell scripts.')
186
+ .option('--socket <path>', 'Use a custom socket (default: shared server)')
187
+ .action(async (name, opts) => {
188
+ await guardTmux(async () => {
189
+ const exists = await hasSession(name, opts.socket);
190
+ process.exit(exists ? 0 : 1);
191
+ });
192
+ });
193
+ // ─── split ──────────────────────────────────────────────────────────────────
194
+ const splitCmd = tmux
195
+ .command('split <name> <direction>')
196
+ .description('Split the active pane of a session. Direction: h (left/right) or v (top/bottom).')
197
+ .option('-c, --cmd <command>', 'Command to launch in the new pane')
198
+ .option('-d, --cwd <dir>', 'Working directory for the new pane')
199
+ .option('--socket <path>', 'Use a custom socket (default: shared server)')
200
+ .option('--json', 'Output as JSON (returns new pane id like %3)');
201
+ setHelpSections(splitCmd, {
202
+ examples: `
203
+ # Split horizontally (panes side-by-side) and start a second agent
204
+ agents tmux split team h --cmd "agents run codex"
205
+
206
+ # Split vertically (panes stacked) — empty shell
207
+ agents tmux split team v
208
+ `,
209
+ });
210
+ splitCmd.action(async (name, direction, opts) => {
211
+ await guardTmux(async () => {
212
+ if (direction !== 'h' && direction !== 'v') {
213
+ console.error(chalk.red(`Invalid direction "${direction}". Use h or v.`));
214
+ process.exit(1);
215
+ }
216
+ const paneId = await splitPane({
217
+ name,
218
+ direction,
219
+ cmd: opts.cmd,
220
+ cwd: opts.cwd ? path.resolve(opts.cwd) : undefined,
221
+ socket: opts.socket,
222
+ });
223
+ if (opts.json) {
224
+ console.log(JSON.stringify({ paneId }));
225
+ }
226
+ else {
227
+ console.log(paneId);
228
+ }
229
+ });
230
+ });
231
+ // ─── send ───────────────────────────────────────────────────────────────────
232
+ const sendCmd = tmux
233
+ .command('send <target> <keys>')
234
+ .description('Send keystrokes to a session. Target is "name" or "name:pane" (e.g. team:%2 or team:1).')
235
+ .option('--no-enter', 'Do not append Enter after the keys')
236
+ .option('--raw', 'Send literally (-l) without tmux key-name interpretation (C-c, Enter, etc.)')
237
+ .option('--socket <path>', 'Use a custom socket (default: shared server)');
238
+ setHelpSections(sendCmd, {
239
+ examples: `
240
+ # Type a command into the session and press Enter
241
+ agents tmux send team "echo hello"
242
+
243
+ # Send Ctrl-C to interrupt
244
+ agents tmux send team "C-c"
245
+
246
+ # Send literal text (no key-name interpretation)
247
+ agents tmux send team "C-c" --raw
248
+
249
+ # Target a specific pane
250
+ agents tmux send team:%2 "ls -la"
251
+ `,
252
+ });
253
+ sendCmd.action(async (target, keys, opts) => {
254
+ await guardTmux(async () => {
255
+ const { name, pane } = splitTarget(target);
256
+ await sendKeys({
257
+ name,
258
+ pane,
259
+ keys,
260
+ noEnter: !opts.enter,
261
+ raw: !!opts.raw,
262
+ socket: opts.socket,
263
+ });
264
+ });
265
+ });
266
+ // ─── capture ────────────────────────────────────────────────────────────────
267
+ const captureCmd = tmux
268
+ .command('capture <target>')
269
+ .description('Print the contents of a pane. Target is "name" or "name:pane".')
270
+ .option('-l, --lines <n>', 'Include this many extra history lines above the visible screen')
271
+ .option('--ansi', 'Keep ANSI escape codes (default strips them)')
272
+ .option('--socket <path>', 'Use a custom socket (default: shared server)');
273
+ setHelpSections(captureCmd, {
274
+ examples: `
275
+ # See current screen
276
+ agents tmux capture team
277
+
278
+ # Include the last 500 history lines
279
+ agents tmux capture team --lines 500
280
+
281
+ # Target a specific pane
282
+ agents tmux capture team:%3
283
+ `,
284
+ });
285
+ captureCmd.action(async (target, opts) => {
286
+ await guardTmux(async () => {
287
+ const { name, pane } = splitTarget(target);
288
+ const text = await capturePane({
289
+ name,
290
+ pane,
291
+ lines: opts.lines ? parseInt(opts.lines, 10) : undefined,
292
+ ansi: !!opts.ansi,
293
+ socket: opts.socket,
294
+ });
295
+ process.stdout.write(text);
296
+ if (!text.endsWith('\n'))
297
+ process.stdout.write('\n');
298
+ });
299
+ });
300
+ // ─── info ───────────────────────────────────────────────────────────────────
301
+ const infoCmd = tmux
302
+ .command('info <name>')
303
+ .description('Show provenance for a session (cmd, cwd, created_at, source, labels).')
304
+ .option('--json', 'Output as JSON');
305
+ infoCmd.action(async (name, opts) => {
306
+ const meta = readSessionMeta(name);
307
+ if (!meta) {
308
+ console.error(chalk.yellow(`No meta for "${name}" (session may exist but wasn't created via agents tmux).`));
309
+ process.exit(1);
310
+ }
311
+ if (opts.json) {
312
+ console.log(JSON.stringify(meta, null, 2));
313
+ return;
314
+ }
315
+ printMeta(meta);
316
+ });
317
+ // ─── kill ───────────────────────────────────────────────────────────────────
318
+ const killCmd = tmux
319
+ .command('kill <name>')
320
+ .description('Kill one tmux session. Idempotent — exits 0 even if the session was already gone.')
321
+ .option('--socket <path>', 'Use a custom socket (default: shared server)');
322
+ killCmd.action(async (name, opts) => {
323
+ await guardTmux(async () => {
324
+ const killed = await killSession(name, opts.socket);
325
+ if (!killed) {
326
+ console.log(chalk.gray(`No session "${name}" — nothing to do.`));
327
+ }
328
+ });
329
+ });
330
+ // ─── kill-all ───────────────────────────────────────────────────────────────
331
+ const killAllCmd = tmux
332
+ .command('kill-all')
333
+ .description('Kill every session on the shared server and remove the socket. Requires --yes.')
334
+ .option('--yes', 'Confirm — required, no interactive prompt')
335
+ .option('--socket <path>', 'Use a custom socket (default: shared server)');
336
+ killAllCmd.action(async (opts) => {
337
+ await guardTmux(async () => {
338
+ if (!opts.yes) {
339
+ console.error(chalk.red('Refusing to kill-all without --yes.'));
340
+ process.exit(1);
341
+ }
342
+ const n = await killAll(opts.socket);
343
+ console.log(chalk.gray(`Killed ${n} session${n === 1 ? '' : 's'}.`));
344
+ });
345
+ });
346
+ }
347
+ // ─── helpers ──────────────────────────────────────────────────────────────────
348
+ /** Wrap a command action so tmux-specific errors render cleanly instead of throwing. */
349
+ async function guardTmux(fn) {
350
+ try {
351
+ assertTmuxAvailable();
352
+ await fn();
353
+ }
354
+ catch (err) {
355
+ if (err instanceof TmuxUnavailableError) {
356
+ console.error(chalk.red(err.message));
357
+ process.exit(127);
358
+ }
359
+ if (err instanceof TmuxSessionError) {
360
+ console.error(chalk.red(err.message));
361
+ process.exit(1);
362
+ }
363
+ if (err instanceof TmuxCommandError) {
364
+ console.error(chalk.red(err.message));
365
+ process.exit(err.code ?? 1);
366
+ }
367
+ throw err;
368
+ }
369
+ }
370
+ /** Parse `name` or `name:pane` (pane may be `%id` or a numeric index). */
371
+ function splitTarget(target) {
372
+ const idx = target.indexOf(':');
373
+ if (idx === -1)
374
+ return { name: target };
375
+ return { name: target.slice(0, idx), pane: target.slice(idx + 1) || undefined };
376
+ }
377
+ function validateSource(s) {
378
+ if (s === 'cli' || s === 'extension' || s === 'teams' || s === 'external')
379
+ return s;
380
+ return 'cli';
381
+ }
382
+ function collectLabel(value, acc) {
383
+ const eq = value.indexOf('=');
384
+ if (eq === -1)
385
+ return acc;
386
+ acc[value.slice(0, eq)] = value.slice(eq + 1);
387
+ return acc;
388
+ }
389
+ function formatAge(secs) {
390
+ if (secs < 60)
391
+ return `${Math.max(0, Math.floor(secs))}s`;
392
+ const m = Math.floor(secs / 60);
393
+ if (m < 60)
394
+ return `${m}m`;
395
+ const h = Math.floor(m / 60);
396
+ return `${h}h ${m % 60}m`;
397
+ }
398
+ function truncate(s, n) {
399
+ return s.length > n ? `${s.slice(0, n - 1)}…` : s;
400
+ }
401
+ function printMeta(m) {
402
+ console.log(` ${chalk.bold('name')} ${m.name}`);
403
+ console.log(` ${chalk.bold('socket')} ${m.socket}`);
404
+ console.log(` ${chalk.bold('createdAt')} ${new Date(m.createdAt).toISOString()}`);
405
+ console.log(` ${chalk.bold('source')} ${m.source}`);
406
+ if (m.cwd)
407
+ console.log(` ${chalk.bold('cwd')} ${m.cwd}`);
408
+ if (m.cmd)
409
+ console.log(` ${chalk.bold('cmd')} ${m.cmd}`);
410
+ if (m.labels) {
411
+ for (const [k, v] of Object.entries(m.labels)) {
412
+ console.log(` ${chalk.bold('label')} ${k}=${v}`);
413
+ }
414
+ }
415
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * `agents trash` — list and restore soft-deleted version directories.
3
3
  *
4
- * `removeVersion` moves a version dir to ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/
4
+ * `removeVersion` moves a version dir to ~/.agents/.history/trash/versions/<agent>/<version>/<timestamp>/
5
5
  * instead of hard-deleting. These commands let the user inspect what's there
6
6
  * and put a soft-deleted version back. The trash never auto-expires; only
7
- * `rm -rf ~/.agents-system/trash/` removes bytes from disk.
7
+ * `rm -rf ~/.agents/.history/trash/` removes bytes from disk.
8
8
  */
9
9
  import type { Command } from 'commander';
10
10
  export declare function registerTrashCommands(program: Command): void;
@@ -143,7 +143,7 @@ export function registerTrashCommands(program) {
143
143
  });
144
144
  trash
145
145
  .command('restore <target>')
146
- .description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents-system/versions/')
146
+ .description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents/.history/versions/')
147
147
  .action((target) => {
148
148
  const parsed = parseAgentVersion(target);
149
149
  if (!parsed) {