@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
@@ -13,6 +13,7 @@ import * as yaml from 'yaml';
13
13
  import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonPid, readDaemonLog, } from '../lib/daemon.js';
14
14
  import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
15
15
  import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, } from '../lib/routines.js';
16
+ import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
16
17
  import { getRoutinesDir } from '../lib/state.js';
17
18
  import { IS_WINDOWS } from '../lib/platform/index.js';
18
19
  import { safeJoin } from '../lib/paths.js';
@@ -21,6 +22,21 @@ import { JobScheduler } from '../lib/scheduler.js';
21
22
  import { detectOverdueJobs } from '../lib/overdue.js';
22
23
  import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
23
24
  import { setHelpSections } from '../lib/help.js';
25
+ /**
26
+ * Human label for what fires a job: its cron schedule, or its event trigger
27
+ * for schedule-less (trigger-only) routines.
28
+ */
29
+ function fireConditionLabel(job) {
30
+ if (job.schedule)
31
+ return humanizeCron(job.schedule, job.timezone);
32
+ if (job.trigger) {
33
+ const scope = job.trigger.repo
34
+ ? ` (${job.trigger.repo}${job.trigger.branch ? `@${job.trigger.branch}` : ''})`
35
+ : '';
36
+ return `on ${job.trigger.event}${scope}`;
37
+ }
38
+ return '-';
39
+ }
24
40
  /** Start or reload the background scheduler so newly-added jobs fire on time. */
25
41
  function ensureSchedulerRunning() {
26
42
  if (isDaemonRunning()) {
@@ -73,7 +89,7 @@ async function pickJob(message, filter, alternatives = [], cwd) {
73
89
  message,
74
90
  choices: jobs.map((job) => ({
75
91
  value: job.name,
76
- name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule})`)}`,
92
+ name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
77
93
  })),
78
94
  });
79
95
  }
@@ -159,8 +175,9 @@ export function registerRoutinesCommands(program) {
159
175
  agent: job.agent ?? null,
160
176
  workflow: job.workflow ?? null,
161
177
  repo: job.repo ?? null,
162
- schedule: job.schedule,
163
- scheduleHuman: humanizeCron(job.schedule, job.timezone),
178
+ schedule: job.schedule ?? null,
179
+ scheduleHuman: fireConditionLabel(job),
180
+ trigger: job.trigger ?? null,
164
181
  timezone: job.timezone ?? null,
165
182
  enabled: job.enabled,
166
183
  overdue: overdueSet.has(job.name),
@@ -193,7 +210,7 @@ export function registerRoutinesCommands(program) {
193
210
  for (const job of jobs) {
194
211
  const nextRun = scheduler.getNextRun(job.name);
195
212
  const nextStr = humanizeNextRun(nextRun ?? null, now, job.timezone);
196
- let schedStr = humanizeCron(job.schedule, job.timezone);
213
+ let schedStr = fireConditionLabel(job);
197
214
  if (job.endAt) {
198
215
  const end = new Date(job.endAt);
199
216
  const endLabel = Number.isFinite(end.getTime())
@@ -565,6 +582,82 @@ export function registerRoutinesCommands(program) {
565
582
  }
566
583
  console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
567
584
  });
585
+ routinesCmd
586
+ .command('webhook')
587
+ .description('Fire trigger-based routines from a single GitHub webhook payload (read from --file or stdin). One-shot: matches and fires, then exits. For a long-running receiver, run this behind your own HTTP forwarder.')
588
+ .requiredOption('--event <name>', 'GitHub event name, as sent in the X-GitHub-Event header (e.g. pull_request, push, workflow_run)')
589
+ .option('--file <path>', 'Read the webhook JSON payload from this file instead of stdin')
590
+ .option('--dry-run', 'Show which routines would fire without firing them')
591
+ .action(async (options) => {
592
+ // Load the raw JSON payload: --file wins, else drain stdin.
593
+ let raw;
594
+ if (options.file) {
595
+ const resolved = path.resolve(options.file);
596
+ if (!fs.existsSync(resolved)) {
597
+ console.log(chalk.red(`File not found: ${resolved}`));
598
+ process.exit(1);
599
+ }
600
+ raw = fs.readFileSync(resolved, 'utf-8');
601
+ }
602
+ else {
603
+ if (process.stdin.isTTY) {
604
+ console.log(chalk.red('No payload provided. Pass --file <path> or pipe the webhook JSON on stdin.'));
605
+ process.exit(1);
606
+ }
607
+ const chunks = [];
608
+ for await (const chunk of process.stdin)
609
+ chunks.push(chunk);
610
+ raw = Buffer.concat(chunks).toString('utf-8');
611
+ }
612
+ let payload;
613
+ try {
614
+ const parsed = raw.trim() ? JSON.parse(raw) : {};
615
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
616
+ throw new Error('payload must be a JSON object');
617
+ }
618
+ payload = parsed;
619
+ }
620
+ catch (err) {
621
+ console.log(chalk.red(`Invalid webhook payload JSON: ${err.message}`));
622
+ process.exit(1);
623
+ }
624
+ const webhook = { event: options.event, payload };
625
+ // Matching is intentionally user-layer only (fireWebhookJobs defaults to
626
+ // listJobs() with no cwd), mirroring `run`/`catchup`: a webhook must never
627
+ // fire a cloned project repo's `.agents/routines/*.yml` and run an
628
+ // attacker-supplied prompt under the user's agent session.
629
+ if (options.dryRun) {
630
+ const matched = matchJobsToWebhook(listAllJobs(), webhook);
631
+ if (matched.length === 0) {
632
+ console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
633
+ return;
634
+ }
635
+ console.log(chalk.bold(`${matched.length} routine(s) would fire on ${options.event}:\n`));
636
+ for (const job of matched) {
637
+ console.log(` ${chalk.cyan(job.name)} — ${fireConditionLabel(job)}`);
638
+ }
639
+ console.log(chalk.gray('\n(dry run — no routines triggered)'));
640
+ return;
641
+ }
642
+ // Fired jobs run detached via executeJobDetached (the same path cron
643
+ // uses). Keep the daemon alive so each run's meta.json is finalized.
644
+ if (!isDaemonRunning()) {
645
+ const started = startDaemon();
646
+ if (started.pid) {
647
+ console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
648
+ }
649
+ }
650
+ const fired = await fireWebhookJobs(webhook);
651
+ if (fired.length === 0) {
652
+ console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
653
+ return;
654
+ }
655
+ console.log(chalk.bold(`Fired ${fired.length} routine(s) on ${options.event}:\n`));
656
+ for (const f of fired) {
657
+ console.log(` ${chalk.cyan(f.jobName)} → ${chalk.green('started')} (run: ${f.runId})`);
658
+ }
659
+ console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
660
+ });
568
661
  routinesCmd
569
662
  .command('logs [name]')
570
663
  .description('Read stdout from the most recent execution. Use --run to see a specific past run.')
@@ -675,9 +768,12 @@ export function registerRoutinesCommands(program) {
675
768
  if (result.method === 'already-running') {
676
769
  console.log(chalk.yellow(`Scheduler already running (PID: ${result.pid})`));
677
770
  }
678
- else {
771
+ else if (result.pid) {
679
772
  console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
680
773
  }
774
+ else {
775
+ console.log(chalk.yellow('Scheduler start dispatched but no PID surfaced. Check `agents routines status`.'));
776
+ }
681
777
  });
682
778
  routinesCmd
683
779
  .command('stop')
@@ -23,7 +23,7 @@ import chalk from 'chalk';
23
23
  import * as crypto from 'crypto';
24
24
  import * as fs from 'fs';
25
25
  import * as path from 'path';
26
- import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
26
+ import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
27
27
  import { getBackupsDir } from '../lib/state.js';
28
28
  import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
29
29
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -101,7 +101,7 @@ function migrateOne(record) {
101
101
  export function registerSecretsMigrateAclCommand(secrets) {
102
102
  secrets
103
103
  .command('migrate-acl')
104
- .description('Refresh existing keychain ACLs to use the signed Agents CLI helper. Dry-run by default.')
104
+ .description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
105
105
  .option('--commit', 'Perform writes (default is dry-run reporting only)')
106
106
  .option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
107
107
  .option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
@@ -115,88 +115,137 @@ export function registerSecretsMigrateAclCommand(secrets) {
115
115
  if (!prefix.startsWith(ITEM_PREFIX)) {
116
116
  throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
117
117
  }
118
- // Default: migrate ONLY legacy stragglers (items still in the file-based
119
- // keychain) modern DP items need no rewrite and touching them is a
120
- // Touch ID prompt per item for nothing. `--all` forces the old full
121
- // rewrite. Either way this is one command over every matching item — no
122
- // one-by-one invocation.
118
+ // Two independent classes of work under one command:
119
+ // (a) Legacy ACL stragglers items still in the file-based keychain
120
+ // with a pre-migration trusted-app ACL. Rewritten value-by-value.
121
+ // (b) Orphaned access groups data-protection items filed under a
122
+ // pre-#279 non-concrete group, invisible to the pinned queries.
123
+ // Re-homed by the helper behind a single Touch ID.
124
+ // Default: only legacy stragglers for (a); `--all` forces a full rewrite.
123
125
  const names = opts.all ? listKeychainItems(prefix) : listLegacyKeychainItems(prefix);
124
126
  const items = names.map((item) => {
125
127
  const localExists = hasKeychainToken(item);
126
128
  return { item, sync: !localExists };
127
129
  });
128
- if (items.length === 0) {
130
+ const orphans = listOrphanedKeychainItems(prefix);
131
+ if (items.length === 0 && orphans.length === 0) {
129
132
  console.log(opts.all
130
133
  ? chalk.gray(`No keychain items with prefix '${prefix}'.`)
131
- : chalk.green(`Nothing to migrate — all items under '${prefix}' already use the modern ACL.`));
134
+ : chalk.green(`Nothing to migrate — all items under '${prefix}' use the modern ACL and access group.`));
132
135
  return;
133
136
  }
134
137
  const label = opts.all ? 'item' : 'legacy item';
135
- console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' to migrate.`));
138
+ if (items.length > 0) {
139
+ console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' with an outdated ACL.`));
140
+ }
141
+ if (orphans.length > 0) {
142
+ console.log(chalk.bold(`Found ${orphans.length} orphaned item(s) under '${prefix}' in a stale access group.`));
143
+ }
136
144
  if (!opts.commit) {
137
145
  for (const { item, sync } of items) {
138
146
  console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
139
147
  }
148
+ for (const item of orphans) {
149
+ console.log(` ${chalk.cyan(item)} ${chalk.yellow('(orphaned access group)')}`);
150
+ }
140
151
  console.log();
141
152
  console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
142
153
  return;
143
154
  }
144
- // Commit phase. Snapshot every value first (one batched read behind a
145
- // single helper process, so DP items share one auth context), encrypt,
146
- // then mutate.
147
- const fetched = getKeychainTokens(items.map((i) => i.item));
148
- const records = [];
149
- for (const { item, sync } of items) {
150
- const value = fetched.get(item);
151
- if (value === undefined) {
152
- console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
153
- continue;
155
+ // ---- Commit phase ----
156
+ let okCount = 0;
157
+ let failCount = 0;
158
+ // (a) Legacy ACL rewrite. Snapshot every value first (one batched read
159
+ // behind a single helper process), encrypt, then delete + re-add. A total
160
+ // read failure here counts the legacy items as failed but does NOT abort —
161
+ // the orphan sweep (b) is independent and must still run.
162
+ if (items.length > 0) {
163
+ const fetched = getKeychainTokens(items.map((i) => i.item));
164
+ const records = [];
165
+ for (const { item, sync } of items) {
166
+ const value = fetched.get(item);
167
+ if (value === undefined) {
168
+ console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
169
+ continue;
170
+ }
171
+ records.push({ item, sync, value });
154
172
  }
155
- records.push({ item, sync, value });
156
- }
157
- if (records.length === 0) {
158
- console.error(chalk.red('No items could be read. Aborting before any writes.'));
159
- process.exit(1);
160
- }
161
- const passphrase = opts.passphraseEnv
162
- ? (() => {
163
- const v = process.env[opts.passphraseEnv];
164
- if (!v)
165
- throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
166
- if (v.length < MIN_PASSPHRASE_LEN)
167
- throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
168
- return v;
169
- })()
170
- : await promptPassphrase();
171
- const backupPath = writeEncryptedBackup(records, passphrase);
172
- // Compute a quick fingerprint so the user can sanity-check recovery without
173
- // decrypting. Hash of (count, sorted item names).
174
- const fingerprint = crypto
175
- .createHash('sha256')
176
- .update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
177
- .digest('hex')
178
- .slice(0, 12);
179
- console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
180
- const results = [];
181
- for (const record of records) {
182
- const r = migrateOne(record);
183
- results.push(r);
184
- if (r.status === 'ok') {
185
- console.log(` ${chalk.green('ok')} ${record.item}`);
173
+ if (records.length === 0) {
174
+ console.error(chalk.red('No legacy items could be read — skipping legacy rewrite.'));
175
+ failCount += items.length;
186
176
  }
187
177
  else {
188
- console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
178
+ const passphrase = opts.passphraseEnv
179
+ ? (() => {
180
+ const v = process.env[opts.passphraseEnv];
181
+ if (!v)
182
+ throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
183
+ if (v.length < MIN_PASSPHRASE_LEN)
184
+ throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
185
+ return v;
186
+ })()
187
+ : await promptPassphrase();
188
+ const backupPath = writeEncryptedBackup(records, passphrase);
189
+ // Quick fingerprint so the user can sanity-check recovery without decrypting.
190
+ const fingerprint = crypto
191
+ .createHash('sha256')
192
+ .update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
193
+ .digest('hex')
194
+ .slice(0, 12);
195
+ console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
196
+ const results = [];
197
+ for (const record of records) {
198
+ const r = migrateOne(record);
199
+ results.push(r);
200
+ if (r.status === 'ok') {
201
+ console.log(` ${chalk.green('ok')} ${record.item}`);
202
+ }
203
+ else {
204
+ console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
205
+ }
206
+ }
207
+ const ok = results.filter((r) => r.status === 'ok').length;
208
+ okCount += ok;
209
+ failCount += results.length - ok;
210
+ if (results.length - ok > 0) {
211
+ console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
212
+ }
213
+ }
214
+ }
215
+ // (b) Orphan re-home — one helper call, one Touch ID for the batch. The
216
+ // helper adds the pinned copy before deleting the orphan, so no pre-write
217
+ // backup is needed (a failed add leaves the orphan intact and readable).
218
+ if (orphans.length > 0) {
219
+ console.log(chalk.bold(`Re-homing ${orphans.length} orphaned item(s) into the current access group (one Touch ID)…`));
220
+ const results = migrateOrphanedKeychainItems(prefix);
221
+ const healed = new Set();
222
+ for (const r of results) {
223
+ healed.add(r.item);
224
+ if (r.status === 'ok') {
225
+ okCount += 1;
226
+ console.log(` ${chalk.green('ok')} ${r.item}`);
227
+ }
228
+ else {
229
+ failCount += 1;
230
+ console.log(` ${chalk.red(r.status)} ${r.item} ${chalk.gray(r.detail ?? '')}`);
231
+ }
232
+ }
233
+ // Any orphan we listed but the helper couldn't reach (e.g. under a
234
+ // different signing team) — surface it, never drop it silently.
235
+ for (const item of orphans) {
236
+ if (!healed.has(item)) {
237
+ failCount += 1;
238
+ console.log(` ${chalk.red('fail')} ${item} ${chalk.gray('unreachable (different signing team?)')}`);
239
+ }
189
240
  }
190
241
  }
191
- const okCount = results.filter((r) => r.status === 'ok').length;
192
- const failCount = results.length - okCount;
242
+ const total = okCount + failCount;
193
243
  console.log();
194
244
  if (failCount === 0) {
195
- console.log(chalk.green(`Migrated ${okCount}/${results.length} item(s).`));
245
+ console.log(chalk.green(`Migrated ${okCount}/${total} item(s).`));
196
246
  }
197
247
  else {
198
- console.error(chalk.yellow(`Migrated ${okCount}/${results.length} item(s); ${failCount} failed.`));
199
- console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
248
+ console.error(chalk.yellow(`Migrated ${okCount}/${total} item(s); ${failCount} failed.`));
200
249
  process.exit(1);
201
250
  }
202
251
  }
@@ -7,7 +7,16 @@
7
7
  */
8
8
  import { type Command } from 'commander';
9
9
  import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
10
+ import { quoteWin32ExecArg } from '../lib/platform/index.js';
11
+ import { type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
10
12
  export { SSH_TARGET_RE, assertValidSshTarget };
13
+ /**
14
+ * Build the child environment for `agents secrets exec`. Strips
15
+ * loader/interpreter hijack vars (matching agent spawns in exec.ts) and never
16
+ * forwards AGENTS_SECRETS_PASSPHRASE — the master decryption key must not reach
17
+ * the executed command.
18
+ */
19
+ export declare function buildSecretsExecEnv(parentEnv: NodeJS.ProcessEnv, secretEnv: Record<string, string>): NodeJS.ProcessEnv;
11
20
  /**
12
21
  * Serialize a resolved env map to `.env` lines that round-trip losslessly through
13
22
  * `parseDotenv` on the remote: `KEY="VALUE"`. parseDotenv strips exactly one outer
@@ -16,25 +25,29 @@ export { SSH_TARGET_RE, assertValidSshTarget };
16
25
  * parse, so multi-line values are rejected rather than silently corrupted.
17
26
  */
18
27
  export declare function bundleEnvToDotenv(env: Record<string, string>): string;
28
+ /** The POLICY column for `secrets list`: the prompt policy, plus a "Nh left"
29
+ * hint when a `daily` bundle is currently held by the secrets-agent. `held`
30
+ * maps bundle name → expiry epoch-ms (from agentStatus()). */
31
+ export declare function renderPolicyCol(b: SecretsBundle, held?: Map<string, number>): string;
19
32
  /** Register the `agents secrets` command tree. */
20
33
  export declare function registerSecretsCommands(program: Command): void;
34
+ /** Validate a prompt-policy value, throwing a clear message on a bad one (the
35
+ * caller's try/catch renders it and exits). Accepts the legacy `biometry` /
36
+ * `session` / `none` tokens as aliases for `always` / `daily` / `never` so older
37
+ * flags and scripts keep working. `never`/`none` is the no-biometry-ACL tier —
38
+ * accepted here, then gated behind a loud confirmation in the command layer. */
39
+ export declare function parsePolicyOpt(raw: string | undefined): SecretsPolicy;
21
40
  /**
22
- * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
23
- * `spawn(..., { shell: true })` on win32 (`agents secrets exec`). cmd.exe does
24
- * NO quoting of its own, so an unquoted arg with a space is split into several
25
- * args, and a cmd metacharacter (`&|<>()^`) would be interpreted by the shell.
26
- * We wrap any arg with whitespace, a quote, or a metacharacter in double quotes
27
- * and escape embedded quotes / trailing backslashes per the CommandLineToArgvW
28
- * rules, so the *child's* argv parse reconstructs the original argument.
29
- *
30
- * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
31
- * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
32
- * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
33
- * `agents secrets exec` runs a caller-supplied command against a bundle the
34
- * caller owns, so caller-controlled `%`/`!` is not a privilege boundary. If that
35
- * ever changes (exec'ing an untrusted command line), route through a shell that
36
- * disables expansion rather than relying on this quoter. An empty arg becomes
37
- * `""`. Exported for tests. No-ops on non-Windows (the caller only invokes it
38
- * under `process.platform === 'win32'`).
41
+ * Gate a create/switch to the `never` prompt-policy behind an explicit,
42
+ * deliberate acknowledgement it is the least-safe tier (no user-presence
43
+ * check on any read). Returns:
44
+ * - `'ok'` — not `never`, or already acknowledged via `--i-understand`.
45
+ * - `'prompt'` interactive shell: caller must run a loud confirm prompt.
46
+ * Throws in a non-interactive shell when the flag is absent, so a headless
47
+ * `create --policy never` can't silently downgrade a bundle's protection.
39
48
  */
40
- export declare function quoteWin32ExecArg(arg: string): string;
49
+ export declare function assertNeverPolicyAcknowledged(policy: SecretsPolicy | undefined, opts: {
50
+ iUnderstand?: boolean;
51
+ interactive: boolean;
52
+ }): 'ok' | 'prompt';
53
+ export { quoteWin32ExecArg };