@phnx-labs/agents-cli 1.20.34 → 1.20.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -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;
@@ -11,7 +11,7 @@
11
11
  * names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
12
12
  * AGENT.md / the file itself) so users can click straight to the source.
13
13
  */
14
- import { execSync } from 'child_process';
14
+ import { execFileSync } from 'child_process';
15
15
  import { addHostOption } from '../lib/hosts/option.js';
16
16
  import * as fs from 'fs';
17
17
  import * as os from 'os';
@@ -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
  }
@@ -468,35 +512,34 @@ function renderRepoSummary(repo, options) {
468
512
  export function repoGitInfo(root) {
469
513
  const git = (args) => {
470
514
  try {
471
- return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
472
- .toString().trim();
515
+ return execFileSync('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
473
516
  }
474
517
  catch {
475
518
  return null;
476
519
  }
477
520
  };
478
- const branch = git('rev-parse --abbrev-ref HEAD');
521
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD']);
479
522
  if (branch === null)
480
523
  return null;
481
- // Read status WITHOUT trimming — git()'s .trim() would strip the leading
524
+ // Read status WITHOUT trimming — trimming would strip the leading
482
525
  // space of the first porcelain line (`XY path`), corrupting the path slice.
483
526
  let statusRaw;
484
527
  try {
485
- statusRaw = execSync(`git -C ${JSON.stringify(root)} status --porcelain`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
528
+ statusRaw = execFileSync('git', ['-C', root, 'status', '--porcelain'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
486
529
  }
487
530
  catch {
488
531
  statusRaw = null;
489
532
  }
490
533
  const dirtyFiles = statusRaw ? statusRaw.split('\n').filter(Boolean).map(l => l.slice(3)) : [];
491
534
  let lastCommit = null;
492
- const log = git('log -1 --format=%h%x1f%s%x1f%cr');
535
+ const log = git(['log', '-1', '--format=%h%x1f%s%x1f%cr']);
493
536
  if (log) {
494
537
  const [sha, subject, relative] = log.split('\x1f');
495
538
  if (sha)
496
539
  lastCommit = { sha, subject: subject ?? '', relative: relative ?? '' };
497
540
  }
498
541
  let ahead = null, behind = null;
499
- const counts = git("rev-list --left-right --count '@{upstream}...HEAD'");
542
+ const counts = git(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
500
543
  if (counts) {
501
544
  const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
502
545
  if (Number.isFinite(b) && Number.isFinite(a)) {
@@ -504,7 +547,7 @@ export function repoGitInfo(root) {
504
547
  ahead = a;
505
548
  }
506
549
  }
507
- return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git('remote get-url origin'), lastCommit, ahead, behind };
550
+ return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git(['remote', 'get-url', 'origin']), lastCommit, ahead, behind };
508
551
  }
509
552
  // ─── Summary mode ────────────────────────────────────────────────────────────
510
553
  async function renderSummary(agent, version, versionHome, options) {
@@ -551,8 +594,10 @@ async function renderSummary(agent, version, versionHome, options) {
551
594
  ['alias', aliasPath],
552
595
  ['strategy', strategy],
553
596
  ];
554
- for (const [k, v] of rows)
555
- console.log(` ${k.padEnd(10)} ${v}`);
597
+ for (const [k, v] of rows) {
598
+ const prefix = ` ${k.padEnd(10)} `;
599
+ console.log(prefix + truncateValueForPrefix(prefix, v));
600
+ }
556
601
  console.log('\n' + chalk.bold('Capabilities'));
557
602
  for (const cap of CAPABILITY_NAMES) {
558
603
  const res = capabilities[cap];
@@ -603,7 +648,8 @@ function renderItemList(header, jsonHead, kind, items, options) {
603
648
  const tag = chalk.gray(`[${item.source}]`.padEnd(10));
604
649
  console.log(` ${tag} ${termLink(chalk.cyan(item.name), item.linkTarget)}`);
605
650
  if (item.description) {
606
- console.log(` ${chalk.gray(truncate(item.description, 90))}`);
651
+ const prefix = ' ';
652
+ console.log(prefix + chalk.gray(truncateToWidth(item.description, Math.max(1, terminalWidth() - stringWidth(prefix)))));
607
653
  }
608
654
  if (item.groups)
609
655
  printGroupRows(item.groups);
@@ -1171,7 +1217,7 @@ function readFirstProseLine(p) {
1171
1217
  }
1172
1218
  // ─── OSC-8 + path helpers ────────────────────────────────────────────────────
1173
1219
  function termLink(text, filePath) {
1174
- if (!filePath)
1220
+ if (!filePath || !process.stdout.isTTY)
1175
1221
  return text;
1176
1222
  const url = `file://${filePath}`;
1177
1223
  return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `agents lock` — write a deterministic `agents.lock` for the resolved resource
3
+ * set at the project root; `agents lock --frozen` verifies the live resources
4
+ * match that lock EXACTLY and fails closed (non-zero exit + a clear diff) on any
5
+ * drift. The reproducible-CI slice of governance #337.
6
+ *
7
+ * This is OFF the default path: normal `agents add` / `agents sync` never touch
8
+ * the lock. Generation and verification only run when this command is invoked.
9
+ */
10
+ import { Command } from 'commander';
11
+ /** Register the `agents lock` command. */
12
+ export declare function registerLockCommand(program: Command): void;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * `agents lock` — write a deterministic `agents.lock` for the resolved resource
3
+ * set at the project root; `agents lock --frozen` verifies the live resources
4
+ * match that lock EXACTLY and fails closed (non-zero exit + a clear diff) on any
5
+ * drift. The reproducible-CI slice of governance #337.
6
+ *
7
+ * This is OFF the default path: normal `agents add` / `agents sync` never touch
8
+ * the lock. Generation and verification only run when this command is invoked.
9
+ */
10
+ import chalk from 'chalk';
11
+ import { enumerateLockSources, buildLock, writeLock, readLock, verifyLock, lockDiffIsClean, resolveProjectRoot, LOCK_FILENAME, } from '../lib/lock.js';
12
+ /** Register the `agents lock` command. */
13
+ export function registerLockCommand(program) {
14
+ program
15
+ .command('lock')
16
+ .summary('Write or verify agents.lock — a SHA-256 manifest of resolved resources')
17
+ .description('Capture a deterministic SHA-256 per resolved resource file (commands, skills, hooks, rules, mcp, permissions, subagents — project > user > system > extras) into an agents.lock at the project root.\n\n' +
18
+ 'Run bare to (re)generate the lock. Pass --frozen to VERIFY the live resolved resources match an existing agents.lock exactly — it exits non-zero with an added/removed/changed diff on any mismatch, and errors if no lock exists yet. That is the reproducible-CI gate; normal installs and syncs are unaffected.')
19
+ .option('--cwd <path>', 'Working directory to resolve resources and the project root from')
20
+ .option('--frozen', 'Verify resolved resources match agents.lock exactly; fail (non-zero) on any drift', false)
21
+ .action((opts) => {
22
+ runLock(opts);
23
+ });
24
+ }
25
+ function runLock(opts) {
26
+ const cwd = opts.cwd || process.cwd();
27
+ const projectRoot = resolveProjectRoot(cwd);
28
+ const sources = enumerateLockSources(cwd);
29
+ if (opts.frozen) {
30
+ verifyFrozen(projectRoot, sources);
31
+ return;
32
+ }
33
+ const lock = buildLock(sources);
34
+ const written = writeLock(projectRoot, lock);
35
+ const count = Object.keys(lock.resources).length;
36
+ console.log(chalk.green(`Wrote ${LOCK_FILENAME}`) +
37
+ chalk.gray(` — ${count} resource file(s) → ${written}`));
38
+ }
39
+ function verifyFrozen(projectRoot, sources) {
40
+ let existing;
41
+ try {
42
+ existing = readLock(projectRoot);
43
+ }
44
+ catch (e) {
45
+ console.error(chalk.red(`agents lock --frozen: ${e.message}`));
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ if (!existing) {
50
+ console.error(chalk.red(`No ${LOCK_FILENAME} at ${projectRoot}.`));
51
+ console.error(chalk.gray('Generate one first: agents lock'));
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+ const diff = verifyLock(existing, sources);
56
+ if (lockDiffIsClean(diff)) {
57
+ const count = Object.keys(existing.resources).length;
58
+ console.log(chalk.green(`✓ ${count} resource file(s) match ${LOCK_FILENAME}`));
59
+ return;
60
+ }
61
+ console.error(chalk.red(`✗ resources drifted from ${LOCK_FILENAME}:`));
62
+ for (const key of diff.changed)
63
+ console.error(chalk.yellow(` changed ${key}`));
64
+ for (const key of diff.added)
65
+ console.error(chalk.yellow(` added ${key}`));
66
+ for (const key of diff.removed)
67
+ console.error(chalk.yellow(` removed ${key}`));
68
+ console.error(chalk.gray('Re-lock once the change is intended: agents lock'));
69
+ process.exitCode = 1;
70
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `agents logs` — unified, discoverable run-log viewer.
3
+ *
4
+ * Resolves a run across two substrates and shows (or `-f` follows) its log:
5
+ * - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
6
+ * - sessions (the local index) → transcript, tailed via the sessions tailer
7
+ *
8
+ * `[id]`/`--session` load directly (host task tried first, then session). With no
9
+ * id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
10
+ * shown, several open the fuzzy picker (or, non-TTY, print the list).
11
+ *
12
+ * Additive: `agents hosts logs` and `agents sessions tail` are unchanged and share
13
+ * the same underlying helpers (showHostTaskLog / streamSessionTail).
14
+ */
15
+ import type { Command } from 'commander';
16
+ /** Register the top-level `agents logs` command. */
17
+ export declare function registerLogsCommand(program: Command): void;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `agents logs` — unified, discoverable run-log viewer.
3
+ *
4
+ * Resolves a run across two substrates and shows (or `-f` follows) its log:
5
+ * - host-dispatch tasks (`agents run --host`) → combined-stdout log, offset-tailed
6
+ * - sessions (the local index) → transcript, tailed via the sessions tailer
7
+ *
8
+ * `[id]`/`--session` load directly (host task tried first, then session). With no
9
+ * id, `--host`/`--agent`/`--version` filter a merged candidate list; one match is
10
+ * shown, several open the fuzzy picker (or, non-TTY, print the list).
11
+ *
12
+ * Additive: `agents hosts logs` and `agents sessions tail` are unchanged and share
13
+ * the same underlying helpers (showHostTaskLog / streamSessionTail).
14
+ */
15
+ import chalk from 'chalk';
16
+ import { discoverSessions, resolveSessionById } from '../lib/session/discover.js';
17
+ import { parseAgentFilter, renderSessionLog } from './sessions.js';
18
+ import { streamSessionTail, isTailable } from './sessions-tail.js';
19
+ import { showHostTaskLog } from '../lib/hosts/logs.js';
20
+ import { listTasks } from '../lib/hosts/tasks.js';
21
+ import { itemPicker } from '../lib/picker.js';
22
+ /** Compact one-line label used by both the picker and the non-TTY list. */
23
+ function candidateLabel(c) {
24
+ if (c.kind === 'task') {
25
+ const t = c.task;
26
+ const status = t.status === 'completed' ? chalk.green(t.status)
27
+ : t.status === 'failed' ? chalk.red(t.status)
28
+ : chalk.yellow(t.status);
29
+ return `${chalk.gray('task')} ${t.id.slice(0, 8).padEnd(9)} ${t.host.padEnd(14)} ${t.agent.padEnd(8)} ${status} ${t.prompt.slice(0, 40)}`;
30
+ }
31
+ const s = c.session;
32
+ const ver = s.version ? chalk.gray(`@${s.version}`) : '';
33
+ const title = s.label || s.topic || '';
34
+ return `${chalk.gray('sess')} ${s.shortId.padEnd(9)} ${(s.agent + ver).padEnd(14)} ${chalk.gray(s.timestamp.slice(0, 16))} ${title.slice(0, 40)}`;
35
+ }
36
+ /** Show a resolved session — follow (tail) or render its transcript. */
37
+ async function showSession(session, follow) {
38
+ if (follow) {
39
+ if (!isTailable(session.agent)) {
40
+ console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
41
+ process.exit(2);
42
+ }
43
+ await streamSessionTail(session, {});
44
+ return;
45
+ }
46
+ await renderSessionLog(session);
47
+ }
48
+ async function showCandidate(c, follow) {
49
+ if (c.kind === 'task') {
50
+ const res = await showHostTaskLog(c.task.id, follow);
51
+ if (res.exitCode !== undefined)
52
+ process.exitCode = res.exitCode;
53
+ return;
54
+ }
55
+ await showSession(c.session, follow);
56
+ }
57
+ /** Resolve an explicit id/--session: host task first, then a session. */
58
+ async function showById(id, follow) {
59
+ const hostRes = await showHostTaskLog(id, follow);
60
+ if (hostRes.found) {
61
+ if (hostRes.exitCode !== undefined)
62
+ process.exitCode = hostRes.exitCode;
63
+ return;
64
+ }
65
+ const sessions = await discoverSessions({ all: true, limit: 5000 });
66
+ const matches = resolveSessionById(sessions, id);
67
+ if (matches.length === 0) {
68
+ console.error(chalk.red(`No run or session found matching "${id}".`));
69
+ process.exit(1);
70
+ }
71
+ await showSession(matches[0], follow);
72
+ }
73
+ async function runLogs(id, opts) {
74
+ const follow = !!opts.follow;
75
+ const directId = opts.session ?? id;
76
+ if (directId) {
77
+ await showById(directId, follow);
78
+ return;
79
+ }
80
+ const { agent, version } = parseAgentFilter(opts.agent);
81
+ const wantVersion = opts.version ?? version;
82
+ // Host tasks carry no session-index metadata; sessions carry no host tag.
83
+ // So --host scopes to dispatched tasks, and --version to sessions.
84
+ const candidates = [];
85
+ let tasks = listTasks();
86
+ if (opts.host)
87
+ tasks = tasks.filter((t) => t.host === opts.host);
88
+ if (agent)
89
+ tasks = tasks.filter((t) => t.agent === agent);
90
+ for (const t of tasks)
91
+ candidates.push({ kind: 'task', task: t });
92
+ if (!opts.host) {
93
+ const sessions = await discoverSessions({ agent, version: wantVersion, limit: 50 });
94
+ for (const s of sessions)
95
+ candidates.push({ kind: 'session', session: s });
96
+ }
97
+ if (candidates.length === 0) {
98
+ console.error(chalk.yellow('No matching runs. Dispatch one: agents run <agent> "<task>" [--host <name>]'));
99
+ process.exit(1);
100
+ }
101
+ if (candidates.length === 1) {
102
+ await showCandidate(candidates[0], follow);
103
+ return;
104
+ }
105
+ // Multiple sessions matched → picker if interactive, else a list to pick from.
106
+ if (!process.stdin.isTTY) {
107
+ console.error(chalk.yellow(`${candidates.length} runs match. Pass an id or --session <id>:`));
108
+ for (const c of candidates.slice(0, 30))
109
+ console.error(' ' + candidateLabel(c));
110
+ process.exit(1);
111
+ }
112
+ const picked = await itemPicker({
113
+ message: 'Select a run to view its log:',
114
+ items: candidates,
115
+ filter: (query) => {
116
+ const q = query.trim().toLowerCase();
117
+ if (!q)
118
+ return candidates;
119
+ return candidates.filter((c) => candidateLabel(c).toLowerCase().includes(q));
120
+ },
121
+ labelFor: (c) => candidateLabel(c),
122
+ shortIdFor: (c) => (c.kind === 'task' ? c.task.id : c.session.shortId),
123
+ });
124
+ if (!picked)
125
+ return;
126
+ await showCandidate(picked.item, follow);
127
+ }
128
+ /** Register the top-level `agents logs` command. */
129
+ export function registerLogsCommand(program) {
130
+ program
131
+ .command('logs [id]')
132
+ .description('Show a run’s log — a host-dispatch task or a session. -f to follow a live one.')
133
+ .option('--host <name>', 'Scope to runs dispatched to a host')
134
+ .option('-a, --agent <agent>', 'Filter by agent (e.g. claude, codex@0.116.0)')
135
+ .option('--version <version>', 'Filter by agent version')
136
+ .option('--session <id>', 'Select a session/run by id (same as the positional id)')
137
+ .option('-f, --follow', 'Follow live output')
138
+ .action((id, opts) => runLogs(id, opts));
139
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `agents message <target> <text>` — send a message to a running agent.
3
+ *
4
+ * Unifies two delivery paths behind one verb:
5
+ * - a live LOCAL/teams/loop agent → enqueue into its file-spool mailbox; the
6
+ * PreToolUse hook injects it at the agent's next tool call.
7
+ * - a CLOUD task id → the existing provider follow-up path (was
8
+ * `agents cloud message`).
9
+ *
10
+ * Cross-host is handled one layer up: `--host <h>` routes the whole command over
11
+ * ssh via `REMOTE_PASSTHROUGH` (see src/lib/hosts/passthrough.ts), so the box is
12
+ * written on the host that actually owns the agent.
13
+ */
14
+ import type { Command } from 'commander';
15
+ export declare function registerMessageCommand(program: Command): void;
@@ -0,0 +1,56 @@
1
+ import chalk from 'chalk';
2
+ import { getActiveSessions } from '../lib/session/active.js';
3
+ import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
4
+ import { resolveProvider } from '../lib/cloud/registry.js';
5
+ import { mailboxDir, enqueue } from '../lib/mailbox.js';
6
+ import { resolveMessageTarget } from '../lib/mailbox-target.js';
7
+ function die(msg, code = 1) {
8
+ console.error(chalk.red(msg));
9
+ process.exit(code);
10
+ }
11
+ export function registerMessageCommand(program) {
12
+ program
13
+ .command('message <target> <text>')
14
+ .description('Send a message to a running agent (delivered at its next tool call) or a cloud task.')
15
+ .option('--from <who>', 'Label recorded as the sender of this message')
16
+ .action(async (target, text, opts) => {
17
+ if (!target.trim()) {
18
+ die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
19
+ }
20
+ const sessions = await getActiveSessions();
21
+ const res = resolveMessageTarget(target, sessions, (id) => getTaskById(id) != null);
22
+ switch (res.kind) {
23
+ case 'cloud': {
24
+ const task = getTaskById(res.id);
25
+ const provider = resolveProvider(task.provider);
26
+ try {
27
+ await provider.message(res.id, text);
28
+ updateTaskStatus(res.id, 'running');
29
+ console.log(chalk.green(`Message sent to cloud task ${res.id}. Agent is continuing.`));
30
+ }
31
+ catch (err) {
32
+ die(err.message);
33
+ }
34
+ return;
35
+ }
36
+ case 'local': {
37
+ try {
38
+ const msgId = enqueue(mailboxDir(res.id), { to: res.id, text, from: opts.from });
39
+ console.log(chalk.green(`Queued message ${msgId} for ${res.id}. `) +
40
+ chalk.dim('The agent will see it at its next tool call.'));
41
+ }
42
+ catch (err) {
43
+ die(err.message);
44
+ }
45
+ return;
46
+ }
47
+ case 'ambiguous': {
48
+ const lines = res.candidates.map((c) => ` ${c.id} ${chalk.dim(c.label)}`).join('\n');
49
+ die(`"${target}" matches ${res.candidates.length} running agents:\n${lines}\nRe-run with a full id.`);
50
+ return;
51
+ }
52
+ case 'none':
53
+ die(`No running agent or cloud task matches "${target}". List targets with \`agents sessions --active\`.`);
54
+ }
55
+ });
56
+ }
@@ -8,3 +8,6 @@
8
8
  import type { Command } from 'commander';
9
9
  /** Register the hidden `agents models` command. */
10
10
  export declare function registerModelsCommand(program: Command): void;
11
+ export declare function formatModelSourceLine(kind: string, sourcePath: string, width?: number): string;
12
+ export declare function formatModelAliasLines(parts: string[], width?: number): string[];
13
+ export declare function formatModelSummaryLine(star: string, id: string, displayName?: string, alias?: string, width?: number): string;
@@ -11,6 +11,8 @@ import { homeDir } from '../lib/platform/index.js';
11
11
  import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
12
12
  import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
13
13
  import { getModelCatalog, locateModelSource } from '../lib/models.js';
14
+ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
15
+ import { wrapJoined } from './inspect.js';
14
16
  const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi'];
15
17
  /**
16
18
  * Agents that don't necessarily install under ~/.agents/versions (cursor ships
@@ -131,21 +133,19 @@ function printCatalog(agent, version, isDefault, options) {
131
133
  console.log(chalk.yellow(` No models extracted from ${src.kind} at ${src.path}.`));
132
134
  return;
133
135
  }
134
- console.log(chalk.gray(` source: ${src.kind} (${shortPath(src.path)})`));
136
+ console.log(chalk.gray(formatModelSourceLine(src.kind, shortPath(src.path))));
135
137
  if (Object.keys(catalog.aliases).length > 0) {
136
138
  const parts = Object.entries(catalog.aliases).map(([alias, id]) => `${chalk.cyan(alias)}=${id}`);
137
- console.log(chalk.gray(` aliases: `) + parts.join(', '));
139
+ for (const line of formatModelAliasLines(parts))
140
+ console.log(line);
138
141
  }
139
142
  console.log();
140
143
  for (const model of catalog.models) {
141
144
  const star = model.isDefault ? chalk.green('*') : ' ';
142
- const display = model.displayName && model.displayName !== model.id
143
- ? chalk.gray(` (${model.displayName})`)
144
- : '';
145
- const aliasTag = model.alias ? chalk.cyan(` [${model.alias}]`) : '';
146
- console.log(` ${star} ${chalk.bold(model.id)}${display}${aliasTag}`);
145
+ console.log(formatModelSummaryLine(star, model.id, model.displayName, model.alias));
147
146
  if (model.description) {
148
- console.log(chalk.gray(` ${model.description}`));
147
+ const descPrefix = ' ';
148
+ console.log(chalk.gray(descPrefix + truncateToWidth(model.description, Math.max(1, terminalWidth() - stringWidth(descPrefix)))));
149
149
  }
150
150
  if (options.cloud && model.perCloud) {
151
151
  const c = model.perCloud;
@@ -175,3 +175,22 @@ function printCatalog(agent, version, isDefault, options) {
175
175
  function shortPath(p) {
176
176
  return p.replace(homeDir(), '~');
177
177
  }
178
+ export function formatModelSourceLine(kind, sourcePath, width = terminalWidth()) {
179
+ const prefix = ` source: ${kind} (`;
180
+ const suffix = ')';
181
+ const room = Math.max(1, width - stringWidth(prefix) - stringWidth(suffix));
182
+ return prefix + truncateToWidth(sourcePath, room) + suffix;
183
+ }
184
+ export function formatModelAliasLines(parts, width = terminalWidth()) {
185
+ return wrapJoined(chalk.gray(' aliases: '), parts, ', ', width);
186
+ }
187
+ export function formatModelSummaryLine(star, id, displayName, alias, width = terminalWidth()) {
188
+ const display = displayName && displayName !== id ? ` (${displayName})` : '';
189
+ const aliasTag = alias ? ` [${alias}]` : '';
190
+ const prefix = ` ${star} `;
191
+ const plain = `${id}${display}${aliasTag}`;
192
+ if (stringWidth(plain) > Math.max(1, width - stringWidth(prefix))) {
193
+ return prefix + truncateToWidth(plain, Math.max(1, width - stringWidth(prefix)));
194
+ }
195
+ return prefix + chalk.bold(id) + chalk.gray(display) + chalk.cyan(aliasTag);
196
+ }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
+ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
3
4
  import * as fs from 'fs';
4
5
  import * as os from 'os';
5
6
  import * as path from 'path';
@@ -191,9 +192,15 @@ When to use:
191
192
  return;
192
193
  }
193
194
  console.log(chalk.bold('Installed Permission Sets:\n'));
195
+ const permCols = terminalWidth();
194
196
  for (const perm of sets) {
195
- const desc = perm.set.description ? ` - ${chalk.gray(perm.set.description)}` : '';
196
- console.log(` ${chalk.cyan(perm.name)}${desc}`);
197
+ const prefix = ` ${chalk.cyan(perm.name)}`;
198
+ // Cap the description to the line so a prose-paragraph set (400+ chars) can't smear.
199
+ const budget = permCols - stringWidth(prefix) - 3;
200
+ const desc = perm.set.description && budget > 3
201
+ ? ` - ${chalk.gray(truncateToWidth(perm.set.description, budget))}`
202
+ : '';
203
+ console.log(`${prefix}${desc}`);
197
204
  console.log(` ${chalk.gray(`${perm.set.allow.length} allow, ${perm.set.deny?.length || 0} deny rules`)}`);
198
205
  }
199
206
  }
@@ -28,6 +28,36 @@ export declare function resourceUnit(file: string): {
28
28
  kind: RepoResourceKind;
29
29
  unit: string;
30
30
  };
31
+ /** One resource kind + action, collapsed to a unit count (e.g. 2 new skills). */
32
+ export interface CountedResource {
33
+ action: ChangeAction;
34
+ kind: RepoResourceKind;
35
+ count: number;
36
+ }
37
+ /** Resource-level summary of a changed-file set. */
38
+ export interface ResourceDelta {
39
+ /** Counts in display order (grouped new -> changed -> removed, then RESOURCE_ORDER). */
40
+ counts: CountedResource[];
41
+ /** Total distinct resource units changed. */
42
+ total: number;
43
+ }
44
+ /**
45
+ * Collapse a changed-file set to distinct resource units, then count them by
46
+ * (action, kind). A skill whose three files all changed counts as one changed
47
+ * skill; a unit with mixed add+modify reads as a single change. This is the
48
+ * shared structured core behind every SYNC / CHANGES rendering.
49
+ */
50
+ export declare function resourceDelta(entries: {
51
+ action: ChangeAction;
52
+ file: string;
53
+ }[]): ResourceDelta;
54
+ /**
55
+ * Kind-only brief for the compact (wide-terminal) layout: `(24 skills, 9 commands, +20)`
56
+ * — the top `maxKinds` kinds by display order, with a `+N` remainder. Drops the
57
+ * new/changed verb (the arrow already carries direction) to stay short. Returns
58
+ * '' when there is nothing to summarize.
59
+ */
60
+ export declare function deltaBrief(delta: ResourceDelta, maxKinds?: number): string;
31
61
  /**
32
62
  * Render a set of changed files as a resource-level summary, e.g.
33
63
  * `2 new skills, 1 changed hook`. Counts distinct resource units (a skill whose
@@ -39,6 +69,10 @@ export declare function formatResourceDelta(entries: {
39
69
  action: ChangeAction;
40
70
  file: string;
41
71
  }[], maxParts?: number): string;
72
+ /** `owner/repo` from a git URL, for the compact card header. Falls back to the raw URL. */
73
+ export declare function repoSlug(url: string): string;
74
+ /** Pack colored phrases into `, `-joined lines no wider than `width`. */
75
+ export declare function wrapPhrases(parts: string[], width: number): string[];
42
76
  /** Register the `agents repo` command tree. */
43
77
  export declare function registerRepoCommands(program: Command): void;
44
78
  export {};