@vellumai/assistant 0.3.4 → 0.3.6

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 (506) hide show
  1. package/Dockerfile +2 -0
  2. package/README.md +88 -2
  3. package/eslint.config.mjs +31 -0
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +4 -1
  6. package/scripts/ipc/generate-swift.ts +31 -2
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +438 -1
  8. package/src/__tests__/approval-conversation-turn.test.ts +214 -0
  9. package/src/__tests__/approval-hardcoded-copy-guard.test.ts +41 -0
  10. package/src/__tests__/approval-message-composer.test.ts +253 -0
  11. package/src/__tests__/browser-manager.test.ts +1 -0
  12. package/src/__tests__/call-conversation-messages.test.ts +130 -0
  13. package/src/__tests__/call-domain.test.ts +12 -2
  14. package/src/__tests__/call-orchestrator.test.ts +799 -249
  15. package/src/__tests__/call-pointer-messages.test.ts +148 -0
  16. package/src/__tests__/call-recovery.test.ts +3 -0
  17. package/src/__tests__/call-routes-http.test.ts +32 -2
  18. package/src/__tests__/call-store.test.ts +3 -0
  19. package/src/__tests__/channel-approval-routes.test.ts +1277 -98
  20. package/src/__tests__/channel-approval.test.ts +37 -0
  21. package/src/__tests__/channel-approvals.test.ts +36 -50
  22. package/src/__tests__/channel-guardian.test.ts +630 -22
  23. package/src/__tests__/channel-readiness-service.test.ts +324 -0
  24. package/src/__tests__/checker.test.ts +14 -7
  25. package/src/__tests__/clarification-resolver.test.ts +44 -24
  26. package/src/__tests__/commit-message-enrichment-service.test.ts +9 -4
  27. package/src/__tests__/computer-use-session-working-dir.test.ts +8 -0
  28. package/src/__tests__/config-schema.test.ts +14 -8
  29. package/src/__tests__/context-window-manager.test.ts +30 -2
  30. package/src/__tests__/contradiction-checker.test.ts +20 -5
  31. package/src/__tests__/credential-security-invariants.test.ts +7 -2
  32. package/src/__tests__/daemon-lifecycle.test.ts +13 -12
  33. package/src/__tests__/db-migration-rollback.test.ts +752 -0
  34. package/src/__tests__/dictation-mode-detection.test.ts +63 -0
  35. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +2 -0
  36. package/src/__tests__/entity-search.test.ts +615 -0
  37. package/src/__tests__/fuzzy-match-property.test.ts +5 -5
  38. package/src/__tests__/guardian-action-store.test.ts +123 -0
  39. package/src/__tests__/guardian-action-sweep.test.ts +277 -0
  40. package/src/__tests__/guardian-dispatch.test.ts +389 -0
  41. package/src/__tests__/guardian-question-copy.test.ts +47 -0
  42. package/src/__tests__/handlers-telegram-config.test.ts +4 -2
  43. package/src/__tests__/handlers-twilio-config.test.ts +533 -0
  44. package/src/__tests__/intent-routing.test.ts +2 -0
  45. package/src/__tests__/ipc-snapshot.test.ts +291 -1
  46. package/src/__tests__/memory-upsert-concurrency.test.ts +828 -0
  47. package/src/__tests__/messaging-send-tool.test.ts +65 -0
  48. package/src/__tests__/model-intents.test.ts +96 -0
  49. package/src/__tests__/no-direct-anthropic-sdk-imports.test.ts +42 -0
  50. package/src/__tests__/oauth2-gateway-transport.test.ts +130 -0
  51. package/src/__tests__/onboarding-starter-tasks.test.ts +2 -0
  52. package/src/__tests__/provider-commit-message-generator.test.ts +89 -13
  53. package/src/__tests__/provider-error-scenarios.test.ts +621 -0
  54. package/src/__tests__/provider-fail-open-selection.test.ts +119 -0
  55. package/src/__tests__/qdrant-manager.test.ts +27 -20
  56. package/src/__tests__/relay-server.test.ts +779 -40
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +6 -0
  58. package/src/__tests__/run-orchestrator.test.ts +42 -4
  59. package/src/__tests__/runtime-runs-http.test.ts +17 -1
  60. package/src/__tests__/runtime-runs.test.ts +16 -0
  61. package/src/__tests__/schedule-store.test.ts +18 -4
  62. package/src/__tests__/scheduler-recurrence.test.ts +13 -4
  63. package/src/__tests__/session-abort-tool-results.test.ts +6 -0
  64. package/src/__tests__/session-agent-loop.test.ts +857 -0
  65. package/src/__tests__/session-conflict-gate.test.ts +6 -0
  66. package/src/__tests__/session-pre-run-repair.test.ts +6 -0
  67. package/src/__tests__/session-profile-injection.test.ts +6 -0
  68. package/src/__tests__/session-provider-retry-repair.test.ts +6 -0
  69. package/src/__tests__/session-queue.test.ts +6 -0
  70. package/src/__tests__/session-runtime-assembly.test.ts +321 -13
  71. package/src/__tests__/session-slash-known.test.ts +6 -0
  72. package/src/__tests__/session-slash-queue.test.ts +6 -0
  73. package/src/__tests__/session-slash-unknown.test.ts +6 -0
  74. package/src/__tests__/session-surfaces-task-progress.test.ts +2 -0
  75. package/src/__tests__/session-tool-setup-app-refresh.test.ts +1 -0
  76. package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -0
  77. package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -0
  78. package/src/__tests__/session-workspace-injection.test.ts +6 -0
  79. package/src/__tests__/session-workspace-tool-tracking.test.ts +6 -0
  80. package/src/__tests__/skills.test.ts +2 -0
  81. package/src/__tests__/sms-messaging-provider.test.ts +126 -0
  82. package/src/__tests__/starter-task-flow.test.ts +2 -0
  83. package/src/__tests__/swarm-dag-pathological.test.ts +535 -0
  84. package/src/__tests__/system-prompt.test.ts +2 -0
  85. package/src/__tests__/task-management-tools.test.ts +2 -2
  86. package/src/__tests__/task-runner.test.ts +14 -4
  87. package/src/__tests__/terminal-tools.test.ts +25 -19
  88. package/src/__tests__/tool-execution-abort-cleanup.test.ts +545 -0
  89. package/src/__tests__/tool-executor-shell-integration.test.ts +11 -11
  90. package/src/__tests__/tool-executor.test.ts +23 -24
  91. package/src/__tests__/trust-store.test.ts +3 -3
  92. package/src/__tests__/twilio-rest.test.ts +29 -0
  93. package/src/__tests__/twilio-routes-elevenlabs.test.ts +3 -0
  94. package/src/__tests__/twilio-routes-twiml.test.ts +11 -0
  95. package/src/__tests__/twilio-routes.test.ts +167 -11
  96. package/src/__tests__/twitter-cli-error-shaping.test.ts +2 -2
  97. package/src/__tests__/user-reference.test.ts +2 -0
  98. package/src/__tests__/voice-quality.test.ts +222 -0
  99. package/src/__tests__/web-search.test.ts +46 -30
  100. package/src/__tests__/work-item-output.test.ts +110 -0
  101. package/src/agent/loop.ts +1 -1
  102. package/src/agent-heartbeat/agent-heartbeat-service.ts +2 -10
  103. package/src/amazon/client.ts +1418 -0
  104. package/src/amazon/request-extractor.ts +135 -0
  105. package/src/amazon/session.ts +109 -0
  106. package/src/autonomy/autonomy-store.ts +5 -5
  107. package/src/browser-extension-relay/client.ts +124 -0
  108. package/src/browser-extension-relay/protocol.ts +63 -0
  109. package/src/browser-extension-relay/server.ts +177 -0
  110. package/src/bundler/app-bundler.ts +3 -3
  111. package/src/bundler/bundle-signer.ts +1 -1
  112. package/src/bundler/signature-verifier.ts +1 -1
  113. package/src/calls/call-conversation-messages.ts +33 -0
  114. package/src/calls/call-domain.ts +114 -10
  115. package/src/calls/call-orchestrator.ts +268 -59
  116. package/src/calls/call-pointer-messages.ts +53 -0
  117. package/src/calls/call-recovery.ts +3 -8
  118. package/src/calls/call-store.ts +69 -87
  119. package/src/calls/elevenlabs-config.ts +3 -2
  120. package/src/calls/guardian-action-sweep.ts +105 -0
  121. package/src/calls/guardian-dispatch.ts +203 -0
  122. package/src/calls/guardian-question-copy.ts +133 -0
  123. package/src/calls/relay-server.ts +466 -8
  124. package/src/calls/speaker-identification.ts +1 -1
  125. package/src/calls/twilio-config.ts +22 -14
  126. package/src/calls/twilio-provider.ts +6 -4
  127. package/src/calls/twilio-rest.ts +308 -7
  128. package/src/calls/twilio-routes.ts +65 -12
  129. package/src/calls/types.ts +3 -1
  130. package/src/channels/types.ts +25 -0
  131. package/src/cli/amazon.ts +815 -0
  132. package/src/cli/config-commands.ts +2 -2
  133. package/src/cli/core-commands.ts +4 -3
  134. package/src/cli/influencer.ts +244 -0
  135. package/src/cli/map.ts +89 -6
  136. package/src/cli.ts +1 -1
  137. package/src/config/agent-schema.ts +171 -0
  138. package/src/config/bundled-skills/amazon/SKILL.md +127 -0
  139. package/src/config/bundled-skills/amazon/icon.svg +13 -0
  140. package/src/config/bundled-skills/api-mapping/SKILL.md +78 -0
  141. package/src/config/bundled-skills/browser/SKILL.md +1 -0
  142. package/src/config/bundled-skills/browser/TOOLS.json +17 -0
  143. package/src/config/bundled-skills/browser/tools/browser-wait-for-download.ts +25 -0
  144. package/src/config/bundled-skills/doordash/SKILL.md +51 -51
  145. package/src/config/bundled-skills/email-setup/SKILL.md +14 -5
  146. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +183 -0
  147. package/src/config/bundled-skills/influencer/SKILL.md +144 -0
  148. package/src/config/bundled-skills/knowledge-graph/SKILL.md +15 -0
  149. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +56 -0
  150. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +185 -0
  151. package/src/config/bundled-skills/macos-automation/icon.svg +12 -0
  152. package/src/config/bundled-skills/media-processing/SKILL.md +176 -0
  153. package/src/config/bundled-skills/media-processing/TOOLS.json +230 -0
  154. package/src/config/bundled-skills/media-processing/__tests__/concurrency-pool.test.ts +77 -0
  155. package/src/config/bundled-skills/media-processing/__tests__/cost-tracker.test.ts +69 -0
  156. package/src/config/bundled-skills/media-processing/__tests__/preprocess.test.ts +303 -0
  157. package/src/config/bundled-skills/media-processing/services/concurrency-pool.ts +55 -0
  158. package/src/config/bundled-skills/media-processing/services/cost-tracker.ts +86 -0
  159. package/src/config/bundled-skills/media-processing/services/gemini-map.ts +339 -0
  160. package/src/config/bundled-skills/media-processing/services/preprocess.ts +551 -0
  161. package/src/config/bundled-skills/media-processing/services/processing-pipeline.ts +259 -0
  162. package/src/config/bundled-skills/media-processing/services/reduce.ts +197 -0
  163. package/src/config/bundled-skills/media-processing/tools/analyze-keyframes.ts +136 -0
  164. package/src/config/bundled-skills/media-processing/tools/extract-keyframes.ts +59 -0
  165. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +195 -0
  166. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +197 -0
  167. package/src/config/bundled-skills/media-processing/tools/media-diagnostics.ts +143 -0
  168. package/src/config/bundled-skills/media-processing/tools/media-status.ts +75 -0
  169. package/src/config/bundled-skills/media-processing/tools/query-media-events.ts +65 -0
  170. package/src/config/bundled-skills/messaging/SKILL.md +33 -8
  171. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -7
  172. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +2 -1
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
  174. package/src/config/bundled-skills/phone-calls/SKILL.md +88 -23
  175. package/src/config/bundled-skills/twitter/SKILL.md +19 -3
  176. package/src/config/bundled-skills/twitter/icon.svg +14 -0
  177. package/src/config/bundled-tool-registry.ts +310 -0
  178. package/src/config/calls-schema.ts +181 -0
  179. package/src/config/core-schema.ts +309 -0
  180. package/src/config/defaults.ts +28 -3
  181. package/src/config/env-registry.ts +162 -0
  182. package/src/config/env.ts +175 -0
  183. package/src/config/loader.ts +6 -6
  184. package/src/config/memory-schema.ts +528 -0
  185. package/src/config/sandbox-schema.ts +55 -0
  186. package/src/config/schema.ts +158 -1133
  187. package/src/config/skill-state.ts +1 -1
  188. package/src/config/skills-schema.ts +32 -0
  189. package/src/config/skills.ts +35 -24
  190. package/src/config/system-prompt.ts +131 -56
  191. package/src/config/templates/IDENTITY.md +2 -2
  192. package/src/config/templates/SOUL.md +1 -1
  193. package/src/config/types.ts +1 -0
  194. package/src/config/user-reference.ts +4 -9
  195. package/src/config/vellum-skills/catalog.json +6 -7
  196. package/src/config/vellum-skills/chatgpt-import/tools/chatgpt-import.ts +5 -1
  197. package/src/config/vellum-skills/slack-oauth-setup/SKILL.md +4 -3
  198. package/src/config/vellum-skills/sms-setup/SKILL.md +216 -0
  199. package/src/config/vellum-skills/twilio-setup/SKILL.md +40 -8
  200. package/src/context/window-manager.ts +27 -7
  201. package/src/daemon/approval-generators.ts +186 -0
  202. package/src/daemon/approved-devices-store.ts +140 -0
  203. package/src/daemon/assistant-attachments.ts +1 -1
  204. package/src/daemon/classifier.ts +35 -32
  205. package/src/daemon/config-watcher.ts +1 -1
  206. package/src/daemon/daemon-control.ts +217 -0
  207. package/src/daemon/handlers/apps.ts +2 -3
  208. package/src/daemon/handlers/config-channels.ts +158 -0
  209. package/src/daemon/handlers/config-inbox.ts +540 -0
  210. package/src/daemon/handlers/config-ingress.ts +231 -0
  211. package/src/daemon/handlers/config-integrations.ts +258 -0
  212. package/src/daemon/handlers/config-model.ts +143 -0
  213. package/src/daemon/handlers/config-parental.ts +163 -0
  214. package/src/daemon/handlers/config-scheduling.ts +172 -0
  215. package/src/daemon/handlers/config-slack.ts +92 -0
  216. package/src/daemon/handlers/config-telegram.ts +301 -0
  217. package/src/daemon/handlers/config-tools.ts +177 -0
  218. package/src/daemon/handlers/config-trust.ts +104 -0
  219. package/src/daemon/handlers/config-twilio.ts +1080 -0
  220. package/src/daemon/handlers/config.ts +53 -1689
  221. package/src/daemon/handlers/diagnostics.ts +1 -1
  222. package/src/daemon/handlers/dictation.ts +180 -0
  223. package/src/daemon/handlers/documents.ts +18 -32
  224. package/src/daemon/handlers/identity.ts +14 -23
  225. package/src/daemon/handlers/index.ts +11 -0
  226. package/src/daemon/handlers/misc.ts +3 -5
  227. package/src/daemon/handlers/pairing.ts +98 -0
  228. package/src/daemon/handlers/sessions.ts +56 -5
  229. package/src/daemon/handlers/shared.ts +6 -1
  230. package/src/daemon/handlers/skills.ts +1 -1
  231. package/src/daemon/handlers/twitter-auth.ts +2 -0
  232. package/src/daemon/handlers/work-items.ts +17 -9
  233. package/src/daemon/handlers/workspace-files.ts +4 -3
  234. package/src/daemon/install-cli-launchers.ts +113 -0
  235. package/src/daemon/ipc-contract/apps.ts +356 -0
  236. package/src/daemon/ipc-contract/browser.ts +74 -0
  237. package/src/daemon/ipc-contract/computer-use.ts +151 -0
  238. package/src/daemon/ipc-contract/diagnostics.ts +56 -0
  239. package/src/daemon/ipc-contract/documents.ts +74 -0
  240. package/src/daemon/ipc-contract/inbox.ts +209 -0
  241. package/src/daemon/ipc-contract/integrations.ts +284 -0
  242. package/src/daemon/ipc-contract/memory.ts +48 -0
  243. package/src/daemon/ipc-contract/messages.ts +211 -0
  244. package/src/daemon/ipc-contract/pairing.ts +45 -0
  245. package/src/daemon/ipc-contract/parental-control.ts +95 -0
  246. package/src/daemon/ipc-contract/schedules.ts +97 -0
  247. package/src/daemon/ipc-contract/sessions.ts +315 -0
  248. package/src/daemon/ipc-contract/shared.ts +42 -0
  249. package/src/daemon/ipc-contract/skills.ts +120 -0
  250. package/src/daemon/ipc-contract/subagents.ts +58 -0
  251. package/src/daemon/ipc-contract/surfaces.ts +250 -0
  252. package/src/daemon/ipc-contract/trust.ts +60 -0
  253. package/src/daemon/ipc-contract/work-items.ts +225 -0
  254. package/src/daemon/ipc-contract/workspace.ts +113 -0
  255. package/src/daemon/ipc-contract-inventory.json +70 -0
  256. package/src/daemon/ipc-contract-inventory.ts +55 -29
  257. package/src/daemon/ipc-contract.ts +229 -2426
  258. package/src/daemon/ipc-protocol.ts +1 -1
  259. package/src/daemon/ipc-validate.ts +7 -0
  260. package/src/daemon/lifecycle.ts +97 -377
  261. package/src/daemon/pairing-store.ts +177 -0
  262. package/src/daemon/providers-setup.ts +43 -0
  263. package/src/daemon/ride-shotgun-handler.ts +68 -3
  264. package/src/daemon/server.ts +66 -46
  265. package/src/daemon/session-agent-loop-handlers.ts +421 -0
  266. package/src/daemon/session-agent-loop.ts +117 -275
  267. package/src/daemon/session-dynamic-profile.ts +1 -1
  268. package/src/daemon/session-history.ts +1 -1
  269. package/src/daemon/session-media-retry.ts +1 -1
  270. package/src/daemon/session-messaging.ts +37 -2
  271. package/src/daemon/session-notifiers.ts +5 -25
  272. package/src/daemon/session-process.ts +99 -59
  273. package/src/daemon/session-queue-manager.ts +96 -4
  274. package/src/daemon/session-runtime-assembly.ts +199 -10
  275. package/src/daemon/session-surfaces.ts +19 -4
  276. package/src/daemon/session-tool-setup.ts +30 -30
  277. package/src/daemon/session-workspace.ts +1 -1
  278. package/src/daemon/session.ts +35 -2
  279. package/src/daemon/shutdown-handlers.ts +122 -0
  280. package/src/daemon/trace-emitter.ts +1 -1
  281. package/src/daemon/watch-handler.ts +36 -33
  282. package/src/doordash/cart-queries.ts +787 -0
  283. package/src/doordash/client.ts +144 -127
  284. package/src/doordash/order-queries.ts +85 -0
  285. package/src/doordash/queries.ts +10 -1308
  286. package/src/doordash/search-queries.ts +203 -0
  287. package/src/doordash/session.ts +3 -2
  288. package/src/doordash/store-queries.ts +246 -0
  289. package/src/doordash/types.ts +367 -0
  290. package/src/email/providers/agentmail.ts +2 -1
  291. package/src/email/providers/index.ts +3 -2
  292. package/src/email/service.ts +3 -2
  293. package/src/errors.ts +43 -0
  294. package/src/home-base/prebuilt/seed.ts +1 -1
  295. package/src/hooks/cli.ts +6 -5
  296. package/src/hooks/config.ts +6 -8
  297. package/src/hooks/discovery.ts +6 -5
  298. package/src/hooks/manager.ts +4 -3
  299. package/src/hooks/runner.ts +2 -2
  300. package/src/hooks/templates.ts +5 -5
  301. package/src/inbound/public-ingress-urls.ts +6 -4
  302. package/src/index.ts +4 -2
  303. package/src/influencer/client.ts +1104 -0
  304. package/src/instrument.ts +4 -3
  305. package/src/logfire.ts +4 -3
  306. package/src/memory/admin.ts +25 -35
  307. package/src/memory/attachments-store.ts +4 -7
  308. package/src/memory/channel-delivery-store.ts +30 -1
  309. package/src/memory/channel-guardian-store.ts +202 -2
  310. package/src/memory/clarification-resolver.ts +37 -33
  311. package/src/memory/conflict-store.ts +67 -61
  312. package/src/memory/contradiction-checker.ts +141 -117
  313. package/src/memory/conversation-store.ts +335 -51
  314. package/src/memory/db-connection.ts +27 -4
  315. package/src/memory/db-init.ts +265 -4
  316. package/src/memory/db.ts +14 -1
  317. package/src/memory/embedding-backend.ts +27 -5
  318. package/src/memory/embedding-ollama.ts +2 -1
  319. package/src/memory/entity-extractor.ts +38 -35
  320. package/src/memory/guardian-action-store.ts +430 -0
  321. package/src/memory/inbox-escalation-projection.ts +59 -0
  322. package/src/memory/inbox-thread-store.ts +218 -0
  323. package/src/memory/ingress-invite-store.ts +338 -0
  324. package/src/memory/ingress-member-store.ts +350 -0
  325. package/src/memory/items-extractor.ts +91 -97
  326. package/src/memory/job-handlers/index-maintenance.ts +3 -3
  327. package/src/memory/job-handlers/media-processing.ts +69 -0
  328. package/src/memory/job-handlers/summarization.ts +32 -26
  329. package/src/memory/job-utils.ts +3 -10
  330. package/src/memory/jobs-store.ts +8 -10
  331. package/src/memory/jobs-worker.ts +55 -36
  332. package/src/memory/media-store.ts +759 -0
  333. package/src/memory/migrations/001-job-deferrals.ts +45 -0
  334. package/src/memory/migrations/002-tool-invocations-fk.ts +43 -0
  335. package/src/memory/migrations/003-memory-fts-backfill.ts +24 -0
  336. package/src/memory/migrations/004-entity-relation-dedup.ts +87 -0
  337. package/src/memory/migrations/005-fingerprint-scope-unique.ts +80 -0
  338. package/src/memory/migrations/006-scope-salted-fingerprints.ts +62 -0
  339. package/src/memory/migrations/007-assistant-id-to-self.ts +254 -0
  340. package/src/memory/migrations/008-remove-assistant-id-columns.ts +208 -0
  341. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +83 -0
  342. package/src/memory/migrations/010-ext-conv-bindings-channel-chat-unique.ts +56 -0
  343. package/src/memory/migrations/011-call-sessions-provider-sid-dedup.ts +63 -0
  344. package/src/memory/migrations/012-call-sessions-add-initiated-from.ts +19 -0
  345. package/src/memory/migrations/013-guardian-action-tables.ts +68 -0
  346. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +76 -0
  347. package/src/memory/migrations/015-drop-active-search-index.ts +27 -0
  348. package/src/memory/migrations/016-memory-segments-indexes.ts +11 -0
  349. package/src/memory/migrations/017-memory-items-indexes.ts +10 -0
  350. package/src/memory/migrations/018-remaining-table-indexes.ts +13 -0
  351. package/src/memory/migrations/index.ts +24 -0
  352. package/src/memory/migrations/registry.ts +79 -0
  353. package/src/memory/migrations/validate-migration-state.ts +69 -0
  354. package/src/memory/qdrant-manager.ts +49 -8
  355. package/src/memory/query-builder.ts +1 -1
  356. package/src/memory/raw-query.ts +119 -0
  357. package/src/memory/recall-cache.ts +4 -1
  358. package/src/memory/retriever.ts +165 -47
  359. package/src/memory/schema-migration.ts +25 -984
  360. package/src/memory/schema.ts +228 -7
  361. package/src/memory/search/entity.ts +205 -31
  362. package/src/memory/search/lexical.ts +81 -52
  363. package/src/memory/search/ranking.ts +27 -23
  364. package/src/memory/search/semantic.ts +157 -19
  365. package/src/memory/search/types.ts +24 -0
  366. package/src/memory/shared-app-links-store.ts +4 -5
  367. package/src/memory/validation.ts +19 -0
  368. package/src/messaging/draft-store.ts +5 -6
  369. package/src/messaging/provider-types.ts +2 -0
  370. package/src/messaging/providers/sms/adapter.ts +201 -0
  371. package/src/messaging/providers/sms/client.ts +93 -0
  372. package/src/messaging/providers/sms/types.ts +7 -0
  373. package/src/messaging/providers/telegram-bot/adapter.ts +2 -5
  374. package/src/messaging/providers/whatsapp/adapter.ts +136 -0
  375. package/src/messaging/providers/whatsapp/client.ts +67 -0
  376. package/src/messaging/style-analyzer.ts +5 -4
  377. package/src/messaging/thread-summarizer.ts +61 -69
  378. package/src/messaging/triage-engine.ts +62 -71
  379. package/src/migrations/config-merge.ts +53 -0
  380. package/src/migrations/data-layout.ts +68 -0
  381. package/src/migrations/data-merge.ts +33 -0
  382. package/src/migrations/hooks-merge.ts +90 -0
  383. package/src/migrations/index.ts +6 -0
  384. package/src/migrations/log.ts +23 -0
  385. package/src/migrations/skills-merge.ts +33 -0
  386. package/src/migrations/workspace-layout.ts +79 -0
  387. package/src/permissions/checker.ts +133 -11
  388. package/src/permissions/prompter.ts +14 -0
  389. package/src/permissions/shell-identity.ts +31 -1
  390. package/src/permissions/trust-store.ts +21 -1
  391. package/src/providers/anthropic/client.ts +4 -4
  392. package/src/providers/failover.ts +2 -2
  393. package/src/providers/model-intents.ts +70 -0
  394. package/src/providers/ollama/client.ts +2 -1
  395. package/src/providers/provider-send-message.ts +176 -0
  396. package/src/providers/registry.ts +71 -30
  397. package/src/providers/retry.ts +35 -1
  398. package/src/providers/types.ts +12 -1
  399. package/src/runtime/approval-conversation-turn.ts +97 -0
  400. package/src/runtime/approval-message-composer.ts +253 -0
  401. package/src/runtime/channel-approval-parser.ts +36 -2
  402. package/src/runtime/channel-approvals.ts +11 -24
  403. package/src/runtime/channel-guardian-service.ts +88 -21
  404. package/src/runtime/channel-readiness-service.ts +418 -0
  405. package/src/runtime/channel-readiness-types.ts +35 -0
  406. package/src/runtime/channel-retry-sweep.ts +184 -0
  407. package/src/runtime/guardian-context-resolver.ts +108 -0
  408. package/src/runtime/http-server.ts +275 -717
  409. package/src/runtime/http-types.ts +59 -3
  410. package/src/runtime/middleware/auth.ts +116 -0
  411. package/src/runtime/middleware/error-handler.ts +33 -0
  412. package/src/runtime/middleware/twilio-validation.ts +127 -0
  413. package/src/runtime/routes/app-routes.ts +1 -1
  414. package/src/runtime/routes/call-routes.ts +51 -7
  415. package/src/runtime/routes/channel-delivery-routes.ts +170 -0
  416. package/src/runtime/routes/channel-guardian-routes.ts +1191 -0
  417. package/src/runtime/routes/channel-inbound-routes.ts +1152 -0
  418. package/src/runtime/routes/channel-route-shared.ts +144 -0
  419. package/src/runtime/routes/channel-routes.ts +32 -1588
  420. package/src/runtime/routes/conversation-routes.ts +50 -7
  421. package/src/runtime/routes/events-routes.ts +2 -2
  422. package/src/runtime/routes/identity-routes.ts +126 -0
  423. package/src/runtime/routes/pairing-routes.ts +143 -0
  424. package/src/runtime/routes/run-routes.ts +15 -1
  425. package/src/runtime/run-orchestrator.ts +86 -35
  426. package/src/schedule/schedule-store.ts +36 -32
  427. package/src/schedule/scheduler.ts +3 -3
  428. package/src/security/encrypted-store.ts +5 -7
  429. package/src/security/oauth2.ts +45 -15
  430. package/src/security/parental-control-store.ts +183 -0
  431. package/src/security/secret-allowlist.ts +4 -3
  432. package/src/security/secret-scanner.ts +5 -5
  433. package/src/security/secure-keys.ts +1 -1
  434. package/src/security/token-manager.ts +3 -2
  435. package/src/services/vercel-deploy.ts +6 -2
  436. package/src/skills/tool-manifest.ts +3 -3
  437. package/src/skills/vellum-catalog-remote.ts +75 -16
  438. package/src/slack/slack-webhook.ts +2 -1
  439. package/src/swarm/orchestrator.ts +92 -1
  440. package/src/swarm/router-planner.ts +6 -9
  441. package/src/swarm/worker-prompts.ts +9 -12
  442. package/src/tasks/task-compiler.ts +19 -28
  443. package/src/tasks/task-runner.ts +1 -1
  444. package/src/tools/assets/materialize.ts +2 -2
  445. package/src/tools/assets/search.ts +15 -14
  446. package/src/tools/browser/__tests__/auth-detector.test.ts +1 -0
  447. package/src/tools/browser/auto-navigate.ts +1 -0
  448. package/src/tools/browser/browser-execution.ts +10 -1
  449. package/src/tools/browser/browser-manager.ts +119 -4
  450. package/src/tools/browser/network-recorder.ts +5 -0
  451. package/src/tools/calls/call-start.ts +1 -0
  452. package/src/tools/credentials/broker.ts +11 -2
  453. package/src/tools/credentials/metadata-store.ts +18 -14
  454. package/src/tools/credentials/post-connect-hooks.ts +61 -0
  455. package/src/tools/credentials/vault.ts +49 -23
  456. package/src/tools/execution-target.ts +11 -1
  457. package/src/tools/executor.ts +68 -9
  458. package/src/tools/host-terminal/cli-discover.ts +1 -1
  459. package/src/tools/network/script-proxy/http-forwarder.ts +1 -1
  460. package/src/tools/network/script-proxy/mitm-handler.ts +1 -1
  461. package/src/tools/network/script-proxy/server.ts +1 -1
  462. package/src/tools/network/script-proxy/session-manager.ts +6 -5
  463. package/src/tools/network/web-fetch.ts +18 -2
  464. package/src/tools/network/web-search.ts +8 -4
  465. package/src/tools/reminder/reminder-store.ts +14 -15
  466. package/src/tools/schedule/create.ts +1 -0
  467. package/src/tools/schedule/list.ts +2 -1
  468. package/src/tools/shared/filesystem/file-ops-service.ts +5 -7
  469. package/src/tools/skills/skill-script-runner.ts +24 -9
  470. package/src/tools/skills/skill-tool-factory.ts +1 -0
  471. package/src/tools/tasks/work-item-enqueue.ts +2 -2
  472. package/src/tools/terminal/evaluate-typescript.ts +21 -12
  473. package/src/tools/terminal/parser.ts +50 -0
  474. package/src/tools/types.ts +2 -0
  475. package/src/tools/watcher/delete.ts +6 -0
  476. package/src/tools/weather/service.ts +1 -1
  477. package/src/twitter/client.ts +190 -24
  478. package/src/twitter/router.ts +1 -1
  479. package/src/twitter/session.ts +4 -3
  480. package/src/util/clipboard.ts +1 -1
  481. package/src/util/errors.ts +65 -8
  482. package/src/util/fs.ts +40 -0
  483. package/src/util/json.ts +10 -0
  484. package/src/util/log-redact.ts +189 -0
  485. package/src/util/logger.ts +19 -17
  486. package/src/util/object.ts +3 -0
  487. package/src/util/platform.ts +105 -363
  488. package/src/util/pricing.ts +1 -1
  489. package/src/util/promise-guard.ts +1 -1
  490. package/src/util/retry.ts +19 -0
  491. package/src/util/row-mapper.ts +79 -0
  492. package/src/util/silently.ts +21 -0
  493. package/src/watcher/engine.ts +5 -1
  494. package/src/watcher/provider-types.ts +20 -0
  495. package/src/watcher/providers/github.ts +156 -0
  496. package/src/watcher/providers/gmail.ts +1 -0
  497. package/src/watcher/providers/google-calendar.ts +1 -0
  498. package/src/watcher/providers/linear.ts +460 -0
  499. package/src/watcher/providers/slack.ts +1 -0
  500. package/src/work-items/work-item-runner.ts +1 -1
  501. package/src/workspace/git-service.ts +1 -1
  502. package/src/workspace/provider-commit-message-generator.ts +51 -22
  503. package/src/__tests__/call-bridge.test.ts +0 -517
  504. package/src/__tests__/session-process-bridge.test.ts +0 -244
  505. package/src/calls/call-bridge.ts +0 -168
  506. package/src/config/vellum-skills/google-oauth-setup/SKILL.md +0 -199
@@ -0,0 +1,1418 @@
1
+ /**
2
+ * Amazon REST API client.
3
+ *
4
+ * ARCHITECTURE
5
+ * ============
6
+ * All requests run inside a Chrome browser tab via CDP Runtime.evaluate(), NOT
7
+ * from Node.js directly. This means:
8
+ *
9
+ * 1. There are TWO DOMs in every function:
10
+ * - `document` = the browser's currently-rendered page (e.g. amazon.com homepage)
11
+ * - `doc` = a DOMParser-parsed document from a fetch() response (e.g. product page)
12
+ * CSRF tokens, offer IDs, and form fields must be extracted from `doc` (the fetched
13
+ * page), NOT from `document`. The browser's live page rarely has the data we need.
14
+ *
15
+ * 2. Session cookies live in the Chrome-CDP browser profile
16
+ * (~Library/Application Support/Google/Chrome-CDP). The session.json on disk is only
17
+ * used to validate that a session exists. Actual auth goes through the browser's cookies.
18
+ *
19
+ * AMAZON FRESH vs REGULAR CART
20
+ * ============================
21
+ * Fresh and regular Amazon use completely different cart APIs:
22
+ * - Fresh: POST /alm/addtofreshcart (JSON body)
23
+ * - Regular: POST /gp/add-to-cart/json (form-encoded body)
24
+ *
25
+ * Fresh cart POST requires ALL of these fields or it silently fails:
26
+ * - `offerListingDiscriminator` (short code like "A0P3", from escaped JSON in product HTML)
27
+ * - `offerListingID` (long URL-encoded hash, from escaped JSON in product HTML)
28
+ * - `anti-csrftoken-a2z` header (from <input> or <meta> in the FETCHED product page doc)
29
+ * - `csrfToken` in payload (from escaped JSON in product HTML)
30
+ *
31
+ * DEBUGGING
32
+ * =========
33
+ * Use `--verbose` on `cart add` to dump all extracted fields and raw responses.
34
+ * If a field shows "EMPTY", the product page format likely changed and the
35
+ * extraction regex needs updating. Check the escaped JSON patterns in the HTML:
36
+ * - Fields are typically in: \\"fieldName\\":\\"value\\" (backslash-escaped quotes)
37
+ * - Or HTML entities: &quot;fieldName&quot;:&quot;value&quot;
38
+ *
39
+ * ERROR HANDLING
40
+ * ==============
41
+ * NEVER silently fall through to stale data. If a POST fails, throw an error with
42
+ * the extracted field values so the caller knows exactly what went wrong. The
43
+ * get-cart-items fallback endpoint returns whatever is already in the cart, NOT
44
+ * what was just added. Always validate the target ASIN is present before returning.
45
+ *
46
+ * runWithBackoff() retries on HTTP 403, but not all 403s are rate limits. A 403
47
+ * from /alm/addtofreshcart with "fakeOfferId" means the request payload was wrong,
48
+ * not that we're rate-limited. Check the response body before classifying the error.
49
+ */
50
+
51
+ import {
52
+ loadSession,
53
+ type AmazonSession,
54
+ } from './session.js';
55
+ import type { ExtractedCredential } from '../tools/browser/network-recording-types.js';
56
+ import { extensionRelayServer } from '../browser-extension-relay/server.js';
57
+ import type { ExtensionCommand, ExtensionResponse } from '../browser-extension-relay/protocol.js';
58
+ import { readHttpToken } from '../util/platform.js';
59
+ import { getRuntimeHttpPort } from '../config/env.js';
60
+
61
+ const AMAZON_BASE = 'https://www.amazon.com';
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Relay command routing
65
+ // ---------------------------------------------------------------------------
66
+ // When running inside the daemon, extensionRelayServer has a live WebSocket.
67
+ // When running out-of-process (CLI), the relay isn't available, so we fall
68
+ // back to the daemon's HTTP endpoint POST /v1/browser-relay/command.
69
+ // ---------------------------------------------------------------------------
70
+
71
+ async function sendRelayCommand(command: Record<string, unknown>): Promise<ExtensionResponse> {
72
+ // Try in-process relay first (works when running inside the daemon)
73
+ const status = extensionRelayServer.getStatus();
74
+ if (status.connected) {
75
+ return extensionRelayServer.sendCommand(command as Omit<ExtensionCommand, 'id'>);
76
+ }
77
+
78
+ // Fall back to HTTP relay endpoint on the daemon
79
+ const token = readHttpToken();
80
+ if (!token) {
81
+ throw new Error('Browser extension relay is not connected and no HTTP token found. Is the daemon running?');
82
+ }
83
+
84
+ const port = getRuntimeHttpPort() ?? 7821;
85
+ const resp = await fetch(`http://127.0.0.1:${port}/v1/browser-relay/command`, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'Authorization': `Bearer ${token}`,
90
+ },
91
+ body: JSON.stringify(command),
92
+ });
93
+
94
+ if (!resp.ok) {
95
+ const body = await resp.text();
96
+ throw new Error(`Relay HTTP command failed (${resp.status}): ${body}`);
97
+ }
98
+
99
+ return await resp.json() as ExtensionResponse;
100
+ }
101
+
102
+ /** Thrown when the session is missing or expired. The CLI handles this specially. */
103
+ export class SessionExpiredError extends Error {
104
+ constructor(reason: string) {
105
+ super(reason);
106
+ this.name = 'SessionExpiredError';
107
+ }
108
+ }
109
+
110
+ /** Thrown when Amazon returns HTTP 403 (rate limited or bot detected). */
111
+ export class RateLimitError extends Error {
112
+ constructor(reason: string) {
113
+ super(reason);
114
+ this.name = 'RateLimitError';
115
+ }
116
+ }
117
+
118
+ function requireSession(): AmazonSession {
119
+ const session = loadSession();
120
+ if (!session) {
121
+ throw new SessionExpiredError('No Amazon session found.');
122
+ }
123
+ return session;
124
+ }
125
+
126
+ /**
127
+ * Prepare for an Amazon request: validate session, find a Chrome tab,
128
+ * and sync session cookies into the browser. Returns the tab ID.
129
+ */
130
+ async function prepareRequest(): Promise<{ tabId: number; session: AmazonSession }> {
131
+ const session = requireSession();
132
+ const tabId = await findAmazonTab();
133
+ // Skip cookie sync — use Chrome's own live cookies instead of overwriting with stale CLI ones
134
+ // await syncCookiesToBrowser(session.cookies);
135
+ return { tabId, session };
136
+ }
137
+
138
+ /**
139
+ * Find a Chrome tab on amazon.com via the browser-relay extension.
140
+ * Opens a new Amazon tab if none is currently open.
141
+ */
142
+ async function findAmazonTab(): Promise<number> {
143
+ const resp = await sendRelayCommand({ action: 'find_tab', url: '*://*.amazon.com/*' });
144
+ if (resp.success && resp.tabId !== undefined) {
145
+ return resp.tabId;
146
+ }
147
+
148
+ // No Amazon tab open — create one
149
+ const newTab = await sendRelayCommand({
150
+ action: 'new_tab',
151
+ url: 'https://www.amazon.com',
152
+ });
153
+ if (!newTab.success || newTab.tabId === undefined) {
154
+ throw new SessionExpiredError('Could not open an Amazon tab in Chrome.');
155
+ }
156
+ return newTab.tabId;
157
+ }
158
+
159
+ /**
160
+ * Inject saved session cookies into Chrome via the browser-relay extension.
161
+ * Uses chrome.cookies.set so fetch() calls in the tab context carry the session.
162
+ */
163
+ let lastCookieSyncTime = 0;
164
+ const COOKIE_SYNC_INTERVAL = 60_000; // re-sync at most once per minute
165
+
166
+ async function _syncCookiesToBrowser(cookies: ExtractedCredential[]): Promise<void> {
167
+ const now = Date.now();
168
+ if (now - lastCookieSyncTime < COOKIE_SYNC_INTERVAL) return;
169
+
170
+ for (const cookie of cookies) {
171
+ const domain = cookie.domain || '.amazon.com';
172
+ const cleanDomain = domain.startsWith('.') ? domain.slice(1) : domain;
173
+ await extensionRelayServer.sendCommand({
174
+ action: 'set_cookie',
175
+ cookie: {
176
+ url: `https://${cleanDomain}`,
177
+ name: cookie.name,
178
+ value: cookie.value,
179
+ domain,
180
+ path: cookie.path || '/',
181
+ secure: cookie.secure ?? true,
182
+ httpOnly: cookie.httpOnly ?? false,
183
+ ...(cookie.expires ? { expirationDate: cookie.expires } : {}),
184
+ },
185
+ });
186
+ }
187
+
188
+ lastCookieSyncTime = Date.now();
189
+ }
190
+
191
+ /**
192
+ * Execute a JavaScript expression inside a Chrome tab via the browser-relay extension.
193
+ * Drop-in replacement for the former CDP Runtime.evaluate path.
194
+ * Returns the JSON-parsed result value.
195
+ */
196
+ async function cdpEval(tabId: number, script: string): Promise<unknown> {
197
+ let resp: ExtensionResponse;
198
+ try {
199
+ resp = await sendRelayCommand({ action: 'evaluate', tabId, code: script });
200
+ } catch (err) {
201
+ const msg = err instanceof Error ? err.message : String(err);
202
+ if (msg.includes('not connected')) {
203
+ throw new SessionExpiredError(
204
+ 'Browser extension relay is not connected. Load the Vellum extension in Chrome.',
205
+ );
206
+ }
207
+ throw err;
208
+ }
209
+
210
+ if (!resp.success) {
211
+ throw new Error(`Browser eval failed: ${resp.error ?? 'unknown error'}`);
212
+ }
213
+
214
+ const value = resp.result;
215
+ if (value == null) {
216
+ throw new Error('Empty browser eval response');
217
+ }
218
+
219
+ try {
220
+ return typeof value === 'string' ? JSON.parse(value) : value;
221
+ } catch {
222
+ return value;
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Handle the raw result object returned from cdpEval scripts.
228
+ * Throws appropriate errors for auth failures, rate limits, and other errors.
229
+ */
230
+ function handleResult(result: Record<string, unknown>): void {
231
+ if (result.__error) {
232
+ if (result.__status === 401) {
233
+ throw new SessionExpiredError('Amazon session has expired.');
234
+ }
235
+ if (result.__status === 403) {
236
+ throw new RateLimitError('Amazon rate limit hit (HTTP 403).');
237
+ }
238
+ throw new Error(
239
+ (result.__message as string | undefined) ??
240
+ `Amazon request failed with status ${result.__status ?? 'unknown'}`,
241
+ );
242
+ }
243
+ }
244
+
245
+ let lastRequestTime = 0;
246
+
247
+ async function runWithBackoff<T>(fn: () => Promise<T>): Promise<T> {
248
+ const backoffSchedule = [5000, 10000, 20000];
249
+
250
+ for (let attempt = 0; ; attempt++) {
251
+ // Inter-request delay
252
+ const now = Date.now();
253
+ const elapsed = now - lastRequestTime;
254
+ if (lastRequestTime > 0 && elapsed < 2000) {
255
+ await new Promise(r => setTimeout(r, 2000 - elapsed));
256
+ }
257
+
258
+ try {
259
+ lastRequestTime = Date.now();
260
+ return await fn();
261
+ } catch (err) {
262
+ if (err instanceof RateLimitError && attempt < backoffSchedule.length) {
263
+ const delay = backoffSchedule[attempt];
264
+ process.stderr.write(
265
+ `[amazon] Rate limited, retrying in ${delay / 1000}s... (attempt ${attempt + 1}/${backoffSchedule.length})\n`,
266
+ );
267
+ await new Promise(r => setTimeout(r, delay));
268
+ continue;
269
+ }
270
+ throw err;
271
+ }
272
+ }
273
+ }
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // Public types
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export interface ProductSearchResult {
280
+ asin: string;
281
+ title: string;
282
+ price: string;
283
+ priceValue: number | null;
284
+ isPrime: boolean;
285
+ isFresh: boolean;
286
+ imageUrl?: string;
287
+ rating?: string;
288
+ reviewCount?: string;
289
+ }
290
+
291
+ export interface ProductVariation {
292
+ dimensionName: string;
293
+ value: string;
294
+ asin: string;
295
+ isAvailable: boolean;
296
+ priceValue: number | null;
297
+ }
298
+
299
+ export interface ProductDetails {
300
+ asin: string;
301
+ parentAsin?: string;
302
+ title: string;
303
+ price: string;
304
+ priceValue: number | null;
305
+ variations: ProductVariation[];
306
+ isFresh: boolean;
307
+ imageUrl?: string;
308
+ rating?: string;
309
+ reviewCount?: string;
310
+ }
311
+
312
+ export interface CartItem {
313
+ cartItemId: string;
314
+ asin: string;
315
+ title: string;
316
+ quantity: number;
317
+ price: string;
318
+ isFresh: boolean;
319
+ }
320
+
321
+ export interface CartSummary {
322
+ items: CartItem[];
323
+ subtotal: string;
324
+ itemCount: number;
325
+ }
326
+
327
+ export interface DeliverySlot {
328
+ slotId: string;
329
+ date: string;
330
+ timeWindow: string;
331
+ price: string;
332
+ isAvailable: boolean;
333
+ }
334
+
335
+ export interface PaymentMethod {
336
+ paymentMethodId: string;
337
+ type: string;
338
+ last4: string;
339
+ isDefault: boolean;
340
+ }
341
+
342
+ export interface CheckoutSummary {
343
+ subtotal: string;
344
+ shipping: string;
345
+ tax: string;
346
+ total: string;
347
+ paymentMethods: PaymentMethod[];
348
+ deliveryDate?: string;
349
+ }
350
+
351
+ export interface PlaceOrderResult {
352
+ orderId: string;
353
+ estimatedDelivery?: string;
354
+ }
355
+
356
+ // ---------------------------------------------------------------------------
357
+ // Public API
358
+ // ---------------------------------------------------------------------------
359
+
360
+ /**
361
+ * Search Amazon for products.
362
+ * Use isFresh: true to search Amazon Fresh grocery items.
363
+ */
364
+ export async function search(
365
+ query: string,
366
+ opts: { isFresh?: boolean; limit?: number } = {},
367
+ ): Promise<ProductSearchResult[]> {
368
+ const { tabId } = await prepareRequest();
369
+
370
+ return runWithBackoff(async () => {
371
+ const url = opts.isFresh
372
+ ? `${AMAZON_BASE}/s?k=${encodeURIComponent(query)}&i=fresh-foods`
373
+ : `${AMAZON_BASE}/s?k=${encodeURIComponent(query)}`;
374
+ const limit = opts.limit ?? 20;
375
+ const isFreshFlag = JSON.stringify(!!opts.isFresh);
376
+
377
+ const script = `
378
+ (async function() {
379
+ try {
380
+ var resp = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
381
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
382
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
383
+ var html = await resp.text();
384
+ var parser = new DOMParser();
385
+ var doc = parser.parseFromString(html, 'text/html');
386
+ var results = [];
387
+ var cards = doc.querySelectorAll('[data-component-type="s-search-result"][data-asin]');
388
+ for (var i = 0; i < Math.min(cards.length, ${limit}); i++) {
389
+ var el = cards[i];
390
+ var asin = el.getAttribute('data-asin');
391
+ if (!asin || asin.length < 6) continue;
392
+ var titleEl = el.querySelector('h2 .a-text-normal') || el.querySelector('h2 a span') || el.querySelector('.s-title-instructions-style');
393
+ var priceEl = el.querySelector('.a-price .a-offscreen');
394
+ var imgEl = el.querySelector('img.s-image');
395
+ var ratingEl = el.querySelector('.a-icon-star-small .a-icon-alt') || el.querySelector('[aria-label*="stars"]');
396
+ var reviewEl = el.querySelector('[aria-label*="reviews"]') || el.querySelector('.s-underline-text');
397
+ var isPrime = !!el.querySelector('.a-icon-prime');
398
+ var isFreshEl = !!el.querySelector('[aria-label*="Fresh"]') || html.includes('amazon.com/fresh') && i < 5;
399
+ var priceText = priceEl ? priceEl.textContent.trim() : '';
400
+ var priceNum = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : null;
401
+ results.push({
402
+ asin: asin,
403
+ title: titleEl ? titleEl.textContent.trim() : '',
404
+ price: priceText,
405
+ priceValue: isNaN(priceNum) ? null : priceNum,
406
+ isPrime: isPrime,
407
+ isFresh: ${isFreshFlag} || isFreshEl,
408
+ imageUrl: imgEl ? imgEl.getAttribute('src') : undefined,
409
+ rating: ratingEl ? ratingEl.getAttribute('aria-label') || ratingEl.textContent.trim() : undefined,
410
+ reviewCount: reviewEl ? reviewEl.textContent.trim() : undefined,
411
+ });
412
+ }
413
+ return JSON.stringify({ __status: resp.status, __data: results });
414
+ } catch(e) {
415
+ return JSON.stringify({ __error: true, __message: e.message });
416
+ }
417
+ })()
418
+ `;
419
+
420
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
421
+ handleResult(result);
422
+ return result.__data as ProductSearchResult[];
423
+ });
424
+ }
425
+
426
+ /**
427
+ * Get product details for a specific ASIN, including variations.
428
+ */
429
+ export async function getProductDetails(
430
+ asin: string,
431
+ opts: { isFresh?: boolean } = {},
432
+ ): Promise<ProductDetails> {
433
+ const { tabId } = await prepareRequest();
434
+
435
+ return runWithBackoff(async () => {
436
+ const url = `${AMAZON_BASE}/dp/${asin}`;
437
+ const isFreshFlag = JSON.stringify(!!opts.isFresh);
438
+
439
+ const script = `
440
+ (async function() {
441
+ try {
442
+ var resp = await fetch(${JSON.stringify(url)}, {
443
+ credentials: 'include',
444
+ headers: { 'Accept': 'text/html,application/xhtml+xml' }
445
+ });
446
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
447
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
448
+ var html = await resp.text();
449
+ var parser = new DOMParser();
450
+ var doc = parser.parseFromString(html, 'text/html');
451
+
452
+ // Title
453
+ var titleEl = doc.getElementById('productTitle') ||
454
+ doc.querySelector('.product-title-word-break') ||
455
+ doc.querySelector('h1#title');
456
+ var title = titleEl ? titleEl.textContent.trim() : '';
457
+
458
+ // Price
459
+ var priceEl = doc.querySelector('#priceblock_ourprice .a-offscreen') ||
460
+ doc.querySelector('#priceblock_dealprice .a-offscreen') ||
461
+ doc.querySelector('.a-price .a-offscreen') ||
462
+ doc.querySelector('#price_inside_buybox');
463
+ var priceText = priceEl ? priceEl.textContent.trim() : '';
464
+ var priceNum = priceText ? parseFloat(priceText.replace(/[^0-9.]/g, '')) : null;
465
+
466
+ // Image
467
+ var imgEl = doc.getElementById('landingImage') || doc.querySelector('#imgBlkFront');
468
+ var imageUrl = imgEl ? (imgEl.getAttribute('data-a-dynamic-image') ? Object.keys(JSON.parse(imgEl.getAttribute('data-a-dynamic-image') || '{}'))[0] : imgEl.getAttribute('src')) : undefined;
469
+
470
+ // Rating
471
+ var ratingEl = doc.querySelector('#acrPopover') || doc.querySelector('[data-hook="rating-out-of-text"]');
472
+ var rating = ratingEl ? ratingEl.getAttribute('title') || ratingEl.textContent.trim() : undefined;
473
+
474
+ var reviewEl = doc.getElementById('acrCustomerReviewText');
475
+ var reviewCount = reviewEl ? reviewEl.textContent.trim() : undefined;
476
+
477
+ // Parent ASIN (for variation child products)
478
+ var parentAsinEl = doc.querySelector('[data-asin]') || doc.querySelector('[name="ASIN"]');
479
+ var parentAsin = undefined;
480
+ var m = html.match(/"parentAsin"\s*:\s*"([A-Z0-9]+)"/);
481
+ if (m) parentAsin = m[1];
482
+
483
+ // Detect Fresh
484
+ var isFresh = ${isFreshFlag} || html.includes('amazon.com/fresh') || !!doc.querySelector('[aria-label*="Fresh"]');
485
+
486
+ // Variations — parse from inline JS objects
487
+ var variations = [];
488
+ try {
489
+ var dimMatch = html.match(/dimensionRelationshipsStr\s*=\s*'([^']+)'/);
490
+ if (!dimMatch) dimMatch = html.match(/"dimensionRelationshipsStr"\s*:\s*"([^"]+)"/);
491
+ if (dimMatch) {
492
+ var dimStr = dimMatch[1].replace(/\\\\/g, '\\\\').replace(/\\'/g, "'");
493
+ var dimData = JSON.parse(dimStr);
494
+ if (Array.isArray(dimData)) {
495
+ for (var i = 0; i < dimData.length; i++) {
496
+ var dimItem = dimData[i];
497
+ var attrs = dimItem.variationAttributes || [];
498
+ for (var j = 0; j < attrs.length; j++) {
499
+ variations.push({
500
+ dimensionName: attrs[j].variationName || '',
501
+ value: attrs[j].value || '',
502
+ asin: dimItem.asin || '',
503
+ isAvailable: dimItem.isPrime !== undefined ? true : !dimItem.unavailable,
504
+ priceValue: dimItem.price ? parseFloat(String(dimItem.price).replace(/[^0-9.]/g, '')) : null,
505
+ });
506
+ }
507
+ }
508
+ }
509
+ }
510
+ } catch(ve) { /* skip variation parsing errors */ }
511
+
512
+ // Fallback: look for asinVariationValues
513
+ if (variations.length === 0) {
514
+ try {
515
+ var asinVarMatch = html.match(/asinVariationValues\s*=\s*(\{[^;]+\})/);
516
+ if (asinVarMatch) {
517
+ var asinVarData = JSON.parse(asinVarMatch[1]);
518
+ var dims = Object.keys(asinVarData);
519
+ for (var di = 0; di < dims.length; di++) {
520
+ var dim = dims[di];
521
+ var asins = Object.keys(asinVarData[dim]);
522
+ for (var ai = 0; ai < asins.length; ai++) {
523
+ variations.push({
524
+ dimensionName: dim,
525
+ value: asinVarData[dim][asins[ai]],
526
+ asin: asins[ai],
527
+ isAvailable: true,
528
+ priceValue: null,
529
+ });
530
+ }
531
+ }
532
+ }
533
+ } catch(ve2) { /* skip */ }
534
+ }
535
+
536
+ return JSON.stringify({
537
+ __status: resp.status,
538
+ __data: {
539
+ asin: ${JSON.stringify(asin)},
540
+ parentAsin: parentAsin,
541
+ title: title,
542
+ price: priceText,
543
+ priceValue: isNaN(priceNum) ? null : priceNum,
544
+ variations: variations,
545
+ isFresh: isFresh,
546
+ imageUrl: imageUrl,
547
+ rating: rating,
548
+ reviewCount: reviewCount,
549
+ }
550
+ });
551
+ } catch(e) {
552
+ return JSON.stringify({ __error: true, __message: e.message });
553
+ }
554
+ })()
555
+ `;
556
+
557
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
558
+ handleResult(result);
559
+ return result.__data as ProductDetails;
560
+ });
561
+ }
562
+
563
+ /**
564
+ * Add an item to the Amazon cart.
565
+ * First fetches the product page to extract the offerListingID required by Amazon.
566
+ */
567
+ export async function addToCart(opts: {
568
+ asin: string;
569
+ quantity?: number;
570
+ isFresh?: boolean;
571
+ verbose?: boolean;
572
+ }): Promise<CartSummary> {
573
+ const { tabId } = await prepareRequest();
574
+ const quantity = opts.quantity ?? 1;
575
+ const productUrl = `${AMAZON_BASE}/dp/${opts.asin}`;
576
+
577
+ // ─── Non-Fresh: navigate + click approach ───────────────────────────
578
+ // Amazon's handle-buy-box endpoint rejects fetch() requests (returns 404)
579
+ // because it checks Sec-Fetch-* headers that only real browser form
580
+ // submissions provide. So for non-Fresh items we navigate the actual
581
+ // Chrome tab to the product page and click the Add to Cart button.
582
+ if (!opts.isFresh) {
583
+ return runWithBackoff(async () => {
584
+ // Step 1: Navigate to the product page
585
+ await sendRelayCommand({ action: 'navigate', tabId, url: productUrl });
586
+
587
+ // Step 2: Wait for the page to load and the Add to Cart button to appear
588
+ // Poll up to 10 seconds for the button
589
+ let buttonClicked = false;
590
+ for (let attempt = 0; attempt < 10; attempt++) {
591
+ await new Promise(r => setTimeout(r, 1000));
592
+ const clickResult = await cdpEval(tabId, `
593
+ (function() {
594
+ try {
595
+ // Check if we're on the right product page
596
+ var titleEl = document.querySelector('#productTitle');
597
+ if (!titleEl) return JSON.stringify({ ready: false, reason: 'no product title yet' });
598
+
599
+ // Set quantity if needed
600
+ ${quantity > 1 ? `
601
+ var qtySelect = document.querySelector('#quantity');
602
+ if (qtySelect) {
603
+ qtySelect.value = '${quantity}';
604
+ qtySelect.dispatchEvent(new Event('change', { bubbles: true }));
605
+ }
606
+ ` : ''}
607
+
608
+ // Find and click the Add to Cart button
609
+ var btn = document.querySelector('#add-to-cart-button')
610
+ || document.querySelector('input[name="submit.add-to-cart"]')
611
+ || document.querySelector('#submit\\.add-to-cart');
612
+ if (!btn) return JSON.stringify({ ready: true, clicked: false, reason: 'no add-to-cart button found' });
613
+
614
+ btn.click();
615
+ return JSON.stringify({ ready: true, clicked: true, buttonId: btn.id || btn.name });
616
+ } catch(e) {
617
+ return JSON.stringify({ ready: false, reason: e.message });
618
+ }
619
+ })()
620
+ `) as Record<string, unknown>;
621
+
622
+ if (clickResult && clickResult.clicked) {
623
+ buttonClicked = true;
624
+ break;
625
+ }
626
+ }
627
+
628
+ if (!buttonClicked) {
629
+ throw new Error('Could not find or click the Add to Cart button on the product page after 10 seconds.');
630
+ }
631
+
632
+ // Step 3: Wait for the cart confirmation page to load and extract cart info
633
+ // Poll up to 8 seconds for the confirmation
634
+ await new Promise(r => setTimeout(r, 2000)); // initial wait for navigation
635
+ let cartData: Record<string, unknown> | null = null;
636
+ for (let attempt = 0; attempt < 6; attempt++) {
637
+ const confirmResult = await cdpEval(tabId, `
638
+ (function() {
639
+ try {
640
+ var title = document.title || '';
641
+ // Check for cart/confirmation page indicators
642
+ var confirmEl = document.querySelector('#huc-v2-order-row-confirm-text')
643
+ || document.querySelector('#NATC_SMART_WAGON_CONF_MSG_SUCCESS')
644
+ || document.querySelector('#sw-atc-confirmation')
645
+ || document.querySelector('.a-alert-heading');
646
+ var isCartPage = title.toLowerCase().indexOf('cart') !== -1
647
+ || title.toLowerCase().indexOf('added') !== -1;
648
+ var confirmText = confirmEl ? confirmEl.textContent.trim().substring(0, 100) : '';
649
+ var isConfirmed = confirmText.toLowerCase().indexOf('added') !== -1 || isCartPage;
650
+
651
+ if (!isConfirmed) return JSON.stringify({ confirmed: false, pageTitle: title });
652
+
653
+ // Extract cart count
654
+ var cartCountEl = document.querySelector('#nav-cart-count');
655
+ var cartCount = cartCountEl ? cartCountEl.textContent.trim() : '0';
656
+
657
+ // Extract subtotal if visible
658
+ var subtotalEl = document.querySelector('#sc-subtotal-amount-activecart')
659
+ || document.querySelector('.a-text-bold .sc-price');
660
+ var subtotal = subtotalEl ? subtotalEl.textContent.trim() : '';
661
+
662
+ return JSON.stringify({
663
+ confirmed: true,
664
+ pageTitle: title,
665
+ confirmText: confirmText.substring(0, 50),
666
+ cartCount: cartCount,
667
+ subtotal: subtotal,
668
+ });
669
+ } catch(e) {
670
+ return JSON.stringify({ confirmed: false, reason: e.message });
671
+ }
672
+ })()
673
+ `) as Record<string, unknown>;
674
+
675
+ if (confirmResult && confirmResult.confirmed) {
676
+ cartData = confirmResult;
677
+ break;
678
+ }
679
+ await new Promise(r => setTimeout(r, 1000));
680
+ }
681
+
682
+ // Build the cart summary
683
+ const items = [{
684
+ cartItemId: opts.asin,
685
+ asin: opts.asin,
686
+ title: '',
687
+ quantity: quantity,
688
+ price: '',
689
+ isFresh: false,
690
+ }];
691
+
692
+ const cart: CartSummary & { __debug?: unknown; __verbose?: unknown } = {
693
+ items,
694
+ subtotal: (cartData?.subtotal as string) || '',
695
+ itemCount: parseInt((cartData?.cartCount as string) || '0', 10) || items.length,
696
+ };
697
+
698
+ if (!cartData?.confirmed) {
699
+ // Button was clicked but we couldn't confirm. It likely still worked
700
+ // (Amazon sometimes shows interstitials). Return optimistic result.
701
+ cart.__debug = { warning: 'Could not confirm cart page, but button click succeeded.' };
702
+ } else {
703
+ cart.__debug = {
704
+ confirmText: cartData.confirmText,
705
+ cartCount: cartData.cartCount,
706
+ pageTitle: cartData.pageTitle,
707
+ };
708
+ }
709
+
710
+ return cart;
711
+ });
712
+ }
713
+
714
+ // ─── Fresh items: fetch-based approach (works fine) ─────────────────
715
+ return runWithBackoff(async () => {
716
+ const script = `
717
+ (async function() {
718
+ try {
719
+ // Fetch the product page to extract Fresh-specific payload
720
+ var dpResp = await fetch(${JSON.stringify(productUrl)}, {
721
+ credentials: 'include',
722
+ headers: { 'Accept': 'text/html,application/xhtml+xml' }
723
+ });
724
+ if (dpResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
725
+ if (dpResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
726
+
727
+ var html = await dpResp.text();
728
+ var parser = new DOMParser();
729
+ var doc = parser.parseFromString(html, 'text/html');
730
+
731
+ // Extract anti-CSRF token
732
+ var antiCsrf = '';
733
+ var csrfMeta = doc.querySelector('meta[name="anti-csrftoken-a2z"]');
734
+ if (csrfMeta && csrfMeta.content) {
735
+ antiCsrf = csrfMeta.content;
736
+ } else {
737
+ var csrfInp = doc.querySelector('input[name="anti-csrftoken-a2z"]');
738
+ if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
739
+ else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
740
+ }
741
+
742
+ // Extract csrfToken from product page
743
+ var csrfInput = doc.querySelector('input[name="csrfToken"]');
744
+ var csrfToken = csrfInput ? csrfInput.value : '';
745
+ if (!csrfToken) {
746
+ var csrfMatch = html.match(/"csrfToken"\\s*:\\s*"([^"\\\\]+)"/);
747
+ if (csrfMatch) csrfToken = csrfMatch[1];
748
+ }
749
+ if (!csrfToken) { var ck = document.cookie.match(/csrf-main=([^;]+)/); if (ck) csrfToken = decodeURIComponent(ck[1]); }
750
+
751
+ // Fresh add-to-cart: extract the EXACT payload Amazon embeds in the
752
+ // data-fresh-add-to-cart attribute on the product page.
753
+ var freshAtcEl = doc.querySelector('[data-action="fresh-add-to-cart"]');
754
+ var freshPayload;
755
+ if (freshAtcEl && freshAtcEl.getAttribute('data-fresh-add-to-cart')) {
756
+ freshPayload = JSON.parse(freshAtcEl.getAttribute('data-fresh-add-to-cart'));
757
+ freshPayload.qsUID = 'atfc-' + (freshPayload.clientID || 'fresh-dp') + '-' + Date.now();
758
+ freshPayload.prevSelectedQty = 0;
759
+ freshPayload.isStepperFlag = false;
760
+ freshPayload.setQuantityFlag = false;
761
+ freshPayload.quantityData = {
762
+ quantity: String(${quantity}),
763
+ quantitySuffix: '',
764
+ price: '',
765
+ renderableSellingQuantity: String(${quantity}),
766
+ };
767
+ freshPayload.sellingUnit = freshPayload.sellingUnit || 'units';
768
+ freshPayload.sellingDimension = freshPayload.sellingDimension || 'count';
769
+ } else {
770
+ var discMatch = html.match(/\\"offerListingDiscriminator\\":\\"([^\\"]+)\\"/)
771
+ || html.match(/&quot;offerListingDiscriminator&quot;:&quot;([^&]+)&quot;/);
772
+ var offerDiscriminator = discMatch ? discMatch[1] : '';
773
+ var freshOfferIdMatch = html.match(/\\"offerListingID\\":\\"([^\\"]+)\\"/)
774
+ || html.match(/&quot;offerListingID&quot;:&quot;([^&]+)&quot;/);
775
+ var freshOfferListingID = freshOfferIdMatch ? freshOfferIdMatch[1] : '';
776
+ var sessionMatch = document.cookie.match(/session-id=([^;]+)/);
777
+ var sessionId = sessionMatch ? decodeURIComponent(sessionMatch[1]) : '';
778
+
779
+ freshPayload = {
780
+ qsUID: 'atfc-alm-mod-dp-' + Date.now(),
781
+ prevSelectedQty: 0,
782
+ isStepperFlag: false,
783
+ setQuantityFlag: false,
784
+ quantityData: {
785
+ quantity: String(${quantity}),
786
+ quantitySuffix: '',
787
+ price: '',
788
+ renderableSellingQuantity: String(${quantity}),
789
+ },
790
+ sellingUnit: 'units',
791
+ sellingDimension: 'count',
792
+ reftag: 'alm-dp-atc-so-fs',
793
+ csrfToken: csrfToken,
794
+ clientID: 'alm-mod-dp',
795
+ isItemSoldByCount: 'true',
796
+ brandId: 'QW1hem9uIEZyZXNo',
797
+ asin: ${JSON.stringify(opts.asin)},
798
+ sessionID: sessionId,
799
+ storeId: 'dc6d4e0d03d7c0a581c85a754396fe17eb8e54f3',
800
+ promotionId: 'any',
801
+ };
802
+ if (offerDiscriminator) freshPayload.offerListingDiscriminator = offerDiscriminator;
803
+ if (freshOfferListingID) freshPayload.offerListingID = freshOfferListingID;
804
+ }
805
+
806
+ var freshReftag = freshPayload.reftag || 'alm-dp-atc-so-fs';
807
+ var addResp = await fetch('${AMAZON_BASE}/alm/addtofreshcart?ref_=' + freshReftag + '&discoveredAsins.0=' + encodeURIComponent(${JSON.stringify(opts.asin)}) + '&almBrandId=QW1hem9uIEZyZXNo', {
808
+ method: 'POST',
809
+ headers: {
810
+ 'Content-Type': 'application/json',
811
+ 'Accept': 'application/json, */*',
812
+ 'anti-csrftoken-a2z': antiCsrf,
813
+ },
814
+ body: JSON.stringify(freshPayload),
815
+ credentials: 'include',
816
+ });
817
+
818
+ if (addResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
819
+ if (addResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
820
+
821
+ var addText = await addResp.text();
822
+ var addOk = addResp.ok;
823
+ var cartJson = null;
824
+ try { cartJson = JSON.parse(addText); } catch(e) {}
825
+
826
+ var items = [];
827
+ if (cartJson && cartJson.clientResponseModel && Array.isArray(cartJson.clientResponseModel.items)) {
828
+ items = cartJson.clientResponseModel.items.map(function(item) {
829
+ return {
830
+ cartItemId: item.itemId || item.ASIN || '',
831
+ asin: item.ASIN || '',
832
+ title: '',
833
+ quantity: item.quantity || 1,
834
+ price: '',
835
+ isFresh: true,
836
+ };
837
+ });
838
+ }
839
+
840
+ // Fresh fallback: get-cart-items
841
+ if (items.length === 0) {
842
+ try {
843
+ var freshCartResp = await fetch('${AMAZON_BASE}/cart/add-to-cart/get-cart-items?clientName=SiteWideActionExecutor&_=' + Date.now(), {
844
+ credentials: 'include',
845
+ headers: { 'Accept': 'application/json, */*' }
846
+ });
847
+ if (freshCartResp.ok) {
848
+ var freshCartData = await freshCartResp.json();
849
+ if (Array.isArray(freshCartData)) {
850
+ var allFreshItems = freshCartData.filter(function(i) { return i.cartType === 'LOCAL_MARKET'; });
851
+ var targetFound = allFreshItems.some(function(i) { return i.asin === ${JSON.stringify(opts.asin)}; });
852
+ if (!targetFound) {
853
+ return JSON.stringify({
854
+ __error: true,
855
+ __message: 'Add-to-cart failed: ASIN ' + ${JSON.stringify(opts.asin)} + ' was not found in the Fresh cart after adding. The item may be unavailable or the session cookies may be stale. Try running vellum amazon refresh.'
856
+ });
857
+ }
858
+ items = allFreshItems.map(function(item) {
859
+ return { cartItemId: item.asin || '', asin: item.asin || '', title: '', quantity: item.quantity || 1, price: '', isFresh: true };
860
+ });
861
+ }
862
+ }
863
+ } catch(fe) { /* ignore */ }
864
+ }
865
+
866
+ return JSON.stringify({
867
+ __status: addResp.status,
868
+ __ok: addOk,
869
+ __data: { items: items, subtotal: '', itemCount: items.length },
870
+ __addCartJson: cartJson ? JSON.stringify(cartJson).substring(0, 500) : null,
871
+ });
872
+ } catch(e) {
873
+ return JSON.stringify({ __error: true, __message: e.message });
874
+ }
875
+ })()
876
+ `;
877
+
878
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
879
+ handleResult(result);
880
+
881
+ const cart = result.__data as CartSummary & { __debug?: unknown };
882
+
883
+ if (result.__ok === false) {
884
+ const rawSnippet = result.__addCartJson ? ` | raw: ${(result.__addCartJson as string).substring(0, 150)}` : '';
885
+ throw new Error(`Fresh add-to-cart POST failed (status=${result.__status}${rawSnippet}).`);
886
+ }
887
+
888
+ cart.__debug = {
889
+ addCartJson: result.__addCartJson,
890
+ httpStatus: result.__status,
891
+ httpOk: result.__ok,
892
+ };
893
+ return cart;
894
+ });
895
+ }
896
+
897
+ /**
898
+ * Remove an item from the Amazon cart by cart item ID.
899
+ */
900
+ export async function removeFromCart(opts: {
901
+ cartItemId: string;
902
+ }): Promise<CartSummary> {
903
+ const { tabId } = await prepareRequest();
904
+
905
+ return runWithBackoff(async () => {
906
+ const url = `${AMAZON_BASE}/gp/cart/view.html`;
907
+ const body = `cartItemId.${opts.cartItemId}=${opts.cartItemId}&quantity.${opts.cartItemId}=0&submit.delete.${opts.cartItemId}=Delete&ie=UTF8&action=delete`;
908
+
909
+ const script = `
910
+ (async function() {
911
+ try {
912
+ var antiCsrf = '';
913
+ var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
914
+ if (csrfMeta && csrfMeta.content) {
915
+ antiCsrf = csrfMeta.content;
916
+ } else {
917
+ var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
918
+ if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
919
+ else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
920
+ }
921
+
922
+ var resp = await fetch(${JSON.stringify(url)}, {
923
+ method: 'POST',
924
+ headers: {
925
+ 'Content-Type': 'application/x-www-form-urlencoded',
926
+ 'anti-csrftoken-a2z': antiCsrf,
927
+ },
928
+ body: ${JSON.stringify(body)},
929
+ credentials: 'include',
930
+ });
931
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
932
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
933
+ return JSON.stringify({ __status: resp.status, __ok: resp.ok });
934
+ } catch(e) {
935
+ return JSON.stringify({ __error: true, __message: e.message });
936
+ }
937
+ })()
938
+ `;
939
+
940
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
941
+ handleResult(result);
942
+ return viewCart();
943
+ });
944
+ }
945
+
946
+ /**
947
+ * View the current Amazon cart contents.
948
+ */
949
+ export async function viewCart(): Promise<CartSummary> {
950
+ const { tabId } = await prepareRequest();
951
+
952
+ return runWithBackoff(async () => {
953
+ // Combine two sources:
954
+ // 1. /gp/cart/view.html HTML page — regular Amazon items
955
+ // 2. get-cart-items JSON endpoint — Fresh (LOCAL_MARKET) items only
956
+ const script = `
957
+ (async function() {
958
+ try {
959
+ var items = [];
960
+ var subtotalText = '';
961
+
962
+ // --- Regular cart: parse /gp/cart/view.html HTML ---
963
+ try {
964
+ var cartResp = await fetch('${AMAZON_BASE}/gp/cart/view.html', {
965
+ credentials: 'include',
966
+ headers: { 'Accept': 'text/html,application/xhtml+xml,*/*' }
967
+ });
968
+ if (cartResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
969
+ if (cartResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
970
+ if (cartResp.ok) {
971
+ var cartHtml = await cartResp.text();
972
+ var parser = new DOMParser();
973
+ var doc = parser.parseFromString(cartHtml, 'text/html');
974
+ // Each cart item row has data-asin and a quantity input
975
+ doc.querySelectorAll('[data-asin]').forEach(function(el) {
976
+ var asin = el.getAttribute('data-asin');
977
+ if (!asin || asin.length < 6) return;
978
+ var qtyInput = el.querySelector('[name^="quantity."]') || el.querySelector('input[type="text"]');
979
+ var qty = qtyInput ? (parseInt(qtyInput.value, 10) || 1) : 1;
980
+ var titleEl = el.querySelector('.a-truncate-full') || el.querySelector('.sc-product-title') || el.querySelector('[class*="product-title"]');
981
+ var priceEl = el.querySelector('.a-price .a-offscreen') || el.querySelector('[class*="price"]');
982
+ var cartItemIdMatch = (el.innerHTML || '').match(/cartItemId[=\\s:"]+([A-Z0-9]+)/i);
983
+ var cartItemId = cartItemIdMatch ? cartItemIdMatch[1] : asin;
984
+ items.push({
985
+ cartItemId: cartItemId,
986
+ asin: asin,
987
+ title: titleEl ? titleEl.textContent.trim() : '',
988
+ quantity: qty,
989
+ price: priceEl ? priceEl.textContent.trim() : '',
990
+ isFresh: false,
991
+ });
992
+ });
993
+ // Subtotal
994
+ var subtotalEl = doc.querySelector('#sc-subtotal-amount-activecart .a-price .a-offscreen') ||
995
+ doc.querySelector('[id*="subtotal"] .a-price .a-offscreen') ||
996
+ doc.querySelector('.sc-price-sign');
997
+ if (subtotalEl) subtotalText = subtotalEl.textContent.trim();
998
+ }
999
+ } catch(re) { /* ignore regular cart errors, still try Fresh */ }
1000
+
1001
+ // --- Fresh cart: get-cart-items JSON endpoint (LOCAL_MARKET only) ---
1002
+ try {
1003
+ var freshResp = await fetch('${AMAZON_BASE}/cart/add-to-cart/get-cart-items?clientName=SiteWideActionExecutor&_=' + Date.now(), {
1004
+ credentials: 'include',
1005
+ headers: { 'Accept': 'application/json, */*' }
1006
+ });
1007
+ if (freshResp.ok) {
1008
+ var freshData = await freshResp.json();
1009
+ if (Array.isArray(freshData)) {
1010
+ freshData.forEach(function(item) {
1011
+ if (item.cartType === 'LOCAL_MARKET') {
1012
+ items.push({
1013
+ cartItemId: item.asin || '',
1014
+ asin: item.asin || '',
1015
+ title: '',
1016
+ quantity: item.quantity || 1,
1017
+ price: '',
1018
+ isFresh: true,
1019
+ });
1020
+ }
1021
+ });
1022
+ }
1023
+ }
1024
+ } catch(fe) { /* ignore Fresh cart errors */ }
1025
+
1026
+ return JSON.stringify({
1027
+ __status: 200,
1028
+ __data: { items: items, subtotal: subtotalText, itemCount: items.length }
1029
+ });
1030
+ } catch(e) {
1031
+ return JSON.stringify({ __error: true, __message: e.message });
1032
+ }
1033
+ })()
1034
+ `;
1035
+
1036
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1037
+ handleResult(result);
1038
+ return result.__data as CartSummary;
1039
+ });
1040
+ }
1041
+
1042
+ /**
1043
+ * Get available Amazon Fresh delivery slots.
1044
+ */
1045
+ export async function getFreshDeliverySlots(): Promise<DeliverySlot[]> {
1046
+ const { tabId } = await prepareRequest();
1047
+
1048
+ return runWithBackoff(async () => {
1049
+ // Amazon Fresh delivery windows API
1050
+ const url = `${AMAZON_BASE}/fresh/deliverywindows`;
1051
+
1052
+ const script = `
1053
+ (async function() {
1054
+ try {
1055
+ var resp = await fetch(${JSON.stringify(url)}, {
1056
+ credentials: 'include',
1057
+ headers: {
1058
+ 'Accept': 'application/json, text/html,*/*',
1059
+ 'x-requested-with': 'XMLHttpRequest',
1060
+ }
1061
+ });
1062
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1063
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1064
+ var text = await resp.text();
1065
+
1066
+ var slots = [];
1067
+ try {
1068
+ var data = JSON.parse(text);
1069
+ // Normalize various possible response shapes
1070
+ var windows = data.deliveryWindows || data.windows || data.slots || (Array.isArray(data) ? data : []);
1071
+ windows.forEach(function(w) {
1072
+ slots.push({
1073
+ slotId: w.windowId || w.slotId || w.id || '',
1074
+ date: w.date || w.windowStartDate || (w.windowStartDateTimeUtc || '').split('T')[0] || '',
1075
+ timeWindow: w.timeWindow || w.displayString || (w.windowStartDateTimeUtc && w.windowEndDateTimeUtc
1076
+ ? w.windowStartDateTimeUtc.split('T')[1].substring(0,5) + ' - ' + w.windowEndDateTimeUtc.split('T')[1].substring(0,5)
1077
+ : ''),
1078
+ price: (w.price && w.price.localizedDisplayString) ? w.price.localizedDisplayString
1079
+ : (w.deliveryFee || w.fee || 'FREE'),
1080
+ isAvailable: w.isAvailable !== false && !w.isFull,
1081
+ });
1082
+ });
1083
+ } catch(pe) {
1084
+ // HTML response — parse from page
1085
+ var parser = new DOMParser();
1086
+ var doc = parser.parseFromString(text, 'text/html');
1087
+ doc.querySelectorAll('[data-slot-id], [data-window-id]').forEach(function(el) {
1088
+ slots.push({
1089
+ slotId: el.getAttribute('data-slot-id') || el.getAttribute('data-window-id') || '',
1090
+ date: el.getAttribute('data-date') || '',
1091
+ timeWindow: el.getAttribute('data-time-window') || el.textContent.trim(),
1092
+ price: (el.querySelector('.a-price') || el.querySelector('.slot-price') || {textContent: 'FREE'}).textContent.trim(),
1093
+ isAvailable: !el.classList.contains('unavailable') && !el.hasAttribute('disabled'),
1094
+ });
1095
+ });
1096
+ }
1097
+
1098
+ return JSON.stringify({ __status: resp.status, __data: slots });
1099
+ } catch(e) {
1100
+ return JSON.stringify({ __error: true, __message: e.message });
1101
+ }
1102
+ })()
1103
+ `;
1104
+
1105
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1106
+ handleResult(result);
1107
+ return result.__data as DeliverySlot[];
1108
+ });
1109
+ }
1110
+
1111
+ /**
1112
+ * Select an Amazon Fresh delivery slot.
1113
+ */
1114
+ export async function selectFreshDeliverySlot(slotId: string): Promise<{ ok: boolean }> {
1115
+ const { tabId } = await prepareRequest();
1116
+
1117
+ return runWithBackoff(async () => {
1118
+ // Amazon Fresh slot selection endpoint
1119
+ const url = `${AMAZON_BASE}/fresh/api/deliverywindows/select`;
1120
+ const body = JSON.stringify({ windowId: slotId });
1121
+
1122
+ const script = `
1123
+ (async function() {
1124
+ try {
1125
+ var antiCsrf = '';
1126
+ var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
1127
+ if (csrfMeta && csrfMeta.content) {
1128
+ antiCsrf = csrfMeta.content;
1129
+ } else {
1130
+ var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
1131
+ if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
1132
+ else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
1133
+ }
1134
+
1135
+ var resp = await fetch(${JSON.stringify(url)}, {
1136
+ method: 'POST',
1137
+ headers: {
1138
+ 'Content-Type': 'application/json',
1139
+ 'Accept': 'application/json',
1140
+ 'anti-csrftoken-a2z': antiCsrf,
1141
+ 'x-requested-with': 'XMLHttpRequest',
1142
+ },
1143
+ body: ${JSON.stringify(body)},
1144
+ credentials: 'include',
1145
+ });
1146
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1147
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1148
+ var text = await resp.text();
1149
+ return JSON.stringify({ __status: resp.status, __ok: resp.ok, __body: text.substring(0, 500) });
1150
+ } catch(e) {
1151
+ return JSON.stringify({ __error: true, __message: e.message });
1152
+ }
1153
+ })()
1154
+ `;
1155
+
1156
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1157
+ handleResult(result);
1158
+ return { ok: Boolean(result.__ok) };
1159
+ });
1160
+ }
1161
+
1162
+ /**
1163
+ * Get payment methods from the checkout page.
1164
+ */
1165
+ export async function getPaymentMethods(): Promise<PaymentMethod[]> {
1166
+ const { tabId } = await prepareRequest();
1167
+
1168
+ return runWithBackoff(async () => {
1169
+ const url = `${AMAZON_BASE}/gp/buy/payselect/handlers/display.html`;
1170
+
1171
+ const script = `
1172
+ (async function() {
1173
+ try {
1174
+ var resp = await fetch(${JSON.stringify(url)}, {
1175
+ credentials: 'include',
1176
+ headers: { 'Accept': 'text/html,application/xhtml+xml' }
1177
+ });
1178
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1179
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1180
+ var html = await resp.text();
1181
+ var parser = new DOMParser();
1182
+ var doc = parser.parseFromString(html, 'text/html');
1183
+
1184
+ var methods = [];
1185
+ var seen = new Set();
1186
+
1187
+ // Look for payment instruments in the page
1188
+ doc.querySelectorAll('[data-pmid], [id^="payment-instrument-"]').forEach(function(el) {
1189
+ var pmid = el.getAttribute('data-pmid') || el.id.replace('payment-instrument-', '');
1190
+ if (!pmid || seen.has(pmid)) return;
1191
+ seen.add(pmid);
1192
+
1193
+ var textContent = el.textContent || '';
1194
+ var last4Match = textContent.match(/(?:ending|\\*{3,})(\\d{4})/);
1195
+ var last4 = last4Match ? last4Match[1] : '';
1196
+
1197
+ var type = 'Card';
1198
+ if (textContent.toLowerCase().includes('visa')) type = 'Visa';
1199
+ else if (textContent.toLowerCase().includes('mastercard')) type = 'Mastercard';
1200
+ else if (textContent.toLowerCase().includes('amex') || textContent.toLowerCase().includes('american express')) type = 'AmEx';
1201
+ else if (textContent.toLowerCase().includes('discover')) type = 'Discover';
1202
+
1203
+ var isDefault = el.classList.contains('pmts-selected') || !!el.querySelector('[selected]') || false;
1204
+
1205
+ if (last4 || pmid) {
1206
+ methods.push({ paymentMethodId: pmid, type, last4, isDefault });
1207
+ }
1208
+ });
1209
+
1210
+ // Fallback: look for payment method data in inline JSON
1211
+ if (methods.length === 0) {
1212
+ var jsonMatch = html.match(/"paymentInstruments"\s*:\s*(\[[^\]]+\])/);
1213
+ if (jsonMatch) {
1214
+ try {
1215
+ var instruments = JSON.parse(jsonMatch[1]);
1216
+ instruments.forEach(function(inst) {
1217
+ methods.push({
1218
+ paymentMethodId: inst.paymentMethodId || inst.id || '',
1219
+ type: inst.cardType || inst.type || 'Card',
1220
+ last4: inst.last4 || inst.maskedCardNumber || '',
1221
+ isDefault: !!inst.isDefault,
1222
+ });
1223
+ });
1224
+ } catch(e) {}
1225
+ }
1226
+ }
1227
+
1228
+ return JSON.stringify({ __status: resp.status, __data: methods });
1229
+ } catch(e) {
1230
+ return JSON.stringify({ __error: true, __message: e.message });
1231
+ }
1232
+ })()
1233
+ `;
1234
+
1235
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1236
+ handleResult(result);
1237
+ return result.__data as PaymentMethod[];
1238
+ });
1239
+ }
1240
+
1241
+ /**
1242
+ * Get the checkout summary (totals, shipping, payment options).
1243
+ */
1244
+ export async function getCheckoutSummary(): Promise<CheckoutSummary> {
1245
+ const { tabId } = await prepareRequest();
1246
+
1247
+ return runWithBackoff(async () => {
1248
+ const url = `${AMAZON_BASE}/gp/buy/spc/handlers/static-submit-merchantId-data.html`;
1249
+
1250
+ const script = `
1251
+ (async function() {
1252
+ try {
1253
+ var resp = await fetch(${JSON.stringify(url)}, {
1254
+ credentials: 'include',
1255
+ headers: { 'Accept': 'text/html,application/xhtml+xml' }
1256
+ });
1257
+ if (resp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1258
+ if (resp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1259
+ var html = await resp.text();
1260
+ var parser = new DOMParser();
1261
+ var doc = parser.parseFromString(html, 'text/html');
1262
+
1263
+ var getPrice = function(selector) {
1264
+ var el = doc.querySelector(selector);
1265
+ return el ? el.textContent.trim() : '';
1266
+ };
1267
+
1268
+ var subtotal = getPrice('#subtotals-marketplace-table tr:first-child td:last-child') ||
1269
+ getPrice('.order-summary-line-item-price') ||
1270
+ getPrice('[data-component="subtotalAmount"]');
1271
+ var shipping = getPrice('#subtotals-marketplace-table .shipping-row td:last-child') ||
1272
+ getPrice('[data-component="shippingAmount"]') || 'FREE';
1273
+ var tax = getPrice('#subtotals-marketplace-table .tax-row td:last-child') ||
1274
+ getPrice('[data-component="taxAmount"]') || '';
1275
+ var total = getPrice('#subtotals-marketplace-table .grand-total-price') ||
1276
+ getPrice('[data-component="orderTotalAmount"]') ||
1277
+ getPrice('.grand-total-price');
1278
+
1279
+ var deliveryDateEl = doc.querySelector('.delivery-date') || doc.querySelector('[class*="delivery-date"]');
1280
+ var deliveryDate = deliveryDateEl ? deliveryDateEl.textContent.trim() : '';
1281
+
1282
+ // Payment methods
1283
+ var methods = [];
1284
+ doc.querySelectorAll('[data-pmid], .payment-instrument').forEach(function(el) {
1285
+ var pmid = el.getAttribute('data-pmid') || '';
1286
+ if (!pmid) return;
1287
+ var text = el.textContent || '';
1288
+ var last4Match = text.match(/(?:ending|\\*{3,})(\\d{4})/);
1289
+ methods.push({
1290
+ paymentMethodId: pmid,
1291
+ type: text.toLowerCase().includes('visa') ? 'Visa' :
1292
+ text.toLowerCase().includes('mastercard') ? 'Mastercard' : 'Card',
1293
+ last4: last4Match ? last4Match[1] : '',
1294
+ isDefault: !!el.querySelector('[selected]') || el.classList.contains('selected'),
1295
+ });
1296
+ });
1297
+
1298
+ return JSON.stringify({
1299
+ __status: resp.status,
1300
+ __data: { subtotal, shipping, tax, total, paymentMethods: methods, deliveryDate }
1301
+ });
1302
+ } catch(e) {
1303
+ return JSON.stringify({ __error: true, __message: e.message });
1304
+ }
1305
+ })()
1306
+ `;
1307
+
1308
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1309
+ handleResult(result);
1310
+ return result.__data as CheckoutSummary;
1311
+ });
1312
+ }
1313
+
1314
+ /**
1315
+ * Place an Amazon order.
1316
+ * WARNING: This submits a real order. Always confirm with the user first.
1317
+ */
1318
+ export async function placeOrder(opts: {
1319
+ paymentMethodId?: string;
1320
+ deliverySlotId?: string;
1321
+ } = {}): Promise<PlaceOrderResult> {
1322
+ const { tabId } = await prepareRequest();
1323
+
1324
+ return runWithBackoff(async () => {
1325
+ // First load the SPC page to get the order submission token
1326
+ const spcUrl = `${AMAZON_BASE}/gp/buy/spc/handlers/static-submit-merchantId-data.html`;
1327
+
1328
+ const script = `
1329
+ (async function() {
1330
+ try {
1331
+ var antiCsrf = '';
1332
+ var csrfMeta = document.querySelector('meta[name="anti-csrftoken-a2z"]');
1333
+ if (csrfMeta && csrfMeta.content) {
1334
+ antiCsrf = csrfMeta.content;
1335
+ } else {
1336
+ var csrfInp = document.querySelector('input[name="anti-csrftoken-a2z"]');
1337
+ if (csrfInp && csrfInp.value) { antiCsrf = csrfInp.value; }
1338
+ else { var m = document.cookie.match(/anti-csrftoken-a2z=([^;]+)/); if (m) antiCsrf = decodeURIComponent(m[1]); }
1339
+ }
1340
+
1341
+ // Load checkout page to get form token
1342
+ var spcResp = await fetch(${JSON.stringify(spcUrl)}, {
1343
+ credentials: 'include',
1344
+ headers: { 'Accept': 'text/html,application/xhtml+xml' }
1345
+ });
1346
+ if (spcResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1347
+ if (spcResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1348
+ var spcHtml = await spcResp.text();
1349
+
1350
+ // Extract form action and hidden fields
1351
+ var parser = new DOMParser();
1352
+ var doc = parser.parseFromString(spcHtml, 'text/html');
1353
+ var form = doc.querySelector('form#turbo-checkout-pyo-form') || doc.querySelector('form[name="checkout"]') || doc.querySelector('#placeYourOrder form');
1354
+
1355
+ if (!form) {
1356
+ return JSON.stringify({ __error: true, __message: 'Could not find order form on checkout page. Please complete checkout manually in the browser.' });
1357
+ }
1358
+
1359
+ var formAction = form.getAttribute('action') || '/gp/buy/spc/handlers/static-submit-merchantId-data.html';
1360
+ if (!formAction.startsWith('http')) formAction = 'https://www.amazon.com' + formAction;
1361
+
1362
+ // Build form data from hidden inputs
1363
+ var formData = new URLSearchParams();
1364
+ form.querySelectorAll('input[type="hidden"]').forEach(function(inp) {
1365
+ formData.set(inp.name, inp.value);
1366
+ });
1367
+
1368
+ // Apply payment method if specified
1369
+ if (${JSON.stringify(opts.paymentMethodId || '')}) {
1370
+ formData.set('ppw-instrumentId', ${JSON.stringify(opts.paymentMethodId || '')});
1371
+ }
1372
+
1373
+ // Submit order
1374
+ var submitResp = await fetch(formAction, {
1375
+ method: 'POST',
1376
+ headers: {
1377
+ 'Content-Type': 'application/x-www-form-urlencoded',
1378
+ 'anti-csrftoken-a2z': antiCsrf,
1379
+ },
1380
+ body: formData.toString(),
1381
+ credentials: 'include',
1382
+ });
1383
+ if (submitResp.status === 401) return JSON.stringify({ __status: 401, __error: true });
1384
+ if (submitResp.status === 403) return JSON.stringify({ __status: 403, __error: true });
1385
+ var resultHtml = await submitResp.text();
1386
+ var resultDoc = parser.parseFromString(resultHtml, 'text/html');
1387
+
1388
+ // Extract order ID from confirmation page
1389
+ var orderIdEl = resultDoc.querySelector('[class*="order-id"]') ||
1390
+ resultDoc.querySelector('[data-order-id]') ||
1391
+ resultDoc.querySelector('[class*="confirmation"]');
1392
+ var orderId = '';
1393
+ if (orderIdEl) {
1394
+ var oidMatch = (orderIdEl.textContent || '').match(/\\d{3}-\\d{7}-\\d{7}/);
1395
+ if (oidMatch) orderId = oidMatch[0];
1396
+ }
1397
+ // Also check URL for order ID
1398
+ var urlMatch = submitResp.url.match(/orderId=([\\d-]+)/);
1399
+ if (!orderId && urlMatch) orderId = urlMatch[1];
1400
+
1401
+ var deliveryEl = resultDoc.querySelector('[class*="delivery-date"]') || resultDoc.querySelector('[class*="estimated-delivery"]');
1402
+ var estimatedDelivery = deliveryEl ? deliveryEl.textContent.trim() : '';
1403
+
1404
+ return JSON.stringify({
1405
+ __status: submitResp.status,
1406
+ __data: { orderId: orderId || 'confirmed', estimatedDelivery }
1407
+ });
1408
+ } catch(e) {
1409
+ return JSON.stringify({ __error: true, __message: e.message });
1410
+ }
1411
+ })()
1412
+ `;
1413
+
1414
+ const result = await cdpEval(tabId, script) as Record<string, unknown>;
1415
+ handleResult(result);
1416
+ return result.__data as PlaceOrderResult;
1417
+ });
1418
+ }