@vellumai/assistant 0.6.2 → 0.6.4

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 (895) hide show
  1. package/ARCHITECTURE.md +273 -10
  2. package/Dockerfile +2 -3
  3. package/bun.lock +41 -49
  4. package/bunfig.toml +3 -0
  5. package/docs/architecture/memory.md +1 -1
  6. package/docs/backup-troubleshooting.md +52 -0
  7. package/docs/browser-use-architecture-phase2.md +174 -0
  8. package/docs/stt-provider-onboarding.md +120 -0
  9. package/knip.json +12 -2
  10. package/node_modules/@vellumai/ces-contracts/bun.lock +8 -6
  11. package/node_modules/@vellumai/ces-contracts/package.json +3 -3
  12. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
  13. package/openapi.yaml +1111 -86
  14. package/package.json +40 -42
  15. package/scripts/generate-openapi.ts +0 -2
  16. package/scripts/test.sh +73 -18
  17. package/src/__tests__/acp-session.test.ts +43 -0
  18. package/src/__tests__/agent-image-optimize.test.ts +28 -0
  19. package/src/__tests__/agent-loop.test.ts +123 -0
  20. package/src/__tests__/anthropic-provider.test.ts +263 -10
  21. package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
  22. package/src/__tests__/app-executors.test.ts +1 -0
  23. package/src/__tests__/app-source-watcher.test.ts +37 -11
  24. package/src/__tests__/approval-routes-http.test.ts +178 -1
  25. package/src/__tests__/auto-analysis-end-to-end.test.ts +550 -0
  26. package/src/__tests__/auto-analysis-prompt.test.ts +50 -0
  27. package/src/__tests__/browser-fill-credential.test.ts +240 -94
  28. package/src/__tests__/browser-manager.test.ts +40 -27
  29. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  30. package/src/__tests__/browser-skill-endstate.test.ts +31 -7
  31. package/src/__tests__/btw-routes.test.ts +7 -0
  32. package/src/__tests__/call-controller.test.ts +581 -20
  33. package/src/__tests__/catalog-files.test.ts +1000 -0
  34. package/src/__tests__/channel-approvals.test.ts +53 -0
  35. package/src/__tests__/channel-invite-transport.test.ts +2 -2
  36. package/src/__tests__/channel-readiness-routes.test.ts +16 -20
  37. package/src/__tests__/channel-readiness-service.test.ts +12 -7
  38. package/src/__tests__/checker.test.ts +157 -10
  39. package/src/__tests__/clawhub-files.test.ts +347 -0
  40. package/src/__tests__/commit-message-enrichment-service.test.ts +36 -19
  41. package/src/__tests__/config-analysis.test.ts +100 -0
  42. package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
  43. package/src/__tests__/config-schema-cmd.test.ts +2 -2
  44. package/src/__tests__/config-schema.test.ts +1248 -224
  45. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +339 -0
  46. package/src/__tests__/config-watcher.test.ts +43 -8
  47. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
  48. package/src/__tests__/contact-store-user-file.test.ts +512 -0
  49. package/src/__tests__/contacts-write.test.ts +197 -0
  50. package/src/__tests__/context-overflow-approval.test.ts +16 -1
  51. package/src/__tests__/context-window-manager.test.ts +88 -0
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  54. package/src/__tests__/conversation-agent-loop.test.ts +99 -3
  55. package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
  56. package/src/__tests__/conversation-attachments.test.ts +80 -4
  57. package/src/__tests__/conversation-confirmation-signals.test.ts +290 -0
  58. package/src/__tests__/conversation-error.test.ts +70 -0
  59. package/src/__tests__/conversation-fork-crud.test.ts +17 -0
  60. package/src/__tests__/conversation-history-web-search.test.ts +12 -4
  61. package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
  62. package/src/__tests__/conversation-init.benchmark.test.ts +6 -1
  63. package/src/__tests__/conversation-inject-context.test.ts +103 -0
  64. package/src/__tests__/conversation-launcher-skill-regression.test.ts +51 -0
  65. package/src/__tests__/conversation-list-source.test.ts +145 -0
  66. package/src/__tests__/conversation-pre-run-repair.test.ts +2 -0
  67. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  68. package/src/__tests__/conversation-queue.test.ts +946 -62
  69. package/src/__tests__/conversation-routes-disk-view.test.ts +275 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
  72. package/src/__tests__/conversation-runtime-assembly.test.ts +324 -46
  73. package/src/__tests__/conversation-skill-tools.test.ts +7 -4
  74. package/src/__tests__/conversation-slash-commands.test.ts +33 -0
  75. package/src/__tests__/conversation-slash-queue.test.ts +89 -18
  76. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  77. package/src/__tests__/conversation-starter-routes.test.ts +126 -0
  78. package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
  79. package/src/__tests__/conversation-store.test.ts +195 -0
  80. package/src/__tests__/conversation-tool-setup-batch-authorized.test.ts +226 -0
  81. package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
  82. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  83. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  84. package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
  85. package/src/__tests__/credential-health-service.test.ts +352 -0
  86. package/src/__tests__/credential-security-invariants.test.ts +6 -3
  87. package/src/__tests__/credential-vault-unit.test.ts +383 -7
  88. package/src/__tests__/credential-vault.test.ts +152 -13
  89. package/src/__tests__/credentials-cli.test.ts +42 -18
  90. package/src/__tests__/cross-provider-web-search.test.ts +146 -35
  91. package/src/__tests__/date-context.test.ts +4 -4
  92. package/src/__tests__/deterministic-verification-control-plane.test.ts +10 -1
  93. package/src/__tests__/device-id.test.ts +112 -0
  94. package/src/__tests__/docker-signing-key-bootstrap.test.ts +167 -4
  95. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -3
  96. package/src/__tests__/email-html-renderer.test.ts +71 -0
  97. package/src/__tests__/email-invite-adapter.test.ts +36 -32
  98. package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
  99. package/src/__tests__/emit-event-signal.test.ts +71 -0
  100. package/src/__tests__/extension-id-sync-guard.test.ts +222 -0
  101. package/src/__tests__/fixtures/mock-chrome-extension.ts +386 -0
  102. package/src/__tests__/gateway-only-enforcement.test.ts +206 -1
  103. package/src/__tests__/gateway-only-guard.test.ts +2 -0
  104. package/src/__tests__/gemini-provider.test.ts +66 -2
  105. package/src/__tests__/get-skill-detail-audit.test.ts +325 -0
  106. package/src/__tests__/gmail-archive-fallback.test.ts +193 -0
  107. package/src/__tests__/gmail-archive-gate.test.ts +246 -0
  108. package/src/__tests__/gmail-preferences.test.ts +117 -0
  109. package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
  110. package/src/__tests__/headless-browser-interactions.test.ts +738 -359
  111. package/src/__tests__/headless-browser-mode.test.ts +614 -0
  112. package/src/__tests__/headless-browser-navigate.test.ts +528 -49
  113. package/src/__tests__/headless-browser-read-tools.test.ts +274 -100
  114. package/src/__tests__/headless-browser-snapshot.test.ts +250 -77
  115. package/src/__tests__/heartbeat-service.test.ts +70 -17
  116. package/src/__tests__/home-state-routes.test.ts +162 -0
  117. package/src/__tests__/host-bash-proxy.test.ts +145 -1
  118. package/src/__tests__/host-browser-e2e-cloud.test.ts +596 -0
  119. package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
  120. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
  121. package/src/__tests__/host-browser-event-routes.test.ts +350 -0
  122. package/src/__tests__/host-browser-proxy.test.ts +444 -0
  123. package/src/__tests__/host-browser-routes.test.ts +198 -0
  124. package/src/__tests__/host-browser-ws-events-e2e.test.ts +423 -0
  125. package/src/__tests__/host-cu-proxy.test.ts +166 -1
  126. package/src/__tests__/host-file-proxy.test.ts +185 -1
  127. package/src/__tests__/host-file-read-tool.test.ts +52 -0
  128. package/src/__tests__/host-proxy-interface.test.ts +165 -0
  129. package/src/__tests__/host-shell-tool.test.ts +1 -11
  130. package/src/__tests__/http-user-message-parity.test.ts +1 -0
  131. package/src/__tests__/identity-intro-cache.test.ts +40 -10
  132. package/src/__tests__/init-feature-flag-overrides.test.ts +38 -112
  133. package/src/__tests__/integration-status.test.ts +6 -7
  134. package/src/__tests__/jobs-store-upsert-debounced.test.ts +141 -0
  135. package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
  136. package/src/__tests__/llm-context-normalization.test.ts +488 -0
  137. package/src/__tests__/llm-context-route-provider.test.ts +86 -5
  138. package/src/__tests__/llm-usage-store.test.ts +363 -0
  139. package/src/__tests__/mcp-client-auth.test.ts +40 -4
  140. package/src/__tests__/mcp-health-check.test.ts +10 -3
  141. package/src/__tests__/media-stream-output.test.ts +555 -0
  142. package/src/__tests__/media-stream-parser.test.ts +374 -0
  143. package/src/__tests__/media-stream-server-integration.test.ts +1234 -0
  144. package/src/__tests__/media-stream-stt-session.test.ts +588 -0
  145. package/src/__tests__/media-turn-detector.test.ts +440 -0
  146. package/src/__tests__/message-queue.test.ts +125 -0
  147. package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
  148. package/src/__tests__/migration-export-http.test.ts +67 -8
  149. package/src/__tests__/migration-export-streaming.test.ts +66 -0
  150. package/src/__tests__/migration-import-commit-http.test.ts +109 -7
  151. package/src/__tests__/migration-import-preflight-http.test.ts +6 -5
  152. package/src/__tests__/migration-validate-http.test.ts +3 -3
  153. package/src/__tests__/mock-gateway-ipc.ts +151 -0
  154. package/src/__tests__/model-intents.test.ts +2 -2
  155. package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
  156. package/src/__tests__/oauth-apps-routes.test.ts +18 -12
  157. package/src/__tests__/oauth-cli.test.ts +709 -60
  158. package/src/__tests__/oauth-connect-orchestrator.test.ts +118 -24
  159. package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
  160. package/src/__tests__/oauth-provider-serializer.test.ts +147 -10
  161. package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
  162. package/src/__tests__/oauth-providers-routes.test.ts +52 -14
  163. package/src/__tests__/oauth-store.test.ts +1465 -176
  164. package/src/__tests__/oauth2-gateway-transport.test.ts +460 -26
  165. package/src/__tests__/onboarding-template-contract.test.ts +81 -70
  166. package/src/__tests__/openai-provider.test.ts +178 -2
  167. package/src/__tests__/openai-responses-cutover-guard.test.ts +184 -0
  168. package/src/__tests__/openai-responses-provider.test.ts +1105 -0
  169. package/src/__tests__/openrouter-token-estimation.test.ts +100 -0
  170. package/src/__tests__/outlook-categories.test.ts +1 -1
  171. package/src/__tests__/outlook-client-automation.test.ts +1 -1
  172. package/src/__tests__/outlook-compose-tools.test.ts +1 -1
  173. package/src/__tests__/outlook-email-watcher.test.ts +1 -1
  174. package/src/__tests__/outlook-follow-up.test.ts +1 -1
  175. package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
  176. package/src/__tests__/outlook-trash.test.ts +1 -1
  177. package/src/__tests__/outlook-unsubscribe.test.ts +32 -3
  178. package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
  179. package/src/__tests__/permission-mode.test.ts +28 -56
  180. package/src/__tests__/persona-resolver.test.ts +251 -0
  181. package/src/__tests__/platform-bash-auto-approve.test.ts +4 -0
  182. package/src/__tests__/platform-callback-registration.test.ts +19 -0
  183. package/src/__tests__/platform.test.ts +92 -1
  184. package/src/__tests__/post-turn-tool-result-truncation.test.ts +343 -0
  185. package/src/__tests__/prechat-onboarding-contract.test.ts +267 -0
  186. package/src/__tests__/pricing.test.ts +174 -0
  187. package/src/__tests__/proxy-approval-callback.test.ts +18 -0
  188. package/src/__tests__/qdrant-manager.test.ts +29 -8
  189. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +194 -0
  190. package/src/__tests__/relationship-state-contract.test.ts +175 -0
  191. package/src/__tests__/relay-server.test.ts +423 -5
  192. package/src/__tests__/require-fresh-approval.test.ts +40 -1
  193. package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
  194. package/src/__tests__/schedule-routes.test.ts +162 -0
  195. package/src/__tests__/search-skills-unified.test.ts +118 -0
  196. package/src/__tests__/secret-detection-handler.test.ts +84 -0
  197. package/src/__tests__/secret-ingress-http.test.ts +1 -0
  198. package/src/__tests__/secret-scanner-executor.test.ts +4 -0
  199. package/src/__tests__/secure-keys.test.ts +107 -0
  200. package/src/__tests__/send-endpoint-busy.test.ts +8 -1
  201. package/src/__tests__/sequence-store.test.ts +1 -1
  202. package/src/__tests__/server-history-render.test.ts +49 -0
  203. package/src/__tests__/set-permission-mode.test.ts +13 -250
  204. package/src/__tests__/settings-routes.test.ts +201 -0
  205. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  206. package/src/__tests__/skills-file-content-endpoint.test.ts +801 -0
  207. package/src/__tests__/skills-files-catalog-fallback.test.ts +738 -0
  208. package/src/__tests__/skills.test.ts +5 -2
  209. package/src/__tests__/skillssh-files.test.ts +446 -0
  210. package/src/__tests__/slack-block-formatting.test.ts +110 -0
  211. package/src/__tests__/slack-channel-config.test.ts +576 -16
  212. package/src/__tests__/stt-catalog-parity.test.ts +282 -0
  213. package/src/__tests__/stt-stream-session.test.ts +535 -0
  214. package/src/__tests__/subagent-detail.test.ts +44 -2
  215. package/src/__tests__/subagent-disposal.test.ts +1 -0
  216. package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
  217. package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
  218. package/src/__tests__/subagent-manager-notify.test.ts +1 -0
  219. package/src/__tests__/subagent-notify-parent.test.ts +1 -0
  220. package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
  221. package/src/__tests__/subagent-tools.test.ts +1 -0
  222. package/src/__tests__/subagent-types.test.ts +1 -0
  223. package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
  224. package/src/__tests__/system-prompt.test.ts +184 -27
  225. package/src/__tests__/task-scheduler.test.ts +32 -6
  226. package/src/__tests__/telegram-config.test.ts +10 -13
  227. package/src/__tests__/telephony-stt-routing.test.ts +329 -0
  228. package/src/__tests__/terminal-tools.test.ts +25 -5
  229. package/src/__tests__/test-preload.ts +18 -0
  230. package/src/__tests__/test-support/browser-skill-harness.ts +4 -1
  231. package/src/__tests__/tool-approval-handler.test.ts +73 -0
  232. package/src/__tests__/tool-executor-lifecycle-events.test.ts +9 -5
  233. package/src/__tests__/tool-executor-shell-integration.test.ts +4 -0
  234. package/src/__tests__/tool-executor.test.ts +33 -24
  235. package/src/__tests__/tool-result-truncation.test.ts +36 -0
  236. package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
  237. package/src/__tests__/top-level-renderer.test.ts +73 -1
  238. package/src/__tests__/transport-hints-queue.test.ts +14 -29
  239. package/src/__tests__/trust-store.test.ts +7 -1
  240. package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -1
  241. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
  242. package/src/__tests__/tts-catalog-parity.test.ts +345 -0
  243. package/src/__tests__/twilio-routes-twiml.test.ts +512 -114
  244. package/src/__tests__/twilio-routes.test.ts +376 -0
  245. package/src/__tests__/unicode.test.ts +293 -0
  246. package/src/__tests__/update-bulletin-format.test.ts +59 -0
  247. package/src/__tests__/update-bulletin.test.ts +206 -5
  248. package/src/__tests__/usage-routes.test.ts +25 -4
  249. package/src/__tests__/user-reference.test.ts +46 -61
  250. package/src/__tests__/v2-consent-policy.test.ts +103 -0
  251. package/src/__tests__/verification-control-plane-policy.test.ts +4 -0
  252. package/src/__tests__/voice-config-update.test.ts +403 -0
  253. package/src/__tests__/voice-quality.test.ts +434 -19
  254. package/src/__tests__/workspace-heartbeat-service.test.ts +7 -0
  255. package/src/__tests__/workspace-migration-033-stt-service-explicit-config.test.ts +547 -0
  256. package/src/__tests__/workspace-migration-034-remove-calls-voice-transcription-provider.test.ts +596 -0
  257. package/src/__tests__/workspace-migration-drop-user-md.test.ts +368 -0
  258. package/src/__tests__/workspace-migration-meets.test.ts +244 -0
  259. package/src/__tests__/workspace-migration-seed-device-id.test.ts +14 -20
  260. package/src/__tests__/workspace-policy.test.ts +2 -0
  261. package/src/acp/client-handler.ts +30 -4
  262. package/src/agent/image-optimize.ts +24 -12
  263. package/src/agent/loop.ts +55 -9
  264. package/src/approvals/guardian-request-resolvers.ts +21 -15
  265. package/src/backup/__tests__/backup-key.test.ts +152 -0
  266. package/src/backup/__tests__/backup-worker.test.ts +767 -0
  267. package/src/backup/__tests__/list-snapshots.test.ts +87 -0
  268. package/src/backup/__tests__/local-writer.test.ts +218 -0
  269. package/src/backup/__tests__/offsite-writer.test.ts +641 -0
  270. package/src/backup/__tests__/paths.test.ts +300 -0
  271. package/src/backup/__tests__/restore.test.ts +498 -0
  272. package/src/backup/__tests__/snapshot-lock.test.ts +352 -0
  273. package/src/backup/__tests__/stream-crypt.test.ts +228 -0
  274. package/src/backup/backup-key.ts +137 -0
  275. package/src/backup/backup-worker.ts +459 -0
  276. package/src/backup/list-snapshots.ts +147 -0
  277. package/src/backup/local-writer.ts +133 -0
  278. package/src/backup/offsite-writer.ts +222 -0
  279. package/src/backup/paths.ts +226 -0
  280. package/src/backup/restore.ts +322 -0
  281. package/src/backup/snapshot-lock.ts +431 -0
  282. package/src/backup/stream-crypt.ts +263 -0
  283. package/src/browser-session/__tests__/manager.test.ts +297 -0
  284. package/src/browser-session/backends/cdp-inspect.ts +30 -0
  285. package/src/browser-session/backends/extension.ts +26 -0
  286. package/src/browser-session/backends/local.ts +24 -0
  287. package/src/browser-session/events.ts +164 -0
  288. package/src/browser-session/index.ts +27 -0
  289. package/src/browser-session/manager.ts +159 -0
  290. package/src/browser-session/types.ts +28 -0
  291. package/src/bundler/package-resolver.ts +4 -0
  292. package/src/calls/audio-store.ts +11 -5
  293. package/src/calls/call-controller.ts +226 -71
  294. package/src/calls/call-domain.ts +9 -0
  295. package/src/calls/call-speech-output.ts +190 -0
  296. package/src/calls/call-transport.ts +77 -0
  297. package/src/calls/media-stream-audio-transcode.ts +173 -0
  298. package/src/calls/media-stream-output.ts +660 -0
  299. package/src/calls/media-stream-parser.ts +300 -0
  300. package/src/calls/media-stream-protocol.ts +166 -0
  301. package/src/calls/media-stream-server.ts +592 -0
  302. package/src/calls/media-stream-stt-session.ts +460 -0
  303. package/src/calls/media-turn-detector.ts +230 -0
  304. package/src/calls/relay-server.ts +90 -75
  305. package/src/calls/resolve-call-tts-provider.ts +136 -0
  306. package/src/calls/telephony-stt-routing.ts +145 -0
  307. package/src/calls/tts-call-strategy.ts +161 -0
  308. package/src/calls/tts-text-sanitizer.ts +32 -16
  309. package/src/calls/twilio-routes.ts +281 -17
  310. package/src/calls/voice-quality.ts +78 -35
  311. package/src/calls/voice-session-bridge.ts +8 -1
  312. package/src/channels/__tests__/types.test.ts +134 -0
  313. package/src/channels/types.ts +69 -3
  314. package/src/cli/__tests__/run-assistant-command.ts +11 -1
  315. package/src/cli/commands/__tests__/backup.test.ts +1165 -0
  316. package/src/cli/commands/__tests__/domain-register.test.ts +234 -0
  317. package/src/cli/commands/__tests__/domain-status.test.ts +132 -0
  318. package/src/cli/commands/__tests__/email-attachment.test.ts +422 -0
  319. package/src/cli/commands/__tests__/email-download.test.ts +16 -1
  320. package/src/cli/commands/__tests__/email-list.test.ts +22 -4
  321. package/src/cli/commands/__tests__/email-register.test.ts +4 -4
  322. package/src/cli/commands/__tests__/email-send.test.ts +37 -4
  323. package/src/cli/commands/__tests__/email-status.test.ts +5 -1
  324. package/src/cli/commands/__tests__/email-unregister.test.ts +34 -5
  325. package/src/cli/commands/backup.ts +993 -0
  326. package/src/cli/commands/conversations.ts +77 -0
  327. package/src/cli/commands/credentials.ts +3 -4
  328. package/src/cli/commands/domain.ts +210 -0
  329. package/src/cli/commands/email.ts +273 -16
  330. package/src/cli/commands/mcp.ts +16 -4
  331. package/src/cli/commands/oauth/__tests__/connect.test.ts +56 -44
  332. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
  333. package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
  334. package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
  335. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +32 -33
  336. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +330 -0
  337. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +117 -12
  338. package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
  339. package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
  340. package/src/cli/commands/oauth/apps.ts +7 -4
  341. package/src/cli/commands/oauth/connect.ts +6 -3
  342. package/src/cli/commands/oauth/disconnect.ts +1 -1
  343. package/src/cli/commands/oauth/mode.ts +12 -3
  344. package/src/cli/commands/oauth/providers.ts +215 -36
  345. package/src/cli/commands/oauth/shared.ts +7 -6
  346. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +254 -0
  347. package/src/cli/commands/platform/__tests__/connect.test.ts +6 -0
  348. package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
  349. package/src/cli/commands/platform/__tests__/status.test.ts +6 -0
  350. package/src/cli/commands/platform/index.ts +107 -10
  351. package/src/cli/commands/usage.ts +10 -9
  352. package/src/cli/lib/daemon-credential-client.ts +4 -0
  353. package/src/cli/program.ts +30 -4
  354. package/src/config/__tests__/backup-schema.test.ts +134 -0
  355. package/src/config/assistant-feature-flags.ts +61 -62
  356. package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
  357. package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +141 -0
  358. package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
  359. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
  360. package/src/config/bundled-skills/browser/SKILL.md +30 -5
  361. package/src/config/bundled-skills/browser/TOOLS.json +123 -0
  362. package/src/config/bundled-skills/browser/tools/browser-attach.ts +12 -0
  363. package/src/config/bundled-skills/browser/tools/browser-detach.ts +12 -0
  364. package/src/config/bundled-skills/browser/tools/browser-status.ts +12 -0
  365. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +17 -0
  366. package/src/config/bundled-skills/contacts/SKILL.md +5 -2
  367. package/src/config/bundled-skills/document/SKILL.md +4 -0
  368. package/src/config/bundled-skills/gmail/SKILL.md +54 -8
  369. package/src/config/bundled-skills/gmail/TOOLS.json +33 -3
  370. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +116 -9
  371. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +138 -11
  372. package/src/config/bundled-skills/gmail/tools/gmail-preferences-tool.ts +59 -0
  373. package/src/config/bundled-skills/gmail/tools/gmail-preferences.ts +82 -0
  374. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +113 -17
  375. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +2 -2
  376. package/src/config/bundled-skills/media-processing/SKILL.md +3 -9
  377. package/src/config/bundled-skills/media-processing/TOOLS.json +1 -6
  378. package/src/config/bundled-skills/media-processing/__tests__/audio-transcribe.test.ts +125 -0
  379. package/src/config/bundled-skills/media-processing/__tests__/extract-keyframes.test.ts +181 -0
  380. package/src/config/bundled-skills/media-processing/__tests__/preprocess-audio.test.ts +141 -0
  381. package/src/config/bundled-skills/media-processing/services/audio-transcribe.ts +32 -87
  382. package/src/config/bundled-skills/media-processing/services/preprocess.ts +8 -4
  383. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +0 -10
  384. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  385. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +2 -2
  386. package/src/config/bundled-skills/outlook/SKILL.md +9 -2
  387. package/src/config/bundled-skills/outlook/tools/outlook-unsubscribe.ts +2 -2
  388. package/src/config/bundled-skills/phone-calls/SKILL.md +2 -2
  389. package/src/config/bundled-skills/phone-calls/references/CONFIG.md +27 -18
  390. package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +3 -3
  391. package/src/config/bundled-skills/settings/TOOLS.json +3 -3
  392. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +26 -22
  393. package/src/config/bundled-skills/slack/SKILL.md +1 -0
  394. package/src/config/bundled-skills/subagent/SKILL.md +21 -0
  395. package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
  396. package/src/config/bundled-skills/tasks/SKILL.md +5 -0
  397. package/src/config/bundled-skills/transcribe/SKILL.md +9 -14
  398. package/src/config/bundled-skills/transcribe/TOOLS.json +2 -7
  399. package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +256 -0
  400. package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +38 -188
  401. package/src/config/bundled-tool-registry.ts +8 -0
  402. package/src/config/env-registry.ts +38 -0
  403. package/src/config/env.ts +49 -4
  404. package/src/config/feature-flag-registry.json +85 -14
  405. package/src/config/loader.ts +82 -13
  406. package/src/config/sanitize-for-transfer.ts +47 -0
  407. package/src/config/schema.ts +81 -15
  408. package/src/config/schemas/__tests__/stt.test.ts +43 -0
  409. package/src/config/schemas/analysis.ts +51 -0
  410. package/src/config/schemas/backup.ts +72 -0
  411. package/src/config/schemas/calls.ts +1 -26
  412. package/src/config/schemas/elevenlabs.ts +0 -59
  413. package/src/config/schemas/filing.ts +47 -7
  414. package/src/config/schemas/heartbeat.ts +27 -5
  415. package/src/config/schemas/host-browser.ts +112 -0
  416. package/src/config/schemas/inference.ts +1 -1
  417. package/src/config/schemas/memory-lifecycle.ts +14 -2
  418. package/src/config/schemas/memory-retrieval.ts +103 -0
  419. package/src/config/schemas/security.ts +0 -6
  420. package/src/config/schemas/services.ts +52 -0
  421. package/src/config/schemas/stt.ts +59 -0
  422. package/src/config/schemas/tts.ts +230 -0
  423. package/src/config/schemas/updates.ts +14 -0
  424. package/src/config/skills.ts +4 -0
  425. package/src/config/types.ts +4 -1
  426. package/src/contacts/contact-store.ts +56 -11
  427. package/src/contacts/contacts-write.ts +38 -1
  428. package/src/context/post-turn-tool-result-truncation.ts +177 -0
  429. package/src/context/tool-result-truncation.ts +2 -1
  430. package/src/context/window-manager.ts +61 -10
  431. package/src/credential-execution/approval-bridge.ts +49 -15
  432. package/src/credential-execution/executable-discovery.ts +12 -2
  433. package/src/credential-execution/process-manager.ts +33 -2
  434. package/src/credential-health/credential-health-service.ts +366 -0
  435. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +324 -0
  436. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +497 -0
  437. package/src/daemon/__tests__/conversation-tool-setup.test.ts +195 -0
  438. package/src/daemon/__tests__/lifecycle-startup-ordering.test.ts +127 -0
  439. package/src/daemon/app-source-watcher.ts +35 -0
  440. package/src/daemon/config-watcher.ts +99 -5
  441. package/src/daemon/context-overflow-approval.ts +5 -0
  442. package/src/daemon/conversation-agent-loop-handlers.ts +23 -2
  443. package/src/daemon/conversation-agent-loop.ts +153 -42
  444. package/src/daemon/conversation-attachments.ts +40 -0
  445. package/src/daemon/conversation-error.ts +11 -0
  446. package/src/daemon/conversation-history.ts +40 -6
  447. package/src/daemon/conversation-launch.ts +220 -0
  448. package/src/daemon/conversation-lifecycle.ts +59 -9
  449. package/src/daemon/conversation-messaging.ts +37 -3
  450. package/src/daemon/conversation-notifiers.ts +5 -0
  451. package/src/daemon/conversation-process.ts +622 -13
  452. package/src/daemon/conversation-queue-manager.ts +24 -0
  453. package/src/daemon/conversation-runtime-assembly.ts +128 -36
  454. package/src/daemon/conversation-slash.ts +36 -0
  455. package/src/daemon/conversation-surfaces.ts +131 -40
  456. package/src/daemon/conversation-tool-setup.ts +99 -8
  457. package/src/daemon/conversation-usage.ts +7 -4
  458. package/src/daemon/conversation-workspace.ts +12 -0
  459. package/src/daemon/conversation.ts +292 -16
  460. package/src/daemon/date-context.ts +10 -10
  461. package/src/daemon/first-greeting.ts +3 -2
  462. package/src/daemon/handlers/config-slack-channel.ts +269 -94
  463. package/src/daemon/handlers/conversations.ts +13 -141
  464. package/src/daemon/handlers/shared.ts +80 -0
  465. package/src/daemon/handlers/skills.ts +483 -44
  466. package/src/daemon/host-bash-proxy.ts +48 -13
  467. package/src/daemon/host-browser-proxy.ts +192 -0
  468. package/src/daemon/host-cu-proxy.ts +36 -11
  469. package/src/daemon/host-file-proxy.ts +57 -9
  470. package/src/daemon/lifecycle.ts +179 -28
  471. package/src/daemon/message-protocol.ts +13 -0
  472. package/src/daemon/message-types/conversations.ts +89 -14
  473. package/src/daemon/message-types/home.ts +40 -0
  474. package/src/daemon/message-types/host-browser.ts +100 -0
  475. package/src/daemon/message-types/meet.ts +143 -0
  476. package/src/daemon/message-types/messages.ts +19 -5
  477. package/src/daemon/message-types/schedules.ts +34 -2
  478. package/src/daemon/message-types/skills.ts +26 -0
  479. package/src/daemon/message-types/subagents.ts +2 -0
  480. package/src/daemon/message-types/surfaces.ts +2 -0
  481. package/src/daemon/server.ts +439 -14
  482. package/src/daemon/shutdown-handlers.ts +32 -4
  483. package/src/daemon/shutdown-registry.ts +40 -0
  484. package/src/daemon/tool-side-effects.ts +15 -0
  485. package/src/daemon/transport-hints.ts +5 -24
  486. package/src/email/html-renderer.ts +76 -0
  487. package/src/heartbeat/heartbeat-service.ts +93 -7
  488. package/src/home/__tests__/assistant-feed-authoring.test.ts +156 -0
  489. package/src/home/__tests__/emit-feed-event.test.ts +169 -0
  490. package/src/home/__tests__/feed-scheduler.test.ts +194 -0
  491. package/src/home/__tests__/feed-types.test.ts +275 -0
  492. package/src/home/__tests__/feed-writer.test.ts +688 -0
  493. package/src/home/__tests__/phase5-exit-criteria.test.ts +212 -0
  494. package/src/home/__tests__/platform-gmail-digest.test.ts +222 -0
  495. package/src/home/__tests__/progress-formula.test.ts +213 -0
  496. package/src/home/__tests__/relationship-state-writer.test.ts +740 -0
  497. package/src/home/__tests__/rollup-producer.test.ts +398 -0
  498. package/src/home/assistant-feed-authoring.ts +124 -0
  499. package/src/home/emit-feed-event.ts +158 -0
  500. package/src/home/feed-scheduler.ts +247 -0
  501. package/src/home/feed-types.ts +181 -0
  502. package/src/home/feed-writer.ts +469 -0
  503. package/src/home/platform-gmail-digest.ts +163 -0
  504. package/src/home/progress-formula.ts +86 -0
  505. package/src/home/relationship-state-writer.ts +824 -0
  506. package/src/home/relationship-state.ts +143 -0
  507. package/src/home/rollup-producer.ts +384 -0
  508. package/src/hooks/runner.ts +7 -0
  509. package/src/inbound/platform-callback-registration.ts +30 -20
  510. package/src/inbound/public-ingress-urls.ts +12 -0
  511. package/src/instrument.ts +1 -1
  512. package/src/ipc/__tests__/cli-ipc.test.ts +200 -0
  513. package/src/ipc/cli-client.ts +151 -0
  514. package/src/ipc/cli-server.ts +234 -0
  515. package/src/ipc/gateway-client.ts +180 -0
  516. package/src/ipc/routes/index.ts +5 -0
  517. package/src/ipc/routes/wake-conversation.ts +19 -0
  518. package/src/mcp/client.ts +59 -24
  519. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +356 -0
  520. package/src/memory/__tests__/auto-analysis-guard.test.ts +57 -0
  521. package/src/memory/__tests__/conversation-analyze-job.test.ts +232 -0
  522. package/src/memory/__tests__/find-analysis-conversation.test.ts +196 -0
  523. package/src/memory/app-store.ts +31 -1
  524. package/src/memory/attachments-store.ts +70 -0
  525. package/src/memory/auto-analysis-enqueue.ts +127 -0
  526. package/src/memory/auto-analysis-guard.ts +27 -0
  527. package/src/memory/cleanup-schedule-state.ts +37 -0
  528. package/src/memory/conversation-analyze-job.ts +73 -0
  529. package/src/memory/conversation-crud.ts +122 -0
  530. package/src/memory/conversation-disk-view.ts +7 -0
  531. package/src/memory/conversation-group-migration.ts +34 -2
  532. package/src/memory/conversation-queries.ts +6 -5
  533. package/src/memory/conversation-starters-cadence.ts +76 -0
  534. package/src/memory/conversation-title-service.ts +5 -2
  535. package/src/memory/db-init.ts +18 -0
  536. package/src/memory/db-maintenance.ts +108 -0
  537. package/src/memory/db.ts +1 -0
  538. package/src/memory/embedding-backend.test.ts +75 -0
  539. package/src/memory/embedding-backend.ts +131 -5
  540. package/src/memory/embedding-gemini.test.ts +54 -0
  541. package/src/memory/embedding-gemini.ts +20 -9
  542. package/src/memory/embedding-local.ts +176 -17
  543. package/src/memory/graph/consolidation.ts +10 -23
  544. package/src/memory/graph/conversation-graph-memory.ts +15 -0
  545. package/src/memory/graph/extraction-job.ts +15 -0
  546. package/src/memory/graph/extraction.test.ts +23 -0
  547. package/src/memory/graph/extraction.ts +8 -0
  548. package/src/memory/graph/retriever.ts +67 -40
  549. package/src/memory/graph/scoring.test.ts +186 -0
  550. package/src/memory/graph/scoring.ts +31 -1
  551. package/src/memory/graph/store.test.ts +7 -3
  552. package/src/memory/graph/store.ts +47 -12
  553. package/src/memory/graph/tools.ts +1 -1
  554. package/src/memory/group-crud.ts +6 -1
  555. package/src/memory/indexer.ts +95 -16
  556. package/src/memory/job-handlers/cleanup.ts +11 -8
  557. package/src/memory/job-handlers/conversation-starters.ts +16 -10
  558. package/src/memory/jobs-store.ts +64 -4
  559. package/src/memory/jobs-worker.ts +22 -9
  560. package/src/memory/llm-usage-store.ts +137 -60
  561. package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
  562. package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
  563. package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
  564. package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
  565. package/src/memory/migrations/217-conversation-host-access.ts +40 -0
  566. package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
  567. package/src/memory/migrations/219-oauth-providers-token-exchange-body-format.ts +15 -0
  568. package/src/memory/migrations/220-normalize-user-file-by-principal.ts +190 -0
  569. package/src/memory/migrations/221-conversations-archived-at.ts +16 -0
  570. package/src/memory/migrations/index.ts +12 -0
  571. package/src/memory/migrations/registry.ts +16 -0
  572. package/src/memory/qdrant-manager.ts +43 -16
  573. package/src/memory/schema/conversations.ts +3 -0
  574. package/src/memory/schema/oauth.ts +21 -13
  575. package/src/memory/usage-buckets.ts +396 -0
  576. package/src/messaging/providers/gmail/client.ts +57 -6
  577. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +282 -0
  578. package/src/messaging/providers/slack/adapter.ts +143 -38
  579. package/src/messaging/providers/slack/client.ts +16 -0
  580. package/src/messaging/providers/slack/types.ts +4 -0
  581. package/src/notifications/decision-engine.ts +3 -3
  582. package/src/notifications/signal.ts +5 -0
  583. package/src/oauth/AGENTS.md +76 -0
  584. package/src/oauth/__tests__/identity-verifier.test.ts +25 -19
  585. package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
  586. package/src/oauth/byo-connection.test.ts +26 -9
  587. package/src/oauth/byo-connection.ts +10 -8
  588. package/src/oauth/connect-orchestrator.ts +25 -21
  589. package/src/oauth/connect-types.ts +3 -3
  590. package/src/oauth/connection-resolver.test.ts +17 -4
  591. package/src/oauth/connection-resolver.ts +22 -18
  592. package/src/oauth/connection.ts +3 -1
  593. package/src/oauth/manual-token-connection.ts +13 -13
  594. package/src/oauth/oauth-store.ts +223 -100
  595. package/src/oauth/platform-connection.test.ts +101 -3
  596. package/src/oauth/platform-connection.ts +56 -35
  597. package/src/oauth/provider-serializer.ts +31 -5
  598. package/src/oauth/revoke.ts +76 -0
  599. package/src/oauth/seed-providers.ts +133 -87
  600. package/src/oauth/token-persistence.ts +1 -1
  601. package/src/permissions/checker.ts +16 -6
  602. package/src/permissions/defaults.ts +49 -1
  603. package/src/permissions/permission-mode.ts +4 -11
  604. package/src/permissions/prompter.ts +13 -1
  605. package/src/permissions/trust-store.ts +3 -3
  606. package/src/permissions/v2-consent-policy.ts +87 -0
  607. package/src/permissions/workspace-policy.ts +3 -0
  608. package/src/platform/client.test.ts +10 -0
  609. package/src/platform/sync-identity.ts +129 -0
  610. package/src/prompts/persona-resolver.ts +126 -2
  611. package/src/prompts/system-prompt.ts +76 -38
  612. package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
  613. package/src/prompts/templates/BOOTSTRAP.md +59 -105
  614. package/src/prompts/templates/SOUL.md +3 -1
  615. package/src/prompts/templates/UPDATES.md +12 -0
  616. package/src/prompts/templates/channels/slack.md +20 -0
  617. package/src/prompts/update-bulletin-format.ts +26 -9
  618. package/src/prompts/update-bulletin.ts +34 -23
  619. package/src/prompts/user-reference.ts +20 -17
  620. package/src/providers/__tests__/provider-secret-catalog.test.ts +42 -0
  621. package/src/providers/anthropic/client.ts +157 -60
  622. package/src/providers/fireworks/client.ts +2 -2
  623. package/src/providers/gemini/client.ts +9 -1
  624. package/src/providers/model-catalog.ts +6 -0
  625. package/src/providers/model-intents.ts +4 -4
  626. package/src/providers/ollama/client.ts +2 -2
  627. package/src/providers/openai/chat-completions-provider.ts +474 -0
  628. package/src/providers/openai/client.ts +25 -440
  629. package/src/providers/openai/responses-provider.ts +502 -0
  630. package/src/providers/openrouter/client.ts +101 -4
  631. package/src/providers/provider-secret-catalog.ts +139 -0
  632. package/src/providers/registry.ts +2 -2
  633. package/src/providers/retry.ts +14 -3
  634. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +251 -0
  635. package/src/providers/speech-to-text/__tests__/resolve.test.ts +828 -0
  636. package/src/providers/speech-to-text/deepgram-realtime.test.ts +980 -0
  637. package/src/providers/speech-to-text/deepgram-realtime.ts +767 -0
  638. package/src/providers/speech-to-text/deepgram.test.ts +332 -0
  639. package/src/providers/speech-to-text/deepgram.ts +115 -0
  640. package/src/providers/speech-to-text/google-gemini-live-stream.test.ts +743 -0
  641. package/src/providers/speech-to-text/google-gemini-live-stream.ts +625 -0
  642. package/src/providers/speech-to-text/google-gemini.test.ts +226 -0
  643. package/src/providers/speech-to-text/google-gemini.ts +101 -0
  644. package/src/providers/speech-to-text/openai-whisper-stream.test.ts +564 -0
  645. package/src/providers/speech-to-text/openai-whisper-stream.ts +381 -0
  646. package/src/providers/speech-to-text/openai-whisper.test.ts +1 -37
  647. package/src/providers/speech-to-text/openai-whisper.ts +63 -33
  648. package/src/providers/speech-to-text/provider-catalog.ts +306 -0
  649. package/src/providers/speech-to-text/resolve.ts +386 -6
  650. package/src/providers/types.ts +10 -1
  651. package/src/runtime/AGENTS.md +65 -0
  652. package/src/runtime/__tests__/agent-wake.test.ts +831 -0
  653. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
  654. package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
  655. package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
  656. package/src/runtime/__tests__/runtime-mode.test.ts +62 -0
  657. package/src/runtime/__tests__/slack-block-formatting.test.ts +481 -0
  658. package/src/runtime/agent-wake.ts +512 -0
  659. package/src/runtime/assistant-event-hub.ts +2 -2
  660. package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
  661. package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
  662. package/src/runtime/auth/__tests__/route-policy.test.ts +48 -0
  663. package/src/runtime/auth/middleware.ts +98 -0
  664. package/src/runtime/auth/route-policy.ts +33 -9
  665. package/src/runtime/auth/token-service.ts +56 -1
  666. package/src/runtime/btw-sidechain.ts +2 -0
  667. package/src/runtime/capability-tokens.ts +414 -0
  668. package/src/runtime/channel-approvals.ts +18 -5
  669. package/src/runtime/channel-invite-transport.ts +1 -1
  670. package/src/runtime/channel-invite-transports/email.ts +14 -6
  671. package/src/runtime/channel-readiness-service.ts +12 -22
  672. package/src/runtime/chrome-extension-registry.ts +368 -0
  673. package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
  674. package/src/runtime/guardian-decision-types.ts +7 -0
  675. package/src/runtime/http-server.ts +815 -75
  676. package/src/runtime/http-types.ts +6 -2
  677. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
  678. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
  679. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +198 -0
  680. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +360 -0
  681. package/src/runtime/migrations/migration-transport.ts +7 -0
  682. package/src/runtime/migrations/migration-wizard.ts +23 -2
  683. package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
  684. package/src/runtime/migrations/vbundle-builder.ts +145 -38
  685. package/src/runtime/migrations/vbundle-import-analyzer.ts +96 -1
  686. package/src/runtime/migrations/vbundle-importer.ts +89 -5
  687. package/src/runtime/pending-interactions.ts +18 -13
  688. package/src/runtime/routes/__tests__/backup-routes.test.ts +967 -0
  689. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +507 -0
  690. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +208 -0
  691. package/src/runtime/routes/__tests__/stt-routes.test.ts +406 -0
  692. package/src/runtime/routes/__tests__/tts-routes.test.ts +474 -0
  693. package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +148 -17
  694. package/src/runtime/routes/app-management-routes.ts +12 -18
  695. package/src/runtime/routes/approval-routes.ts +90 -16
  696. package/src/runtime/routes/attachment-routes.test.ts +9 -3
  697. package/src/runtime/routes/attachment-routes.ts +216 -17
  698. package/src/runtime/routes/backup-routes.ts +519 -0
  699. package/src/runtime/routes/browser-extension-pair-routes.ts +556 -0
  700. package/src/runtime/routes/btw-routes.ts +8 -6
  701. package/src/runtime/routes/contact-routes.test.ts +298 -0
  702. package/src/runtime/routes/contact-routes.ts +132 -5
  703. package/src/runtime/routes/conversation-analysis-routes.ts +22 -141
  704. package/src/runtime/routes/conversation-management-routes.ts +223 -0
  705. package/src/runtime/routes/conversation-routes.ts +598 -103
  706. package/src/runtime/routes/conversation-starter-routes.ts +78 -16
  707. package/src/runtime/routes/filing-routes.ts +93 -0
  708. package/src/runtime/routes/guardian-action-routes.ts +24 -13
  709. package/src/runtime/routes/home-feed-routes.ts +334 -0
  710. package/src/runtime/routes/home-state-routes.ts +138 -0
  711. package/src/runtime/routes/host-browser-routes.ts +268 -0
  712. package/src/runtime/routes/host-file-routes.ts +9 -1
  713. package/src/runtime/routes/identity-intro-cache.ts +7 -3
  714. package/src/runtime/routes/identity-routes.ts +262 -33
  715. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +46 -39
  716. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +15 -15
  717. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +137 -0
  718. package/src/runtime/routes/integrations/slack/__tests__/share.test.ts +179 -0
  719. package/src/runtime/routes/integrations/slack/channel.ts +11 -3
  720. package/src/runtime/routes/integrations/slack/share.ts +45 -7
  721. package/src/runtime/routes/llm-context-normalization.ts +303 -0
  722. package/src/runtime/routes/log-export-routes.ts +42 -22
  723. package/src/runtime/routes/memory-item-routes.test.ts +3 -2
  724. package/src/runtime/routes/memory-item-routes.ts +1 -7
  725. package/src/runtime/routes/migration-routes.ts +122 -2
  726. package/src/runtime/routes/oauth-apps.ts +15 -17
  727. package/src/runtime/routes/oauth-providers.ts +4 -0
  728. package/src/runtime/routes/schedule-routes.ts +24 -11
  729. package/src/runtime/routes/settings-routes.ts +31 -102
  730. package/src/runtime/routes/skills-routes.ts +128 -9
  731. package/src/runtime/routes/stt-routes.ts +233 -0
  732. package/src/runtime/routes/subagents-routes.ts +14 -10
  733. package/src/runtime/routes/surface-action-routes.ts +41 -2
  734. package/src/runtime/routes/tts-routes.ts +108 -24
  735. package/src/runtime/routes/usage-routes.ts +38 -9
  736. package/src/runtime/routes/user-route-dispatcher.ts +50 -5
  737. package/src/runtime/routes/user-routes.ts +13 -1
  738. package/src/runtime/routes/work-items-routes.ts +8 -1
  739. package/src/runtime/routes/workspace-routes.test.ts +22 -0
  740. package/src/runtime/routes/workspace-routes.ts +8 -1
  741. package/src/runtime/routes/workspace-utils.ts +2 -0
  742. package/src/runtime/runtime-mode.ts +33 -0
  743. package/src/runtime/services/__tests__/analyze-conversation.test.ts +444 -0
  744. package/src/runtime/services/__tests__/analyze-deps-singleton.test.ts +67 -0
  745. package/src/runtime/services/__tests__/auto-analysis-prompt.test.ts +53 -0
  746. package/src/runtime/services/__tests__/manual-analysis-prompt.test.ts +41 -0
  747. package/src/runtime/services/analyze-conversation.ts +344 -0
  748. package/src/runtime/services/analyze-deps-singleton.ts +32 -0
  749. package/src/runtime/services/auto-analysis-prompt.ts +55 -0
  750. package/src/runtime/skill-route-registry.ts +49 -0
  751. package/src/runtime/slack-block-formatting.ts +437 -10
  752. package/src/schedule/scheduler.ts +57 -5
  753. package/src/security/ces-credential-client.ts +20 -0
  754. package/src/security/ces-rpc-credential-backend.ts +17 -0
  755. package/src/security/credential-backend.ts +5 -0
  756. package/src/security/oauth2.ts +68 -29
  757. package/src/security/secure-keys.ts +143 -27
  758. package/src/security/token-manager.ts +31 -10
  759. package/src/sequence/engine.ts +23 -0
  760. package/src/sequence/types.ts +1 -1
  761. package/src/skills/catalog-files.ts +554 -0
  762. package/src/skills/category-inference.ts +122 -0
  763. package/src/skills/clawhub-files.ts +213 -0
  764. package/src/skills/clawhub.ts +84 -23
  765. package/src/skills/skill-file-provider.ts +40 -0
  766. package/src/skills/skillssh-files.ts +395 -0
  767. package/src/skills/skillssh-registry.ts +4 -4
  768. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +392 -0
  769. package/src/stt/__tests__/types.test.ts +89 -0
  770. package/src/stt/daemon-batch-transcriber.ts +195 -0
  771. package/src/stt/stt-stream-session.ts +499 -0
  772. package/src/stt/types.ts +330 -0
  773. package/src/stt/wav-encoder.test.ts +373 -0
  774. package/src/stt/wav-encoder.ts +175 -0
  775. package/src/subagent/manager.ts +169 -40
  776. package/src/subagent/types.ts +19 -0
  777. package/src/tools/apps/executors.ts +11 -2
  778. package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
  779. package/src/tools/browser/__tests__/browser-mode.test.ts +119 -0
  780. package/src/tools/browser/__tests__/browser-status.test.ts +123 -0
  781. package/src/tools/browser/auth-detector.ts +43 -12
  782. package/src/tools/browser/browser-execution.ts +1787 -342
  783. package/src/tools/browser/browser-manager.ts +81 -12
  784. package/src/tools/browser/browser-mode-constants.ts +12 -0
  785. package/src/tools/browser/browser-mode.ts +92 -0
  786. package/src/tools/browser/browser-status-constants.ts +33 -0
  787. package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
  788. package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
  789. package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +1263 -0
  790. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +359 -0
  791. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +1993 -0
  792. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
  793. package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
  794. package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
  795. package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
  796. package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
  797. package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
  798. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +1007 -0
  799. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
  800. package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +744 -0
  801. package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
  802. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +868 -0
  803. package/src/tools/browser/cdp-client/errors.ts +49 -0
  804. package/src/tools/browser/cdp-client/extension-cdp-client.ts +148 -0
  805. package/src/tools/browser/cdp-client/factory.ts +914 -0
  806. package/src/tools/browser/cdp-client/index.ts +28 -0
  807. package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
  808. package/src/tools/browser/cdp-client/types.ts +120 -0
  809. package/src/tools/credentials/vault.ts +35 -6
  810. package/src/tools/filesystem/edit.ts +1 -1
  811. package/src/tools/filesystem/list.ts +1 -1
  812. package/src/tools/filesystem/read.ts +1 -1
  813. package/src/tools/filesystem/write.ts +2 -1
  814. package/src/tools/host-filesystem/edit.ts +1 -1
  815. package/src/tools/host-filesystem/read.ts +12 -15
  816. package/src/tools/host-filesystem/write.ts +1 -1
  817. package/src/tools/host-terminal/host-shell.ts +21 -16
  818. package/src/tools/network/web-fetch.ts +5 -2
  819. package/src/tools/network/web-search.ts +5 -2
  820. package/src/tools/permission-checker.ts +77 -82
  821. package/src/tools/registry.ts +0 -2
  822. package/src/tools/secret-detection-handler.ts +34 -0
  823. package/src/tools/shared/filesystem/image-read.ts +61 -40
  824. package/src/tools/shared/shell-output.ts +3 -1
  825. package/src/tools/side-effects.ts +2 -0
  826. package/src/tools/skills/sandbox-runner.ts +3 -2
  827. package/src/tools/subagent/spawn.ts +47 -3
  828. package/src/tools/subagent/status.ts +2 -0
  829. package/src/tools/system/register.ts +2 -16
  830. package/src/tools/terminal/safe-env.ts +15 -0
  831. package/src/tools/terminal/shell.ts +36 -20
  832. package/src/tools/tool-approval-handler.ts +48 -2
  833. package/src/tools/tool-manifest.ts +21 -0
  834. package/src/tools/types.ts +19 -0
  835. package/src/tools/ui-surface/definitions.ts +6 -1
  836. package/src/tts/__tests__/provider-adapters.test.ts +834 -0
  837. package/src/tts/__tests__/provider-catalog-consistency.test.ts +196 -0
  838. package/src/tts/__tests__/provider-catalog.test.ts +183 -0
  839. package/src/tts/__tests__/provider-registry.test.ts +90 -0
  840. package/src/tts/provider-catalog.ts +201 -0
  841. package/src/tts/provider-registry.ts +73 -0
  842. package/src/tts/providers/deepgram-provider.ts +219 -0
  843. package/src/tts/providers/elevenlabs-provider.ts +211 -0
  844. package/src/tts/providers/fish-audio-provider.ts +183 -0
  845. package/src/tts/providers/index.ts +42 -0
  846. package/src/tts/providers/register-builtins.ts +130 -0
  847. package/src/tts/synthesize-text.ts +110 -0
  848. package/src/tts/tts-config-resolver.ts +78 -0
  849. package/src/tts/types.ts +153 -0
  850. package/src/types/onboarding-context.ts +7 -0
  851. package/src/util/abort-reasons.ts +58 -0
  852. package/src/util/device-id.ts +32 -16
  853. package/src/util/errors.ts +9 -1
  854. package/src/util/platform.ts +63 -24
  855. package/src/util/pricing.ts +66 -3
  856. package/src/util/spawn.ts +1 -1
  857. package/src/util/truncate.ts +4 -2
  858. package/src/util/unicode.ts +201 -0
  859. package/src/version.ts +19 -24
  860. package/src/watcher/engine.ts +23 -0
  861. package/src/watcher/watcher-store.ts +31 -0
  862. package/src/workspace/migrations/003-seed-device-id.ts +9 -3
  863. package/src/workspace/migrations/017-seed-persona-dirs.ts +68 -4
  864. package/src/workspace/migrations/029-seed-pkb.ts +1 -1
  865. package/src/workspace/migrations/031-drop-user-md.ts +317 -0
  866. package/src/workspace/migrations/031-llm-log-retention-zero-to-null.ts +73 -0
  867. package/src/workspace/migrations/032-tts-provider-unification.ts +227 -0
  868. package/src/workspace/migrations/033-stt-service-explicit-config.ts +122 -0
  869. package/src/workspace/migrations/034-remove-calls-voice-transcription-provider.ts +215 -0
  870. package/src/workspace/migrations/035-seed-slack-channel-persona.ts +50 -0
  871. package/src/workspace/migrations/036-update-pkb-index-bar.ts +37 -0
  872. package/src/workspace/migrations/037-create-meets-dir.ts +61 -0
  873. package/src/workspace/migrations/registry.ts +16 -0
  874. package/src/workspace/top-level-renderer.ts +31 -1
  875. package/src/workspace/turn-commit.ts +31 -0
  876. package/src/__tests__/chrome-cdp.test.ts +0 -419
  877. package/src/__tests__/email-cli.test.ts +0 -297
  878. package/src/__tests__/email-service-config-fallback.test.ts +0 -102
  879. package/src/__tests__/permission-mode-sse.test.ts +0 -418
  880. package/src/__tests__/permission-mode-store.test.ts +0 -277
  881. package/src/browser-extension-relay/protocol.ts +0 -63
  882. package/src/browser-extension-relay/server.ts +0 -203
  883. package/src/cli/commands/browser-relay.ts +0 -536
  884. package/src/config/schemas/sandbox.ts +0 -14
  885. package/src/email/guardrails.ts +0 -221
  886. package/src/email/provider.ts +0 -117
  887. package/src/email/providers/agentmail.ts +0 -361
  888. package/src/email/providers/index.ts +0 -65
  889. package/src/email/service.ts +0 -384
  890. package/src/email/types.ts +0 -126
  891. package/src/permissions/permission-mode-store.ts +0 -180
  892. package/src/prompts/templates/USER.md +0 -13
  893. package/src/providers/speech-to-text/types.ts +0 -17
  894. package/src/tools/browser/chrome-cdp.ts +0 -239
  895. package/src/tools/system/set-permission-mode.ts +0 -103
@@ -0,0 +1,1263 @@
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Silence the logger from cdp-inspect-client.
4
+ mock.module("../../../../util/logger.js", () => ({
5
+ getLogger: () => ({
6
+ info: () => {},
7
+ debug: () => {},
8
+ warn: () => {},
9
+ error: () => {},
10
+ }),
11
+ }));
12
+
13
+ // Import under test AFTER mock.module calls so that the module's
14
+ // top-level logger import resolves to our fake.
15
+ const { CdpInspectClient, createCdpInspectClient } =
16
+ await import("../cdp-inspect-client.js");
17
+ const { CdpError } = await import("../errors.js");
18
+ const { CdpWsTransportError } = await import("../cdp-inspect/ws-transport.js");
19
+ const { DevToolsDiscoveryError } = await import("../cdp-inspect/discovery.js");
20
+
21
+ type CdpInspectClientInstance = InstanceType<typeof CdpInspectClient>;
22
+
23
+ /**
24
+ * Minimal fake CdpWsTransport used by the test harness below. The
25
+ * handler is per-send so individual tests can model success, CDP
26
+ * errors, transport errors, and abort behavior on specific methods.
27
+ */
28
+ interface FakeTransportOptions {
29
+ onSend?: (
30
+ method: string,
31
+ params: Record<string, unknown> | undefined,
32
+ opts: { sessionId?: string; signal?: AbortSignal },
33
+ ) => unknown | Promise<unknown>;
34
+ trackSends?: Array<{
35
+ method: string;
36
+ params?: Record<string, unknown>;
37
+ sessionId?: string;
38
+ }>;
39
+ trackDisposeCount?: { count: number };
40
+ }
41
+
42
+ function createFakeTransport(options: FakeTransportOptions) {
43
+ const transport = {
44
+ send: async <T = unknown>(
45
+ method: string,
46
+ params?: Record<string, unknown>,
47
+ opts?: { sessionId?: string; signal?: AbortSignal },
48
+ ): Promise<T> => {
49
+ options.trackSends?.push({
50
+ method,
51
+ params,
52
+ sessionId: opts?.sessionId,
53
+ });
54
+ if (options.onSend) {
55
+ const result = await options.onSend(method, params, opts ?? {});
56
+ return result as T;
57
+ }
58
+ return undefined as T;
59
+ },
60
+ addEventListener: () => () => {},
61
+ dispose: () => {
62
+ if (options.trackDisposeCount) {
63
+ options.trackDisposeCount.count += 1;
64
+ }
65
+ },
66
+ };
67
+ return transport;
68
+ }
69
+
70
+ /**
71
+ * Build a client wired to mocked discovery + transport helpers. The
72
+ * caller supplies handlers for the moving pieces; everything else
73
+ * defaults to a happy-path attach.
74
+ */
75
+ interface HarnessOptions {
76
+ probeImpl?: (opts: unknown) => Promise<{
77
+ browser: string;
78
+ protocolVersion: string;
79
+ webSocketDebuggerUrl: string;
80
+ }>;
81
+ listImpl?: (opts: unknown) => Promise<
82
+ Array<{
83
+ id: string;
84
+ type: string;
85
+ title: string;
86
+ url: string;
87
+ webSocketDebuggerUrl: string;
88
+ }>
89
+ >;
90
+ connectImpl?: (
91
+ url: string,
92
+ opts?: { connectTimeoutMs?: number },
93
+ ) => Promise<ReturnType<typeof createFakeTransport>>;
94
+ transportOnSend?: FakeTransportOptions["onSend"];
95
+ conversationId?: string;
96
+ }
97
+
98
+ interface Harness {
99
+ client: CdpInspectClientInstance;
100
+ sends: Array<{
101
+ method: string;
102
+ params?: Record<string, unknown>;
103
+ sessionId?: string;
104
+ }>;
105
+ disposeCount: { count: number };
106
+ probeCalls: number;
107
+ listCalls: number;
108
+ connectCalls: number;
109
+ attachCallCount: () => number;
110
+ }
111
+
112
+ function createHarness(opts: HarnessOptions = {}): Harness {
113
+ const sends: Array<{
114
+ method: string;
115
+ params?: Record<string, unknown>;
116
+ sessionId?: string;
117
+ }> = [];
118
+ const disposeCount = { count: 0 };
119
+ let probeCalls = 0;
120
+ let listCalls = 0;
121
+ let connectCalls = 0;
122
+
123
+ // Track Target.attachToTarget specifically so tests can assert
124
+ // how many attach attempts the client has made. The counter is
125
+ // bumped ONLY in the default happy-path branch so tests that
126
+ // install a custom `transportOnSend` (and therefore model their
127
+ // own attach semantics) can't accidentally double-count.
128
+ const attachSends: Array<unknown> = [];
129
+
130
+ const defaultOnSend: FakeTransportOptions["onSend"] = (method) => {
131
+ if (method === "Target.attachToTarget") {
132
+ attachSends.push(method);
133
+ return { sessionId: "fake-session-id" };
134
+ }
135
+ return { ok: true };
136
+ };
137
+
138
+ const transportOnSend: FakeTransportOptions["onSend"] = async (
139
+ method,
140
+ params,
141
+ o,
142
+ ) => {
143
+ if (opts.transportOnSend) {
144
+ return opts.transportOnSend(method, params, o);
145
+ }
146
+ return defaultOnSend!(method, params, o);
147
+ };
148
+
149
+ const client = createCdpInspectClient(opts.conversationId ?? "conv-1", {
150
+ host: "127.0.0.1",
151
+ port: 9222,
152
+ discoveryTimeoutMs: 100,
153
+ wsConnectTimeoutMs: 100,
154
+ helpers: {
155
+ probeDevToolsJsonVersion: async (probeOpts: unknown) => {
156
+ probeCalls += 1;
157
+ if (opts.probeImpl) return opts.probeImpl(probeOpts);
158
+ return {
159
+ browser: "Chrome/125.0.0.0",
160
+ protocolVersion: "1.3",
161
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
162
+ };
163
+ },
164
+ listDevToolsTargets: async (listOpts: unknown) => {
165
+ listCalls += 1;
166
+ if (opts.listImpl) return opts.listImpl(listOpts);
167
+ return [
168
+ {
169
+ id: "target-1",
170
+ type: "page",
171
+ title: "Example",
172
+ url: "https://example.com/",
173
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
174
+ },
175
+ ];
176
+ },
177
+ // pickDefaultTarget uses the real implementation — it's pure.
178
+ connectCdpWsTransport: async (
179
+ url: string,
180
+ connectOpts?: { connectTimeoutMs?: number },
181
+ ) => {
182
+ connectCalls += 1;
183
+ if (opts.connectImpl) return opts.connectImpl(url, connectOpts);
184
+ return createFakeTransport({
185
+ onSend: transportOnSend,
186
+ trackSends: sends,
187
+ trackDisposeCount: disposeCount,
188
+ });
189
+ },
190
+ },
191
+ });
192
+
193
+ return {
194
+ client,
195
+ sends,
196
+ disposeCount,
197
+ get probeCalls() {
198
+ return probeCalls;
199
+ },
200
+ get listCalls() {
201
+ return listCalls;
202
+ },
203
+ get connectCalls() {
204
+ return connectCalls;
205
+ },
206
+ attachCallCount: () => attachSends.length,
207
+ };
208
+ }
209
+
210
+ describe("CdpInspectClient", () => {
211
+ beforeEach(() => {
212
+ // no-op — each test gets its own harness
213
+ });
214
+
215
+ test("kind is 'cdp-inspect' and exposes conversationId", () => {
216
+ const { client } = createHarness({ conversationId: "conv-kind" });
217
+ expect(client).toBeInstanceOf(CdpInspectClient);
218
+ expect(client.kind).toBe("cdp-inspect");
219
+ expect(client.conversationId).toBe("conv-kind");
220
+ });
221
+
222
+ test("send() probes version, lists targets, attaches, and forwards the call", async () => {
223
+ const harness = createHarness({
224
+ transportOnSend: (method) => {
225
+ if (method === "Target.attachToTarget") {
226
+ return { sessionId: "session-abc" };
227
+ }
228
+ if (method === "Browser.getVersion") {
229
+ return { product: "HeadlessChrome/125.0.0.0" };
230
+ }
231
+ return undefined;
232
+ },
233
+ });
234
+ const result = await harness.client.send<{ product: string }>(
235
+ "Browser.getVersion",
236
+ );
237
+ expect(result).toEqual({ product: "HeadlessChrome/125.0.0.0" });
238
+ expect(harness.probeCalls).toBe(1);
239
+ expect(harness.listCalls).toBe(1);
240
+ expect(harness.connectCalls).toBe(1);
241
+ // One attach + one forwarded Browser.getVersion.
242
+ expect(harness.sends).toEqual([
243
+ {
244
+ method: "Target.attachToTarget",
245
+ params: { targetId: "target-1", flatten: true },
246
+ sessionId: undefined,
247
+ },
248
+ {
249
+ method: "Browser.getVersion",
250
+ params: undefined,
251
+ sessionId: "session-abc",
252
+ },
253
+ ]);
254
+ });
255
+
256
+ test("multiple send() calls share a single attach", async () => {
257
+ const harness = createHarness();
258
+ await harness.client.send("Runtime.enable");
259
+ await harness.client.send("Page.enable");
260
+ await harness.client.send("DOM.enable");
261
+ expect(harness.probeCalls).toBe(1);
262
+ expect(harness.listCalls).toBe(1);
263
+ expect(harness.connectCalls).toBe(1);
264
+ expect(harness.attachCallCount()).toBe(1);
265
+ expect(harness.sends.length).toBe(4); // 1 attach + 3 forwarded
266
+ });
267
+
268
+ test("concurrent send() calls share a single in-flight attach", async () => {
269
+ const harness = createHarness();
270
+ await Promise.all([
271
+ harness.client.send("Runtime.enable"),
272
+ harness.client.send("Page.enable"),
273
+ harness.client.send("DOM.enable"),
274
+ ]);
275
+ expect(harness.probeCalls).toBe(1);
276
+ expect(harness.listCalls).toBe(1);
277
+ expect(harness.connectCalls).toBe(1);
278
+ expect(harness.attachCallCount()).toBe(1);
279
+ });
280
+
281
+ test("send() retries ensureSession after an initial attach failure", async () => {
282
+ // First probe call rejects (simulating e.g. Chrome not yet listening).
283
+ // Second probe call succeeds. Because the cached sessionPromise must
284
+ // be cleared on rejection, the second send() performs a full retry.
285
+ let probeCount = 0;
286
+ const harness = createHarness({
287
+ probeImpl: async () => {
288
+ probeCount += 1;
289
+ if (probeCount === 1) {
290
+ throw new Error("connect ECONNREFUSED");
291
+ }
292
+ return {
293
+ browser: "Chrome/125.0.0.0",
294
+ protocolVersion: "1.3",
295
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
296
+ };
297
+ },
298
+ });
299
+
300
+ let firstErr: unknown;
301
+ try {
302
+ await harness.client.send("Browser.getVersion");
303
+ } catch (err) {
304
+ firstErr = err;
305
+ }
306
+ expect(firstErr).toBeInstanceOf(CdpError);
307
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe(
308
+ "transport_error",
309
+ );
310
+ expect(probeCount).toBe(1);
311
+ expect(harness.connectCalls).toBe(0);
312
+
313
+ // Second call — cached promise was cleared, so probe + list +
314
+ // connect + attach all run again, then the forwarded call
315
+ // resolves normally. listCalls is only 1 because the first
316
+ // attempt threw inside probeDevToolsJsonVersion before it ever
317
+ // reached listDevToolsTargets.
318
+ const result = await harness.client.send<{ ok: boolean }>(
319
+ "Browser.getVersion",
320
+ );
321
+ expect(result).toEqual({ ok: true });
322
+ expect(probeCount).toBe(2);
323
+ expect(harness.listCalls).toBe(1);
324
+ expect(harness.connectCalls).toBe(1);
325
+ expect(harness.attachCallCount()).toBe(1);
326
+ });
327
+
328
+ test("send() maps CDP protocol errors from attach to CdpError 'cdp_error'", async () => {
329
+ const harness = createHarness({
330
+ transportOnSend: async (method) => {
331
+ if (method === "Target.attachToTarget") {
332
+ throw new CdpWsTransportError(
333
+ "cdp_error",
334
+ "No target with given id found",
335
+ {
336
+ cdpMethod: "Target.attachToTarget",
337
+ cdpCode: -32602,
338
+ cdpMessage: "No target with given id found",
339
+ },
340
+ );
341
+ }
342
+ return undefined;
343
+ },
344
+ });
345
+
346
+ let caught: unknown;
347
+ try {
348
+ await harness.client.send("Browser.getVersion");
349
+ } catch (err) {
350
+ caught = err;
351
+ }
352
+ expect(caught).toBeInstanceOf(CdpError);
353
+ const cdpErr = caught as InstanceType<typeof CdpError>;
354
+ expect(cdpErr.code).toBe("cdp_error");
355
+ expect(cdpErr.message).toBe("No target with given id found");
356
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
357
+ expect(cdpErr.underlying).toBeInstanceOf(CdpWsTransportError);
358
+ });
359
+
360
+ test("send() maps transport failures during attach to CdpError 'transport_error'", async () => {
361
+ const harness = createHarness({
362
+ connectImpl: async () => {
363
+ throw new CdpWsTransportError(
364
+ "transport_error",
365
+ "websocket closed before open",
366
+ );
367
+ },
368
+ });
369
+ let caught: unknown;
370
+ try {
371
+ await harness.client.send("Browser.getVersion");
372
+ } catch (err) {
373
+ caught = err;
374
+ }
375
+ expect(caught).toBeInstanceOf(CdpError);
376
+ const cdpErr = caught as InstanceType<typeof CdpError>;
377
+ expect(cdpErr.code).toBe("transport_error");
378
+ expect(cdpErr.message).toBe("websocket closed before open");
379
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
380
+ });
381
+
382
+ test("send() with an already-aborted signal throws 'aborted' without touching the transport", async () => {
383
+ const harness = createHarness();
384
+ const controller = new AbortController();
385
+ controller.abort();
386
+ let caught: unknown;
387
+ try {
388
+ await harness.client.send(
389
+ "Browser.getVersion",
390
+ undefined,
391
+ controller.signal,
392
+ );
393
+ } catch (err) {
394
+ caught = err;
395
+ }
396
+ expect(caught).toBeInstanceOf(CdpError);
397
+ const cdpErr = caught as InstanceType<typeof CdpError>;
398
+ expect(cdpErr.code).toBe("aborted");
399
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
400
+ // Nothing ran — no discovery, no connect, no transport sends.
401
+ expect(harness.probeCalls).toBe(0);
402
+ expect(harness.listCalls).toBe(0);
403
+ expect(harness.connectCalls).toBe(0);
404
+ expect(harness.sends.length).toBe(0);
405
+ });
406
+
407
+ test("send() classifies as 'aborted' when the signal fires during attach", async () => {
408
+ const controller = new AbortController();
409
+ const harness = createHarness({
410
+ probeImpl: async () => {
411
+ // Simulate caller aborting while discovery is in flight.
412
+ // Discovery itself throws a generic error (as real fetch
413
+ // would), and the abort flag is flipped — we expect the
414
+ // resulting CdpError to carry code "aborted".
415
+ controller.abort();
416
+ throw new Error("aborted during fetch");
417
+ },
418
+ });
419
+ let caught: unknown;
420
+ try {
421
+ await harness.client.send(
422
+ "Browser.getVersion",
423
+ undefined,
424
+ controller.signal,
425
+ );
426
+ } catch (err) {
427
+ caught = err;
428
+ }
429
+ expect(caught).toBeInstanceOf(CdpError);
430
+ const cdpErr = caught as InstanceType<typeof CdpError>;
431
+ expect(cdpErr.code).toBe("aborted");
432
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
433
+ });
434
+
435
+ test("send() classifies as 'aborted' when the signal fires during the forwarded call", async () => {
436
+ const controller = new AbortController();
437
+ const harness = createHarness({
438
+ transportOnSend: async (method) => {
439
+ if (method === "Target.attachToTarget") {
440
+ return { sessionId: "session-abc" };
441
+ }
442
+ // Simulate the transport throwing an abort error after
443
+ // the caller aborts mid-call.
444
+ controller.abort();
445
+ throw new CdpWsTransportError("aborted", "aborted during send", {
446
+ cdpMethod: method,
447
+ });
448
+ },
449
+ });
450
+ let caught: unknown;
451
+ try {
452
+ await harness.client.send(
453
+ "Page.navigate",
454
+ { url: "about:blank" },
455
+ controller.signal,
456
+ );
457
+ } catch (err) {
458
+ caught = err;
459
+ }
460
+ expect(caught).toBeInstanceOf(CdpError);
461
+ const cdpErr = caught as InstanceType<typeof CdpError>;
462
+ expect(cdpErr.code).toBe("aborted");
463
+ expect(cdpErr.cdpMethod).toBe("Page.navigate");
464
+ });
465
+
466
+ test("send() maps forwarded CDP protocol errors to 'cdp_error'", async () => {
467
+ const harness = createHarness({
468
+ transportOnSend: async (method) => {
469
+ if (method === "Target.attachToTarget") {
470
+ return { sessionId: "session-abc" };
471
+ }
472
+ throw new CdpWsTransportError("cdp_error", "invalid expression", {
473
+ cdpMethod: method,
474
+ cdpCode: -32000,
475
+ cdpMessage: "invalid expression",
476
+ });
477
+ },
478
+ });
479
+ let caught: unknown;
480
+ try {
481
+ await harness.client.send("Runtime.evaluate", { expression: "??" });
482
+ } catch (err) {
483
+ caught = err;
484
+ }
485
+ expect(caught).toBeInstanceOf(CdpError);
486
+ const cdpErr = caught as InstanceType<typeof CdpError>;
487
+ expect(cdpErr.code).toBe("cdp_error");
488
+ expect(cdpErr.message).toBe("invalid expression");
489
+ expect(cdpErr.cdpMethod).toBe("Runtime.evaluate");
490
+ expect(cdpErr.cdpParams).toEqual({ expression: "??" });
491
+ });
492
+
493
+ test("dispose() is idempotent and tears down the underlying transport", async () => {
494
+ const harness = createHarness();
495
+ await harness.client.send("Browser.getVersion");
496
+ harness.client.dispose();
497
+ // dispose schedules transport.dispose on the resolved attach
498
+ // promise's then() — flush microtasks.
499
+ await new Promise((resolve) => setTimeout(resolve, 0));
500
+ expect(harness.disposeCount.count).toBe(1);
501
+
502
+ // Second dispose is a no-op.
503
+ harness.client.dispose();
504
+ await new Promise((resolve) => setTimeout(resolve, 0));
505
+ expect(harness.disposeCount.count).toBe(1);
506
+ });
507
+
508
+ test("dispose() without any sends does not call connectCdpWsTransport", async () => {
509
+ const harness = createHarness();
510
+ harness.client.dispose();
511
+ await new Promise((resolve) => setTimeout(resolve, 0));
512
+ expect(harness.connectCalls).toBe(0);
513
+ expect(harness.disposeCount.count).toBe(0);
514
+ });
515
+
516
+ test("send() after dispose throws CdpError with code 'disposed'", async () => {
517
+ const harness = createHarness();
518
+ harness.client.dispose();
519
+ let caught: unknown;
520
+ try {
521
+ await harness.client.send("Browser.getVersion");
522
+ } catch (err) {
523
+ caught = err;
524
+ }
525
+ expect(caught).toBeInstanceOf(CdpError);
526
+ const cdpErr = caught as InstanceType<typeof CdpError>;
527
+ expect(cdpErr.code).toBe("disposed");
528
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
529
+ // No discovery or transport activity took place.
530
+ expect(harness.probeCalls).toBe(0);
531
+ expect(harness.listCalls).toBe(0);
532
+ expect(harness.connectCalls).toBe(0);
533
+ });
534
+
535
+ test("attach that returns no sessionId throws 'cdp_error'", async () => {
536
+ const harness = createHarness({
537
+ transportOnSend: async (method) => {
538
+ if (method === "Target.attachToTarget") {
539
+ // Missing sessionId field — a broken fork response.
540
+ return {};
541
+ }
542
+ return undefined;
543
+ },
544
+ });
545
+ let caught: unknown;
546
+ try {
547
+ await harness.client.send("Browser.getVersion");
548
+ } catch (err) {
549
+ caught = err;
550
+ }
551
+ expect(caught).toBeInstanceOf(CdpError);
552
+ const cdpErr = caught as InstanceType<typeof CdpError>;
553
+ expect(cdpErr.code).toBe("cdp_error");
554
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
555
+ });
556
+
557
+ test("attach failure tears down the partially-opened transport", async () => {
558
+ const localDisposeCount = { count: 0 };
559
+ const transport = createFakeTransport({
560
+ onSend: async (method) => {
561
+ if (method === "Target.attachToTarget") {
562
+ throw new CdpWsTransportError("cdp_error", "attach failed", {
563
+ cdpMethod: method,
564
+ });
565
+ }
566
+ return undefined;
567
+ },
568
+ trackDisposeCount: localDisposeCount,
569
+ });
570
+ const client = createCdpInspectClient("conv-attach-fail", {
571
+ host: "127.0.0.1",
572
+ port: 9222,
573
+ helpers: {
574
+ probeDevToolsJsonVersion: async () => ({
575
+ browser: "Chrome/125.0.0.0",
576
+ protocolVersion: "1.3",
577
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
578
+ }),
579
+ listDevToolsTargets: async () => [
580
+ {
581
+ id: "target-1",
582
+ type: "page",
583
+ title: "Example",
584
+ url: "https://example.com/",
585
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
586
+ },
587
+ ],
588
+ connectCdpWsTransport: async () => transport,
589
+ },
590
+ });
591
+
592
+ let caught: unknown;
593
+ try {
594
+ await client.send("Browser.getVersion");
595
+ } catch (err) {
596
+ caught = err;
597
+ }
598
+ expect(caught).toBeInstanceOf(CdpError);
599
+ // The transport opened by attach() should have been disposed so
600
+ // the socket doesn't leak.
601
+ expect(localDisposeCount.count).toBe(1);
602
+ });
603
+
604
+ test("send() aborts promptly when signal fires during ensureSession", async () => {
605
+ // Discovery is deliberately stalled so the caller has to rely on
606
+ // raceAbort() to cut through. If raceAbort worked correctly, the
607
+ // send() promise rejects with an 'aborted' CdpError the instant
608
+ // the controller fires — even though probeDevToolsJsonVersion is
609
+ // still hanging on an unresolved await.
610
+ const controller = new AbortController();
611
+ let probeSignalSeen: AbortSignal | undefined;
612
+ let probeResolve: (() => void) | undefined;
613
+ const probeStarted = new Promise<void>((resolve) => {
614
+ probeResolve = resolve;
615
+ });
616
+
617
+ const client = createCdpInspectClient("conv-abort-during-ensure", {
618
+ host: "127.0.0.1",
619
+ port: 9222,
620
+ helpers: {
621
+ probeDevToolsJsonVersion: async (probeOpts) => {
622
+ // Capture the signal so we can assert the shared attach
623
+ // controller actually fired downstream.
624
+ probeSignalSeen = (probeOpts as { signal?: AbortSignal }).signal;
625
+ probeResolve?.();
626
+ // Hang forever unless the shared controller fires.
627
+ await new Promise<never>((_, reject) => {
628
+ const onAbort = () => {
629
+ reject(new Error("probe aborted via shared controller"));
630
+ };
631
+ if (probeSignalSeen?.aborted) {
632
+ onAbort();
633
+ } else {
634
+ probeSignalSeen?.addEventListener("abort", onAbort, {
635
+ once: true,
636
+ });
637
+ }
638
+ });
639
+ throw new Error("unreachable");
640
+ },
641
+ listDevToolsTargets: async () => [
642
+ {
643
+ id: "target-1",
644
+ type: "page",
645
+ title: "Example",
646
+ url: "https://example.com/",
647
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
648
+ },
649
+ ],
650
+ connectCdpWsTransport: async () =>
651
+ createFakeTransport({
652
+ onSend: async (method) => {
653
+ if (method === "Target.attachToTarget") {
654
+ return { sessionId: "fake-session-id" };
655
+ }
656
+ return undefined;
657
+ },
658
+ }),
659
+ },
660
+ });
661
+
662
+ const sendPromise = client.send(
663
+ "Browser.getVersion",
664
+ undefined,
665
+ controller.signal,
666
+ );
667
+ // Wait until probe is actually running, then abort.
668
+ await probeStarted;
669
+ controller.abort();
670
+
671
+ let caught: unknown;
672
+ try {
673
+ await sendPromise;
674
+ } catch (err) {
675
+ caught = err;
676
+ }
677
+ expect(caught).toBeInstanceOf(CdpError);
678
+ const cdpErr = caught as InstanceType<typeof CdpError>;
679
+ expect(cdpErr.code).toBe("aborted");
680
+ expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
681
+ // Probe saw a non-null signal, meaning ensureSession plumbed one
682
+ // all the way down to the discovery helper.
683
+ expect(probeSignalSeen).toBeDefined();
684
+ // After the last (and only) waiter aborted, the shared controller
685
+ // should have been aborted — downstream probe's await should have
686
+ // been rejected.
687
+ expect(probeSignalSeen?.aborted).toBe(true);
688
+ });
689
+
690
+ test("a new send() after all waiters abort starts a fresh attach", async () => {
691
+ // Regression test for the race condition where onAbort aborts
692
+ // the shared controller but `this.pending` is only cleared later
693
+ // via the async `.catch()` handler in startAttach(). A new caller
694
+ // entering ensureSession() between those two events would reuse
695
+ // the already-aborted pending attach and immediately fail with an
696
+ // `aborted` error even though it never aborted its own signal.
697
+ //
698
+ // Fix: onAbort now clears `this.pending` synchronously BEFORE
699
+ // firing the shared controller.abort(), so any new caller after
700
+ // the abort starts a fresh attach.
701
+ let probeCount = 0;
702
+ let listCount = 0;
703
+ let connectCount = 0;
704
+
705
+ // The first probe hangs until the shared controller aborts it.
706
+ // The second probe (from the fresh attach) resolves normally.
707
+ const firstProbeStarted = Promise.withResolvers<void>();
708
+
709
+ const client = createCdpInspectClient("conv-race", {
710
+ host: "127.0.0.1",
711
+ port: 9222,
712
+ helpers: {
713
+ probeDevToolsJsonVersion: async (probeOpts) => {
714
+ probeCount += 1;
715
+ const signal = (probeOpts as { signal?: AbortSignal }).signal;
716
+ if (probeCount === 1) {
717
+ firstProbeStarted.resolve();
718
+ // Stall until the shared controller aborts us.
719
+ await new Promise<never>((_, reject) => {
720
+ const onAbort = () => {
721
+ reject(new Error("probe aborted via shared controller"));
722
+ };
723
+ if (signal?.aborted) {
724
+ onAbort();
725
+ } else {
726
+ signal?.addEventListener("abort", onAbort, { once: true });
727
+ }
728
+ });
729
+ throw new Error("unreachable");
730
+ }
731
+ return {
732
+ browser: "Chrome/125.0.0.0",
733
+ protocolVersion: "1.3",
734
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
735
+ };
736
+ },
737
+ listDevToolsTargets: async () => {
738
+ listCount += 1;
739
+ return [
740
+ {
741
+ id: "target-1",
742
+ type: "page",
743
+ title: "Example",
744
+ url: "https://example.com/",
745
+ webSocketDebuggerUrl:
746
+ "ws://127.0.0.1:9222/devtools/page/target-1",
747
+ },
748
+ ];
749
+ },
750
+ connectCdpWsTransport: async () => {
751
+ connectCount += 1;
752
+ return createFakeTransport({
753
+ onSend: async (method) => {
754
+ if (method === "Target.attachToTarget") {
755
+ return { sessionId: "session-race" };
756
+ }
757
+ return { ok: true };
758
+ },
759
+ });
760
+ },
761
+ },
762
+ });
763
+
764
+ // 1. First caller kicks off the attach with signal A.
765
+ const signalA = new AbortController();
766
+ const firstSend = client.send("Runtime.enable", undefined, signalA.signal);
767
+
768
+ // 2. Wait until the first probe has actually started so we know
769
+ // the attach is in-flight and signal A is the only waiter.
770
+ await firstProbeStarted.promise;
771
+
772
+ // 3. Abort signal A. Because it's the only waiter, onAbort will
773
+ // fire the shared controller and (with the fix) clear
774
+ // `this.pending` synchronously.
775
+ signalA.abort();
776
+
777
+ // First caller should reject with `aborted`.
778
+ let firstErr: unknown;
779
+ try {
780
+ await firstSend;
781
+ } catch (err) {
782
+ firstErr = err;
783
+ }
784
+ expect(firstErr).toBeInstanceOf(CdpError);
785
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
786
+
787
+ // At this point, with the fix, `this.pending` has been cleared
788
+ // synchronously — but without the fix, it would still be set to
789
+ // the aborted pending until the `.catch()` handler in
790
+ // startAttach() runs asynchronously. We intentionally do NOT
791
+ // flush microtasks here before kicking off the second send() so
792
+ // that we exercise the race window.
793
+ //
794
+ // 4. New send() with its own signal B. With the fix, this should
795
+ // start a fresh attach and complete successfully. Without the
796
+ // fix, it would reuse the aborted pending and fail with an
797
+ // `aborted` error even though signal B was never aborted.
798
+ const signalB = new AbortController();
799
+ const secondSend = client.send("Page.enable", undefined, signalB.signal);
800
+
801
+ // Second caller should succeed.
802
+ const secondResult = await secondSend;
803
+ expect(secondResult).toEqual({ ok: true });
804
+
805
+ // 5. Assert that the second send() kicked off a fresh attach —
806
+ // probe, list, and connect should all have been called twice.
807
+ expect(probeCount).toBe(2);
808
+ expect(listCount).toBe(1);
809
+ expect(connectCount).toBe(1);
810
+ // listCount and connectCount are 1 because the first attach
811
+ // aborted during the probe stage — it never reached list or
812
+ // connect. The second (fresh) attach ran probe + list + connect
813
+ // all once.
814
+ });
815
+
816
+ test("concurrent send() callers can abort independently", async () => {
817
+ // Two callers race the same in-flight attach. The first caller
818
+ // aborts; the second caller must still complete normally once the
819
+ // shared attach resolves.
820
+ const aborter = new AbortController();
821
+ let probeResolve: (() => void) | undefined;
822
+ let releaseProbe: (() => void) | undefined;
823
+ const probeRunning = new Promise<void>((resolve) => {
824
+ probeResolve = resolve;
825
+ });
826
+ const probeCanFinish = new Promise<void>((resolve) => {
827
+ releaseProbe = resolve;
828
+ });
829
+
830
+ const harness = createHarness({
831
+ probeImpl: async () => {
832
+ probeResolve?.();
833
+ await probeCanFinish;
834
+ return {
835
+ browser: "Chrome/125.0.0.0",
836
+ protocolVersion: "1.3",
837
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
838
+ };
839
+ },
840
+ });
841
+
842
+ const aborted = harness.client.send(
843
+ "Runtime.enable",
844
+ undefined,
845
+ aborter.signal,
846
+ );
847
+ const stable = harness.client.send("Page.enable");
848
+
849
+ await probeRunning;
850
+ aborter.abort();
851
+
852
+ // First caller aborts promptly…
853
+ let firstErr: unknown;
854
+ try {
855
+ await aborted;
856
+ } catch (err) {
857
+ firstErr = err;
858
+ }
859
+ expect(firstErr).toBeInstanceOf(CdpError);
860
+ expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
861
+
862
+ // …but the second caller is still alive and can make progress
863
+ // once the shared attach finishes.
864
+ releaseProbe?.();
865
+ await stable;
866
+ expect(harness.probeCalls).toBe(1);
867
+ expect(harness.listCalls).toBe(1);
868
+ expect(harness.connectCalls).toBe(1);
869
+ expect(harness.attachCallCount()).toBe(1);
870
+ });
871
+ });
872
+
873
+ // ---------------------------------------------------------------------------
874
+ // WS-only fallback — HTTP discovery absent, WS fallback succeeds.
875
+ // ---------------------------------------------------------------------------
876
+
877
+ describe("CdpInspectClient — WS-only fallback", () => {
878
+ test("falls back to WS when HTTP discovery returns invalid_response", async () => {
879
+ const sends: Array<{
880
+ method: string;
881
+ params?: Record<string, unknown>;
882
+ sessionId?: string;
883
+ }> = [];
884
+ let connectCount = 0;
885
+ let discoverViaWsCalls = 0;
886
+
887
+ const client = createCdpInspectClient("conv-ws-fallback", {
888
+ host: "127.0.0.1",
889
+ port: 9222,
890
+ discoveryTimeoutMs: 100,
891
+ wsConnectTimeoutMs: 100,
892
+ helpers: {
893
+ probeDevToolsJsonVersion: async () => {
894
+ throw new DevToolsDiscoveryError(
895
+ "invalid_response",
896
+ "DevTools /json/version returned HTTP 404.",
897
+ );
898
+ },
899
+ listDevToolsTargets: async () => {
900
+ throw new Error("listDevToolsTargets should not be called");
901
+ },
902
+ connectCdpWsTransport: async () => {
903
+ connectCount += 1;
904
+ return createFakeTransport({
905
+ onSend: async (method) => {
906
+ if (method === "Target.attachToTarget") {
907
+ return { sessionId: "ws-session" };
908
+ }
909
+ return { ok: true };
910
+ },
911
+ trackSends: sends,
912
+ });
913
+ },
914
+ discoverTargetsViaWs: async () => {
915
+ discoverViaWsCalls += 1;
916
+ return [
917
+ {
918
+ id: "ws-target-1",
919
+ type: "page",
920
+ title: "WS Page",
921
+ url: "https://example.com/",
922
+ webSocketDebuggerUrl:
923
+ "ws://127.0.0.1:9222/devtools/page/ws-target-1",
924
+ },
925
+ ];
926
+ },
927
+ buildBrowserWsUrl: (host: string, port: number) =>
928
+ `ws://${host}:${port}/devtools/browser`,
929
+ },
930
+ });
931
+
932
+ const result = await client.send<{ ok: boolean }>("Browser.getVersion");
933
+ expect(result).toEqual({ ok: true });
934
+ expect(connectCount).toBe(1);
935
+ expect(discoverViaWsCalls).toBe(1);
936
+ // attach + forwarded call
937
+ expect(sends).toEqual([
938
+ {
939
+ method: "Target.attachToTarget",
940
+ params: { targetId: "ws-target-1", flatten: true },
941
+ sessionId: undefined,
942
+ },
943
+ {
944
+ method: "Browser.getVersion",
945
+ params: undefined,
946
+ sessionId: "ws-session",
947
+ },
948
+ ]);
949
+ });
950
+
951
+ test("falls back to WS when HTTP discovery is unreachable", async () => {
952
+ let discoverViaWsCalls = 0;
953
+
954
+ const client = createCdpInspectClient("conv-ws-unreachable", {
955
+ host: "127.0.0.1",
956
+ port: 9222,
957
+ discoveryTimeoutMs: 100,
958
+ helpers: {
959
+ probeDevToolsJsonVersion: async () => {
960
+ throw new DevToolsDiscoveryError(
961
+ "unreachable",
962
+ "Failed to reach DevTools endpoint: ECONNREFUSED",
963
+ );
964
+ },
965
+ listDevToolsTargets: async () => {
966
+ throw new Error("should not be called");
967
+ },
968
+ connectCdpWsTransport: async () =>
969
+ createFakeTransport({
970
+ onSend: async (method) => {
971
+ if (method === "Target.attachToTarget") {
972
+ return { sessionId: "ws-session" };
973
+ }
974
+ return { ok: true };
975
+ },
976
+ }),
977
+ discoverTargetsViaWs: async () => {
978
+ discoverViaWsCalls += 1;
979
+ return [
980
+ {
981
+ id: "ws-target-1",
982
+ type: "page",
983
+ title: "Page",
984
+ url: "https://example.com/",
985
+ webSocketDebuggerUrl:
986
+ "ws://127.0.0.1:9222/devtools/page/ws-target-1",
987
+ },
988
+ ];
989
+ },
990
+ buildBrowserWsUrl: (host: string, port: number) =>
991
+ `ws://${host}:${port}/devtools/browser`,
992
+ },
993
+ });
994
+
995
+ const result = await client.send<{ ok: boolean }>("Runtime.enable");
996
+ expect(result).toEqual({ ok: true });
997
+ expect(discoverViaWsCalls).toBe(1);
998
+ });
999
+
1000
+ test("does NOT fall back to WS on non_loopback error (safety constraint)", async () => {
1001
+ let connectCalled = false;
1002
+
1003
+ const client = createCdpInspectClient("conv-no-fallback-loopback", {
1004
+ host: "127.0.0.1",
1005
+ port: 9222,
1006
+ discoveryTimeoutMs: 100,
1007
+ helpers: {
1008
+ probeDevToolsJsonVersion: async () => {
1009
+ throw new DevToolsDiscoveryError(
1010
+ "non_loopback",
1011
+ "Refusing to probe non-loopback host",
1012
+ );
1013
+ },
1014
+ listDevToolsTargets: async () => {
1015
+ throw new Error("should not be called");
1016
+ },
1017
+ connectCdpWsTransport: async () => {
1018
+ connectCalled = true;
1019
+ return createFakeTransport({});
1020
+ },
1021
+ discoverTargetsViaWs: async () => {
1022
+ throw new Error("should not be called");
1023
+ },
1024
+ buildBrowserWsUrl: () => "ws://127.0.0.1:9222/devtools/browser",
1025
+ },
1026
+ });
1027
+
1028
+ let caught: unknown;
1029
+ try {
1030
+ await client.send("Browser.getVersion");
1031
+ } catch (err) {
1032
+ caught = err;
1033
+ }
1034
+
1035
+ expect(caught).toBeInstanceOf(CdpError);
1036
+ const cdpErr = caught as InstanceType<typeof CdpError>;
1037
+ expect(cdpErr.code).toBe("transport_error");
1038
+ expect(cdpErr.message).toContain("non-loopback");
1039
+ expect(connectCalled).toBe(false);
1040
+ });
1041
+
1042
+ test("does NOT fall back to WS on non_chrome error", async () => {
1043
+ let connectCalled = false;
1044
+
1045
+ const client = createCdpInspectClient("conv-no-fallback-chrome", {
1046
+ host: "127.0.0.1",
1047
+ port: 9222,
1048
+ discoveryTimeoutMs: 100,
1049
+ helpers: {
1050
+ probeDevToolsJsonVersion: async () => {
1051
+ throw new DevToolsDiscoveryError(
1052
+ "non_chrome",
1053
+ "Not Chrome: Firefox/115.0",
1054
+ );
1055
+ },
1056
+ listDevToolsTargets: async () => {
1057
+ throw new Error("should not be called");
1058
+ },
1059
+ connectCdpWsTransport: async () => {
1060
+ connectCalled = true;
1061
+ return createFakeTransport({});
1062
+ },
1063
+ discoverTargetsViaWs: async () => {
1064
+ throw new Error("should not be called");
1065
+ },
1066
+ buildBrowserWsUrl: () => "ws://127.0.0.1:9222/devtools/browser",
1067
+ },
1068
+ });
1069
+
1070
+ let caught: unknown;
1071
+ try {
1072
+ await client.send("Browser.getVersion");
1073
+ } catch (err) {
1074
+ caught = err;
1075
+ }
1076
+
1077
+ expect(caught).toBeInstanceOf(CdpError);
1078
+ const cdpErr = caught as InstanceType<typeof CdpError>;
1079
+ expect(cdpErr.code).toBe("transport_error");
1080
+ expect(connectCalled).toBe(false);
1081
+ });
1082
+
1083
+ test("does NOT fall back to WS on timeout error", async () => {
1084
+ let connectCalled = false;
1085
+
1086
+ const client = createCdpInspectClient("conv-no-fallback-timeout", {
1087
+ host: "127.0.0.1",
1088
+ port: 9222,
1089
+ discoveryTimeoutMs: 100,
1090
+ helpers: {
1091
+ probeDevToolsJsonVersion: async () => {
1092
+ throw new DevToolsDiscoveryError(
1093
+ "timeout",
1094
+ "Timed out waiting for DevTools HTTP response.",
1095
+ );
1096
+ },
1097
+ listDevToolsTargets: async () => {
1098
+ throw new Error("should not be called");
1099
+ },
1100
+ connectCdpWsTransport: async () => {
1101
+ connectCalled = true;
1102
+ return createFakeTransport({});
1103
+ },
1104
+ discoverTargetsViaWs: async () => {
1105
+ throw new Error("should not be called");
1106
+ },
1107
+ buildBrowserWsUrl: () => "ws://127.0.0.1:9222/devtools/browser",
1108
+ },
1109
+ });
1110
+
1111
+ let caught: unknown;
1112
+ try {
1113
+ await client.send("Browser.getVersion");
1114
+ } catch (err) {
1115
+ caught = err;
1116
+ }
1117
+
1118
+ expect(caught).toBeInstanceOf(CdpError);
1119
+ const cdpErr = caught as InstanceType<typeof CdpError>;
1120
+ expect(cdpErr.code).toBe("transport_error");
1121
+ expect(connectCalled).toBe(false);
1122
+ });
1123
+
1124
+ test("full fallback failure: HTTP invalid_response + WS connect fails", async () => {
1125
+ const client = createCdpInspectClient("conv-full-fail", {
1126
+ host: "127.0.0.1",
1127
+ port: 9222,
1128
+ discoveryTimeoutMs: 100,
1129
+ helpers: {
1130
+ probeDevToolsJsonVersion: async () => {
1131
+ throw new DevToolsDiscoveryError(
1132
+ "invalid_response",
1133
+ "DevTools /json/version returned HTTP 404.",
1134
+ );
1135
+ },
1136
+ listDevToolsTargets: async () => {
1137
+ throw new Error("should not be called");
1138
+ },
1139
+ connectCdpWsTransport: async () => {
1140
+ throw new CdpWsTransportError(
1141
+ "transport_error",
1142
+ "websocket closed before open",
1143
+ );
1144
+ },
1145
+ discoverTargetsViaWs: async () => {
1146
+ throw new Error("should not be called");
1147
+ },
1148
+ buildBrowserWsUrl: (host: string, port: number) =>
1149
+ `ws://${host}:${port}/devtools/browser`,
1150
+ },
1151
+ });
1152
+
1153
+ let caught: unknown;
1154
+ try {
1155
+ await client.send("Browser.getVersion");
1156
+ } catch (err) {
1157
+ caught = err;
1158
+ }
1159
+
1160
+ expect(caught).toBeInstanceOf(CdpError);
1161
+ const cdpErr = caught as InstanceType<typeof CdpError>;
1162
+ expect(cdpErr.code).toBe("transport_error");
1163
+ // Error message should explain both HTTP and WS failures
1164
+ expect(cdpErr.message).toContain("HTTP discovery failed");
1165
+ expect(cdpErr.message).toContain("WS-only fallback also failed");
1166
+ expect(cdpErr.message).toContain("127.0.0.1:9222");
1167
+ });
1168
+
1169
+ test("WS fallback: no page targets yields stable error", async () => {
1170
+ const client = createCdpInspectClient("conv-ws-no-targets", {
1171
+ host: "127.0.0.1",
1172
+ port: 9222,
1173
+ discoveryTimeoutMs: 100,
1174
+ helpers: {
1175
+ probeDevToolsJsonVersion: async () => {
1176
+ throw new DevToolsDiscoveryError("unreachable", "ECONNREFUSED");
1177
+ },
1178
+ listDevToolsTargets: async () => {
1179
+ throw new Error("should not be called");
1180
+ },
1181
+ connectCdpWsTransport: async () =>
1182
+ createFakeTransport({
1183
+ onSend: async () => ({ ok: true }),
1184
+ }),
1185
+ discoverTargetsViaWs: async () => {
1186
+ throw new DevToolsDiscoveryError(
1187
+ "no_targets",
1188
+ "No usable page targets returned by CDP Target.getTargets.",
1189
+ );
1190
+ },
1191
+ buildBrowserWsUrl: (host: string, port: number) =>
1192
+ `ws://${host}:${port}/devtools/browser`,
1193
+ },
1194
+ });
1195
+
1196
+ let caught: unknown;
1197
+ try {
1198
+ await client.send("Browser.getVersion");
1199
+ } catch (err) {
1200
+ caught = err;
1201
+ }
1202
+
1203
+ expect(caught).toBeInstanceOf(CdpError);
1204
+ const cdpErr = caught as InstanceType<typeof CdpError>;
1205
+ expect(cdpErr.code).toBe("transport_error");
1206
+ expect(cdpErr.message).toContain("No usable page targets");
1207
+ });
1208
+
1209
+ test("WS fallback caches session for subsequent calls", async () => {
1210
+ let connectCount = 0;
1211
+ let discoverViaWsCalls = 0;
1212
+
1213
+ const client = createCdpInspectClient("conv-ws-cache", {
1214
+ host: "127.0.0.1",
1215
+ port: 9222,
1216
+ discoveryTimeoutMs: 100,
1217
+ helpers: {
1218
+ probeDevToolsJsonVersion: async () => {
1219
+ throw new DevToolsDiscoveryError(
1220
+ "invalid_response",
1221
+ "no HTTP discovery",
1222
+ );
1223
+ },
1224
+ listDevToolsTargets: async () => {
1225
+ throw new Error("should not be called");
1226
+ },
1227
+ connectCdpWsTransport: async () => {
1228
+ connectCount += 1;
1229
+ return createFakeTransport({
1230
+ onSend: async (method) => {
1231
+ if (method === "Target.attachToTarget") {
1232
+ return { sessionId: "ws-session-cached" };
1233
+ }
1234
+ return { ok: true };
1235
+ },
1236
+ });
1237
+ },
1238
+ discoverTargetsViaWs: async () => {
1239
+ discoverViaWsCalls += 1;
1240
+ return [
1241
+ {
1242
+ id: "t1",
1243
+ type: "page",
1244
+ title: "Page",
1245
+ url: "https://example.com/",
1246
+ webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/t1",
1247
+ },
1248
+ ];
1249
+ },
1250
+ buildBrowserWsUrl: (host: string, port: number) =>
1251
+ `ws://${host}:${port}/devtools/browser`,
1252
+ },
1253
+ });
1254
+
1255
+ await client.send("Runtime.enable");
1256
+ await client.send("Page.enable");
1257
+ await client.send("DOM.enable");
1258
+
1259
+ // Only one WS fallback discovery + one connect + one attach
1260
+ expect(connectCount).toBe(1);
1261
+ expect(discoverViaWsCalls).toBe(1);
1262
+ });
1263
+ });