@phnx-labs/agents-cli 1.20.33 → 1.20.35

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 (159) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +32 -2
  3. package/dist/commands/computer-actions.d.ts +15 -0
  4. package/dist/commands/computer-actions.js +120 -22
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +17 -12
  8. package/dist/commands/doctor.d.ts +11 -0
  9. package/dist/commands/doctor.js +108 -14
  10. package/dist/commands/events.d.ts +16 -0
  11. package/dist/commands/events.js +173 -0
  12. package/dist/commands/exec.js +6 -1
  13. package/dist/commands/hosts.js +28 -24
  14. package/dist/commands/import.js +1 -1
  15. package/dist/commands/inspect.d.ts +2 -0
  16. package/dist/commands/inspect.js +54 -7
  17. package/dist/commands/logs.d.ts +17 -0
  18. package/dist/commands/logs.js +139 -0
  19. package/dist/commands/models.d.ts +3 -0
  20. package/dist/commands/models.js +27 -8
  21. package/dist/commands/permissions.js +9 -2
  22. package/dist/commands/repo.d.ts +34 -0
  23. package/dist/commands/repo.js +243 -65
  24. package/dist/commands/resource-view.d.ts +20 -0
  25. package/dist/commands/resource-view.js +90 -28
  26. package/dist/commands/rules.js +1 -1
  27. package/dist/commands/secrets-migrate.js +23 -11
  28. package/dist/commands/secrets.d.ts +20 -0
  29. package/dist/commands/secrets.js +93 -17
  30. package/dist/commands/sessions-resume.d.ts +2 -0
  31. package/dist/commands/sessions-resume.js +209 -0
  32. package/dist/commands/sessions-tail.d.ts +10 -0
  33. package/dist/commands/sessions-tail.js +11 -0
  34. package/dist/commands/sessions.d.ts +73 -1
  35. package/dist/commands/sessions.js +273 -53
  36. package/dist/commands/status.d.ts +12 -0
  37. package/dist/commands/status.js +81 -0
  38. package/dist/commands/teams.js +70 -6
  39. package/dist/commands/versions.js +2 -1
  40. package/dist/commands/view.d.ts +42 -0
  41. package/dist/commands/view.js +213 -83
  42. package/dist/commands/wallet.d.ts +6 -0
  43. package/dist/commands/wallet.js +22 -5
  44. package/dist/index.js +61 -32
  45. package/dist/lib/acp/harnesses.d.ts +1 -1
  46. package/dist/lib/acp/harnesses.js +2 -2
  47. package/dist/lib/agents.d.ts +12 -0
  48. package/dist/lib/agents.js +145 -41
  49. package/dist/lib/browser/cdp.js +5 -1
  50. package/dist/lib/browser/chrome.js +20 -0
  51. package/dist/lib/browser/drivers/ssh.d.ts +27 -0
  52. package/dist/lib/browser/drivers/ssh.js +94 -19
  53. package/dist/lib/browser/ipc.d.ts +3 -0
  54. package/dist/lib/browser/ipc.js +13 -9
  55. package/dist/lib/browser/service.js +71 -15
  56. package/dist/lib/daemon.d.ts +35 -0
  57. package/dist/lib/daemon.js +33 -5
  58. package/dist/lib/devices/connect.d.ts +3 -2
  59. package/dist/lib/devices/connect.js +5 -3
  60. package/dist/lib/doctor-diff.js +29 -2
  61. package/dist/lib/drift-sync.d.ts +43 -0
  62. package/dist/lib/drift-sync.js +179 -0
  63. package/dist/lib/events.d.ts +9 -2
  64. package/dist/lib/events.js +70 -11
  65. package/dist/lib/exec.d.ts +15 -0
  66. package/dist/lib/exec.js +43 -11
  67. package/dist/lib/hooks.js +8 -2
  68. package/dist/lib/hosts/dispatch.js +6 -7
  69. package/dist/lib/hosts/logs.d.ts +16 -0
  70. package/dist/lib/hosts/logs.js +45 -0
  71. package/dist/lib/hosts/progress.d.ts +66 -0
  72. package/dist/lib/hosts/progress.js +125 -17
  73. package/dist/lib/hosts/ready.d.ts +23 -2
  74. package/dist/lib/hosts/ready.js +35 -13
  75. package/dist/lib/hosts/reconcile.d.ts +53 -0
  76. package/dist/lib/hosts/reconcile.js +81 -0
  77. package/dist/lib/hosts/tasks.d.ts +8 -0
  78. package/dist/lib/hosts/tasks.js +14 -0
  79. package/dist/lib/permissions.d.ts +4 -0
  80. package/dist/lib/permissions.js +35 -0
  81. package/dist/lib/picker.d.ts +24 -0
  82. package/dist/lib/picker.js +124 -0
  83. package/dist/lib/platform/winpath.d.ts +31 -2
  84. package/dist/lib/platform/winpath.js +133 -24
  85. package/dist/lib/plugin-marketplace.d.ts +30 -0
  86. package/dist/lib/plugin-marketplace.js +215 -2
  87. package/dist/lib/plugins.d.ts +5 -0
  88. package/dist/lib/plugins.js +45 -4
  89. package/dist/lib/pwsh.d.ts +11 -0
  90. package/dist/lib/pwsh.js +13 -0
  91. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  92. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  93. package/dist/lib/secrets/agent.d.ts +42 -1
  94. package/dist/lib/secrets/agent.js +89 -11
  95. package/dist/lib/secrets/bundles.js +40 -9
  96. package/dist/lib/secrets/filestore.js +31 -1
  97. package/dist/lib/secrets/index.d.ts +33 -1
  98. package/dist/lib/secrets/index.js +90 -9
  99. package/dist/lib/secrets/windows.d.ts +74 -0
  100. package/dist/lib/secrets/windows.js +440 -0
  101. package/dist/lib/session/active.d.ts +7 -0
  102. package/dist/lib/session/active.js +11 -3
  103. package/dist/lib/session/discover.js +94 -0
  104. package/dist/lib/session/parse.d.ts +6 -0
  105. package/dist/lib/session/parse.js +283 -2
  106. package/dist/lib/session/pid-registry.d.ts +21 -0
  107. package/dist/lib/session/pid-registry.js +80 -0
  108. package/dist/lib/session/remote-active.d.ts +26 -0
  109. package/dist/lib/session/remote-active.js +141 -0
  110. package/dist/lib/session/remote.js +2 -6
  111. package/dist/lib/session/render.js +1 -1
  112. package/dist/lib/session/sync/config.d.ts +8 -0
  113. package/dist/lib/session/sync/config.js +11 -2
  114. package/dist/lib/session/types.d.ts +1 -1
  115. package/dist/lib/session/types.js +1 -1
  116. package/dist/lib/shims.d.ts +30 -2
  117. package/dist/lib/shims.js +107 -50
  118. package/dist/lib/ssh-exec.d.ts +26 -3
  119. package/dist/lib/ssh-exec.js +45 -3
  120. package/dist/lib/ssh-tunnel.d.ts +24 -5
  121. package/dist/lib/ssh-tunnel.js +60 -62
  122. package/dist/lib/staleness/writers/hooks.js +1 -1
  123. package/dist/lib/startup/command-registry.d.ts +3 -0
  124. package/dist/lib/startup/command-registry.js +6 -0
  125. package/dist/lib/sync-status.d.ts +102 -0
  126. package/dist/lib/sync-status.js +135 -0
  127. package/dist/lib/teams/agents.d.ts +24 -0
  128. package/dist/lib/teams/agents.js +30 -1
  129. package/dist/lib/teams/registry.js +25 -9
  130. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  131. package/dist/lib/terminal/backends/ghostty.js +69 -0
  132. package/dist/lib/terminal/backends/index.d.ts +18 -0
  133. package/dist/lib/terminal/backends/index.js +29 -0
  134. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  135. package/dist/lib/terminal/backends/iterm.js +62 -0
  136. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  137. package/dist/lib/terminal/backends/tmux.js +23 -0
  138. package/dist/lib/terminal/engine.d.ts +39 -0
  139. package/dist/lib/terminal/engine.js +54 -0
  140. package/dist/lib/terminal/index.d.ts +14 -0
  141. package/dist/lib/terminal/index.js +5 -0
  142. package/dist/lib/terminal/policy.d.ts +11 -0
  143. package/dist/lib/terminal/policy.js +11 -0
  144. package/dist/lib/terminal/quote.d.ts +11 -0
  145. package/dist/lib/terminal/quote.js +13 -0
  146. package/dist/lib/terminal/shell.d.ts +6 -0
  147. package/dist/lib/terminal/shell.js +23 -0
  148. package/dist/lib/terminal/transport.d.ts +15 -0
  149. package/dist/lib/terminal/transport.js +40 -0
  150. package/dist/lib/terminal/types.d.ts +59 -0
  151. package/dist/lib/terminal/types.js +13 -0
  152. package/dist/lib/types.d.ts +27 -1
  153. package/dist/lib/usage.d.ts +30 -0
  154. package/dist/lib/usage.js +159 -2
  155. package/dist/lib/versions.d.ts +7 -3
  156. package/dist/lib/versions.js +10 -5
  157. package/dist/lib/whats-new.d.ts +9 -0
  158. package/dist/lib/whats-new.js +35 -0
  159. package/package.json +1 -1
@@ -12,6 +12,8 @@ import { unifiedDiff, colorizeUnifiedDiff } from '../lib/diff-text.js';
12
12
  import { listCliStatus } from '../lib/cli-resources.js';
13
13
  import { setHelpSections } from '../lib/help.js';
14
14
  import { heal, healChangedAnything } from '../lib/heal.js';
15
+ import { blocksLocalScripts, getEffectiveExecutionPolicy } from '../lib/platform/winpath.js';
16
+ import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
15
17
  import * as fs from 'fs';
16
18
  const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
17
19
  // ─── overview mode (no target) ────────────────────────────────────────────────
@@ -28,7 +30,6 @@ function divergenceLines(report) {
28
30
  else if (p.status === 'diff')
29
31
  lines.push(`plugin ${p.name} — ${p.detail ?? 'mirror drifted'}`);
30
32
  }
31
- const counts = [];
32
33
  for (const kind of ['commands', 'skills', 'hooks', 'rules', 'mcp', 'permissions', 'subagents']) {
33
34
  const rows = report.kinds[kind];
34
35
  const miss = rows.filter((r) => r.status === 'missing').length;
@@ -39,12 +40,44 @@ function divergenceLines(report) {
39
40
  if (dif)
40
41
  bits.push(`${dif} drifted`);
41
42
  if (bits.length)
42
- counts.push(`${kind} ${bits.join('/')}`);
43
+ lines.push(`${kind.padEnd(11)} ${bits.join(' · ')}`);
43
44
  }
44
- if (counts.length)
45
- lines.push(counts.join(' · '));
46
45
  return lines;
47
46
  }
47
+ function collapseWhitespace(s) {
48
+ return s.replace(/\s+/g, ' ').trim();
49
+ }
50
+ export function wrapLine(prefix, text, width = terminalWidth()) {
51
+ const words = collapseWhitespace(text).split(' ').filter(Boolean);
52
+ if (words.length === 0)
53
+ return [prefix.trimEnd()];
54
+ const continuation = ' '.repeat(stringWidth(prefix));
55
+ const lines = [];
56
+ let linePrefix = prefix;
57
+ let line = prefix;
58
+ let hasWord = false;
59
+ for (const word of words) {
60
+ const room = Math.max(1, width - stringWidth(linePrefix));
61
+ const piece = stringWidth(word) > room ? truncateToWidth(word, room) : word;
62
+ const candidate = hasWord ? `${line} ${piece}` : `${line}${piece}`;
63
+ if (hasWord && stringWidth(candidate) > width) {
64
+ lines.push(line);
65
+ linePrefix = continuation;
66
+ line = continuation + piece;
67
+ hasWord = true;
68
+ }
69
+ else {
70
+ line = candidate;
71
+ hasWord = true;
72
+ }
73
+ }
74
+ lines.push(line);
75
+ return lines;
76
+ }
77
+ function printWrappedLine(prefix, text) {
78
+ for (const line of wrapLine(prefix, text))
79
+ console.log(chalk.gray(line));
80
+ }
48
81
  function checkSyncStatus(cwd) {
49
82
  const rows = [];
50
83
  // Every installed version, not just the default — a stale NON-default version
@@ -129,7 +162,7 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
129
162
  }
130
163
  }
131
164
  if (hidden.length > 0) {
132
- console.log(chalk.gray(` +${hidden.length} more supported (${hidden.join(', ')}) — \`agents add <name>\` to manage`));
165
+ printWrappedLine(' ', `+${hidden.length} more supported (${hidden.join(', ')}) — \`agents add <name>\` to manage`);
133
166
  }
134
167
  console.log();
135
168
  console.log(chalk.bold('Sync status (installed versions)'));
@@ -161,7 +194,7 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
161
194
  // Version homes are reconciled only by management commands, so point at one
162
195
  // rather than promising an auto-sync that never happens.
163
196
  if (anyOutOfSync) {
164
- console.log(chalk.gray(' Reconcile with `agents doctor <agent>@<version> --fix` or `agents sync <agent>@<version>` (not applied on launch).'));
197
+ printWrappedLine(' ', 'Reconcile with `agents doctor <agent>@<version> --fix` or `agents sync <agent>@<version>` (not applied on launch).');
165
198
  }
166
199
  }
167
200
  console.log();
@@ -198,16 +231,65 @@ function renderOverviewText(clis, syncRows, orphanRows, hostClis) {
198
231
  const label = manifest.name.padEnd(nameWidth);
199
232
  const src = chalk.gray(`[${manifest.source}]`);
200
233
  if (installed) {
201
- console.log(` ${chalk.green('ready')} ${label} ${src} ${chalk.gray(manifest.description || '')}`);
234
+ const prefix = ` ${chalk.green('ready')} ${label} ${src}`;
235
+ const desc = manifest.description
236
+ ? ` ${truncateToWidth(collapseWhitespace(manifest.description), Math.max(1, terminalWidth() - stringWidth(prefix) - 2))}`
237
+ : '';
238
+ console.log(prefix + chalk.gray(desc));
202
239
  }
203
240
  else {
204
- console.log(` ${chalk.red('miss ')} ${label} ${src} ${chalk.gray(`not installed — run \`agents cli install ${manifest.name}\``)}`);
241
+ const prefix = ` ${chalk.red('miss ')} ${label} ${src}`;
242
+ const msg = `not installed — run \`agents cli install ${manifest.name}\``;
243
+ const budget = Math.max(1, terminalWidth() - stringWidth(prefix) - 2);
244
+ console.log(prefix + chalk.gray(` ${truncateToWidth(msg, budget)}`));
205
245
  }
206
246
  }
207
247
  }
208
248
  for (const err of hostClis.errors) {
209
249
  console.log(` ${chalk.red('err ')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
210
250
  }
251
+ // On Windows a Restricted/AllSigned execution policy silently breaks the
252
+ // generated `agents.ps1` launcher — postinstall diagnoses it interactively,
253
+ // but a non-interactive install never sees that. Surface it here too.
254
+ renderExecPolicyAdvisory();
255
+ }
256
+ // ─── windows execution-policy advisory ─────────────────────────────────────────
257
+ /**
258
+ * Windows-only advisory lines. When the effective PowerShell execution policy
259
+ * blocks unsigned local `.ps1` scripts (`Restricted`/`AllSigned`), the generated
260
+ * `agents.ps1` launcher fails in PowerShell even when it is on PATH. Surface the
261
+ * remediation; the `.cmd` companion still works, so this is a warning, not an
262
+ * error, and doctor never auto-changes the policy. Pure — returns `[]` on
263
+ * non-Windows or a permissive policy, so it is testable without invoking
264
+ * PowerShell.
265
+ */
266
+ export function execPolicyWarningLines(platform, policy) {
267
+ if (platform !== 'win32')
268
+ return [];
269
+ if (!blocksLocalScripts(policy))
270
+ return [];
271
+ return [
272
+ `PowerShell execution policy is ${policy} — it blocks the generated agents.ps1 launcher.`,
273
+ 'Fix: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned',
274
+ 'The agents.cmd shim still works regardless of the policy.',
275
+ ];
276
+ }
277
+ function renderExecPolicyAdvisory() {
278
+ // Only probe the policy on Windows — getEffectiveExecutionPolicy() spawns
279
+ // powershell, which is a wasted (doomed) process on POSIX where the advisory
280
+ // never applies.
281
+ if (process.platform !== 'win32')
282
+ return;
283
+ const lines = execPolicyWarningLines(process.platform, getEffectiveExecutionPolicy());
284
+ if (lines.length === 0)
285
+ return;
286
+ console.log();
287
+ console.log(chalk.bold('Execution policy (Windows)'));
288
+ const [headline, ...rest] = lines;
289
+ console.log(` ${chalk.yellow('warn ')} ${headline}`);
290
+ for (const line of rest) {
291
+ console.log(chalk.gray(` ${line}`));
292
+ }
211
293
  }
212
294
  function parseTargetArg(arg) {
213
295
  const at = arg.indexOf('@');
@@ -314,8 +396,12 @@ function renderKindSection(kind, rows, layers, options) {
314
396
  }
315
397
  for (const r of visible) {
316
398
  const src = sourceLabel(r, layers);
317
- const detail = r.detail ? chalk.gray(` ${r.detail}`) : '';
318
- console.log(` ${statusLabel(r.status)} ${r.name.padEnd(28)} ${src}${detail}`);
399
+ const name = padToWidth(truncateToWidth(r.name, 28), 28);
400
+ const prefix = ` ${statusLabel(r.status)} ${name} ${src}`;
401
+ const detail = r.detail
402
+ ? chalk.gray(` ${truncateToWidth(collapseWhitespace(r.detail), Math.max(1, terminalWidth() - stringWidth(prefix) - 2))}`)
403
+ : '';
404
+ console.log(prefix + detail);
319
405
  if (options.showDiff && r.status === 'diff' && r.sourcePath && r.homePath) {
320
406
  const expected = readExpectedForDiff(kind, r);
321
407
  const actual = safeRead(r.homePath);
@@ -352,8 +438,10 @@ function readExpectedForDiff(kind, row) {
352
438
  function renderTargetText(report, options) {
353
439
  const label = `${AGENT_NAMES[report.agent] || report.agent}@${report.version}`;
354
440
  console.log(chalk.bold(label));
355
- console.log(chalk.gray(` home: ${report.home}`));
356
- console.log(chalk.gray(` cwd: ${report.cwd}`));
441
+ const homePrefix = ' home: ';
442
+ const cwdPrefix = ' cwd: ';
443
+ console.log(chalk.gray(homePrefix + truncateToWidth(report.home, Math.max(1, terminalWidth() - stringWidth(homePrefix)))));
444
+ console.log(chalk.gray(cwdPrefix + truncateToWidth(report.cwd, Math.max(1, terminalWidth() - stringWidth(cwdPrefix)))));
357
445
  const layerStr = [
358
446
  report.layers.project ? `project=${report.layers.project}` : null,
359
447
  `user=${report.layers.user}`,
@@ -362,7 +450,7 @@ function renderTargetText(report, options) {
362
450
  ? `extras=[${report.layers.extras.map((e) => e.alias).join(',')}]`
363
451
  : null,
364
452
  ].filter(Boolean).join(' ');
365
- console.log(chalk.gray(` layers: ${layerStr}`));
453
+ printWrappedLine(' layers: ', layerStr);
366
454
  // Staleness manifest verdict — single-line summary from the staleness
367
455
  // library, sitting alongside the detailed per-resource diff below.
368
456
  const manifest = loadManifest(report.agent, report.version);
@@ -403,7 +491,7 @@ function renderTargetText(report, options) {
403
491
  }
404
492
  else {
405
493
  console.log(` Verdict: ${verdictParts.join(', ')}.`);
406
- console.log(chalk.gray(` Run \`agents doctor ${report.agent}@${report.version} --fix\` to heal, or \`agents prune cleanup\` to drop extras.`));
494
+ printWrappedLine(' ', `Run \`agents doctor ${report.agent}@${report.version} --fix\` to heal, or \`agents prune cleanup\` to drop extras.`);
407
495
  }
408
496
  }
409
497
  // ─── fix / heal mode ───────────────────────────────────────────────────────────
@@ -547,6 +635,12 @@ export function registerDoctorCommand(program) {
547
635
  return;
548
636
  }
549
637
  renderOverviewText(clis, syncRows, orphanRows, hostClis);
638
+ // Point at the interactive reconcile when anything is out of sync — the
639
+ // report shouldn't be a dead end. `agents status` runs the unified
640
+ // home-reading engine and offers to sync (opt-in, never auto-fires here).
641
+ if (syncRows.some((r) => r.status !== 'fresh')) {
642
+ console.log(chalk.gray('\nRun `agents status` to review and sync what has drifted.'));
643
+ }
550
644
  return;
551
645
  }
552
646
  const parsed = parseTargetArg(target);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `agents events` — read the structured audit/event log.
3
+ *
4
+ * The event log (`~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl`) records every
5
+ * `agents <module> <cmd>` invocation plus richer typed events (secrets access,
6
+ * version installs, ...), each stamped with who ran it and from where (OS user,
7
+ * local vs SSH, remote client IP). This command reads it back — the audit trail
8
+ * for "who accessed a secret / created a team / started an agent, and from which
9
+ * host?".
10
+ *
11
+ * Filter by `--module` (top-level group, e.g. teams), `--command` (path prefix,
12
+ * e.g. "teams create"), `--event` (typed event), `--agent`, and `--since`.
13
+ * `--follow` tails today's log live.
14
+ */
15
+ import type { Command } from 'commander';
16
+ export declare function registerEventsCommand(program: Command): void;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * `agents events` — read the structured audit/event log.
3
+ *
4
+ * The event log (`~/.agents/.cache/logs/events-YYYY-MM-DD.jsonl`) records every
5
+ * `agents <module> <cmd>` invocation plus richer typed events (secrets access,
6
+ * version installs, ...), each stamped with who ran it and from where (OS user,
7
+ * local vs SSH, remote client IP). This command reads it back — the audit trail
8
+ * for "who accessed a secret / created a team / started an agent, and from which
9
+ * host?".
10
+ *
11
+ * Filter by `--module` (top-level group, e.g. teams), `--command` (path prefix,
12
+ * e.g. "teams create"), `--event` (typed event), `--agent`, and `--since`.
13
+ * `--follow` tails today's log live.
14
+ */
15
+ import chalk from 'chalk';
16
+ import * as fs from 'fs';
17
+ import { query, LOGS_PATH } from '../lib/events.js';
18
+ /** Parse `--since`: relative offsets (30s/5m/2h/7d/4w) or an ISO/absolute date. */
19
+ function parseSince(s) {
20
+ const m = s.match(/^(\d+)([smhdw])$/);
21
+ if (m) {
22
+ const n = parseInt(m[1], 10);
23
+ const unitMs = {
24
+ s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000,
25
+ };
26
+ return new Date(Date.now() - n * unitMs[m[2]]);
27
+ }
28
+ const ms = Date.parse(s);
29
+ if (isNaN(ms))
30
+ throw new Error(`Invalid --since value: ${s} (use e.g. 2h, 7d, or an ISO date)`);
31
+ return new Date(ms);
32
+ }
33
+ /** How the run reached this host — 'local' or 'ssh 203.0.113.7'. */
34
+ function originLabel(r) {
35
+ if (r.transport === 'ssh') {
36
+ return chalk.yellow(`ssh${r.sshClientIp ? ' ' + r.sshClientIp : ''}`);
37
+ }
38
+ return chalk.gray('local');
39
+ }
40
+ /** The most useful one-line detail for a record, by event family. */
41
+ function detailFor(r) {
42
+ if (r.command)
43
+ return r.command;
44
+ const bits = [];
45
+ if (typeof r.team === 'string')
46
+ bits.push(`team=${r.team}`);
47
+ if (typeof r.bundle === 'string')
48
+ bits.push(`bundle=${r.bundle}`);
49
+ if (typeof r.skill === 'string')
50
+ bits.push(`skill=${r.skill}`);
51
+ if (typeof r.version === 'string')
52
+ bits.push(`v=${r.version}`);
53
+ if (typeof r.profile === 'string')
54
+ bits.push(`profile=${r.profile}`);
55
+ if (typeof r.error === 'string')
56
+ bits.push(chalk.red(r.error));
57
+ return bits.join(' ');
58
+ }
59
+ function renderRow(r) {
60
+ const time = chalk.gray(r.ts.slice(0, 19).replace('T', ' '));
61
+ const user = `${r.osUser ?? '?'}@${r.hostname}`;
62
+ const ev = r.event.startsWith('error') ? chalk.red(r.event) : chalk.cyan(r.event);
63
+ const agent = r.agent ? chalk.gray(` ${r.agent}`) : '';
64
+ return `${time} ${originLabel(r).padEnd(24)} ${user.padEnd(22)} ${ev.padEnd(26)}${agent} ${detailFor(r)}`;
65
+ }
66
+ export function registerEventsCommand(program) {
67
+ program
68
+ .command('events')
69
+ .description('Read the structured audit/event log (who ran what, from where)')
70
+ .option('--module <name>', 'Only events from this command group (e.g. teams, secrets)')
71
+ .option('--command <path>', 'Only this command path — prefix match (e.g. "teams create")')
72
+ .option('--event <type>', 'Only this typed event (repeatable, e.g. secrets.get)', collect, [])
73
+ .option('--agent <name>', 'Only events tagged with this agent')
74
+ .option('--since <time>', 'Only events newer than this (e.g. 2h, 7d, or ISO date)')
75
+ .option('--limit <n>', 'Max records to show (default 50)', '50')
76
+ .option('--json', 'Output raw records as JSON')
77
+ .option('-f, --follow', "Tail today's log live")
78
+ .addHelpText('after', `
79
+ Examples:
80
+ agents events Recent activity across everything
81
+ agents events --module teams Team lifecycle (create / add / disband)
82
+ agents events --module secrets Every secret accessed or revealed
83
+ agents events --command "teams create" Just team creations
84
+ agents events --event secrets.get --since 7d --json
85
+ agents events -f Live tail`)
86
+ .action(async (options) => {
87
+ if (options.follow) {
88
+ await followLog();
89
+ return;
90
+ }
91
+ const limit = Math.max(1, parseInt(options.limit ?? '50', 10) || 50);
92
+ let startDate;
93
+ try {
94
+ startDate = options.since ? parseSince(options.since) : undefined;
95
+ }
96
+ catch (err) {
97
+ console.error(chalk.red(err.message));
98
+ process.exit(2);
99
+ }
100
+ const records = query({
101
+ startDate,
102
+ eventTypes: options.event && options.event.length ? options.event : undefined,
103
+ agent: options.agent,
104
+ command: options.command,
105
+ module: options.module,
106
+ limit,
107
+ });
108
+ if (options.json) {
109
+ console.log(JSON.stringify(records, null, 2));
110
+ return;
111
+ }
112
+ if (records.length === 0) {
113
+ console.log(chalk.gray('No matching events.'));
114
+ return;
115
+ }
116
+ // query() returns newest-first; print oldest-first so a tail reads naturally.
117
+ for (const r of records.slice().reverse())
118
+ console.log(renderRow(r));
119
+ console.log(chalk.gray(`\n${records.length} event(s). Log: ${LOGS_PATH}`));
120
+ });
121
+ }
122
+ /** commander repeatable-option collector. */
123
+ function collect(value, previous) {
124
+ return previous.concat([value]);
125
+ }
126
+ /** Tail today's event file, printing new lines as they land. */
127
+ async function followLog() {
128
+ const today = new Date();
129
+ const file = `${LOGS_PATH}/events-${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}.jsonl`;
130
+ let offset = 0;
131
+ try {
132
+ offset = fs.statSync(file).size;
133
+ }
134
+ catch {
135
+ // File may not exist yet — start at 0 and pick it up on first write.
136
+ }
137
+ console.log(chalk.gray(`Tailing ${file} — Ctrl-C to stop`));
138
+ const drain = () => {
139
+ let size = 0;
140
+ try {
141
+ size = fs.statSync(file).size;
142
+ }
143
+ catch {
144
+ return;
145
+ }
146
+ if (size <= offset) {
147
+ if (size < offset)
148
+ offset = 0; // rotated/truncated
149
+ return;
150
+ }
151
+ const fd = fs.openSync(file, 'r');
152
+ try {
153
+ const buf = Buffer.alloc(size - offset);
154
+ fs.readSync(fd, buf, 0, buf.length, offset);
155
+ offset = size;
156
+ for (const line of buf.toString('utf-8').split('\n').filter(Boolean)) {
157
+ try {
158
+ console.log(renderRow(JSON.parse(line)));
159
+ }
160
+ catch {
161
+ // Skip malformed lines.
162
+ }
163
+ }
164
+ }
165
+ finally {
166
+ fs.closeSync(fd);
167
+ }
168
+ };
169
+ // Poll — simpler and more portable than fs.watch across platforms/editors.
170
+ await new Promise(() => {
171
+ setInterval(drain, 500);
172
+ });
173
+ }
@@ -294,7 +294,12 @@ export function registerRunCommand(program) {
294
294
  console.log(chalk.green(`Dispatched to ${host.name}.`) + chalk.gray(' Track: agents hosts ps · Follow: agents hosts logs <id> -f'));
295
295
  process.exit(0);
296
296
  }
297
- process.exit(exitCode === undefined || exitCode === -1 ? 1 : exitCode);
297
+ // -1 = the follow window closed but the run continues on the host (the
298
+ // reattach hint is already printed). That's a detach, not a failure —
299
+ // exit 0. Any real remote code passes through.
300
+ if (exitCode === -1)
301
+ process.exit(0);
302
+ process.exit(exitCode ?? 1);
298
303
  }
299
304
  catch (err) {
300
305
  console.error(chalk.red(err.message));
@@ -7,15 +7,16 @@
7
7
  * hosts. Dispatch itself is `agents run --host <name>` (see commands/exec.ts).
8
8
  */
9
9
  import chalk from 'chalk';
10
+ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
10
11
  import { checkbox, confirm } from '@inquirer/prompts';
11
12
  import { assertValidSshTarget } from '../lib/ssh-exec.js';
12
13
  import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js';
13
14
  import { sshTargetFor } from '../lib/hosts/types.js';
14
15
  import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
15
16
  import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
16
- import { listTasks, loadTask, localLogPath } from '../lib/hosts/tasks.js';
17
- import { followHostTask } from '../lib/hosts/progress.js';
18
- import * as fs from 'fs';
17
+ import { listTasks } from '../lib/hosts/tasks.js';
18
+ import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
19
+ import { showHostTaskLog } from '../lib/hosts/logs.js';
19
20
  /** Parse `user@host` or `host` into its pieces. */
20
21
  function parseTarget(target) {
21
22
  const at = target.indexOf('@');
@@ -23,9 +24,12 @@ function parseTarget(target) {
23
24
  return { address: target };
24
25
  return { user: target.slice(0, at), address: target.slice(at + 1) };
25
26
  }
26
- /** Bootstrap/verify agents-cli on a freshly-enrolled host (best-effort, prompts). */
27
- async function maybeBootstrap(target, hostName) {
28
- const probe = probeHost(target);
27
+ /**
28
+ * Bootstrap/verify agents-cli on a freshly-enrolled host (best-effort, prompts).
29
+ * Callers that just probed the host pass the result through to avoid a second,
30
+ * redundant `uname` round-trip; otherwise we probe here.
31
+ */
32
+ async function maybeBootstrap(target, hostName, probe = probeHost(target)) {
29
33
  if (!probe.reachable) {
30
34
  console.log(chalk.yellow(` Not reachable over SSH yet — skipping bootstrap. Fix key auth, then: agents hosts check ${hostName}`));
31
35
  return;
@@ -75,7 +79,7 @@ async function doAdd(name, target, opts) {
75
79
  await registerHost({ name: c, provider: 'local', source, ...(source === 'inline' ? { address: c } : {}), os: probe.os, caps: opts.cap });
76
80
  console.log(chalk.green(`Enrolled ${c}`) + chalk.gray(` (${source}${probe.os ? `, ${probe.os}` : ''})`));
77
81
  if (opts.enroll !== false)
78
- await maybeBootstrap(c, c);
82
+ await maybeBootstrap(c, c, probe);
79
83
  }
80
84
  return;
81
85
  }
@@ -107,7 +111,7 @@ async function doAdd(name, target, opts) {
107
111
  await registerHost(spec);
108
112
  console.log(chalk.green(`Enrolled ${name}`) + chalk.gray(` (${spec.source}${spec.os ? `, ${spec.os}` : ''}${spec.caps?.length ? `, caps: ${spec.caps.join(',')}` : ''})`));
109
113
  if (opts.enroll !== false)
110
- await maybeBootstrap(sshTarget, name);
114
+ await maybeBootstrap(sshTarget, name, probe);
111
115
  }
112
116
  async function doList(json) {
113
117
  const hosts = await listAllHosts();
@@ -122,8 +126,11 @@ async function doList(json) {
122
126
  console.log(chalk.bold('NAME').padEnd(20) + chalk.bold('SOURCE').padEnd(13) + chalk.bold('TARGET').padEnd(28) + chalk.bold('CAPS'));
123
127
  for (const h of hosts) {
124
128
  const tgt = h.source === 'ssh-config' ? chalk.gray('(ssh-config)') : `${h.user ? h.user + '@' : ''}${h.address ?? ''}`;
129
+ // Cap TARGET at its column so a long user@host can't shove CAPS out of alignment.
130
+ const tgtW = stringWidth(tgt);
131
+ const tgtCol = tgtW > 28 ? truncateToWidth(tgt, 28) : tgt + ' '.repeat(28 - tgtW);
125
132
  const mark = h.enrolled ? '' : chalk.gray(' ·available');
126
- console.log(h.name.padEnd(20) + h.source.padEnd(13) + tgt.padEnd(28) + (h.caps?.join(',') ?? '') + mark);
133
+ console.log(h.name.padEnd(20) + h.source.padEnd(13) + tgtCol + (h.caps?.join(',') ?? '') + mark);
127
134
  }
128
135
  }
129
136
  async function doCheck(name) {
@@ -155,7 +162,10 @@ async function doRemove(name) {
155
162
  console.log(chalk.green(`Removed ${name}`));
156
163
  }
157
164
  async function doPs(json) {
158
- const tasks = listTasks();
165
+ // Heal any 'running' record whose local follower died (dropped connection,
166
+ // laptop sleep) against the remote `.exit` before listing, so `ps` never shows
167
+ // a finished run stuck at 'running'.
168
+ const tasks = reconcileRunningTasks(listTasks());
159
169
  if (json) {
160
170
  console.log(JSON.stringify(tasks, null, 2));
161
171
  return;
@@ -164,30 +174,24 @@ async function doPs(json) {
164
174
  console.log(chalk.gray('No host tasks yet. Dispatch one: agents run <agent> "<task>" --host <name>'));
165
175
  return;
166
176
  }
177
+ const cols = terminalWidth();
167
178
  console.log(chalk.bold('ID').padEnd(11) + chalk.bold('HOST').padEnd(16) + chalk.bold('AGENT').padEnd(10) + chalk.bold('STATUS').padEnd(11) + chalk.bold('PROMPT'));
168
179
  for (const t of tasks) {
169
180
  const status = t.status === 'completed' ? chalk.green(t.status) : t.status === 'failed' ? chalk.red(t.status) : chalk.yellow(t.status);
170
- console.log(t.id.padEnd(11) + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + t.prompt.slice(0, 50));
181
+ // Prompt fills the remaining width instead of a fixed 50-char byte slice (98-char rows).
182
+ const promptCol = truncateToWidth(t.prompt, Math.max(12, cols - (11 + 16 + 10 + 11)));
183
+ console.log(t.id.padEnd(11) + t.host.padEnd(16) + t.agent.padEnd(10) + status.padEnd(11) + promptCol);
171
184
  }
172
185
  }
173
186
  async function doLogs(id, follow) {
174
- const task = loadTask(id);
175
- if (!task) {
187
+ const res = await showHostTaskLog(id, follow);
188
+ if (!res.found) {
176
189
  console.log(chalk.red(`Unknown task "${id}".`));
177
190
  process.exitCode = 1;
178
191
  return;
179
192
  }
180
- if (follow && task.status === 'running') {
181
- const code = await followHostTask(task.target, { remoteLog: task.remoteLog, remoteExit: task.remoteExit, taskId: id, echo: true });
182
- process.exitCode = code === -1 ? 1 : code;
183
- return;
184
- }
185
- try {
186
- process.stdout.write(fs.readFileSync(localLogPath(id), 'utf-8'));
187
- }
188
- catch {
189
- console.log(chalk.gray('(no local log captured for this task)'));
190
- }
193
+ if (res.exitCode !== undefined)
194
+ process.exitCode = res.exitCode;
191
195
  }
192
196
  /** Register the `agents hosts` command tree. */
193
197
  export function registerHostsCommand(program) {
@@ -263,7 +263,7 @@ When to use:
263
263
  pointing at the existing install — nothing is copied or moved (except the
264
264
  agent's config dir, which is moved into the version's home). Works for both
265
265
  npm-style packages (claude, codex, gemini, opencode, openclaw) and
266
- installScript-based agents (grok, antigravity, cursor, kiro, goose, roo).
266
+ installScript-based agents (grok, antigravity, cursor, kiro, goose).
267
267
  `)
268
268
  .action(runImport);
269
269
  }
@@ -18,6 +18,8 @@ import { type PluginResourceGroup } from '../lib/plugins.js';
18
18
  /** Resource kinds the inspect command can drill into. */
19
19
  declare const DRILLABLE_KINDS: readonly ["commands", "skills", "hooks", "mcp", "rules", "plugins", "workflows", "subagents"];
20
20
  type DrillableKind = typeof DRILLABLE_KINDS[number];
21
+ /** Wrap a separator-joined value list under `prefix`, preserving a hanging indent. */
22
+ export declare function wrapJoined(prefix: string, items: string[], sep: string, width: number): string[];
21
23
  interface ResourceItem {
22
24
  name: string;
23
25
  source: string;
@@ -31,6 +31,7 @@ import { discoverPlugins, discoverPluginsInDir, pluginResourceGroups } from '../
31
31
  import { PLUGIN_GROUP_COLORS } from './plugins.js';
32
32
  import { countSessionsInScope } from '../lib/session/discover.js';
33
33
  import { damerauLevenshtein } from '../lib/fuzzy.js';
34
+ import { terminalWidth, truncateToWidth, stringWidth, stripAnsi } from '../lib/session/width.js';
34
35
  /** Resource kinds the inspect command can drill into. */
35
36
  const DRILLABLE_KINDS = [
36
37
  'commands',
@@ -68,6 +69,44 @@ const SINGULAR_DRILL_ALIASES = {
68
69
  const CAPABILITY_NAMES = [
69
70
  'hooks', 'mcp', 'skills', 'commands', 'subagents', 'plugins', 'workflows', 'rules', 'allowlist',
70
71
  ];
72
+ /** Wrap a separator-joined value list under `prefix`, preserving a hanging indent. */
73
+ export function wrapJoined(prefix, items, sep, width) {
74
+ if (items.length === 0)
75
+ return [];
76
+ const continuation = ' '.repeat(stringWidth(prefix));
77
+ const fitItem = (item, linePrefix) => {
78
+ const room = Math.max(1, width - stringWidth(linePrefix));
79
+ return stringWidth(item) > room ? truncateToWidth(item, room) : item;
80
+ };
81
+ const lines = [];
82
+ let linePrefix = prefix;
83
+ let line = prefix;
84
+ let hasItem = false;
85
+ for (const raw of items) {
86
+ const item = fitItem(raw, linePrefix);
87
+ const candidate = hasItem ? `${line}${sep}${item}` : `${line}${item}`;
88
+ if (hasItem && stringWidth(candidate) > width) {
89
+ lines.push(line);
90
+ linePrefix = continuation;
91
+ line = continuation + fitItem(raw, linePrefix);
92
+ hasItem = true;
93
+ }
94
+ else {
95
+ line = candidate;
96
+ hasItem = true;
97
+ }
98
+ }
99
+ if (hasItem)
100
+ lines.push(line);
101
+ return lines;
102
+ }
103
+ function printWrappedJoined(prefix, items, sep) {
104
+ for (const line of wrapJoined(prefix, items, sep, terminalWidth()))
105
+ console.log(line);
106
+ }
107
+ function truncateValueForPrefix(prefix, value) {
108
+ return truncateToWidth(stripAnsi(value), Math.max(1, terminalWidth() - stringWidth(prefix)));
109
+ }
71
110
  // ─── Command registration ────────────────────────────────────────────────────
72
111
  export function registerInspectCommand(program) {
73
112
  const cmd = addHostOption(program.command('inspect <target>'))
@@ -425,7 +464,10 @@ function renderRepoSummary(repo, options) {
425
464
  console.log(` ${'git'.padEnd(10)} ${git.branch}${dirty}${url}`);
426
465
  if (git.lastCommit) {
427
466
  const rel = git.lastCommit.relative ? ` ${chalk.gray(`(${git.lastCommit.relative})`)}` : '';
428
- sub('last', `${chalk.cyan(git.lastCommit.sha)} ${truncate(git.lastCommit.subject, 60)}${rel}`);
467
+ const prefix = ` ${''.padEnd(10)} ${chalk.gray('last'.padEnd(8))} `;
468
+ const sha = chalk.cyan(git.lastCommit.sha);
469
+ const subjectWidth = Math.max(8, terminalWidth() - stringWidth(prefix) - stringWidth(sha) - 2 - stringWidth(rel));
470
+ console.log(`${prefix}${sha} ${truncateToWidth(git.lastCommit.subject, subjectWidth)}${rel}`);
429
471
  }
430
472
  if (git.ahead !== null && git.behind !== null && (git.ahead > 0 || git.behind > 0)) {
431
473
  sub('sync', `ahead ${git.ahead} ${chalk.gray('·')} behind ${git.behind}`);
@@ -440,10 +482,12 @@ function renderRepoSummary(repo, options) {
440
482
  console.log(` ${'manifests'.padEnd(10)} ${manifests.join(', ')}`);
441
483
  if (manifest) {
442
484
  if (manifest.versions.length > 0) {
443
- sub('versions', manifest.versions.map(v => `${v.agent} ${chalk.cyan(v.version)}`).join(chalk.gray(' · ')));
485
+ const prefix = ` ${''.padEnd(10)} ${chalk.gray('versions'.padEnd(8))} `;
486
+ printWrappedJoined(prefix, manifest.versions.map(v => `${v.agent} ${chalk.cyan(v.version)}`), chalk.gray(' · '));
444
487
  }
445
488
  if (manifest.strategies.length > 0) {
446
- sub('run', manifest.strategies.map(s => `${s.agent}:${s.strategy}`).join(chalk.gray(' · ')));
489
+ const prefix = ` ${''.padEnd(10)} ${chalk.gray('run'.padEnd(8))} `;
490
+ printWrappedJoined(prefix, manifest.strategies.map(s => `${s.agent}:${s.strategy}`), chalk.gray(' · '));
447
491
  }
448
492
  }
449
493
  }
@@ -551,8 +595,10 @@ async function renderSummary(agent, version, versionHome, options) {
551
595
  ['alias', aliasPath],
552
596
  ['strategy', strategy],
553
597
  ];
554
- for (const [k, v] of rows)
555
- console.log(` ${k.padEnd(10)} ${v}`);
598
+ for (const [k, v] of rows) {
599
+ const prefix = ` ${k.padEnd(10)} `;
600
+ console.log(prefix + truncateValueForPrefix(prefix, v));
601
+ }
556
602
  console.log('\n' + chalk.bold('Capabilities'));
557
603
  for (const cap of CAPABILITY_NAMES) {
558
604
  const res = capabilities[cap];
@@ -603,7 +649,8 @@ function renderItemList(header, jsonHead, kind, items, options) {
603
649
  const tag = chalk.gray(`[${item.source}]`.padEnd(10));
604
650
  console.log(` ${tag} ${termLink(chalk.cyan(item.name), item.linkTarget)}`);
605
651
  if (item.description) {
606
- console.log(` ${chalk.gray(truncate(item.description, 90))}`);
652
+ const prefix = ' ';
653
+ console.log(prefix + chalk.gray(truncateToWidth(item.description, Math.max(1, terminalWidth() - stringWidth(prefix)))));
607
654
  }
608
655
  if (item.groups)
609
656
  printGroupRows(item.groups);
@@ -1171,7 +1218,7 @@ function readFirstProseLine(p) {
1171
1218
  }
1172
1219
  // ─── OSC-8 + path helpers ────────────────────────────────────────────────────
1173
1220
  function termLink(text, filePath) {
1174
- if (!filePath)
1221
+ if (!filePath || !process.stdout.isTTY)
1175
1222
  return text;
1176
1223
  const url = `file://${filePath}`;
1177
1224
  return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;