@phnx-labs/agents-cli 1.20.35 → 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 (225) 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.d.ts +71 -1
  34. package/dist/commands/sessions.js +365 -37
  35. package/dist/commands/setup.js +4 -2
  36. package/dist/commands/sync.d.ts +3 -1
  37. package/dist/commands/sync.js +156 -4
  38. package/dist/commands/teams.js +217 -0
  39. package/dist/commands/versions.js +2 -4
  40. package/dist/commands/watchdog.d.ts +18 -0
  41. package/dist/commands/watchdog.js +238 -0
  42. package/dist/index.js +25 -2
  43. package/dist/lib/audit/log.d.ts +92 -0
  44. package/dist/lib/audit/log.js +177 -0
  45. package/dist/lib/auto-pull.js +2 -1
  46. package/dist/lib/browser/chrome.d.ts +10 -0
  47. package/dist/lib/browser/chrome.js +18 -7
  48. package/dist/lib/browser/drivers/ssh.js +2 -1
  49. package/dist/lib/browser/har.d.ts +84 -0
  50. package/dist/lib/browser/har.js +77 -0
  51. package/dist/lib/browser/ipc.js +24 -3
  52. package/dist/lib/browser/profiles.d.ts +1 -1
  53. package/dist/lib/browser/profiles.js +8 -10
  54. package/dist/lib/browser/refs.d.ts +65 -0
  55. package/dist/lib/browser/refs.js +73 -1
  56. package/dist/lib/browser/runtime-state.js +1 -0
  57. package/dist/lib/browser/service.d.ts +38 -2
  58. package/dist/lib/browser/service.js +112 -8
  59. package/dist/lib/browser/types.d.ts +14 -1
  60. package/dist/lib/budget/live-cloud.d.ts +42 -0
  61. package/dist/lib/budget/live-cloud.js +79 -0
  62. package/dist/lib/budget/live-team.d.ts +31 -0
  63. package/dist/lib/budget/live-team.js +115 -0
  64. package/dist/lib/cloud/codex.js +4 -0
  65. package/dist/lib/cloud/rush.d.ts +12 -1
  66. package/dist/lib/cloud/rush.js +13 -3
  67. package/dist/lib/cloud/types.d.ts +9 -0
  68. package/dist/lib/computer/dispatch.d.ts +8 -0
  69. package/dist/lib/computer/dispatch.js +125 -0
  70. package/dist/lib/computer/loop.d.ts +62 -0
  71. package/dist/lib/computer/loop.js +98 -0
  72. package/dist/lib/computer/model.d.ts +44 -0
  73. package/dist/lib/computer/model.js +157 -0
  74. package/dist/lib/concurrency.d.ts +19 -0
  75. package/dist/lib/concurrency.js +33 -0
  76. package/dist/lib/daemon.d.ts +57 -0
  77. package/dist/lib/daemon.js +192 -16
  78. package/dist/lib/devices/registry.d.ts +7 -0
  79. package/dist/lib/devices/registry.js +24 -0
  80. package/dist/lib/devices/tailscale.js +1 -1
  81. package/dist/lib/drift.d.ts +52 -0
  82. package/dist/lib/drift.js +112 -0
  83. package/dist/lib/events.d.ts +1 -1
  84. package/dist/lib/events.js +31 -13
  85. package/dist/lib/exec.d.ts +17 -0
  86. package/dist/lib/exec.js +79 -13
  87. package/dist/lib/git.d.ts +27 -0
  88. package/dist/lib/git.js +56 -1
  89. package/dist/lib/hooks/cache.d.ts +6 -0
  90. package/dist/lib/hooks/cache.js +54 -12
  91. package/dist/lib/hooks.d.ts +27 -0
  92. package/dist/lib/hooks.js +127 -8
  93. package/dist/lib/hosts/dispatch.d.ts +15 -0
  94. package/dist/lib/hosts/dispatch.js +39 -6
  95. package/dist/lib/hosts/logs.js +30 -1
  96. package/dist/lib/hosts/option.js +1 -1
  97. package/dist/lib/hosts/passthrough.js +3 -1
  98. package/dist/lib/hosts/ready.d.ts +29 -6
  99. package/dist/lib/hosts/ready.js +66 -15
  100. package/dist/lib/hosts/registry.d.ts +19 -2
  101. package/dist/lib/hosts/registry.js +58 -2
  102. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  103. package/dist/lib/hosts/remote-cmd.js +70 -1
  104. package/dist/lib/hosts/remote-os.d.ts +17 -0
  105. package/dist/lib/hosts/remote-os.js +30 -0
  106. package/dist/lib/hosts/session-index.d.ts +34 -0
  107. package/dist/lib/hosts/session-index.js +56 -0
  108. package/dist/lib/hosts/tasks.d.ts +14 -0
  109. package/dist/lib/hosts/tasks.js +15 -0
  110. package/dist/lib/lock.d.ts +93 -0
  111. package/dist/lib/lock.js +207 -0
  112. package/dist/lib/loop.js +16 -1
  113. package/dist/lib/machine-id.d.ts +21 -0
  114. package/dist/lib/machine-id.js +26 -0
  115. package/dist/lib/mailbox-target.d.ts +36 -0
  116. package/dist/lib/mailbox-target.js +45 -0
  117. package/dist/lib/mailbox.d.ts +47 -0
  118. package/dist/lib/mailbox.js +194 -0
  119. package/dist/lib/mcp.d.ts +5 -0
  120. package/dist/lib/mcp.js +24 -8
  121. package/dist/lib/migrate.d.ts +19 -0
  122. package/dist/lib/migrate.js +134 -26
  123. package/dist/lib/overdue.js +3 -0
  124. package/dist/lib/picker.d.ts +2 -0
  125. package/dist/lib/picker.js +4 -1
  126. package/dist/lib/platform/exec.d.ts +46 -0
  127. package/dist/lib/platform/exec.js +74 -0
  128. package/dist/lib/platform/process.d.ts +31 -0
  129. package/dist/lib/platform/process.js +34 -1
  130. package/dist/lib/platform/winpath.js +2 -0
  131. package/dist/lib/plugins.js +16 -6
  132. package/dist/lib/profiles.d.ts +25 -0
  133. package/dist/lib/profiles.js +22 -6
  134. package/dist/lib/pty-client.js +2 -1
  135. package/dist/lib/rotate.d.ts +61 -0
  136. package/dist/lib/rotate.js +52 -0
  137. package/dist/lib/routines.d.ts +40 -2
  138. package/dist/lib/routines.js +66 -8
  139. package/dist/lib/runner.d.ts +11 -2
  140. package/dist/lib/runner.js +49 -7
  141. package/dist/lib/scheduler.js +6 -1
  142. package/dist/lib/secrets/bundles.d.ts +60 -4
  143. package/dist/lib/secrets/bundles.js +131 -12
  144. package/dist/lib/secrets/filestore.d.ts +3 -0
  145. package/dist/lib/secrets/filestore.js +42 -16
  146. package/dist/lib/secrets/index.d.ts +43 -2
  147. package/dist/lib/secrets/index.js +102 -3
  148. package/dist/lib/secrets/mcp.d.ts +93 -0
  149. package/dist/lib/secrets/mcp.js +205 -0
  150. package/dist/lib/secrets/remote.js +12 -5
  151. package/dist/lib/secrets/sync.js +83 -4
  152. package/dist/lib/secrets/windows.js +14 -3
  153. package/dist/lib/serve/data.d.ts +81 -0
  154. package/dist/lib/serve/data.js +91 -0
  155. package/dist/lib/serve/page.d.ts +7 -0
  156. package/dist/lib/serve/page.js +140 -0
  157. package/dist/lib/serve/server.d.ts +46 -0
  158. package/dist/lib/serve/server.js +115 -0
  159. package/dist/lib/session/active.d.ts +54 -0
  160. package/dist/lib/session/active.js +190 -19
  161. package/dist/lib/session/discover.d.ts +37 -0
  162. package/dist/lib/session/discover.js +111 -28
  163. package/dist/lib/session/inject.d.ts +18 -0
  164. package/dist/lib/session/inject.js +21 -0
  165. package/dist/lib/session/parse.js +23 -20
  166. package/dist/lib/session/pid-registry.d.ts +1 -0
  167. package/dist/lib/session/pid-registry.js +24 -0
  168. package/dist/lib/session/provenance.d.ts +14 -2
  169. package/dist/lib/session/provenance.js +39 -8
  170. package/dist/lib/session/remote-active.js +19 -7
  171. package/dist/lib/session/remote-list.d.ts +51 -0
  172. package/dist/lib/session/remote-list.js +213 -0
  173. package/dist/lib/session/remote.d.ts +7 -1
  174. package/dist/lib/session/remote.js +16 -2
  175. package/dist/lib/session/sync/config.d.ts +1 -15
  176. package/dist/lib/session/sync/config.js +4 -20
  177. package/dist/lib/session/types.d.ts +17 -0
  178. package/dist/lib/shims.d.ts +36 -6
  179. package/dist/lib/shims.js +91 -29
  180. package/dist/lib/ssh-exec.js +2 -0
  181. package/dist/lib/ssh-tunnel.js +2 -1
  182. package/dist/lib/startup/command-registry.d.ts +6 -0
  183. package/dist/lib/startup/command-registry.js +13 -1
  184. package/dist/lib/state.d.ts +13 -0
  185. package/dist/lib/state.js +103 -9
  186. package/dist/lib/sync-umbrella.d.ts +14 -7
  187. package/dist/lib/sync-umbrella.js +17 -9
  188. package/dist/lib/teams/forEach.d.ts +110 -0
  189. package/dist/lib/teams/forEach.js +186 -0
  190. package/dist/lib/teams/index.d.ts +1 -0
  191. package/dist/lib/teams/index.js +1 -0
  192. package/dist/lib/teams/pr-watch.d.ts +226 -0
  193. package/dist/lib/teams/pr-watch.js +371 -0
  194. package/dist/lib/teams/supervisor.d.ts +14 -1
  195. package/dist/lib/teams/supervisor.js +19 -0
  196. package/dist/lib/teams/worktree.d.ts +9 -0
  197. package/dist/lib/teams/worktree.js +32 -0
  198. package/dist/lib/terminal/backends/index.d.ts +2 -1
  199. package/dist/lib/terminal/backends/index.js +3 -1
  200. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  201. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  202. package/dist/lib/terminal/index.d.ts +4 -1
  203. package/dist/lib/terminal/index.js +4 -1
  204. package/dist/lib/terminal/inject.d.ts +204 -0
  205. package/dist/lib/terminal/inject.js +247 -0
  206. package/dist/lib/terminal/resolve.d.ts +64 -0
  207. package/dist/lib/terminal/resolve.js +90 -0
  208. package/dist/lib/terminal/types.d.ts +1 -1
  209. package/dist/lib/triggers/webhook.d.ts +85 -0
  210. package/dist/lib/triggers/webhook.js +141 -0
  211. package/dist/lib/versions.d.ts +23 -0
  212. package/dist/lib/versions.js +119 -13
  213. package/dist/lib/watchdog/index.d.ts +3 -0
  214. package/dist/lib/watchdog/index.js +5 -0
  215. package/dist/lib/watchdog/read.d.ts +35 -0
  216. package/dist/lib/watchdog/read.js +149 -0
  217. package/dist/lib/watchdog/runner.d.ts +127 -0
  218. package/dist/lib/watchdog/runner.js +322 -0
  219. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  220. package/dist/lib/watchdog/watchdog.js +166 -0
  221. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  222. package/dist/lib/watchdog/watchdogTail.js +154 -0
  223. package/dist/lib/workflows.d.ts +166 -0
  224. package/dist/lib/workflows.js +193 -0
  225. package/package.json +5 -4
@@ -11,7 +11,7 @@ import { connectSSH, shellQuote } from './drivers/ssh.js';
11
11
  import { clearProfileRuntime } from './runtime-state.js';
12
12
  import { resolveDomainSkill } from './domain-skills.js';
13
13
  import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
14
- import { getRefs, resolveRefToCoords } from './refs.js';
14
+ import { getRefs, resolveRefToCoords, describeRefs, healRef } from './refs.js';
15
15
  import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
16
16
  import { typeEditorText } from './editor.js';
17
17
  import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
@@ -672,6 +672,31 @@ export class BrowserService {
672
672
  { width: 0, height: 0 };
673
673
  return { path: finalPath, bytes: buffer.length, width: dims.width, height: dims.height };
674
674
  }
675
+ /**
676
+ * Export the current tab as a PDF via CDP `Page.printToPDF`. Reuses the
677
+ * screenshot session + tab resolution so `--tab`, path sandboxing, and the
678
+ * auto-path (`sessions/<task>/<ts>.pdf`) all behave identically to
679
+ * `screenshot`. `printBackground: true` matches Chrome's default print flow
680
+ * — without it, dark-mode pages render on a blank sheet.
681
+ */
682
+ async printToPdf(taskId, tabHint, outputPath) {
683
+ const { conn, task } = await this.findTask(taskId);
684
+ const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
685
+ const cdpTargetId = this.getCdpTargetId(task, shortId);
686
+ const target = await this.getTarget(conn, cdpTargetId);
687
+ if (!target) {
688
+ throw new Error(`Tab ${shortId} not found`);
689
+ }
690
+ const sessionId = await this.getSessionId(conn, target.targetId);
691
+ const { data } = (await conn.cdp.send('Page.printToPDF', { printBackground: true, preferCSSPageSize: true }, sessionId));
692
+ const buffer = Buffer.from(data, 'base64');
693
+ const sessionsDir = path.join(getBrowserRuntimeDir(), 'sessions', task.name);
694
+ const automaticPath = path.join(sessionsDir, `${Date.now()}.pdf`);
695
+ const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
696
+ await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
697
+ await fs.promises.writeFile(finalPath, buffer);
698
+ return { path: finalPath, bytes: buffer.length };
699
+ }
675
700
  // ─── Recording ──────────────────────────────────────────────────────────────
676
701
  //
677
702
  // CDP `Page.startScreencast` emits a JPEG frame per `everyNthFrame`. We pipe
@@ -722,7 +747,7 @@ export class BrowserService {
722
747
  '-vsync', 'vfr',
723
748
  '-y',
724
749
  outputPath,
725
- ], { stdio: ['pipe', 'ignore', 'pipe'] });
750
+ ], { stdio: ['pipe', 'ignore', 'pipe'], windowsHide: true });
726
751
  // Wait for the spawn to confirm (or fail) before we wire CDP frames into a
727
752
  // dead pipe. ENOENT from a missing ffmpeg surfaces here as a real error
728
753
  // instead of a silently empty .webm.
@@ -880,7 +905,6 @@ export class BrowserService {
880
905
  return { recording: false };
881
906
  return { recording: true, path: rec.outputPath, elapsedMs: Date.now() - rec.startedAt };
882
907
  }
883
- refsCache = new Map();
884
908
  async refs(taskId, tabHint, opts = {}) {
885
909
  const { conn, task } = await this.findTask(taskId);
886
910
  const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
@@ -889,7 +913,51 @@ export class BrowserService {
889
913
  if (!target)
890
914
  throw new Error(`Tab ${shortId} not found`);
891
915
  const sessionId = await this.getSessionId(conn, target.targetId);
892
- return getRefs(conn.cdp, sessionId, opts);
916
+ const result = await getRefs(conn.cdp, sessionId, opts);
917
+ // Snapshot the stable descriptors AND the opts they were numbered against
918
+ // so a later click/type can self-heal a drifted ref by rebuilding with the
919
+ // same filter. `refs()` is the sole owner of this cache — actions read it,
920
+ // never overwrite it. Persist to tasks.json so it survives a daemon
921
+ // restart.
922
+ this.cacheRefDescriptors(task, shortId, result.nodeMap, result.opts);
923
+ await this.saveTaskState(task.profile, conn.tasks);
924
+ return { refs: result.refs, nodeMap: result.nodeMap };
925
+ }
926
+ /** Record the last ref listing (descriptors + opts) for a tab into state. */
927
+ cacheRefDescriptors(task, shortId, nodeMap, opts) {
928
+ if (!task.refDescriptors)
929
+ task.refDescriptors = {};
930
+ task.refDescriptors[shortId] = { descriptors: describeRefs(nodeMap), opts };
931
+ }
932
+ /**
933
+ * Re-resolve a caller-supplied ref against a freshly-built node map, healing
934
+ * it back to the right element when the integer ref has drifted since the
935
+ * cached `refs` listing. Shared by `click` and `type` so both self-heal
936
+ * identically. Returns the ref to act on plus, when a heal occurred, the
937
+ * {@link HealInfo} to surface. Throws when the cached element is gone.
938
+ */
939
+ resolveHealedRef(snapshot, nodeMap, ref) {
940
+ const cached = snapshot?.descriptors.find((d) => d.ref === ref);
941
+ if (!cached)
942
+ return { targetRef: ref };
943
+ const fresh = nodeMap.get(ref);
944
+ const stillMatches = fresh !== undefined &&
945
+ fresh.role === cached.role &&
946
+ fresh.name === cached.name &&
947
+ fresh.backendNodeId !== undefined;
948
+ if (stillMatches)
949
+ return { targetRef: ref };
950
+ const newRef = healRef(cached, nodeMap);
951
+ if (newRef === null) {
952
+ throw new Error(`Ref ${ref} (${cached.role} "${cached.name}") could not be re-resolved: ` +
953
+ `no matching element on the current page. Re-run 'browser refs' to ` +
954
+ `refresh the ref numbers, or act by position with 'browser click --at X,Y'.`);
955
+ }
956
+ if (newRef === ref)
957
+ return { targetRef: ref };
958
+ console.error(`[browser] self-healed ref ${ref} -> ${newRef} (${cached.role} "${cached.name}") — ` +
959
+ `cached descriptor re-matched after the ref drifted`);
960
+ return { targetRef: newRef, healed: { from: ref, to: newRef, role: cached.role, name: cached.name } };
893
961
  }
894
962
  async click(taskId, ref, tabHint) {
895
963
  const { conn, task } = await this.findTask(taskId);
@@ -899,8 +967,38 @@ export class BrowserService {
899
967
  if (!target)
900
968
  throw new Error(`Tab ${shortId} not found`);
901
969
  const sessionId = await this.getSessionId(conn, target.targetId);
902
- const { nodeMap } = await getRefs(conn.cdp, sessionId, { interactive: false, limit: 1000 });
903
- const { x, y } = await resolveRefToCoords(conn.cdp, sessionId, nodeMap, ref);
970
+ // Rebuild the node map with the SAME opts the cached listing was numbered
971
+ // against, so the caller's ref lands on the element they saw in `browser
972
+ // refs`. Rebuilding with a different filter (the old interactive:false)
973
+ // renumbers every ref and defeats self-heal on the second click. Default
974
+ // to the user-facing interactive numbering when no listing was cached yet.
975
+ const snapshot = task.refDescriptors?.[shortId];
976
+ const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
977
+ const { nodeMap } = await getRefs(conn.cdp, sessionId, buildOpts);
978
+ // Self-heal: the integer ref is positional and drifts on re-render. If the
979
+ // fresh node at this position no longer matches the cached descriptor,
980
+ // re-resolve (by attrs/proximity-tie-broken role+name) BEFORE clicking the
981
+ // wrong element. The cache is owned by refs() and NOT rewritten here — the
982
+ // caller's ref numbers stay anchored to the listing they came from.
983
+ const { targetRef, healed } = this.resolveHealedRef(snapshot, nodeMap, ref);
984
+ const { x, y } = await resolveRefToCoords(conn.cdp, sessionId, nodeMap, targetRef);
985
+ await clickAtCoords(conn.cdp, sessionId, x, y);
986
+ return healed ? { healed } : {};
987
+ }
988
+ /**
989
+ * Click raw viewport coordinates, bypassing ref resolution entirely. Backs
990
+ * `browser click --at X,Y` — the escape hatch when the accessibility tree
991
+ * exposes no usable ref (canvas apps, custom-drawn UI) and the caller has
992
+ * located the target from a screenshot.
993
+ */
994
+ async clickAt(taskId, x, y, tabHint) {
995
+ const { conn, task } = await this.findTask(taskId);
996
+ const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
997
+ const cdpTargetId = this.getCdpTargetId(task, shortId);
998
+ const target = await this.getTarget(conn, cdpTargetId);
999
+ if (!target)
1000
+ throw new Error(`Tab ${shortId} not found`);
1001
+ const sessionId = await this.getSessionId(conn, target.targetId);
904
1002
  await clickAtCoords(conn.cdp, sessionId, x, y);
905
1003
  }
906
1004
  async type(taskId, ref, text, tabHint, clear) {
@@ -911,8 +1009,14 @@ export class BrowserService {
911
1009
  if (!target)
912
1010
  throw new Error(`Tab ${shortId} not found`);
913
1011
  const sessionId = await this.getSessionId(conn, target.targetId);
914
- const { nodeMap } = await getRefs(conn.cdp, sessionId, { interactive: false, limit: 1000 });
915
- const node = nodeMap.get(ref);
1012
+ // Same self-healing story as click(): rebuild against the cached listing's
1013
+ // opts so refs line up with what the user saw, then heal a drifted ref
1014
+ // before typing into the wrong field.
1015
+ const snapshot = task.refDescriptors?.[shortId];
1016
+ const buildOpts = snapshot?.opts ?? { interactive: true, limit: 500 };
1017
+ const { nodeMap } = await getRefs(conn.cdp, sessionId, buildOpts);
1018
+ const { targetRef } = this.resolveHealedRef(snapshot, nodeMap, ref);
1019
+ const node = nodeMap.get(targetRef);
916
1020
  if (!node)
917
1021
  throw new Error(`Ref ${ref} not found`);
918
1022
  if (node.editor) {
@@ -78,6 +78,15 @@ export interface Task {
78
78
  currentTabId?: string;
79
79
  createdAt: number;
80
80
  pid: number;
81
+ /**
82
+ * Per-tab snapshot of the last ref listing captured for that tab
83
+ * (shortId -> {descriptors, opts}). Persisted to tasks.json so a later
84
+ * `click`/`type <ref>` can self-heal a drifted ref — the cached `opts` let
85
+ * the action rebuild its node map with the SAME accessibility filter the
86
+ * listing was numbered against. Owned by `refs()`; actions never overwrite
87
+ * it. See RefSnapshot / RefDescriptor.
88
+ */
89
+ refDescriptors?: Record<string, import('./refs.js').RefSnapshot>;
81
90
  }
82
91
  export interface TabInfo {
83
92
  id: string;
@@ -118,7 +127,7 @@ export interface HistoricalTask {
118
127
  domains: string[];
119
128
  tabCount: number;
120
129
  }
121
- export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
130
+ export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'pdf' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
122
131
  export interface IPCRequest {
123
132
  action: IPCAction;
124
133
  task?: string;
@@ -129,6 +138,8 @@ export interface IPCRequest {
129
138
  expr?: string;
130
139
  path?: string;
131
140
  ref?: number;
141
+ atX?: number;
142
+ atY?: number;
132
143
  text?: string;
133
144
  key?: string;
134
145
  scrollX?: number;
@@ -194,6 +205,8 @@ export interface IPCResponse {
194
205
  height?: number;
195
206
  refs?: string;
196
207
  nodes?: RefNodeJson[];
208
+ /** Human-readable note surfaced to the CLI (e.g. a self-heal notice on click). */
209
+ message?: string;
197
210
  port?: number;
198
211
  pid?: number;
199
212
  fps?: number;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Live budget kill-switch for `agents cloud` dispatch (issue #399).
3
+ *
4
+ * Follow-up to #346, which shipped a pre-flight budget gate for cloud runs but
5
+ * no live mid-run enforcement — a runaway cloud task would keep spending until
6
+ * the user hit Ctrl-C. This wires the same `makeLiveSpendWatcher` the local
7
+ * headless run uses (src/lib/exec.ts) into the cloud SSE stream: each `usage`
8
+ * event from the provider feeds the watcher, and on breach we call
9
+ * `provider.cancel(taskId)` to terminate server-side.
10
+ *
11
+ * Wrapper shape mirrors `renderStream`'s input — it takes an
12
+ * `AsyncIterable<CloudEvent>` and yields the same events downstream. Dormant
13
+ * (returns the source stream unchanged) when no caps are configured.
14
+ */
15
+ import type { CloudProvider, CloudEvent } from '../cloud/types.js';
16
+ import { type BreachInfo } from './enforce.js';
17
+ /** Result surface exposed to the caller so it can act on a mid-stream breach. */
18
+ export interface CloudBudgetGate {
19
+ /** True once a cap crossed and cancel() was invoked. */
20
+ breached(): boolean;
21
+ breach(): BreachInfo | null;
22
+ }
23
+ /**
24
+ * Wrap a cloud event stream with a live budget watcher. On a `usage` event we
25
+ * feed the shared watcher; on the first breach we call `provider.cancel(taskId)`
26
+ * and forward a synthetic `status:'cancelled'` + `error` frame so the renderer
27
+ * shows why the task stopped. Returns null when no caps are configured; callers
28
+ * fall through to the raw stream in that case.
29
+ */
30
+ export declare function wrapStreamWithBudgetGate(args: {
31
+ provider: CloudProvider;
32
+ taskId: string;
33
+ /** Project attribution key — repo slug for Rush, or cwd. */
34
+ project: string;
35
+ /** Agent the dispatch runs under (for per_agent cap accounting). */
36
+ agent: string;
37
+ /** cwd used to resolve the effective budget config. */
38
+ cwd?: string;
39
+ }): {
40
+ wrap: (source: AsyncIterable<CloudEvent>) => AsyncIterable<CloudEvent>;
41
+ gate: CloudBudgetGate;
42
+ } | null;
@@ -0,0 +1,79 @@
1
+ import { capsFromConfig, makeLiveSpendWatcher, } from './enforce.js';
2
+ import { resolveBudgetConfig, hasAnyCap } from './config.js';
3
+ import { loadLedger, localDay, spendForDay, spendForProject } from './ledger.js';
4
+ /**
5
+ * Wrap a cloud event stream with a live budget watcher. On a `usage` event we
6
+ * feed the shared watcher; on the first breach we call `provider.cancel(taskId)`
7
+ * and forward a synthetic `status:'cancelled'` + `error` frame so the renderer
8
+ * shows why the task stopped. Returns null when no caps are configured; callers
9
+ * fall through to the raw stream in that case.
10
+ */
11
+ export function wrapStreamWithBudgetGate(args) {
12
+ const cfg = resolveBudgetConfig(args.cwd);
13
+ if (!hasAnyCap(cfg))
14
+ return null;
15
+ const today = localDay();
16
+ const entries = loadLedger();
17
+ const caps = capsFromConfig(cfg, {
18
+ daySpend: spendForDay(today, entries),
19
+ projectSpend: spendForProject(args.project, entries),
20
+ });
21
+ let firstBreach = null;
22
+ const watcher = makeLiveSpendWatcher({
23
+ caps,
24
+ onBreach: (b) => {
25
+ firstBreach = b;
26
+ },
27
+ });
28
+ async function* wrap(source) {
29
+ try {
30
+ for await (const event of source) {
31
+ if (event.type === 'usage') {
32
+ watcher.feedUsage({
33
+ agent: args.agent,
34
+ model: event.model,
35
+ inputTokens: event.inputTokens ?? 0,
36
+ outputTokens: event.outputTokens ?? 0,
37
+ });
38
+ if (watcher.breached() && firstBreach) {
39
+ // Cancel server-side FIRST so we stop the meter; then surface the
40
+ // breach to the renderer as an error + cancelled status so the CLI
41
+ // exits with a visible reason (not a silent stream close).
42
+ try {
43
+ await args.provider.cancel(args.taskId);
44
+ }
45
+ catch (err) {
46
+ // Best-effort — even if cancel fails, break the stream: the
47
+ // caller sees the error frame and can retry manually.
48
+ yield {
49
+ type: 'error',
50
+ message: `[budget] cap ${firstBreach.cap} exceeded ($${firstBreach.spend.toFixed(2)} > $${firstBreach.limit.toFixed(2)}); cancel FAILED: ${err.message}`,
51
+ timestamp: new Date().toISOString(),
52
+ };
53
+ yield { type: 'status', status: 'cancelled', timestamp: new Date().toISOString() };
54
+ return;
55
+ }
56
+ yield {
57
+ type: 'error',
58
+ message: `[budget] cap ${firstBreach.cap} exceeded ($${firstBreach.spend.toFixed(2)} > $${firstBreach.limit.toFixed(2)}) — cancelled cloud task ${args.taskId}`,
59
+ timestamp: new Date().toISOString(),
60
+ };
61
+ yield { type: 'status', status: 'cancelled', timestamp: new Date().toISOString() };
62
+ return;
63
+ }
64
+ }
65
+ yield event;
66
+ }
67
+ }
68
+ finally {
69
+ watcher.dispose();
70
+ }
71
+ }
72
+ return {
73
+ wrap,
74
+ gate: {
75
+ breached: () => watcher.breached(),
76
+ breach: () => firstBreach,
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,31 @@
1
+ import type { AgentManager } from '../teams/agents.js';
2
+ import { type BreachInfo } from './enforce.js';
3
+ /** Public surface of the team-scoped budget watcher. */
4
+ export interface TeamBudgetWatcher {
5
+ /** Feed any new usage events from every running teammate's stdout.log. */
6
+ poll(): Promise<void>;
7
+ /** True once any cap has been crossed (mirrors LiveSpendWatcher). */
8
+ breached(): boolean;
9
+ /** The first breach seen, or null if none yet. */
10
+ breach(): BreachInfo | null;
11
+ /** Release references and stop tapping streams. Idempotent. */
12
+ dispose(): void;
13
+ }
14
+ /**
15
+ * Build a team-scoped budget watcher for the given team. Returns null when the
16
+ * effective budget config has no caps — matching the local watcher's dormant
17
+ * contract so callers pay nothing when the feature is off.
18
+ *
19
+ * The watcher is seeded with prior-day and prior-project spend from the ledger
20
+ * so a partway-through day counts today's earlier runs against `per_day`.
21
+ * Each `poll()` reads new bytes from every running teammate's stdout.log and
22
+ * feeds parsed usage events into the shared watcher — `onBreach` fires exactly
23
+ * once, mirroring `makeLiveSpendWatcher`.
24
+ */
25
+ export declare function createTeamBudgetWatcher(args: {
26
+ manager: AgentManager;
27
+ team: string;
28
+ /** Project/cwd used to (a) resolve budget config and (b) seed project spend. */
29
+ cwd: string;
30
+ onBreach: (breach: BreachInfo) => void;
31
+ }): TeamBudgetWatcher | null;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Live budget kill-switch for `agents teams` supervisor (issue #399).
3
+ *
4
+ * Follow-up to #346, which wired `makeLiveSpendWatcher` into local `agents run`
5
+ * only. Teams spawns each teammate as its own `agents run --headless --json`
6
+ * process, so per-teammate `per_run` caps already fire from those child
7
+ * watchers. What was still missing was aggregate cross-teammate enforcement
8
+ * (`per_project` / `per_day` / `per_agent` combined), because the ledger only
9
+ * gets written on child close — a long-running teammate would blow the shared
10
+ * cap without any single child watcher noticing.
11
+ *
12
+ * This module reuses `makeLiveSpendWatcher` verbatim: on each supervisor wave
13
+ * we tail every running teammate's stdout.log through `extractUsageEvents`
14
+ * (the same primitive the local run path uses in src/lib/exec.ts), feed the
15
+ * usage events into ONE shared watcher, and expose `breached()` so the
16
+ * supervisor can call `stopByTask` to terminate the whole team.
17
+ *
18
+ * Dormant (returns null) when no caps are configured — same zero-cost contract
19
+ * as the local watcher.
20
+ */
21
+ import * as fs from 'fs';
22
+ import { capsFromConfig, extractUsageEvents, makeLiveSpendWatcher, } from './enforce.js';
23
+ import { resolveBudgetConfig, hasAnyCap } from './config.js';
24
+ import { loadLedger, localDay, spendForDay, spendForProject } from './ledger.js';
25
+ /**
26
+ * Build a team-scoped budget watcher for the given team. Returns null when the
27
+ * effective budget config has no caps — matching the local watcher's dormant
28
+ * contract so callers pay nothing when the feature is off.
29
+ *
30
+ * The watcher is seeded with prior-day and prior-project spend from the ledger
31
+ * so a partway-through day counts today's earlier runs against `per_day`.
32
+ * Each `poll()` reads new bytes from every running teammate's stdout.log and
33
+ * feeds parsed usage events into the shared watcher — `onBreach` fires exactly
34
+ * once, mirroring `makeLiveSpendWatcher`.
35
+ */
36
+ export function createTeamBudgetWatcher(args) {
37
+ const cfg = resolveBudgetConfig(args.cwd);
38
+ if (!hasAnyCap(cfg))
39
+ return null;
40
+ const today = localDay();
41
+ const entries = loadLedger();
42
+ const caps = capsFromConfig(cfg, {
43
+ daySpend: spendForDay(today, entries),
44
+ projectSpend: spendForProject(args.cwd, entries),
45
+ });
46
+ let firstBreach = null;
47
+ const watcher = makeLiveSpendWatcher({
48
+ caps,
49
+ onBreach: (b) => {
50
+ firstBreach = b;
51
+ args.onBreach(b);
52
+ },
53
+ });
54
+ const cursors = new Map();
55
+ let disposed = false;
56
+ return {
57
+ async poll() {
58
+ if (disposed || watcher.breached())
59
+ return;
60
+ const teammates = await args.manager.listByTask(args.team);
61
+ for (const agent of teammates) {
62
+ // Only tap running local teammates: PENDING has no output yet, cloud
63
+ // teammates emit through a different channel (their own SSE stream),
64
+ // and terminal states produce no new bytes.
65
+ if (agent.status !== 'running')
66
+ continue;
67
+ if (agent.cloudProvider)
68
+ continue;
69
+ const stdoutPath = await agent.getStdoutPath();
70
+ let stat;
71
+ try {
72
+ stat = fs.statSync(stdoutPath);
73
+ }
74
+ catch {
75
+ continue; // File not yet created — nothing to read.
76
+ }
77
+ const cursor = cursors.get(agent.agentId) ?? { offset: 0, pending: '' };
78
+ if (stat.size <= cursor.offset) {
79
+ cursors.set(agent.agentId, cursor);
80
+ continue;
81
+ }
82
+ const fd = fs.openSync(stdoutPath, 'r');
83
+ try {
84
+ const toRead = stat.size - cursor.offset;
85
+ const buffer = Buffer.alloc(toRead);
86
+ const bytesRead = fs.readSync(fd, buffer, 0, toRead, cursor.offset);
87
+ const chunk = buffer.toString('utf-8', 0, bytesRead);
88
+ cursor.offset += bytesRead;
89
+ const { events, rest } = extractUsageEvents(chunk, cursor.pending, undefined, agent.agentType);
90
+ cursor.pending = rest;
91
+ for (const ev of events) {
92
+ watcher.feedUsage(ev);
93
+ if (watcher.breached())
94
+ break;
95
+ }
96
+ }
97
+ finally {
98
+ fs.closeSync(fd);
99
+ }
100
+ cursors.set(agent.agentId, cursor);
101
+ if (watcher.breached())
102
+ return;
103
+ }
104
+ },
105
+ breached: () => watcher.breached(),
106
+ breach: () => firstBreach,
107
+ dispose() {
108
+ if (disposed)
109
+ return;
110
+ disposed = true;
111
+ watcher.dispose();
112
+ cursors.clear();
113
+ },
114
+ };
115
+ }
@@ -104,6 +104,10 @@ export class CodexCloudProvider {
104
104
  cancel: false,
105
105
  message: false,
106
106
  multiRepo: false,
107
+ // `codex cloud exec --env <id> <prompt>` is the only dispatch surface the
108
+ // codex CLI exposes — it has no flag to attach images or ride-along skills,
109
+ // and the env bundles its own context at creation time. Both stay false
110
+ // until the upstream CLI grows an attachment surface.
107
111
  skills: false,
108
112
  images: false,
109
113
  };
@@ -4,7 +4,7 @@
4
4
  * Auth: reads the session token from ~/.rush/user.yaml (written by `rush login`).
5
5
  * Requires the Rush GitHub App installed on the target repo.
6
6
  */
7
- import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOptions, ProviderCapabilities } from './types.js';
7
+ import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOptions, ProviderCapabilities, ImageAttachment, SkillRef } from './types.js';
8
8
  export declare const RUSH_CONSENT_PATH: string;
9
9
  export declare function hasRushUploadConsent(accountFingerprint: string, opts?: DispatchOptions, consentPath?: string): boolean;
10
10
  /** One version's entry in the account manifest sent on every dispatch. */
@@ -63,6 +63,17 @@ export declare function buildDispatchBody(input: {
63
63
  }>;
64
64
  accountManifest?: AccountManifest | null;
65
65
  accountTokens?: AccountTokenEntry[] | null;
66
+ /**
67
+ * Skill ride-alongs so the cloud pod isn't context-blind. Forwarded verbatim
68
+ * as `skills` so the Factory Floor can mount them by id/version before the
69
+ * agent runs. Omitted when empty.
70
+ */
71
+ skills?: SkillRef[] | null;
72
+ /**
73
+ * Base64 image attachments for vision dispatch. Sliced to
74
+ * MAX_IMAGES_PER_DISPATCH — extras are dropped, never sent. Omitted when empty.
75
+ */
76
+ images?: ImageAttachment[] | null;
66
77
  }): Record<string, unknown>;
67
78
  /** A single account registered in Rush Cloud's multi-account rotation pool. */
68
79
  export interface RemoteAccount {
@@ -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 };