@phnx-labs/agents-cli 1.20.35 → 1.20.36

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 (225) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -15,15 +15,16 @@ import chalk from 'chalk';
15
15
  import ora from 'ora';
16
16
  import { SESSION_AGENTS } from '../lib/session/types.js';
17
17
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
18
- import { looksLikePath, toComparablePath, homeDir } from '../lib/platform/index.js';
18
+ import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
19
19
  import { getActiveSessions } from '../lib/session/active.js';
20
20
  import { machineId, normalizeHost } from '../lib/session/sync/config.js';
21
- import { gatherRemoteActive } from '../lib/session/remote-active.js';
21
+ import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
22
+ import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
22
23
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
23
24
  import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
24
25
  import { filterTeamSessions } from '../lib/session/team-filter.js';
25
26
  import { parseSession } from '../lib/session/parse.js';
26
- import { runRemoteSessions } from '../lib/session/remote.js';
27
+ import { runRemoteSessions, buildForwardedArgs } from '../lib/session/remote.js';
27
28
  import { formatRelativeTime } from '../lib/session/relative-time.js';
28
29
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
29
30
  import { renderMarkdown } from '../lib/markdown.js';
@@ -36,6 +37,7 @@ import { setHelpSections } from '../lib/help.js';
36
37
  import { registerSessionsTailCommand } from './sessions-tail.js';
37
38
  import { registerSessionsSyncCommand } from './sessions-sync.js';
38
39
  import { registerSessionsResumeCommand } from './sessions-resume.js';
40
+ import { registerSessionsInjectCommand } from './sessions-inject.js';
39
41
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
40
42
  const CLAUDE_RESUME_MATCH_WINDOW_MS = 10 * 60_000;
41
43
  const LOAD_VERBS = ['Loading', 'Scanning', 'Gathering', 'Indexing', 'Reading'];
@@ -78,6 +80,12 @@ function createScanProgressTracker(verbs, suffix, spinner) {
78
80
  }
79
81
  const PICKER_RECENT_COUNT = 15;
80
82
  const PICKER_POOL_LIMIT = 200;
83
+ // The grouped default view ("overview"): fetch a generous recency-ordered pool
84
+ // for accurate per-project totals, show each project's most-recent rows grouped
85
+ // by project, newest-active project first.
86
+ const OVERVIEW_ROWS_PER_PROJECT = 5; // recent rows shown per project before "· N more"
87
+ const OVERVIEW_POOL_LIMIT = 1000; // fetch cap — accurate per-project totals up to this
88
+ const OVERVIEW_MAX_PROJECTS = 12; // project groups shown before "+N more projects"
81
89
  /**
82
90
  * Resolve a path-like query to an absolute directory path.
83
91
  */
@@ -423,6 +431,41 @@ export function dedupeByMachineSession(sessions) {
423
431
  }
424
432
  return out;
425
433
  }
434
+ /**
435
+ * Order a merged listing so the local machine's sessions come first, then each
436
+ * remote machine as a contiguous block (more sessions first, then name), with
437
+ * every machine keeping its incoming order (timestamp) within the block. Also
438
+ * dedupes: a session present both locally (a synced mirror copy) and via live
439
+ * fan-out collapses to one, keyed by machine + session id. Rows are keyed by
440
+ * `machine` (discover tags local rows with the local id; fan-out tags remote
441
+ * rows with the peer id) falling back to `localMachine` when untagged. Pure —
442
+ * `localMachine` is injected so the ordering is testable without os.hostname().
443
+ */
444
+ export function mergeLocalFirst(sessions, localMachine) {
445
+ const byMachine = new Map();
446
+ const seen = new Set();
447
+ for (const s of sessions) {
448
+ const machine = s.machine || localMachine;
449
+ if (s.id) {
450
+ const dedupeKey = `${machine}:${s.id}`;
451
+ if (seen.has(dedupeKey))
452
+ continue;
453
+ seen.add(dedupeKey);
454
+ }
455
+ (byMachine.get(machine) ?? byMachine.set(machine, []).get(machine)).push(s);
456
+ }
457
+ const keys = Array.from(byMachine.keys()).sort((a, b) => {
458
+ if (a === localMachine)
459
+ return -1;
460
+ if (b === localMachine)
461
+ return 1;
462
+ const ac = byMachine.get(a).length, bc = byMachine.get(b).length;
463
+ if (ac !== bc)
464
+ return bc - ac;
465
+ return a.localeCompare(b);
466
+ });
467
+ return keys.flatMap((k) => byMachine.get(k));
468
+ }
426
469
  /** Print one machine's workspace tree, indented under its machine header. */
427
470
  function renderWorkspaceLayout(layout, base) {
428
471
  let first = true;
@@ -599,8 +642,18 @@ async function sessionsAction(query, options) {
599
642
  // Interactive picker loads a deep pool but shows only recent sessions
600
643
  // until the user starts typing. Non-interactive/JSON uses the explicit limit.
601
644
  const isInteractive = !options.json && isInteractiveTerminal();
602
- const limit = parseInt(options.limit || (isInteractive ? String(PICKER_POOL_LIMIT) : '50'), 10);
603
- const since = options.since ?? (isInteractive && !options.all ? '30d' : undefined);
645
+ // The grouped project overview is the default for a bare interactive listing:
646
+ // no query, no path drill-in, not explicitly --flat/--tree. It drops the silent
647
+ // cwd-scope + 50-cap + 30-day window that hide most of a large index.
648
+ const wantsOverview = isInteractive && !searchQuery && !pathFilter && !options.flat && !options.tree;
649
+ const limit = wantsOverview
650
+ ? OVERVIEW_POOL_LIMIT
651
+ : parseInt(options.limit || (isInteractive ? String(PICKER_POOL_LIMIT) : '50'), 10);
652
+ // Overview: recency order across the whole index, no default window; an explicit
653
+ // --since still narrows. Non-overview keeps the prior interactive-30d default.
654
+ const since = wantsOverview
655
+ ? options.since
656
+ : (options.since ?? (isInteractive && !options.all ? '30d' : undefined));
604
657
  const spinner = options.json ? null : ora().start();
605
658
  const tracker = createScanProgressTracker(LOAD_VERBS, 'sessions', spinner);
606
659
  try {
@@ -613,7 +666,7 @@ async function sessionsAction(query, options) {
613
666
  const scope = {
614
667
  agent,
615
668
  version,
616
- all: pathFilter ? undefined : options.all,
669
+ all: pathFilter ? undefined : (wantsOverview ? true : options.all),
617
670
  cwd: process.cwd(),
618
671
  cwdPrefix: pathFilter,
619
672
  project: options.project,
@@ -657,12 +710,42 @@ async function sessionsAction(query, options) {
657
710
  if (options.json) {
658
711
  const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
659
712
  const serializable = filtered.map(s => {
660
- const { _matchedTerms, _bm25Score, ...rest } = s;
713
+ const { _matchedTerms, _bm25Score, _remote, ...rest } = s;
661
714
  return rest;
662
715
  });
663
716
  process.stdout.write(JSON.stringify(serializable, null, 2) + '\n');
664
717
  return;
665
718
  }
719
+ // Cross-machine fan-out: unless --local (or we ARE a peer answering a
720
+ // parent's sweep), fold in other online machines' sessions live over SSH so
721
+ // the list spans the fleet without any sync — each remote row carries the
722
+ // machine it came from, and the picker/table label + group by it. Only the
723
+ // interactive picker and the printed table get this; --json and single-id
724
+ // resolution above stay local (a peer answers for itself; scripts get a
725
+ // deterministic local slice). Best-effort: a fan-out failure leaves the
726
+ // local list intact rather than erroring the whole command.
727
+ const forceLocal = options.local === true || process.env[NO_FANOUT_ENV] === '1';
728
+ if (!forceLocal) {
729
+ // Pass the hosts set so a variadic `--host a b` never leaks a host as a
730
+ // query (defensive: the --host-without-active early return above already
731
+ // means we only get here in auto-discovery mode, with no --host in argv).
732
+ const forwarded = buildForwardedArgs(process.argv, new Set(options.host ?? []));
733
+ if (!forwarded.includes('--json'))
734
+ forwarded.push('--json');
735
+ const fanSpinner = isInteractiveTerminal() ? ora('Reaching other machines...').start() : null;
736
+ try {
737
+ const { sessions: remoteSessions } = await gatherRemoteList(forwarded, options.host);
738
+ if (remoteSessions.length > 0) {
739
+ sessions = mergeLocalFirst([...sessions, ...remoteSessions], machineId());
740
+ }
741
+ }
742
+ catch {
743
+ // fan-out is an enrichment, never a hard dependency
744
+ }
745
+ finally {
746
+ fanSpinner?.stop();
747
+ }
748
+ }
666
749
  if (sessions.length === 0) {
667
750
  if (pathFilter) {
668
751
  console.log(chalk.gray(`No sessions found for ${pathFilter}.`));
@@ -675,9 +758,19 @@ async function sessionsAction(query, options) {
675
758
  }
676
759
  return;
677
760
  }
678
- // --tree is a printed grouped listing, not an interactive pick render it
679
- // directly even in a TTY.
680
- if (isInteractiveTerminal() && !options.tree) {
761
+ // The grouped project overview is the bare interactive default: a scannable
762
+ // dashboard of the whole fleet grouped by project, newest-active first.
763
+ // Interact/resume via `agents sessions <project>` or `agents sessions resume`.
764
+ if (wantsOverview) {
765
+ const liveIndex = await maybeLiveIndex(options);
766
+ // Per-project row cap is fixed (--limit carries a default of 50 and drives
767
+ // the fetch pool, not the display); `--all` expands every group instead.
768
+ printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all });
769
+ return;
770
+ }
771
+ // --tree / --flat are printed listings, not an interactive pick — render them
772
+ // directly even in a TTY. A search query keeps the interactive picker.
773
+ if (isInteractiveTerminal() && !options.tree && !options.flat) {
681
774
  const message = pathFilter
682
775
  ? `Search sessions (${path.basename(pathFilter)}):`
683
776
  : formatSearchMessage(options);
@@ -688,7 +781,7 @@ async function sessionsAction(query, options) {
688
781
  }
689
782
  return;
690
783
  }
691
- // Non-interactive fallback (piped output)
784
+ // Non-interactive fallback (piped output) or --flat/--tree.
692
785
  const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
693
786
  const liveIndex = await maybeLiveIndex(options);
694
787
  printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
@@ -724,7 +817,7 @@ function metaSignals(s) {
724
817
  * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
725
818
  * when `showTicket` — otherwise a listing with no refs would waste a column of
726
819
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
727
- function flatSessionRow(session, live, showTicket = false) {
820
+ function flatSessionRow(session, live, showTicket = false, cols = {}) {
728
821
  const agentColor = colorAgent(session.agent);
729
822
  const when = formatRelativeTime(session.timestamp);
730
823
  const project = session.project || '-';
@@ -735,17 +828,26 @@ function flatSessionRow(session, live, showTicket = false) {
735
828
  // resting one falls back to its opening topic.
736
829
  const doing = preview || (tag ? `${tag}${session.topic ?? ''}` : session.topic);
737
830
  const wt = session.worktreeSlug ? chalk.magenta(`wt:${session.worktreeSlug}`) : '';
831
+ // The machine column only earns its width when the listing spans more than one
832
+ // box (i.e. the cross-machine fan-out folded remotes in) — same rule and
833
+ // pool-derived width as the picker.
834
+ const machineColW = cols.machineWidth ?? PICKER_MACHINE_W;
835
+ const machineCell = cols.showMachine
836
+ ? chalk.gray(padToWidth(truncateToWidth((cols.machineLabel?.(session.machine ?? '') ?? session.machine ?? '') || '-', machineColW - 1), machineColW))
837
+ : '';
738
838
  const TICKET_W = 10;
739
839
  const ticketCell = showTicket
740
840
  ? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
741
841
  : '';
742
842
  const glyphW = glyph ? 2 : 0;
843
+ const machineW = cols.showMachine ? machineColW : 0;
743
844
  const ticketW = showTicket ? TICKET_W + 1 : 0;
744
845
  const wtW = wt ? stringWidth(wt) + 1 : 0;
745
- const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - ticketW - wtW - stringWidth(when) - 1);
846
+ const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - machineW - ticketW - wtW - stringWidth(when) - 1);
746
847
  return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
747
848
  agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
748
849
  chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
850
+ machineCell +
749
851
  chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
750
852
  (glyph ? glyph + ' ' : '') +
751
853
  renderTopicCell(label, doing, '', topicW, topicW) +
@@ -793,6 +895,79 @@ async function maybeLiveIndex(options) {
793
895
  return undefined;
794
896
  }
795
897
  }
898
+ /**
899
+ * Group key for the overview: prefer the indexed project name; else fold the cwd
900
+ * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
901
+ * repo, and a monorepo subdir falls back to its leaf dir basename. Pure.
902
+ */
903
+ export function overviewProjectKey(s) {
904
+ if (s.project && s.project.trim())
905
+ return s.project.trim();
906
+ const cwd = (s.cwd ?? '').replace(/\/+$/, '');
907
+ if (!cwd)
908
+ return '(no project)';
909
+ const wt = cwd.match(/\/([^/]+)\/\.agents\/worktrees\//);
910
+ if (wt)
911
+ return wt[1];
912
+ const parts = cwd.split('/');
913
+ return parts[parts.length - 1] || cwd;
914
+ }
915
+ /**
916
+ * Turn a recency-descending pool into project groups: each group shows its
917
+ * `perProjectCap` most-recent sessions (the rest become `· N more`), and groups
918
+ * are ordered by their most-recent session so the newest-active project leads.
919
+ * `perProjectCap = Infinity` expands every group. Pure — unit-tested.
920
+ */
921
+ export function buildOverviewGroups(pool, perProjectCap) {
922
+ const byKey = new Map();
923
+ for (const s of pool) {
924
+ const k = overviewProjectKey(s);
925
+ (byKey.get(k) ?? byKey.set(k, []).get(k)).push(s);
926
+ }
927
+ const cap = Math.max(1, perProjectCap);
928
+ const groups = [];
929
+ for (const [key, rows] of byKey) {
930
+ const shown = rows.slice(0, cap); // rows are recency-desc (pool was sorted)
931
+ groups.push({ key, total: rows.length, shown, more: rows.length - shown.length, maxTs: rows[0].timestamp });
932
+ }
933
+ groups.sort((a, b) => (a.maxTs < b.maxTs ? 1 : a.maxTs > b.maxTs ? -1 : a.key.localeCompare(b.key)));
934
+ return { groups, projectCount: byKey.size };
935
+ }
936
+ /**
937
+ * The grouped project overview — the bare interactive default. Shows the latest
938
+ * sessions grouped under their project, newest-active project first, with a
939
+ * `· N more` per project and a `+N more projects` when the list is capped.
940
+ */
941
+ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
942
+ const { groups } = buildOverviewGroups(pool, opts.expand ? Infinity : opts.perProjectCap);
943
+ const shownGroups = opts.expand ? groups : groups.slice(0, OVERVIEW_MAX_PROJECTS);
944
+ const hiddenProjects = groups.length - shownGroups.length;
945
+ const total = pool.length;
946
+ const projWord = groups.length === 1 ? 'project' : 'projects';
947
+ console.log(chalk.gray(`${total} session${total === 1 ? '' : 's'} · ${groups.length} ${projWord} · recent activity\n`));
948
+ let first = true;
949
+ for (const g of shownGroups) {
950
+ if (!first)
951
+ console.log();
952
+ first = false;
953
+ const { glyph } = liveGlyphAndPreview(liveIndex?.get(g.shown[0].id));
954
+ const head = `${chalk.cyan('▸')} ${chalk.cyan.bold(g.key)} ${chalk.gray(String(g.total))}` +
955
+ `${glyph ? ' ' + glyph : ''} ${chalk.gray(formatRelativeTime(g.maxTs))}`;
956
+ console.log(head);
957
+ for (const s of g.shown)
958
+ console.log(treeSessionRow(s, liveIndex?.get(s.id)));
959
+ if (g.more > 0)
960
+ console.log(' ' + chalk.gray(`· ${g.more} more`));
961
+ }
962
+ console.log();
963
+ const parts = [chalk.gray('newest first')];
964
+ if (hiddenProjects > 0)
965
+ parts.push(chalk.gray(`+${hiddenProjects} more project${hiddenProjects === 1 ? '' : 's'} · agents sessions --all`));
966
+ parts.push(chalk.gray('agents sessions <project> to drill in · --flat for the plain list'));
967
+ console.log(parts.join(chalk.gray(' · ')));
968
+ if (hiddenCount > 0)
969
+ console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
970
+ }
796
971
  function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
797
972
  if (tree) {
798
973
  // Group by directory; drop the id/version columns from view. The short id
@@ -823,10 +998,12 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
823
998
  return;
824
999
  }
825
1000
  // Only show the ticket column when at least one row carries a ref — otherwise
826
- // it's a column of dashes that steals width from every topic.
1001
+ // it's a column of dashes that steals width from every topic. The machine
1002
+ // column (and its compact labels) is computed the same way the picker does it.
827
1003
  const showTicket = sessions.some((s) => ticketLabel(s) !== '');
1004
+ const cols = pickerColumnsFor(sessions);
828
1005
  for (const session of sessions)
829
- console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket));
1006
+ console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols));
830
1007
  const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
831
1008
  console.log(chalk.gray(`\n${countLine}`));
832
1009
  if (hiddenCount > 0) {
@@ -983,7 +1160,58 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
983
1160
  }
984
1161
  return out + padding;
985
1162
  }
986
- export function formatPickerLabel(s, query) {
1163
+ /** Fallback machine-column width when a pool-derived width isn't supplied.
1164
+ * `pickerColumnsFor` normally computes `machineWidth` sized to the actual
1165
+ * hostnames, floored/capped by these bounds so common ids like `yosemite-s0`
1166
+ * (11) fit whole while a pathological hostname can't devour the topic column. */
1167
+ const PICKER_MACHINE_W = 11;
1168
+ const PICKER_MACHINE_MIN = 8;
1169
+ const PICKER_MACHINE_MAX = 18;
1170
+ /** Column width that shows every compacted hostname in `machines` whole (one
1171
+ * trailing space for separation), bounded by MIN/MAX. */
1172
+ function machineColumnWidth(machines, label) {
1173
+ const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
1174
+ return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
1175
+ }
1176
+ /**
1177
+ * Compact display form for machine ids: strip the longest shared dash-delimited
1178
+ * prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
1179
+ * ("zion", "mac-mini") stay whole. Stripping a *common* prefix can't collide,
1180
+ * and at least one segment is always kept.
1181
+ */
1182
+ export function machineLabeler(machines) {
1183
+ const uniq = [...new Set(machines.filter(Boolean))];
1184
+ if (uniq.length < 2)
1185
+ return (m) => m;
1186
+ const parts = uniq.map((m) => m.split('-'));
1187
+ const min = Math.min(...parts.map((p) => p.length));
1188
+ let shared = 0;
1189
+ while (shared < min - 1 && parts.every((p) => p[shared] === parts[0][shared]))
1190
+ shared++;
1191
+ if (shared === 0)
1192
+ return (m) => m;
1193
+ return (m) => {
1194
+ const p = m.split('-');
1195
+ return p.length > shared ? p.slice(shared).join('-') : m;
1196
+ };
1197
+ }
1198
+ /**
1199
+ * Column flags for a picker, computed once over the whole pool so every row
1200
+ * aligns: the machine column only earns its width when the listing spans more
1201
+ * than one box, the ticket column only when some row carries a PR/ticket ref.
1202
+ */
1203
+ export function pickerColumnsFor(sessions) {
1204
+ const machines = sessions.map((s) => s.machine).filter((m) => !!m);
1205
+ const distinct = [...new Set(machines)];
1206
+ const machineLabel = machineLabeler(machines);
1207
+ return {
1208
+ showMachine: distinct.length > 1,
1209
+ machineLabel,
1210
+ machineWidth: machineColumnWidth(distinct, machineLabel),
1211
+ showTicket: sessions.some((s) => ticketLabel(s) !== ''),
1212
+ };
1213
+ }
1214
+ export function formatPickerLabel(s, query, cols = {}) {
987
1215
  const agentColor = colorAgent(s.agent);
988
1216
  const when = formatRelativeTime(s.timestamp);
989
1217
  const project = s.project || '-';
@@ -991,20 +1219,56 @@ export function formatPickerLabel(s, query) {
991
1219
  const label = s.label;
992
1220
  const topic = tag ? `${tag}${s.topic ?? ''}` : s.topic;
993
1221
  const versionStr = s.version || '-';
1222
+ const wt = s.worktreeSlug ? chalk.magenta(`wt:${s.worktreeSlug}`) : '';
1223
+ const machineW = cols.machineWidth ?? PICKER_MACHINE_W;
1224
+ const machineCell = cols.showMachine
1225
+ ? chalk.gray(padRight(truncate((cols.machineLabel?.(s.machine ?? '') ?? s.machine ?? '') || '-', machineW - 1), machineW))
1226
+ : '';
1227
+ const TICKET_W = 10;
1228
+ const ticketCell = cols.showTicket
1229
+ ? chalk.blue(padRight(truncate(ticketLabel(s) || '-', TICKET_W), TICKET_W + 1))
1230
+ : '';
1231
+ // The picker prepends a gutter (cursor, plus a checkbox in multi-select mode);
1232
+ // reserve it, plus the conditional columns, so the topic shrinks to fit and
1233
+ // rows never wrap.
1234
+ const gutter = cols.gutter ?? 2;
1235
+ const machineColW = cols.showMachine ? machineW : 0;
1236
+ const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
1237
+ const wtW = wt ? stringWidth(wt) + 1 : 0;
1238
+ const topicW = Math.max(16, terminalWidth() - gutter - (10 + 9 + 8 + 16) - machineColW - ticketW - wtW - stringWidth(when) - 1);
994
1239
  return (chalk.white(padRight(s.shortId, 10)) +
995
1240
  agentColor(padRight(truncate(s.agent, 8), 9)) +
996
1241
  chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
1242
+ machineCell +
997
1243
  chalk.cyan(padRight(truncate(project, 14), 16)) +
998
- renderTopicCell(label, topic, query, 48, 50) +
1244
+ renderTopicCell(label, topic, query, topicW, topicW) +
1245
+ ticketCell +
1246
+ (wt ? wt + ' ' : '') +
999
1247
  chalk.gray(when));
1000
1248
  }
1249
+ /** Hints rotated above the picker so the flags/features stay discoverable. */
1250
+ const PICKER_TIPS = [
1251
+ 'Tip: narrow with -a/--agent (e.g. -a codex), or --project <name> for another folder.',
1252
+ "Tip: --all searches every directory; -H/--host <machine> folds in another box's sessions.",
1253
+ 'Tip: just type to fuzzy-search prompts and responses; press space to preview a session.',
1254
+ 'Tip: --since 2d / --until <date> bound the time window; pass a session id to open it directly.',
1255
+ ];
1256
+ /**
1257
+ * Pick a hint to show above the picker. Deterministic (keys off the pool size)
1258
+ * so it stays fixed across the picker's re-renders within a single run.
1259
+ */
1260
+ export function formatPickerTip(sessions) {
1261
+ return chalk.gray(PICKER_TIPS[sessions.length % PICKER_TIPS.length]);
1262
+ }
1001
1263
  export async function pickSessionInteractive(sessions, message = 'Search sessions:', initialSearch, hiddenCount = 0) {
1002
1264
  if (hiddenCount > 0) {
1003
1265
  console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
1004
1266
  }
1267
+ const cols = pickerColumnsFor(sessions);
1005
1268
  try {
1006
1269
  return await sessionPicker({
1007
1270
  message,
1271
+ subtitle: formatPickerTip(sessions),
1008
1272
  sessions,
1009
1273
  filter: (query) => {
1010
1274
  // No query: show the full pool (picker viewport still paginates via pageSize).
@@ -1013,7 +1277,7 @@ export async function pickSessionInteractive(sessions, message = 'Search session
1013
1277
  return sessions;
1014
1278
  return filterSessionsByQuery(sessions, query);
1015
1279
  },
1016
- labelFor: (s, query) => formatPickerLabel(s, query),
1280
+ labelFor: (s, query) => formatPickerLabel(s, query, cols),
1017
1281
  pageSize: PICKER_RECENT_COUNT,
1018
1282
  initialSearch,
1019
1283
  });
@@ -1024,7 +1288,41 @@ export async function pickSessionInteractive(sessions, message = 'Search session
1024
1288
  throw err;
1025
1289
  }
1026
1290
  }
1291
+ /**
1292
+ * The machine a picked session lives on when its transcript is on that peer's
1293
+ * disk (folded in over the live fan-out), else undefined. Keys off `_remote`,
1294
+ * NOT `machine !== local`: a synced mirror is machine-tagged too, but its file
1295
+ * is a local mirror path, so it must be read/resumed locally like any other.
1296
+ */
1297
+ function remoteMachineOf(session) {
1298
+ return session._remote ? session.machine : undefined;
1299
+ }
1300
+ /** True when the peer wasn't a dialable device; prints one clear line so a
1301
+ * remote pick never dead-ends silently. */
1302
+ function warnNoPeerTarget(machine, session) {
1303
+ console.log(chalk.yellow(`Session ${session.shortId} lives on ${machine}, which isn't a reachable device right now.`));
1304
+ console.log(chalk.gray(`Register/wake it (ag devices), or run there: agents ssh ${machine}`));
1305
+ }
1027
1306
  async function handlePickedSession(picked) {
1307
+ // A session on another machine is read/resumed ON that machine over SSH — its
1308
+ // transcript and agent binary live there. Both actions execute on the peer
1309
+ // (not a local `--host` hop, which would discover locally and dead-end for a
1310
+ // session that exists only on the peer).
1311
+ const remote = remoteMachineOf(picked.session);
1312
+ if (remote) {
1313
+ if (picked.action === 'view') {
1314
+ const rc = await runOnPeer(['sessions', picked.session.shortId, '--markdown'], remote);
1315
+ if (rc === 'no-target')
1316
+ warnNoPeerTarget(remote, picked.session);
1317
+ }
1318
+ else {
1319
+ console.log(chalk.gray(`Resuming ${picked.session.shortId} on ${remote} over SSH...`));
1320
+ const rc = await runOnPeer(['sessions', 'resume', picked.session.shortId], remote, { tty: true });
1321
+ if (rc === 'no-target')
1322
+ warnNoPeerTarget(remote, picked.session);
1323
+ }
1324
+ return;
1325
+ }
1028
1326
  if (picked.action === 'view') {
1029
1327
  await renderSession(picked.session, 'summary', {});
1030
1328
  return;
@@ -1049,26 +1347,49 @@ export async function resumeSessionInPlace(session) {
1049
1347
  return;
1050
1348
  }
1051
1349
  console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
1052
- await new Promise((resolve) => {
1053
- const child = spawn(resume[0], resume.slice(1), {
1054
- cwd,
1055
- stdio: 'inherit',
1056
- shell: false,
1057
- });
1350
+ // Resolve the (possibly version-pinned) launcher up front. On Windows the
1351
+ // agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
1352
+ // spawnResumeCommand), a missing command exits non-zero rather than emitting
1353
+ // an ENOENT `error` event — so detect a removed version here instead of
1354
+ // relying on that event, keeping the /continue fallback working on every OS.
1355
+ if (!findExecutable(resume[0]) && session.version) {
1356
+ const fallback = buildFallbackCommand(session);
1357
+ if (fallback) {
1358
+ console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
1359
+ await spawnResumeCommand(fallback, cwd);
1360
+ return;
1361
+ }
1362
+ }
1363
+ await spawnResumeCommand(resume, cwd);
1364
+ }
1365
+ /**
1366
+ * Spawn a resume command as a foreground takeover (inherited stdio), resolving
1367
+ * when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
1368
+ * `spawn` can't exec directly — a bare-name `shell:false` spawn throws
1369
+ * `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
1370
+ * The spawn is guarded because such a failure can be thrown synchronously;
1371
+ * without the guard it would surface under an unrelated "Failed to discover
1372
+ * sessions" catch upstream instead of a truthful launch error.
1373
+ */
1374
+ function spawnResumeCommand(cmd, cwd) {
1375
+ return new Promise((resolve) => {
1376
+ let child;
1377
+ try {
1378
+ child = spawn(cmd[0], cmd.slice(1), {
1379
+ cwd,
1380
+ stdio: 'inherit',
1381
+ shell: needsWindowsShell(cmd[0]),
1382
+ });
1383
+ }
1384
+ catch (err) {
1385
+ console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
1386
+ resolve();
1387
+ return;
1388
+ }
1058
1389
  child.on('error', (err) => {
1059
- if (err.code === 'ENOENT' && session.version) {
1060
- const fallback = buildFallbackCommand(session);
1061
- if (fallback) {
1062
- console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
1063
- const fb = spawn(fallback[0], fallback.slice(1), { cwd, stdio: 'inherit', shell: false });
1064
- fb.on('error', (e) => { console.error(chalk.red(`Failed: ${e.message}`)); resolve(); });
1065
- fb.on('close', () => resolve());
1066
- return;
1067
- }
1068
- }
1069
- console.error(chalk.red(`Failed to launch ${resume[0]}: ${err.message}`));
1390
+ console.error(chalk.red(`Failed to launch ${cmd[0]}: ${err.message}`));
1070
1391
  if (err.code === 'ENOENT') {
1071
- console.error(chalk.gray(`Make sure '${resume[0]}' is on your PATH.`));
1392
+ console.error(chalk.gray(`Make sure '${cmd[0]}' is on your PATH.`));
1072
1393
  }
1073
1394
  resolve();
1074
1395
  });
@@ -1481,9 +1802,10 @@ export function registerSessionsCommands(program) {
1481
1802
  .option('--artifacts', 'List all files written or edited during a session')
1482
1803
  .option('--artifact <name>', 'Read a specific artifact by filename or path (outputs to stdout)')
1483
1804
  .option('--active', 'Show only sessions running right now across terminals, teams, cloud, and headless agents')
1484
- .option('--local', 'With --active: only this machine — skip the cross-machine SSH fan-out')
1805
+ .option('--local', 'Only this machine — skip the cross-machine SSH fan-out (default listing and --active)')
1485
1806
  .option('--waiting', 'With --active: show only sessions waiting on your input (exits non-zero if any)')
1486
1807
  .option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
1808
+ .option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
1487
1809
  .option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
1488
1810
  .option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
1489
1811
  .option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)');
@@ -1501,6 +1823,10 @@ export function registerSessionsCommands(program) {
1501
1823
  # Show only what's running right now (terminals, teams, cloud, headless)
1502
1824
  agents sessions --active
1503
1825
 
1826
+ # The interactive list folds in other online machines automatically,
1827
+ # labelled by host with this machine first. Stay local with --local:
1828
+ agents sessions --local
1829
+
1504
1830
  # Search across every directory, not just this project
1505
1831
  agents sessions "topic" --all
1506
1832
 
@@ -1514,6 +1840,7 @@ export function registerSessionsCommands(program) {
1514
1840
  agents sessions --all "deploy script" --host box-a --host box-b
1515
1841
  `,
1516
1842
  notes: `
1843
+ - The interactive listing folds in your other online machines automatically (live over SSH, no sync) — each row is labelled by host, this machine first. Use --local to skip the fan-out; --json and single-id lookups stay local.
1517
1844
  - --host runs the query on the remote's own index over SSH (host alias or user@host); repeat or pass several to fan out. SSH access is the only auth.
1518
1845
  - --include and --exclude are mutually exclusive.
1519
1846
  - --first and --last are mutually exclusive.
@@ -1528,6 +1855,7 @@ export function registerSessionsCommands(program) {
1528
1855
  registerSessionsTailCommand(sessionsCmd);
1529
1856
  registerSessionsSyncCommand(sessionsCmd);
1530
1857
  registerSessionsResumeCommand(sessionsCmd);
1858
+ registerSessionsInjectCommand(sessionsCmd);
1531
1859
  }
1532
1860
  function formatNoSessionsMessage(showAll, project) {
1533
1861
  const projectQuery = project?.trim();
@@ -76,9 +76,11 @@ export async function runSetup(program, options = {}) {
76
76
  console.log(chalk.gray(`Populate ~/.agents/.system/ yourself before running other commands that depend on it.\n`));
77
77
  }
78
78
  else {
79
- console.log(chalk.gray(`Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents/.system/.\n`));
79
+ console.log(chalk.gray(alreadyConfigured
80
+ ? `Updating the system repo from ${systemRepoSlug(systemRepo)} in ~/.agents/.system/.\n`
81
+ : `Cloning the system repo from ${systemRepoSlug(systemRepo)} into ~/.agents/.system/.\n`));
80
82
  ensureAgentsDir();
81
- const spinner = ora('Cloning system repo...').start();
83
+ const spinner = ora(alreadyConfigured ? 'Updating system repo...' : 'Cloning system repo...').start();
82
84
  if (isGitRepo(agentsDir)) {
83
85
  // --force on an existing repo: pull instead of re-clone
84
86
  const result = await pullRepo(agentsDir);
@@ -2,10 +2,12 @@
2
2
  * `agents sync` — synchronize central resources into an installed agent version.
3
3
  *
4
4
  * Forms:
5
- * agents sync # umbrella: fetch remote (repos+secrets+sessions) -> reconcile all
5
+ * agents sync # umbrella: fetch config repos -> reconcile all (secrets/sessions opt-in)
6
6
  * agents sync --repos|--secrets|--sessions # umbrella: fetch only those, then reconcile
7
7
  * agents sync --cloud # umbrella: fetch all, skip reconcile
8
8
  * agents sync --local # umbrella: reconcile all, no fetch
9
+ * agents sync system # one repo: git pull --rebase (pull-only mirror)
10
+ * agents sync user # one repo: git pull --rebase + push
9
11
  * agents sync claude # one agent: uses default/sole installed version
10
12
  * agents sync claude@2.1.142 # one agent: explicit version
11
13
  * agents sync claude@latest # one agent: newest installed