@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
@@ -6,7 +6,88 @@
6
6
  // The daemon already implements every method; this file is the thin, typed
7
7
  // CLI skin over it plus a shared target resolver so callers stay in bundle-id
8
8
  // space and never hand-manage pids.
9
- import { openComputerClient, describeTransport, } from '../lib/computer-rpc.js';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { openComputerClient, describeTransport, resolvePolicyPath, } from '../lib/computer-rpc.js';
12
+ import { COMPUTER_INPUT_GATED_VERBS, formatComputerPermissionGrantHint, } from '../lib/permissions.js';
13
+ function isComputerInputVerb(verb) {
14
+ return typeof verb === 'string' && COMPUTER_INPUT_GATED_VERBS.includes(verb);
15
+ }
16
+ function computerSessionId(env = process.env) {
17
+ return env.CODEX_THREAD_ID
18
+ || env.CLAUDE_CODE_SESSION_ID
19
+ || env.CLAUDE_SESSION_ID
20
+ || env.AGENTS_SESSION_ID
21
+ || env.AGENTS_RUN_ID
22
+ || null;
23
+ }
24
+ function admissionCachePath(env = process.env) {
25
+ if (env.AGENTS_COMPUTER_ADMISSION_CACHE)
26
+ return env.AGENTS_COMPUTER_ADMISSION_CACHE;
27
+ return path.join(path.dirname(resolvePolicyPath()), 'computer-target-admissions.json');
28
+ }
29
+ function targetSelector(opts) {
30
+ return opts.bundle ? `bundle:${opts.bundle}` : 'frontmost';
31
+ }
32
+ function readAdmissionCache(filePath) {
33
+ try {
34
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
35
+ return Array.isArray(parsed.admissions) ? parsed.admissions.filter(isAdmission) : [];
36
+ }
37
+ catch {
38
+ return [];
39
+ }
40
+ }
41
+ function isAdmission(v) {
42
+ if (!v || typeof v !== 'object')
43
+ return false;
44
+ const r = v;
45
+ return r.sessionId !== undefined
46
+ && typeof r.sessionId === 'string'
47
+ && typeof r.selector === 'string'
48
+ && r.gateClass === 'input'
49
+ && typeof r.pid === 'number'
50
+ && typeof r.bundle_id === 'string'
51
+ && typeof r.name === 'string'
52
+ && typeof r.admittedAtMs === 'number'
53
+ && isComputerInputVerb(r.admittedByVerb);
54
+ }
55
+ function rememberAdmission(app, opts) {
56
+ const sessionId = computerSessionId(opts.env);
57
+ if (!sessionId)
58
+ return;
59
+ const filePath = admissionCachePath(opts.env);
60
+ const admissions = readAdmissionCache(filePath);
61
+ const next = {
62
+ sessionId,
63
+ selector: opts.selector,
64
+ gateClass: 'input',
65
+ pid: app.pid,
66
+ bundle_id: app.bundle_id,
67
+ name: app.name,
68
+ admittedAtMs: opts.nowMs ?? Date.now(),
69
+ admittedByVerb: opts.verb,
70
+ };
71
+ const filtered = admissions.filter((a) => !(a.sessionId === sessionId && a.selector === opts.selector && a.gateClass === 'input'));
72
+ filtered.push(next);
73
+ try {
74
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
75
+ fs.writeFileSync(filePath, JSON.stringify({ admissions: filtered }, null, 2), { mode: 0o600 });
76
+ }
77
+ catch {
78
+ // Cache persistence is best-effort; the daemon remains the final gate.
79
+ }
80
+ }
81
+ function findAdmission(opts) {
82
+ const sessionId = computerSessionId(opts.env);
83
+ if (!sessionId)
84
+ return null;
85
+ const admissions = readAdmissionCache(admissionCachePath(opts.env));
86
+ const matches = admissions
87
+ .filter((a) => a.sessionId === sessionId && a.selector === opts.selector && a.gateClass === 'input')
88
+ .sort((a, b) => b.admittedAtMs - a.admittedAtMs);
89
+ return matches[0] ?? null;
90
+ }
10
91
  // Pure target picker — exercised by unit tests. Precedence: explicit --pid,
11
92
  // then --bundle, then the frontmost active allow-listed app (the same default
12
93
  // `screenshot` uses). Kept side-effect-free so the resolution rules are
@@ -24,7 +105,7 @@ export function pickTarget(list, opts) {
24
105
  if (!app) {
25
106
  return {
26
107
  ok: false,
27
- error: `bundle not in allow list (or not running): ${opts.bundle}\nadd Computer(${opts.bundle}) to a permissions group, then \`agents computer reload\``,
108
+ error: `bundle not in allow list (or not running): ${opts.bundle}\n${formatComputerPermissionGrantHint(opts.bundle)}`,
28
109
  };
29
110
  }
30
111
  return { ok: true, app };
@@ -33,7 +114,7 @@ export function pickTarget(list, opts) {
33
114
  if (!active) {
34
115
  return {
35
116
  ok: false,
36
- error: 'no active app found in allow list\nadd Computer(<bundle-id>) to a permissions group, then `agents computer reload`',
117
+ error: `no active app found in allow list\n${formatComputerPermissionGrantHint()}`,
37
118
  };
38
119
  }
39
120
  return { ok: true, app: active };
@@ -141,20 +222,37 @@ export function unwrap(r) {
141
222
  }
142
223
  return r.result ?? {};
143
224
  }
144
- // Resolve the target pid via list_apps + pickTarget, printing a precise error
145
- // and exiting when no target matches.
146
- async function resolveTargetPid(client, opts) {
225
+ export async function resolveTargetPidDecision(client, opts, gate) {
147
226
  // A directly-supplied pid skips the list_apps roundtrip — the daemon gates.
148
227
  if (opts.pid != null)
149
- return opts.pid;
228
+ return { ok: true, pid: opts.pid, source: 'pid' };
150
229
  const apps = unwrap(await client.call('list_apps'));
151
230
  const list = apps.apps || [];
152
231
  const picked = pickTarget(list, opts);
232
+ const verb = gate?.verb;
233
+ const selector = targetSelector(opts);
234
+ if (picked.ok && isComputerInputVerb(verb)) {
235
+ rememberAdmission(picked.app, { selector, verb, env: gate?.env, nowMs: gate?.nowMs });
236
+ }
153
237
  if (!picked.ok) {
154
- console.error(picked.error);
238
+ if (isComputerInputVerb(verb)) {
239
+ const admitted = findAdmission({ selector, env: gate?.env });
240
+ if (admitted)
241
+ return { ok: true, pid: admitted.pid, source: 'session_admission' };
242
+ }
243
+ return { ok: false, error: picked.error };
244
+ }
245
+ return { ok: true, pid: picked.app.pid, source: 'list_apps' };
246
+ }
247
+ // Resolve the target pid via list_apps + pickTarget, printing a precise error
248
+ // and exiting when no target matches.
249
+ async function resolveTargetPid(client, opts, gate) {
250
+ const resolved = await resolveTargetPidDecision(client, opts, gate);
251
+ if (!resolved.ok) {
252
+ console.error(resolved.error);
155
253
  process.exit(1);
156
254
  }
157
- return picked.app.pid;
255
+ return resolved.pid;
158
256
  }
159
257
  // --raise flag: app-level focus_window before the main action so coordinate
160
258
  // clicks and keystrokes land on a visible, key window.
@@ -210,7 +308,7 @@ export function registerActionCommands(program) {
210
308
  .option('--depth <n>', 'Max tree depth', (v) => parseInt(v, 10))
211
309
  .option('--json', 'Emit compact JSON (default: pretty)')).action(async (opts) => {
212
310
  await withClient(async (client) => {
213
- const pid = await resolveTargetPid(client, opts);
311
+ const pid = await resolveTargetPid(client, opts, { verb: 'describe' });
214
312
  const params = { pid };
215
313
  if (opts.depth != null)
216
314
  params.max_depth = opts.depth;
@@ -228,7 +326,7 @@ export function registerActionCommands(program) {
228
326
  .option('--raise', 'Bring the target app to the front first')
229
327
  .option('--json', 'Emit JSON'))).action(async (opts) => {
230
328
  await withClient(async (client) => {
231
- const pid = await resolveTargetPid(client, opts);
329
+ const pid = await resolveTargetPid(client, opts, { verb: 'click' });
232
330
  const spec = buildElementOrCoords(opts);
233
331
  if (!spec.ok) {
234
332
  console.error(spec.error);
@@ -250,7 +348,7 @@ export function registerActionCommands(program) {
250
348
  .description('Right-click (context menu) an element or coordinate')
251
349
  .option('--json', 'Emit JSON'))).action(async (opts) => {
252
350
  await withClient(async (client) => {
253
- const pid = await resolveTargetPid(client, opts);
351
+ const pid = await resolveTargetPid(client, opts, { verb: 'right-click' });
254
352
  const spec = buildElementOrCoords(opts);
255
353
  if (!spec.ok) {
256
354
  console.error(spec.error);
@@ -269,7 +367,7 @@ export function registerActionCommands(program) {
269
367
  .option('--allow-secure-field', 'Permit typing into a password field')
270
368
  .option('--json', 'Emit JSON'))).action(async (opts) => {
271
369
  await withClient(async (client) => {
272
- const pid = await resolveTargetPid(client, opts);
370
+ const pid = await resolveTargetPid(client, opts, { verb: 'type' });
273
371
  const spec = buildElementOrCoords(opts);
274
372
  if (!spec.ok) {
275
373
  console.error(spec.error);
@@ -295,7 +393,7 @@ export function registerActionCommands(program) {
295
393
  .option('--char-delay <ms>', 'Inter-character delay in ms (default 4; raise for lossy keyboard relays like VM guests, e.g. 25). Clamped to [1, 250].', (v) => parseInt(v, 10))
296
394
  .option('--json', 'Emit JSON')).action(async (opts) => {
297
395
  await withClient(async (client) => {
298
- const pid = await resolveTargetPid(client, opts);
396
+ const pid = await resolveTargetPid(client, opts, { verb: 'type-text' });
299
397
  await raiseIfRequested(client, pid, opts.raise);
300
398
  const params = { pid, text: opts.text };
301
399
  if (opts.commit)
@@ -319,7 +417,7 @@ export function registerActionCommands(program) {
319
417
  .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
320
418
  .option('--json', 'Emit JSON')).action(async (opts) => {
321
419
  await withClient(async (client) => {
322
- const pid = await resolveTargetPid(client, opts);
420
+ const pid = await resolveTargetPid(client, opts, { verb: 'key' });
323
421
  await raiseIfRequested(client, pid, opts.raise);
324
422
  const params = { pid, keys: opts.keys };
325
423
  if (opts.requireFrontmost)
@@ -350,7 +448,7 @@ export function registerActionCommands(program) {
350
448
  process.exit(1);
351
449
  }
352
450
  await withClient(async (client) => {
353
- const pid = await resolveTargetPid(client, opts);
451
+ const pid = await resolveTargetPid(client, opts, { verb: 'drag' });
354
452
  await raiseIfRequested(client, pid, opts.raise);
355
453
  const params = {
356
454
  pid,
@@ -373,7 +471,7 @@ export function registerActionCommands(program) {
373
471
  .option('--raise', 'Bring the target app to the front first')
374
472
  .option('--json', 'Emit JSON'))).action(async (opts) => {
375
473
  await withClient(async (client) => {
376
- const pid = await resolveTargetPid(client, opts);
474
+ const pid = await resolveTargetPid(client, opts, { verb: 'scroll' });
377
475
  await raiseIfRequested(client, pid, opts.raise);
378
476
  const params = { pid };
379
477
  if (opts.id)
@@ -398,7 +496,7 @@ export function registerActionCommands(program) {
398
496
  .requiredOption('--action <name>', 'AX action name')
399
497
  .option('--json', 'Emit JSON')).action(async (opts) => {
400
498
  await withClient(async (client) => {
401
- const pid = await resolveTargetPid(client, opts);
499
+ const pid = await resolveTargetPid(client, opts, { verb: 'ax-action' });
402
500
  const res = unwrap(await client.call('ax_action', { pid, element_id: opts.id, action: opts.action }));
403
501
  emit(res, Boolean(opts.json), () => `performed ${opts.action}`);
404
502
  });
@@ -410,7 +508,7 @@ export function registerActionCommands(program) {
410
508
  .requiredOption('--id <@eN>', 'Element id from `describe`')
411
509
  .option('--json', 'Emit JSON')).action(async (opts) => {
412
510
  await withClient(async (client) => {
413
- const pid = await resolveTargetPid(client, opts);
511
+ const pid = await resolveTargetPid(client, opts, { verb: 'focus' });
414
512
  const res = unwrap(await client.call('set_focus', { pid, element_id: opts.id }));
415
513
  emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
416
514
  });
@@ -426,7 +524,7 @@ export function registerActionCommands(program) {
426
524
  .option('--title <s>', 'Raise the window whose title contains this string')
427
525
  .option('--json', 'Emit JSON')).action(async (opts) => {
428
526
  await withClient(async (client) => {
429
- const pid = await resolveTargetPid(client, opts);
527
+ const pid = await resolveTargetPid(client, opts, { verb: 'raise' });
430
528
  const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
431
529
  emit(res, Boolean(opts.json), () => {
432
530
  const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
@@ -455,7 +553,7 @@ export function registerActionCommands(program) {
455
553
  const params = { ...spec.params };
456
554
  // duration-only waits don't need a target pid
457
555
  if (params.duration_ms == null)
458
- params.pid = await resolveTargetPid(client, opts);
556
+ params.pid = await resolveTargetPid(client, opts, { verb: 'wait' });
459
557
  const res = unwrap(await client.call('wait', params));
460
558
  emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
461
559
  });
@@ -468,7 +566,7 @@ export function registerActionCommands(program) {
468
566
  .option('--max-chars <n>', 'Cap the extracted text length', (v) => parseInt(v, 10))
469
567
  .option('--json', 'Emit JSON')).action(async (opts) => {
470
568
  await withClient(async (client) => {
471
- const pid = await resolveTargetPid(client, opts);
569
+ const pid = await resolveTargetPid(client, opts, { verb: 'get-text' });
472
570
  const params = { pid };
473
571
  if (opts.id)
474
572
  params.element_id = opts.id;
@@ -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')
@@ -4,6 +4,7 @@ import { discoverSessions, parseTimeFilter } from '../lib/session/discover.js';
4
4
  import { queryUsageRollup, topSessionsByCost, } from '../lib/session/db.js';
5
5
  import { formatUsd, PRICING_VERSION } from '../lib/pricing/index.js';
6
6
  import { formatDuration } from '../lib/session/render.js';
7
+ import { terminalWidth, truncateToWidth, stringWidth, padToWidth } from '../lib/session/width.js';
7
8
  export function registerCostCommand(program) {
8
9
  addHostOption(program.command('cost'))
9
10
  .description('Roll up $ cost and duration across local agent sessions')
@@ -84,27 +85,36 @@ async function costAction(options) {
84
85
  // Top sessions by cost.
85
86
  if (top.length > 0) {
86
87
  out.push(chalk.bold('Top sessions by cost'));
88
+ const cols = terminalWidth();
89
+ const showProject = cols >= 100;
87
90
  const costW = Math.max(...top.map(t => formatUsd(t.costUsd).length), 4);
88
91
  for (const t of top) {
89
92
  const cost = formatUsd(t.costUsd).padStart(costW);
90
93
  const dur = t.durationMs > 0 ? formatDuration(t.durationMs) : '—';
91
- const label = t.meta.label || t.meta.topic || '(untitled)';
92
- const proj = t.meta.project ? chalk.gray(` ${t.meta.project}`) : '';
93
- out.push(` ${chalk.green(cost)} ${chalk.gray(t.meta.shortId)} ${chalk.cyan(t.meta.agent.padEnd(7))} ${truncate(label, 48)}` +
94
- proj +
95
- chalk.gray(` ${dur}`));
94
+ const label = (t.meta.label || t.meta.topic || '(untitled)').replace(/\s+/g, ' ').trim();
95
+ const proj = showProject && t.meta.project ? chalk.gray(` ${t.meta.project}`) : '';
96
+ const prefix = ` ${chalk.green(cost)} ${chalk.gray(t.meta.shortId)} ${chalk.cyan(t.meta.agent.padEnd(7))} `;
97
+ const suffix = proj + chalk.gray(` ${dur}`);
98
+ const topicW = Math.max(12, cols - stringWidth(prefix) - stringWidth(suffix));
99
+ out.push(prefix + truncateToWidth(label, topicW) + suffix);
96
100
  }
97
101
  out.push('');
98
102
  }
99
103
  // Per-agent / per-project / per-day breakdown.
100
104
  const groupLabel = groupBy === 'agent' ? 'agent' : groupBy === 'project' ? 'project' : 'day';
101
105
  out.push(chalk.bold(`By ${groupLabel}`));
102
- const keyW = Math.max(...breakdown.map(r => r.key.length), groupLabel.length);
106
+ const cols = terminalWidth();
103
107
  const costW2 = Math.max(...breakdown.map(r => formatUsd(r.costUsd).length), 4);
108
+ const countW = Math.max(...breakdown.map(r => String(r.sessionCount).length), 1);
109
+ const sessionW = Math.max(...breakdown.map(r => `${String(r.sessionCount).padStart(countW)} session${r.sessionCount !== 1 ? 's' : ''}`.length));
110
+ const durationW = Math.max(...breakdown.map(r => r.durationMs > 0 ? stringWidth(formatDuration(r.durationMs)) : 1), 1);
111
+ const fixedW = 2 + 2 + costW2 + 2 + sessionW + 2 + durationW;
112
+ const keyW = Math.max(8, Math.min(Math.max(...breakdown.map(r => stringWidth(r.key)), groupLabel.length), cols - fixedW));
104
113
  for (const r of breakdown) {
105
114
  const cost = formatUsd(r.costUsd).padStart(costW2);
106
115
  const dur = r.durationMs > 0 ? formatDuration(r.durationMs) : '—';
107
- out.push(` ${r.key.padEnd(keyW)} ${chalk.green(cost)} ${chalk.gray(`${r.sessionCount} session${r.sessionCount !== 1 ? 's' : ''}`)} ${chalk.gray(dur)}`);
116
+ const sessions = `${String(r.sessionCount).padStart(countW)} session${r.sessionCount !== 1 ? 's' : ''}`;
117
+ out.push(` ${padToWidth(truncateToWidth(r.key, keyW), keyW)} ${chalk.green(cost)} ${chalk.gray(padToWidth(sessions, sessionW))} ${chalk.gray(padToWidth(dur, durationW))}`);
108
118
  }
109
119
  console.log(out.join('\n'));
110
120
  }
@@ -132,8 +142,3 @@ function renderDailyHistogram(daily) {
132
142
  }
133
143
  return lines.join('\n');
134
144
  }
135
- /** Truncate a string to n chars with an ellipsis. */
136
- function truncate(s, n) {
137
- const oneLine = s.replace(/\s+/g, ' ').trim();
138
- return oneLine.length > n ? oneLine.slice(0, n - 1) + '…' : oneLine;
139
- }
@@ -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')
@@ -22,6 +22,7 @@
22
22
  * apply pending sync.
23
23
  */
24
24
  import type { Command } from 'commander';
25
+ export declare function wrapLine(prefix: string, text: string, width?: number): string[];
25
26
  /**
26
27
  * Windows-only advisory lines. When the effective PowerShell execution policy
27
28
  * blocks unsigned local `.ps1` scripts (`Restricted`/`AllSigned`), the generated