@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
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Protocol types for the Chrome extension relay bridge.
|
|
3
|
-
*
|
|
4
|
-
* Messages flow:
|
|
5
|
-
* Assistant → ExtensionRelayServer → WebSocket → Chrome Extension → Tab (JS eval)
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface CookieSpec {
|
|
9
|
-
url: string;
|
|
10
|
-
name: string;
|
|
11
|
-
value: string;
|
|
12
|
-
domain?: string;
|
|
13
|
-
path?: string;
|
|
14
|
-
secure?: boolean;
|
|
15
|
-
httpOnly?: boolean;
|
|
16
|
-
expirationDate?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Command sent from the server to the extension over WebSocket.
|
|
21
|
-
*/
|
|
22
|
-
export interface ExtensionCommand {
|
|
23
|
-
id: string; // UUID
|
|
24
|
-
action:
|
|
25
|
-
| "evaluate"
|
|
26
|
-
| "navigate"
|
|
27
|
-
| "get_cookies"
|
|
28
|
-
| "set_cookie"
|
|
29
|
-
| "screenshot"
|
|
30
|
-
| "find_tab"
|
|
31
|
-
| "new_tab";
|
|
32
|
-
tabId?: number;
|
|
33
|
-
code?: string; // for evaluate
|
|
34
|
-
url?: string; // for navigate / find_tab / new_tab
|
|
35
|
-
domain?: string; // for get_cookies
|
|
36
|
-
cookie?: CookieSpec;
|
|
37
|
-
timeoutMs?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Response sent from the extension back to the server.
|
|
42
|
-
*/
|
|
43
|
-
export interface ExtensionResponse {
|
|
44
|
-
id: string;
|
|
45
|
-
success: boolean;
|
|
46
|
-
result?: unknown;
|
|
47
|
-
error?: string;
|
|
48
|
-
tabId?: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Periodic heartbeat from the extension to the server.
|
|
53
|
-
*/
|
|
54
|
-
export interface ExtensionHeartbeat {
|
|
55
|
-
type: "heartbeat";
|
|
56
|
-
extensionVersion: string;
|
|
57
|
-
connectedTabs: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Any message received from the extension (heartbeat or command response).
|
|
62
|
-
*/
|
|
63
|
-
export type ExtensionInboundMessage = ExtensionHeartbeat | ExtensionResponse;
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket server for the Chrome extension relay bridge.
|
|
3
|
-
*
|
|
4
|
-
* Holds a single active extension connection. Commands are sent as JSON
|
|
5
|
-
* and matched to pending Promises by UUID.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ServerWebSocket } from "bun";
|
|
9
|
-
|
|
10
|
-
import { getLogger } from "../util/logger.js";
|
|
11
|
-
import type {
|
|
12
|
-
ExtensionCommand,
|
|
13
|
-
ExtensionHeartbeat,
|
|
14
|
-
ExtensionInboundMessage,
|
|
15
|
-
ExtensionResponse,
|
|
16
|
-
} from "./protocol.js";
|
|
17
|
-
|
|
18
|
-
const log = getLogger("browser-extension-relay");
|
|
19
|
-
|
|
20
|
-
const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
|
|
21
|
-
|
|
22
|
-
interface PendingCommand {
|
|
23
|
-
resolve: (value: ExtensionResponse) => void;
|
|
24
|
-
reject: (reason: Error) => void;
|
|
25
|
-
timer: ReturnType<typeof setTimeout>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface BrowserRelayWebSocketData {
|
|
29
|
-
wsType: "browser-relay";
|
|
30
|
-
connectionId: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ExtensionRelayStatus {
|
|
34
|
-
connected: boolean;
|
|
35
|
-
connectionId: string | null;
|
|
36
|
-
lastHeartbeatAt: number | null;
|
|
37
|
-
pendingCommandCount: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Manages the single active Chrome extension WebSocket connection and
|
|
42
|
-
* dispatches commands to it.
|
|
43
|
-
*/
|
|
44
|
-
export class ExtensionRelayServer {
|
|
45
|
-
private ws: ServerWebSocket<BrowserRelayWebSocketData> | null = null;
|
|
46
|
-
private connectionId: string | null = null;
|
|
47
|
-
private lastHeartbeatAt: number | null = null;
|
|
48
|
-
private pendingCommands = new Map<string, PendingCommand>();
|
|
49
|
-
|
|
50
|
-
// ── WebSocket lifecycle ────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
handleOpen(ws: ServerWebSocket<BrowserRelayWebSocketData>): void {
|
|
53
|
-
const newId = ws.data.connectionId;
|
|
54
|
-
|
|
55
|
-
if (this.ws) {
|
|
56
|
-
log.warn(
|
|
57
|
-
{ oldConnectionId: this.connectionId, newConnectionId: newId },
|
|
58
|
-
"Browser extension relay: new connection displaced an existing one",
|
|
59
|
-
);
|
|
60
|
-
try {
|
|
61
|
-
this.ws.close(1001, "Displaced by new connection");
|
|
62
|
-
} catch {
|
|
63
|
-
// best-effort
|
|
64
|
-
}
|
|
65
|
-
this.rejectAllPending(
|
|
66
|
-
new Error("Extension reconnected — previous commands cancelled"),
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
this.ws = ws;
|
|
71
|
-
this.connectionId = newId;
|
|
72
|
-
log.info({ connectionId: newId }, "Browser extension relay connected");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
handleMessage(
|
|
76
|
-
ws: ServerWebSocket<BrowserRelayWebSocketData>,
|
|
77
|
-
raw: string,
|
|
78
|
-
): void {
|
|
79
|
-
let msg: ExtensionInboundMessage;
|
|
80
|
-
try {
|
|
81
|
-
msg = JSON.parse(raw) as ExtensionInboundMessage;
|
|
82
|
-
} catch {
|
|
83
|
-
log.warn(
|
|
84
|
-
{ connectionId: ws.data.connectionId },
|
|
85
|
-
"Browser extension relay: failed to parse message",
|
|
86
|
-
);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if ("type" in msg && msg.type === "heartbeat") {
|
|
91
|
-
this.handleHeartbeat(msg);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Otherwise it's a command response
|
|
96
|
-
const response = msg as ExtensionResponse;
|
|
97
|
-
const pending = this.pendingCommands.get(response.id);
|
|
98
|
-
if (!pending) {
|
|
99
|
-
log.warn(
|
|
100
|
-
{ id: response.id },
|
|
101
|
-
"Browser extension relay: received response for unknown command id",
|
|
102
|
-
);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
clearTimeout(pending.timer);
|
|
107
|
-
this.pendingCommands.delete(response.id);
|
|
108
|
-
pending.resolve(response);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
handleClose(
|
|
112
|
-
ws: ServerWebSocket<BrowserRelayWebSocketData>,
|
|
113
|
-
code: number,
|
|
114
|
-
reason?: string,
|
|
115
|
-
): void {
|
|
116
|
-
const closedId = ws.data.connectionId;
|
|
117
|
-
if (this.connectionId !== closedId) {
|
|
118
|
-
// Stale close for a displaced connection — ignore
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
log.info(
|
|
123
|
-
{ connectionId: closedId, code, reason },
|
|
124
|
-
"Browser extension relay disconnected",
|
|
125
|
-
);
|
|
126
|
-
this.ws = null;
|
|
127
|
-
this.connectionId = null;
|
|
128
|
-
this.rejectAllPending(new Error(`Extension disconnected (code=${code})`));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// ── Command dispatch ───────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Send a command to the extension and wait for its response.
|
|
135
|
-
*/
|
|
136
|
-
sendCommand(
|
|
137
|
-
command: Omit<ExtensionCommand, "id">,
|
|
138
|
-
timeoutMs: number = DEFAULT_COMMAND_TIMEOUT_MS,
|
|
139
|
-
): Promise<ExtensionResponse> {
|
|
140
|
-
if (!this.ws) {
|
|
141
|
-
return Promise.reject(new Error("Browser extension is not connected"));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const id = crypto.randomUUID();
|
|
145
|
-
const fullCommand: ExtensionCommand = { ...command, id };
|
|
146
|
-
|
|
147
|
-
return new Promise<ExtensionResponse>((resolve, reject) => {
|
|
148
|
-
const timer = setTimeout(() => {
|
|
149
|
-
this.pendingCommands.delete(id);
|
|
150
|
-
reject(
|
|
151
|
-
new Error(
|
|
152
|
-
`Browser extension command timed out after ${timeoutMs}ms (action=${command.action})`,
|
|
153
|
-
),
|
|
154
|
-
);
|
|
155
|
-
}, timeoutMs);
|
|
156
|
-
|
|
157
|
-
this.pendingCommands.set(id, { resolve, reject, timer });
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
this.ws!.send(JSON.stringify(fullCommand));
|
|
161
|
-
} catch (err) {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
this.pendingCommands.delete(id);
|
|
164
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// ── Status ─────────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
getStatus(): ExtensionRelayStatus {
|
|
172
|
-
return {
|
|
173
|
-
connected: !!this.ws,
|
|
174
|
-
connectionId: this.connectionId,
|
|
175
|
-
lastHeartbeatAt: this.lastHeartbeatAt,
|
|
176
|
-
pendingCommandCount: this.pendingCommands.size,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ── Private helpers ────────────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
private handleHeartbeat(msg: ExtensionHeartbeat): void {
|
|
183
|
-
this.lastHeartbeatAt = Date.now();
|
|
184
|
-
log.debug(
|
|
185
|
-
{
|
|
186
|
-
extensionVersion: msg.extensionVersion,
|
|
187
|
-
connectedTabs: msg.connectedTabs,
|
|
188
|
-
},
|
|
189
|
-
"Browser extension heartbeat received",
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private rejectAllPending(err: Error): void {
|
|
194
|
-
for (const [id, pending] of this.pendingCommands) {
|
|
195
|
-
clearTimeout(pending.timer);
|
|
196
|
-
pending.reject(err);
|
|
197
|
-
this.pendingCommands.delete(id);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Module-level singleton — imported by http-server and client. */
|
|
203
|
-
export const extensionRelayServer = new ExtensionRelayServer();
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const SandboxConfigSchema = z
|
|
4
|
-
.object({
|
|
5
|
-
enabled: z
|
|
6
|
-
.boolean({ error: "sandbox.enabled must be a boolean" })
|
|
7
|
-
.default(false)
|
|
8
|
-
.describe(
|
|
9
|
-
"Whether to run tool executions in a sandboxed environment for safety",
|
|
10
|
-
),
|
|
11
|
-
})
|
|
12
|
-
.describe("Sandbox configuration for isolating tool executions");
|
|
13
|
-
|
|
14
|
-
export type SandboxConfig = z.infer<typeof SandboxConfigSchema>;
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Singleton runtime store for the two-axis permission mode.
|
|
3
|
-
*
|
|
4
|
-
* Reads initial state from the `permissions` section of config.json on
|
|
5
|
-
* initialization and persists mutations back to the config file via the
|
|
6
|
-
* raw-config read/write helpers so env-var–derived keys are never leaked
|
|
7
|
-
* to disk.
|
|
8
|
-
*
|
|
9
|
-
* Downstream consumers (e.g. SSE broadcast) register change listeners
|
|
10
|
-
* via `onModeChanged()`.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
invalidateConfigCache,
|
|
15
|
-
loadConfig,
|
|
16
|
-
loadRawConfig,
|
|
17
|
-
saveRawConfig,
|
|
18
|
-
} from "../config/loader.js";
|
|
19
|
-
import { getLogger } from "../util/logger.js";
|
|
20
|
-
import type { PermissionMode } from "./permission-mode.js";
|
|
21
|
-
import { DEFAULT_PERMISSION_MODE } from "./permission-mode.js";
|
|
22
|
-
|
|
23
|
-
const log = getLogger("permission-mode-store");
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Types
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
export type ModeChangeListener = (mode: PermissionMode) => void;
|
|
30
|
-
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
// Module-level state
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
let currentMode: PermissionMode = { ...DEFAULT_PERMISSION_MODE };
|
|
36
|
-
let initialized = false;
|
|
37
|
-
const listeners: ModeChangeListener[] = [];
|
|
38
|
-
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
40
|
-
// Internal helpers
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
|
|
43
|
-
function notifyListeners(): void {
|
|
44
|
-
const snapshot = { ...currentMode };
|
|
45
|
-
for (const listener of listeners) {
|
|
46
|
-
try {
|
|
47
|
-
listener(snapshot);
|
|
48
|
-
} catch (err) {
|
|
49
|
-
log.error({ err }, "Error in permission mode change listener");
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Persist the current in-memory permission mode to config.json.
|
|
56
|
-
*
|
|
57
|
-
* Uses the raw-config pattern (loadRawConfig → mutate → saveRawConfig) so
|
|
58
|
-
* that env-var–derived fields (API keys, dataDir) are never written to disk.
|
|
59
|
-
*/
|
|
60
|
-
function persistToConfig(): void {
|
|
61
|
-
try {
|
|
62
|
-
const raw = loadRawConfig();
|
|
63
|
-
|
|
64
|
-
// Ensure the permissions object exists
|
|
65
|
-
if (
|
|
66
|
-
raw.permissions == null ||
|
|
67
|
-
typeof raw.permissions !== "object" ||
|
|
68
|
-
Array.isArray(raw.permissions)
|
|
69
|
-
) {
|
|
70
|
-
raw.permissions = {};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const permissions = raw.permissions as Record<string, unknown>;
|
|
74
|
-
permissions.askBeforeActing = currentMode.askBeforeActing;
|
|
75
|
-
permissions.hostAccess = currentMode.hostAccess;
|
|
76
|
-
|
|
77
|
-
saveRawConfig(raw);
|
|
78
|
-
|
|
79
|
-
// Invalidate the cached config so the next loadConfig() picks up the
|
|
80
|
-
// persisted values rather than returning stale in-memory state.
|
|
81
|
-
invalidateConfigCache();
|
|
82
|
-
} catch (err) {
|
|
83
|
-
log.error({ err }, "Failed to persist permission mode to config");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Public API
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Initialize the store from the current config. Safe to call multiple times;
|
|
93
|
-
* subsequent calls are no-ops unless `resetForTesting()` has been called.
|
|
94
|
-
*/
|
|
95
|
-
export function initPermissionModeStore(): void {
|
|
96
|
-
if (initialized) return;
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const config = loadConfig();
|
|
100
|
-
currentMode = {
|
|
101
|
-
askBeforeActing: config.permissions.askBeforeActing,
|
|
102
|
-
hostAccess: config.permissions.hostAccess,
|
|
103
|
-
};
|
|
104
|
-
} catch (err) {
|
|
105
|
-
log.warn(
|
|
106
|
-
{ err },
|
|
107
|
-
"Failed to load permission mode from config; using defaults",
|
|
108
|
-
);
|
|
109
|
-
currentMode = { ...DEFAULT_PERMISSION_MODE };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
initialized = true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Return the current permission mode. Initializes from config on first call
|
|
117
|
-
* if `initPermissionModeStore()` hasn't been called yet.
|
|
118
|
-
*/
|
|
119
|
-
export function getMode(): PermissionMode {
|
|
120
|
-
if (!initialized) {
|
|
121
|
-
initPermissionModeStore();
|
|
122
|
-
}
|
|
123
|
-
return { ...currentMode };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Update the `askBeforeActing` axis. Persists to config.json and notifies
|
|
128
|
-
* change listeners.
|
|
129
|
-
*/
|
|
130
|
-
export function setAskBeforeActing(value: boolean): void {
|
|
131
|
-
if (!initialized) {
|
|
132
|
-
initPermissionModeStore();
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (currentMode.askBeforeActing === value) return;
|
|
136
|
-
|
|
137
|
-
currentMode.askBeforeActing = value;
|
|
138
|
-
persistToConfig();
|
|
139
|
-
notifyListeners();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Update the `hostAccess` axis. Persists to config.json and notifies
|
|
144
|
-
* change listeners.
|
|
145
|
-
*/
|
|
146
|
-
export function setHostAccess(value: boolean): void {
|
|
147
|
-
if (!initialized) {
|
|
148
|
-
initPermissionModeStore();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (currentMode.hostAccess === value) return;
|
|
152
|
-
|
|
153
|
-
currentMode.hostAccess = value;
|
|
154
|
-
persistToConfig();
|
|
155
|
-
notifyListeners();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Register a callback that fires whenever the permission mode changes.
|
|
160
|
-
* Returns an unsubscribe function.
|
|
161
|
-
*/
|
|
162
|
-
export function onModeChanged(callback: ModeChangeListener): () => void {
|
|
163
|
-
listeners.push(callback);
|
|
164
|
-
return () => {
|
|
165
|
-
const idx = listeners.indexOf(callback);
|
|
166
|
-
if (idx >= 0) {
|
|
167
|
-
listeners.splice(idx, 1);
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Reset the store to uninitialized state. **Test-only** — production code
|
|
174
|
-
* should never call this.
|
|
175
|
-
*/
|
|
176
|
-
export function resetForTesting(): void {
|
|
177
|
-
currentMode = { ...DEFAULT_PERMISSION_MODE };
|
|
178
|
-
initialized = false;
|
|
179
|
-
listeners.length = 0;
|
|
180
|
-
}
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared Chrome CDP session management.
|
|
3
|
-
*
|
|
4
|
-
* Consolidates the duplicated launch / readiness / window-management logic
|
|
5
|
-
* that was previously copy-pasted across the Amazon and DoorDash CLIs.
|
|
6
|
-
* Callers get back a {@link CdpSession} with structured metadata so they can
|
|
7
|
-
* make cleanup decisions (e.g. only kill Chrome if *we* launched it).
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { execSync, spawn as spawnChild } from "node:child_process";
|
|
11
|
-
import { homedir } from "node:os";
|
|
12
|
-
import { join as pathJoin } from "node:path";
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Constants
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
const DEFAULT_CDP_PORT = 9222;
|
|
19
|
-
const DEFAULT_CDP_BASE = `http://localhost:${DEFAULT_CDP_PORT}`;
|
|
20
|
-
const DEFAULT_USER_DATA_DIR = pathJoin(
|
|
21
|
-
homedir(),
|
|
22
|
-
"Library/Application Support/Google/Chrome-CDP",
|
|
23
|
-
);
|
|
24
|
-
const CHROME_APP_PATH =
|
|
25
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
26
|
-
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Types
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
|
-
export interface CdpSession {
|
|
32
|
-
/** Base URL for the CDP HTTP endpoints (e.g. `http://localhost:9222`). */
|
|
33
|
-
baseUrl: string;
|
|
34
|
-
/** Whether this helper launched Chrome (true) or it was already running (false). */
|
|
35
|
-
launchedByUs: boolean;
|
|
36
|
-
/** The `--user-data-dir` used for the Chrome instance. */
|
|
37
|
-
userDataDir: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface EnsureChromeOptions {
|
|
41
|
-
/** CDP port. Defaults to `9222`. */
|
|
42
|
-
port?: number;
|
|
43
|
-
/** User data directory for Chrome. Defaults to `~/Library/Application Support/Google/Chrome-CDP`. */
|
|
44
|
-
userDataDir?: string;
|
|
45
|
-
/** Initial URL to open when launching Chrome. */
|
|
46
|
-
startUrl?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// Readiness check
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Returns `true` when a CDP endpoint is responding at the given base URL
|
|
55
|
-
* and has at least one open page tab. A CDP endpoint with zero tabs is
|
|
56
|
-
* stale and unusable - callers should treat it as not ready.
|
|
57
|
-
*/
|
|
58
|
-
export async function isCdpReady(
|
|
59
|
-
cdpBase: string = DEFAULT_CDP_BASE,
|
|
60
|
-
): Promise<boolean> {
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch(`${cdpBase}/json/version`);
|
|
63
|
-
if (!res.ok) return false;
|
|
64
|
-
|
|
65
|
-
// Verify there's at least one page tab - a CDP endpoint with no tabs
|
|
66
|
-
// is a stale Chrome process that should be relaunched.
|
|
67
|
-
const listRes = await fetch(`${cdpBase}/json/list`);
|
|
68
|
-
if (!listRes.ok) return false;
|
|
69
|
-
const targets = (await listRes.json()) as Array<{ type: string }>;
|
|
70
|
-
return targets.some((t) => t.type === "page");
|
|
71
|
-
} catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// Launch / ensure
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Ensure a Chrome instance with CDP is available. If one is already listening
|
|
82
|
-
* on the target port, returns immediately. Otherwise spawns a new detached
|
|
83
|
-
* Chrome process and waits for the CDP endpoint to become ready.
|
|
84
|
-
*
|
|
85
|
-
* Returns a {@link CdpSession} with metadata about the running instance.
|
|
86
|
-
*/
|
|
87
|
-
export async function ensureChromeWithCdp(
|
|
88
|
-
options: EnsureChromeOptions = {},
|
|
89
|
-
): Promise<CdpSession> {
|
|
90
|
-
const port = options.port ?? DEFAULT_CDP_PORT;
|
|
91
|
-
const baseUrl = `http://localhost:${port}`;
|
|
92
|
-
const userDataDir = options.userDataDir ?? DEFAULT_USER_DATA_DIR;
|
|
93
|
-
|
|
94
|
-
if (await isCdpReady(baseUrl)) {
|
|
95
|
-
return { baseUrl, launchedByUs: false, userDataDir };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// If CDP is responding but has no tabs (stale), kill the process holding the port.
|
|
99
|
-
try {
|
|
100
|
-
const versionRes = await fetch(`${baseUrl}/json/version`);
|
|
101
|
-
if (versionRes.ok) {
|
|
102
|
-
// Stale Chrome - CDP up but no tabs. Kill it so we can relaunch.
|
|
103
|
-
try {
|
|
104
|
-
execSync(`lsof -ti :${port} | xargs kill -9 2>/dev/null`, {
|
|
105
|
-
stdio: "ignore",
|
|
106
|
-
});
|
|
107
|
-
} catch {
|
|
108
|
-
// Ignore - process may have already exited.
|
|
109
|
-
}
|
|
110
|
-
// Brief wait for port to clear.
|
|
111
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
112
|
-
}
|
|
113
|
-
} catch {
|
|
114
|
-
// CDP not responding at all - port is free, proceed to launch.
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const args = [
|
|
118
|
-
`--remote-debugging-port=${port}`,
|
|
119
|
-
`--force-renderer-accessibility`,
|
|
120
|
-
`--user-data-dir=${userDataDir}`,
|
|
121
|
-
];
|
|
122
|
-
if (options.startUrl) {
|
|
123
|
-
args.push(options.startUrl);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
spawnChild(CHROME_APP_PATH, args, {
|
|
127
|
-
detached: true,
|
|
128
|
-
stdio: "ignore",
|
|
129
|
-
}).unref();
|
|
130
|
-
|
|
131
|
-
// Poll until CDP responds (up to 15 s)
|
|
132
|
-
for (let i = 0; i < 30; i++) {
|
|
133
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
134
|
-
if (await isCdpReady(baseUrl)) {
|
|
135
|
-
return { baseUrl, launchedByUs: true, userDataDir };
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
throw new Error("Chrome started but CDP endpoint not responding after 15s");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Window management helpers
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Look up the first page target and return its WebSocket debugger URL.
|
|
148
|
-
*/
|
|
149
|
-
async function findPageTarget(cdpBase: string): Promise<string | null> {
|
|
150
|
-
const res = await fetch(`${cdpBase}/json/list`);
|
|
151
|
-
const targets = (await res.json()) as Array<{
|
|
152
|
-
type: string;
|
|
153
|
-
webSocketDebuggerUrl: string;
|
|
154
|
-
}>;
|
|
155
|
-
const page = targets.find((t) => t.type === "page");
|
|
156
|
-
return page?.webSocketDebuggerUrl ?? null;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Set the window state of the Chrome window owning the first page target.
|
|
161
|
-
* Used by both minimize and restore.
|
|
162
|
-
*/
|
|
163
|
-
async function setWindowState(
|
|
164
|
-
cdpBase: string,
|
|
165
|
-
windowState: "minimized" | "normal",
|
|
166
|
-
): Promise<void> {
|
|
167
|
-
const wsUrl = await findPageTarget(cdpBase);
|
|
168
|
-
if (!wsUrl) return;
|
|
169
|
-
|
|
170
|
-
const ws = new WebSocket(wsUrl);
|
|
171
|
-
|
|
172
|
-
await new Promise<void>((resolve, reject) => {
|
|
173
|
-
const timeout = setTimeout(() => {
|
|
174
|
-
ws.close();
|
|
175
|
-
reject(new Error(`CDP ${windowState} timed out`));
|
|
176
|
-
}, 5000);
|
|
177
|
-
|
|
178
|
-
ws.addEventListener("open", () => {
|
|
179
|
-
ws.send(JSON.stringify({ id: 1, method: "Browser.getWindowForTarget" }));
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
ws.addEventListener("message", (event) => {
|
|
183
|
-
const msg = JSON.parse(String(event.data)) as {
|
|
184
|
-
id: number;
|
|
185
|
-
result?: { windowId: number };
|
|
186
|
-
error?: { message: string };
|
|
187
|
-
};
|
|
188
|
-
if (msg.id === 1 && msg.result) {
|
|
189
|
-
ws.send(
|
|
190
|
-
JSON.stringify({
|
|
191
|
-
id: 2,
|
|
192
|
-
method: "Browser.setWindowBounds",
|
|
193
|
-
params: {
|
|
194
|
-
windowId: msg.result.windowId,
|
|
195
|
-
bounds: { windowState },
|
|
196
|
-
},
|
|
197
|
-
}),
|
|
198
|
-
);
|
|
199
|
-
} else if (msg.id === 1) {
|
|
200
|
-
clearTimeout(timeout);
|
|
201
|
-
ws.close();
|
|
202
|
-
reject(new Error("Browser.getWindowForTarget failed"));
|
|
203
|
-
} else if (msg.id === 2) {
|
|
204
|
-
clearTimeout(timeout);
|
|
205
|
-
ws.close();
|
|
206
|
-
if (msg.error) {
|
|
207
|
-
reject(
|
|
208
|
-
new Error(`Browser.setWindowBounds failed: ${msg.error.message}`),
|
|
209
|
-
);
|
|
210
|
-
} else {
|
|
211
|
-
resolve();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
ws.addEventListener("error", (err) => {
|
|
217
|
-
clearTimeout(timeout);
|
|
218
|
-
reject(err);
|
|
219
|
-
});
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Minimize the Chrome window associated with the CDP session.
|
|
225
|
-
*/
|
|
226
|
-
export async function minimizeChromeWindow(
|
|
227
|
-
cdpBase: string = DEFAULT_CDP_BASE,
|
|
228
|
-
): Promise<void> {
|
|
229
|
-
await setWindowState(cdpBase, "minimized");
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Restore (un-minimize) the Chrome window associated with the CDP session.
|
|
234
|
-
*/
|
|
235
|
-
export async function restoreChromeWindow(
|
|
236
|
-
cdpBase: string = DEFAULT_CDP_BASE,
|
|
237
|
-
): Promise<void> {
|
|
238
|
-
await setWindowState(cdpBase, "normal");
|
|
239
|
-
}
|