@phnx-labs/agents-cli 1.20.78 → 1.20.82

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 (176) hide show
  1. package/CHANGELOG.md +487 -0
  2. package/README.md +25 -25
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +9 -0
  5. package/dist/commands/activity.js +153 -24
  6. package/dist/commands/apply.js +11 -4
  7. package/dist/commands/cli.js +1 -1
  8. package/dist/commands/commands.d.ts +1 -1
  9. package/dist/commands/commands.js +7 -6
  10. package/dist/commands/defaults.js +3 -11
  11. package/dist/commands/doctor.d.ts +65 -0
  12. package/dist/commands/doctor.js +575 -31
  13. package/dist/commands/events.js +1 -1
  14. package/dist/commands/exec.js +24 -5
  15. package/dist/commands/feed.d.ts +4 -0
  16. package/dist/commands/feed.js +87 -11
  17. package/dist/commands/harness.js +2 -2
  18. package/dist/commands/hooks.js +1 -1
  19. package/dist/commands/import.js +8 -4
  20. package/dist/commands/logs.js +6 -0
  21. package/dist/commands/models.js +1 -1
  22. package/dist/commands/packages.js +6 -6
  23. package/dist/commands/permissions.js +1 -1
  24. package/dist/commands/profiles.js +1 -1
  25. package/dist/commands/pull.d.ts +1 -1
  26. package/dist/commands/pull.js +4 -4
  27. package/dist/commands/repo.d.ts +78 -0
  28. package/dist/commands/repo.js +187 -11
  29. package/dist/commands/routines.js +12 -2
  30. package/dist/commands/rules.js +13 -12
  31. package/dist/commands/secrets.d.ts +3 -1
  32. package/dist/commands/secrets.js +100 -32
  33. package/dist/commands/sessions-inject.d.ts +7 -4
  34. package/dist/commands/sessions-inject.js +18 -18
  35. package/dist/commands/sessions-picker.js +32 -3
  36. package/dist/commands/sessions-resume.d.ts +16 -0
  37. package/dist/commands/sessions-resume.js +14 -13
  38. package/dist/commands/sessions.d.ts +31 -0
  39. package/dist/commands/sessions.js +141 -16
  40. package/dist/commands/set.d.ts +15 -0
  41. package/dist/commands/set.js +79 -0
  42. package/dist/commands/setup-fleet.d.ts +21 -0
  43. package/dist/commands/setup-fleet.js +201 -0
  44. package/dist/commands/setup-secrets.d.ts +20 -0
  45. package/dist/commands/setup-secrets.js +223 -0
  46. package/dist/commands/setup.js +17 -3
  47. package/dist/commands/skills.js +1 -1
  48. package/dist/commands/sync.js +7 -2
  49. package/dist/commands/teams-picker.js +0 -1
  50. package/dist/commands/teams.d.ts +33 -1
  51. package/dist/commands/teams.js +208 -44
  52. package/dist/commands/versions.js +10 -5
  53. package/dist/commands/view.d.ts +1 -0
  54. package/dist/commands/view.js +11 -0
  55. package/dist/index.js +5 -8
  56. package/dist/lib/acp/harnesses.js +0 -7
  57. package/dist/lib/activity.d.ts +156 -0
  58. package/dist/lib/activity.js +282 -0
  59. package/dist/lib/agents.d.ts +9 -2
  60. package/dist/lib/agents.js +72 -63
  61. package/dist/lib/browser/service.js +3 -0
  62. package/dist/lib/browser/types.d.ts +7 -0
  63. package/dist/lib/capabilities.js +6 -2
  64. package/dist/lib/crabbox/lease.js +2 -2
  65. package/dist/lib/crabbox/setup-copy.d.ts +4 -4
  66. package/dist/lib/crabbox/setup-copy.js +4 -4
  67. package/dist/lib/daemon.d.ts +18 -0
  68. package/dist/lib/daemon.js +47 -6
  69. package/dist/lib/devices/fleet.d.ts +1 -1
  70. package/dist/lib/devices/fleet.js +1 -1
  71. package/dist/lib/doctor-diff.d.ts +20 -0
  72. package/dist/lib/doctor-diff.js +6 -1
  73. package/dist/lib/drift.d.ts +31 -11
  74. package/dist/lib/drift.js +58 -7
  75. package/dist/lib/events.d.ts +8 -1
  76. package/dist/lib/events.js +10 -1
  77. package/dist/lib/exec.js +35 -8
  78. package/dist/lib/feed-post.d.ts +28 -1
  79. package/dist/lib/feed-post.js +110 -2
  80. package/dist/lib/fleet/apply.js +8 -1
  81. package/dist/lib/fleet/auth-sync.d.ts +18 -1
  82. package/dist/lib/fleet/auth-sync.js +25 -11
  83. package/dist/lib/fleet/remote-login.js +5 -0
  84. package/dist/lib/git.d.ts +14 -14
  85. package/dist/lib/git.js +41 -44
  86. package/dist/lib/hooks.d.ts +42 -0
  87. package/dist/lib/hooks.js +137 -93
  88. package/dist/lib/hosts/dispatch.d.ts +28 -1
  89. package/dist/lib/hosts/dispatch.js +33 -5
  90. package/dist/lib/hosts/option.js +2 -2
  91. package/dist/lib/hosts/passthrough.d.ts +22 -1
  92. package/dist/lib/hosts/passthrough.js +238 -13
  93. package/dist/lib/hosts/run-target.d.ts +2 -0
  94. package/dist/lib/hosts/run-target.js +1 -0
  95. package/dist/lib/hq/floor.d.ts +1 -1
  96. package/dist/lib/hq/floor.js +6 -1
  97. package/dist/lib/mcp.js +3 -71
  98. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  99. package/dist/lib/menubar/notify-desktop.d.ts +15 -0
  100. package/dist/lib/menubar/notify-desktop.js +41 -5
  101. package/dist/lib/merged-resources.d.ts +11 -0
  102. package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
  103. package/dist/lib/permissions.d.ts +0 -32
  104. package/dist/lib/permissions.js +4 -157
  105. package/dist/lib/picker.js +1 -1
  106. package/dist/lib/platform/process.d.ts +16 -5
  107. package/dist/lib/platform/process.js +54 -0
  108. package/dist/lib/plugins.d.ts +0 -8
  109. package/dist/lib/plugins.js +4 -110
  110. package/dist/lib/project-resources.js +4 -1
  111. package/dist/lib/refresh.d.ts +5 -3
  112. package/dist/lib/refresh.js +7 -5
  113. package/dist/lib/resources/commands.js +4 -1
  114. package/dist/lib/resources/mcp.js +0 -4
  115. package/dist/lib/resources/permissions.d.ts +1 -1
  116. package/dist/lib/resources/permissions.js +1 -5
  117. package/dist/lib/resources/rules.js +4 -1
  118. package/dist/lib/resources/skills.js +4 -1
  119. package/dist/lib/resources/subagents.js +4 -0
  120. package/dist/lib/resources/types.d.ts +1 -1
  121. package/dist/lib/routines.d.ts +20 -0
  122. package/dist/lib/routines.js +6 -0
  123. package/dist/lib/run-defaults.d.ts +1 -0
  124. package/dist/lib/run-defaults.js +9 -0
  125. package/dist/lib/runner.d.ts +0 -12
  126. package/dist/lib/runner.js +49 -10
  127. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  128. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  129. package/dist/lib/secrets/agent.d.ts +16 -0
  130. package/dist/lib/secrets/agent.js +105 -3
  131. package/dist/lib/secrets/audit.d.ts +46 -0
  132. package/dist/lib/secrets/audit.js +56 -0
  133. package/dist/lib/secrets/bundles.d.ts +11 -8
  134. package/dist/lib/secrets/bundles.js +31 -23
  135. package/dist/lib/secrets/index.d.ts +2 -1
  136. package/dist/lib/secrets/index.js +8 -2
  137. package/dist/lib/secrets/remote.d.ts +75 -2
  138. package/dist/lib/secrets/remote.js +139 -5
  139. package/dist/lib/secrets/sync.js +3 -3
  140. package/dist/lib/session/active.d.ts +110 -26
  141. package/dist/lib/session/active.js +161 -50
  142. package/dist/lib/session/actor-sidecar.d.ts +23 -0
  143. package/dist/lib/session/actor-sidecar.js +101 -0
  144. package/dist/lib/session/db.d.ts +4 -1
  145. package/dist/lib/session/db.js +65 -8
  146. package/dist/lib/session/discover.d.ts +4 -0
  147. package/dist/lib/session/discover.js +13 -0
  148. package/dist/lib/session/hook-sessions.d.ts +9 -0
  149. package/dist/lib/session/hook-sessions.js +57 -8
  150. package/dist/lib/session/pid-registry.d.ts +13 -0
  151. package/dist/lib/session/render.d.ts +2 -0
  152. package/dist/lib/session/render.js +1 -1
  153. package/dist/lib/session/types.d.ts +16 -0
  154. package/dist/lib/staleness/detectors/commands.js +2 -2
  155. package/dist/lib/staleness/detectors/permissions.js +1 -45
  156. package/dist/lib/staleness/writers/commands.js +4 -5
  157. package/dist/lib/staleness/writers/hooks.js +1 -1
  158. package/dist/lib/startup/command-registry.d.ts +1 -2
  159. package/dist/lib/startup/command-registry.js +2 -4
  160. package/dist/lib/subagents-registry.js +5 -12
  161. package/dist/lib/subagents.d.ts +0 -10
  162. package/dist/lib/subagents.js +0 -12
  163. package/dist/lib/teams/agents.d.ts +0 -2
  164. package/dist/lib/teams/agents.js +10 -27
  165. package/dist/lib/teams/index.d.ts +1 -1
  166. package/dist/lib/teams/index.js +1 -1
  167. package/dist/lib/types.d.ts +17 -9
  168. package/dist/lib/usage.d.ts +28 -5
  169. package/dist/lib/usage.js +271 -8
  170. package/dist/lib/versions.js +7 -1
  171. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  172. package/dist/lib/watchdog/watchdog.js +25 -13
  173. package/package.json +1 -2
  174. package/dist/commands/check.d.ts +0 -15
  175. package/dist/commands/check.js +0 -180
  176. package/dist/commands/resources.d.ts +0 -5
@@ -40,12 +40,23 @@ export declare function liveGlyphAndPreview(a: ActiveSession | undefined): {
40
40
  glyph: string;
41
41
  preview: string;
42
42
  };
43
+ /**
44
+ * The one-word live status for a listing row — `working` / `waiting` / `idle`,
45
+ * the same three states the `--active` column shows, so the default list is no
46
+ * longer just a glyph. `waiting` is the actionable "needs you" case (a question /
47
+ * permission / plan-review), kept distinct from `idle` (stopped) and `working`.
48
+ * Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
49
+ * exported for the row tests.
50
+ */
51
+ export declare function liveStatusWord(a: ActiveSession | undefined): string;
43
52
  /**
44
53
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
45
54
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
46
55
  * into a scannable column instead of jamming against a truncated topic.
47
56
  */
48
57
  export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'>): string;
58
+ export declare function linkTicketCell(s: Pick<SessionMeta, 'ticketId' | 'prNumber' | 'prUrl'>, label: string): string;
59
+ export declare function linkCwdCell(s: Pick<SessionMeta, 'cwd' | '_remote'>, label: string): string;
49
60
  /**
50
61
  * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
51
62
  * joins active sessions on ticketId + project, but the raw ActiveSession nests
@@ -58,7 +69,16 @@ export declare function ticketLabel(s: Pick<SessionMeta, 'ticketId' | 'prNumber'
58
69
  export declare function serializeActiveSessionsForJson(sessions: ActiveSession[]): Array<ActiveSession & {
59
70
  ticketId: string | null;
60
71
  project: string | null;
72
+ prLink: string | null;
61
73
  }>;
74
+ /**
75
+ * Compact owner display for the `--active` owner column: the local-part of a
76
+ * resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
77
+ * when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
78
+ * or absent (a launch predating actor stamping). Honest by design — an
79
+ * unresolved local run shows no owner rather than inventing one.
80
+ */
81
+ export declare function ownerLabel(s: ActiveSession): string;
62
82
  /** Grouped + sorted view of active sessions for the --active renderer. */
63
83
  export interface ActiveSessionsLayout {
64
84
  workspaces: Array<{
@@ -170,6 +190,13 @@ export declare function gatherActiveSessions(opts?: {
170
190
  sessions: ActiveSession[];
171
191
  remoteDeviceCount: number;
172
192
  }>;
193
+ /** One flat table row:
194
+ * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
195
+ * `doing` is the live preview when running, else the topic. The `ticket` column
196
+ * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
197
+ * when `showTicket` — otherwise a listing with no refs would waste a column of
198
+ * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
199
+ export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns): string;
173
200
  /**
174
201
  * Group key for the overview: prefer the indexed project name; else fold the cwd
175
202
  * to its repo — a worktree (`.../<repo>/.agents/worktrees/<slug>`) folds to the
@@ -221,6 +248,10 @@ export interface PickerColumns {
221
248
  /** Total width of the machine column, sized to the widest compacted hostname
222
249
  * in the pool (capped). Falls back to PICKER_MACHINE_W when absent. */
223
250
  machineWidth?: number;
251
+ /** Render the model column only when at least one row carries a model. */
252
+ showModel?: boolean;
253
+ /** Pool-sized model column width, including one trailing separator cell. */
254
+ modelWidth?: number;
224
255
  /** Render the ticket/PR column (only when at least one row carries a ref). */
225
256
  showTicket?: boolean;
226
257
  /**
@@ -32,7 +32,7 @@ import { filterTeamSessions } from '../lib/session/team-filter.js';
32
32
  import { parseSession } from '../lib/session/parse.js';
33
33
  import { runRemoteSessions, buildForwardedArgs, ensureWholeIndex } from '../lib/session/remote.js';
34
34
  import { formatRelativeTime } from '../lib/session/relative-time.js';
35
- import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkUrl } from '../lib/session/render.js';
35
+ import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList, linkPath, linkUrl, shortenModel } from '../lib/session/render.js';
36
36
  import { linearIssueUrl } from '../lib/session/linear.js';
37
37
  import { renderMarkdown } from '../lib/markdown.js';
38
38
  import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
@@ -182,6 +182,11 @@ function statusColor(status) {
182
182
  case 'idle': return chalk.gray;
183
183
  case 'queued': return chalk.blue;
184
184
  case 'input_required': return chalk.yellow;
185
+ // Dead process: dimmed so it recedes from the live rows without being mistaken
186
+ // for the gray `idle` (which reads as "done, waiting for you" — a live state).
187
+ case 'closed': return chalk.dim;
188
+ // Days-stale / dangling: red so a session nobody is driving stands out.
189
+ case 'abandoned': return chalk.red;
185
190
  // Alive but un-introspectable (a harness whose transcript we can't parse).
186
191
  // Magenta so it never reads as the gray "idle" it used to be faked as.
187
192
  case 'unknown': return chalk.magenta;
@@ -303,6 +308,10 @@ export function formatActiveRowDescription(s) {
303
308
  }
304
309
  /** Short human word for a session's activity (falls back to the coarse status). */
305
310
  function activityLabel(s) {
311
+ // Lifecycle status wins over any residual parsed activity — a dead/dangling
312
+ // session must read `closed`/`abandoned`, not the `idle` its stale tail infers.
313
+ if (s.status === 'closed' || s.status === 'abandoned')
314
+ return s.status;
306
315
  if (s.activity === 'waiting_input')
307
316
  return 'waiting';
308
317
  if (s.activity === 'working')
@@ -335,14 +344,54 @@ export function indexActiveBySessionId(active) {
335
344
  export function liveGlyphAndPreview(a) {
336
345
  if (!a)
337
346
  return { glyph: '', preview: '' };
347
+ // `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
348
+ // so an opaque harness is never mistaken for a finished one. `⊘` = abandoned /
349
+ // dangling; `×` = closed (dead pid) — both distinct from the `○` idle a live,
350
+ // stopped session shows, so a gone session never masquerades as a resting one.
351
+ if (a.status === 'abandoned')
352
+ return { glyph: statusColor(a.status)('⊘'), preview: buildSessionDescription(a) };
353
+ if (a.status === 'closed')
354
+ return { glyph: statusColor(a.status)('×'), preview: buildSessionDescription(a) };
338
355
  const waiting = a.status === 'input_required' || a.activity === 'waiting_input';
339
356
  const running = a.status === 'running' || a.activity === 'working';
340
- // `◌` (dotted) = alive but un-introspectable — visually distinct from `○` idle
341
- // so an opaque harness is never mistaken for a finished one.
342
357
  const unknown = a.status === 'unknown';
343
- const shape = waiting ? '◐' : running ? '●' : unknown ? '◌' : '○';
358
+ const shape = waiting ? '◐'
359
+ : running ? '●'
360
+ : unknown ? '◌'
361
+ : '○';
344
362
  return { glyph: statusColor(a.status)(shape), preview: buildSessionDescription(a) };
345
363
  }
364
+ /**
365
+ * The one-word live status for a listing row — `working` / `waiting` / `idle`,
366
+ * the same three states the `--active` column shows, so the default list is no
367
+ * longer just a glyph. `waiting` is the actionable "needs you" case (a question /
368
+ * permission / plan-review), kept distinct from `idle` (stopped) and `working`.
369
+ * Empty for a not-live row, and for the rare no-signal `unknown`. Pure +
370
+ * exported for the row tests.
371
+ */
372
+ export function liveStatusWord(a) {
373
+ if (!a)
374
+ return '';
375
+ // Lifecycle status is definitive — surface it ahead of any parsed activity.
376
+ if (a.status === 'closed' || a.status === 'abandoned')
377
+ return a.status;
378
+ if (a.status === 'input_required' || a.activity === 'waiting_input')
379
+ return 'waiting';
380
+ if (a.status === 'running' || a.activity === 'working')
381
+ return 'working';
382
+ if (a.status === 'idle' || a.activity === 'idle')
383
+ return 'idle';
384
+ if (a.status === 'queued')
385
+ return 'queued';
386
+ return '';
387
+ }
388
+ /** The colored, space-padded status cell for a listing row (empty when not live). */
389
+ function liveStatusCell(live) {
390
+ const word = liveStatusWord(live);
391
+ if (!word || !live)
392
+ return { cell: '', width: 0 };
393
+ return { cell: statusColor(live.status)(padToWidth(word, 8)), width: 8 };
394
+ }
346
395
  /**
347
396
  * The tracker/PR ref for a session's dedicated column: the ticket id when known,
348
397
  * else `PR#<n>`, else empty. Pulled out of the trailing badge blob so refs align
@@ -351,6 +400,21 @@ export function liveGlyphAndPreview(a) {
351
400
  export function ticketLabel(s) {
352
401
  return s.ticketId ?? (s.prNumber ? `PR#${s.prNumber}` : '');
353
402
  }
403
+ function ticketUrl(s) {
404
+ if (s.ticketId)
405
+ return linearIssueUrl(s.ticketId);
406
+ return s.prNumber ? s.prUrl : undefined;
407
+ }
408
+ export function linkTicketCell(s, label) {
409
+ const url = ticketUrl(s);
410
+ return url && label.trim() !== '-' ? linkUrl(url, label) : label;
411
+ }
412
+ export function linkCwdCell(s, label) {
413
+ return s.cwd && !s._remote ? linkPath(s.cwd, label) : label;
414
+ }
415
+ function modelLabel(model) {
416
+ return model ? shortenModel(model) : '-';
417
+ }
354
418
  /**
355
419
  * The row shape `agents sessions --active --json` emits. RUSH-1981: a watcher
356
420
  * joins active sessions on ticketId + project, but the raw ActiveSession nests
@@ -365,6 +429,7 @@ export function serializeActiveSessionsForJson(sessions) {
365
429
  ...s,
366
430
  ticketId: s.ticket?.id ?? null,
367
431
  project: s.cwd ? path.basename(s.cwd) : null,
432
+ prLink: s.pr?.url ?? null,
368
433
  }));
369
434
  }
370
435
  /**
@@ -435,11 +500,12 @@ function locatorBadge(s) {
435
500
  * terminal width so the row never wraps.
436
501
  */
437
502
  function printActiveRow(s, indent) {
438
- // shortId (8-char) · agent · host · status · badges · identity+todos+snippet
503
+ // shortId (8-char) · agent · host · status · owner · badges · identity+todos+snippet
439
504
  const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', 9));
440
505
  const kindCol = colorAgent(s.kind)(padToWidth(truncateToWidth(s.kind, 8), 9));
441
506
  const hostCol = chalk.gray(padToWidth(truncateToWidth(s.host ?? '-', 8), 9));
442
507
  const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), 8), 9));
508
+ const ownerCol = chalk.cyan(padToWidth(truncateToWidth(ownerLabel(s), 8), 9));
443
509
  const fork = s.pidCount && s.pidCount > 1 ? chalk.dim(`×${s.pidCount} `) : '';
444
510
  const badges = (fork ? fork : '') + [signalBadges(s), locatorBadge(s)].filter(Boolean).join(' ');
445
511
  // Identity (label/project/ticket clickable) + checklist + live snippet.
@@ -447,10 +513,24 @@ function printActiveRow(s, indent) {
447
513
  // via the SSH fan-out already populated (including todos when the peer has them).
448
514
  const desc = formatActiveRowDescription(s) || '-';
449
515
  // Fill the remaining width with the preview so nothing wraps under tmux/SSH.
450
- const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
516
+ const fixed = stringWidth(indent) + 9 + 9 + 9 + 9 + 9 + (badges ? stringWidth(badges) + 1 : 0);
451
517
  const room = Math.max(12, terminalWidth() - fixed - 1);
452
518
  const descCol = chalk.white(truncateToWidth(desc, room));
453
- console.log(indent + idCol + kindCol + hostCol + statusCol + (badges ? badges + ' ' : '') + descCol);
519
+ console.log(indent + idCol + kindCol + hostCol + statusCol + ownerCol + (badges ? badges + ' ' : '') + descCol);
520
+ }
521
+ /**
522
+ * Compact owner display for the `--active` owner column: the local-part of a
523
+ * resolved actor email/login (`muqsit@getrush.ai` -> `muqsit`), the id as-is
524
+ * when it has no `@`, and `-` when the actor is unresolved (`UNRESOLVED@<host>`)
525
+ * or absent (a launch predating actor stamping). Honest by design — an
526
+ * unresolved local run shows no owner rather than inventing one.
527
+ */
528
+ export function ownerLabel(s) {
529
+ const owner = s.owner;
530
+ if (!owner || owner.startsWith('UNRESOLVED@'))
531
+ return '-';
532
+ const at = owner.indexOf('@');
533
+ return at > 0 ? owner.slice(0, at) : owner;
454
534
  }
455
535
  /**
456
536
  * Short label for an IDE window. The slice key in live-terminals.json is
@@ -666,8 +746,8 @@ async function runRemoteSessionsJson(hosts) {
666
746
  process.stdout.write(serializeSessionsJson(sessions));
667
747
  }
668
748
  /**
669
- * `running N · idle N · waiting N · queued N · unknown N` for a bucket of
670
- * sessions (zero buckets omitted). Same bucketing as the grand-total summary so
749
+ * `running N · idle N · waiting N · queued N · closed N · abandoned N · unknown N`
750
+ * for a bucket of sessions (zero buckets omitted). Same bucketing as the summary so
671
751
  * per-group counts reconcile with the `(total)` beside the header — the `unknown`
672
752
  * bucket is what keeps an alive-but-opaque row from silently vanishing from the
673
753
  * tally. Empty when nothing.
@@ -677,6 +757,8 @@ function groupTally(sessions) {
677
757
  const idle = sessions.filter(s => s.status === 'idle').length;
678
758
  const waiting = sessions.filter(s => s.status === 'input_required').length;
679
759
  const queued = sessions.filter(s => s.status === 'queued').length;
760
+ const closed = sessions.filter(s => s.status === 'closed').length;
761
+ const abandoned = sessions.filter(s => s.status === 'abandoned').length;
680
762
  const unknown = sessions.filter(s => s.status === 'unknown').length;
681
763
  const parts = [];
682
764
  if (running)
@@ -687,6 +769,10 @@ function groupTally(sessions) {
687
769
  parts.push(`${waiting} waiting`);
688
770
  if (queued)
689
771
  parts.push(`${queued} queued`);
772
+ if (closed)
773
+ parts.push(`${closed} closed`);
774
+ if (abandoned)
775
+ parts.push(`${abandoned} abandoned`);
690
776
  if (unknown)
691
777
  parts.push(`${unknown} unknown`);
692
778
  return parts.join(' · ');
@@ -958,6 +1044,21 @@ async function renderSessionPreview(query, scope) {
958
1044
  console.log(chalk.gray(`No session matches "${query}".`));
959
1045
  return;
960
1046
  }
1047
+ // Lead with the live status when the session is still running, so the preview
1048
+ // says working / waiting / idle up front — not just the historical transcript.
1049
+ let live;
1050
+ try {
1051
+ live = indexActiveBySessionId(await getActiveSessions()).get(session.id);
1052
+ }
1053
+ catch { /* plain preview on any probe failure */ }
1054
+ if (live) {
1055
+ const { glyph } = liveGlyphAndPreview(live);
1056
+ const word = liveStatusWord(live) || live.status;
1057
+ const needsYou = live.status === 'input_required' || live.activity === 'waiting_input';
1058
+ const reason = live.awaitingReason ? ` (${live.awaitingReason.replace('_', ' ')})` : '';
1059
+ const suffix = needsYou ? chalk.yellow(` ← needs you${reason}`) : '';
1060
+ console.log(`${glyph} ${statusColor(live.status)(word)}${suffix}`);
1061
+ }
961
1062
  console.log(buildPreview(session));
962
1063
  }
963
1064
  /** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
@@ -1322,12 +1423,12 @@ function metaSignals(s) {
1322
1423
  };
1323
1424
  }
1324
1425
  /** One flat table row:
1325
- * shortId · agent · version · project · [glyph] label·doing · [ticket] · [wt] · time
1426
+ * shortId · agent · version · model · project · [glyph] label·doing · [ticket] · [wt] · time
1326
1427
  * `doing` is the live preview when running, else the topic. The `ticket` column
1327
1428
  * (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
1328
1429
  * when `showTicket` — otherwise a listing with no refs would waste a column of
1329
1430
  * dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
1330
- function flatSessionRow(session, live, showTicket = false, cols = {}) {
1431
+ export function flatSessionRow(session, live, showTicket = false, cols = {}) {
1331
1432
  const agentColor = colorAgent(session.agent);
1332
1433
  const when = formatRelativeTime(session.lastActivity ?? session.timestamp);
1333
1434
  const project = session.project || '-';
@@ -1350,19 +1451,31 @@ function flatSessionRow(session, live, showTicket = false, cols = {}) {
1350
1451
  : '';
1351
1452
  const TICKET_W = 10;
1352
1453
  const ticketCell = showTicket
1353
- ? chalk.blue(padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1))
1454
+ ? chalk.blue(linkTicketCell(session, padToWidth(truncateToWidth(ticketLabel(session) || '-', TICKET_W), TICKET_W + 1)))
1354
1455
  : '';
1456
+ // Live status word (working / waiting / idle) next to the glyph — the default
1457
+ // list is no longer a bare glyph. Empty (zero width) for resting rows.
1458
+ const { cell: statusCell, width: statusW } = liveStatusCell(live);
1355
1459
  const glyphW = glyph ? 2 : 0;
1356
1460
  const machineW = cols.showMachine ? machineColW : 0;
1357
1461
  const ticketW = showTicket ? TICKET_W + 1 : 0;
1358
1462
  const wtW = wt ? stringWidth(wt) + 1 : 0;
1359
- const topicW = Math.max(16, terminalWidth() - (10 + 9 + 8 + 16) - glyphW - machineW - ticketW - wtW - stringWidth(when) - 1);
1463
+ const width = terminalWidth();
1464
+ const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
1465
+ const fixedW = (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + stringWidth(when) + 1;
1466
+ const modelSlack = width - fixedW - 16;
1467
+ const modelW = requestedModelW <= modelSlack
1468
+ ? requestedModelW
1469
+ : modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
1470
+ const topicW = Math.max(16, width - fixedW - modelW);
1360
1471
  return (chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
1361
1472
  agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
1362
1473
  chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
1474
+ (modelW ? chalk.yellow(padToWidth(truncateToWidth(modelLabel(session.model), modelW - 1), modelW)) : '') +
1363
1475
  machineCell +
1364
- chalk.cyan(padToWidth(truncateToWidth(project, 14), 16)) +
1476
+ chalk.cyan(linkCwdCell(session, padToWidth(truncateToWidth(project, 14), 16))) +
1365
1477
  (glyph ? glyph + ' ' : '') +
1478
+ statusCell +
1366
1479
  renderTopicCell(label, doing, '', topicW, topicW) +
1367
1480
  ticketCell +
1368
1481
  (wt ? wt + ' ' : '') +
@@ -1383,13 +1496,15 @@ function treeSessionRow(session, live) {
1383
1496
  const badges = signalBadges(metaSignals(session));
1384
1497
  const badgeW = badges ? stringWidth(badges) + 1 : 0;
1385
1498
  const head = label ? `${label} · ${topic}` : topic;
1499
+ const { cell: statusCell, width: statusW } = liveStatusCell(live);
1386
1500
  const glyphW = glyph ? 2 : 0;
1387
- const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - badgeW - stringWidth(when) - 1);
1501
+ const topicW = Math.max(12, terminalWidth() - (2 + 9 + 8) - glyphW - statusW - badgeW - stringWidth(when) - 1);
1388
1502
  return (' ' +
1389
1503
  chalk.dim(padToWidth(session.shortId, 9)) +
1390
1504
  agentColor(padToWidth(truncateToWidth(session.agent, 7), 8)) +
1391
1505
  (badges ? badges + ' ' : '') +
1392
1506
  (glyph ? glyph + ' ' : '') +
1507
+ statusCell +
1393
1508
  padToWidth(chalk.white(truncateToWidth(head, topicW)), topicW) +
1394
1509
  ' ' + chalk.gray(when));
1395
1510
  }
@@ -1506,7 +1621,9 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
1506
1621
  console.log();
1507
1622
  first = false;
1508
1623
  const group = byDir.get(key);
1509
- console.log(`${chalk.cyan.bold(shortCwd(key))} ${chalk.gray(`(${group.length})`)}`);
1624
+ const cwd = group.find((s) => s.cwd && !s._remote)?.cwd;
1625
+ const header = cwd ? linkPath(cwd, shortCwd(key)) : shortCwd(key);
1626
+ console.log(`${chalk.cyan.bold(header)} ${chalk.gray(`(${group.length})`)}`);
1510
1627
  for (const s of group)
1511
1628
  console.log(treeSessionRow(s, liveIndex?.get(s.id)));
1512
1629
  }
@@ -1702,12 +1819,18 @@ function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
1702
1819
  const PICKER_MACHINE_W = 11;
1703
1820
  const PICKER_MACHINE_MIN = 8;
1704
1821
  const PICKER_MACHINE_MAX = 18;
1822
+ const PICKER_MODEL_MIN = 6;
1823
+ const PICKER_MODEL_MAX = 13;
1705
1824
  /** Column width that shows every compacted hostname in `machines` whole (one
1706
1825
  * trailing space for separation), bounded by MIN/MAX. */
1707
1826
  function machineColumnWidth(machines, label) {
1708
1827
  const widest = machines.reduce((w, m) => Math.max(w, stringWidth(label(m))), 0);
1709
1828
  return Math.min(PICKER_MACHINE_MAX, Math.max(PICKER_MACHINE_MIN, widest + 1));
1710
1829
  }
1830
+ function modelColumnWidth(sessions) {
1831
+ const widest = sessions.reduce((width, session) => (Math.max(width, session.model ? stringWidth(modelLabel(session.model)) : 0)), 0);
1832
+ return Math.min(PICKER_MODEL_MAX, Math.max(PICKER_MODEL_MIN, widest + 1));
1833
+ }
1711
1834
  /**
1712
1835
  * Compact display form for machine ids: strip the longest shared dash-delimited
1713
1836
  * prefix so "yosemite-s0"/"yosemite-s1" read as "s0"/"s1" while unrelated ids
@@ -1743,6 +1866,8 @@ export function pickerColumnsFor(sessions) {
1743
1866
  showMachine: distinct.length > 1,
1744
1867
  machineLabel,
1745
1868
  machineWidth: machineColumnWidth(distinct, machineLabel),
1869
+ showModel: sessions.some((s) => !!s.model),
1870
+ modelWidth: modelColumnWidth(sessions),
1746
1871
  showTicket: sessions.some((s) => ticketLabel(s) !== ''),
1747
1872
  };
1748
1873
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `agents set` — ergonomic top-level setter for per-agent/version run defaults.
3
+ *
4
+ * `agents set claude@2.1.220 --model opus-5` pins the default model (and/or
5
+ * mode) that `agents run` uses for that agent version. It reads and writes the
6
+ * same store as `agents defaults run set` (agents.yaml -> run.defaults), so the
7
+ * two stay consistent — `set` is just the short front door.
8
+ *
9
+ * agents set # list every configured default
10
+ * agents set claude@2.1.220 # show the default for one version
11
+ * agents set claude@2.1.220 --model opus-5
12
+ * agents set 'claude:*' --mode auto --model opus
13
+ */
14
+ import type { Command } from 'commander';
15
+ export declare function registerSetCommand(program: Command): void;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * `agents set` — ergonomic top-level setter for per-agent/version run defaults.
3
+ *
4
+ * `agents set claude@2.1.220 --model opus-5` pins the default model (and/or
5
+ * mode) that `agents run` uses for that agent version. It reads and writes the
6
+ * same store as `agents defaults run set` (agents.yaml -> run.defaults), so the
7
+ * two stay consistent — `set` is just the short front door.
8
+ *
9
+ * agents set # list every configured default
10
+ * agents set claude@2.1.220 # show the default for one version
11
+ * agents set claude@2.1.220 --model opus-5
12
+ * agents set 'claude:*' --mode auto --model opus
13
+ */
14
+ import chalk from 'chalk';
15
+ import { setHelpSections } from '../lib/help.js';
16
+ import { formatRunDefaultEntry, listRunDefaults, parseRunDefaultSelector, setRunDefault, } from '../lib/run-defaults.js';
17
+ export function registerSetCommand(program) {
18
+ const set = program
19
+ .command('set [selector]')
20
+ .description('Set the default model/mode an agent version uses for `agents run`')
21
+ .option('--model <model>', 'Default model or model alias, forwarded via --model')
22
+ .option('--mode <mode>', "Default mode: plan, edit, auto, skip. 'full' accepted as alias for skip.")
23
+ .action((selector, options) => {
24
+ try {
25
+ const hasFlags = options.model !== undefined || options.mode !== undefined;
26
+ if (!selector) {
27
+ if (hasFlags) {
28
+ throw new Error('Selector is required when passing --model/--mode. Example: agents set claude@2.1.220 --model opus-5');
29
+ }
30
+ const entries = listRunDefaults();
31
+ if (entries.length === 0) {
32
+ console.log(chalk.gray('No agent defaults configured.'));
33
+ console.log(chalk.gray('Set one with: agents set claude@2.1.220 --model opus-5'));
34
+ return;
35
+ }
36
+ console.log(chalk.bold('Agent Defaults\n'));
37
+ for (const entry of entries) {
38
+ console.log(` ${formatRunDefaultEntry(entry)}`);
39
+ }
40
+ return;
41
+ }
42
+ if (!hasFlags) {
43
+ const parsed = parseRunDefaultSelector(selector);
44
+ const entry = listRunDefaults().find((e) => e.selector === parsed.selector);
45
+ if (!entry || (!entry.defaults.mode && !entry.defaults.model)) {
46
+ console.log(chalk.gray(`No default set for ${parsed.selector}.`));
47
+ console.log(chalk.gray(`Set one with: agents set ${selector} --model <model>`));
48
+ return;
49
+ }
50
+ console.log(` ${formatRunDefaultEntry(entry)}`);
51
+ return;
52
+ }
53
+ const entry = setRunDefault(selector, {
54
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
55
+ ...(options.model !== undefined ? { model: options.model } : {}),
56
+ });
57
+ console.log(chalk.green('Set default:'));
58
+ console.log(` ${formatRunDefaultEntry(entry)}`);
59
+ }
60
+ catch (err) {
61
+ console.error(chalk.red(err.message));
62
+ process.exit(1);
63
+ }
64
+ });
65
+ setHelpSections(set, {
66
+ examples: `
67
+ agents set claude@2.1.220 --model opus-5
68
+ agents set 'claude:*' --mode auto --model opus
69
+ agents set claude@2.1.220
70
+ agents set
71
+ `,
72
+ notes: `
73
+ Selectors use <agent>@<version> or <agent>:<version>; * matches all versions.
74
+ Exact selectors override wildcard selectors field by field.
75
+ Writes the same store as 'agents defaults run set'. Explicit flags on
76
+ 'agents run' always win over configured defaults.
77
+ `,
78
+ });
79
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `agents setup fleet` — guided Tailscale device onboarding.
3
+ *
4
+ * This wizard is a front door over the existing fleet/device commands: sync
5
+ * Tailscale into the registry, choose auth, render SSH config, test devices, and
6
+ * optionally run the fleet updater without reimplementing those subcommands.
7
+ */
8
+ import type { Command } from 'commander';
9
+ interface SetupFleetOptions {
10
+ auth?: string;
11
+ bundle?: string;
12
+ bundleKey?: string;
13
+ yes?: boolean;
14
+ writeSshConfig?: boolean;
15
+ testConnectivity?: boolean;
16
+ fleetUpdate?: boolean;
17
+ }
18
+ export declare function runFleetSetupWizard(opts?: SetupFleetOptions): Promise<boolean>;
19
+ /** Register `agents setup fleet` under the parent `setup` command. */
20
+ export declare function registerSetupFleetCommand(setupCmd: Command): void;
21
+ export {};