@phnx-labs/agents-cli 1.20.35 → 1.20.37

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 (236) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/LICENSE +185 -21
  3. package/README.md +8 -4
  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.js +81 -0
  11. package/dist/commands/daemon.js +4 -1
  12. package/dist/commands/doctor.js +1 -89
  13. package/dist/commands/events.js +3 -3
  14. package/dist/commands/exec.d.ts +19 -0
  15. package/dist/commands/exec.js +277 -11
  16. package/dist/commands/hosts.js +10 -6
  17. package/dist/commands/inspect.js +8 -9
  18. package/dist/commands/lock.d.ts +12 -0
  19. package/dist/commands/lock.js +70 -0
  20. package/dist/commands/message.d.ts +15 -0
  21. package/dist/commands/message.js +56 -0
  22. package/dist/commands/routines.js +101 -5
  23. package/dist/commands/secrets-migrate.js +106 -57
  24. package/dist/commands/secrets.d.ts +31 -18
  25. package/dist/commands/secrets.js +156 -75
  26. package/dist/commands/serve.d.ts +10 -0
  27. package/dist/commands/serve.js +37 -0
  28. package/dist/commands/sessions-inject.d.ts +14 -0
  29. package/dist/commands/sessions-inject.js +111 -0
  30. package/dist/commands/sessions-picker.d.ts +2 -0
  31. package/dist/commands/sessions-picker.js +24 -3
  32. package/dist/commands/sessions-resume.js +20 -8
  33. package/dist/commands/sessions-sync.d.ts +3 -0
  34. package/dist/commands/sessions-sync.js +44 -4
  35. package/dist/commands/sessions.d.ts +78 -1
  36. package/dist/commands/sessions.js +506 -66
  37. package/dist/commands/setup.js +4 -2
  38. package/dist/commands/sync.d.ts +3 -1
  39. package/dist/commands/sync.js +156 -4
  40. package/dist/commands/teams.js +217 -0
  41. package/dist/commands/versions.js +2 -4
  42. package/dist/commands/watchdog.d.ts +18 -0
  43. package/dist/commands/watchdog.js +238 -0
  44. package/dist/index.js +25 -2
  45. package/dist/lib/audit/log.d.ts +92 -0
  46. package/dist/lib/audit/log.js +177 -0
  47. package/dist/lib/auto-pull.js +2 -1
  48. package/dist/lib/browser/chrome.d.ts +10 -0
  49. package/dist/lib/browser/chrome.js +18 -7
  50. package/dist/lib/browser/drivers/ssh.js +2 -1
  51. package/dist/lib/browser/har.d.ts +84 -0
  52. package/dist/lib/browser/har.js +77 -0
  53. package/dist/lib/browser/ipc.js +24 -3
  54. package/dist/lib/browser/profiles.d.ts +1 -1
  55. package/dist/lib/browser/profiles.js +8 -10
  56. package/dist/lib/browser/refs.d.ts +65 -0
  57. package/dist/lib/browser/refs.js +73 -1
  58. package/dist/lib/browser/runtime-state.js +1 -0
  59. package/dist/lib/browser/service.d.ts +38 -2
  60. package/dist/lib/browser/service.js +112 -8
  61. package/dist/lib/browser/types.d.ts +14 -1
  62. package/dist/lib/budget/live-cloud.d.ts +42 -0
  63. package/dist/lib/budget/live-cloud.js +79 -0
  64. package/dist/lib/budget/live-team.d.ts +31 -0
  65. package/dist/lib/budget/live-team.js +115 -0
  66. package/dist/lib/cloud/codex.js +4 -0
  67. package/dist/lib/cloud/rush.d.ts +12 -1
  68. package/dist/lib/cloud/rush.js +13 -3
  69. package/dist/lib/cloud/types.d.ts +9 -0
  70. package/dist/lib/computer/dispatch.d.ts +8 -0
  71. package/dist/lib/computer/dispatch.js +125 -0
  72. package/dist/lib/computer/loop.d.ts +62 -0
  73. package/dist/lib/computer/loop.js +98 -0
  74. package/dist/lib/computer/model.d.ts +44 -0
  75. package/dist/lib/computer/model.js +157 -0
  76. package/dist/lib/concurrency.d.ts +19 -0
  77. package/dist/lib/concurrency.js +33 -0
  78. package/dist/lib/daemon.d.ts +57 -0
  79. package/dist/lib/daemon.js +196 -18
  80. package/dist/lib/devices/registry.d.ts +7 -0
  81. package/dist/lib/devices/registry.js +24 -0
  82. package/dist/lib/devices/resolve-target.d.ts +24 -0
  83. package/dist/lib/devices/resolve-target.js +80 -0
  84. package/dist/lib/devices/tailscale.js +1 -1
  85. package/dist/lib/drift.d.ts +52 -0
  86. package/dist/lib/drift.js +112 -0
  87. package/dist/lib/events.d.ts +1 -1
  88. package/dist/lib/events.js +31 -13
  89. package/dist/lib/exec.d.ts +17 -0
  90. package/dist/lib/exec.js +79 -13
  91. package/dist/lib/git.d.ts +27 -0
  92. package/dist/lib/git.js +56 -1
  93. package/dist/lib/hooks/cache.d.ts +6 -0
  94. package/dist/lib/hooks/cache.js +54 -12
  95. package/dist/lib/hooks.d.ts +27 -0
  96. package/dist/lib/hooks.js +127 -8
  97. package/dist/lib/hosts/dispatch.d.ts +15 -0
  98. package/dist/lib/hosts/dispatch.js +39 -6
  99. package/dist/lib/hosts/logs.js +30 -1
  100. package/dist/lib/hosts/option.js +1 -1
  101. package/dist/lib/hosts/passthrough.js +3 -1
  102. package/dist/lib/hosts/ready.d.ts +29 -6
  103. package/dist/lib/hosts/ready.js +66 -15
  104. package/dist/lib/hosts/registry.d.ts +19 -2
  105. package/dist/lib/hosts/registry.js +58 -2
  106. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  107. package/dist/lib/hosts/remote-cmd.js +70 -1
  108. package/dist/lib/hosts/remote-os.d.ts +17 -0
  109. package/dist/lib/hosts/remote-os.js +30 -0
  110. package/dist/lib/hosts/session-index.d.ts +34 -0
  111. package/dist/lib/hosts/session-index.js +56 -0
  112. package/dist/lib/hosts/tasks.d.ts +14 -0
  113. package/dist/lib/hosts/tasks.js +15 -0
  114. package/dist/lib/lock.d.ts +93 -0
  115. package/dist/lib/lock.js +207 -0
  116. package/dist/lib/loop.js +16 -1
  117. package/dist/lib/machine-id.d.ts +21 -0
  118. package/dist/lib/machine-id.js +26 -0
  119. package/dist/lib/mailbox-target.d.ts +36 -0
  120. package/dist/lib/mailbox-target.js +45 -0
  121. package/dist/lib/mailbox.d.ts +47 -0
  122. package/dist/lib/mailbox.js +194 -0
  123. package/dist/lib/mcp.d.ts +5 -0
  124. package/dist/lib/mcp.js +24 -8
  125. package/dist/lib/migrate.d.ts +19 -0
  126. package/dist/lib/migrate.js +134 -26
  127. package/dist/lib/overdue.js +3 -0
  128. package/dist/lib/picker.d.ts +2 -0
  129. package/dist/lib/picker.js +4 -1
  130. package/dist/lib/platform/exec.d.ts +46 -0
  131. package/dist/lib/platform/exec.js +74 -0
  132. package/dist/lib/platform/process.d.ts +31 -0
  133. package/dist/lib/platform/process.js +34 -1
  134. package/dist/lib/platform/winpath.js +2 -0
  135. package/dist/lib/plugins.js +16 -6
  136. package/dist/lib/profiles.d.ts +25 -0
  137. package/dist/lib/profiles.js +22 -6
  138. package/dist/lib/pty-client.js +2 -1
  139. package/dist/lib/rotate.d.ts +61 -0
  140. package/dist/lib/rotate.js +52 -0
  141. package/dist/lib/routines.d.ts +40 -2
  142. package/dist/lib/routines.js +66 -8
  143. package/dist/lib/runner.d.ts +11 -2
  144. package/dist/lib/runner.js +49 -7
  145. package/dist/lib/scheduler.js +6 -1
  146. package/dist/lib/secrets/bundles.d.ts +60 -4
  147. package/dist/lib/secrets/bundles.js +131 -12
  148. package/dist/lib/secrets/filestore.d.ts +3 -0
  149. package/dist/lib/secrets/filestore.js +42 -16
  150. package/dist/lib/secrets/index.d.ts +43 -2
  151. package/dist/lib/secrets/index.js +102 -3
  152. package/dist/lib/secrets/mcp.d.ts +93 -0
  153. package/dist/lib/secrets/mcp.js +205 -0
  154. package/dist/lib/secrets/remote.js +12 -5
  155. package/dist/lib/secrets/sync.js +83 -4
  156. package/dist/lib/secrets/windows.js +14 -3
  157. package/dist/lib/serve/data.d.ts +81 -0
  158. package/dist/lib/serve/data.js +91 -0
  159. package/dist/lib/serve/page.d.ts +7 -0
  160. package/dist/lib/serve/page.js +140 -0
  161. package/dist/lib/serve/server.d.ts +46 -0
  162. package/dist/lib/serve/server.js +115 -0
  163. package/dist/lib/session/active.d.ts +73 -0
  164. package/dist/lib/session/active.js +199 -22
  165. package/dist/lib/session/db.d.ts +2 -1
  166. package/dist/lib/session/db.js +41 -5
  167. package/dist/lib/session/discover.d.ts +39 -0
  168. package/dist/lib/session/discover.js +127 -29
  169. package/dist/lib/session/ghostty-tabs.d.ts +33 -0
  170. package/dist/lib/session/ghostty-tabs.js +126 -0
  171. package/dist/lib/session/inject.d.ts +18 -0
  172. package/dist/lib/session/inject.js +21 -0
  173. package/dist/lib/session/parse.js +23 -20
  174. package/dist/lib/session/pid-registry.d.ts +1 -0
  175. package/dist/lib/session/pid-registry.js +24 -0
  176. package/dist/lib/session/provenance.d.ts +14 -2
  177. package/dist/lib/session/provenance.js +39 -8
  178. package/dist/lib/session/relative-time.js +6 -2
  179. package/dist/lib/session/remote-active.js +19 -17
  180. package/dist/lib/session/remote-list.d.ts +51 -0
  181. package/dist/lib/session/remote-list.js +205 -0
  182. package/dist/lib/session/remote.d.ts +7 -1
  183. package/dist/lib/session/remote.js +20 -4
  184. package/dist/lib/session/sync/config.d.ts +14 -15
  185. package/dist/lib/session/sync/config.js +60 -20
  186. package/dist/lib/session/types.d.ts +23 -0
  187. package/dist/lib/shims.d.ts +36 -6
  188. package/dist/lib/shims.js +91 -29
  189. package/dist/lib/ssh-exec.js +2 -0
  190. package/dist/lib/ssh-tunnel.js +2 -1
  191. package/dist/lib/startup/command-registry.d.ts +6 -0
  192. package/dist/lib/startup/command-registry.js +13 -1
  193. package/dist/lib/state.d.ts +13 -0
  194. package/dist/lib/state.js +103 -9
  195. package/dist/lib/sync-umbrella.d.ts +14 -7
  196. package/dist/lib/sync-umbrella.js +21 -13
  197. package/dist/lib/teams/forEach.d.ts +110 -0
  198. package/dist/lib/teams/forEach.js +186 -0
  199. package/dist/lib/teams/index.d.ts +1 -0
  200. package/dist/lib/teams/index.js +1 -0
  201. package/dist/lib/teams/pr-watch.d.ts +226 -0
  202. package/dist/lib/teams/pr-watch.js +371 -0
  203. package/dist/lib/teams/supervisor.d.ts +14 -1
  204. package/dist/lib/teams/supervisor.js +19 -0
  205. package/dist/lib/teams/worktree.d.ts +9 -0
  206. package/dist/lib/teams/worktree.js +32 -0
  207. package/dist/lib/terminal/backends/index.d.ts +2 -1
  208. package/dist/lib/terminal/backends/index.js +3 -1
  209. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  210. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  211. package/dist/lib/terminal/index.d.ts +4 -1
  212. package/dist/lib/terminal/index.js +4 -1
  213. package/dist/lib/terminal/inject.d.ts +204 -0
  214. package/dist/lib/terminal/inject.js +247 -0
  215. package/dist/lib/terminal/resolve.d.ts +64 -0
  216. package/dist/lib/terminal/resolve.js +90 -0
  217. package/dist/lib/terminal/types.d.ts +1 -1
  218. package/dist/lib/tmux/session.d.ts +10 -0
  219. package/dist/lib/tmux/session.js +31 -0
  220. package/dist/lib/triggers/webhook.d.ts +85 -0
  221. package/dist/lib/triggers/webhook.js +141 -0
  222. package/dist/lib/versions.d.ts +23 -0
  223. package/dist/lib/versions.js +119 -13
  224. package/dist/lib/watchdog/index.d.ts +3 -0
  225. package/dist/lib/watchdog/index.js +5 -0
  226. package/dist/lib/watchdog/read.d.ts +35 -0
  227. package/dist/lib/watchdog/read.js +149 -0
  228. package/dist/lib/watchdog/runner.d.ts +127 -0
  229. package/dist/lib/watchdog/runner.js +322 -0
  230. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  231. package/dist/lib/watchdog/watchdog.js +166 -0
  232. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  233. package/dist/lib/watchdog/watchdogTail.js +154 -0
  234. package/dist/lib/workflows.d.ts +166 -0
  235. package/dist/lib/workflows.js +193 -0
  236. package/package.json +5 -4
@@ -10,6 +10,8 @@ import { BrowserDaemonNotRunningError, formatBrowserDaemonNotRunningError, sendI
10
10
  import { browserTaskPicker } from './browser-picker.js';
11
11
  import { isInteractiveTerminal } from './utils.js';
12
12
  import { registerCommandGroups, setHelpSections } from '../lib/help.js';
13
+ import { buildHar } from '../lib/browser/har.js';
14
+ import { getCliVersion } from '../lib/version.js';
13
15
  /**
14
16
  * Resolve which browser task a command targets. Order:
15
17
  * 1. `--task <name>` flag (explicit per-command override)
@@ -44,7 +46,7 @@ const BROWSER_HELP_GROUPS = [
44
46
  },
45
47
  {
46
48
  title: 'Capture evidence',
47
- names: ['console', 'errors', 'requests', 'responsebody', 'record', 'logs'],
49
+ names: ['console', 'errors', 'requests', 'responsebody', 'record', 'pdf', 'logs'],
48
50
  },
49
51
  ];
50
52
  export function registerBrowserCommand(program) {
@@ -766,6 +768,31 @@ function registerTaskCommands(browser) {
766
768
  console.error(`Tip: auto-saving to ${dir}. Pass --output <path> to choose a path.`);
767
769
  }
768
770
  });
771
+ browser
772
+ .command('pdf [output]')
773
+ .description('Export the current tab as PDF via CDP Page.printToPDF — auto-saved under sessions/<task>/ when [output] is omitted')
774
+ .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
775
+ .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
776
+ .action(async (output, opts) => {
777
+ const task = resolveTaskName(opts);
778
+ const response = await sendIPCRequest({
779
+ action: 'pdf',
780
+ task,
781
+ tabId: opts.tab,
782
+ path: output,
783
+ });
784
+ if (!response.ok) {
785
+ console.error(response.error);
786
+ process.exit(1);
787
+ }
788
+ console.log(response.path);
789
+ const size = humanizeBytes(response.bytes);
790
+ console.error(`Saved PDF to ${response.path} (${size})`);
791
+ if (!output && response.path) {
792
+ const dir = path.dirname(response.path);
793
+ console.error(`Tip: auto-saving to ${dir}. Pass a path argument to choose one.`);
794
+ }
795
+ });
769
796
  browser
770
797
  .command('evaluate')
771
798
  .description('Evaluate JavaScript in current tab')
@@ -1129,6 +1156,16 @@ function registerTaskCommands(browser) {
1129
1156
  }
1130
1157
  process.exit(1);
1131
1158
  }
1159
+ // Warn (on stderr, so it never corrupts --json stdout) when the a11y tree
1160
+ // exposed nothing to click — the caller should fall back to a screenshot
1161
+ // plus coordinate click.
1162
+ const count = response.nodes?.length ?? 0;
1163
+ if (count === 0) {
1164
+ const scope = opts.all ? 'elements' : 'interactive elements';
1165
+ console.error(`No ${scope} found. The page may render its UI on a canvas or in a custom widget ` +
1166
+ `the accessibility tree doesn't expose. Take a screenshot ` +
1167
+ `(\`browser screenshot\`) and click by position with \`browser click --at X,Y\`.`);
1168
+ }
1132
1169
  if (opts.json) {
1133
1170
  console.log(JSON.stringify(response.nodes ?? [], null, 2));
1134
1171
  return;
@@ -1136,23 +1173,46 @@ function registerTaskCommands(browser) {
1136
1173
  console.log(response.refs);
1137
1174
  });
1138
1175
  browser
1139
- .command('click <ref>')
1140
- .description('Click an element by ref')
1176
+ .command('click [ref]')
1177
+ .description('Click an element by ref, or raw coordinates with --at X,Y')
1141
1178
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1142
1179
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1180
+ .option('--at <x,y>', 'Click viewport coordinates (e.g. --at 320,540), bypassing ref resolution')
1143
1181
  .action(async (ref, opts) => {
1144
1182
  const task = resolveTaskName(opts);
1183
+ let requestExtra;
1184
+ if (opts.at !== undefined) {
1185
+ const parts = String(opts.at).split(',').map((s) => Number(s.trim()));
1186
+ if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n))) {
1187
+ console.error(`Invalid --at value "${opts.at}". Expected X,Y (e.g. --at 320,540).`);
1188
+ process.exit(1);
1189
+ }
1190
+ requestExtra = { atX: parts[0], atY: parts[1] };
1191
+ }
1192
+ else if (ref !== undefined) {
1193
+ const parsed = parseInt(ref, 10);
1194
+ if (!Number.isFinite(parsed)) {
1195
+ console.error(`Invalid ref "${ref}". Pass an integer ref or use --at X,Y.`);
1196
+ process.exit(1);
1197
+ }
1198
+ requestExtra = { ref: parsed };
1199
+ }
1200
+ else {
1201
+ console.error('Provide a ref (e.g. `click 5`) or coordinates (`click --at X,Y`).');
1202
+ process.exit(1);
1203
+ return;
1204
+ }
1145
1205
  const response = await sendIPCRequest({
1146
1206
  action: 'click',
1147
1207
  task,
1148
1208
  tabId: opts.tab,
1149
- ref: parseInt(ref, 10),
1209
+ ...requestExtra,
1150
1210
  });
1151
1211
  if (!response.ok) {
1152
1212
  console.error(response.error);
1153
1213
  process.exit(1);
1154
1214
  }
1155
- console.log('Clicked');
1215
+ console.log(response.message ? `Clicked (${response.message})` : 'Clicked');
1156
1216
  });
1157
1217
  browser
1158
1218
  .command('type <ref>')
@@ -1423,13 +1483,18 @@ function registerTaskCommands(browser) {
1423
1483
  // ─── Network ─────────────────────────────────────────────────────────────────
1424
1484
  browser
1425
1485
  .command('requests')
1426
- .description('Read captured network requests')
1486
+ .description('Read captured network requests. --format har emits a HAR 1.2 JSON document.')
1427
1487
  .option(TASK_OPTION_FLAG, TASK_OPTION_DESC)
1428
1488
  .option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
1429
1489
  .option('-f, --filter <text>', 'Filter URLs containing text')
1430
1490
  .option('--clear', 'Clear requests after reading')
1491
+ .option('--format <format>', 'Output format: table (default) or har', 'table')
1431
1492
  .action(async (opts) => {
1432
1493
  const task = resolveTaskName(opts);
1494
+ if (opts.format !== 'table' && opts.format !== 'har') {
1495
+ console.error('--format must be "table" or "har"');
1496
+ process.exit(1);
1497
+ }
1433
1498
  const response = await sendIPCRequest({
1434
1499
  action: 'requests',
1435
1500
  task,
@@ -1441,13 +1506,22 @@ function registerTaskCommands(browser) {
1441
1506
  console.error(response.error);
1442
1507
  process.exit(1);
1443
1508
  }
1444
- if (!response.requests || response.requests.length === 0) {
1509
+ const requests = response.requests ?? [];
1510
+ if (opts.format === 'har') {
1511
+ const har = buildHar(requests, {
1512
+ creatorName: 'agents-cli',
1513
+ creatorVersion: getCliVersion(),
1514
+ });
1515
+ console.log(JSON.stringify(har, null, 2));
1516
+ return;
1517
+ }
1518
+ if (requests.length === 0) {
1445
1519
  console.log('No requests captured');
1446
1520
  return;
1447
1521
  }
1448
1522
  console.log('METHOD'.padEnd(8) + 'STATUS'.padEnd(8) + 'URL');
1449
1523
  console.log('-'.repeat(72));
1450
- for (const req of response.requests) {
1524
+ for (const req of requests) {
1451
1525
  const status = req.status ? String(req.status) : '...';
1452
1526
  console.log(`${req.method.padEnd(8)}${status.padEnd(8)}${req.url.slice(0, 100)}`);
1453
1527
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * `agents check` — scriptable drift gate for CI.
3
+ *
4
+ * Runs the SAME drift/divergence diagnostic `agents doctor` computes (via the
5
+ * shared `computeDrift` in `../lib/drift.js`) and turns it into an exit code:
6
+ * non-zero when any installed version is stale or never-synced, zero when the
7
+ * whole install is fresh. `agents doctor` is the human report; `agents check`
8
+ * is the machine gate — same engine, different output.
9
+ *
10
+ * Orphans are surfaced informationally but never fail the check: they are a
11
+ * `prune` concern, not sync drift (mirrors the sync-status engine, where an
12
+ * orphan alone never flags needsSync).
13
+ */
14
+ import type { Command } from 'commander';
15
+ export declare function registerCheckCommand(program: Command): void;
@@ -0,0 +1,84 @@
1
+ import chalk from 'chalk';
2
+ import { AGENTS, ALL_AGENT_IDS } from '../lib/agents.js';
3
+ import { setHelpSections } from '../lib/help.js';
4
+ import { computeDrift } from '../lib/drift.js';
5
+ const AGENT_NAMES = Object.fromEntries(ALL_AGENT_IDS.map((id) => [id, AGENTS[id].name]));
6
+ function label(row) {
7
+ return `${AGENT_NAMES[row.agent] || row.agent}@${row.version}`;
8
+ }
9
+ export function registerCheckCommand(program) {
10
+ const checkCmd = program
11
+ .command('check')
12
+ .description('CI drift gate: exit non-zero when any installed version is out of sync (stale or never-synced), zero when clean.')
13
+ .option('--json', 'Output machine-readable JSON')
14
+ .option('-q, --quiet', 'Suppress per-version lines; print only the one-line verdict')
15
+ .option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())');
16
+ setHelpSections(checkCmd, {
17
+ examples: `
18
+ # Fail the build if anything drifted (exit 1), pass if clean (exit 0)
19
+ agents check
20
+
21
+ # Just the verdict line, nothing per-version
22
+ agents check --quiet
23
+
24
+ # Machine-readable, for scripting
25
+ agents check --json
26
+
27
+ # Gate in a CI step
28
+ agents check || { echo "resources drifted — run 'agents doctor --fix'"; exit 1; }
29
+ `,
30
+ });
31
+ checkCmd.action((opts) => {
32
+ const cwd = opts.cwd ? opts.cwd : process.cwd();
33
+ const drift = computeDrift(cwd);
34
+ if (opts.json) {
35
+ console.log(JSON.stringify({
36
+ hasDrift: drift.hasDrift,
37
+ stale: drift.staleCount,
38
+ neverSynced: drift.neverSyncedCount,
39
+ orphanVersions: drift.orphanVersionCount,
40
+ versions: drift.syncRows.map((r) => ({
41
+ agent: r.agent,
42
+ version: r.version,
43
+ status: r.status,
44
+ isDefault: r.isDefault,
45
+ divergence: r.divergence ?? [],
46
+ })),
47
+ }, null, 2));
48
+ process.exit(drift.hasDrift ? 1 : 0);
49
+ }
50
+ if (drift.syncRows.length === 0) {
51
+ // Nothing installed is a clean state, not a failure — CI on a fresh
52
+ // checkout with no versions should pass, not error.
53
+ console.log(chalk.gray('check: no installed versions — nothing to verify'));
54
+ process.exit(0);
55
+ }
56
+ if (!drift.hasDrift) {
57
+ const orphanNote = drift.orphanVersionCount > 0
58
+ ? chalk.gray(` (${drift.orphanVersionCount} version(s) carry orphans — run \`agents prune cleanup\`)`)
59
+ : '';
60
+ console.log(`${chalk.green('ok')} ${drift.syncRows.length} version(s) in sync${orphanNote}`);
61
+ process.exit(0);
62
+ }
63
+ // Drift: one-line verdict always, per-version detail unless --quiet.
64
+ const parts = [];
65
+ if (drift.staleCount > 0)
66
+ parts.push(`${drift.staleCount} stale`);
67
+ if (drift.neverSyncedCount > 0)
68
+ parts.push(`${drift.neverSyncedCount} never-synced`);
69
+ console.error(`${chalk.red('drift')} ${parts.join(', ')} of ${drift.syncRows.length} version(s)`);
70
+ if (!opts.quiet) {
71
+ for (const row of drift.syncRows) {
72
+ if (row.status === 'fresh')
73
+ continue;
74
+ const tag = row.status === 'stale' ? chalk.yellow('stale') : chalk.gray('cold ');
75
+ console.error(` ${tag} ${label(row)}`);
76
+ for (const line of row.divergence ?? []) {
77
+ console.error(chalk.gray(` ${line}`));
78
+ }
79
+ }
80
+ console.error(chalk.gray('\nReconcile with `agents doctor --fix` (or `agents doctor <agent>@<version> --fix`).'));
81
+ }
82
+ process.exit(1);
83
+ });
84
+ }
@@ -1,10 +1,39 @@
1
1
  import chalk from 'chalk';
2
2
  import * as fs from 'fs';
3
+ import * as path from 'path';
3
4
  import ora from 'ora';
4
5
  import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/cloud/registry.js';
5
6
  import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
6
7
  import { renderStream } from '../lib/cloud/stream.js';
7
- import { MissingTargetError } from '../lib/cloud/types.js';
8
+ import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from '../lib/cloud/types.js';
9
+ import { normalizeTriggerEvent, validateTrigger, writeJob, jobExists, GITHUB_TRIGGER_EVENTS } from '../lib/routines.js';
10
+ /** Map a supported image file extension to its wire mimeType. Rejects anything else. */
11
+ function imageMimeFromPath(file) {
12
+ const ext = path.extname(file).toLowerCase();
13
+ if (ext === '.png')
14
+ return 'image/png';
15
+ if (ext === '.jpg' || ext === '.jpeg')
16
+ return 'image/jpeg';
17
+ if (ext === '.webp')
18
+ return 'image/webp';
19
+ die(`Unsupported image type ${JSON.stringify(ext || file)}. Use .png, .jpg/.jpeg, or .webp.`);
20
+ }
21
+ /** Read one image file into a base64 ImageAttachment, dying with a clear error if it's missing. */
22
+ function readImageAttachment(file) {
23
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
24
+ die(`Image not found: ${file}`);
25
+ }
26
+ const mimeType = imageMimeFromPath(file);
27
+ return { data: fs.readFileSync(file).toString('base64'), mimeType };
28
+ }
29
+ /** Parse a `--skill <id>` value (`id` or `id@version`) into a SkillRef. */
30
+ function parseSkillRef(raw) {
31
+ const at = raw.lastIndexOf('@');
32
+ if (at > 0) {
33
+ return { id: raw.slice(0, at), version: raw.slice(at + 1) };
34
+ }
35
+ return { id: raw };
36
+ }
8
37
  /** Print an error message to stderr and exit. */
9
38
  function die(msg, code = 1) {
10
39
  console.error(chalk.red(msg));
@@ -146,6 +175,8 @@ Examples:
146
175
  return acc;
147
176
  })
148
177
  .option('--branch <name>', 'Target git branch')
178
+ .option('--on <event>', 'Register this run as an event trigger instead of dispatching now: pull_request (pr), push, issue_comment, workflow_run. Persists a trigger-bound routine.')
179
+ .option('--name <name>', 'Routine name to register under (with --on). Defaults to a generated name.')
149
180
  .option('-p, --prompt <text>', 'Inline prompt (alternative to positional argument)')
150
181
  .option('--timeout <duration>', 'Kill after duration (e.g., 30m, 2h)')
151
182
  .option('--model <model>', 'Model override')
@@ -153,6 +184,16 @@ Examples:
153
184
  .option('--computer <name>', 'Factory/Droid computer target')
154
185
  .option('--autonomy <level>', 'Factory/Droid autonomy: low, medium, high (default high)')
155
186
  .option('--mode <mode>', 'Execution mode (e.g., plan, edit, full)')
187
+ .option('--image <path>', `Attach an image (.png/.jpg/.webp) for vision dispatch. Repeatable, up to ${MAX_IMAGES_PER_DISPATCH} (Rush Cloud only).`, (value, previous) => {
188
+ const acc = Array.isArray(previous) ? previous : [];
189
+ acc.push(value);
190
+ return acc;
191
+ })
192
+ .option('--skill <id>', 'Ride-along skill by id (or id@version). Repeatable (Rush Cloud only).', (value, previous) => {
193
+ const acc = Array.isArray(previous) ? previous : [];
194
+ acc.push(value);
195
+ return acc;
196
+ })
156
197
  .option('-b, --balanced', 'Shortcut for --strategy balanced. Route the factory run across all healthy accounts.')
157
198
  .option('--strategy <strategy>', 'Account selection strategy for the factory: balanced. Sends all healthy accounts so the factory pod rotates between them on rate-limit.')
158
199
  .option('--upload-account-tokens', 'Upload Claude OAuth credentials to Rush Cloud on first dispatch (consent recorded for future runs).')
@@ -224,6 +265,73 @@ Examples:
224
265
  }
225
266
  if (options.uploadAccountTokens)
226
267
  dispatchOptions.providerOptions.uploadAccountTokens = true;
268
+ // --on <event>: register this run as an event trigger instead of
269
+ // dispatching now. We parse + validate the event, attach it to
270
+ // dispatchOptions.trigger, and persist a trigger-bound routine so the
271
+ // local webhook receiver can fire it (src/lib/triggers/webhook.ts).
272
+ // Remote firing of the trigger is a follow-up.
273
+ if (options.on) {
274
+ const event = normalizeTriggerEvent(options.on);
275
+ if (!event) {
276
+ die(`Unknown --on event "${options.on}". Use one of: ${GITHUB_TRIGGER_EVENTS.join(', ')} (aliases: pr, comment, workflow).`);
277
+ }
278
+ const trigger = { type: 'github_event', event: event };
279
+ if (repoValues[0])
280
+ trigger.repo = repoValues[0];
281
+ if (options.branch)
282
+ trigger.branch = options.branch;
283
+ const triggerErrors = validateTrigger(trigger);
284
+ if (triggerErrors.length > 0)
285
+ die(`Invalid trigger: ${triggerErrors.join(', ')}`);
286
+ dispatchOptions.trigger = trigger;
287
+ const routineName = options.name
288
+ || `cloud-${event}-${(repoValues[0] || 'any').replace(/[^a-z0-9]+/gi, '-')}`.toLowerCase();
289
+ if (jobExists(routineName)) {
290
+ die(`A routine named "${routineName}" already exists. Pass --name to register under a different name.`);
291
+ }
292
+ const routine = {
293
+ name: routineName,
294
+ trigger,
295
+ agent: options.agent || 'claude',
296
+ mode: 'plan',
297
+ effort: 'auto',
298
+ timeout: options.timeout || '10m',
299
+ enabled: true,
300
+ prompt,
301
+ };
302
+ if (repoValues[0])
303
+ routine.repo = repoValues[0];
304
+ writeJob(routine);
305
+ if (json) {
306
+ console.log(JSON.stringify({ ok: true, registered: routineName, trigger }, null, 2));
307
+ }
308
+ else {
309
+ console.log(chalk.green(`Registered trigger routine "${routineName}"`));
310
+ console.log(` ${chalk.dim('on:')} ${event}${trigger.repo ? ` (${trigger.repo}${trigger.branch ? `@${trigger.branch}` : ''})` : ''}`);
311
+ console.log(` ${chalk.dim('agent:')} ${routine.agent}`);
312
+ console.log(chalk.dim(`\nFires when a matching GitHub webhook is delivered to the local receiver. Remote firing is a follow-up.`));
313
+ }
314
+ return;
315
+ }
316
+ // Vision attachments + ride-along skills. Only wire them when the resolved
317
+ // provider advertises support — otherwise fail loud rather than silently
318
+ // drop the flags the user passed.
319
+ const imagePaths = Array.isArray(options.image) ? options.image : [];
320
+ const skillIds = Array.isArray(options.skill) ? options.skill : [];
321
+ const caps = provider.capabilities();
322
+ if (imagePaths.length > 0) {
323
+ if (!caps.images)
324
+ die(`${provider.name} does not support image attachments.`);
325
+ if (imagePaths.length > MAX_IMAGES_PER_DISPATCH) {
326
+ die(`Too many images: ${imagePaths.length}. Max is ${MAX_IMAGES_PER_DISPATCH} per dispatch.`);
327
+ }
328
+ dispatchOptions.images = imagePaths.map(readImageAttachment);
329
+ }
330
+ if (skillIds.length > 0) {
331
+ if (!caps.skills)
332
+ die(`${provider.name} does not support ride-along skills.`);
333
+ dispatchOptions.skills = skillIds.map(parseSkillRef);
334
+ }
227
335
  // Dispatch. On a missing pre-provisioned target (Codex env / Factory
228
336
  // computer), offer an interactive picker instead of a raw error.
229
337
  const dispatchOnce = async () => {
@@ -269,11 +377,29 @@ Examples:
269
377
  if (options.follow === false)
270
378
  return;
271
379
  try {
272
- const result = await renderStream(provider.stream(task.id), { json });
380
+ // Live budget kill-switch (issue #399). Reuses makeLiveSpendWatcher to
381
+ // feed the provider's `usage` events into a shared watcher; on a cap
382
+ // breach we call provider.cancel(task.id) mid-stream. Dormant (returns
383
+ // null) when no caps are configured, so the raw stream flows unchanged.
384
+ const { wrapStreamWithBudgetGate } = await import('../lib/budget/live-cloud.js');
385
+ const gated = wrapStreamWithBudgetGate({
386
+ provider,
387
+ taskId: task.id,
388
+ project: task.repo ?? task.repos?.[0] ?? process.cwd(),
389
+ agent: task.agent ?? 'cloud',
390
+ cwd: process.cwd(),
391
+ });
392
+ const eventSource = gated ? gated.wrap(provider.stream(task.id)) : provider.stream(task.id);
393
+ const result = await renderStream(eventSource, { json });
273
394
  updateTaskStatus(task.id, result.status, {
274
395
  summary: result.summary,
275
396
  prUrl: result.prUrl,
276
397
  });
398
+ if (gated?.gate.breached()) {
399
+ const b = gated.gate.breach();
400
+ process.stderr.write(`[budget] cap ${b?.cap} exceeded — cancelled cloud task ${task.id}\n`);
401
+ process.exitCode = 7; // Mirrors BUDGET_KILL_EXIT_CODE for CI/headless.
402
+ }
277
403
  }
278
404
  catch (err) {
279
405
  // Stream disconnect is OK — task keeps running
@@ -415,11 +541,25 @@ Examples:
415
541
  die(`Task ${id} not found locally. Run 'agents cloud list' first.`);
416
542
  const provider = resolveProvider(task.provider);
417
543
  try {
418
- const result = await renderStream(provider.stream(id), { json });
544
+ // Live budget kill-switch (issue #399) wrap the stream so a cap
545
+ // breach mid-stream cancels the task server-side. Dormant when no caps.
546
+ const { wrapStreamWithBudgetGate } = await import('../lib/budget/live-cloud.js');
547
+ const gated = wrapStreamWithBudgetGate({
548
+ provider,
549
+ taskId: id,
550
+ project: task.repo ?? task.repos?.[0] ?? process.cwd(),
551
+ agent: task.agent ?? 'cloud',
552
+ cwd: process.cwd(),
553
+ });
554
+ const eventSource = gated ? gated.wrap(provider.stream(id)) : provider.stream(id);
555
+ const result = await renderStream(eventSource, { json });
419
556
  updateTaskStatus(id, result.status, {
420
557
  summary: result.summary,
421
558
  prUrl: result.prUrl,
422
559
  });
560
+ if (gated?.gate.breached()) {
561
+ process.exitCode = 7;
562
+ }
423
563
  }
424
564
  catch (err) {
425
565
  process.stderr.write(chalk.dim(`\nStream ended. ${err.message}\n`));
@@ -6,10 +6,14 @@ import { registerCommandGroups } from '../lib/help.js';
6
6
  import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, resolveTcpEndpoint, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
7
7
  import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, } from '../lib/ssh-tunnel.js';
8
8
  import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
9
+ import { runComputerLoop } from '../lib/computer/loop.js';
10
+ import { makeVerbDispatcher } from '../lib/computer/dispatch.js';
11
+ import { makeClaudeResponder, resolveApiKey, DEFAULT_CLAUDE_MODEL, DEFAULT_CLAUDE_BASE_URL } from '../lib/computer/model.js';
9
12
  // Help groups — mirror `agents browser` so the mental model carries over.
10
13
  const COMPUTER_HELP_GROUPS = [
11
14
  { title: 'Installation', names: ['setup'] },
12
15
  { title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
16
+ { title: 'Autonomous', names: ['run'] },
13
17
  { title: 'Observe', names: ['apps', 'describe', 'screenshot', 'get-text'] },
14
18
  { title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
15
19
  ];
@@ -99,6 +103,7 @@ export function registerComputerSubcommands(program) {
99
103
  registerStopCommand(program);
100
104
  registerReloadCommand(program);
101
105
  registerStatusCommand(program);
106
+ registerRunCommand(program);
102
107
  registerScreenshotCommand(program);
103
108
  registerActionCommands(program);
104
109
  registerCommandGroups(program, COMPUTER_HELP_GROUPS);
@@ -154,6 +159,82 @@ function registerStatusCommand(program) {
154
159
  }
155
160
  });
156
161
  }
162
+ // run — the embedded observe -> act -> verify agent loop. A reasoning model
163
+ // (Claude API by default, or any Anthropic-shaped endpoint via --base-url for
164
+ // Ollama / vLLM / LiteLLM) drives the EXISTING computer verbs as tools over the
165
+ // daemon socket. New subcommand: the explicit verb interface external agents
166
+ // use is unchanged. The loop auto-switches to the screenshot/coordinate path
167
+ // when an app's AX tree comes back opaque (WebView / canvas).
168
+ function registerRunCommand(program) {
169
+ program
170
+ .command('run')
171
+ .description('Autonomously drive an app from a natural-language task (embedded model loop over the computer verbs)')
172
+ .requiredOption('--task <s>', 'Natural-language task, e.g. "open Notes and write a haiku"')
173
+ .option('--bundle <id>', 'Bundle id to focus the loop on (default: frontmost allow-listed app)')
174
+ .option('--base-url <url>', `Reasoning model base URL — Anthropic wire shape (default: ${DEFAULT_CLAUDE_BASE_URL}; set to a local Ollama/vLLM/LiteLLM endpoint for offline parity)`)
175
+ .option('--model <id>', `Model id (default: ${DEFAULT_CLAUDE_MODEL})`)
176
+ .option('--max-steps <n>', 'Max model turns before giving up', (v) => parseInt(v, 10), 12)
177
+ .option('--max-tokens <n>', 'Max tokens per model turn', (v) => parseInt(v, 10), 1024)
178
+ .option('--host <device>', 'Drive a remote Windows device (requires `agents computer start --host <device>` first)')
179
+ .option('--json', 'Emit the final loop result as JSON')
180
+ .action(async (opts) => {
181
+ const apiKey = resolveApiKey({ apiKey: undefined, baseUrl: opts.baseUrl });
182
+ // The Claude API needs a key; a local/offline endpoint (non-default base
183
+ // URL) usually ignores it, so only hard-fail on the default endpoint.
184
+ if (!apiKey && !opts.baseUrl) {
185
+ console.error('no API key. Set ANTHROPIC_API_KEY (or AGENTS_COMPUTER_API_KEY), or point --base-url at a local endpoint.');
186
+ process.exit(1);
187
+ }
188
+ const responder = makeClaudeResponder({
189
+ baseUrl: opts.baseUrl,
190
+ model: opts.model,
191
+ maxTokens: opts.maxTokens,
192
+ });
193
+ await withClient(async (client) => {
194
+ const dispatch = makeVerbDispatcher(client);
195
+ const targetInput = opts.bundle ? { bundle: opts.bundle } : {};
196
+ const result = await runComputerLoop({
197
+ task: opts.task,
198
+ responder: (state) => responder({ ...state, task: describeTaskWithTarget(opts.task, opts.bundle) }),
199
+ dispatch: (call) => dispatch({ ...call, input: { ...targetInput, ...call.input } }),
200
+ maxSteps: opts.maxSteps,
201
+ onEvent: opts.json ? undefined : (e) => printLoopEvent(e),
202
+ });
203
+ if (opts.json) {
204
+ console.log(JSON.stringify(result, null, 2));
205
+ return;
206
+ }
207
+ console.log('');
208
+ if (result.status === 'done') {
209
+ console.log(`done (${result.turns} turn${result.turns === 1 ? '' : 's'}): ${result.finalText ?? ''}`);
210
+ }
211
+ else {
212
+ console.log(`stopped: hit max-steps (${result.turns} turns) without a completion signal`);
213
+ }
214
+ });
215
+ });
216
+ }
217
+ // Fold the target bundle into the task text so the model biases toward it.
218
+ function describeTaskWithTarget(task, bundle) {
219
+ return bundle ? `${task}\n(Target app bundle id: ${bundle})` : task;
220
+ }
221
+ // Human progress line per loop event — verb + a one-line result digest.
222
+ function printLoopEvent(e) {
223
+ if (e.kind === 'turn') {
224
+ console.log(`--- turn ${e.index + 1} ---`);
225
+ }
226
+ else if (e.kind === 'dispatch') {
227
+ const tag = e.visionFallback ? ' [ax opaque]' : '';
228
+ const status = e.result.ok ? 'ok' : `error: ${e.result.error ?? 'unknown'}`;
229
+ console.log(` ${e.call.name}${tag} -> ${status}`);
230
+ }
231
+ else if (e.kind === 'vision_switch') {
232
+ console.log(' (switching to vision path: screenshot + coordinate clicks)');
233
+ }
234
+ else if (e.kind === 'done') {
235
+ console.log(` model: ${e.text}`);
236
+ }
237
+ }
157
238
  function registerScreenshotCommand(program) {
158
239
  program
159
240
  .command('screenshot')
@@ -40,9 +40,12 @@ you never need to start it manually.
40
40
  if (result.method === 'already-running') {
41
41
  console.log(chalk.yellow(`Daemon already running (PID: ${result.pid})`));
42
42
  }
43
- else {
43
+ else if (result.pid) {
44
44
  console.log(chalk.green(`Daemon started (PID: ${result.pid}, method: ${result.method})`));
45
45
  }
46
+ else {
47
+ console.log(chalk.yellow('Daemon start dispatched but no PID surfaced. Check `agents routines status`.'));
48
+ }
46
49
  });
47
50
  daemonCmd
48
51
  .command('stop')