@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
@@ -10,7 +10,7 @@ import * as os from 'os';
10
10
  import * as crypto from 'crypto';
11
11
  import * as yaml from 'yaml';
12
12
  import { getCloudDir } from '../state.js';
13
- import { resolveDispatchRepos } from './types.js';
13
+ import { resolveDispatchRepos, MAX_IMAGES_PER_DISPATCH } from './types.js';
14
14
  import { parseSSE } from './stream.js';
15
15
  import { listInstalledVersions, getVersionHomePath } from '../versions.js';
16
16
  import { getAccountInfo } from '../agents.js';
@@ -286,6 +286,12 @@ export function buildDispatchBody(input) {
286
286
  if (input.accountTokens && input.accountTokens.length > 0) {
287
287
  body.account_tokens = input.accountTokens;
288
288
  }
289
+ if (input.skills && input.skills.length > 0) {
290
+ body.skills = input.skills;
291
+ }
292
+ if (input.images && input.images.length > 0) {
293
+ body.images = input.images.slice(0, MAX_IMAGES_PER_DISPATCH);
294
+ }
289
295
  return body;
290
296
  }
291
297
  /** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
@@ -332,8 +338,8 @@ export class RushCloudProvider {
332
338
  cancel: true,
333
339
  message: true,
334
340
  multiRepo: true,
335
- skills: false,
336
- images: false,
341
+ skills: true,
342
+ images: true,
337
343
  };
338
344
  }
339
345
  async dispatch(options) {
@@ -387,6 +393,8 @@ export class RushCloudProvider {
387
393
  resolvedRepos,
388
394
  accountManifest,
389
395
  strategy,
396
+ skills: options.skills,
397
+ images: options.images,
390
398
  });
391
399
  let res = await api('POST', '/api/v1/cloud-runs', token, body);
392
400
  // Server detects drift (new account or rotated token) by comparing the
@@ -429,6 +437,8 @@ export class RushCloudProvider {
429
437
  resolvedRepos,
430
438
  accountManifest,
431
439
  accountTokens,
440
+ skills: options.skills,
441
+ images: options.images,
432
442
  });
433
443
  res = await api('POST', '/api/v1/cloud-runs', token, retryBody);
434
444
  // Persist consent on first successful upload so we don't re-prompt
@@ -5,6 +5,7 @@
5
5
  * Factory) implement, plus the shared task and event types that flow through
6
6
  * the dispatch pipeline.
7
7
  */
8
+ import type { JobTrigger } from '../routines.js';
8
9
  /**
9
10
  * Identifier for a supported cloud dispatch backend.
10
11
  *
@@ -119,6 +120,14 @@ export declare const MAX_IMAGES_PER_DISPATCH = 5;
119
120
  export interface DispatchOptions {
120
121
  prompt: string;
121
122
  agent?: string;
123
+ /**
124
+ * Event/webhook trigger to register with this dispatch. Set by
125
+ * `agents cloud run --on <event>`: instead of dispatching immediately, the
126
+ * dispatch is persisted as a trigger-bound routine so the local webhook
127
+ * receiver can fire it when the event arrives. Remote firing of the trigger
128
+ * is a follow-up; today the flag parses, validates, and persists.
129
+ */
130
+ trigger?: JobTrigger;
122
131
  /**
123
132
  * Legacy single-repo target. Still honored: if `repos` is empty, this
124
133
  * becomes the only repo. Providers that support multi-repo treat
@@ -0,0 +1,8 @@
1
+ import type { ComputerClient } from '../computer-rpc.js';
2
+ import { type AppInfo } from '../../commands/computer-actions.js';
3
+ import type { VerbDispatcher } from './loop.js';
4
+ declare function rpcMethodFor(verb: string): string;
5
+ declare function toRpcParams(verb: string, input: Record<string, unknown>, pid?: number): Record<string, unknown>;
6
+ export declare function makeVerbDispatcher(client: ComputerClient): VerbDispatcher;
7
+ export { rpcMethodFor, toRpcParams };
8
+ export type { AppInfo };
@@ -0,0 +1,125 @@
1
+ // Verb dispatcher for the `computer run` loop.
2
+ //
3
+ // Maps a loop VerbCall (CLI-verb name + friendly input) onto the EXISTING
4
+ // computer-helper RPC methods over the daemon socket. It reimplements no verb —
5
+ // it names the same RPC methods the explicit `agents computer <verb>` commands
6
+ // call, so the external-agent verb surface is untouched. The daemon stays the
7
+ // single authority on permissions and targeting.
8
+ import { resolveTargetPidDecision } from '../../commands/computer-actions.js';
9
+ // CLI verb -> daemon RPC method. Only the rename cases need listing; the rest
10
+ // pass through by name.
11
+ const RPC_METHOD = {
12
+ apps: 'list_apps',
13
+ 'get-text': 'get_text',
14
+ 'type-text': 'type_text',
15
+ 'ax-action': 'ax_action',
16
+ 'right-click': 'right_click',
17
+ launch: 'launch_app',
18
+ focus: 'set_focus',
19
+ raise: 'focus_window',
20
+ };
21
+ // Verbs that do not need a resolved target pid.
22
+ const NO_TARGET = new Set(['apps', 'launch']);
23
+ function rpcMethodFor(verb) {
24
+ return RPC_METHOD[verb] ?? verb;
25
+ }
26
+ // Translate the model's friendly input keys into the daemon's param names.
27
+ // pid is injected by the caller after target resolution.
28
+ function toRpcParams(verb, input, pid) {
29
+ const p = {};
30
+ if (pid != null)
31
+ p.pid = pid;
32
+ const copyIf = (from, to) => {
33
+ if (input[from] != null)
34
+ p[to] = input[from];
35
+ };
36
+ switch (verb) {
37
+ case 'describe':
38
+ copyIf('depth', 'max_depth');
39
+ break;
40
+ case 'screenshot':
41
+ copyIf('quality', 'quality');
42
+ break;
43
+ case 'get-text':
44
+ copyIf('id', 'element_id');
45
+ copyIf('maxChars', 'max_chars');
46
+ break;
47
+ case 'click':
48
+ case 'right-click':
49
+ case 'scroll':
50
+ copyIf('id', 'element_id');
51
+ copyIf('x', 'x');
52
+ copyIf('y', 'y');
53
+ copyIf('count', 'count');
54
+ copyIf('dx', 'dx');
55
+ copyIf('dy', 'dy');
56
+ break;
57
+ case 'type':
58
+ copyIf('id', 'element_id');
59
+ copyIf('x', 'x');
60
+ copyIf('y', 'y');
61
+ copyIf('text', 'text');
62
+ copyIf('commit', 'commit');
63
+ break;
64
+ case 'type-text':
65
+ copyIf('text', 'text');
66
+ copyIf('commit', 'commit');
67
+ break;
68
+ case 'key':
69
+ copyIf('keys', 'keys');
70
+ break;
71
+ case 'ax-action':
72
+ copyIf('id', 'element_id');
73
+ copyIf('action', 'action');
74
+ break;
75
+ case 'focus':
76
+ copyIf('id', 'element_id');
77
+ break;
78
+ case 'launch':
79
+ copyIf('bundle', 'bundle_id');
80
+ copyIf('path', 'path');
81
+ copyIf('name', 'name');
82
+ break;
83
+ case 'wait': {
84
+ if (input.duration != null)
85
+ p.duration_ms = input.duration;
86
+ copyIf('id', 'element_id');
87
+ copyIf('until', 'until');
88
+ const locator = {};
89
+ for (const k of ['role', 'label', 'identifier']) {
90
+ if (input[k] != null)
91
+ locator[k] = input[k];
92
+ }
93
+ if (Object.keys(locator).length > 0)
94
+ p.locator = locator;
95
+ break;
96
+ }
97
+ default:
98
+ break;
99
+ }
100
+ return p;
101
+ }
102
+ // Build a dispatcher bound to a live client. Every call is a single daemon
103
+ // round-trip; errors are returned (never thrown) so the loop can feed them
104
+ // back to the model as a tool_result.
105
+ export function makeVerbDispatcher(client) {
106
+ return async (call) => {
107
+ const verb = call.name;
108
+ const input = call.input ?? {};
109
+ let pid;
110
+ if (!NO_TARGET.has(verb) && !(verb === 'wait' && input.duration != null)) {
111
+ const resolved = await resolveTargetPidDecision(client, { pid: input.pid, bundle: input.bundle });
112
+ if (!resolved.ok)
113
+ return { ok: false, error: resolved.error };
114
+ pid = resolved.pid;
115
+ }
116
+ const method = rpcMethodFor(verb);
117
+ const params = toRpcParams(verb, input, pid);
118
+ const res = await client.call(method, params);
119
+ if (res.error)
120
+ return { ok: false, error: `${res.error.code}: ${res.error.message}` };
121
+ return { ok: true, result: res.result ?? {} };
122
+ };
123
+ }
124
+ // Exposed for tests/diagnostics.
125
+ export { rpcMethodFor, toRpcParams };
@@ -0,0 +1,62 @@
1
+ export interface VerbCall {
2
+ name: string;
3
+ input: Record<string, unknown>;
4
+ }
5
+ export interface VerbResult {
6
+ ok: boolean;
7
+ result?: Record<string, unknown>;
8
+ error?: string;
9
+ }
10
+ export interface LoopStep {
11
+ call: VerbCall;
12
+ result: VerbResult;
13
+ visionFallback?: boolean;
14
+ }
15
+ export interface LoopState {
16
+ task: string;
17
+ steps: LoopStep[];
18
+ visionMode: boolean;
19
+ }
20
+ export interface ModelDecision {
21
+ toolCalls: VerbCall[];
22
+ done?: {
23
+ text: string;
24
+ };
25
+ }
26
+ export type ModelResponder = (state: LoopState) => ModelDecision | Promise<ModelDecision>;
27
+ export type VerbDispatcher = (call: VerbCall) => VerbResult | Promise<VerbResult>;
28
+ export interface LoopConfig {
29
+ task: string;
30
+ responder: ModelResponder;
31
+ dispatch: VerbDispatcher;
32
+ maxSteps: number;
33
+ onEvent?: (event: LoopEvent) => void;
34
+ }
35
+ export type LoopEvent = {
36
+ kind: 'turn';
37
+ index: number;
38
+ } | {
39
+ kind: 'dispatch';
40
+ call: VerbCall;
41
+ result: VerbResult;
42
+ visionFallback?: boolean;
43
+ } | {
44
+ kind: 'vision_switch';
45
+ describe: VerbResult;
46
+ } | {
47
+ kind: 'done';
48
+ text: string;
49
+ } | {
50
+ kind: 'max_steps';
51
+ };
52
+ export interface LoopResult {
53
+ status: 'done' | 'max_steps';
54
+ finalText?: string;
55
+ steps: LoopStep[];
56
+ dispatched: string[];
57
+ visionMode: boolean;
58
+ turns: number;
59
+ }
60
+ export declare const AX_OPAQUE_ELEMENT_THRESHOLD = 3;
61
+ export declare function isAxOpaque(result: VerbResult): boolean;
62
+ export declare function runComputerLoop(config: LoopConfig): Promise<LoopResult>;
@@ -0,0 +1,98 @@
1
+ // The embedded observe -> act -> verify loop behind `agents computer run`.
2
+ //
3
+ // This module owns ONLY the decision logic: given a reasoning model (a
4
+ // boundary, injected as `responder`) and a verb dispatcher (the existing
5
+ // computer-helper RPC surface, injected as `dispatch`), it drives the model's
6
+ // tool calls against the daemon, step by step, until the model declares the
7
+ // task done or `maxSteps` is hit.
8
+ //
9
+ // The model and the daemon are boundaries — neither is code under test. The
10
+ // loop itself (verb ordering, max-steps, the AX -> vision fallback) is the
11
+ // unit under test. See loop.test.ts, which injects a scripted fake responder
12
+ // and a fake dispatcher.
13
+ // Below this many AX elements the tree is treated as opaque — a WebView, a
14
+ // Qt/Electron surface that never populated its AX tree, or a canvas app. The
15
+ // model then works from pixels + coordinate clicks instead of element ids.
16
+ export const AX_OPAQUE_ELEMENT_THRESHOLD = 3;
17
+ // Describe-time heuristic. An explicit `ax_opaque: true` from the daemon wins.
18
+ // Otherwise: a failed describe is NOT opaque (it's an error, surface it), and a
19
+ // successful one is opaque when its element_count is at/under the threshold or
20
+ // its tree has no children. Pure so the rule is unit-testable in isolation.
21
+ export function isAxOpaque(result) {
22
+ const r = result.result ?? {};
23
+ if (r.ax_opaque === true)
24
+ return true;
25
+ if (!result.ok)
26
+ return false;
27
+ const count = typeof r.element_count === 'number' ? r.element_count : undefined;
28
+ if (count !== undefined)
29
+ return count <= AX_OPAQUE_ELEMENT_THRESHOLD;
30
+ const tree = r.tree;
31
+ const children = tree && Array.isArray(tree.children) ? tree.children : [];
32
+ return children.length === 0;
33
+ }
34
+ // Drive the loop to completion. Returns the transcript, the ordered verb names
35
+ // dispatched, and whether the vision fallback ever fired.
36
+ export async function runComputerLoop(config) {
37
+ const state = { task: config.task, steps: [], visionMode: false };
38
+ const dispatched = [];
39
+ const emit = config.onEvent ?? (() => { });
40
+ let turns = 0;
41
+ while (turns < config.maxSteps) {
42
+ emit({ kind: 'turn', index: turns });
43
+ turns++;
44
+ const decision = await config.responder(state);
45
+ if (decision.done) {
46
+ emit({ kind: 'done', text: decision.done.text });
47
+ return {
48
+ status: 'done',
49
+ finalText: decision.done.text,
50
+ steps: state.steps,
51
+ dispatched,
52
+ visionMode: state.visionMode,
53
+ turns,
54
+ };
55
+ }
56
+ for (const call of decision.toolCalls) {
57
+ const result = await config.dispatch(call);
58
+ dispatched.push(call.name);
59
+ // AX -> vision fallback. When a describe comes back opaque, the element
60
+ // ids the model would click are useless, so the loop flips visionMode and
61
+ // immediately hands the model pixels: it auto-dispatches a screenshot of
62
+ // the same target. The explicit verbs stay untouched for external callers
63
+ // — this switch lives entirely in the loop.
64
+ if (call.name === 'describe' && isAxOpaque(result)) {
65
+ state.visionMode = true;
66
+ state.steps.push({ call, result, visionFallback: true });
67
+ emit({ kind: 'dispatch', call, result, visionFallback: true });
68
+ emit({ kind: 'vision_switch', describe: result });
69
+ const shotCall = { name: 'screenshot', input: pickTargetInput(call.input) };
70
+ const shot = await config.dispatch(shotCall);
71
+ dispatched.push(shotCall.name);
72
+ state.steps.push({ call: shotCall, result: shot });
73
+ emit({ kind: 'dispatch', call: shotCall, result: shot });
74
+ continue;
75
+ }
76
+ state.steps.push({ call, result });
77
+ emit({ kind: 'dispatch', call, result });
78
+ }
79
+ }
80
+ emit({ kind: 'max_steps' });
81
+ return {
82
+ status: 'max_steps',
83
+ steps: state.steps,
84
+ dispatched,
85
+ visionMode: state.visionMode,
86
+ turns,
87
+ };
88
+ }
89
+ // Carry the target selector (pid/bundle) from the describe call onto the
90
+ // auto-injected screenshot so the vision fallback captures the same surface.
91
+ function pickTargetInput(input) {
92
+ const out = {};
93
+ if (input.pid != null)
94
+ out.pid = input.pid;
95
+ if (input.bundle != null)
96
+ out.bundle = input.bundle;
97
+ return out;
98
+ }
@@ -0,0 +1,44 @@
1
+ import type { LoopState, ModelDecision, ModelResponder } from './loop.js';
2
+ interface ToolSpec {
3
+ name: string;
4
+ description: string;
5
+ input_schema: {
6
+ type: 'object';
7
+ properties: Record<string, unknown>;
8
+ required?: string[];
9
+ };
10
+ }
11
+ export declare const COMPUTER_TOOLS: ToolSpec[];
12
+ export interface ClaudeResponderConfig {
13
+ baseUrl?: string;
14
+ apiKey?: string;
15
+ model?: string;
16
+ maxTokens?: number;
17
+ fetchImpl?: typeof fetch;
18
+ }
19
+ export declare const DEFAULT_CLAUDE_BASE_URL = "https://api.anthropic.com";
20
+ export declare const DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";
21
+ export declare const ANTHROPIC_VERSION = "2023-06-01";
22
+ export declare function resolveApiKey(config: ClaudeResponderConfig, env?: NodeJS.ProcessEnv): string;
23
+ export declare function buildSystemPrompt(state: LoopState): string;
24
+ type AnthropicBlock = {
25
+ type: 'text';
26
+ text: string;
27
+ } | {
28
+ type: 'tool_use';
29
+ id: string;
30
+ name: string;
31
+ input: Record<string, unknown>;
32
+ } | {
33
+ type: 'tool_result';
34
+ tool_use_id: string;
35
+ content: string;
36
+ };
37
+ interface AnthropicMessage {
38
+ role: 'user' | 'assistant';
39
+ content: AnthropicBlock[];
40
+ }
41
+ export declare function buildMessages(state: LoopState): AnthropicMessage[];
42
+ export declare function parseDecision(body: unknown): ModelDecision;
43
+ export declare function makeClaudeResponder(config?: ClaudeResponderConfig): ModelResponder;
44
+ export {};
@@ -0,0 +1,157 @@
1
+ // Default reasoning model for the `computer run` loop.
2
+ //
3
+ // This is a BOUNDARY, not loop logic. It turns the loop's LoopState into an
4
+ // Anthropic Messages API request (tool use), sends it to the Claude API by
5
+ // default OR to a configurable base URL (Ollama / vLLM / LiteLLM serving the
6
+ // Anthropic wire shape) for local/offline parity, and parses the reply back
7
+ // into a ModelDecision the loop can act on.
8
+ //
9
+ // The loop (loop.ts) never imports this — it takes a ModelResponder callback,
10
+ // so the unit tests inject a scripted fake instead of ever hitting a network.
11
+ const TARGET_PROPS = {
12
+ bundle: { type: 'string', description: 'Bundle id of the target app (default: frontmost allow-listed app)' },
13
+ pid: { type: 'number', description: 'Target pid (overrides bundle)' },
14
+ };
15
+ export const COMPUTER_TOOLS = [
16
+ { name: 'apps', description: 'List allow-listed apps currently running.', input_schema: { type: 'object', properties: {} } },
17
+ {
18
+ name: 'describe',
19
+ description: 'Dump the accessibility tree of the target app. Element ids feed click/type via id. If the tree is opaque the loop auto-captures a screenshot for you.',
20
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, depth: { type: 'number' } } },
21
+ },
22
+ {
23
+ name: 'screenshot',
24
+ description: 'Capture the target app window as an image (the vision path — use coordinate clicks off it).',
25
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS } },
26
+ },
27
+ {
28
+ name: 'get-text',
29
+ description: 'Extract visible text from the target app (or a subtree via id).',
30
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' } } },
31
+ },
32
+ {
33
+ name: 'click',
34
+ description: 'Click an element (id from describe) or a screen coordinate (x,y). Use x,y after a vision screenshot.',
35
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' } } },
36
+ },
37
+ {
38
+ name: 'type',
39
+ description: 'Set a field value (id) or paste at a coordinate. Set commit=true to submit.',
40
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, id: { type: 'string' }, x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, commit: { type: 'boolean' } }, required: ['text'] },
41
+ },
42
+ {
43
+ name: 'type-text',
44
+ description: 'Type a unicode string into the focused field. Focus it first with click or focus.',
45
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, text: { type: 'string' }, commit: { type: 'boolean' } }, required: ['text'] },
46
+ },
47
+ {
48
+ name: 'key',
49
+ description: 'Send a key chord, e.g. "cmd+s", "enter", "esc".',
50
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, keys: { type: 'string' } }, required: ['keys'] },
51
+ },
52
+ {
53
+ name: 'launch',
54
+ description: 'Launch an app by bundle id, path, or name.',
55
+ input_schema: { type: 'object', properties: { bundle: { type: 'string' }, path: { type: 'string' }, name: { type: 'string' } } },
56
+ },
57
+ {
58
+ name: 'wait',
59
+ description: 'Wait for a duration (ms) or for an element to appear.',
60
+ input_schema: { type: 'object', properties: { ...TARGET_PROPS, duration: { type: 'number' }, id: { type: 'string' }, role: { type: 'string' }, label: { type: 'string' } } },
61
+ },
62
+ ];
63
+ export const DEFAULT_CLAUDE_BASE_URL = 'https://api.anthropic.com';
64
+ export const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6';
65
+ export const ANTHROPIC_VERSION = '2023-06-01';
66
+ // Resolve the API key: explicit config, then the standard Anthropic env var,
67
+ // then a computer-run-specific override. A local Anthropic-shaped endpoint
68
+ // (Ollama/vLLM/LiteLLM) usually ignores the key, so an empty key is allowed
69
+ // when a non-default base URL is set.
70
+ export function resolveApiKey(config, env = process.env) {
71
+ return config.apiKey || env.AGENTS_COMPUTER_API_KEY || env.ANTHROPIC_API_KEY || '';
72
+ }
73
+ // Build the system prompt. In vision mode we steer the model onto coordinate
74
+ // clicks off screenshots, because element ids from an opaque tree are useless.
75
+ export function buildSystemPrompt(state) {
76
+ const lines = [
77
+ 'You are driving a macOS desktop through accessibility + vision tools to accomplish a task.',
78
+ 'Loop: observe (describe / screenshot / get-text), act (click / type / key), then verify the result before the next step.',
79
+ 'Prefer element ids from describe. When you are confident the task is complete, stop calling tools and reply with a short completion summary.',
80
+ `Task: ${state.task}`,
81
+ ];
82
+ if (state.visionMode) {
83
+ lines.push('NOTE: the accessibility tree for the current surface is opaque (WebView / canvas). Work from the screenshot and click by x,y coordinates instead of element ids.');
84
+ }
85
+ return lines.join('\n');
86
+ }
87
+ // Reconstruct the Anthropic message array from the loop transcript. We rebuild
88
+ // from scratch each turn (the loop holds canonical state), synthesizing stable
89
+ // tool_use ids per step so each tool_result pairs with its call.
90
+ export function buildMessages(state) {
91
+ const messages = [
92
+ { role: 'user', content: [{ type: 'text', text: `Begin. Task: ${state.task}` }] },
93
+ ];
94
+ state.steps.forEach((step, i) => {
95
+ const id = `call_${i}`;
96
+ messages.push({ role: 'assistant', content: [{ type: 'tool_use', id, name: step.call.name, input: step.call.input }] });
97
+ const summary = step.result.ok
98
+ ? JSON.stringify(step.result.result ?? {}).slice(0, 4000)
99
+ : `error: ${step.result.error ?? 'unknown'}`;
100
+ const note = step.visionFallback ? ' [ax tree opaque — auto-captured screenshot follows; use coordinate clicks]' : '';
101
+ messages.push({ role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content: summary + note }] });
102
+ });
103
+ return messages;
104
+ }
105
+ // Parse the Anthropic response body into a ModelDecision. tool_use blocks
106
+ // become tool calls; a reply with no tool calls is treated as the model
107
+ // finishing, with its text as the summary.
108
+ export function parseDecision(body) {
109
+ const content = body.content;
110
+ const blocks = Array.isArray(content) ? content : [];
111
+ const toolCalls = [];
112
+ const texts = [];
113
+ for (const block of blocks) {
114
+ const b = block;
115
+ if (b.type === 'tool_use' && typeof b.name === 'string') {
116
+ toolCalls.push({ name: b.name, input: b.input ?? {} });
117
+ }
118
+ else if (b.type === 'text' && typeof b.text === 'string') {
119
+ texts.push(b.text);
120
+ }
121
+ }
122
+ if (toolCalls.length === 0) {
123
+ return { toolCalls: [], done: { text: texts.join('\n').trim() || 'no further action' } };
124
+ }
125
+ return { toolCalls };
126
+ }
127
+ // The default responder: one Anthropic Messages call per turn.
128
+ export function makeClaudeResponder(config = {}) {
129
+ const baseUrl = (config.baseUrl || DEFAULT_CLAUDE_BASE_URL).replace(/\/+$/, '');
130
+ const model = config.model || DEFAULT_CLAUDE_MODEL;
131
+ const maxTokens = config.maxTokens ?? 1024;
132
+ const doFetch = config.fetchImpl ?? fetch;
133
+ const apiKey = resolveApiKey(config);
134
+ return async (state) => {
135
+ const res = await doFetch(`${baseUrl}/v1/messages`, {
136
+ method: 'POST',
137
+ headers: {
138
+ 'content-type': 'application/json',
139
+ 'x-api-key': apiKey,
140
+ 'anthropic-version': ANTHROPIC_VERSION,
141
+ },
142
+ body: JSON.stringify({
143
+ model,
144
+ max_tokens: maxTokens,
145
+ system: buildSystemPrompt(state),
146
+ tools: COMPUTER_TOOLS,
147
+ messages: buildMessages(state),
148
+ }),
149
+ });
150
+ if (!res.ok) {
151
+ const text = await res.text().catch(() => '');
152
+ throw new Error(`model request failed: ${res.status} ${res.statusText} ${text.slice(0, 500)}`);
153
+ }
154
+ const body = await res.json();
155
+ return parseDecision(body);
156
+ };
157
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Bounded, order-preserving async mapper.
3
+ *
4
+ * Runs `fn` over `items` with at most `concurrency` calls in flight, and — when
5
+ * `staggerMs` is set — spaces successive task starts at least that far apart so
6
+ * spawns trickle out instead of firing as one simultaneous burst. Results come
7
+ * back in input order regardless of completion order.
8
+ *
9
+ * The stagger matters beyond scheduling: a burst of identical child spawns (per-PID
10
+ * `lsof`, multi-dotfile scans) reads to behavioral EDR as recon/enumeration. A
11
+ * bounded, spread-out spawn rate produces the same data without the burst signature.
12
+ */
13
+ export interface BoundedMapOptions {
14
+ /** Maximum number of `fn` calls running at once. Coerced to >= 1. */
15
+ concurrency: number;
16
+ /** Minimum spacing (ms) between successive task starts. 0 (default) = no spacing. */
17
+ staggerMs?: number;
18
+ }
19
+ export declare function mapBounded<T, R>(items: readonly T[], fn: (item: T, index: number) => Promise<R>, opts: BoundedMapOptions): Promise<R[]>;
@@ -0,0 +1,33 @@
1
+ function delay(ms) {
2
+ return new Promise(resolve => setTimeout(resolve, ms));
3
+ }
4
+ export async function mapBounded(items, fn, opts) {
5
+ const concurrency = Math.max(1, Math.floor(opts.concurrency));
6
+ const stagger = Math.max(0, opts.staggerMs ?? 0);
7
+ const results = new Array(items.length);
8
+ if (items.length === 0)
9
+ return results;
10
+ let cursor = 0;
11
+ // Shared gate: the earliest time the next task is allowed to start. Updated
12
+ // atomically (no await between read and write) so parallel workers each claim
13
+ // a distinct slot spaced `stagger` apart.
14
+ let nextStart = 0;
15
+ async function worker() {
16
+ for (;;) {
17
+ const i = cursor++;
18
+ if (i >= items.length)
19
+ return;
20
+ if (stagger > 0) {
21
+ const now = performance.now();
22
+ const wait = Math.max(0, nextStart - now);
23
+ nextStart = Math.max(now, nextStart) + stagger;
24
+ if (wait > 0)
25
+ await delay(wait);
26
+ }
27
+ results[i] = await fn(items[i], i);
28
+ }
29
+ }
30
+ const workers = Math.min(concurrency, items.length);
31
+ await Promise.all(Array.from({ length: workers }, () => worker()));
32
+ return results;
33
+ }