@phnx-labs/agents-cli 1.20.74 → 1.20.76

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 (199) hide show
  1. package/CHANGELOG.md +893 -0
  2. package/README.md +36 -10
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.js +21 -3
  5. package/dist/commands/attach.d.ts +10 -0
  6. package/dist/commands/attach.js +41 -0
  7. package/dist/commands/computer.js +3 -3
  8. package/dist/commands/detach-core.d.ts +46 -0
  9. package/dist/commands/detach-core.js +61 -0
  10. package/dist/commands/detach.d.ts +5 -0
  11. package/dist/commands/detach.js +171 -0
  12. package/dist/commands/doctor.js +18 -0
  13. package/dist/commands/exec.js +31 -3
  14. package/dist/commands/feed.d.ts +7 -1
  15. package/dist/commands/feed.js +47 -4
  16. package/dist/commands/go.d.ts +9 -2
  17. package/dist/commands/go.js +16 -6
  18. package/dist/commands/harness.d.ts +14 -0
  19. package/dist/commands/harness.js +145 -0
  20. package/dist/commands/import.js +38 -8
  21. package/dist/commands/inspect.js +5 -2
  22. package/dist/commands/profiles.d.ts +20 -0
  23. package/dist/commands/profiles.js +68 -21
  24. package/dist/commands/repo.js +14 -0
  25. package/dist/commands/routines.js +15 -2
  26. package/dist/commands/secrets.js +44 -21
  27. package/dist/commands/sessions-browser.js +1 -1
  28. package/dist/commands/sessions-export.js +19 -5
  29. package/dist/commands/sessions-migrate.d.ts +29 -0
  30. package/dist/commands/sessions-migrate.js +596 -0
  31. package/dist/commands/sessions-picker.js +16 -1
  32. package/dist/commands/sessions.d.ts +36 -13
  33. package/dist/commands/sessions.js +210 -40
  34. package/dist/commands/setup-computer.js +2 -2
  35. package/dist/commands/ssh.js +71 -35
  36. package/dist/commands/status.js +10 -2
  37. package/dist/commands/versions.js +15 -6
  38. package/dist/commands/view.d.ts +5 -0
  39. package/dist/commands/view.js +24 -4
  40. package/dist/commands/watchdog.d.ts +7 -2
  41. package/dist/commands/watchdog.js +73 -57
  42. package/dist/index.js +58 -7
  43. package/dist/lib/activity.d.ts +14 -4
  44. package/dist/lib/activity.js +17 -5
  45. package/dist/lib/actor.d.ts +50 -0
  46. package/dist/lib/actor.js +166 -0
  47. package/dist/lib/agent-spec/provider.js +2 -1
  48. package/dist/lib/agent-spec/resolve.js +19 -5
  49. package/dist/lib/agent-spec/types.d.ts +9 -1
  50. package/dist/lib/agents.d.ts +21 -0
  51. package/dist/lib/agents.js +70 -6
  52. package/dist/lib/cloud/codex.d.ts +2 -0
  53. package/dist/lib/cloud/codex.js +14 -3
  54. package/dist/lib/cloud/session-index.d.ts +32 -0
  55. package/dist/lib/cloud/session-index.js +58 -0
  56. package/dist/lib/cloud/store.d.ts +7 -0
  57. package/dist/lib/cloud/store.js +25 -0
  58. package/dist/lib/config-transfer.js +4 -0
  59. package/dist/lib/daemon.js +18 -2
  60. package/dist/lib/devices/connect.d.ts +15 -1
  61. package/dist/lib/devices/connect.js +15 -1
  62. package/dist/lib/devices/fleet.d.ts +21 -0
  63. package/dist/lib/devices/fleet.js +26 -0
  64. package/dist/lib/devices/health-report.d.ts +38 -0
  65. package/dist/lib/devices/health-report.js +214 -0
  66. package/dist/lib/devices/health.js +4 -1
  67. package/dist/lib/devices/reachability.d.ts +31 -0
  68. package/dist/lib/devices/reachability.js +40 -0
  69. package/dist/lib/devices/registry.d.ts +33 -0
  70. package/dist/lib/devices/registry.js +37 -0
  71. package/dist/lib/devices/resolve-target.d.ts +15 -27
  72. package/dist/lib/devices/resolve-target.js +63 -102
  73. package/dist/lib/devices/sync.d.ts +18 -0
  74. package/dist/lib/devices/sync.js +23 -1
  75. package/dist/lib/devices/tailscale.d.ts +3 -0
  76. package/dist/lib/devices/tailscale.js +1 -0
  77. package/dist/lib/events.d.ts +1 -1
  78. package/dist/lib/exec.d.ts +66 -0
  79. package/dist/lib/exec.js +195 -6
  80. package/dist/lib/feed-post.d.ts +63 -0
  81. package/dist/lib/feed-post.js +204 -0
  82. package/dist/lib/fleet/apply.d.ts +32 -2
  83. package/dist/lib/fleet/apply.js +97 -10
  84. package/dist/lib/fleet/types.d.ts +11 -0
  85. package/dist/lib/fs-walk.d.ts +13 -0
  86. package/dist/lib/fs-walk.js +16 -7
  87. package/dist/lib/hooks.d.ts +1 -0
  88. package/dist/lib/hooks.js +156 -0
  89. package/dist/lib/hosts/dispatch.d.ts +7 -0
  90. package/dist/lib/hosts/dispatch.js +2 -0
  91. package/dist/lib/hosts/passthrough.js +2 -0
  92. package/dist/lib/hosts/reconnect.d.ts +78 -0
  93. package/dist/lib/hosts/reconnect.js +127 -0
  94. package/dist/lib/hosts/registry.d.ts +75 -14
  95. package/dist/lib/hosts/registry.js +205 -30
  96. package/dist/lib/hosts/remote-cmd.js +1 -0
  97. package/dist/lib/hosts/run-target.d.ts +13 -5
  98. package/dist/lib/hosts/run-target.js +26 -10
  99. package/dist/lib/hosts/session-index.d.ts +18 -1
  100. package/dist/lib/hosts/session-index.js +37 -2
  101. package/dist/lib/hosts/session-marker.d.ts +33 -0
  102. package/dist/lib/hosts/session-marker.js +51 -0
  103. package/dist/lib/hosts/tasks.d.ts +8 -4
  104. package/dist/lib/import.d.ts +2 -0
  105. package/dist/lib/import.js +35 -2
  106. package/dist/lib/menubar/install-menubar.d.ts +20 -0
  107. package/dist/lib/menubar/install-menubar.js +45 -1
  108. package/dist/lib/migrate.d.ts +16 -0
  109. package/dist/lib/migrate.js +36 -0
  110. package/dist/lib/models.d.ts +27 -0
  111. package/dist/lib/models.js +54 -1
  112. package/dist/lib/overdue.d.ts +7 -2
  113. package/dist/lib/overdue.js +18 -11
  114. package/dist/lib/picker.js +4 -1
  115. package/dist/lib/platform/process.d.ts +17 -0
  116. package/dist/lib/platform/process.js +70 -0
  117. package/dist/lib/profiles-presets.js +9 -7
  118. package/dist/lib/profiles.d.ts +31 -0
  119. package/dist/lib/profiles.js +70 -0
  120. package/dist/lib/pty-server.d.ts +2 -10
  121. package/dist/lib/pty-server.js +4 -38
  122. package/dist/lib/registry.d.ts +1 -1
  123. package/dist/lib/registry.js +48 -8
  124. package/dist/lib/rotate.d.ts +18 -0
  125. package/dist/lib/rotate.js +28 -0
  126. package/dist/lib/routines.d.ts +26 -0
  127. package/dist/lib/routines.js +19 -2
  128. package/dist/lib/runner.js +104 -7
  129. package/dist/lib/secrets/agent.d.ts +66 -1
  130. package/dist/lib/secrets/agent.js +154 -90
  131. package/dist/lib/secrets/bundles.d.ts +34 -0
  132. package/dist/lib/secrets/bundles.js +179 -19
  133. package/dist/lib/secrets/index.d.ts +15 -1
  134. package/dist/lib/secrets/index.js +21 -3
  135. package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
  136. package/dist/lib/secrets/rc-hygiene.js +154 -0
  137. package/dist/lib/secrets/remote.d.ts +7 -4
  138. package/dist/lib/secrets/remote.js +7 -4
  139. package/dist/lib/secrets/sync-commands.d.ts +21 -0
  140. package/dist/lib/secrets/sync-commands.js +21 -0
  141. package/dist/lib/self-update.d.ts +23 -2
  142. package/dist/lib/self-update.js +86 -5
  143. package/dist/lib/session/active.d.ts +114 -7
  144. package/dist/lib/session/active.js +323 -52
  145. package/dist/lib/session/cloud.js +2 -1
  146. package/dist/lib/session/db.d.ts +48 -0
  147. package/dist/lib/session/db.js +238 -14
  148. package/dist/lib/session/detached.d.ts +30 -0
  149. package/dist/lib/session/detached.js +92 -0
  150. package/dist/lib/session/discover.d.ts +465 -2
  151. package/dist/lib/session/discover.js +1648 -468
  152. package/dist/lib/session/fork.js +2 -1
  153. package/dist/lib/session/hook-sessions.d.ts +43 -0
  154. package/dist/lib/session/hook-sessions.js +135 -0
  155. package/dist/lib/session/linear.d.ts +6 -0
  156. package/dist/lib/session/linear.js +55 -0
  157. package/dist/lib/session/migrate-targets.d.ts +65 -0
  158. package/dist/lib/session/migrate-targets.js +94 -0
  159. package/dist/lib/session/migrations.d.ts +37 -0
  160. package/dist/lib/session/migrations.js +60 -0
  161. package/dist/lib/session/parse.d.ts +18 -0
  162. package/dist/lib/session/parse.js +130 -30
  163. package/dist/lib/session/pid-registry.d.ts +34 -2
  164. package/dist/lib/session/pid-registry.js +49 -2
  165. package/dist/lib/session/render.d.ts +7 -2
  166. package/dist/lib/session/render.js +22 -10
  167. package/dist/lib/session/short-id.d.ts +17 -0
  168. package/dist/lib/session/short-id.js +20 -0
  169. package/dist/lib/session/state.js +6 -2
  170. package/dist/lib/shims.d.ts +1 -1
  171. package/dist/lib/shims.js +20 -6
  172. package/dist/lib/sqlite.d.ts +3 -2
  173. package/dist/lib/sqlite.js +27 -4
  174. package/dist/lib/staleness/detectors/commands.js +1 -1
  175. package/dist/lib/staleness/detectors/workflows.js +13 -2
  176. package/dist/lib/staleness/writers/commands.js +7 -7
  177. package/dist/lib/staleness/writers/workflows.d.ts +4 -2
  178. package/dist/lib/startup/command-registry.d.ts +1 -0
  179. package/dist/lib/startup/command-registry.js +3 -0
  180. package/dist/lib/state.d.ts +1 -1
  181. package/dist/lib/state.js +1 -32
  182. package/dist/lib/teams/agents.d.ts +19 -10
  183. package/dist/lib/teams/agents.js +40 -39
  184. package/dist/lib/types.d.ts +37 -2
  185. package/dist/lib/usage.d.ts +33 -1
  186. package/dist/lib/usage.js +172 -12
  187. package/dist/lib/versions.d.ts +9 -2
  188. package/dist/lib/versions.js +37 -8
  189. package/dist/lib/watchdog/log.d.ts +43 -0
  190. package/dist/lib/watchdog/log.js +69 -0
  191. package/dist/lib/watchdog/routine.d.ts +44 -0
  192. package/dist/lib/watchdog/routine.js +69 -0
  193. package/dist/lib/watchdog/runner.d.ts +51 -7
  194. package/dist/lib/watchdog/runner.js +239 -64
  195. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  196. package/dist/lib/watchdog/watchdog.js +31 -16
  197. package/dist/lib/workflows.d.ts +16 -0
  198. package/dist/lib/workflows.js +110 -1
  199. package/package.json +1 -1
@@ -33,6 +33,19 @@ export declare function liveGlyphAndPreview(a: ActiveSession | undefined): {
33
33
  * into a scannable column instead of jamming against a truncated topic.
34
34
  */
35
35
  export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'>): string;
36
+ /**
37
+ * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
38
+ * joins active sessions on ticketId + project, but the raw ActiveSession nests
39
+ * the ticket (`ticket.id`) and carries no `project` at all — so a naive join
40
+ * silently drops every row. Emit both as flat, always-present top-level keys
41
+ * (null when unknown) alongside the raw fields, so every active row is joinable.
42
+ * `project` uses the same derivation SessionMeta does — basename(cwd) (see
43
+ * discover.ts) — so the active view and the history view join identically.
44
+ */
45
+ export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
46
+ ticketId: string | null;
47
+ project: string | null;
48
+ }>;
36
49
  /** Grouped + sorted view of active sessions for the --active renderer. */
37
50
  export interface ActiveSessionsLayout {
38
51
  workspaces: Array<{
@@ -204,8 +217,8 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
204
217
  * Resume a session in the current terminal — a foreground takeover of this
205
218
  * process. Used by the single-select picker and by `sessions resume` when the
206
219
  * chosen destination is "in place" (unknown emulator / off-macOS, single pick).
207
- * Falls back to the current version via `/continue` when the version-pinned
208
- * binary is missing (ENOENT).
220
+ * Falls back to the same resume invocation against the current version when the
221
+ * version-pinned launcher is genuinely missing.
209
222
  */
210
223
  export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
211
224
  /**
@@ -224,23 +237,33 @@ export declare function resumeSpawnInvocation(cmd: string[], platform?: NodeJS.P
224
237
  args: string[];
225
238
  shell: boolean;
226
239
  };
227
- /**
228
- * Build the shell command that resumes a picked session.
229
- *
230
- * When the session's originating version is known, uses the version-pinned
231
- * binary (e.g. `claude@2.1.138`) so the resume always runs in the same
232
- * isolated HOME where the JSONL was written — regardless of which version is
233
- * currently the default. Falls back to the bare shim when version is unknown.
234
- *
235
- * If the versioned binary is missing (version was removed), the ENOENT
236
- * handler in handlePickedSession retries via buildFallbackCommand.
237
- */
238
240
  export declare function buildResumeCommand(session: SessionMeta): string[] | null;
239
241
  interface AgentFilter {
240
242
  agent?: SessionAgentId;
241
243
  version?: string;
242
244
  }
243
245
  export declare function parseAgentFilter(agentName?: string): AgentFilter;
246
+ /**
247
+ * How a `sessions <query>` argument was resolved against the pool.
248
+ *
249
+ * `byId` records that the rows came from an id lookup, so only then does an
250
+ * ambiguous result mean "your id prefix is too short". `completeId` records that
251
+ * the query was a whole session id: it is unique by construction, so a miss is
252
+ * final and must NOT widen into a text/content search.
253
+ */
254
+ export interface SessionQueryResolution {
255
+ matches: SessionMeta[];
256
+ byId: boolean;
257
+ completeId: boolean;
258
+ }
259
+ /**
260
+ * The single entry point for turning a `sessions <query>` argument into rows.
261
+ *
262
+ * A complete session id resolves by id alone. Anything else keeps the existing
263
+ * ladder: id lookup first (so a short id still wins), then the ranked
264
+ * metadata+content search.
265
+ */
266
+ export declare function resolveSessionQuery(pool: SessionMeta[], query: string): SessionQueryResolution;
244
267
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
245
268
  export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
246
269
  /** Register the `agents sessions` command with all its options and help text. */
@@ -26,14 +26,16 @@ import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.
26
26
  import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
27
27
  import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
28
28
  import { inferSessionState } from '../lib/session/state.js';
29
- import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
29
+ import { discoverSessions, countSessionsInScope, resolveSessionById, isCompleteSessionId, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
30
+ import { findSessionsById } from '../lib/session/db.js';
30
31
  import { filterTeamSessions } from '../lib/session/team-filter.js';
31
32
  import { parseSession } from '../lib/session/parse.js';
32
33
  import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
33
34
  import { formatRelativeTime } from '../lib/session/relative-time.js';
34
35
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
35
36
  import { renderMarkdown } from '../lib/markdown.js';
36
- import { colorAgent, resolveAgentName } from '../lib/agents.js';
37
+ import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
38
+ import { getShimsDir } from '../lib/state.js';
37
39
  import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
38
40
  import { resolveVersionAliasLoose } from '../lib/versions.js';
39
41
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -44,9 +46,12 @@ import { registerSessionsSyncCommand } from './sessions-sync.js';
44
46
  import { registerSessionsResumeCommand } from './sessions-resume.js';
45
47
  import { registerGoCommand } from './go.js';
46
48
  import { registerFocusCommand } from './focus.js';
49
+ import { registerDetachCommand } from './detach.js';
50
+ import { registerAttachCommand } from './attach.js';
47
51
  import { registerSessionsInjectCommand } from './sessions-inject.js';
48
52
  import { registerSessionsExportCommand } from './sessions-export.js';
49
53
  import { registerSessionsImportCommand } from './sessions-import.js';
54
+ import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
50
55
  import { runBrowserSessions } from '../lib/browser/sessions-list.js';
51
56
  const SESSION_AGENT_FILTER_HELP = `Filter by agent, e.g. claude, codex, claude@2.0.65`;
52
57
  /**
@@ -176,6 +181,9 @@ function statusColor(status) {
176
181
  case 'idle': return chalk.gray;
177
182
  case 'queued': return chalk.blue;
178
183
  case 'input_required': return chalk.yellow;
184
+ // Alive but un-introspectable (a harness whose transcript we can't parse).
185
+ // Magenta so it never reads as the gray "idle" it used to be faked as.
186
+ case 'unknown': return chalk.magenta;
179
187
  }
180
188
  }
181
189
  function contextColor(context) {
@@ -274,7 +282,10 @@ export function liveGlyphAndPreview(a) {
274
282
  return { glyph: '', preview: '' };
275
283
  const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
276
284
  const running = a.status === 'running' || a.activity === 'working';
277
- const shape = waiting ? '◐' : running ? '●' : '○';
285
+ // `◌` (dotted) = alive but un-introspectable visually distinct from `○` idle
286
+ // so an opaque harness is never mistaken for a finished one.
287
+ const unknown = a.status === 'unknown';
288
+ const shape = waiting ? '◐' : running ? '●' : unknown ? '◌' : '○';
278
289
  return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
279
290
  }
280
291
  /**
@@ -285,6 +296,22 @@ export function liveGlyphAndPreview(a) {
285
296
  export function ticketLabel(s) {
286
297
  return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
287
298
  }
299
+ /**
300
+ * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
301
+ * joins active sessions on ticketId + project, but the raw ActiveSession nests
302
+ * the ticket (`ticket.id`) and carries no `project` at all — so a naive join
303
+ * silently drops every row. Emit both as flat, always-present top-level keys
304
+ * (null when unknown) alongside the raw fields, so every active row is joinable.
305
+ * `project` uses the same derivation SessionMeta does — basename(cwd) (see
306
+ * discover.ts) — so the active view and the history view join identically.
307
+ */
308
+ export function serializeActiveSessionsForJson(sessions) {
309
+ return sessions.map((s) => ({
310
+ ...s,
311
+ ticketId: s.ticket?.id ?? null,
312
+ project: s.cwd ? path.basename(s.cwd) : null,
313
+ }));
314
+ }
288
315
  /**
289
316
  * Compact, colour-coded badges for the durable/awaiting signals. Text-only (no
290
317
  * emoji, per repo convention): `plan` / `ask` / `perm` for why it's waiting,
@@ -573,15 +600,18 @@ async function runRemoteSessionsJson(hosts) {
573
600
  process.stdout.write(serializeSessionsJson(sessions));
574
601
  }
575
602
  /**
576
- * `running N · idle N · waiting N · queued N` for a bucket of sessions (zero
577
- * buckets omitted). Same bucketing as the grand-total summary so per-group
578
- * counts reconcile with the `(total)` beside the header. Empty when nothing.
603
+ * `running N · idle N · waiting N · queued N · unknown N` for a bucket of
604
+ * sessions (zero buckets omitted). Same bucketing as the grand-total summary so
605
+ * per-group counts reconcile with the `(total)` beside the header the `unknown`
606
+ * bucket is what keeps an alive-but-opaque row from silently vanishing from the
607
+ * tally. Empty when nothing.
579
608
  */
580
609
  function groupTally(sessions) {
581
610
  const running = sessions.filter(s => s.status === 'running').length;
582
611
  const idle = sessions.filter(s => s.status === 'idle').length;
583
612
  const waiting = sessions.filter(s => s.status === 'input_required').length;
584
613
  const queued = sessions.filter(s => s.status === 'queued').length;
614
+ const unknown = sessions.filter(s => s.status === 'unknown').length;
585
615
  const parts = [];
586
616
  if (running)
587
617
  parts.push(`${running} running`);
@@ -591,6 +621,8 @@ function groupTally(sessions) {
591
621
  parts.push(`${waiting} waiting`);
592
622
  if (queued)
593
623
  parts.push(`${queued} queued`);
624
+ if (unknown)
625
+ parts.push(`${unknown} unknown`);
594
626
  return parts.join(' · ');
595
627
  }
596
628
  /** Print one machine's workspace tree, indented under its machine header. */
@@ -747,7 +779,7 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
747
779
  // present so a supervising agent or hook can poll it as a gate.
748
780
  const sessions = waitingOnly ? merged.filter(s => s.status === 'input_required') : merged;
749
781
  if (asJson) {
750
- process.stdout.write(JSON.stringify(sessions, null, 2) + '\n');
782
+ process.stdout.write(JSON.stringify(serializeActiveSessionsForJson(sessions), null, 2) + '\n');
751
783
  if (waitingOnly && sessions.length > 0)
752
784
  process.exitCode = 1;
753
785
  return;
@@ -835,10 +867,15 @@ function canonicalSessionsCommand(query, options) {
835
867
  async function renderSessionPreview(query, scope) {
836
868
  const discovered = await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 });
837
869
  const pool = applyScopeFilters(discovered, scope);
838
- const matches = resolveSessionById(pool, query);
839
- const session = (matches.length > 0 ? matches : filterSessionsByQuery(pool, query))[0];
870
+ const { matches, completeId } = resolveSessionQuery(pool, query);
871
+ const session = matches[0];
840
872
  if (!session) {
841
- console.log(chalk.gray(`No session matches "${query}".`));
873
+ // A complete id that missed is not "no match for this text" — say which, and
874
+ // give the same fleet pointer the render paths give.
875
+ if (completeId)
876
+ notFoundByIdMessage(query).forEach(l => console.log(l));
877
+ else
878
+ console.log(chalk.gray(`No session matches "${query}".`));
842
879
  return;
843
880
  }
844
881
  console.log(buildPreview(session));
@@ -1043,11 +1080,14 @@ async function sessionsAction(query, options) {
1043
1080
  sortBy,
1044
1081
  origin: options.routine ? 'routine' : undefined,
1045
1082
  };
1083
+ let hiddenUnmanaged = 0;
1046
1084
  let sessions = await discoverSessions({
1047
1085
  ...scope,
1048
1086
  limit,
1049
1087
  excludeTeamOrigin: !options.teams,
1050
1088
  onProgress: tracker.onProgress,
1089
+ includeUnmanaged: options.unmanaged,
1090
+ onHiddenUnmanaged: (n) => { hiddenUnmanaged = n; },
1051
1091
  });
1052
1092
  tracker.stop();
1053
1093
  spinner?.stop();
@@ -1121,6 +1161,9 @@ async function sessionsAction(query, options) {
1121
1161
  if (hiddenCount > 0) {
1122
1162
  console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
1123
1163
  }
1164
+ if (hiddenUnmanaged > 0) {
1165
+ console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
1166
+ }
1124
1167
  return;
1125
1168
  }
1126
1169
  // The grouped project overview is the bare interactive default: a scannable
@@ -1130,7 +1173,7 @@ async function sessionsAction(query, options) {
1130
1173
  const liveIndex = await maybeLiveIndex(options);
1131
1174
  // Per-project row cap is fixed (--limit carries a default of 50 and drives
1132
1175
  // the fetch pool, not the display); `--all` expands every group instead.
1133
- printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all });
1176
+ printSessionOverview(sessions, hiddenCount, liveIndex, { perProjectCap: OVERVIEW_ROWS_PER_PROJECT, expand: !!options.all, hiddenUnmanaged });
1134
1177
  return;
1135
1178
  }
1136
1179
  // --tree / --flat are printed listings, not an interactive pick — render them
@@ -1150,6 +1193,10 @@ async function sessionsAction(query, options) {
1150
1193
  const filtered = searchQuery ? filterSessionsByQuery(sessions, searchQuery) : sessions;
1151
1194
  const liveIndex = await maybeLiveIndex(options);
1152
1195
  printSessionTable(filtered, hiddenCount, options.tree === true, liveIndex);
1196
+ // Every listing path must say what it dropped — a hidden default that stays
1197
+ // silent in one render mode is the failure this footer exists to prevent.
1198
+ if (hiddenUnmanaged > 0)
1199
+ console.log(chalk.gray(formatUnmanagedHiddenFooter(hiddenUnmanaged)));
1153
1200
  }
1154
1201
  catch (err) {
1155
1202
  tracker.stop();
@@ -1158,8 +1205,14 @@ async function sessionsAction(query, options) {
1158
1205
  process.exit(1);
1159
1206
  }
1160
1207
  }
1208
+ /** Whether a query should route to the single-session render rather than the
1209
+ * listing. The hex-ish test catches a bare id prefix; `isCompleteSessionId`
1210
+ * additionally catches the prefixed whole ids (`session_…`, `ses_…`) that the
1211
+ * hex test rejects — without it those never reach the id-only resolution and
1212
+ * still content-search. */
1161
1213
  function looksLikeSessionId(query) {
1162
- return /^[0-9a-f-]{6,}$/i.test(query.trim());
1214
+ const trimmed = query.trim();
1215
+ return /^[0-9a-f-]{6,}$/i.test(trimmed) || isCompleteSessionId(trimmed);
1163
1216
  }
1164
1217
  function teamTag(session) {
1165
1218
  const origin = session.teamOrigin;
@@ -1337,6 +1390,8 @@ function printSessionOverview(pool, hiddenCount, liveIndex, opts) {
1337
1390
  console.log(parts.join(chalk.gray(' · ')));
1338
1391
  if (hiddenCount > 0)
1339
1392
  console.log(chalk.gray(formatTeamHiddenFooter(hiddenCount)));
1393
+ if (opts.hiddenUnmanaged)
1394
+ console.log(chalk.gray(formatUnmanagedHiddenFooter(opts.hiddenUnmanaged)));
1340
1395
  }
1341
1396
  function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
1342
1397
  if (tree) {
@@ -1720,8 +1775,8 @@ export async function handlePickedSession(picked) {
1720
1775
  * Resume a session in the current terminal — a foreground takeover of this
1721
1776
  * process. Used by the single-select picker and by `sessions resume` when the
1722
1777
  * chosen destination is "in place" (unknown emulator / off-macOS, single pick).
1723
- * Falls back to the current version via `/continue` when the version-pinned
1724
- * binary is missing (ENOENT).
1778
+ * Falls back to the same resume invocation against the current version when the
1779
+ * version-pinned launcher is genuinely missing.
1725
1780
  */
1726
1781
  export async function resumeSessionInPlace(session) {
1727
1782
  const cwd = session.cwd && fs.existsSync(session.cwd)
@@ -1738,11 +1793,17 @@ export async function resumeSessionInPlace(session) {
1738
1793
  // agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
1739
1794
  // spawnResumeCommand), a missing command exits non-zero rather than emitting
1740
1795
  // an ENOENT `error` event — so detect a removed version here instead of
1741
- // relying on that event, keeping the /continue fallback working on every OS.
1742
- if (!findExecutable(resume[0]) && session.version) {
1796
+ // relying on that event, keeping the fallback working on every OS.
1797
+ // `resume[0]` is an absolute alias path when one exists on disk, so only a bare
1798
+ // name still needs a PATH lookup. Checking existsSync first is what keeps an
1799
+ // isolated install (shims deliberately off PATH) out of the fallback.
1800
+ const launcherFound = path.isAbsolute(resume[0])
1801
+ ? fs.existsSync(resume[0])
1802
+ : !!findExecutable(resume[0]);
1803
+ if (!launcherFound && session.version) {
1743
1804
  const fallback = buildFallbackCommand(session);
1744
1805
  if (fallback) {
1745
- console.log(chalk.gray(`Version ${session.version} is not installed. Falling back to current version via /continue...`));
1806
+ console.log(chalk.gray(`Version ${session.version} is not installed. Resuming with the current version instead...`));
1746
1807
  await spawnResumeCommand(fallback, cwd);
1747
1808
  return;
1748
1809
  }
@@ -1819,18 +1880,55 @@ function spawnResumeCommand(cmd, cwd) {
1819
1880
  * If the versioned binary is missing (version was removed), the ENOENT
1820
1881
  * handler in handlePickedSession retries via buildFallbackCommand.
1821
1882
  */
1883
+ /**
1884
+ * The agent's own resume invocation, given whichever launcher we resolved.
1885
+ * Keeping the verb in one place is what lets the version-pinned and fallback
1886
+ * paths stay in agreement — they previously drifted into `/continue`, which is
1887
+ * not a command either CLI has.
1888
+ */
1889
+ function resumeArgv(agent, id, launcher) {
1890
+ switch (agent) {
1891
+ case 'claude': return [launcher, '--resume', id];
1892
+ case 'codex': return [launcher, 'resume', id];
1893
+ case 'opencode': return [launcher, '--session', id];
1894
+ default: return null;
1895
+ }
1896
+ }
1897
+ /**
1898
+ * Absolute path of the on-disk versioned alias, or null when it isn't there.
1899
+ *
1900
+ * Resume must not resolve `<cli>@<version>` by bare name. The shims directory is
1901
+ * deliberately absent from PATH for an isolated install — that is precisely what
1902
+ * `--isolated` promises — so a PATH lookup can never find the alias, and resume
1903
+ * degraded to the fallback 100% of the time for isolated copies. Mirrors the
1904
+ * resolution `buildExecCommand` already does for `agents run`.
1905
+ */
1906
+ function versionedAliasIfPresent(agent, version) {
1907
+ const cli = AGENTS[agent]?.cliCommand ?? agent;
1908
+ const base = path.join(getShimsDir(), `${cli}@${version}`);
1909
+ if (process.platform === 'win32' && fs.existsSync(`${base}.cmd`))
1910
+ return `${base}.cmd`;
1911
+ if (fs.existsSync(base))
1912
+ return base;
1913
+ return null;
1914
+ }
1822
1915
  export function buildResumeCommand(session) {
1823
1916
  switch (session.agent) {
1824
- case 'claude':
1825
- if (session.version)
1826
- return [`claude@${session.version}`, '--resume', session.id];
1827
- return ['claude', '--resume', session.id];
1828
- case 'codex':
1829
- if (session.version)
1830
- return [`codex@${session.version}`, 'resume', session.id];
1831
- return ['codex', 'resume', session.id];
1917
+ // opencode sessions are shared across versions, so resume is deliberately NOT
1918
+ // version-pinned — it always goes through the plain launcher.
1832
1919
  case 'opencode':
1833
- return ['opencode', '--session', session.id];
1920
+ return resumeArgv('opencode', session.id, 'opencode');
1921
+ case 'claude':
1922
+ case 'codex': {
1923
+ const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
1924
+ if (session.version) {
1925
+ const alias = versionedAliasIfPresent(session.agent, session.version);
1926
+ // Absolute path when the alias exists; otherwise the bare versioned name,
1927
+ // which still resolves for a non-isolated install whose shims are on PATH.
1928
+ return resumeArgv(session.agent, session.id, alias ?? `${cli}@${session.version}`);
1929
+ }
1930
+ return resumeArgv(session.agent, session.id, cli);
1931
+ }
1834
1932
  case 'gemini':
1835
1933
  case 'antigravity':
1836
1934
  case 'openclaw':
@@ -1843,13 +1941,18 @@ export function buildResumeCommand(session) {
1843
1941
  return null;
1844
1942
  }
1845
1943
  }
1846
- /** Fallback resume command when the versioned binary is unavailable (ENOENT). */
1944
+ /**
1945
+ * Fallback when the pinned version really is gone: the same resume invocation
1946
+ * against the current version.
1947
+ *
1948
+ * This used to spawn `<cli> "/continue <id>"`, feeding a slash command into the
1949
+ * TUI as a prompt. Neither CLI has `/continue` — codex documents `/resume` — so
1950
+ * the agent received an unrecognised command and the session was not resumed at
1951
+ * all. Reusing resumeArgv keeps the two paths from drifting apart again.
1952
+ */
1847
1953
  function buildFallbackCommand(session) {
1848
- switch (session.agent) {
1849
- case 'claude': return ['claude', `/continue ${session.id}`];
1850
- case 'codex': return ['codex', `/continue ${session.id}`];
1851
- default: return null;
1852
- }
1954
+ const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
1955
+ return resumeArgv(session.agent, session.id, cli);
1853
1956
  }
1854
1957
  // ---------------------------------------------------------------------------
1855
1958
  // Cloud session source (--cloud)
@@ -1956,6 +2059,51 @@ function formatSearchMessage(options) {
1956
2059
  return 'Search sessions:';
1957
2060
  return `Search sessions (${filters.join(', ')}):`;
1958
2061
  }
2062
+ /**
2063
+ * The single entry point for turning a `sessions <query>` argument into rows.
2064
+ *
2065
+ * A complete session id resolves by id alone. Anything else keeps the existing
2066
+ * ladder: id lookup first (so a short id still wins), then the ranked
2067
+ * metadata+content search.
2068
+ */
2069
+ export function resolveSessionQuery(pool, query) {
2070
+ // Normalize ONCE here. isCompleteSessionId trims but resolveSessionById does
2071
+ // not, so a padded id ("<uuid> ", e.g. pasted from a terminal) would classify
2072
+ // as complete and then miss the id lookup — reporting a session that IS on
2073
+ // this machine as absent.
2074
+ const normalized = query.trim();
2075
+ const completeId = isCompleteSessionId(normalized);
2076
+ const byIdMatches = resolveSessionById(pool, normalized);
2077
+ if (byIdMatches.length > 0)
2078
+ return { matches: byIdMatches, byId: true, completeId };
2079
+ if (completeId) {
2080
+ // The pool is only what discoverSessions walked — a minority of the index
2081
+ // (measured: 2,798 of 7,614 rows), because it re-reads live agent homes and
2082
+ // skips whole classes of indexed session. Declaring absence from the pool
2083
+ // alone denied 1,315 sessions whose transcript is on this disk right now.
2084
+ // Ask the index itself, the same authoritative lookup `fork` and `exec` use.
2085
+ return { matches: findSessionsById(normalized), byId: true, completeId };
2086
+ }
2087
+ return { matches: filterSessionsByQuery(pool, normalized), byId: false, completeId };
2088
+ }
2089
+ /** Explain an ambiguous resolution. Only a short id can be lengthened: a complete
2090
+ * id is already maximal, and a search phrase was never an id to begin with. */
2091
+ function ambiguityHint(byId, completeId) {
2092
+ if (completeId)
2093
+ return 'That is already a complete id — these rows share it as a prefix.';
2094
+ return byId
2095
+ ? 'Pass a longer ID to narrow it down.'
2096
+ : 'That matched on text, not an id. Pass a session id, or narrow the search.';
2097
+ }
2098
+ /** Explain a complete-id miss, which no local rephrasing can fix. Echoes the
2099
+ * normalized id so a pasted, padded argument doesn't produce an unrunnable hint. */
2100
+ function notFoundByIdMessage(query) {
2101
+ const id = query.trim();
2102
+ return [
2103
+ chalk.red(`No session with id ${id} on this machine.`),
2104
+ chalk.gray(`Search the fleet with: agents sessions ${id} --device <host>`),
2105
+ ];
2106
+ }
1959
2107
  /** Filter and rank sessions by a multi-term search query across metadata and content. */
1960
2108
  export function filterSessionsByQuery(sessions, query) {
1961
2109
  const trimmed = query?.trim().toLowerCase() || '';
@@ -2086,11 +2234,13 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
2086
2234
  });
2087
2235
  tracker.stop();
2088
2236
  const allSessions = applyScopeFilters(discovered, scope);
2089
- const matches = resolveSessionById(allSessions, query);
2090
- const queryMatches = matches.length > 0 ? matches : filterSessionsByQuery(allSessions, query);
2237
+ const { matches: queryMatches, byId, completeId } = resolveSessionQuery(allSessions, query);
2091
2238
  if (queryMatches.length === 0) {
2092
2239
  spinner.stop();
2093
- console.error(chalk.red(`No session found matching: ${query}`));
2240
+ if (completeId)
2241
+ notFoundByIdMessage(query).forEach(l => console.error(l));
2242
+ else
2243
+ console.error(chalk.red(`No session found matching: ${query}`));
2094
2244
  process.exit(1);
2095
2245
  }
2096
2246
  if (queryMatches.length > 1) {
@@ -2099,7 +2249,7 @@ async function renderArtifactsGlobal(query, listAll, name, scope) {
2099
2249
  for (const m of queryMatches.slice(0, 10)) {
2100
2250
  console.error(chalk.cyan(` ${m.shortId} ${m.id} ${m.label ?? m.topic ?? ''}`));
2101
2251
  }
2102
- console.error(chalk.gray('Pass a longer ID to narrow it down.'));
2252
+ console.error(chalk.gray(ambiguityHint(byId, completeId)));
2103
2253
  process.exit(1);
2104
2254
  }
2105
2255
  spinner.stop();
@@ -2127,13 +2277,20 @@ async function renderOneSession(query, mode, scope) {
2127
2277
  tracker.stop();
2128
2278
  const allSessions = applyScopeFilters(discovered, scope);
2129
2279
  let session;
2130
- const matches = resolveSessionById(allSessions, query);
2131
- let queryMatches = matches.length > 0 ? matches : filterSessionsByQuery(allSessions, query);
2132
- if (queryMatches.length === 0) {
2280
+ const resolution = resolveSessionQuery(allSessions, query);
2281
+ let queryMatches = resolution.matches;
2282
+ let byId = resolution.byId;
2283
+ const completeId = resolution.completeId;
2284
+ // Widen to the transcript content index only for a genuine search phrase. A
2285
+ // complete id is unique, so widening could only ever surface a DIFFERENT
2286
+ // session that happens to mention the id — which is what made `sessions
2287
+ // <uuid>` render an unrelated transcript.
2288
+ if (queryMatches.length === 0 && !completeId) {
2133
2289
  const contentResults = searchContentIndex(allSessions, query);
2134
2290
  if (contentResults.size > 0) {
2135
2291
  const matchedSessions = Array.from(contentResults.values())
2136
2292
  .sort((a, b) => (b._bm25Score ?? 0) - (a._bm25Score ?? 0));
2293
+ byId = false;
2137
2294
  if (matchedSessions.length === 1) {
2138
2295
  session = matchedSessions[0];
2139
2296
  }
@@ -2155,6 +2312,10 @@ async function renderOneSession(query, mode, scope) {
2155
2312
  process.exit(1);
2156
2313
  }
2157
2314
  }
2315
+ else if (completeId) {
2316
+ notFoundByIdMessage(query).forEach(l => console.error(l));
2317
+ process.exit(1);
2318
+ }
2158
2319
  else {
2159
2320
  console.error(chalk.red(`No session found matching: ${query}`));
2160
2321
  console.error(chalk.gray('Run "agents sessions" to browse sessions.'));
@@ -2168,7 +2329,7 @@ async function renderOneSession(query, mode, scope) {
2168
2329
  for (const match of queryMatches.slice(0, 10)) {
2169
2330
  console.error(chalk.cyan(` ${match.shortId} ${match.id} ${match.label ?? match.topic ?? ''}`));
2170
2331
  }
2171
- console.error(chalk.gray('Pass a longer ID to narrow it down.'));
2332
+ console.error(chalk.gray(ambiguityHint(byId, completeId)));
2172
2333
  process.exit(1);
2173
2334
  }
2174
2335
  else {
@@ -2205,6 +2366,7 @@ export function registerSessionsCommands(program) {
2205
2366
  .option('--grok', 'Shorthand for --agent grok')
2206
2367
  .option('--opencode', 'Shorthand for --agent opencode')
2207
2368
  .option('--all', 'Include sessions from every directory (not just current project)')
2369
+ .option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
2208
2370
  .option('--teams', 'Include team-spawned sessions (hidden by default)')
2209
2371
  .option('--routine', 'Show only sessions archived from routine runs')
2210
2372
  .option('-p, --project <name>', 'Filter by project name (searches across all directories)')
@@ -2293,9 +2455,13 @@ export function registerSessionsCommands(program) {
2293
2455
  registerSessionsResumeCommand(sessionsCmd);
2294
2456
  registerGoCommand(sessionsCmd);
2295
2457
  registerFocusCommand(sessionsCmd);
2458
+ registerDetachCommand(sessionsCmd);
2459
+ registerAttachCommand(sessionsCmd);
2296
2460
  registerSessionsInjectCommand(sessionsCmd);
2297
2461
  registerSessionsExportCommand(sessionsCmd);
2298
2462
  registerSessionsImportCommand(sessionsCmd);
2463
+ registerSessionsMigrateCommand(sessionsCmd);
2464
+ registerSessionsMigrationsCommand(sessionsCmd);
2299
2465
  }
2300
2466
  function formatNoSessionsMessage(showAll, project) {
2301
2467
  const projectQuery = project?.trim();
@@ -2307,6 +2473,10 @@ function formatNoSessionsMessage(showAll, project) {
2307
2473
  const command = 'agents sessions --all';
2308
2474
  return `No sessions found for ${process.cwd()}. Run "${command}" to see sessions from every directory.`;
2309
2475
  }
2476
+ function formatUnmanagedHiddenFooter(hiddenCount) {
2477
+ const noun = hiddenCount === 1 ? 'session' : 'sessions';
2478
+ return `(${hiddenCount} ${noun} from your own unmanaged installs hidden — use --unmanaged to show)`;
2479
+ }
2310
2480
  function formatTeamHiddenFooter(hiddenCount) {
2311
2481
  const noun = hiddenCount === 1 ? 'team session' : 'team sessions';
2312
2482
  return `(${hiddenCount} ${noun} hidden — use --teams to show, or \`agents teams status\`)`;
@@ -37,7 +37,7 @@ export async function runComputerWizard() {
37
37
  return false;
38
38
  }
39
39
  // 1. Download + verify + install the signed, notarized helper.
40
- console.log(chalk.bold('Installing the Computer Helper...'));
40
+ console.log(chalk.bold('Installing the Agents Computer helper...'));
41
41
  try {
42
42
  await installComputerHelperMacLocal();
43
43
  }
@@ -57,7 +57,7 @@ export async function runComputerWizard() {
57
57
  }
58
58
  // 3. Guide the two permission grants if not already trusted.
59
59
  if (!trusted) {
60
- console.log(chalk.bold('\nGrant two permissions to "Computer Helper" (one-time):'));
60
+ console.log(chalk.bold('\nGrant two permissions to "Agents Computer" (one-time):'));
61
61
  console.log(' 1. ' + chalk.cyan('Accessibility') + chalk.dim(' — lets it click/type'));
62
62
  console.log(' 2. ' + chalk.cyan('Screen Recording') + chalk.dim(' — lets it screenshot windows'));
63
63
  console.log(chalk.dim('\nOpening System Settings > Privacy & Security ...'));