@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
@@ -7,11 +7,13 @@
7
7
  */
8
8
  import { Option } from 'commander';
9
9
  import chalk from 'chalk';
10
+ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
10
11
  import * as fs from 'fs';
11
- import { spawnSync } from 'child_process';
12
- import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
12
+ import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
13
+ import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
14
+ import { ensureDaemonStarted } from '../lib/daemon.js';
13
15
  import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
14
- import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
16
+ import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
15
17
  import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
16
18
  import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
17
19
  import { DEFAULT_TTL_MS, agentLoad, agentLock, agentStatus, ensureAgentRunning, installSecretsAgentService, runAgentLoadFromStdin, runSecretsAgent, secretsAgentServiceInstalled, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
@@ -139,6 +141,31 @@ function readStdinSync() {
139
141
  // SSH target validation is defined canonically in src/lib/ssh-exec.ts and
140
142
  // re-exported here for back-compat with existing importers of these symbols.
141
143
  export { SSH_TARGET_RE, assertValidSshTarget };
144
+ /**
145
+ * Build the child environment for `agents secrets exec`. Strips
146
+ * loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
147
+ * forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
148
+ * the executed command.
149
+ */
150
+ export function buildSecretsExecEnv(parentEnv, secretEnv) {
151
+ const env = { ...sanitizeProcessEnv(parentEnv), ...secretEnv };
152
+ delete env.AGENTS_SECRETS_PASSPHRASE;
153
+ return env;
154
+ }
155
+ /**
156
+ * Resolve the CLI version for the MCP server's `serverInfo.version`. package.json
157
+ * sits at the repo root — two levels up from both `src/commands/` (bun/tsx dev)
158
+ * and `dist/commands/` (built). Cosmetic only, so any failure falls back cleanly.
159
+ */
160
+ function getCliVersion() {
161
+ try {
162
+ const pkgPath = new URL('../../package.json', import.meta.url);
163
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version ?? '0.0.0';
164
+ }
165
+ catch {
166
+ return '0.0.0';
167
+ }
168
+ }
142
169
  /** POSIX single-quote a string for safe interpolation into a remote shell command. */
143
170
  function shellQuote(s) {
144
171
  return `'${s.replace(/'/g, `'\\''`)}'`;
@@ -264,14 +291,19 @@ function compactRemaining(expiresAt) {
264
291
  /** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
265
292
  * hint when a `daily` bundle is currently held by the secrets-agent. `held`
266
293
  * maps bundle name → expiry epoch-ms (from agentStatus()). */
267
- function renderPolicyCol(b, held) {
294
+ export function renderPolicyCol(b, held) {
295
+ // `never` is loud on purpose — it's the only tier with no user-presence gate.
296
+ if (bundlePolicy(b) === 'never')
297
+ return chalk.red.bold('never · NO ACL');
268
298
  if (bundlePolicy(b) === 'always')
269
299
  return chalk.yellow('always ask');
270
300
  const exp = held?.get(b.name);
271
301
  return exp ? chalk.green(`daily · ${compactRemaining(exp)} left`) : chalk.gray('daily');
272
302
  }
303
+ /** Below this width the fixed date columns no longer fit; `list` uses cards. */
304
+ const SECRETS_WIDE = 96;
273
305
  /** Format a single bundle as a table row for the `secrets list` output. */
274
- function renderBundleRow(b, held) {
306
+ function renderBundleRow(b, held, cols = terminalWidth()) {
275
307
  const entries = describeBundle(b);
276
308
  const keys = entries.length;
277
309
  const expiringCount = countExpiringSoon(b.meta);
@@ -296,10 +328,26 @@ function renderBundleRow(b, held) {
296
328
  `${padVisible(used, 7)}`;
297
329
  // Mark file-backed bundles so `list` distinguishes them from keychain ones.
298
330
  const tag = b.backend === 'file' ? chalk.magenta('[file] ') : '';
299
- const desc = b.description ? chalk.gray(safePrint(b.description)) : '';
331
+ // Cap the free-form description to whatever space is left on the line so a long
332
+ // description can't push the row to 200+ chars and wrap into a smear.
333
+ const budget = cols - stringWidth(head) - 1 - stringWidth(tag);
334
+ const desc = b.description && budget > 3
335
+ ? chalk.gray(truncateToWidth(safePrint(b.description), budget))
336
+ : '';
300
337
  const trailer = `${tag}${desc}`.trimEnd();
301
338
  return trailer ? `${head} ${trailer}` : head.trimEnd();
302
339
  }
340
+ /** Narrow-terminal card: name + compact meta on one line, description below. */
341
+ function renderBundleCard(b, held, cols) {
342
+ const keys = describeBundle(b).length;
343
+ const used = b.last_used ? relativeAge(b.last_used) : (b.created_at ? 'never' : '?');
344
+ const tag = b.backend === 'file' ? chalk.magenta(' [file]') : '';
345
+ const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, held) + chalk.gray(` · used ${used}`);
346
+ const line1 = `${chalk.cyan(b.name)} ${meta}${tag}`;
347
+ if (!b.description)
348
+ return line1;
349
+ return `${line1}\n ${chalk.gray(truncateToWidth(safePrint(b.description), cols - 4))}`;
350
+ }
303
351
  /** Colorize a variable source kind (literal, keychain, env, file, exec). */
304
352
  function kindLabel(kind) {
305
353
  switch (kind) {
@@ -487,7 +535,7 @@ export function registerSecretsCommands(program) {
487
535
  { title: 'Agent commands', names: ['start', 'stop', 'unlock', 'lock', 'status', 'policy'] },
488
536
  { title: 'Raw item commands', names: ['get', 'set'] },
489
537
  { title: 'Sync commands', names: ['push', 'pull', 'remote-list'] },
490
- { title: 'Utilities', names: ['exec', 'generate', 'migrate-acl'] },
538
+ { title: 'Utilities', names: ['exec', 'mcp', 'generate', 'migrate-acl'] },
491
539
  ]);
492
540
  cmd
493
541
  .command('list')
@@ -519,9 +567,17 @@ export function registerSecretsCommands(program) {
519
567
  /* broker not running — render policy without the countdown */
520
568
  }
521
569
  }
522
- console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
523
- for (const b of bundles) {
524
- console.log(renderBundleRow(b, held));
570
+ const cols = terminalWidth();
571
+ if (cols >= SECRETS_WIDE) {
572
+ console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
573
+ for (const b of bundles) {
574
+ console.log(renderBundleRow(b, held, cols));
575
+ }
576
+ }
577
+ else {
578
+ for (const b of bundles) {
579
+ console.log(renderBundleCard(b, held, cols));
580
+ }
525
581
  }
526
582
  });
527
583
  cmd
@@ -562,9 +618,14 @@ export function registerSecretsCommands(program) {
562
618
  console.log(chalk.yellow('allow_exec: true'));
563
619
  if (bundle.backend === 'file')
564
620
  console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
565
- console.log(bundlePolicy(bundle) === 'daily'
566
- ? chalk.gray('policy: daily (ask once, then held ~24h until screen-lock / sleep / logout)')
567
- : chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
621
+ if (bundlePolicy(bundle) === 'never') {
622
+ console.log(chalk.red.bold('policy: never NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
623
+ }
624
+ else {
625
+ console.log(bundlePolicy(bundle) === 'daily'
626
+ ? chalk.gray('policy: daily (ask once, then held ~24h until screen-lock / sleep / logout)')
627
+ : chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
628
+ }
568
629
  if (bundle.created_at)
569
630
  console.log(chalk.gray(`created_at: ${bundle.created_at} (${humanAge(bundle.created_at)})`));
570
631
  if (bundle.updated_at)
@@ -686,8 +747,9 @@ export function registerSecretsCommands(program) {
686
747
  .description('Create an empty bundle')
687
748
  .option('--description <text>', 'Free-form description')
688
749
  .option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
689
- .option('--policy <policy>', 'prompt policy: daily (default, ask once a day) or always (ask every time)')
750
+ .option('--policy <policy>', 'prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand)')
690
751
  .addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
752
+ .option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
691
753
  .option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted, headless-readable)', 'keychain')
692
754
  .option('--force', 'Overwrite an existing bundle')
693
755
  .action(async (name, opts) => {
@@ -703,6 +765,13 @@ export function registerSecretsCommands(program) {
703
765
  console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
704
766
  process.exit(1);
705
767
  }
768
+ // The `never` tier is the least-safe option — gate it loudly. Throws in a
769
+ // headless shell without --i-understand; prompts otherwise.
770
+ const ack = assertNeverPolicyAcknowledged(policy, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
771
+ if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(resolvedName))) {
772
+ console.error(chalk.yellow('Aborted.'));
773
+ return;
774
+ }
706
775
  const bundle = {
707
776
  name: resolvedName,
708
777
  description: opts.description,
@@ -712,11 +781,16 @@ export function registerSecretsCommands(program) {
712
781
  vars: {},
713
782
  };
714
783
  writeBundle(bundle);
715
- const tags = [
716
- bundlePolicy(bundle) === 'daily' ? 'policy: daily' : 'policy: always ask',
717
- backend === 'file' ? 'backend: file' : null,
718
- ].filter(Boolean);
784
+ const policyTag = bundlePolicy(bundle) === 'daily'
785
+ ? 'policy: daily'
786
+ : bundlePolicy(bundle) === 'always'
787
+ ? 'policy: always ask'
788
+ : 'policy: never (NO biometry ACL)';
789
+ const tags = [policyTag, backend === 'file' ? 'backend: file' : null].filter(Boolean);
719
790
  console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
791
+ if (bundlePolicy(bundle) === 'never') {
792
+ console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
793
+ }
720
794
  if (backend === 'file') {
721
795
  console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
722
796
  }
@@ -850,7 +924,7 @@ export function registerSecretsCommands(program) {
850
924
  secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
851
925
  }
852
926
  const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
853
- bundleItemStore(bundle.backend).set(item, secretValue);
927
+ bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' }).set(item, secretValue);
854
928
  bundle.vars[resolvedKey] = keychainRef(resolvedKey);
855
929
  applyMeta();
856
930
  writeBundle(bundle);
@@ -1096,7 +1170,7 @@ Examples:
1096
1170
  vars: {},
1097
1171
  };
1098
1172
  }
1099
- const store = bundleItemStore(bundle.backend);
1173
+ const store = bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' });
1100
1174
  let added = 0;
1101
1175
  let skipped = 0;
1102
1176
  if (opts.from1password) {
@@ -1218,20 +1292,18 @@ Examples:
1218
1292
  const remoteCmd = `bash -lc ${shellQuote(remoteAgents)}`;
1219
1293
  let failures = 0;
1220
1294
  for (const host of hosts) {
1221
- const res = spawnSync('ssh', ['-o', 'BatchMode=yes', host, remoteCmd], {
1222
- input,
1223
- stdio: ['pipe', 'pipe', 'pipe'],
1224
- encoding: 'utf-8',
1225
- });
1226
- if (res.error) {
1295
+ // Routed through the shared ssh engine: full hardened options
1296
+ // (BatchMode, ConnectTimeout, keepalive) + control-socket reuse.
1297
+ const res = sshExec(host, remoteCmd, { input });
1298
+ if (res.code === null) {
1227
1299
  failures++;
1228
- console.error(chalk.red(`${host}: ${res.error.message}`));
1300
+ console.error(chalk.red(`${host}: ${res.stderr.trim() || (res.timedOut ? 'ssh timed out' : 'ssh failed')}`));
1229
1301
  continue;
1230
1302
  }
1231
- if (res.status !== 0) {
1303
+ if (res.code !== 0) {
1232
1304
  failures++;
1233
1305
  const msg = (res.stderr || res.stdout || '').trim();
1234
- console.error(chalk.red(`${host}: remote import failed (exit ${res.status ?? 'signal'})${msg ? `: ${msg}` : ''}`));
1306
+ console.error(chalk.red(`${host}: remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`));
1235
1307
  continue;
1236
1308
  }
1237
1309
  const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
@@ -1308,6 +1380,8 @@ Examples:
1308
1380
  .command('exec <bundle> [command...]')
1309
1381
  .description('Run a command with the bundle\'s secrets injected into the environment (use --host to resolve the bundle from a remote machine, ephemerally)')
1310
1382
  .option('--host <target>', 'Resolve <bundle> on a remote host over SSH and inject it (ephemeral — never stored on this machine)')
1383
+ .option('--keys <keys>', 'Inject only this comma-separated subset of keys (e.g. KEY1,KEY2). Missing keys are an error.')
1384
+ .option('--allow-expired', 'Inject keys even if their expiry date has passed (overrides the pre-run expiry abort).')
1311
1385
  .allowUnknownOption()
1312
1386
  .action(async (bundleName, commandParts, execOpts) => {
1313
1387
  try {
@@ -1316,28 +1390,42 @@ Examples:
1316
1390
  process.exit(1);
1317
1391
  }
1318
1392
  const [cmd, ...args] = commandParts;
1393
+ const keysSubset = execOpts.keys
1394
+ ? execOpts.keys.split(',').map((k) => k.trim()).filter(Boolean)
1395
+ : undefined;
1319
1396
  let secretEnv;
1320
1397
  if (execOpts.host) {
1398
+ // Least-privilege flags do not yet cross the SSH resolver —
1399
+ // silently applying them would inject the full remote env or an
1400
+ // expired key. Fail loud so the user can drop the flag or run
1401
+ // locally instead.
1402
+ const { assertRemoteBundleFlagsUnsupported } = await import('../lib/secrets/bundles.js');
1403
+ assertRemoteBundleFlagsUnsupported(bundleName, execOpts.host, { keys: keysSubset, allowExpired: execOpts.allowExpired }, { keysFlag: '--keys', allowExpiredFlag: '--allow-expired' });
1321
1404
  secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
1322
1405
  }
1323
1406
  else {
1324
1407
  const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
1325
- secretEnv = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` }).env;
1408
+ secretEnv = readAndResolveBundleEnv(bundleName, {
1409
+ caller: `command ${cmd}`,
1410
+ keys: keysSubset,
1411
+ allowExpired: execOpts.allowExpired,
1412
+ }).env;
1326
1413
  }
1327
1414
  const { spawn } = await import('child_process');
1328
1415
  // On Windows, spawn without a shell ENOENTs for `.cmd`/`.bat` launchers
1329
1416
  // (npm, yarn, most JS CLIs) and shell built-ins, so we set shell:true.
1330
1417
  // With shell:true Node hands cmd.exe a single command line with NO quoting
1331
- // of its own, so args containing spaces or cmd metacharacters must be
1332
- // quoted here (quoteWin32ExecArg) or they'd be split. See that helper for
1418
+ // of its own. Compose that line ourselves (composeWin32CommandLine quotes
1419
+ // every token) and pass an EMPTY args array so Node never concatenates the
1420
+ // user-supplied args unescaped (DEP0190 + injection). See that helper for
1333
1421
  // the cmd.exe %VAR%/!VAR! expansion caveat.
1334
1422
  const useShell = process.platform === 'win32';
1335
- const spawnCmd = useShell ? quoteWin32ExecArg(cmd) : cmd;
1336
- const spawnArgs = useShell ? args.map(quoteWin32ExecArg) : args;
1423
+ const spawnCmd = useShell ? composeWin32CommandLine(cmd, args) : cmd;
1424
+ const spawnArgs = useShell ? [] : args;
1337
1425
  const proc = spawn(spawnCmd, spawnArgs, {
1338
1426
  stdio: 'inherit',
1339
1427
  shell: useShell,
1340
- env: { ...process.env, ...secretEnv },
1428
+ env: buildSecretsExecEnv(process.env, secretEnv),
1341
1429
  });
1342
1430
  proc.on('close', (code) => process.exit(code ?? 0));
1343
1431
  proc.on('error', (err) => {
@@ -1352,6 +1440,17 @@ Examples:
1352
1440
  process.exit(1);
1353
1441
  }
1354
1442
  });
1443
+ cmd
1444
+ .command('mcp')
1445
+ .description('Run a stdio MCP server exposing get_secret(bundle, key) — hand credentials to an MCP-speaking agent by name at call time, never through the child process environment')
1446
+ .action(async () => {
1447
+ // JIT credential delivery (#333): unlike `secrets exec`, which bakes every
1448
+ // resolved value into the child's env, this speaks MCP over stdio so a
1449
+ // framework requests one secret at a time and the raw value never enters
1450
+ // process.env. stdout is the JSON-RPC channel — nothing else may print there.
1451
+ const { runSecretsMcpServer } = await import('../lib/secrets/mcp.js');
1452
+ await runSecretsMcpServer({ version: getCliVersion() });
1453
+ });
1355
1454
  cmd
1356
1455
  .command('generate [length]')
1357
1456
  .description('Generate a random password')
@@ -1454,6 +1553,12 @@ Examples:
1454
1553
  console.error(chalk.red('Could not start the secrets-agent.'));
1455
1554
  process.exit(1);
1456
1555
  }
1556
+ // #415: the daemon should be always-on for any background need, not only
1557
+ // after `routines add`. A user who only ever unlocks secrets still gets
1558
+ // the daemon installed + running here. `ensureAgentRunning` above only
1559
+ // brings up the standalone secrets broker, not the daemon. Idempotent
1560
+ // (single-instance start lock, #414) and best-effort — never blocks unlock.
1561
+ ensureDaemonStarted();
1457
1562
  let loaded = 0;
1458
1563
  for (const name of targets) {
1459
1564
  try {
@@ -1523,8 +1628,9 @@ Examples:
1523
1628
  cmd
1524
1629
  .command('policy <bundle> [policy]')
1525
1630
  .alias('tier')
1526
- .description("Show or set a bundle's prompt policy: daily (default, ask once a day) or always (ask every time).")
1527
- .action((bundleName, policyArg) => {
1631
+ .description("Show or set a bundle's prompt policy: daily (default, ask once a day), always (ask every time), or never (silent, NO biometry ACL).")
1632
+ .option('--i-understand', 'Confirm switching to the "never" policy (no biometry ACL) without an interactive prompt')
1633
+ .action(async (bundleName, policyArg, opts) => {
1528
1634
  try {
1529
1635
  const bundle = readBundle(bundleName);
1530
1636
  if (policyArg === undefined) {
@@ -1532,15 +1638,24 @@ Examples:
1532
1638
  return;
1533
1639
  }
1534
1640
  const next = parsePolicyOpt(policyArg);
1641
+ // Switching to `never` drops the biometry ACL — gate it exactly like create.
1642
+ const ack = assertNeverPolicyAcknowledged(next, { iUnderstand: opts.iUnderstand, interactive: isInteractiveTerminal() });
1643
+ if (ack === 'prompt' && !(await confirmNeverPolicyInteractive(bundle.name))) {
1644
+ console.error(chalk.yellow('Aborted.'));
1645
+ return;
1646
+ }
1535
1647
  bundle.policy = next;
1536
1648
  writeBundle(bundle);
1537
1649
  console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
1538
1650
  if (next === 'daily') {
1539
1651
  console.log(chalk.gray('Held by the secrets-agent for ~24h after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
1540
1652
  }
1541
- else {
1653
+ else if (next === 'always') {
1542
1654
  console.log(chalk.gray('Asks for Touch ID every time — never auto-held.'));
1543
1655
  }
1656
+ else {
1657
+ console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only.'));
1658
+ }
1544
1659
  }
1545
1660
  catch (err) {
1546
1661
  console.error(chalk.red(err.message));
@@ -1589,23 +1704,52 @@ Examples:
1589
1704
  registerSecretsSyncCommands(cmd);
1590
1705
  registerSecretsMigrateAclCommand(cmd);
1591
1706
  }
1592
- /** Validate a prompt-policy value, exiting with a clear message on a bad one.
1593
- * Accepts the legacy `biometry`/`session` tokens as aliases for `always`/`daily`
1594
- * so older flags and scripts keep working. `never`/`none` (no biometry ACL) is
1595
- * rejected explicitly it needs a separate signed-helper change (see
1596
- * https://github.com/phnx-labs/agents-cli/issues/421). */
1597
- function parsePolicyOpt(raw) {
1707
+ /** Validate a prompt-policy value, throwing a clear message on a bad one (the
1708
+ * caller's try/catch renders it and exits). Accepts the legacy `biometry` /
1709
+ * `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
1710
+ * flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier
1711
+ * accepted here, then gated behind a loud confirmation in the command layer. */
1712
+ export function parsePolicyOpt(raw) {
1598
1713
  const v = (raw ?? 'always').toLowerCase();
1599
1714
  if (v === 'always' || v === 'biometry')
1600
1715
  return 'always';
1601
1716
  if (v === 'daily' || v === 'session')
1602
1717
  return 'daily';
1603
- if (v === 'never' || v === 'none') {
1604
- console.error(chalk.red("policy 'never' (no biometry ACL) is not available yet — see https://github.com/phnx-labs/agents-cli/issues/421. Use 'always' or 'daily'."));
1605
- process.exit(1);
1718
+ if (v === 'never' || v === 'none')
1719
+ return 'never';
1720
+ throw new Error(`Invalid policy '${raw}'. Use 'always', 'daily', or 'never'.`);
1721
+ }
1722
+ /**
1723
+ * Gate a create/switch to the `never` prompt-policy behind an explicit,
1724
+ * deliberate acknowledgement — it is the least-safe tier (no user-presence
1725
+ * check on any read). Returns:
1726
+ * - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
1727
+ * - `'prompt'` — interactive shell: caller must run a loud confirm prompt.
1728
+ * Throws in a non-interactive shell when the flag is absent, so a headless
1729
+ * `create --policy never` can't silently downgrade a bundle's protection.
1730
+ */
1731
+ export function assertNeverPolicyAcknowledged(policy, opts) {
1732
+ if (policy !== 'never')
1733
+ return 'ok';
1734
+ if (opts.iUnderstand)
1735
+ return 'ok';
1736
+ if (!opts.interactive) {
1737
+ throw new Error("Refusing to set the 'never' prompt-policy without confirmation. This tier stores " +
1738
+ 'the bundle WITHOUT the biometry access control: every read is silent, with no Touch ID ' +
1739
+ 'and no user-presence check — any code running as you can read it. Re-run with ' +
1740
+ '--i-understand to confirm you want an unprotected, automation-only bundle.');
1606
1741
  }
1607
- console.error(chalk.red(`Invalid policy '${raw}'. Use 'always' or 'daily'.`));
1608
- process.exit(1);
1742
+ return 'prompt';
1743
+ }
1744
+ /** Loud confirm prompt shown before creating/switching to `never` in a TTY.
1745
+ * Returns true to proceed. Kept separate from assertNeverPolicyAcknowledged so
1746
+ * the decision logic stays pure and unit-testable. */
1747
+ async function confirmNeverPolicyInteractive(bundleName) {
1748
+ console.error(chalk.red.bold('WARNING: policy "never" stores this bundle with NO biometry ACL.'));
1749
+ console.error(chalk.red(`Reads of '${bundleName}' will be fully silent — no Touch ID, no user-presence check.`));
1750
+ console.error(chalk.yellow('Use it only for low-sensitivity, automation-only credentials.'));
1751
+ const { confirm } = await import('@inquirer/prompts');
1752
+ return confirm({ message: `Store '${bundleName}' WITHOUT biometry protection?`, default: false });
1609
1753
  }
1610
1754
  /** Validate a --backend value, exiting with a clear message on a bad one. */
1611
1755
  function parseBackendOpt(raw) {
@@ -1629,48 +1773,9 @@ function humanRemaining(expiresAt) {
1629
1773
  const days = Math.round(hours / 24);
1630
1774
  return `locks in ${days} day${days === 1 ? '' : 's'}`;
1631
1775
  }
1632
- /**
1633
- * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
1634
- * `spawn(..., { shell: true })` on win32 (`agents secrets exec`). cmd.exe does
1635
- * NO quoting of its own, so an unquoted arg with a space is split into several
1636
- * args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
1637
- * We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
1638
- * and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
1639
- * rules, so the *child's* argv parse reconstructs the original argument.
1640
- *
1641
- * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
1642
- * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
1643
- * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
1644
- * `agents secrets exec` runs a caller-supplied command against a bundle the
1645
- * caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
1646
- * ever changes (exec'ing an untrusted command line), route through a shell that
1647
- * disables expansion rather than relying on this quoter. An empty arg becomes
1648
- * `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
1649
- * under `process.platform === 'win32'`).
1650
- */
1651
- export function quoteWin32ExecArg(arg) {
1652
- if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
1653
- return arg;
1654
- let result = '"';
1655
- let backslashes = 0;
1656
- for (const ch of arg) {
1657
- if (ch === '\\') {
1658
- backslashes += 1;
1659
- continue;
1660
- }
1661
- if (ch === '"') {
1662
- // Double the run of backslashes, then escape this quote.
1663
- result += '\\'.repeat(backslashes * 2 + 1) + '"';
1664
- backslashes = 0;
1665
- continue;
1666
- }
1667
- result += '\\'.repeat(backslashes) + ch;
1668
- backslashes = 0;
1669
- }
1670
- // Trailing backslashes precede the closing quote → must be doubled.
1671
- result += '\\'.repeat(backslashes * 2) + '"';
1672
- return result;
1673
- }
1776
+ // `quoteWin32ExecArg` now lives in lib/platform/exec.ts (shared with the agent
1777
+ // run/shim spawn paths); re-exported here for the colocated secrets tests.
1778
+ export { quoteWin32ExecArg };
1674
1779
  /**
1675
1780
  * Copy text to the system clipboard, cross-platform.
1676
1781
  * macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `agents serve` — read-only local web companion.
3
+ *
4
+ * Boots a localhost-only (127.0.0.1) HTTP + SSE server that renders a live
5
+ * dashboard of team status + per-worktree diffs, scheduled routines, and cloud
6
+ * tasks. Everything is a viewer over data other commands already own — there
7
+ * are no mutation endpoints. See src/lib/serve/.
8
+ */
9
+ import type { Command } from 'commander';
10
+ export declare function registerServeCommand(program: Command): void;
@@ -0,0 +1,37 @@
1
+ import chalk from 'chalk';
2
+ import { startServeServer, DEFAULT_SERVE_PORT, SERVE_HOST } from '../lib/serve/server.js';
3
+ export function registerServeCommand(program) {
4
+ program
5
+ .command('serve')
6
+ .description('Read-only local web companion: team diffs, routines, and cloud status (binds 127.0.0.1 only).')
7
+ .option('-p, --port <n>', `Port to bind on ${SERVE_HOST}`, String(DEFAULT_SERVE_PORT))
8
+ .option('--interval <ms>', 'SSE refresh cadence in milliseconds', '3000')
9
+ .action(async (opts) => {
10
+ const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
11
+ const intervalMs = parseInt(opts.interval ?? '', 10) || 3000;
12
+ try {
13
+ const { server, port: bound } = await startServeServer(port, {
14
+ cwd: process.cwd(),
15
+ intervalMs,
16
+ });
17
+ const url = `http://${SERVE_HOST}:${bound}`;
18
+ console.log(`${chalk.green('agents serve')} ${chalk.dim('→')} ${chalk.cyan(url)}`);
19
+ console.log(chalk.dim('read-only · localhost only · Ctrl-C to stop'));
20
+ const shutdown = () => {
21
+ server.close(() => process.exit(0));
22
+ };
23
+ process.on('SIGINT', shutdown);
24
+ process.on('SIGTERM', shutdown);
25
+ }
26
+ catch (err) {
27
+ const msg = String(err?.message ?? err);
28
+ if (msg.includes('EADDRINUSE')) {
29
+ console.error(chalk.red(`Port ${port} is already in use. Pass --port <n> to pick another.`));
30
+ }
31
+ else {
32
+ console.error(chalk.red(`Could not start serve: ${msg}`));
33
+ }
34
+ process.exit(1);
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `agents sessions inject <sessionId> <text>` — deliver text into the terminal a
3
+ * running session lives in. The CLI face of the Terminal Engine's Gap 2 primitive
4
+ * (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
5
+ * (RUSH-1415) can shell out to nudge a stalled agent with "continue".
6
+ *
7
+ * Resolution: find the active session by id, map its `provenance.reply` rail
8
+ * (provenance.ts:47) to an engine `InjectTarget` and inject. Today only tmux rails
9
+ * are externally addressable; `--pane`/`--pty` target a backend directly when the
10
+ * handle is already known (skipping the session lookup).
11
+ */
12
+ import type { Command } from 'commander';
13
+ /** Attach the `inject` subcommand to an existing `sessions` command. */
14
+ export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;