@moretea-labs/forge 1.5.1 → 1.7.0
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/AGENTS.md +2 -0
- package/CHANGELOG.md +46 -0
- package/CLAUDE.md +2 -0
- package/DESIGN.md +7 -2
- package/README.md +4 -4
- package/README.zh-CN.md +2 -2
- package/SKILL.md +2 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/agents/openai.yaml +1 -1
- package/assets/hooks/hook-input.sh +11 -8
- package/assets/hooks/lib/session-state.sh +10 -7
- package/assets/hooks/lib/workflow-state.sh +13 -177
- package/assets/hooks/post-tool-observer.sh +8 -2
- package/assets/hooks/pre-edit-guard.sh +8 -39
- package/assets/hooks/prompt-guard.sh +7 -83
- package/assets/hooks/session-start-context.sh +56 -1
- package/assets/partials/05-workflow.partial.md +1 -1
- package/assets/partials-agents/03-orchestration.partial.md +1 -1
- package/assets/plugin-registry.v1.json +54 -3
- package/assets/reference-configs/document-generation.md +1 -1
- package/assets/reference-configs/harness-overview.md +1 -1
- package/assets/reference-configs/hook-operations.md +1 -1
- package/assets/reference-configs/sprint-contracts.md +3 -3
- package/assets/skill-version.json +2 -2
- package/assets/templates/contract.template.md +2 -7
- package/assets/templates/helpers/capture-plan.sh +2 -2
- package/assets/templates/helpers/check-task-workflow.sh +4 -4
- package/assets/templates/helpers/contract-worktree.sh +1 -21
- package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
- package/assets/templates/helpers/migrate-project-template.sh +22 -45
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +12 -37
- package/assets/templates/helpers/ship-worktrees.sh +2 -29
- package/assets/templates/helpers/sprint-backlog.sh +6 -6
- package/assets/templates/helpers/verify-contract.sh +1 -111
- package/assets/templates/helpers/verify-sprint.sh +3 -180
- package/assets/templates/plan.template.md +1 -1
- package/assets/templates/review.template.md +9 -34
- package/assets/workflow-contract.v1.json +10 -19
- package/docs/README.md +10 -10
- package/docs/architecture/CURRENT.md +170 -0
- package/docs/architecture/EVOLUTION.md +42 -0
- package/docs/architecture/current/README.md +4 -156
- package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
- package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
- package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
- package/docs/architecture/history/README.md +7 -0
- package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
- package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
- package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
- package/docs/architecture/history.md +3 -34
- package/docs/architecture/index.md +15 -98
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
- package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
- package/docs/architecture/versions/1.6.md +38 -0
- package/docs/architecture/versions/1.7.md +41 -0
- package/docs/forge-chatgpt-browser-engine.md +2 -2
- package/docs/forge-chatgpt-mcp-setup.md +4 -28
- package/docs/forge-chatgpt-supervised-automation.md +3 -3
- package/docs/forge-github-issue-launcher.md +1 -1
- package/docs/forge-runtime-self-healing-loop.md +1 -1
- package/docs/operations/controller-browser-plugin.md +38 -22
- package/docs/operations/controller-performance-and-502.md +124 -0
- package/docs/operations/controller-reliability-runbook.md +1 -1
- package/docs/operations/features.md +3 -3
- package/docs/operations/github-repository.md +1 -1
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +5 -3
- package/docs/tutorials/01-install-and-start.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/wiki/Architecture.md +6 -6
- package/docs/wiki/Implementation.md +2 -2
- package/docs/wiki/Installation.md +1 -1
- package/docs/wiki/Integrations.md +2 -2
- package/docs/wiki/Quick-Start.md +1 -1
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/package.json +29 -10
- package/scripts/activate-source-baseline.ts +17 -13
- package/scripts/benchmark-background-check-overlap.ts +228 -0
- package/scripts/benchmark-mcp-host.sh +117 -0
- package/scripts/benchmark-mcp-transport.ts +170 -0
- package/scripts/benchmark-quality-harness.ts +146 -0
- package/scripts/benchmark-route-session-concurrency.ts +207 -6
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
- package/scripts/benchmark-typescript-navigation.ts +186 -0
- package/scripts/benchmark-work-efficiency.ts +334 -0
- package/scripts/capture-plan.sh +3 -3
- package/scripts/check-agent-tooling.sh +8 -35
- package/scripts/check-architecture-sync.sh +17 -86
- package/scripts/check-mcp-compatibility.ts +14 -0
- package/scripts/check-public-docs.sh +14 -1
- package/scripts/check-runtime-architecture.mjs +107 -64
- package/scripts/check-stable-baseline.ts +92 -0
- package/scripts/check-task-workflow.sh +4 -4
- package/scripts/contract-worktree.sh +1 -21
- package/scripts/ensure-task-workflow.sh +22 -55
- package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
- package/scripts/install-standalone-recovery.ts +24 -0
- package/scripts/lib/project-init-lib.sh +15 -44
- package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
- package/scripts/migrate-project-template.sh +23 -1179
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +11 -36
- package/scripts/run-governed-gate.ts +2 -1
- package/scripts/ship-worktrees.sh +2 -29
- package/scripts/smoke-ios-physical-device.ts +4 -2
- package/scripts/smoke-runtime-recovery.ts +1 -35
- package/scripts/smoke-schedule-engine.ts +1 -20
- package/scripts/sprint-backlog.sh +1 -1
- package/scripts/verify-contract.sh +1 -111
- package/scripts/verify-forge-runtime.sh +0 -1
- package/scripts/verify-sprint.sh +3 -180
- package/scripts/workflow-contract.ts +1 -5
- package/src/cli/commands/chatgpt.ts +31 -2
- package/src/cli/commands/init-hook.ts +3 -3
- package/src/cli/commands/mcp.ts +9 -5
- package/src/cli/commands/plugin.ts +37 -10
- package/src/cli/commands/recovery.ts +25 -3
- package/src/cli/commands/runtime.ts +55 -3
- package/src/cli/commands/security.ts +12 -1
- package/src/cli/commands/setup-profile.ts +31 -21
- package/src/cli/controller/check-runner.ts +106 -3
- package/src/cli/controller/composite-result.ts +1 -1
- package/src/cli/controller/context/focus.ts +51 -0
- package/src/cli/controller/context/instruction-context.ts +114 -0
- package/src/cli/controller/context/known-paths.ts +76 -0
- package/src/cli/controller/context/query-planning.ts +110 -0
- package/src/cli/controller/context/rule-resolution.ts +532 -0
- package/src/cli/controller/context/source-materializer.ts +278 -0
- package/src/cli/controller/context/structural.ts +16 -0
- package/src/cli/controller/context/types.ts +286 -0
- package/src/cli/controller/context-pack.ts +898 -430
- package/src/cli/controller/launch-agents.ts +77 -2
- package/src/cli/controller/operational-plan.ts +2 -2
- package/src/cli/controller/work-mode.ts +6 -4
- package/src/cli/editing/edit-session.ts +60 -7
- package/src/cli/github/github.ts +133 -0
- package/src/cli/hook/prompt-guard-decision.ts +17 -18
- package/src/cli/hook/prompt-intents.ts +1 -1
- package/src/cli/installer/managed-entries.ts +15 -1
- package/src/cli/local-bridge/console-automations.ts +225 -67
- package/src/cli/local-bridge/console-view-models.ts +3 -35
- package/src/cli/local-bridge/facade-api.ts +2 -217
- package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
- package/src/cli/local-bridge/frontend/src/styles.css +27 -11
- package/src/cli/local-bridge/frontend/src/types.ts +8 -7
- package/src/cli/local-bridge/job-store.ts +1 -490
- package/src/cli/local-bridge/server.ts +29 -532
- package/src/cli/local-bridge/ui-dist/app.css +1 -1
- package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
- package/src/cli/mcp/auth.ts +5 -5
- package/src/cli/mcp/instructions.ts +3 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -189
- package/src/cli/mcp/multi-repository.ts +16 -5
- package/src/cli/mcp/oauth.ts +175 -26
- package/src/cli/mcp/paths.ts +8 -2
- package/src/cli/mcp/repository-tools.ts +356 -101
- package/src/cli/mcp/server.ts +625 -54
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-selection.ts +1 -1
- package/src/cli/mcp/toolset.ts +42 -5
- package/src/cli/mcp/transports/http.ts +125 -15
- package/src/cli/mcp/transports/session-registry.ts +67 -5
- package/src/cli/mcp/types.ts +3 -3
- package/src/cli/repositories/command-classifier.ts +209 -14
- package/src/cli/repositories/command-executor.ts +55 -521
- package/src/cli/repositories/command-process.ts +108 -0
- package/src/cli/repositories/command-scope.ts +99 -41
- package/src/cli/repositories/controller-home.ts +7 -8
- package/src/cli/repositories/git-command-executor.ts +2 -0
- package/src/cli/repositories/locks.ts +53 -16
- package/src/cli/repositories/repository-snapshot.ts +121 -0
- package/src/cli/repositories/safe-patch.ts +6 -4
- package/src/cli/repositories/selected-path-actions.ts +17 -9
- package/src/cli/repositories/structured-git.ts +110 -10
- package/src/cli/repository/inspector.ts +565 -62
- package/src/cli/repository/session-cache.ts +58 -17
- package/src/cli/v81-entry.ts +1 -4
- package/src/effects/fs-transaction.ts +85 -17
- package/src/runtime/context/codegraph-read-provider.ts +233 -2
- package/src/runtime/context/codegraph-sidecar.cjs +74 -40
- package/src/runtime/context/generic-lsp-provider.ts +355 -0
- package/src/runtime/context/lsp-client.ts +283 -0
- package/src/runtime/context/semantic-navigation.ts +275 -0
- package/src/runtime/context/swift-navigation.ts +565 -0
- package/src/runtime/context/typescript-navigation.ts +226 -0
- package/src/runtime/control-plane/dispatch-priority.ts +28 -0
- package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
- package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
- package/src/runtime/control-plane/execution/session-store.ts +18 -0
- package/src/runtime/control-plane/execution/validation.ts +15 -2
- package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
- package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
- package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
- package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
- package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
- package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
- package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
- package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
- package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
- package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
- package/src/runtime/control-plane/facade/index.ts +1 -0
- package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
- package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
- package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
- package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
- package/src/runtime/control-plane/facade/types.ts +55 -11
- package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
- package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
- package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
- package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
- package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
- package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
- package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
- package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
- package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
- package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
- package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
- package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
- package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
- package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
- package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
- package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
- package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
- package/src/runtime/control-plane/release-retention.ts +62 -2
- package/src/runtime/control-plane/routing/route-policy.ts +7 -16
- package/src/runtime/control-plane/runtime-status-client.ts +0 -2
- package/src/runtime/diagnostics/mcp-timing.ts +19 -0
- package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
- package/src/runtime/diagnostics/performance.ts +24 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/execution/jobs/store.ts +58 -4
- package/src/runtime/execution/jobs/types.ts +1 -1
- package/src/runtime/execution/managed-workspace.ts +203 -1
- package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
- package/src/runtime/execution/process-runtime/check-result.ts +63 -2
- package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
- package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
- package/src/runtime/execution/process-runtime/gc.ts +98 -12
- package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
- package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
- package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
- package/src/runtime/execution/process-runtime/runtime.ts +59 -4
- package/src/runtime/execution/process-runtime/store.ts +7 -0
- package/src/runtime/execution/process-runtime/types.ts +2 -0
- package/src/runtime/execution/thin-harness/async-process.ts +12 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
- package/src/runtime/execution/workers/executor.ts +0 -48
- package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
- package/src/runtime/gateway/mcp/process-tools.ts +83 -37
- package/src/runtime/gateway/mcp/router.ts +185 -83
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
- package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
- package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
- package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
- package/src/runtime/plugins/browser-adapter.ts +2925 -318
- package/src/runtime/plugins/browser-automation-service.ts +10 -0
- package/src/runtime/plugins/browser-handoff-host.ts +6 -31
- package/src/runtime/plugins/browser-handoff.ts +92 -5
- package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
- package/src/runtime/plugins/browser-node-bridge.ts +6 -34
- package/src/runtime/plugins/browser-provider-registry.ts +109 -0
- package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
- package/src/runtime/plugins/browser-runtime.ts +183 -0
- package/src/runtime/plugins/browser-session-authority.ts +332 -0
- package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
- package/src/runtime/plugins/external-adapter.ts +324 -3
- package/src/runtime/plugins/external-registration.ts +84 -5
- package/src/runtime/plugins/external-unix-socket.ts +7 -1
- package/src/runtime/plugins/first-party-registry.ts +4 -2
- package/src/runtime/plugins/github-adapter.ts +125 -0
- package/src/runtime/plugins/gmail-adapter.ts +28 -2
- package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
- package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
- package/src/runtime/plugins/ios-adapter.ts +106 -16
- package/src/runtime/plugins/ios-agent-device.ts +234 -64
- package/src/runtime/plugins/ios-physical-device.ts +1143 -392
- package/src/runtime/plugins/lightweight-action.ts +153 -0
- package/src/runtime/plugins/local-system-adapter.ts +125 -8
- package/src/runtime/plugins/macos-capability-broker.ts +55 -9
- package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
- package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
- package/src/runtime/plugins/resend-adapter.ts +1 -1
- package/src/runtime/plugins/store.ts +366 -24
- package/src/runtime/plugins/types.ts +39 -2
- package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
- package/src/runtime/projections/git-status-sampler.ts +16 -16
- package/src/runtime/projections/materialized-view.ts +28 -10
- package/src/runtime/recovery/actions.ts +0 -10
- package/src/runtime/recovery/classifier.ts +0 -9
- package/src/runtime/recovery/maintenance-executor.ts +415 -18
- package/src/runtime/recovery/probe.ts +0 -1
- package/src/runtime/recovery/types.ts +0 -1
- package/src/runtime/root/mcp-transport.ts +180 -10
- package/src/runtime/root/package-connector-service.ts +186 -8
- package/src/runtime/root/package-runtime-release.ts +143 -17
- package/src/runtime/root/package-runtime-service.ts +430 -22
- package/src/runtime/root/release-manifest.ts +7 -2
- package/src/runtime/root/release-materialize.ts +162 -19
- package/src/runtime/root/release-store.ts +48 -11
- package/src/runtime/root/runtime.ts +23 -5
- package/src/runtime/root/service.ts +100 -27
- package/src/runtime/root/types.ts +2 -0
- package/src/runtime/safe-tooling/index.ts +0 -1
- package/src/runtime/safe-tooling/ios-development.ts +321 -63
- package/src/runtime/safe-tooling/types.ts +0 -30
- package/src/runtime/shared/trusted-executable.ts +62 -0
- package/src/runtime/shared/trusted-node-executable.ts +48 -0
- package/src/runtime/standalone-recovery/core.ts +702 -94
- package/src/runtime/standalone-recovery/entry.ts +99 -23
- package/src/runtime/standalone-recovery/installer.ts +19 -3
- package/src/runtime/standalone-recovery/service-labels.ts +2 -0
- package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
- package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
- package/src/runtime/workflow/schedules/engine.ts +509 -80
- package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
- package/src/runtime/workflow/schedules/settlement.ts +48 -4
- package/src/runtime/workflow/schedules/store.ts +131 -10
- package/src/runtime/workflow/schedules/types.ts +6 -3
- package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
- package/src/testing/test-governance.ts +8 -40
- package/assets/reference-configs/heartbeat-triage.md +0 -57
- package/assets/templates/helpers/harness-trace-grade.sh +0 -150
- package/assets/templates/helpers/heartbeat-triage.sh +0 -361
- package/assets/templates/helpers/maintenance-triage.sh +0 -167
- package/bin/forge-desktop-helper.mjs +0 -204
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
- package/docs/architecture/current/architecture-invariants.md +0 -331
- package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
- package/docs/architecture/current/codegraph-context-provider.md +0 -226
- package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
- package/docs/architecture/current/controller-repository-decoupling.md +0 -148
- package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
- package/docs/architecture/current/entity-model.md +0 -592
- package/docs/architecture/current/external-plugin-governance.md +0 -396
- package/docs/architecture/current/failure-recovery.md +0 -447
- package/docs/architecture/current/governance.md +0 -213
- package/docs/architecture/current/human-interaction-plane.md +0 -87
- package/docs/architecture/current/implementation-status.md +0 -128
- package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
- package/docs/architecture/current/legacy-governance.md +0 -58
- package/docs/architecture/current/local-workspace-target-grants.md +0 -70
- package/docs/architecture/current/migration-roadmap.md +0 -105
- package/docs/architecture/current/multi-repository-execution.md +0 -371
- package/docs/architecture/current/operations-runbook.md +0 -45
- package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
- package/docs/architecture/current/repository-access-modes.md +0 -102
- package/docs/architecture/current/route-policy.md +0 -15
- package/docs/architecture/current/route-session-concurrency.md +0 -85
- package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
- package/docs/architecture/current/runtime-directory-map.md +0 -49
- package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
- package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
- package/docs/architecture/current/system-overview.md +0 -144
- package/docs/architecture/current/target-requirements-traceability.md +0 -202
- package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
- package/docs/architecture/current/thin-harness-v1.md +0 -311
- package/docs/architecture/current/verification-and-release-gates.md +0 -391
- package/docs/forge-autonomous-goal-loop.md +0 -9
- package/docs/forge-provider-configuration.md +0 -7
- package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
- package/docs/operations/controller-desktop-plugin.md +0 -112
- package/docs/operations/gmail-assistant-routines.md +0 -186
- package/docs/operations/personal-assistant-operator-guide.md +0 -61
- package/docs/operations/provider-configuration.md +0 -31
- package/scripts/harness-trace-grade.sh +0 -150
- package/scripts/heartbeat-triage.sh +0 -361
- package/scripts/maintenance-triage.sh +0 -167
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
- package/src/cli/repositories/goal-registry.ts +0 -239
- package/src/runtime/assistant/action-proposals.ts +0 -279
- package/src/runtime/assistant/intent.ts +0 -395
- package/src/runtime/assistant/model-provider.ts +0 -460
- package/src/runtime/assistant/openapi.ts +0 -223
- package/src/runtime/assistant/policy.ts +0 -89
- package/src/runtime/assistant/readiness.ts +0 -258
- package/src/runtime/assistant/routine-runtime.ts +0 -477
- package/src/runtime/assistant/schedule-binding.ts +0 -171
- package/src/runtime/assistant/standing-grants.ts +0 -375
- package/src/runtime/assistant/store.ts +0 -243
- package/src/runtime/assistant/types.ts +0 -135
- package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
- package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
- package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
- package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
- package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
- package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
- package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
- package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
- package/src/runtime/control-plane/goal-loop/index.ts +0 -13
- package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
- package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
- package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
- package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
- package/src/runtime/control-plane/goal-loop/types.ts +0 -378
- package/src/runtime/control-plane/startup-recovery.ts +0 -198
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
- package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
- package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
- package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
- package/src/runtime/plugins/desktop-adapter.ts +0 -374
- package/src/runtime/safe-tooling/browser-safe.ts +0 -133
- package/src/runtime/workflow/findings/store.ts +0 -204
- package/src/runtime/workflow/findings/types.ts +0 -58
- package/src/runtime/workflow/portfolio/engine.ts +0 -20
- package/src/runtime/workflow/portfolio/store.ts +0 -185
- package/src/runtime/workflow/portfolio/types.ts +0 -29
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { execFile } from 'child_process';
|
|
2
|
-
import { existsSync, writeFileSync } from 'fs';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { basename, extname } from 'path';
|
|
4
5
|
import {
|
|
5
6
|
callBrowserAutomationBroker,
|
|
6
7
|
captureBrowserAutomationRegion,
|
|
7
8
|
type BrowserAutomationBrokerAction,
|
|
9
|
+
type BrowserAutomationTrustedInput,
|
|
8
10
|
} from './browser-automation-service';
|
|
9
11
|
import { AssistantPluginError } from './errors';
|
|
10
12
|
|
|
@@ -41,6 +43,20 @@ export interface MacOsBrowserMetadata {
|
|
|
41
43
|
loading?: boolean;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
export interface MacOsBrowserTabInventoryEntry {
|
|
47
|
+
windowId: string;
|
|
48
|
+
tabId: string;
|
|
49
|
+
url: string;
|
|
50
|
+
title: string;
|
|
51
|
+
active: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface MacOsBrowserTabInventory {
|
|
55
|
+
product: MacOsBrowserProduct;
|
|
56
|
+
tabs: MacOsBrowserTabInventoryEntry[];
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
44
60
|
export interface MacOsBrowserAttachAttempt {
|
|
45
61
|
product: MacOsBrowserProduct;
|
|
46
62
|
appName: string;
|
|
@@ -74,7 +90,9 @@ interface MacOsBrowserRuntimeHooks {
|
|
|
74
90
|
// Test-only escape hatches. Production intentionally leaves these undefined so
|
|
75
91
|
// Apple Events and screen capture are attributed to the stable helper process.
|
|
76
92
|
runAppleScript?: (script: string, args: string[], timeoutMs: number) => Promise<string>;
|
|
93
|
+
tabInventory?: (product: MacOsBrowserProduct, timeoutMs: number) => Promise<MacOsBrowserTabInventory>;
|
|
77
94
|
captureRegion?: (region: { x: number; y: number; width: number; height: number }, path: string, timeoutMs: number) => Promise<Buffer>;
|
|
95
|
+
trustedInput?: (request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>, timeoutMs: number) => Promise<void>;
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
function execFileText(file: string, args: string[], timeoutMs: number): Promise<string> {
|
|
@@ -143,8 +161,72 @@ async function captureBrowserAutomation(
|
|
|
143
161
|
return bytes;
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
async function sendBrowserTrustedInput(
|
|
165
|
+
request: Extract<BrowserAutomationBrokerAction, { action: 'trusted_input' }>,
|
|
166
|
+
timeoutMs: number,
|
|
167
|
+
): Promise<void> {
|
|
168
|
+
if (runtimeHooks.trustedInput) {
|
|
169
|
+
await runtimeHooks.trustedInput(request, timeoutMs);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
let result: Record<string, unknown>;
|
|
173
|
+
try {
|
|
174
|
+
result = await callBrowserAutomationBroker(request, timeoutMs);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
177
|
+
if ((error instanceof AssistantPluginError && error.code === 'BROWSER_AUTOMATION_ACTION_UNSUPPORTED')
|
|
178
|
+
|| /\bBROWSER_AUTOMATION_ACTION_UNSUPPORTED\b/.test(message)) {
|
|
179
|
+
throw new AssistantPluginError(
|
|
180
|
+
'PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE',
|
|
181
|
+
'Installed Forge Desktop Operator does not support native browser trusted input. Keep the exact tab explicitly foregrounded and use a separately grounded Desktop Operator interaction, or upgrade the stable Desktop Operator provider.',
|
|
182
|
+
{ retryable: false, details: { browserProduct: request.product, requiredProviderAction: 'trusted_input' } },
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
if (result.performed !== true) {
|
|
188
|
+
throw new AssistantPluginError(
|
|
189
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
190
|
+
'Stable Forge macOS capability broker did not confirm trusted browser input.',
|
|
191
|
+
{ retryable: true },
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
146
196
|
let runtimeHooks: MacOsBrowserRuntimeHooks = { ...defaultRuntimeHooks };
|
|
147
197
|
let lastAttachObservation: MacOsBrowserAttachObservation | undefined;
|
|
198
|
+
const MAX_WARM_NATIVE_PAGE_HANDLES = 128;
|
|
199
|
+
const warmNativePageHandles = new Map<string, MacOsAppleEventsPage>();
|
|
200
|
+
|
|
201
|
+
function nativePageHandleKey(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): string {
|
|
202
|
+
// tabId is the stable browser entity. The windowId is only a mutable placement hint and
|
|
203
|
+
// is re-resolved from live inventory immediately before exact-target Apple Events operations.
|
|
204
|
+
return `${product}:${ref.tabId}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function rememberMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef, page: MacOsAppleEventsPage): void {
|
|
208
|
+
const key = nativePageHandleKey(product, ref);
|
|
209
|
+
warmNativePageHandles.delete(key);
|
|
210
|
+
warmNativePageHandles.set(key, page);
|
|
211
|
+
while (warmNativePageHandles.size > MAX_WARM_NATIVE_PAGE_HANDLES) {
|
|
212
|
+
const oldest = warmNativePageHandles.keys().next().value;
|
|
213
|
+
if (typeof oldest !== 'string') break;
|
|
214
|
+
warmNativePageHandles.delete(oldest);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function invalidateMacOsBrowserPageHandle(product: MacOsBrowserProduct, ref: MacOsBrowserTabRef): void {
|
|
219
|
+
warmNativePageHandles.delete(nativePageHandleKey(product, ref));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function invalidateMacOsBrowserPageHandles(): void {
|
|
223
|
+
warmNativePageHandles.clear();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function macOsBrowserPageHandleStale(error: unknown): boolean {
|
|
227
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
228
|
+
return /FORGE_BROWSER_TAB_NOT_FOUND|BROWSER_AUTOMATION_TAB_NOT_FOUND|target tab.*not found|can[’']t get tab .*invalid index.*-1719/i.test(message);
|
|
229
|
+
}
|
|
148
230
|
|
|
149
231
|
export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRuntimeHooks>): void {
|
|
150
232
|
runtimeHooks = { ...defaultRuntimeHooks, ...hooks };
|
|
@@ -153,6 +235,7 @@ export function setMacOsBrowserRuntimeHooksForTest(hooks: Partial<MacOsBrowserRu
|
|
|
153
235
|
export function resetMacOsBrowserRuntimeHooksForTest(): void {
|
|
154
236
|
runtimeHooks = { ...defaultRuntimeHooks };
|
|
155
237
|
lastAttachObservation = undefined;
|
|
238
|
+
warmNativePageHandles.clear();
|
|
156
239
|
}
|
|
157
240
|
|
|
158
241
|
export function getMacOsBrowserAttachObservation(): MacOsBrowserAttachObservation | undefined {
|
|
@@ -197,12 +280,120 @@ set targetWindow to front window
|
|
|
197
280
|
set targetTab to active tab of targetWindow
|
|
198
281
|
set windowBounds to bounds of targetWindow
|
|
199
282
|
set separator to ASCII character 30
|
|
200
|
-
return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text)
|
|
283
|
+
return (frontmost as text) & separator & (URL of targetTab as text) & separator & (title of targetTab as text) & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & "true" & separator & (loading of targetTab as text)
|
|
201
284
|
`);
|
|
202
285
|
}
|
|
203
286
|
|
|
287
|
+
function listTabsScript(browser: MacOsBrowserDefinition): string {
|
|
288
|
+
return `on replaceText(sourceText, needle, replacement)
|
|
289
|
+
set previousDelimiters to AppleScript's text item delimiters
|
|
290
|
+
set AppleScript's text item delimiters to needle
|
|
291
|
+
set sourceItems to every text item of sourceText
|
|
292
|
+
set AppleScript's text item delimiters to replacement
|
|
293
|
+
set resultText to sourceItems as text
|
|
294
|
+
set AppleScript's text item delimiters to previousDelimiters
|
|
295
|
+
return resultText
|
|
296
|
+
end replaceText
|
|
297
|
+
|
|
298
|
+
on cleanField(sourceText, recordSeparator, fieldSeparator)
|
|
299
|
+
set cleaned to my replaceText(sourceText as text, recordSeparator, " ")
|
|
300
|
+
return my replaceText(cleaned, fieldSeparator, " ")
|
|
301
|
+
end cleanField
|
|
302
|
+
|
|
303
|
+
${browserTellScript(browser, `
|
|
304
|
+
set recordSeparator to ASCII character 30
|
|
305
|
+
set fieldSeparator to ASCII character 31
|
|
306
|
+
set maxTabs to 256
|
|
307
|
+
set returnedCount to 0
|
|
308
|
+
set truncatedInventory to false
|
|
309
|
+
set outputText to "false"
|
|
310
|
+
repeat with candidateWindow in windows
|
|
311
|
+
set activeTabId to ""
|
|
312
|
+
try
|
|
313
|
+
set activeTabId to ((id of active tab of candidateWindow) as text)
|
|
314
|
+
end try
|
|
315
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
316
|
+
if returnedCount is greater than or equal to maxTabs then
|
|
317
|
+
set truncatedInventory to true
|
|
318
|
+
exit repeat
|
|
319
|
+
end if
|
|
320
|
+
set candidateWindowId to ((id of candidateWindow) as text)
|
|
321
|
+
set candidateTabId to ((id of candidateTab) as text)
|
|
322
|
+
set candidateURL to my cleanField((URL of candidateTab as text), recordSeparator, fieldSeparator)
|
|
323
|
+
set candidateTitle to my cleanField((title of candidateTab as text), recordSeparator, fieldSeparator)
|
|
324
|
+
set candidateActive to (candidateTabId is activeTabId)
|
|
325
|
+
set outputText to outputText & recordSeparator & candidateWindowId & fieldSeparator & candidateTabId & fieldSeparator & (candidateActive as text) & fieldSeparator & candidateURL & fieldSeparator & candidateTitle
|
|
326
|
+
set returnedCount to returnedCount + 1
|
|
327
|
+
end repeat
|
|
328
|
+
if truncatedInventory then exit repeat
|
|
329
|
+
end repeat
|
|
330
|
+
if truncatedInventory then
|
|
331
|
+
set outputText to "true" & text 6 thru -1 of outputText
|
|
332
|
+
end if
|
|
333
|
+
return outputText
|
|
334
|
+
`)}
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function parseTabInventory(product: MacOsBrowserProduct, raw: string): MacOsBrowserTabInventory {
|
|
339
|
+
const records = raw.split(RECORD_SEPARATOR);
|
|
340
|
+
const marker = (records.shift() ?? '').trim().toLowerCase();
|
|
341
|
+
if (marker !== 'true' && marker !== 'false') {
|
|
342
|
+
throw new AssistantPluginError(
|
|
343
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
344
|
+
'Stable Forge macOS capability broker returned an invalid browser tab inventory marker.',
|
|
345
|
+
{ retryable: true },
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const tabs: MacOsBrowserTabInventoryEntry[] = [];
|
|
349
|
+
const fieldSeparator = String.fromCharCode(31);
|
|
350
|
+
for (const record of records) {
|
|
351
|
+
if (!record) continue;
|
|
352
|
+
const fields = record.split(fieldSeparator);
|
|
353
|
+
if (fields.length < 5) {
|
|
354
|
+
throw new AssistantPluginError(
|
|
355
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
356
|
+
'Stable Forge macOS capability broker returned incomplete browser tab inventory metadata.',
|
|
357
|
+
{ retryable: true },
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const windowId = (fields[0] ?? '').trim();
|
|
361
|
+
const tabId = (fields[1] ?? '').trim();
|
|
362
|
+
const activeText = (fields[2] ?? '').trim().toLowerCase();
|
|
363
|
+
if (!windowId || !tabId || (activeText !== 'true' && activeText !== 'false')) {
|
|
364
|
+
throw new AssistantPluginError(
|
|
365
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
366
|
+
'Stable Forge macOS capability broker returned invalid browser tab inventory metadata.',
|
|
367
|
+
{ retryable: true },
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
tabs.push({ windowId, tabId, active: activeText === 'true', url: fields[3] ?? '', title: fields.slice(4).join(fieldSeparator) });
|
|
371
|
+
if (tabs.length > 256) {
|
|
372
|
+
throw new AssistantPluginError(
|
|
373
|
+
'PLUGIN_MACOS_CAPABILITY_BROKER_PROTOCOL_ERROR',
|
|
374
|
+
'Stable Forge macOS capability broker exceeded the bounded browser tab inventory size.',
|
|
375
|
+
{ retryable: true },
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return { product, tabs, truncated: marker === 'true' };
|
|
380
|
+
}
|
|
381
|
+
|
|
204
382
|
function targetTabPreamble(ref: MacOsBrowserTabRef): string {
|
|
205
|
-
return `set
|
|
383
|
+
return `set targetTabId to ${quotedAppleScript(ref.tabId)}
|
|
384
|
+
set targetWindow to missing value
|
|
385
|
+
set targetTab to missing value
|
|
386
|
+
repeat with candidateWindow in windows
|
|
387
|
+
repeat with candidateTab in tabs of candidateWindow
|
|
388
|
+
if ((id of candidateTab) as text) is targetTabId then
|
|
389
|
+
set targetWindow to candidateWindow
|
|
390
|
+
set targetTab to candidateTab
|
|
391
|
+
exit repeat
|
|
392
|
+
end if
|
|
393
|
+
end repeat
|
|
394
|
+
if targetTab is not missing value then exit repeat
|
|
395
|
+
end repeat
|
|
396
|
+
if targetTab is missing value then error "FORGE_BROWSER_TAB_NOT_FOUND:" & targetTabId`;
|
|
206
397
|
}
|
|
207
398
|
|
|
208
399
|
function targetMetadataScript(browser: MacOsBrowserDefinition, ref: MacOsBrowserTabRef): string {
|
|
@@ -211,7 +402,7 @@ ${targetTabPreamble(ref)}
|
|
|
211
402
|
set windowBounds to bounds of targetWindow
|
|
212
403
|
set separator to ASCII character 30
|
|
213
404
|
set targetIsActive to ((id of active tab of targetWindow) is (id of targetTab))
|
|
214
|
-
return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator &
|
|
405
|
+
return (frontmost as text) & separator & (URL of targetTab as text) & separator & "" & separator & ((item 1 of windowBounds) as text) & separator & ((item 2 of windowBounds) as text) & separator & ((item 3 of windowBounds) as text) & separator & ((item 4 of windowBounds) as text) & separator & ((id of targetWindow) as text) & separator & ((id of targetTab) as text) & separator & (targetIsActive as text) & separator & (loading of targetTab as text)
|
|
215
406
|
`);
|
|
216
407
|
}
|
|
217
408
|
|
|
@@ -416,7 +607,8 @@ function serializeEvaluation(expression: string | ((...args: unknown[]) => unkno
|
|
|
416
607
|
return `(() => {
|
|
417
608
|
try {
|
|
418
609
|
const value = (${source});
|
|
419
|
-
|
|
610
|
+
const page = { url: String(document.location.href || ''), title: String(document.title || '') };
|
|
611
|
+
return JSON.stringify({ ok: true, value: value === undefined ? { __forgeUndefined: true } : value, page });
|
|
420
612
|
} catch (error) {
|
|
421
613
|
return JSON.stringify({ ok: false, error: String(error && (error.stack || error.message) || error) });
|
|
422
614
|
}
|
|
@@ -464,11 +656,23 @@ function keyEventSource(selector: string | undefined, keySpec: string): string {
|
|
|
464
656
|
})()`;
|
|
465
657
|
}
|
|
466
658
|
|
|
659
|
+
export function nativeDomLoadStateSatisfied(
|
|
660
|
+
state: string,
|
|
661
|
+
readyState: string,
|
|
662
|
+
currentUrl: string,
|
|
663
|
+
requireHttpUrl: boolean,
|
|
664
|
+
): boolean {
|
|
665
|
+
if (requireHttpUrl && !/^https?:\/\//i.test(currentUrl ?? '')) return false;
|
|
666
|
+
return state === 'load' || state === 'networkidle'
|
|
667
|
+
? readyState === 'complete'
|
|
668
|
+
: readyState === 'interactive' || readyState === 'complete';
|
|
669
|
+
}
|
|
670
|
+
|
|
467
671
|
export class MacOsAppleEventsPage {
|
|
468
672
|
private metadata: MacOsBrowserMetadata;
|
|
469
673
|
private readonly browser: MacOsBrowserDefinition;
|
|
470
|
-
private
|
|
471
|
-
private
|
|
674
|
+
private timeoutMs: number;
|
|
675
|
+
private targetRef?: MacOsBrowserTabRef;
|
|
472
676
|
|
|
473
677
|
constructor(attachment: MacOsBrowserAttachment, timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS, targetRef?: MacOsBrowserTabRef) {
|
|
474
678
|
this.metadata = { ...attachment.metadata, ...(targetRef ? { windowId: targetRef.windowId, tabId: targetRef.tabId } : {}) };
|
|
@@ -479,12 +683,20 @@ export class MacOsAppleEventsPage {
|
|
|
479
683
|
|
|
480
684
|
private async runAutomation(
|
|
481
685
|
request: BrowserAutomationBrokerAction,
|
|
482
|
-
script: string,
|
|
686
|
+
script: string | ((resolvedRef?: MacOsBrowserTabRef) => string),
|
|
483
687
|
args: string[] = [],
|
|
484
688
|
timeoutMs = this.timeoutMs,
|
|
485
689
|
): Promise<string> {
|
|
486
690
|
try {
|
|
487
|
-
|
|
691
|
+
let effectiveRequest = request;
|
|
692
|
+
let resolvedRef: MacOsBrowserTabRef | undefined;
|
|
693
|
+
if ('ref' in request && request.ref) {
|
|
694
|
+
resolvedRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, request.ref, timeoutMs);
|
|
695
|
+
this.targetRef = resolvedRef;
|
|
696
|
+
effectiveRequest = { ...request, ref: resolvedRef } as BrowserAutomationBrokerAction;
|
|
697
|
+
}
|
|
698
|
+
const effectiveScript = typeof script === 'function' ? script(resolvedRef ?? this.targetRef) : script;
|
|
699
|
+
return await runBrowserAutomationText(effectiveRequest, effectiveScript, args, timeoutMs);
|
|
488
700
|
} catch (error) {
|
|
489
701
|
const message = error instanceof Error ? error.message : String(error);
|
|
490
702
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_OPERATION_FAILED', `${this.browser.appName} Apple Events operation failed: ${message}`, {
|
|
@@ -495,11 +707,13 @@ export class MacOsAppleEventsPage {
|
|
|
495
707
|
}
|
|
496
708
|
|
|
497
709
|
private async refreshMetadata(): Promise<MacOsBrowserMetadata> {
|
|
498
|
-
const script = this.targetRef ? targetMetadataScript(this.browser, this.targetRef) : metadataScript(this.browser);
|
|
499
710
|
this.metadata = parseMetadata(this.browser.product, await this.runAutomation(
|
|
500
711
|
{ action: 'metadata', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
|
|
501
|
-
|
|
712
|
+
(resolvedRef) => resolvedRef ? targetMetadataScript(this.browser, resolvedRef) : metadataScript(this.browser),
|
|
502
713
|
));
|
|
714
|
+
if (this.targetRef && this.metadata.windowId && this.metadata.tabId) {
|
|
715
|
+
this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
|
|
716
|
+
}
|
|
503
717
|
return this.metadata;
|
|
504
718
|
}
|
|
505
719
|
|
|
@@ -507,7 +721,7 @@ export class MacOsAppleEventsPage {
|
|
|
507
721
|
try {
|
|
508
722
|
return await this.runAutomation(
|
|
509
723
|
{ action: 'execute_javascript', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), source },
|
|
510
|
-
executeJavaScriptScript(this.browser,
|
|
724
|
+
(resolvedRef) => executeJavaScriptScript(this.browser, resolvedRef),
|
|
511
725
|
[source],
|
|
512
726
|
timeoutMs,
|
|
513
727
|
);
|
|
@@ -527,18 +741,28 @@ export class MacOsAppleEventsPage {
|
|
|
527
741
|
return this.targetRef ? { ...this.targetRef } : undefined;
|
|
528
742
|
}
|
|
529
743
|
|
|
744
|
+
updateTimeout(timeoutMs: number): void {
|
|
745
|
+
if (Number.isFinite(timeoutMs) && timeoutMs > 0) this.timeoutMs = Math.trunc(timeoutMs);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
attachmentSnapshot(): MacOsBrowserAttachment {
|
|
749
|
+
return {
|
|
750
|
+
metadata: { ...this.metadata, bounds: { ...this.metadata.bounds } },
|
|
751
|
+
attempts: [{
|
|
752
|
+
product: this.browser.product,
|
|
753
|
+
appName: this.browser.appName,
|
|
754
|
+
bundleId: this.browser.bundleId,
|
|
755
|
+
status: 'selected',
|
|
756
|
+
frontmost: this.metadata.frontmost,
|
|
757
|
+
}],
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
530
761
|
async goto(url: string, options: Record<string, unknown> = {}): Promise<unknown> {
|
|
531
762
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
532
|
-
if (this.targetRef) {
|
|
533
|
-
throw new AssistantPluginError(
|
|
534
|
-
'PLUGIN_BROWSER_BACKGROUND_NAVIGATION_REQUIRES_REPLACEMENT',
|
|
535
|
-
'Chrome/Vivaldi background tabs cannot be navigated reliably in place through Apple Events; create a replacement plugin-owned tab instead.',
|
|
536
|
-
{ retryable: true, details: { browserProduct: this.browser.product, windowId: this.targetRef.windowId, tabId: this.targetRef.tabId } },
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
763
|
await this.runAutomation(
|
|
540
|
-
{ action: 'navigate', product: this.browser.product, url },
|
|
541
|
-
navigateScript(this.browser),
|
|
764
|
+
{ action: 'navigate', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}), url },
|
|
765
|
+
(resolvedRef) => navigateScript(this.browser, resolvedRef),
|
|
542
766
|
[url],
|
|
543
767
|
timeout,
|
|
544
768
|
);
|
|
@@ -551,7 +775,7 @@ export class MacOsAppleEventsPage {
|
|
|
551
775
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
552
776
|
await this.runAutomation(
|
|
553
777
|
{ action: 'reload', product: this.browser.product, ...(this.targetRef ? { ref: this.targetRef } : {}) },
|
|
554
|
-
reloadScript(this.browser,
|
|
778
|
+
(resolvedRef) => reloadScript(this.browser, resolvedRef),
|
|
555
779
|
[],
|
|
556
780
|
timeout,
|
|
557
781
|
);
|
|
@@ -609,21 +833,183 @@ export class MacOsAppleEventsPage {
|
|
|
609
833
|
|
|
610
834
|
async evaluate<T>(expression: string | ((...args: unknown[]) => unknown), arg?: unknown): Promise<T> {
|
|
611
835
|
const raw = await this.executeJavaScriptRaw(serializeEvaluation(expression, arg));
|
|
612
|
-
let parsed: { ok?: boolean; value?: unknown; error?: string };
|
|
836
|
+
let parsed: { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
|
|
613
837
|
try {
|
|
614
|
-
parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string };
|
|
838
|
+
parsed = JSON.parse(raw) as { ok?: boolean; value?: unknown; error?: string; page?: { url?: unknown; title?: unknown } };
|
|
615
839
|
} catch {
|
|
616
840
|
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_PROTOCOL_ERROR', `${this.browser.appName} returned an invalid JavaScript result.`, {
|
|
617
841
|
retryable: true,
|
|
618
842
|
});
|
|
619
843
|
}
|
|
620
844
|
if (!parsed.ok) throw new Error(parsed.error || 'Browser JavaScript evaluation failed.');
|
|
845
|
+
if (parsed.page && typeof parsed.page.url === 'string' && parsed.page.url) {
|
|
846
|
+
this.metadata = {
|
|
847
|
+
...this.metadata,
|
|
848
|
+
url: parsed.page.url,
|
|
849
|
+
title: typeof parsed.page.title === 'string' ? parsed.page.title : this.metadata.title,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
621
852
|
if (parsed.value && typeof parsed.value === 'object' && (parsed.value as { __forgeUndefined?: unknown }).__forgeUndefined === true) {
|
|
622
853
|
return undefined as T;
|
|
623
854
|
}
|
|
624
855
|
return parsed.value as T;
|
|
625
856
|
}
|
|
626
857
|
|
|
858
|
+
async setInputFiles(selector: string, files: string | string[]): Promise<void> {
|
|
859
|
+
const filePaths = Array.isArray(files) ? files : [files];
|
|
860
|
+
if (filePaths.length === 0 || filePaths.length > 32) {
|
|
861
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Native browser file input accepts 1-32 local files.', { retryable: false });
|
|
862
|
+
}
|
|
863
|
+
for (const filePath of filePaths) {
|
|
864
|
+
if (!filePath || !existsSync(filePath)) {
|
|
865
|
+
throw new AssistantPluginError('PLUGIN_ACTION_ARGUMENT_INVALID', 'Local file does not exist.', { retryable: false });
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (filePaths.length === 1) {
|
|
870
|
+
const filePath = filePaths[0];
|
|
871
|
+
const bytes = readFileSync(filePath);
|
|
872
|
+
const fileName = basename(filePath);
|
|
873
|
+
const extension = extname(fileName).toLowerCase();
|
|
874
|
+
const mimeType = extension === '.pdf' ? 'application/pdf'
|
|
875
|
+
: extension === '.zip' ? 'application/zip'
|
|
876
|
+
: extension === '.png' ? 'image/png'
|
|
877
|
+
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
878
|
+
: 'application/octet-stream';
|
|
879
|
+
const bufferKey = `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
|
880
|
+
const encoded = bytes.toString('base64');
|
|
881
|
+
const chunkChars = 192 * 1024;
|
|
882
|
+
try {
|
|
883
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] = ''; return true; })()`);
|
|
884
|
+
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
885
|
+
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
886
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
887
|
+
}
|
|
888
|
+
const attached = await this.evaluate<{ name?: string; size?: number }>(`(() => {
|
|
889
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
890
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
891
|
+
const expectedName = ${JSON.stringify(fileName)};
|
|
892
|
+
const expectedSize = ${bytes.length};
|
|
893
|
+
const encoded = String(globalThis[${JSON.stringify(bufferKey)}] || '');
|
|
894
|
+
const binary = atob(encoded);
|
|
895
|
+
const data = new Uint8Array(binary.length);
|
|
896
|
+
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
897
|
+
const file = new File([data], expectedName, { type: ${JSON.stringify(mimeType)} });
|
|
898
|
+
const transfer = new DataTransfer();
|
|
899
|
+
transfer.items.add(file);
|
|
900
|
+
element.files = transfer.files;
|
|
901
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
902
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
903
|
+
const selected = element.files && element.files[0];
|
|
904
|
+
return selected ? { name: selected.name, size: selected.size, expectedName, expectedSize } : null;
|
|
905
|
+
})()`);
|
|
906
|
+
if (attached?.name !== fileName || attached?.size !== bytes.length) {
|
|
907
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local file on the target input.', {
|
|
908
|
+
retryable: true,
|
|
909
|
+
details: { expectedName: fileName, expectedSize: bytes.length, actualName: attached?.name, actualSize: attached?.size },
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
} finally {
|
|
913
|
+
await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
|
|
914
|
+
}
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const attachments = filePaths.map((filePath) => {
|
|
919
|
+
const bytes = readFileSync(filePath);
|
|
920
|
+
const fileName = basename(filePath);
|
|
921
|
+
const extension = extname(fileName).toLowerCase();
|
|
922
|
+
const mimeType = extension === '.pdf' ? 'application/pdf'
|
|
923
|
+
: extension === '.zip' ? 'application/zip'
|
|
924
|
+
: extension === '.png' ? 'image/png'
|
|
925
|
+
: extension === '.jpg' || extension === '.jpeg' ? 'image/jpeg'
|
|
926
|
+
: 'application/octet-stream';
|
|
927
|
+
return {
|
|
928
|
+
bytes,
|
|
929
|
+
fileName,
|
|
930
|
+
mimeType,
|
|
931
|
+
bufferKey: `__forgeLocalFile_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (attachments.length > 1) {
|
|
936
|
+
await this.evaluate(`(() => {
|
|
937
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
938
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
939
|
+
if (!element.multiple) throw new Error('Target input does not allow multiple files.');
|
|
940
|
+
const transfer = new DataTransfer();
|
|
941
|
+
element.files = transfer.files;
|
|
942
|
+
return true;
|
|
943
|
+
})()`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const chunkChars = 192 * 1024;
|
|
947
|
+
try {
|
|
948
|
+
for (let index = 0; index < attachments.length; index += 1) {
|
|
949
|
+
const attachment = attachments[index];
|
|
950
|
+
const encoded = attachment.bytes.toString('base64');
|
|
951
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] = ''; return true; })()`);
|
|
952
|
+
for (let offset = 0; offset < encoded.length; offset += chunkChars) {
|
|
953
|
+
const chunk = encoded.slice(offset, offset + chunkChars);
|
|
954
|
+
await this.evaluate(`(() => { globalThis[${JSON.stringify(attachment.bufferKey)}] += ${JSON.stringify(chunk)}; return true; })()`);
|
|
955
|
+
}
|
|
956
|
+
const append = attachments.length > 1 && index > 0;
|
|
957
|
+
await this.evaluate(`(() => {
|
|
958
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
959
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
960
|
+
const expectedName = ${JSON.stringify(attachment.fileName)};
|
|
961
|
+
const expectedSize = ${attachment.bytes.length};
|
|
962
|
+
const encoded = String(globalThis[${JSON.stringify(attachment.bufferKey)}] || '');
|
|
963
|
+
const binary = atob(encoded);
|
|
964
|
+
const data = new Uint8Array(binary.length);
|
|
965
|
+
for (let i = 0; i < binary.length; i += 1) data[i] = binary.charCodeAt(i);
|
|
966
|
+
const file = new File([data], expectedName, { type: ${JSON.stringify(attachment.mimeType)} });
|
|
967
|
+
const transfer = new DataTransfer();
|
|
968
|
+
if (${append ? 'true' : 'false'}) {
|
|
969
|
+
for (const existing of Array.from(element.files || [])) transfer.items.add(existing);
|
|
970
|
+
}
|
|
971
|
+
transfer.items.add(file);
|
|
972
|
+
element.files = transfer.files;
|
|
973
|
+
return Array.from(element.files || []).map((selected) => ({ name: selected.name, size: selected.size }));
|
|
974
|
+
})()`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const selected = await this.evaluate<Array<{ name?: string; size?: number }>>(`(() => {
|
|
978
|
+
const element = document.querySelector(${JSON.stringify(selector)});
|
|
979
|
+
if (!(element instanceof HTMLInputElement) || element.type !== 'file') throw new Error('Selector must resolve to an input[type=file].');
|
|
980
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
981
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
982
|
+
return Array.from(element.files || []).map((file) => ({ name: file.name, size: file.size }));
|
|
983
|
+
})()`);
|
|
984
|
+
const expected = attachments.map((attachment) => ({ name: attachment.fileName, size: attachment.bytes.length }));
|
|
985
|
+
const matches = selected.length === expected.length && expected.every((item, index) => selected[index]?.name === item.name && selected[index]?.size === item.size);
|
|
986
|
+
if (!matches) {
|
|
987
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_NATIVE_FILE_ATTACH_FAILED', 'Native browser did not retain the requested local files on the target input.', {
|
|
988
|
+
retryable: true,
|
|
989
|
+
details: { expected, actual: selected },
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
} finally {
|
|
993
|
+
for (const attachment of attachments) {
|
|
994
|
+
await this.evaluate(`(() => { try { delete globalThis[${JSON.stringify(attachment.bufferKey)}]; } catch {} return true; })()`).catch(() => undefined);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
async foregroundState(): Promise<{ frontmost: boolean; active: boolean }> {
|
|
1000
|
+
const metadata = await this.refreshMetadata();
|
|
1001
|
+
return { frontmost: metadata.frontmost, active: metadata.active === true };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
async trustedInput(input: BrowserAutomationTrustedInput): Promise<void> {
|
|
1005
|
+
if (!this.targetRef) {
|
|
1006
|
+
throw new AssistantPluginError('PLUGIN_BROWSER_TRUSTED_INPUT_UNAVAILABLE', 'Native trusted input requires an exact saved browser tab.', { retryable: false });
|
|
1007
|
+
}
|
|
1008
|
+
this.targetRef = await resolveCurrentMacOsBrowserTabRef(this.browser.product, this.targetRef, this.timeoutMs);
|
|
1009
|
+
await sendBrowserTrustedInput({ action: 'trusted_input', product: this.browser.product, ref: this.targetRef, input }, this.timeoutMs);
|
|
1010
|
+
await this.refreshMetadata();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
627
1013
|
async screenshot(options: Record<string, unknown>): Promise<Buffer> {
|
|
628
1014
|
const path = typeof options.path === 'string' ? options.path : undefined;
|
|
629
1015
|
if (!path) throw new Error('Native browser screenshot requires an output path.');
|
|
@@ -638,7 +1024,40 @@ export class MacOsAppleEventsPage {
|
|
|
638
1024
|
}
|
|
639
1025
|
|
|
640
1026
|
async click(selector: string): Promise<void> {
|
|
641
|
-
await this.evaluate(selectorSource(selector,
|
|
1027
|
+
await this.evaluate(selectorSource(selector, `
|
|
1028
|
+
if (element instanceof HTMLOptionElement) {
|
|
1029
|
+
const selectElement = element.closest('select');
|
|
1030
|
+
if (!(selectElement instanceof HTMLSelectElement)) throw new Error('Option is not attached to a select.');
|
|
1031
|
+
if (element.disabled || selectElement.disabled) throw new Error('Option is disabled.');
|
|
1032
|
+
if (!selectElement.multiple) {
|
|
1033
|
+
const valueSetter = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')?.set;
|
|
1034
|
+
if (valueSetter) valueSetter.call(selectElement, element.value);
|
|
1035
|
+
else selectElement.value = element.value;
|
|
1036
|
+
} else {
|
|
1037
|
+
element.selected = true;
|
|
1038
|
+
}
|
|
1039
|
+
selectElement.dispatchEvent(new Event('input', { bubbles: true }));
|
|
1040
|
+
selectElement.dispatchEvent(new Event('change', { bubbles: true }));
|
|
1041
|
+
if (!Array.from(selectElement.selectedOptions).includes(element)) throw new Error('Option selection did not persist.');
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
if (typeof element.click !== "function") throw new Error("Element is not clickable.");
|
|
1045
|
+
const rect = typeof element.getBoundingClientRect === 'function' ? element.getBoundingClientRect() : { left: 0, top: 0, width: 0, height: 0 };
|
|
1046
|
+
const clientX = Number(rect.left || 0) + Number(rect.width || 0) / 2;
|
|
1047
|
+
const clientY = Number(rect.top || 0) + Number(rect.height || 0) / 2;
|
|
1048
|
+
const mouseInit = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1, clientX, clientY };
|
|
1049
|
+
if (typeof PointerEvent === 'function') {
|
|
1050
|
+
element.dispatchEvent(new PointerEvent('pointerdown', { ...mouseInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
|
1051
|
+
}
|
|
1052
|
+
element.dispatchEvent(new MouseEvent('mousedown', mouseInit));
|
|
1053
|
+
if (typeof element.focus === 'function') element.focus();
|
|
1054
|
+
if (typeof PointerEvent === 'function') {
|
|
1055
|
+
element.dispatchEvent(new PointerEvent('pointerup', { ...mouseInit, buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
|
1056
|
+
}
|
|
1057
|
+
element.dispatchEvent(new MouseEvent('mouseup', { ...mouseInit, buttons: 0 }));
|
|
1058
|
+
element.click();
|
|
1059
|
+
return true;
|
|
1060
|
+
`));
|
|
642
1061
|
}
|
|
643
1062
|
|
|
644
1063
|
async dblclick(selector: string): Promise<void> {
|
|
@@ -681,10 +1100,20 @@ export class MacOsAppleEventsPage {
|
|
|
681
1100
|
return await this.evaluate(selectorSource(selector, `
|
|
682
1101
|
if (!(element instanceof HTMLSelectElement)) throw new Error('Element is not a select.');
|
|
683
1102
|
const selected = new Set(${JSON.stringify(normalized)});
|
|
684
|
-
|
|
1103
|
+
let matched = 0;
|
|
1104
|
+
for (const option of element.options) {
|
|
1105
|
+
const shouldSelect = selected.has(option.value) || selected.has(option.label);
|
|
1106
|
+
option.selected = shouldSelect;
|
|
1107
|
+
if (shouldSelect) matched += 1;
|
|
1108
|
+
}
|
|
1109
|
+
if (matched === 0) throw new Error('Requested select option was not found.');
|
|
685
1110
|
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
686
1111
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
687
|
-
|
|
1112
|
+
const actual = Array.from(element.selectedOptions);
|
|
1113
|
+
if (!actual.some((option) => selected.has(option.value) || selected.has(option.label))) {
|
|
1114
|
+
throw new Error('Requested select option did not persist.');
|
|
1115
|
+
}
|
|
1116
|
+
return actual.map((option) => option.value);
|
|
688
1117
|
`));
|
|
689
1118
|
}
|
|
690
1119
|
|
|
@@ -730,25 +1159,37 @@ export class MacOsAppleEventsPage {
|
|
|
730
1159
|
|
|
731
1160
|
async waitForLoadState(state = 'domcontentloaded', options: Record<string, unknown> = {}): Promise<void> {
|
|
732
1161
|
const timeout = typeof options.timeout === 'number' ? Math.trunc(options.timeout) : this.timeoutMs;
|
|
1162
|
+
const requireHttpUrl = options.requireHttpUrl === true;
|
|
733
1163
|
const deadline = Date.now() + timeout;
|
|
734
1164
|
while (Date.now() <= deadline) {
|
|
735
1165
|
if (this.targetRef) {
|
|
736
1166
|
try {
|
|
737
1167
|
const metadata = await this.refreshMetadata();
|
|
1168
|
+
// Browser actions only admit HTTP(S) targets. Native Chrome/Vivaldi can
|
|
1169
|
+
// expose several internal URLs (new-tab, error/interstitial, extension
|
|
1170
|
+
// bootstrap pages) while navigation is still settling. Treat every
|
|
1171
|
+
// non-HTTP(S) metadata URL as transitional here; the adapter still
|
|
1172
|
+
// applies the strict final HTTP(S) URL validator before persisting a
|
|
1173
|
+
// session, so this does not widen the accepted navigation surface.
|
|
1174
|
+
const transitionalUrl = !/^https?:\/\//i.test(metadata.url ?? '');
|
|
1175
|
+
if (transitionalUrl) {
|
|
1176
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
738
1179
|
if (metadata.loading === false) return;
|
|
739
1180
|
} catch {
|
|
740
1181
|
// Fall through to DOM readiness when native loading metadata is unavailable.
|
|
741
1182
|
}
|
|
742
1183
|
}
|
|
743
1184
|
try {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
1185
|
+
const domState = await this.evaluate<{ readyState: string; url: string }>(
|
|
1186
|
+
'({ readyState: String(document.readyState || ""), url: String(document.location.href || "") })',
|
|
1187
|
+
);
|
|
1188
|
+
if (nativeDomLoadStateSatisfied(state, domState.readyState, domState.url, requireHttpUrl)) return;
|
|
748
1189
|
} catch (error) {
|
|
749
1190
|
if (error instanceof AssistantPluginError && error.code === 'PLUGIN_BROWSER_JAVASCRIPT_PERMISSION_REQUIRED') {
|
|
750
1191
|
const metadata = await this.refreshMetadata();
|
|
751
|
-
if (
|
|
1192
|
+
if (/^https?:\/\//i.test(metadata.url ?? '')) return;
|
|
752
1193
|
}
|
|
753
1194
|
// Navigation can transiently invalidate the current JavaScript execution context.
|
|
754
1195
|
}
|
|
@@ -768,6 +1209,9 @@ export class MacOsAppleEventsPage {
|
|
|
768
1209
|
async bringToFront(): Promise<void> {
|
|
769
1210
|
if (this.targetRef) {
|
|
770
1211
|
this.metadata = await activateMacOsBrowserOwnedTab(this.browser.product, this.targetRef, this.timeoutMs);
|
|
1212
|
+
if (this.metadata.windowId && this.metadata.tabId) {
|
|
1213
|
+
this.targetRef = { windowId: this.metadata.windowId, tabId: this.metadata.tabId };
|
|
1214
|
+
}
|
|
771
1215
|
return;
|
|
772
1216
|
}
|
|
773
1217
|
await this.runAutomation(
|
|
@@ -783,15 +1227,71 @@ export class MacOsAppleEventsPage {
|
|
|
783
1227
|
};
|
|
784
1228
|
}
|
|
785
1229
|
|
|
1230
|
+
async function resolveCurrentMacOsBrowserTabRef(
|
|
1231
|
+
product: MacOsBrowserProduct,
|
|
1232
|
+
ref: MacOsBrowserTabRef,
|
|
1233
|
+
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
1234
|
+
): Promise<MacOsBrowserTabRef> {
|
|
1235
|
+
// Existing unit-level AppleScript hooks historically model one exact script at a time.
|
|
1236
|
+
// Preserve that narrow test seam unless a live inventory hook is explicitly supplied;
|
|
1237
|
+
// production always resolves through the stable Desktop Operator broker inventory.
|
|
1238
|
+
const inventory = runtimeHooks.tabInventory
|
|
1239
|
+
? await runtimeHooks.tabInventory(product, timeoutMs)
|
|
1240
|
+
: runtimeHooks.runAppleScript
|
|
1241
|
+
? undefined
|
|
1242
|
+
: await listMacOsBrowserTabs(product, timeoutMs);
|
|
1243
|
+
if (!inventory) return ref;
|
|
1244
|
+
|
|
1245
|
+
const matches = inventory.tabs.filter((candidate) => candidate.tabId === ref.tabId);
|
|
1246
|
+
if (inventory.truncated || matches.length !== 1) {
|
|
1247
|
+
throw new AssistantPluginError(
|
|
1248
|
+
'PLUGIN_BROWSER_NATIVE_TAB_IDENTITY_UNPROVEN',
|
|
1249
|
+
inventory.truncated
|
|
1250
|
+
? `Cannot prove the current ${product} tab identity because live inventory was truncated.`
|
|
1251
|
+
: matches.length === 0
|
|
1252
|
+
? `Saved ${product} tab ${ref.tabId} no longer exists in live inventory.`
|
|
1253
|
+
: `Saved ${product} tab ${ref.tabId} is ambiguous in live inventory.`,
|
|
1254
|
+
{
|
|
1255
|
+
retryable: true,
|
|
1256
|
+
details: {
|
|
1257
|
+
browserProduct: product,
|
|
1258
|
+
tabId: ref.tabId,
|
|
1259
|
+
savedWindowId: ref.windowId,
|
|
1260
|
+
candidateCount: matches.length,
|
|
1261
|
+
inventoryTruncated: inventory.truncated,
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const match = matches[0]!;
|
|
1268
|
+
return { windowId: match.windowId, tabId: match.tabId };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export async function listMacOsBrowserTabs(
|
|
1272
|
+
product: MacOsBrowserProduct,
|
|
1273
|
+
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
1274
|
+
): Promise<MacOsBrowserTabInventory> {
|
|
1275
|
+
const browser = browserDefinition(product);
|
|
1276
|
+
const raw = await runBrowserAutomationText(
|
|
1277
|
+
{ action: 'list_tabs', product },
|
|
1278
|
+
listTabsScript(browser),
|
|
1279
|
+
[],
|
|
1280
|
+
timeoutMs,
|
|
1281
|
+
);
|
|
1282
|
+
return parseTabInventory(product, raw);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
786
1285
|
export async function readMacOsBrowserOwnedTabMetadata(
|
|
787
1286
|
product: MacOsBrowserProduct,
|
|
788
1287
|
ref: MacOsBrowserTabRef,
|
|
789
1288
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
790
1289
|
): Promise<MacOsBrowserMetadata> {
|
|
791
1290
|
const browser = browserDefinition(product);
|
|
1291
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
792
1292
|
return parseMetadata(product, await runBrowserAutomationText(
|
|
793
|
-
{ action: 'metadata', product, ref },
|
|
794
|
-
targetMetadataScript(browser,
|
|
1293
|
+
{ action: 'metadata', product, ref: resolvedRef },
|
|
1294
|
+
targetMetadataScript(browser, resolvedRef),
|
|
795
1295
|
[],
|
|
796
1296
|
timeoutMs,
|
|
797
1297
|
));
|
|
@@ -803,13 +1303,33 @@ export async function activateMacOsBrowserOwnedTab(
|
|
|
803
1303
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
804
1304
|
): Promise<MacOsBrowserMetadata> {
|
|
805
1305
|
const browser = browserDefinition(product);
|
|
1306
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
806
1307
|
await runBrowserAutomationText(
|
|
807
|
-
{ action: 'activate', product, ref },
|
|
808
|
-
activateTargetTabScript(browser,
|
|
1308
|
+
{ action: 'activate', product, ref: resolvedRef },
|
|
1309
|
+
activateTargetTabScript(browser, resolvedRef),
|
|
809
1310
|
[],
|
|
810
1311
|
timeoutMs,
|
|
811
1312
|
);
|
|
812
|
-
|
|
1313
|
+
const metadata = await readMacOsBrowserOwnedTabMetadata(product, resolvedRef, timeoutMs);
|
|
1314
|
+
if (metadata.tabId !== resolvedRef.tabId || metadata.active !== true) {
|
|
1315
|
+
throw new AssistantPluginError(
|
|
1316
|
+
'PLUGIN_BROWSER_NATIVE_ACTIVATION_POSTCONDITION_FAILED',
|
|
1317
|
+
`${browser.appName} did not prove that the exact requested tab became active.`,
|
|
1318
|
+
{
|
|
1319
|
+
retryable: true,
|
|
1320
|
+
details: {
|
|
1321
|
+
browserProduct: product,
|
|
1322
|
+
requestedTabId: resolvedRef.tabId,
|
|
1323
|
+
resolvedWindowId: resolvedRef.windowId,
|
|
1324
|
+
observedWindowId: metadata.windowId,
|
|
1325
|
+
observedTabId: metadata.tabId,
|
|
1326
|
+
observedActive: metadata.active,
|
|
1327
|
+
observedFrontmost: metadata.frontmost,
|
|
1328
|
+
},
|
|
1329
|
+
},
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
return metadata;
|
|
813
1333
|
}
|
|
814
1334
|
|
|
815
1335
|
export function createMacOsBrowserPage(attachment: MacOsBrowserAttachment, timeoutMs?: number): MacOsAppleEventsPage {
|
|
@@ -831,10 +1351,12 @@ export async function createMacOsBrowserOwnedPage(
|
|
|
831
1351
|
const parts = raw.split(RECORD_SEPARATOR);
|
|
832
1352
|
if (parts.length < 2) throw new Error(`Browser scripting returned incomplete owned-tab metadata for ${browser.appName}.`);
|
|
833
1353
|
const ref = { windowId: parseBrowserId(parts[0] ?? '', 'window id'), tabId: parseBrowserId(parts[1] ?? '', 'tab id') };
|
|
834
|
-
|
|
1354
|
+
const page = new MacOsAppleEventsPage({
|
|
835
1355
|
...attachment,
|
|
836
1356
|
metadata: { ...attachment.metadata, url, title: '', windowId: ref.windowId, tabId: ref.tabId, active: false },
|
|
837
1357
|
}, timeoutMs, ref);
|
|
1358
|
+
rememberMacOsBrowserPageHandle(attachment.metadata.product, ref, page);
|
|
1359
|
+
return page;
|
|
838
1360
|
}
|
|
839
1361
|
|
|
840
1362
|
export async function reattachMacOsBrowserOwnedPage(
|
|
@@ -842,12 +1364,26 @@ export async function reattachMacOsBrowserOwnedPage(
|
|
|
842
1364
|
ref: MacOsBrowserTabRef,
|
|
843
1365
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
844
1366
|
): Promise<{ page: MacOsAppleEventsPage; attachment: MacOsBrowserAttachment }> {
|
|
1367
|
+
const cacheKey = nativePageHandleKey(product, ref);
|
|
1368
|
+
const cached = warmNativePageHandles.get(cacheKey);
|
|
1369
|
+
if (cached) {
|
|
1370
|
+
cached.updateTimeout(timeoutMs);
|
|
1371
|
+
// Refresh insertion order so bounded eviction behaves as a small LRU.
|
|
1372
|
+
warmNativePageHandles.delete(cacheKey);
|
|
1373
|
+
warmNativePageHandles.set(cacheKey, cached);
|
|
1374
|
+
return { page: cached, attachment: cached.attachmentSnapshot() };
|
|
1375
|
+
}
|
|
845
1376
|
const metadata = await readMacOsBrowserOwnedTabMetadata(product, ref, timeoutMs);
|
|
846
1377
|
const attachment: MacOsBrowserAttachment = {
|
|
847
1378
|
metadata,
|
|
848
1379
|
attempts: [{ product, appName: metadata.appName, bundleId: metadata.bundleId, status: 'selected', frontmost: metadata.frontmost }],
|
|
849
1380
|
};
|
|
850
|
-
|
|
1381
|
+
const resolvedRef = metadata.windowId && metadata.tabId
|
|
1382
|
+
? { windowId: metadata.windowId, tabId: metadata.tabId }
|
|
1383
|
+
: ref;
|
|
1384
|
+
const page = new MacOsAppleEventsPage(attachment, timeoutMs, resolvedRef);
|
|
1385
|
+
rememberMacOsBrowserPageHandle(product, resolvedRef, page);
|
|
1386
|
+
return { page, attachment };
|
|
851
1387
|
}
|
|
852
1388
|
|
|
853
1389
|
export async function closeMacOsBrowserOwnedTab(
|
|
@@ -856,10 +1392,15 @@ export async function closeMacOsBrowserOwnedTab(
|
|
|
856
1392
|
timeoutMs = DEFAULT_NATIVE_TIMEOUT_MS,
|
|
857
1393
|
): Promise<void> {
|
|
858
1394
|
const browser = browserDefinition(product);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1395
|
+
try {
|
|
1396
|
+
const resolvedRef = await resolveCurrentMacOsBrowserTabRef(product, ref, timeoutMs);
|
|
1397
|
+
await runBrowserAutomationText(
|
|
1398
|
+
{ action: 'close_tab', product, ref: resolvedRef },
|
|
1399
|
+
closeOwnedTabScript(browser, resolvedRef),
|
|
1400
|
+
[],
|
|
1401
|
+
timeoutMs,
|
|
1402
|
+
);
|
|
1403
|
+
} finally {
|
|
1404
|
+
invalidateMacOsBrowserPageHandle(product, ref);
|
|
1405
|
+
}
|
|
865
1406
|
}
|