@vellumai/assistant 0.6.2 → 0.6.3
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.
- package/bun.lock +40 -40
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +184 -69
- package/package.json +41 -41
- package/scripts/generate-openapi.ts +1 -2
- package/src/__tests__/acp-session.test.ts +43 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- package/src/__tests__/browser-fill-credential.test.ts +229 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/catalog-files.test.ts +862 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +125 -48
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +1 -1
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-queue.test.ts +45 -2
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
- package/src/__tests__/conversation-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +2 -2
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/gemini-provider.test.ts +2 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +707 -371
- package/src/__tests__/headless-browser-navigate.test.ts +389 -47
- package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
- package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
- package/src/__tests__/host-bash-proxy.test.ts +150 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
- package/src/__tests__/host-cu-proxy.test.ts +171 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +61 -2
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +101 -1
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +17 -12
- package/src/__tests__/oauth-cli.test.ts +707 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +50 -14
- package/src/__tests__/oauth-store.test.ts +1386 -182
- package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
- package/src/__tests__/onboarding-template-contract.test.ts +75 -57
- package/src/__tests__/openai-provider.test.ts +2 -2
- package/src/__tests__/outlook-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
- package/src/__tests__/slack-channel-config.test.ts +12 -15
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +72 -1
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -6
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- package/src/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -0
- package/src/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +53 -3
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/email.ts +18 -13
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +200 -36
- package/src/cli/commands/oauth/shared.ts +5 -5
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
- package/src/cli/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -0
- package/src/cli/program.ts +1 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- package/src/config/bundled-skills/contacts/SKILL.md +3 -0
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -0
- package/src/config/env-registry.ts +14 -0
- package/src/config/env.ts +21 -0
- package/src/config/feature-flag-registry.json +44 -5
- package/src/config/loader.ts +56 -1
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +46 -5
- package/src/config/schemas/host-browser.ts +66 -0
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/config/types.ts +0 -1
- package/src/context/post-turn-tool-result-truncation.ts +176 -0
- package/src/context/window-manager.ts +19 -1
- package/src/credential-execution/approval-bridge.ts +49 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +58 -24
- package/src/daemon/conversation-attachments.ts +40 -0
- package/src/daemon/conversation-process.ts +48 -1
- package/src/daemon/conversation-runtime-assembly.ts +118 -36
- package/src/daemon/conversation-surfaces.ts +37 -36
- package/src/daemon/conversation-tool-setup.ts +74 -8
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +226 -8
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -140
- package/src/daemon/handlers/shared.ts +58 -0
- package/src/daemon/handlers/skills.ts +232 -37
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +191 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +65 -11
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +55 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -5
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +92 -12
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +5 -24
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +23 -0
- package/src/memory/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/retriever.ts +40 -22
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/llm-usage-store.ts +45 -4
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +1 -0
- package/src/memory/schema/oauth.ts +18 -13
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +8 -8
- package/src/oauth/byo-connection.ts +7 -7
- package/src/oauth/connect-orchestrator.ts +23 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +16 -16
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +214 -100
- package/src/oauth/platform-connection.test.ts +3 -3
- package/src/oauth/platform-connection.ts +4 -4
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +126 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- package/src/providers/anthropic/client.ts +1 -0
- package/src/providers/types.ts +1 -1
- package/src/runtime/AGENTS.md +23 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +6 -7
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/chrome-extension-registry.ts +332 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +425 -70
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
- package/src/runtime/migrations/migration-transport.ts +6 -0
- package/src/runtime/migrations/migration-wizard.ts +22 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
- package/src/runtime/migrations/vbundle-importer.ts +55 -5
- package/src/runtime/pending-interactions.ts +29 -13
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/browser-cdp-routes.ts +229 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +301 -27
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/host-browser-routes.ts +279 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-routes.ts +259 -16
- package/src/runtime/routes/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +87 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +9 -97
- package/src/runtime/routes/skills-routes.ts +52 -2
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/usage-routes.ts +8 -7
- package/src/runtime/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- package/src/schedule/scheduler.ts +7 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +42 -25
- package/src/security/secure-keys.ts +118 -25
- package/src/security/token-manager.ts +23 -10
- package/src/skills/catalog-files.ts +492 -0
- package/src/subagent/manager.ts +131 -26
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- package/src/tools/browser/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +645 -340
- package/src/tools/browser/browser-manager.ts +36 -12
- package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
- package/src/tools/browser/cdp-client/errors.ts +34 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
- package/src/tools/browser/cdp-client/factory.ts +204 -0
- package/src/tools/browser/cdp-client/index.ts +14 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +52 -0
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/terminal/shell.ts +21 -16
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -0
- package/src/util/platform.ts +14 -19
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common CDP idioms that each browser tool would otherwise reimplement:
|
|
3
|
+
* selector resolution, mouse/keyboard dispatch, screenshot capture,
|
|
4
|
+
* navigation, polling waits, and small Runtime.evaluate wrappers.
|
|
5
|
+
*
|
|
6
|
+
* Every helper takes a {@link CdpClient} as its first argument, forwards
|
|
7
|
+
* an optional {@link AbortSignal} verbatim to `CdpClient.send`, and
|
|
8
|
+
* throws a {@link CdpError} on failure. The module is pure plumbing —
|
|
9
|
+
* no I/O beyond the injected CdpClient — which keeps it trivial to
|
|
10
|
+
* unit-test against a fake in-memory client.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CdpError } from "./errors.js";
|
|
14
|
+
import type { CdpClient } from "./types.js";
|
|
15
|
+
|
|
16
|
+
// ── Selector / node resolution ────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a CSS selector to a CDP `backendNodeId`. Runs
|
|
20
|
+
* `DOM.getDocument` → `DOM.querySelector` → `DOM.describeNode` and
|
|
21
|
+
* throws {@link CdpError} with `code: "cdp_error"` if no element
|
|
22
|
+
* matches (CDP signals this by returning `nodeId: 0`).
|
|
23
|
+
*/
|
|
24
|
+
export async function querySelectorBackendNodeId(
|
|
25
|
+
cdp: CdpClient,
|
|
26
|
+
selector: string,
|
|
27
|
+
signal?: AbortSignal,
|
|
28
|
+
): Promise<number> {
|
|
29
|
+
const { root } = await cdp.send<{ root: { nodeId: number } }>(
|
|
30
|
+
"DOM.getDocument",
|
|
31
|
+
{},
|
|
32
|
+
signal,
|
|
33
|
+
);
|
|
34
|
+
const { nodeId } = await cdp.send<{ nodeId: number }>(
|
|
35
|
+
"DOM.querySelector",
|
|
36
|
+
{ nodeId: root.nodeId, selector },
|
|
37
|
+
signal,
|
|
38
|
+
);
|
|
39
|
+
if (!nodeId) {
|
|
40
|
+
throw new CdpError("cdp_error", `Element not found: ${selector}`, {
|
|
41
|
+
cdpMethod: "DOM.querySelector",
|
|
42
|
+
cdpParams: { selector },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const { node } = await cdp.send<{ node: { backendNodeId: number } }>(
|
|
46
|
+
"DOM.describeNode",
|
|
47
|
+
{ nodeId, depth: 0 },
|
|
48
|
+
signal,
|
|
49
|
+
);
|
|
50
|
+
return node.backendNodeId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Scroll the element identified by `backendNodeId` into view if needed. */
|
|
54
|
+
export async function scrollIntoViewIfNeeded(
|
|
55
|
+
cdp: CdpClient,
|
|
56
|
+
backendNodeId: number,
|
|
57
|
+
signal?: AbortSignal,
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
await cdp.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }, signal);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Read the element's content-quad via `DOM.getBoxModel` and return the
|
|
64
|
+
* midpoint in viewport coordinates. CDP returns `content` as a flat
|
|
65
|
+
* 8-number array `[x1,y1, x2,y2, x3,y3, x4,y4]`.
|
|
66
|
+
*/
|
|
67
|
+
export async function getCenterPoint(
|
|
68
|
+
cdp: CdpClient,
|
|
69
|
+
backendNodeId: number,
|
|
70
|
+
signal?: AbortSignal,
|
|
71
|
+
): Promise<{ x: number; y: number }> {
|
|
72
|
+
const { model } = await cdp.send<{
|
|
73
|
+
model: { content: number[] };
|
|
74
|
+
}>("DOM.getBoxModel", { backendNodeId }, signal);
|
|
75
|
+
const xs = [
|
|
76
|
+
model.content[0]!,
|
|
77
|
+
model.content[2]!,
|
|
78
|
+
model.content[4]!,
|
|
79
|
+
model.content[6]!,
|
|
80
|
+
];
|
|
81
|
+
const ys = [
|
|
82
|
+
model.content[1]!,
|
|
83
|
+
model.content[3]!,
|
|
84
|
+
model.content[5]!,
|
|
85
|
+
model.content[7]!,
|
|
86
|
+
];
|
|
87
|
+
return {
|
|
88
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
89
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Focus an element by `backendNodeId` via `DOM.focus`. */
|
|
94
|
+
export async function focusElement(
|
|
95
|
+
cdp: CdpClient,
|
|
96
|
+
backendNodeId: number,
|
|
97
|
+
signal?: AbortSignal,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
await cdp.send("DOM.focus", { backendNodeId }, signal);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Mouse / keyboard / wheel dispatch ─────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Dispatch a full left-click (mouseMoved + mousePressed + mouseReleased)
|
|
106
|
+
* at the given viewport point.
|
|
107
|
+
*/
|
|
108
|
+
export async function dispatchClickAt(
|
|
109
|
+
cdp: CdpClient,
|
|
110
|
+
point: { x: number; y: number },
|
|
111
|
+
signal?: AbortSignal,
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
const base = { x: point.x, y: point.y, button: "left", clickCount: 1 };
|
|
114
|
+
await cdp.send(
|
|
115
|
+
"Input.dispatchMouseEvent",
|
|
116
|
+
{ ...base, type: "mouseMoved" },
|
|
117
|
+
signal,
|
|
118
|
+
);
|
|
119
|
+
await cdp.send(
|
|
120
|
+
"Input.dispatchMouseEvent",
|
|
121
|
+
{ ...base, type: "mousePressed" },
|
|
122
|
+
signal,
|
|
123
|
+
);
|
|
124
|
+
await cdp.send(
|
|
125
|
+
"Input.dispatchMouseEvent",
|
|
126
|
+
{ ...base, type: "mouseReleased" },
|
|
127
|
+
signal,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Dispatch a single mouseMoved (hover) at the given viewport point. */
|
|
132
|
+
export async function dispatchHoverAt(
|
|
133
|
+
cdp: CdpClient,
|
|
134
|
+
point: { x: number; y: number },
|
|
135
|
+
signal?: AbortSignal,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
await cdp.send(
|
|
138
|
+
"Input.dispatchMouseEvent",
|
|
139
|
+
{ type: "mouseMoved", x: point.x, y: point.y, button: "none" },
|
|
140
|
+
signal,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Insert text at the currently focused element via `Input.insertText`.
|
|
146
|
+
* Unlike synthesizing individual key events, this dispatches the right
|
|
147
|
+
* `input`/`change` events that form controls expect.
|
|
148
|
+
*/
|
|
149
|
+
export async function dispatchInsertText(
|
|
150
|
+
cdp: CdpClient,
|
|
151
|
+
text: string,
|
|
152
|
+
signal?: AbortSignal,
|
|
153
|
+
): Promise<void> {
|
|
154
|
+
await cdp.send("Input.insertText", { text }, signal);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Per-key descriptor used by {@link dispatchKeyPress}. Mirrors the
|
|
159
|
+
* fields CDP's `Input.dispatchKeyEvent` accepts. `text` is set only
|
|
160
|
+
* for printable keys (so we know to also dispatch a `char` event).
|
|
161
|
+
*/
|
|
162
|
+
interface KeyDescriptor {
|
|
163
|
+
key: string;
|
|
164
|
+
code: string;
|
|
165
|
+
windowsVirtualKeyCode: number;
|
|
166
|
+
text?: string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Subset of the US keyboard layout used to populate
|
|
171
|
+
* `Input.dispatchKeyEvent` params. Without these fields, sites that
|
|
172
|
+
* read `event.keyCode` (e.g. `event.keyCode === 13` for Enter) or
|
|
173
|
+
* `event.code` see zeros and the press is silently ignored.
|
|
174
|
+
*
|
|
175
|
+
* Single-character keys (a-z, A-Z, 0-9) are resolved dynamically by
|
|
176
|
+
* {@link resolveKeyDescriptor} to keep the static map small.
|
|
177
|
+
*/
|
|
178
|
+
const KEY_DESCRIPTORS: Record<string, KeyDescriptor> = {
|
|
179
|
+
Enter: {
|
|
180
|
+
key: "Enter",
|
|
181
|
+
code: "Enter",
|
|
182
|
+
windowsVirtualKeyCode: 13,
|
|
183
|
+
text: "\r",
|
|
184
|
+
},
|
|
185
|
+
Tab: { key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, text: "\t" },
|
|
186
|
+
Escape: { key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 },
|
|
187
|
+
Backspace: {
|
|
188
|
+
key: "Backspace",
|
|
189
|
+
code: "Backspace",
|
|
190
|
+
windowsVirtualKeyCode: 8,
|
|
191
|
+
},
|
|
192
|
+
Delete: { key: "Delete", code: "Delete", windowsVirtualKeyCode: 46 },
|
|
193
|
+
Insert: { key: "Insert", code: "Insert", windowsVirtualKeyCode: 45 },
|
|
194
|
+
ArrowUp: { key: "ArrowUp", code: "ArrowUp", windowsVirtualKeyCode: 38 },
|
|
195
|
+
ArrowDown: {
|
|
196
|
+
key: "ArrowDown",
|
|
197
|
+
code: "ArrowDown",
|
|
198
|
+
windowsVirtualKeyCode: 40,
|
|
199
|
+
},
|
|
200
|
+
ArrowLeft: {
|
|
201
|
+
key: "ArrowLeft",
|
|
202
|
+
code: "ArrowLeft",
|
|
203
|
+
windowsVirtualKeyCode: 37,
|
|
204
|
+
},
|
|
205
|
+
ArrowRight: {
|
|
206
|
+
key: "ArrowRight",
|
|
207
|
+
code: "ArrowRight",
|
|
208
|
+
windowsVirtualKeyCode: 39,
|
|
209
|
+
},
|
|
210
|
+
// Navigation keys. Sites commonly check `event.keyCode` for these
|
|
211
|
+
// (PageDown = 34 to scroll a page, Home = 36 to jump to top, etc.)
|
|
212
|
+
// so omitting `code`/`windowsVirtualKeyCode` makes the press a
|
|
213
|
+
// silent no-op on those handlers.
|
|
214
|
+
Home: { key: "Home", code: "Home", windowsVirtualKeyCode: 36 },
|
|
215
|
+
End: { key: "End", code: "End", windowsVirtualKeyCode: 35 },
|
|
216
|
+
PageUp: { key: "PageUp", code: "PageUp", windowsVirtualKeyCode: 33 },
|
|
217
|
+
PageDown: { key: "PageDown", code: "PageDown", windowsVirtualKeyCode: 34 },
|
|
218
|
+
// Space is special: Playwright callers use "Space" as the key name
|
|
219
|
+
// but `event.key` is actually " ". Accept both spellings so either
|
|
220
|
+
// calling convention works, and always emit `code: "Space"` +
|
|
221
|
+
// `windowsVirtualKeyCode: 32` so Space-to-activate / Space-to-scroll
|
|
222
|
+
// handlers fire correctly.
|
|
223
|
+
Space: { key: " ", code: "Space", windowsVirtualKeyCode: 32, text: " " },
|
|
224
|
+
" ": { key: " ", code: "Space", windowsVirtualKeyCode: 32, text: " " },
|
|
225
|
+
// Function keys (F1-F12). Virtual key codes 112-123 per the Windows
|
|
226
|
+
// input API. `resolveKeyDescriptor` cannot derive these dynamically
|
|
227
|
+
// because they are multi-character names with no 1:1 char mapping.
|
|
228
|
+
F1: { key: "F1", code: "F1", windowsVirtualKeyCode: 112 },
|
|
229
|
+
F2: { key: "F2", code: "F2", windowsVirtualKeyCode: 113 },
|
|
230
|
+
F3: { key: "F3", code: "F3", windowsVirtualKeyCode: 114 },
|
|
231
|
+
F4: { key: "F4", code: "F4", windowsVirtualKeyCode: 115 },
|
|
232
|
+
F5: { key: "F5", code: "F5", windowsVirtualKeyCode: 116 },
|
|
233
|
+
F6: { key: "F6", code: "F6", windowsVirtualKeyCode: 117 },
|
|
234
|
+
F7: { key: "F7", code: "F7", windowsVirtualKeyCode: 118 },
|
|
235
|
+
F8: { key: "F8", code: "F8", windowsVirtualKeyCode: 119 },
|
|
236
|
+
F9: { key: "F9", code: "F9", windowsVirtualKeyCode: 120 },
|
|
237
|
+
F10: { key: "F10", code: "F10", windowsVirtualKeyCode: 121 },
|
|
238
|
+
F11: { key: "F11", code: "F11", windowsVirtualKeyCode: 122 },
|
|
239
|
+
F12: { key: "F12", code: "F12", windowsVirtualKeyCode: 123 },
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve a key name into a {@link KeyDescriptor}. Single-character
|
|
244
|
+
* keys (a-z, A-Z, 0-9) are computed on demand: `code` is `KeyA`/
|
|
245
|
+
* `Digit0`/etc., `windowsVirtualKeyCode` is the uppercase ASCII code,
|
|
246
|
+
* and `text` is the literal character. Returns `null` for unknown
|
|
247
|
+
* multi-character keys so callers can fall back to a minimal event.
|
|
248
|
+
*/
|
|
249
|
+
function resolveKeyDescriptor(key: string): KeyDescriptor | null {
|
|
250
|
+
const fromMap = KEY_DESCRIPTORS[key];
|
|
251
|
+
if (fromMap) return fromMap;
|
|
252
|
+
if (key.length !== 1) return null;
|
|
253
|
+
const charCode = key.charCodeAt(0);
|
|
254
|
+
// a-z / A-Z
|
|
255
|
+
if (
|
|
256
|
+
(charCode >= 65 && charCode <= 90) ||
|
|
257
|
+
(charCode >= 97 && charCode <= 122)
|
|
258
|
+
) {
|
|
259
|
+
const upper = key.toUpperCase();
|
|
260
|
+
return {
|
|
261
|
+
key,
|
|
262
|
+
code: `Key${upper}`,
|
|
263
|
+
windowsVirtualKeyCode: upper.charCodeAt(0),
|
|
264
|
+
text: key,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
// 0-9
|
|
268
|
+
if (charCode >= 48 && charCode <= 57) {
|
|
269
|
+
return {
|
|
270
|
+
key,
|
|
271
|
+
code: `Digit${key}`,
|
|
272
|
+
windowsVirtualKeyCode: charCode,
|
|
273
|
+
text: key,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Other printable ASCII (space, punctuation): still emit text + the
|
|
277
|
+
// raw char code so sites that check `event.key` and `event.charCode`
|
|
278
|
+
// see something sensible.
|
|
279
|
+
if (charCode >= 32 && charCode <= 126) {
|
|
280
|
+
return {
|
|
281
|
+
key,
|
|
282
|
+
code: "",
|
|
283
|
+
windowsVirtualKeyCode: charCode,
|
|
284
|
+
text: key,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Press a single key (keyDown + optional `char` + keyUp). Resolves
|
|
292
|
+
* the key name to a {@link KeyDescriptor} so CDP receives the right
|
|
293
|
+
* `code` / `windowsVirtualKeyCode` / `text` fields — required by
|
|
294
|
+
* sites that check `event.keyCode` (e.g. Enter-to-submit) or
|
|
295
|
+
* `event.code`. For printable keys we also dispatch a `char` event
|
|
296
|
+
* between keyDown and keyUp so the character is actually inserted
|
|
297
|
+
* into focused inputs.
|
|
298
|
+
*/
|
|
299
|
+
export async function dispatchKeyPress(
|
|
300
|
+
cdp: CdpClient,
|
|
301
|
+
key: string,
|
|
302
|
+
signal?: AbortSignal,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const desc = resolveKeyDescriptor(key);
|
|
305
|
+
if (!desc) {
|
|
306
|
+
// Unknown multi-character key (e.g. F-keys we have not mapped).
|
|
307
|
+
// Fall back to the minimal payload so callers still see a
|
|
308
|
+
// keyDown/keyUp pair, and warn so we can extend the map.
|
|
309
|
+
|
|
310
|
+
console.warn(
|
|
311
|
+
`dispatchKeyPress: no descriptor for key "${key}", sending minimal event`,
|
|
312
|
+
);
|
|
313
|
+
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key }, signal);
|
|
314
|
+
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key }, signal);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const baseParams: Record<string, unknown> = {
|
|
319
|
+
key: desc.key,
|
|
320
|
+
code: desc.code,
|
|
321
|
+
windowsVirtualKeyCode: desc.windowsVirtualKeyCode,
|
|
322
|
+
};
|
|
323
|
+
if (desc.text !== undefined) {
|
|
324
|
+
baseParams.text = desc.text;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await cdp.send(
|
|
328
|
+
"Input.dispatchKeyEvent",
|
|
329
|
+
{ ...baseParams, type: "keyDown" },
|
|
330
|
+
signal,
|
|
331
|
+
);
|
|
332
|
+
if (desc.text !== undefined) {
|
|
333
|
+
await cdp.send(
|
|
334
|
+
"Input.dispatchKeyEvent",
|
|
335
|
+
{ ...baseParams, type: "char" },
|
|
336
|
+
signal,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
await cdp.send(
|
|
340
|
+
"Input.dispatchKeyEvent",
|
|
341
|
+
{ ...baseParams, type: "keyUp" },
|
|
342
|
+
signal,
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Dispatch a wheel scroll delta at the given viewport point. */
|
|
347
|
+
export async function dispatchWheelScroll(
|
|
348
|
+
cdp: CdpClient,
|
|
349
|
+
point: { x: number; y: number },
|
|
350
|
+
delta: { deltaX: number; deltaY: number },
|
|
351
|
+
signal?: AbortSignal,
|
|
352
|
+
): Promise<void> {
|
|
353
|
+
await cdp.send(
|
|
354
|
+
"Input.dispatchMouseEvent",
|
|
355
|
+
{
|
|
356
|
+
type: "mouseWheel",
|
|
357
|
+
x: point.x,
|
|
358
|
+
y: point.y,
|
|
359
|
+
deltaX: delta.deltaX,
|
|
360
|
+
deltaY: delta.deltaY,
|
|
361
|
+
},
|
|
362
|
+
signal,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ── Runtime.evaluate wrappers ─────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
/** Get the current page URL via `Runtime.evaluate("document.location.href")`. */
|
|
369
|
+
export async function getCurrentUrl(
|
|
370
|
+
cdp: CdpClient,
|
|
371
|
+
signal?: AbortSignal,
|
|
372
|
+
): Promise<string> {
|
|
373
|
+
const { result } = await cdp.send<{ result: { value: string } }>(
|
|
374
|
+
"Runtime.evaluate",
|
|
375
|
+
{ expression: "document.location.href", returnByValue: true },
|
|
376
|
+
signal,
|
|
377
|
+
);
|
|
378
|
+
return result.value;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Get the current page title via `Runtime.evaluate("document.title")`. */
|
|
382
|
+
export async function getPageTitle(
|
|
383
|
+
cdp: CdpClient,
|
|
384
|
+
signal?: AbortSignal,
|
|
385
|
+
): Promise<string> {
|
|
386
|
+
const { result } = await cdp.send<{ result: { value: string } }>(
|
|
387
|
+
"Runtime.evaluate",
|
|
388
|
+
{ expression: "document.title", returnByValue: true },
|
|
389
|
+
signal,
|
|
390
|
+
);
|
|
391
|
+
return result.value ?? "";
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Evaluate a JS expression via `Runtime.evaluate` and return the
|
|
396
|
+
* deserialized value. Throws {@link CdpError} with `code: "cdp_error"`
|
|
397
|
+
* if the expression threw (surfaced via CDP's `exceptionDetails`).
|
|
398
|
+
*
|
|
399
|
+
* Defaults: `returnByValue: true`, `awaitPromise: true`, `userGesture: true`.
|
|
400
|
+
*/
|
|
401
|
+
export async function evaluateExpression<T = unknown>(
|
|
402
|
+
cdp: CdpClient,
|
|
403
|
+
expression: string,
|
|
404
|
+
opts?: { awaitPromise?: boolean },
|
|
405
|
+
signal?: AbortSignal,
|
|
406
|
+
): Promise<T> {
|
|
407
|
+
const res = await cdp.send<{
|
|
408
|
+
result: { value: T };
|
|
409
|
+
exceptionDetails?: {
|
|
410
|
+
text?: string;
|
|
411
|
+
exception?: { description?: string };
|
|
412
|
+
};
|
|
413
|
+
}>(
|
|
414
|
+
"Runtime.evaluate",
|
|
415
|
+
{
|
|
416
|
+
expression,
|
|
417
|
+
returnByValue: true,
|
|
418
|
+
awaitPromise: opts?.awaitPromise ?? true,
|
|
419
|
+
userGesture: true,
|
|
420
|
+
},
|
|
421
|
+
signal,
|
|
422
|
+
);
|
|
423
|
+
if (res.exceptionDetails) {
|
|
424
|
+
const msg =
|
|
425
|
+
res.exceptionDetails.exception?.description ??
|
|
426
|
+
res.exceptionDetails.text ??
|
|
427
|
+
"Runtime.evaluate exception";
|
|
428
|
+
throw new CdpError("cdp_error", msg, {
|
|
429
|
+
cdpMethod: "Runtime.evaluate",
|
|
430
|
+
cdpParams: { expression },
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
return res.result.value;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// ── Screenshot ────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Capture a JPEG screenshot via `Page.captureScreenshot` and return the
|
|
440
|
+
* decoded bytes as a Node `Buffer`. Defaults to quality 80. Pass
|
|
441
|
+
* `fullPage: true` to capture beyond the viewport.
|
|
442
|
+
*/
|
|
443
|
+
export async function captureScreenshotJpeg(
|
|
444
|
+
cdp: CdpClient,
|
|
445
|
+
opts: { quality?: number; fullPage?: boolean } = {},
|
|
446
|
+
signal?: AbortSignal,
|
|
447
|
+
): Promise<Buffer> {
|
|
448
|
+
const { data } = await cdp.send<{ data: string }>(
|
|
449
|
+
"Page.captureScreenshot",
|
|
450
|
+
{
|
|
451
|
+
format: "jpeg",
|
|
452
|
+
quality: opts.quality ?? 80,
|
|
453
|
+
captureBeyondViewport: opts.fullPage === true,
|
|
454
|
+
},
|
|
455
|
+
signal,
|
|
456
|
+
);
|
|
457
|
+
return Buffer.from(data, "base64");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Navigation / waiting ──────────────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Navigate to `url` and wait until the new document has committed
|
|
464
|
+
* (the URL has changed from the pre-navigation URL, or it's a
|
|
465
|
+
* same-URL reload) AND `document.readyState` has reached
|
|
466
|
+
* `interactive` or `complete`, or the timeout elapses.
|
|
467
|
+
*
|
|
468
|
+
* CDP's `Page.navigate` resolves as soon as the request is sent, not
|
|
469
|
+
* when the page has loaded. Subscribing to lifecycle events would
|
|
470
|
+
* require a long-lived event channel that the extension-backed
|
|
471
|
+
* CdpClient cannot currently provide, so this helper polls both
|
|
472
|
+
* `document.readyState` and `document.location.href` via
|
|
473
|
+
* {@link evaluateExpression} — which works uniformly across both
|
|
474
|
+
* Playwright-backed and extension-backed clients.
|
|
475
|
+
*
|
|
476
|
+
* The commit-detection step is the interesting part: on same-origin
|
|
477
|
+
* navigations or cached responses, the browser can return
|
|
478
|
+
* `readyState === "complete"` from the OLD execution context for a
|
|
479
|
+
* brief window after `Page.navigate` resolves but before the new
|
|
480
|
+
* document has been installed. Reading only `readyState` would
|
|
481
|
+
* accept that stale state and report success against the old URL.
|
|
482
|
+
* Combining the two observations in a single evaluate and requiring
|
|
483
|
+
* an observed URL change closes that race.
|
|
484
|
+
*
|
|
485
|
+
* Returns `{ finalUrl, timedOut }`. `finalUrl` is the last `href`
|
|
486
|
+
* observed inside the polling loop (so it reflects the new document
|
|
487
|
+
* even on commit races) and may differ from `url` if the page
|
|
488
|
+
* redirected.
|
|
489
|
+
*/
|
|
490
|
+
export async function navigateAndWait(
|
|
491
|
+
cdp: CdpClient,
|
|
492
|
+
url: string,
|
|
493
|
+
opts: { timeoutMs?: number } = {},
|
|
494
|
+
signal?: AbortSignal,
|
|
495
|
+
): Promise<{ finalUrl: string; timedOut: boolean }> {
|
|
496
|
+
const timeoutMs = opts.timeoutMs ?? 15_000;
|
|
497
|
+
|
|
498
|
+
// Capture the pre-navigation URL so the polling loop can detect
|
|
499
|
+
// when the new document has committed. If the pre-read fails (rare
|
|
500
|
+
// — e.g. a fresh about:blank that hasn't initialized a Runtime
|
|
501
|
+
// context yet), we fall back to readyState-only polling because we
|
|
502
|
+
// have no baseline to compare against.
|
|
503
|
+
let urlBeforeNav = "";
|
|
504
|
+
try {
|
|
505
|
+
urlBeforeNav = await getCurrentUrl(cdp, signal);
|
|
506
|
+
} catch {
|
|
507
|
+
// Non-fatal: urlBeforeNav stays empty and commit detection becomes
|
|
508
|
+
// a no-op (see `committed` below).
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// CDP's `Page.navigate` does NOT throw on transport-layer errors
|
|
512
|
+
// (DNS failure, connection refused, etc.). Instead it resolves with
|
|
513
|
+
// `{ frameId, errorText? }` and we have to surface the failure
|
|
514
|
+
// ourselves. Otherwise we silently start polling readyState on the
|
|
515
|
+
// OLD page (which is "complete") and report success with the stale
|
|
516
|
+
// URL.
|
|
517
|
+
const navResp = await cdp.send<{ frameId?: string; errorText?: string }>(
|
|
518
|
+
"Page.navigate",
|
|
519
|
+
{ url },
|
|
520
|
+
signal,
|
|
521
|
+
);
|
|
522
|
+
if (navResp?.errorText) {
|
|
523
|
+
throw new CdpError("cdp_error", navResp.errorText, {
|
|
524
|
+
cdpMethod: "Page.navigate",
|
|
525
|
+
cdpParams: { url },
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Same-URL reloads (including `about:blank` → `about:blank`) can't
|
|
530
|
+
// be detected via URL change. Fall back to readyState-only polling
|
|
531
|
+
// in that case, matching the pre-commit-detection behavior.
|
|
532
|
+
const sameUrlReload = urlBeforeNav !== "" && url === urlBeforeNav;
|
|
533
|
+
|
|
534
|
+
const startedAt = Date.now();
|
|
535
|
+
// Track exit reason explicitly so the post-loop classification does
|
|
536
|
+
// not race against `Date.now()` (the final read could otherwise
|
|
537
|
+
// push us across the timeout boundary and falsely flip `timedOut`
|
|
538
|
+
// back to true).
|
|
539
|
+
let completed = false;
|
|
540
|
+
// Track the last href we successfully observed from inside the
|
|
541
|
+
// loop. We prefer this to a post-loop `getCurrentUrl` call because
|
|
542
|
+
// the latter races the very same commit window that motivates the
|
|
543
|
+
// in-loop commit check.
|
|
544
|
+
let lastKnownHref = urlBeforeNav;
|
|
545
|
+
|
|
546
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
547
|
+
if (signal?.aborted) {
|
|
548
|
+
throw new CdpError("aborted", "Navigation aborted");
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Query `readyState` and `location.href` in a single evaluate so
|
|
552
|
+
// the two observations come from the same execution context and
|
|
553
|
+
// cannot straddle a commit boundary.
|
|
554
|
+
try {
|
|
555
|
+
const snapshot = await evaluateExpression<{
|
|
556
|
+
readyState: string;
|
|
557
|
+
href: string;
|
|
558
|
+
}>(
|
|
559
|
+
cdp,
|
|
560
|
+
"({ readyState: document.readyState, href: document.location.href })",
|
|
561
|
+
{},
|
|
562
|
+
signal,
|
|
563
|
+
);
|
|
564
|
+
if (snapshot && typeof snapshot.href === "string") {
|
|
565
|
+
lastKnownHref = snapshot.href;
|
|
566
|
+
}
|
|
567
|
+
const readyStateOk =
|
|
568
|
+
snapshot?.readyState === "interactive" ||
|
|
569
|
+
snapshot?.readyState === "complete";
|
|
570
|
+
// On cross-URL navigations, require BOTH a ready readyState
|
|
571
|
+
// AND an observed URL change so we don't accept the OLD
|
|
572
|
+
// page's "complete" state before the new document has
|
|
573
|
+
// committed. Same-URL reloads and missing pre-nav URLs fall
|
|
574
|
+
// back to readyState-only because there's nothing to compare.
|
|
575
|
+
const committed =
|
|
576
|
+
sameUrlReload ||
|
|
577
|
+
urlBeforeNav === "" ||
|
|
578
|
+
(typeof snapshot?.href === "string" && snapshot.href !== urlBeforeNav);
|
|
579
|
+
if (readyStateOk && committed) {
|
|
580
|
+
completed = true;
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
} catch (err) {
|
|
584
|
+
// `Runtime.evaluate` can fail transiently while the old
|
|
585
|
+
// execution context is being torn down and the new one has
|
|
586
|
+
// not yet been created ("Execution context was destroyed" /
|
|
587
|
+
// "Cannot find context with specified id"). Treat CDP errors
|
|
588
|
+
// as retry-worthy; the timeout bound below guarantees we
|
|
589
|
+
// don't loop forever. Abort errors are re-thrown so the
|
|
590
|
+
// caller's AbortSignal is still honoured promptly.
|
|
591
|
+
if (err instanceof CdpError && err.code === "aborted") throw err;
|
|
592
|
+
if (!(err instanceof CdpError)) throw err;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const timedOut = !completed;
|
|
599
|
+
|
|
600
|
+
// Prefer the last href observed inside the loop. If the loop never
|
|
601
|
+
// produced a successful observation (e.g. all evaluates failed to
|
|
602
|
+
// transient context errors), fall back to `getCurrentUrl` as a
|
|
603
|
+
// best-effort read.
|
|
604
|
+
let finalUrl = lastKnownHref;
|
|
605
|
+
if (finalUrl === "") {
|
|
606
|
+
try {
|
|
607
|
+
finalUrl = await getCurrentUrl(cdp, signal);
|
|
608
|
+
} catch {
|
|
609
|
+
// Nothing more to do — surface empty string and let the caller
|
|
610
|
+
// decide how to render it.
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return { finalUrl, timedOut };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Poll until a selector matches an element in the requested state,
|
|
618
|
+
* then return its `backendNodeId`. Throws {@link CdpError} on timeout
|
|
619
|
+
* or abort.
|
|
620
|
+
*
|
|
621
|
+
* `state` controls the readiness check:
|
|
622
|
+
* - `"visible"` (default): the element must be in the DOM AND have a
|
|
623
|
+
* non-zero bounding box AND not be `display:none` /
|
|
624
|
+
* `visibility:hidden`. This matches Playwright's
|
|
625
|
+
* `page.waitForSelector` default and is the right semantics for
|
|
626
|
+
* click/hover targets that may be hydrated asynchronously.
|
|
627
|
+
* - `"attached"`: the element only needs to exist in the DOM. Useful
|
|
628
|
+
* for `browser_wait_for` selector mode where the caller just wants
|
|
629
|
+
* to know "did this node appear at all" regardless of layout.
|
|
630
|
+
*/
|
|
631
|
+
export async function waitForSelector(
|
|
632
|
+
cdp: CdpClient,
|
|
633
|
+
selector: string,
|
|
634
|
+
timeoutMs: number,
|
|
635
|
+
signal?: AbortSignal,
|
|
636
|
+
opts: { state?: "attached" | "visible" } = {},
|
|
637
|
+
): Promise<number> {
|
|
638
|
+
const state = opts.state ?? "visible";
|
|
639
|
+
const startedAt = Date.now();
|
|
640
|
+
const escapedSel = JSON.stringify(selector);
|
|
641
|
+
const expression =
|
|
642
|
+
state === "visible"
|
|
643
|
+
? `(() => {
|
|
644
|
+
const el = document.querySelector(${escapedSel});
|
|
645
|
+
if (!el) return false;
|
|
646
|
+
const r = el.getBoundingClientRect();
|
|
647
|
+
const cs = getComputedStyle(el);
|
|
648
|
+
return r.width > 0 && r.height > 0 && cs.display !== "none" && cs.visibility !== "hidden";
|
|
649
|
+
})()`
|
|
650
|
+
: `document.querySelector(${escapedSel}) !== null`;
|
|
651
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
652
|
+
if (signal?.aborted) {
|
|
653
|
+
throw new CdpError("aborted", "waitForSelector aborted");
|
|
654
|
+
}
|
|
655
|
+
const ready = await evaluateExpression<boolean>(
|
|
656
|
+
cdp,
|
|
657
|
+
expression,
|
|
658
|
+
{},
|
|
659
|
+
signal,
|
|
660
|
+
);
|
|
661
|
+
if (ready) {
|
|
662
|
+
return await querySelectorBackendNodeId(cdp, selector, signal);
|
|
663
|
+
}
|
|
664
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
665
|
+
}
|
|
666
|
+
throw new CdpError("cdp_error", `Timed out waiting for ${selector}`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Poll `document.body.innerText` for a substring. Throws
|
|
671
|
+
* {@link CdpError} on timeout or abort.
|
|
672
|
+
*/
|
|
673
|
+
export async function waitForText(
|
|
674
|
+
cdp: CdpClient,
|
|
675
|
+
text: string,
|
|
676
|
+
timeoutMs: number,
|
|
677
|
+
signal?: AbortSignal,
|
|
678
|
+
): Promise<void> {
|
|
679
|
+
const escaped = JSON.stringify(text);
|
|
680
|
+
const startedAt = Date.now();
|
|
681
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
682
|
+
if (signal?.aborted) {
|
|
683
|
+
throw new CdpError("aborted", "waitForText aborted");
|
|
684
|
+
}
|
|
685
|
+
const found = await evaluateExpression<boolean>(
|
|
686
|
+
cdp,
|
|
687
|
+
`(document.body?.innerText ?? "").includes(${escaped})`,
|
|
688
|
+
{},
|
|
689
|
+
signal,
|
|
690
|
+
);
|
|
691
|
+
if (found) return;
|
|
692
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
693
|
+
}
|
|
694
|
+
throw new CdpError("cdp_error", `Timed out waiting for text: ${text}`);
|
|
695
|
+
}
|