chainlesschain 0.162.158 → 0.162.159

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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
  6. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  7. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
  52. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  103. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
  128. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  129. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-manifest.json +17 -3
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +143 -3
  153. package/src/commands/browse.js +86 -0
  154. package/src/commands/cloud.js +232 -0
  155. package/src/commands/doctor.js +61 -6
  156. package/src/commands/mcp.js +61 -0
  157. package/src/commands/plugin.js +63 -1
  158. package/src/commands/routine.js +367 -0
  159. package/src/commands/session.js +326 -0
  160. package/src/data/changelog.json +7 -2
  161. package/src/harness/jsonl-session-store.js +82 -2
  162. package/src/harness/session-index.js +295 -0
  163. package/src/harness/session-mirror.js +142 -0
  164. package/src/harness/transcript-integrity.js +182 -0
  165. package/src/index.js +8 -0
  166. package/src/lib/background-agent-supervisor.js +126 -2
  167. package/src/lib/channels/channel-manager.js +126 -0
  168. package/src/lib/channels/telegram-channel.js +112 -0
  169. package/src/lib/channels/webhook-channel.js +140 -0
  170. package/src/lib/chrome-connector.js +386 -0
  171. package/src/lib/cloud/bundle.js +110 -0
  172. package/src/lib/cloud/cloud-client.js +152 -0
  173. package/src/lib/computer-use/authorization.js +173 -0
  174. package/src/lib/computer-use/computer-use-server.js +276 -0
  175. package/src/lib/computer-use/control-backend.js +150 -0
  176. package/src/lib/doctor-checkup.js +652 -0
  177. package/src/lib/jsonl-session-store.js +4 -0
  178. package/src/lib/lsp/lsp-server-registry.js +48 -21
  179. package/src/lib/plugin-runtime/governance.js +174 -0
  180. package/src/lib/pr-link-ledger.js +221 -0
  181. package/src/lib/routine-store.js +352 -0
  182. package/src/lib/session-store-guard.js +40 -0
  183. package/src/lib/session-usage.js +318 -2
  184. package/src/lib/sub-agent-context.js +23 -0
  185. package/src/repl/agent-repl.js +361 -3
  186. package/src/repl/bg-dashboard.js +355 -0
  187. package/src/repl/fast-mode.js +103 -0
  188. package/src/repl/fullscreen-tui.js +197 -0
  189. package/src/repl/slash-command-registry.js +11 -3
  190. package/src/repl/voice-dictation.js +132 -0
  191. package/src/runtime/agent-core.js +242 -1
  192. package/src/runtime/coding-agent-contract-shared.cjs +88 -0
  193. package/src/runtime/coding-agent-policy.cjs +17 -0
  194. package/src/runtime/headless-runner.js +71 -7
  195. package/src/runtime/headless-stream.js +14 -8
  196. package/src/runtime/policies/agent-policy.js +3 -0
  197. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
  199. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  200. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  201. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -73,6 +73,7 @@ const _deps = {
73
73
  homedir: () => os.homedir(),
74
74
  platform: () => process.platform,
75
75
  env: () => process.env,
76
+ tmpdir: () => os.tmpdir(),
76
77
  httpGet,
77
78
  importPlaywright: () => import("playwright"),
78
79
  };
@@ -346,4 +347,389 @@ export async function captureState({
346
347
  }
347
348
  }
348
349
 
350
+ // ─── Browser Action mode (gap-analysis #6) ────────────────────────────────
351
+ //
352
+ // captureState/browser_state stay the read-only DEFAULT. performActions is
353
+ // the explicit, approval-gated capability that DRIVES the connected browser:
354
+ // click / type / press / navigate / waitForSelector / screenshot /
355
+ // assertText. Every executed step is appended to an audit JSONL under
356
+ // ~/.chainlesschain/browser-actions/<date>.jsonl (CC_BROWSER_ACTIONS_DIR
357
+ // overrides the dir for tests). Screenshot paths are ALWAYS generated
358
+ // internally — a caller-supplied path is refused (same invariant as
359
+ // browser_state: an action tool must not double as an arbitrary-file writer).
360
+
361
+ export const SUPPORTED_BROWSER_ACTIONS = Object.freeze([
362
+ "click",
363
+ "type",
364
+ "press",
365
+ "navigate",
366
+ "waitForSelector",
367
+ "screenshot",
368
+ "assertText",
369
+ ]);
370
+ export const MAX_BROWSER_ACTIONS = 30;
371
+ export const DEFAULT_ACTION_TIMEOUT_MS = 10000;
372
+ export const MAX_ACTION_TIMEOUT_MS = 30000;
373
+
374
+ const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
375
+
376
+ /** Audit log directory (CC_BROWSER_ACTIONS_DIR override for tests). */
377
+ export function browserActionsDir({ deps = _deps } = {}) {
378
+ const env = deps.env();
379
+ if (env.CC_BROWSER_ACTIONS_DIR) return env.CC_BROWSER_ACTIONS_DIR;
380
+ return path.join(deps.homedir(), ".chainlesschain", "browser-actions");
381
+ }
382
+
383
+ /**
384
+ * Resolve an optional caller CDP endpoint to a PORT, enforcing the connector's
385
+ * loopback-only contract: only http:// against 127.0.0.1 / localhost / [::1]
386
+ * is accepted. performActions then connects to `http://127.0.0.1:<port>` —
387
+ * the caller string is never used as the connection target verbatim, so a
388
+ * crafted endpoint cannot widen the trust boundary the prior security fix
389
+ * established.
390
+ */
391
+ export function resolveLoopbackCdpPort(
392
+ cdpUrl,
393
+ fallbackPort = DEFAULT_CDP_PORT,
394
+ ) {
395
+ if (cdpUrl == null || cdpUrl === "") return normalizeCdpPort(fallbackPort);
396
+ let parsed;
397
+ try {
398
+ parsed = new URL(String(cdpUrl));
399
+ } catch {
400
+ throw new Error(`Invalid CDP endpoint: ${cdpUrl}`);
401
+ }
402
+ if (parsed.protocol !== "http:") {
403
+ throw new Error(
404
+ `CDP endpoint must be http:// on loopback (got ${parsed.protocol})`,
405
+ );
406
+ }
407
+ if (!LOOPBACK_HOSTS.has(parsed.hostname.toLowerCase())) {
408
+ throw new Error(
409
+ `CDP endpoint must be loopback-only (127.0.0.1 / localhost) — refusing host "${parsed.hostname}"`,
410
+ );
411
+ }
412
+ return normalizeCdpPort(parsed.port || DEFAULT_CDP_PORT);
413
+ }
414
+
415
+ function truncateForAudit(value, cap = 200) {
416
+ const s = String(value);
417
+ return s.length > cap ? `${s.slice(0, cap)}…` : s;
418
+ }
419
+
420
+ /**
421
+ * Validate + normalize the caller's action list BEFORE anything connects.
422
+ * Throws on the first invalid action — performActions turns that into
423
+ * {ok:false, error} without touching the browser. Screenshot paths are
424
+ * generated here (deps.tmpdir), and any caller-supplied path key is refused.
425
+ */
426
+ export function normalizeBrowserActions(actions, { deps = _deps } = {}) {
427
+ if (!Array.isArray(actions) || actions.length === 0) {
428
+ throw new Error("actions must be a non-empty array of action objects");
429
+ }
430
+ if (actions.length > MAX_BROWSER_ACTIONS) {
431
+ throw new Error(
432
+ `too many actions (${actions.length}) — max ${MAX_BROWSER_ACTIONS} per call`,
433
+ );
434
+ }
435
+ const requireSelector = (raw, type) => {
436
+ const sel = raw.selector;
437
+ if (typeof sel !== "string" || sel.trim() === "" || sel.length > 1000) {
438
+ throw new Error(`${type} requires a non-empty "selector" string`);
439
+ }
440
+ return sel;
441
+ };
442
+ return actions.map((raw, i) => {
443
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
444
+ throw new Error(`action[${i}] must be an object with a "type"`);
445
+ }
446
+ const type = String(raw.type || "");
447
+ if (!SUPPORTED_BROWSER_ACTIONS.includes(type)) {
448
+ throw new Error(
449
+ `action[${i}] has unsupported type "${type}". Supported: ${SUPPORTED_BROWSER_ACTIONS.join(", ")}`,
450
+ );
451
+ }
452
+ switch (type) {
453
+ case "click":
454
+ return { type, selector: requireSelector(raw, "click") };
455
+ case "type": {
456
+ if (typeof raw.text !== "string") {
457
+ throw new Error(`type requires a "text" string`);
458
+ }
459
+ return { type, selector: requireSelector(raw, "type"), text: raw.text };
460
+ }
461
+ case "press": {
462
+ if (
463
+ typeof raw.key !== "string" ||
464
+ raw.key.trim() === "" ||
465
+ raw.key.length > 50
466
+ ) {
467
+ throw new Error(`press requires a non-empty "key" string`);
468
+ }
469
+ return { type, key: raw.key };
470
+ }
471
+ case "navigate": {
472
+ let parsed;
473
+ try {
474
+ parsed = new URL(String(raw.url || ""));
475
+ } catch {
476
+ throw new Error(`navigate has an invalid "url": ${raw.url}`);
477
+ }
478
+ if (!["http:", "https:"].includes(parsed.protocol)) {
479
+ throw new Error(
480
+ `navigate url must be http(s):// (got ${parsed.protocol})`,
481
+ );
482
+ }
483
+ return { type, url: String(raw.url) };
484
+ }
485
+ case "waitForSelector": {
486
+ const t = Number(raw.timeoutMs ?? raw.timeout_ms ?? 5000);
487
+ const timeoutMs = Math.min(
488
+ Math.max(Number.isFinite(t) ? t : 5000, 1),
489
+ MAX_ACTION_TIMEOUT_MS,
490
+ );
491
+ return {
492
+ type,
493
+ selector: requireSelector(raw, "waitForSelector"),
494
+ timeoutMs,
495
+ };
496
+ }
497
+ case "screenshot": {
498
+ // The path is GENERATED — never caller-supplied. Refuse loudly so a
499
+ // model (or user) learns the invariant instead of being silently
500
+ // second-guessed.
501
+ for (const k of ["path", "file", "output", "screenshotPath"]) {
502
+ if (raw[k] != null) {
503
+ throw new Error(
504
+ `screenshot path is generated internally — remove "${k}"`,
505
+ );
506
+ }
507
+ }
508
+ return {
509
+ type,
510
+ screenshotPath: path.join(
511
+ deps.tmpdir(),
512
+ `cc-browser-act-${Date.now()}-${i}.png`,
513
+ ),
514
+ };
515
+ }
516
+ case "assertText": {
517
+ if (typeof raw.expected !== "string" || raw.expected === "") {
518
+ throw new Error(`assertText requires a non-empty "expected" string`);
519
+ }
520
+ return {
521
+ type,
522
+ selector: requireSelector(raw, "assertText"),
523
+ expected: raw.expected,
524
+ };
525
+ }
526
+ /* c8 ignore next 2 -- unreachable: type already validated above */
527
+ default:
528
+ throw new Error(`unsupported action type "${type}"`);
529
+ }
530
+ });
531
+ }
532
+
533
+ /** One JSONL row per EXECUTED step (explicit utf-8, best-effort). */
534
+ function appendActionAudit(entry, deps) {
535
+ const dir = browserActionsDir({ deps });
536
+ deps.fs.mkdirSync(dir, { recursive: true });
537
+ const date = entry.ts.slice(0, 10);
538
+ deps.fs.appendFileSync(
539
+ path.join(dir, `${date}.jsonl`),
540
+ JSON.stringify(entry) + "\n",
541
+ "utf-8",
542
+ );
543
+ }
544
+
545
+ /**
546
+ * Does the dedicated connector profile own the CDP port we are attaching to?
547
+ * Chrome writes DevToolsActivePort (first line = port) into the user-data-dir
548
+ * it was launched with; if the file under ~/.chainlesschain/chrome-profile is
549
+ * missing or names a different port, we are driving some OTHER profile —
550
+ * possibly the user's real logged-in browser — and the result carries a
551
+ * profileWarning.
552
+ */
553
+ function connectorProfileOwnsPort(port, deps) {
554
+ try {
555
+ const marker = path.join(
556
+ connectorProfileDir({ deps }),
557
+ "DevToolsActivePort",
558
+ );
559
+ if (!deps.fs.existsSync(marker)) return false;
560
+ const firstLine = String(deps.fs.readFileSync(marker, "utf-8"))
561
+ .split(/\r?\n/)[0]
562
+ .trim();
563
+ return Number(firstLine) === Number(port);
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+
569
+ async function runOneAction(page, act, timeoutMs) {
570
+ switch (act.type) {
571
+ case "click":
572
+ await page.click(act.selector, { timeout: timeoutMs });
573
+ return `clicked ${act.selector}`;
574
+ case "type":
575
+ await page.fill(act.selector, act.text, { timeout: timeoutMs });
576
+ return `typed ${act.text.length} chars into ${act.selector}`;
577
+ case "press":
578
+ await page.keyboard.press(act.key);
579
+ return `pressed ${act.key}`;
580
+ case "navigate":
581
+ await page.goto(act.url, {
582
+ waitUntil: "domcontentloaded",
583
+ timeout: timeoutMs,
584
+ });
585
+ return `navigated to ${act.url}`;
586
+ case "waitForSelector":
587
+ await page.waitForSelector(act.selector, { timeout: act.timeoutMs });
588
+ return `selector appeared: ${act.selector}`;
589
+ case "screenshot":
590
+ await page.screenshot({ path: act.screenshotPath });
591
+ return `screenshot saved: ${act.screenshotPath}`;
592
+ case "assertText": {
593
+ const text = await page.textContent(act.selector, {
594
+ timeout: timeoutMs,
595
+ });
596
+ if (text == null) {
597
+ throw new Error(`assertText: no element matches ${act.selector}`);
598
+ }
599
+ if (!text.includes(act.expected)) {
600
+ throw new Error(
601
+ `assertText FAILED: "${truncateForAudit(act.expected, 80)}" not found in ${act.selector} (got: ${truncateForAudit(text.trim(), 120)})`,
602
+ );
603
+ }
604
+ return `assertText passed: ${act.selector} contains "${truncateForAudit(act.expected, 80)}"`;
605
+ }
606
+ /* c8 ignore next 2 -- unreachable: normalizeBrowserActions validated type */
607
+ default:
608
+ throw new Error(`unsupported action type "${act.type}"`);
609
+ }
610
+ }
611
+
612
+ /**
613
+ * Execute an ordered list of explicit browser actions against the connected
614
+ * Chrome. Fail-fast on the first failed step unless `continueOnError`. Each
615
+ * step result is {ok, action, detail, durationMs}; the overall result adds
616
+ * final page url/title, `executed` count, optional `profileWarning` (attached
617
+ * browser is NOT the dedicated connector profile) and `auditError` (audit
618
+ * write failed — actions themselves are never aborted for audit IO).
619
+ */
620
+ export async function performActions(
621
+ actions,
622
+ {
623
+ port = DEFAULT_CDP_PORT,
624
+ cdpUrl = null,
625
+ tab = 0,
626
+ continueOnError = false,
627
+ sessionId = null,
628
+ actionTimeoutMs = DEFAULT_ACTION_TIMEOUT_MS,
629
+ deps = _deps,
630
+ } = {},
631
+ ) {
632
+ let normalized;
633
+ let resolvedPort;
634
+ try {
635
+ resolvedPort = resolveLoopbackCdpPort(cdpUrl, port);
636
+ normalized = normalizeBrowserActions(actions, { deps });
637
+ } catch (err) {
638
+ return { ok: false, error: err.message };
639
+ }
640
+ const stepTimeout = Math.min(
641
+ Math.max(Number(actionTimeoutMs) || DEFAULT_ACTION_TIMEOUT_MS, 1),
642
+ MAX_ACTION_TIMEOUT_MS,
643
+ );
644
+
645
+ let playwright;
646
+ try {
647
+ playwright = await deps.importPlaywright();
648
+ } catch {
649
+ return {
650
+ ok: false,
651
+ error: "playwright is not installed — npm install playwright",
652
+ };
653
+ }
654
+ let browser;
655
+ try {
656
+ browser = await playwright.chromium.connectOverCDP(
657
+ `http://127.0.0.1:${resolvedPort}`,
658
+ );
659
+ } catch (err) {
660
+ return {
661
+ ok: false,
662
+ error:
663
+ `cannot attach to CDP on port ${resolvedPort} (${err.message.split("\n")[0]}) — ` +
664
+ "launch a debuggable Chrome first (cc browse chrome launch)",
665
+ };
666
+ }
667
+ try {
668
+ // Same post-connect target-attach race as captureState: poll briefly.
669
+ let pages = [];
670
+ for (let i = 0; i < 20; i++) {
671
+ pages = browser.contexts().flatMap((c) => c.pages());
672
+ if (pages.length > 0) break;
673
+ await new Promise((r) => setTimeout(r, 150));
674
+ }
675
+ if (pages.length === 0) {
676
+ return { ok: false, error: "the connected Chrome has no open tabs" };
677
+ }
678
+ const index = Math.min(Math.max(0, Number(tab) || 0), pages.length - 1);
679
+ const page = pages[index];
680
+
681
+ const result = { ok: false, port: resolvedPort, tab: index, steps: [] };
682
+ if (!connectorProfileOwnsPort(resolvedPort, deps)) {
683
+ result.profileWarning =
684
+ "the attached Chrome does not appear to be running the dedicated connector profile " +
685
+ "(~/.chainlesschain/chrome-profile) — actions run in that browser's REAL session";
686
+ }
687
+
688
+ let auditError = null;
689
+ for (const act of normalized) {
690
+ const started = Date.now();
691
+ const step = { ok: false, action: act.type, detail: "", durationMs: 0 };
692
+ try {
693
+ step.detail = await runOneAction(page, act, stepTimeout);
694
+ step.ok = true;
695
+ if (act.screenshotPath) step.screenshotPath = act.screenshotPath;
696
+ } catch (err) {
697
+ step.detail = String(err && err.message ? err.message : err).split(
698
+ "\n",
699
+ )[0];
700
+ }
701
+ step.durationMs = Date.now() - started;
702
+ result.steps.push(step);
703
+ try {
704
+ const entry = {
705
+ ts: new Date(started).toISOString(),
706
+ action: act.type,
707
+ ok: step.ok,
708
+ durationMs: step.durationMs,
709
+ };
710
+ if (sessionId) entry.sessionId = String(sessionId);
711
+ if (act.selector) entry.selector = truncateForAudit(act.selector);
712
+ if (act.url) entry.url = truncateForAudit(act.url);
713
+ if (act.key) entry.key = truncateForAudit(act.key, 50);
714
+ appendActionAudit(entry, deps);
715
+ } catch (err) {
716
+ auditError = err.message;
717
+ }
718
+ if (!step.ok && !continueOnError) break;
719
+ }
720
+
721
+ result.executed = result.steps.length;
722
+ result.ok =
723
+ result.steps.length === normalized.length &&
724
+ result.steps.every((s) => s.ok);
725
+ if (auditError) result.auditError = auditError;
726
+ result.url = page.url();
727
+ result.title = await page.title().catch(() => "");
728
+ return result;
729
+ } finally {
730
+ // connectOverCDP close() disconnects the client; the browser lives on.
731
+ await browser.close().catch(() => {});
732
+ }
733
+ }
734
+
349
735
  export { _deps };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Git branch bundling + result reflow for cloud handoff. A `git bundle` of the
3
+ * current branch (base64) is the portable snapshot the private runner clones;
4
+ * the runner returns a patch/plan/pr/artifacts which we apply/persist locally.
5
+ *
6
+ * All git calls go through the injectable `run` so the flow is testable without
7
+ * a repo.
8
+ */
9
+
10
+ import { execFileSync } from "node:child_process";
11
+ import { writeFileSync, mkdtempSync } from "node:fs";
12
+ import { readFileSync, rmSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { tmpdir } from "node:os";
15
+
16
+ export const _deps = { execFileSync };
17
+
18
+ function git(args, cwd, deps = _deps) {
19
+ return deps
20
+ .execFileSync("git", args, {
21
+ cwd,
22
+ encoding: "utf-8",
23
+ maxBuffer: 64 * 1024 * 1024,
24
+ stdio: ["ignore", "pipe", "pipe"],
25
+ })
26
+ .toString();
27
+ }
28
+
29
+ /**
30
+ * Bundle the current branch into a base64 git bundle.
31
+ * @returns {{ bundle:string, branch:string, baseSha:string, bytes:number }}
32
+ */
33
+ export function bundleBranch(cwd = process.cwd(), deps = _deps) {
34
+ const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], cwd, deps).trim();
35
+ const baseSha = git(["rev-parse", "HEAD"], cwd, deps).trim();
36
+ const tmp = mkdtempSync(join(tmpdir(), "cc-cloud-bundle-"));
37
+ const bundleFile = join(tmp, "branch.bundle");
38
+ try {
39
+ // Bundle the full branch history so the runner can clone a working repo.
40
+ git(["bundle", "create", bundleFile, "HEAD", branch], cwd, deps);
41
+ const buf = readFileSync(bundleFile);
42
+ return {
43
+ bundle: buf.toString("base64"),
44
+ branch,
45
+ baseSha,
46
+ bytes: buf.length,
47
+ };
48
+ } finally {
49
+ try {
50
+ rmSync(tmp, { recursive: true, force: true });
51
+ } catch {
52
+ /* best-effort */
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Apply a unified-diff patch the runner produced onto the local worktree.
59
+ * Uses `git apply --3way` so it merges cleanly on top of local edits when
60
+ * possible; returns { applied, reason }.
61
+ */
62
+ export function applyResultPatch(patch, cwd = process.cwd(), deps = _deps) {
63
+ if (!patch || !String(patch).trim()) {
64
+ return { applied: false, reason: "no patch in result" };
65
+ }
66
+ const tmp = mkdtempSync(join(tmpdir(), "cc-cloud-patch-"));
67
+ const patchFile = join(tmp, "result.patch");
68
+ try {
69
+ writeFileSync(patchFile, patch, "utf-8");
70
+ git(["apply", "--3way", "--whitespace=nowarn", patchFile], cwd, deps);
71
+ return { applied: true, reason: "patch applied (3way)" };
72
+ } catch (err) {
73
+ return {
74
+ applied: false,
75
+ reason: `patch did not apply cleanly: ${err.message.split("\n")[0]}`,
76
+ };
77
+ } finally {
78
+ try {
79
+ rmSync(tmp, { recursive: true, force: true });
80
+ } catch {
81
+ /* best-effort */
82
+ }
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Persist non-patch result artifacts (plan text, artifact blobs) under the
88
+ * job's local dir and return where they landed. `writeFile` injected.
89
+ */
90
+ export function persistResultArtifacts(result, destDir, deps = {}) {
91
+ const writeFile = deps.writeFileSync || writeFileSync;
92
+ const written = [];
93
+ if (result?.plan) {
94
+ const p = join(destDir, "plan.md");
95
+ writeFile(p, String(result.plan), "utf-8");
96
+ written.push(p);
97
+ }
98
+ for (const art of Array.isArray(result?.artifacts) ? result.artifacts : []) {
99
+ if (!art?.name || art.content == null) continue;
100
+ const safe = String(art.name).replace(/[^\w.-]/g, "_");
101
+ const p = join(destDir, safe);
102
+ const content =
103
+ art.encoding === "base64"
104
+ ? Buffer.from(String(art.content), "base64")
105
+ : String(art.content);
106
+ writeFile(p, content);
107
+ written.push(p);
108
+ }
109
+ return written;
110
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Self-hosted cloud handoff (gap-2026-07-11 P1#7) — the private-runner version
3
+ * of Claude Code's --cloud/--teleport. `cc cloud run "<task>"` bundles the
4
+ * current git branch, POSTs it to a configured private runner, and records the
5
+ * job locally; `cc cloud status/attach` poll and reflow the runner's result
6
+ * (plan / patch / PR / artifacts) back into the local workspace.
7
+ *
8
+ * No Anthropic cloud dependency: the runner is any HTTP endpoint speaking the
9
+ * small protocol below (a reference runner can be a box running `cc agent`).
10
+ *
11
+ * POST {base}/v1/jobs {task, bundle(base64), branch, baseSha} → {jobId}
12
+ * GET {base}/v1/jobs/{id} → {status: queued|running|done|failed, summary?}
13
+ * GET {base}/v1/jobs/{id}/result → {patch?, plan?, pr?, artifacts?, log?}
14
+ *
15
+ * Auth: Bearer token (config cloud.token / CC_CLOUD_TOKEN). Transport (fetch)
16
+ * and the bundle builder are injected so this is unit-testable offline.
17
+ *
18
+ * Local job ledger: ~/.chainlesschain/cloud-jobs/<jobId>.json
19
+ */
20
+
21
+ import {
22
+ existsSync,
23
+ mkdirSync,
24
+ readdirSync,
25
+ readFileSync,
26
+ writeFileSync,
27
+ } from "node:fs";
28
+ import { join } from "node:path";
29
+ import { getHomeDir } from "../paths.js";
30
+
31
+ export const _deps = { fetch: (...a) => globalThis.fetch(...a) };
32
+
33
+ export function cloudJobsDir() {
34
+ const dir = join(getHomeDir(), "cloud-jobs");
35
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
36
+ return dir;
37
+ }
38
+
39
+ function jobPath(jobId) {
40
+ if (!/^[\w.-]+$/.test(String(jobId || ""))) {
41
+ throw new Error(`invalid job id: ${jobId}`);
42
+ }
43
+ return join(cloudJobsDir(), `${jobId}.json`);
44
+ }
45
+
46
+ export function saveJob(job) {
47
+ writeFileSync(jobPath(job.jobId), JSON.stringify(job, null, 2), "utf-8");
48
+ return job;
49
+ }
50
+
51
+ export function readJob(jobId) {
52
+ try {
53
+ return JSON.parse(readFileSync(jobPath(jobId), "utf-8"));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export function listJobs() {
60
+ const dir = cloudJobsDir();
61
+ if (!existsSync(dir)) return [];
62
+ return readdirSync(dir)
63
+ .filter((f) => f.endsWith(".json"))
64
+ .map((f) => {
65
+ try {
66
+ return JSON.parse(readFileSync(join(dir, f), "utf-8"));
67
+ } catch {
68
+ return null;
69
+ }
70
+ })
71
+ .filter(Boolean)
72
+ .sort((a, b) => (b.submittedAt || 0) - (a.submittedAt || 0));
73
+ }
74
+
75
+ export class CloudClient {
76
+ /**
77
+ * @param {object} opts { baseUrl, token, deps? }
78
+ */
79
+ constructor(opts = {}) {
80
+ this.baseUrl = String(opts.baseUrl || "").replace(/\/+$/, "");
81
+ if (!this.baseUrl) {
82
+ throw new Error(
83
+ "no cloud runner configured — set cloud.baseUrl / CC_CLOUD_URL",
84
+ );
85
+ }
86
+ this.token = opts.token || null;
87
+ this.deps = { ..._deps, ...(opts.deps || {}) };
88
+ }
89
+
90
+ _headers(extra = {}) {
91
+ return {
92
+ "Content-Type": "application/json",
93
+ ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}),
94
+ ...extra,
95
+ };
96
+ }
97
+
98
+ async _json(method, path, body) {
99
+ const res = await this.deps.fetch(`${this.baseUrl}${path}`, {
100
+ method,
101
+ headers: this._headers(),
102
+ ...(body ? { body: JSON.stringify(body) } : {}),
103
+ });
104
+ if (!res.ok) {
105
+ throw new Error(`cloud runner ${method} ${path} → HTTP ${res.status}`);
106
+ }
107
+ return res.json();
108
+ }
109
+
110
+ /** Submit a job. `bundle` is a base64 git bundle of the branch. */
111
+ async submit({ task, bundle, branch, baseSha }) {
112
+ const data = await this._json("POST", "/v1/jobs", {
113
+ task,
114
+ bundle,
115
+ branch,
116
+ baseSha,
117
+ });
118
+ if (!data?.jobId) throw new Error("runner did not return a jobId");
119
+ return data;
120
+ }
121
+
122
+ async status(jobId) {
123
+ return this._json("GET", `/v1/jobs/${encodeURIComponent(jobId)}`);
124
+ }
125
+
126
+ async result(jobId) {
127
+ return this._json("GET", `/v1/jobs/${encodeURIComponent(jobId)}/result`);
128
+ }
129
+ }
130
+
131
+ export const TERMINAL_CLOUD_STATUS = new Set(["done", "failed"]);
132
+
133
+ /**
134
+ * Poll a job until terminal or the deadline. `onTick(status)` reports progress.
135
+ * `sleep`/`now` injected for tests.
136
+ */
137
+ export async function pollUntilDone(client, jobId, opts = {}) {
138
+ const intervalMs = opts.intervalMs ?? 3000;
139
+ const timeoutMs = opts.timeoutMs ?? 30 * 60_000;
140
+ const sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
141
+ const now = opts.now || (() => Date.now());
142
+ const deadline = now() + timeoutMs;
143
+ for (;;) {
144
+ const status = await client.status(jobId);
145
+ opts.onTick?.(status);
146
+ if (TERMINAL_CLOUD_STATUS.has(status.status)) return status;
147
+ if (now() >= deadline) {
148
+ return { status: "timeout", summary: "poll deadline reached" };
149
+ }
150
+ await sleep(intervalMs);
151
+ }
152
+ }