@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -0,0 +1,387 @@
1
+ /**
2
+ * `agents sessions trace <selectors...>` (alias: `agents trace <selectors...>`) —
3
+ * render one session as a derived trajectory, or two as a compare.
4
+ *
5
+ * One model (`buildTrajectory`), three renderings, audience auto-selected: a
6
+ * person at a TTY gets the self-contained HTML opened in a browser; a piped or
7
+ * headless caller (an agent) gets the compact text trajectory; `--json` emits the
8
+ * versioned envelope the AGI EXT Fleet panel and triaging agents consume. Explicit
9
+ * `--html/--text/--json` always win.
10
+ *
11
+ * Exactly two resolved selectors turn the single trajectory into a **compare**
12
+ * (`diffTrajectories`, PR2/3) — the same three renderings, laid on a shared axis
13
+ * with a divergence marker. `--tree` turns one selector into a **lineage**
14
+ * (`buildLineage`, PR3/3): the orchestrator and every session it spawned, edges
15
+ * read from the team records. Three or more selectors still fail loud.
16
+ */
17
+ import * as fs from 'fs';
18
+ import * as path from 'path';
19
+ import chalk from 'chalk';
20
+ import { setHelpSections } from '../lib/help.js';
21
+ import { showFile } from '../lib/open-url.js';
22
+ import { knownSecretValuesFromEnv } from '../lib/redact.js';
23
+ import { getCacheDir } from '../lib/state.js';
24
+ import { discoverSessions } from '../lib/session/discover.js';
25
+ import { parseSession } from '../lib/session/parse.js';
26
+ import { buildTrajectory } from '../lib/session/trajectory.js';
27
+ import { diffTrajectories } from '../lib/session/trajectory-compare.js';
28
+ import { buildLineage } from '../lib/session/trajectory-lineage.js';
29
+ import { renderTrajectoryHtml, renderTrajectoryCompareHtml, renderLineageHtml } from '../lib/session/trajectory-html.js';
30
+ import { renderTrajectoryText, renderTrajectoryCompareText, renderLineageText } from '../lib/session/trajectory-text.js';
31
+ import { parseAgentFilter } from './sessions.js';
32
+ import { selectSessions } from './sessions-export.js';
33
+ /** Versioned envelope emitted by `--json` — the stable contract for consumers. */
34
+ export const SESSIONS_TRACE_SCHEMA_VERSION = 1;
35
+ /** Build the versioned `--json` envelope for a single-session trajectory. */
36
+ export function buildTraceEnvelope(models) {
37
+ return {
38
+ schemaVersion: SESSIONS_TRACE_SCHEMA_VERSION,
39
+ kind: 'sessions-trace',
40
+ layout: 'single',
41
+ sessions: models,
42
+ };
43
+ }
44
+ /** Build the versioned `--json` envelope for a two-session compare. */
45
+ export function buildCompareTraceEnvelope(cmp) {
46
+ return {
47
+ schemaVersion: SESSIONS_TRACE_SCHEMA_VERSION,
48
+ kind: 'sessions-trace',
49
+ layout: 'compare',
50
+ sessions: [cmp.a, cmp.b],
51
+ diff: {
52
+ divergence: cmp.divergence,
53
+ added: cmp.added,
54
+ removed: cmp.removed,
55
+ summaryA: cmp.summaryA,
56
+ summaryB: cmp.summaryB,
57
+ truncatedA: cmp.truncatedA,
58
+ truncatedB: cmp.truncatedB,
59
+ },
60
+ };
61
+ }
62
+ /**
63
+ * Build the versioned `--json` envelope for a lineage. `sessions` carries the
64
+ * ROOT's trajectory only: the graph's own numbers come from the indexed session
65
+ * rows, so a consumer pays one transcript parse instead of one per teammate —
66
+ * `agents sessions trace <child>` is how you get a child's full trajectory.
67
+ */
68
+ export function buildLineageTraceEnvelope(lineage, rootTrajectory) {
69
+ return {
70
+ schemaVersion: SESSIONS_TRACE_SCHEMA_VERSION,
71
+ kind: 'sessions-trace',
72
+ layout: 'lineage',
73
+ sessions: [rootTrajectory],
74
+ lineage: {
75
+ rootId: lineage.rootId,
76
+ nodes: lineage.nodes,
77
+ edges: lineage.edges,
78
+ teams: lineage.teams,
79
+ unresolvedParentIds: lineage.unresolvedParentIds,
80
+ },
81
+ };
82
+ }
83
+ /**
84
+ * Pick the rendering by audience: explicit `--html/--text/--json` win; otherwise
85
+ * a person at a TTY gets the visual HTML and a piped/headless caller (an agent)
86
+ * gets the compact text trajectory.
87
+ */
88
+ export function chooseFormat(options, isTTY) {
89
+ if (options.json)
90
+ return 'json';
91
+ if (options.html)
92
+ return 'html';
93
+ if (options.text)
94
+ return 'text';
95
+ return isTTY ? 'html' : 'text';
96
+ }
97
+ /**
98
+ * Decide the trace layout from what the user TYPED (`selectorCount`) vs what
99
+ * those selectors RESOLVED to (`resolvedCount`). Keyed on selector count so a
100
+ * single content-search selector that happens to match two sessions never
101
+ * silently becomes a compare — every unsupported combination fails loud with a
102
+ * clear message. Pure, so the boundaries are unit-tested without the command.
103
+ */
104
+ export function decideTraceLayout(options, selectorCount, resolvedCount) {
105
+ if (options.tree) {
106
+ if (options.compare) {
107
+ throw new Error('`--tree` (lineage) and `--compare` are different layouts — pass one or the other.');
108
+ }
109
+ if (selectorCount !== 1) {
110
+ throw new Error(`Lineage roots at ONE session; you passed ${selectorCount} selectors. ` +
111
+ 'Pass the orchestrator session whose team you want to see.');
112
+ }
113
+ if (resolvedCount !== 1) {
114
+ throw new Error(`The selector must match exactly one session to root a lineage; it matched ${resolvedCount}. ` +
115
+ 'Use a more specific selector (e.g. the full session id).');
116
+ }
117
+ return 'lineage';
118
+ }
119
+ const wantCompare = options.compare === true || selectorCount >= 2;
120
+ if (wantCompare) {
121
+ if (selectorCount !== 2) {
122
+ throw new Error(`Comparing needs exactly two selectors; you passed ${selectorCount}. ` +
123
+ 'Pass two session selectors (ids or queries), each matching one session.');
124
+ }
125
+ if (resolvedCount !== 2) {
126
+ throw new Error(`Each selector must match exactly one session to compare; the two selectors matched ${resolvedCount}. ` +
127
+ 'Use more specific selectors (e.g. full session ids).');
128
+ }
129
+ return 'compare';
130
+ }
131
+ if (resolvedCount > 1) {
132
+ throw new Error(`${resolvedCount} sessions matched — pass a more specific selector for one trajectory, ` +
133
+ 'or two selectors to compare them.');
134
+ }
135
+ return 'single';
136
+ }
137
+ /** Attach the trace behaviour to a command node (canonical or top-level alias). */
138
+ export function configureTraceCommand(cmd) {
139
+ cmd
140
+ .description('Visualize a session as a trajectory — a tool-call timeline you can read at a glance. Opens a visual for a person; prints a compact trajectory for an agent.')
141
+ .option('--html', 'Force the HTML rendering (opens in a browser)')
142
+ .option('--text', 'Force the compact text trajectory')
143
+ .option('--json', 'Emit the versioned sessions-trace JSON envelope')
144
+ .option('-o, --output <path>', 'Write the rendering to a path instead of opening/printing')
145
+ .option('--no-open', 'Do not open the HTML; print its path instead')
146
+ .option('--errors-only', 'Collapse the text trajectory to error steps and their neighbours')
147
+ .option('--compare', 'Force the compare layout for exactly two selectors (this is also the default for two)')
148
+ .option('--tree', 'Render the lineage of one session — it and every session it spawned')
149
+ .option('--no-redact', 'Local-only: skip secret redaction of derived labels (never for a shared file)')
150
+ .option('--all', 'Search every directory and all time, not just this project')
151
+ .option('--since <time>', 'Only sessions newer than this (7d, 4w, an ISO date)')
152
+ .option('--limit <n>', 'Max sessions to scan when resolving the selector (default 100)')
153
+ .option('-a, --agent <agent>', 'Filter by agent type (e.g. claude, codex)');
154
+ setHelpSections(cmd, {
155
+ examples: `# Open one session's trajectory in a browser (a person at a terminal)
156
+ agents sessions trace a1b2c3d4
157
+
158
+ # The compact text trajectory an agent reads in-context
159
+ agents sessions trace a1b2c3d4 --text
160
+
161
+ # Just the failures and their neighbours — for a triaging agent
162
+ agents sessions trace a1b2c3d4 --text --errors-only
163
+
164
+ # The stable JSON envelope for the AGI EXT Fleet panel or a tool
165
+ agents sessions trace a1b2c3d4 --json
166
+
167
+ # Write the self-contained HTML to a file without opening it
168
+ agents sessions trace a1b2c3d4 --html -o trace.html --no-open
169
+
170
+ # Two selectors — compare where a passing run and a failing one diverge
171
+ agents sessions trace a1b2c3d4 e5f6a7b8
172
+
173
+ # The compare, as text — for a triaging agent comparing harnesses
174
+ agents sessions trace a1b2c3d4 e5f6a7b8 --text
175
+
176
+ # The team an orchestrator spawned, as a graph (one selector + --tree)
177
+ agents sessions trace a1b2c3d4 --tree
178
+
179
+ # The same lineage as an indented tree an agent can read
180
+ agents sessions trace a1b2c3d4 --tree --text`,
181
+ notes: `Audience auto-select: with no --html/--text/--json, HTML opens on a TTY (a person) and
182
+ the compact text trajectory prints when piped or headless (an agent). The HTML is self-contained
183
+ (no CDN, no external asset) and redacted by default, as safe to share as an 'agents sessions share' page.
184
+
185
+ One selector renders the single trajectory; exactly two selectors compare them (a shared time axis,
186
+ the first divergence point, and the step-level diff); one selector with --tree renders its lineage —
187
+ the session and every session it spawned, with the edges read from the teams records
188
+ (teamOrigin.parentSessionId), never from an inline Task sub-agent, which is a step inside one transcript
189
+ and has no session of its own. --json emits { schemaVersion, kind: 'sessions-trace',
190
+ layout: 'single' | 'compare' | 'lineage', sessions: [SessionTrajectory], diff?, lineage? } — the contract
191
+ consumers read so nothing re-parses a transcript.
192
+
193
+ Lineage resolves children from the scanned pool, so a very old team may need a wider --limit or --since.
194
+ Three or more selectors, and --tree with more than one selector, fail loud.`,
195
+ });
196
+ cmd.action(async (selectors, _options, command) => {
197
+ // Merge parent globals: under `agents sessions trace`, the `sessions` command
198
+ // owns `--json`/`--no-redact`/`--all`/`--agent`/`--since` and captures them, so
199
+ // read the merged view (as `render`/`insights` do) — the top-level `agents
200
+ // trace` alias has no such parent and its own options fill the same fields.
201
+ const options = command.optsWithGlobals();
202
+ // Lineage resolves its children FROM the scanned pool, and a teammate can sit
203
+ // well below the orchestrator in recency order, so --tree scans wider by
204
+ // default than the single/compare layouts. An explicit --limit still wins.
205
+ const defaultLimit = options.tree ? 500 : 100;
206
+ const limit = Math.max(1, Number.parseInt(options.limit || String(defaultLimit), 10) || defaultLimit);
207
+ const agent = parseAgentFilter(options.agent).agent;
208
+ const pool = await discoverSessions({
209
+ // Default to every directory so an id from any project resolves, matching
210
+ // `agents sessions render` (sessions-render.ts:126).
211
+ all: options.all !== false,
212
+ since: options.since,
213
+ limit,
214
+ agent: agent ?? undefined,
215
+ });
216
+ const sessions = selectSessions(pool, selectors);
217
+ if (sessions.length === 0) {
218
+ process.stderr.write(chalk.yellow('No sessions matched the selection.\n'));
219
+ process.exitCode = 1;
220
+ return;
221
+ }
222
+ // Layout is keyed on what the USER TYPED (selector count), not the resolved
223
+ // count — a single content-search selector matching two sessions must NOT
224
+ // silently become a compare. All the fail-loud boundaries live in the pure
225
+ // decideTraceLayout so they are unit-tested (see sessions-trace.test.ts).
226
+ const layout = decideTraceLayout(options, selectors.length, sessions.length);
227
+ const redact = options.redact !== false;
228
+ const knownSecrets = redact ? knownSecretValuesFromEnv() : undefined;
229
+ const format = chooseFormat(options, Boolean(process.stdout.isTTY));
230
+ const buildOne = (session) => {
231
+ const events = parseSession(session.filePath, session.agent);
232
+ return buildTrajectory(events, session, { redact, knownSecrets });
233
+ };
234
+ if (layout === 'lineage') {
235
+ // The pool must keep team-origin rows: they are hidden from the ordinary
236
+ // listing by default (AGENTS.md invariant 7), and they ARE the children.
237
+ // discoverSessions does not apply that presentation filter, so `pool` is
238
+ // already the teams-included set the graph needs.
239
+ const root = sessions[0];
240
+ // An id-shaped selector resolves through the session INDEX, which reaches
241
+ // rows the scanned pool does not hold (selectSessions -> findSessionsById,
242
+ // sessions-export.ts:386). Seed the pool with the resolved root so the
243
+ // graph always roots where the user pointed.
244
+ const pooled = pool.some((s) => s.id === root.id) ? pool : [root, ...pool];
245
+ const lineage = buildLineage(pooled, { rootId: root.id });
246
+ const idPart = root.shortId || root.id;
247
+ if (format === 'json') {
248
+ const out = JSON.stringify(buildLineageTraceEnvelope(lineage, buildOne(root)), null, 2) + '\n';
249
+ if (options.output) {
250
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
251
+ process.stderr.write(chalk.green(`Wrote lineage JSON to ${options.output}\n`));
252
+ }
253
+ else {
254
+ process.stdout.write(out);
255
+ }
256
+ return;
257
+ }
258
+ if (format === 'text') {
259
+ const out = renderLineageText(lineage);
260
+ if (options.output) {
261
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
262
+ process.stderr.write(chalk.green(`Wrote lineage text to ${options.output}\n`));
263
+ }
264
+ else {
265
+ process.stdout.write(out);
266
+ }
267
+ return;
268
+ }
269
+ // HTML.
270
+ const html = renderLineageHtml(lineage);
271
+ if (options.output) {
272
+ fs.writeFileSync(options.output, html, { mode: 0o600 });
273
+ process.stdout.write(`${path.resolve(options.output)}\n`);
274
+ return;
275
+ }
276
+ const dir = path.join(getCacheDir(), 'traces');
277
+ fs.mkdirSync(dir, { recursive: true });
278
+ const file = path.join(dir, `${idPart}-lineage-${Date.now()}.html`);
279
+ fs.writeFileSync(file, html, { mode: 0o600 });
280
+ if (options.open === false) {
281
+ process.stdout.write(`${file}\n`);
282
+ }
283
+ else {
284
+ await showFile(file);
285
+ process.stderr.write(chalk.green(`Opened lineage for ${idPart}: ${file}\n`));
286
+ }
287
+ return;
288
+ }
289
+ if (layout === 'compare') {
290
+ const cmp = diffTrajectories(buildOne(sessions[0]), buildOne(sessions[1]));
291
+ const idPart = `${sessions[0].shortId || sessions[0].id}-vs-${sessions[1].shortId || sessions[1].id}`;
292
+ if (format === 'json') {
293
+ const out = JSON.stringify(buildCompareTraceEnvelope(cmp), null, 2) + '\n';
294
+ if (options.output) {
295
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
296
+ process.stderr.write(chalk.green(`Wrote compare JSON to ${options.output}\n`));
297
+ }
298
+ else {
299
+ process.stdout.write(out);
300
+ }
301
+ return;
302
+ }
303
+ if (format === 'text') {
304
+ const out = renderTrajectoryCompareText(cmp);
305
+ if (options.output) {
306
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
307
+ process.stderr.write(chalk.green(`Wrote compare text to ${options.output}\n`));
308
+ }
309
+ else {
310
+ process.stdout.write(out);
311
+ }
312
+ return;
313
+ }
314
+ // HTML.
315
+ const html = renderTrajectoryCompareHtml(cmp);
316
+ if (options.output) {
317
+ fs.writeFileSync(options.output, html, { mode: 0o600 });
318
+ process.stdout.write(`${path.resolve(options.output)}\n`);
319
+ return;
320
+ }
321
+ const dir = path.join(getCacheDir(), 'traces');
322
+ fs.mkdirSync(dir, { recursive: true });
323
+ const file = path.join(dir, `${idPart}-${Date.now()}.html`);
324
+ fs.writeFileSync(file, html, { mode: 0o600 });
325
+ if (options.open === false) {
326
+ process.stdout.write(`${file}\n`);
327
+ }
328
+ else {
329
+ await showFile(file);
330
+ process.stderr.write(chalk.green(`Opened compare for ${idPart}: ${file}\n`));
331
+ }
332
+ return;
333
+ }
334
+ // One selector — the single-session trajectory.
335
+ const session = sessions[0];
336
+ const model = buildOne(session);
337
+ if (format === 'json') {
338
+ const out = JSON.stringify(buildTraceEnvelope([model]), null, 2) + '\n';
339
+ if (options.output) {
340
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
341
+ process.stderr.write(chalk.green(`Wrote trajectory JSON to ${options.output}\n`));
342
+ }
343
+ else {
344
+ process.stdout.write(out);
345
+ }
346
+ return;
347
+ }
348
+ if (format === 'text') {
349
+ const out = renderTrajectoryText(model, { errorsOnly: options.errorsOnly === true });
350
+ if (options.output) {
351
+ fs.writeFileSync(options.output, out, { mode: 0o600 });
352
+ process.stderr.write(chalk.green(`Wrote text trajectory to ${options.output}\n`));
353
+ }
354
+ else {
355
+ process.stdout.write(out);
356
+ }
357
+ return;
358
+ }
359
+ // HTML.
360
+ const html = renderTrajectoryHtml(model);
361
+ if (options.output) {
362
+ fs.writeFileSync(options.output, html, { mode: 0o600 });
363
+ process.stdout.write(`${path.resolve(options.output)}\n`);
364
+ return;
365
+ }
366
+ const dir = path.join(getCacheDir(), 'traces');
367
+ fs.mkdirSync(dir, { recursive: true });
368
+ const file = path.join(dir, `${session.shortId || session.id}-${Date.now()}.html`);
369
+ fs.writeFileSync(file, html, { mode: 0o600 });
370
+ if (options.open === false) {
371
+ process.stdout.write(`${file}\n`);
372
+ }
373
+ else {
374
+ await showFile(file);
375
+ process.stderr.write(chalk.green(`Opened trajectory for ${session.shortId || session.id}: ${file}\n`));
376
+ }
377
+ });
378
+ return cmd;
379
+ }
380
+ /** Canonical `agents sessions trace <selectors...>`. */
381
+ export function registerSessionsTraceCommand(sessionsCmd) {
382
+ configureTraceCommand(sessionsCmd.command('trace <selectors...>'));
383
+ }
384
+ /** Top-level alias `agents trace <selectors...>` (mirrors `agents insights`). */
385
+ export function registerTraceCommand(program) {
386
+ configureTraceCommand(program.command('trace <selectors...>'));
387
+ }
@@ -66,10 +66,12 @@ import { registerGoCommand } from './go.js';
66
66
  import { registerFocusCommand } from './focus.js';
67
67
  import { registerReconnectCommand } from './reconnect.js';
68
68
  import { registerDetachCommand } from './detach.js';
69
+ import { registerSessionsStopCommand } from './sessions-stop.js';
69
70
  import { registerAttachCommand } from './attach.js';
70
71
  import { registerSessionsInjectCommand } from './sessions-inject.js';
71
72
  import { registerSessionsExportCommand } from './sessions-export.js';
72
73
  import { registerSessionsRenderCommand } from './sessions-render.js';
74
+ import { registerSessionsTraceCommand } from './sessions-trace.js';
73
75
  import { registerSessionsShareCommand } from './sessions-share.js';
74
76
  import { registerSessionsImportCommand } from './sessions-import.js';
75
77
  import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
@@ -5170,10 +5172,12 @@ export function registerSessionsCommands(program) {
5170
5172
  registerFocusCommand(sessionsCmd);
5171
5173
  registerReconnectCommand(sessionsCmd);
5172
5174
  registerDetachCommand(sessionsCmd);
5175
+ registerSessionsStopCommand(sessionsCmd);
5173
5176
  registerAttachCommand(sessionsCmd);
5174
5177
  registerSessionsInjectCommand(sessionsCmd);
5175
5178
  registerSessionsExportCommand(sessionsCmd);
5176
5179
  registerSessionsRenderCommand(sessionsCmd);
5180
+ registerSessionsTraceCommand(sessionsCmd);
5177
5181
  registerSessionsShareCommand(sessionsCmd);
5178
5182
  registerSessionsImportCommand(sessionsCmd);
5179
5183
  registerSessionsMigrateCommand(sessionsCmd);
@@ -10,10 +10,25 @@
10
10
  * rsync / `agents sessions --device` resolve the same logical names.
11
11
  */
12
12
  import type { Command } from 'commander';
13
+ import { type DeviceRegistry } from '../lib/devices/registry.js';
13
14
  import { type FanOutDeviceTarget } from '../lib/devices/fleet.js';
15
+ import { type DeviceStats } from '../lib/devices/health.js';
14
16
  import type { AgentId } from '../lib/types.js';
15
17
  import { type HostHarnessResult } from '../lib/devices/harness-inventory.js';
16
18
  import { type CrabboxBox } from '../lib/crabbox/cli.js';
19
+ /**
20
+ * Render the device list. When `statsMap` is provided, resource columns are
21
+ * appended — a `spec` cell (cores / total RAM / total disk), normalized load,
22
+ * memory and disk-used percentages, a headroom badge, and (in `full` mode)
23
+ * free/total memory — with the per-device role and `description` riding the
24
+ * tail, so it's obvious which boxes have room and what each box is for.
25
+ * Without it (probe skipped) the classic reachability line is used. A fleet
26
+ * capacity summary is appended whenever stats were gathered.
27
+ */
28
+ export declare function renderDeviceTable(reg: DeviceRegistry, names: string[], self: string | undefined, statsMap?: Map<string, DeviceStats>, full?: boolean, interactiveHost?: string, opts?: {
29
+ width?: number;
30
+ ignoredCount?: number;
31
+ }): string[];
17
32
  /**
18
33
  * Live "Leased boxes" section for `agents devices` (F4, RUSH-1923), computed
19
34
  * from `crabboxList()` — these are ephemeral crabbox leases, NEVER written into