diff --git a/.gitignore b/.gitignore
index 2dc4818c..b9ae2c37 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,7 @@
.env.*
!.env.example
node_modules
+
+# Computer Use runtime (auto-generated)
+.runtime/
+extracted-natives/
diff --git a/README.en.md b/README.en.md
index afb19c87..a37390f3 100644
--- a/README.en.md
+++ b/README.en.md
@@ -17,6 +17,7 @@ A **locally runnable version** repaired from the leaked Claude Code source, with
- [Quick Start](#quick-start)
- [Environment Variables](#environment-variables)
- [Fallback Mode](#fallback-mode)
+- [Computer Use Desktop Control](#computer-use-desktop-control)
- [FAQ](#faq)
- [Fixes Compared with the Original Leaked Source](#fixes-compared-with-the-original-leaked-source)
- [Project Structure](#project-structure)
@@ -30,8 +31,11 @@ A **locally runnable version** repaired from the leaked Claude Code source, with
- `--print` headless mode for scripts and CI
- MCP server, plugin, and Skills support
- Custom API endpoint and model support ([Third-Party Models Guide](docs/third-party-models.en.md))
+- **Computer Use desktop control** (screenshots, mouse, keyboard, app management) — [Guide](docs/computer-use.en.md)
- Fallback Recovery CLI mode
+> **Computer Use Note**: This project includes a **modified version of Computer Use**. The official implementation relies on Anthropic's private native modules. We replaced the entire underlying operation layer with a Python bridge (`pyautogui` + `mss` + `pyobjc`), enabling anyone to use Computer Use on macOS. See the [Computer Use Guide](docs/computer-use.en.md) for details.
+
---
## Architecture Overview
@@ -214,6 +218,23 @@ CLAUDE_CODE_FORCE_RECOVERY_CLI=1 ./bin/claude-haha
---
+## Computer Use Desktop Control
+
+This project enables and modifies Claude Code's Computer Use feature (internal codename "Chicago"), allowing AI models to directly control your macOS desktop — screenshots, mouse clicks, keyboard input, app management.
+
+**Underlying modification**: The official implementation depends on Anthropic's private native modules (`@ant/computer-use-swift`, `@ant/computer-use-input`). This project replaces them entirely with a Python bridge using `pyautogui` (mouse/keyboard), `mss` (screenshots), and `pyobjc` (macOS APIs) — no closed-source binaries required.
+
+```bash
+# Ensure Python 3 and macOS Accessibility/Screen Recording permissions, then:
+./bin/claude-haha
+> Take a screenshot
+> Open Safari and search for something
+```
+
+For supported platforms, technical architecture, and approaches we tried, see: **[Computer Use Guide](docs/computer-use.en.md)**
+
+---
+
## Fixes Compared with the Original Leaked Source
The leaked source could not run directly. This repository mainly fixes the following issues:
diff --git a/README.md b/README.md
index c715228d..b4998cd8 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
- [快速开始](#快速开始)
- [环境变量说明](#环境变量说明)
- [降级模式](#降级模式)
+- [Computer Use 桌面控制](#computer-use-桌面控制)
- [常见问题](#常见问题)
- [相对于原始泄露源码的修复](#相对于原始泄露源码的修复)
- [项目结构](#项目结构)
@@ -30,8 +31,11 @@
- `--print` 无头模式(脚本/CI 场景)
- 支持 MCP 服务器、插件、Skills
- 支持自定义 API 端点和模型([第三方模型使用指南](docs/third-party-models.md))
+- **Computer Use 桌面控制**(截屏、鼠标、键盘、应用管理)— [使用指南](docs/computer-use.md)
- 降级 Recovery CLI 模式
+> **Computer Use 说明**:本项目包含**魔改版的 Computer Use** 功能。官方实现依赖 Anthropic 私有原生模块,我们替换了整个底层操作层,使用 Python bridge(`pyautogui` + `mss` + `pyobjc`)实现,使得任何人都可以在 macOS 上使用。详见 [Computer Use 功能指南](docs/computer-use.md)。
+
---
## 架构概览
@@ -214,6 +218,23 @@ CLAUDE_CODE_FORCE_RECOVERY_CLI=1 ./bin/claude-haha
---
+## Computer Use 桌面控制
+
+本项目启用并改造了 Claude Code 的 Computer Use 功能(内部代号 "Chicago"),让 AI 模型可以直接控制你的 macOS 桌面——截屏、鼠标点击、键盘输入、应用管理。
+
+**底层改造**:官方实现依赖 Anthropic 私有原生模块(`@ant/computer-use-swift`、`@ant/computer-use-input`),本项目用 Python bridge 完全替代,使用 `pyautogui`(鼠标键盘)、`mss`(截图)、`pyobjc`(macOS API),无需任何闭源二进制。
+
+```bash
+# 确保有 Python 3 和 macOS 辅助功能/屏幕录制权限,然后直接使用:
+./bin/claude-haha
+> 帮我截个屏
+> 打开网易云音乐搜索一首歌
+```
+
+详细说明、支持的设备列表、技术架构和尝试过的方案请参考:**[Computer Use 功能指南](docs/computer-use.md)**
+
+---
+
## 相对于原始泄露源码的修复
泄露的源码无法直接运行,主要修复了以下问题:
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 00000000..0d26313e
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,816 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 0,
+ "workspaces": {
+ "": {
+ "name": "claude-code-local",
+ "dependencies": {
+ "@anthropic-ai/sandbox-runtime": "^0.0.44",
+ "@anthropic-ai/sdk": "^0.80.0",
+ "@aws-sdk/client-bedrock-runtime": "^3.1020.0",
+ "@commander-js/extra-typings": "^14.0.0",
+ "@growthbook/growthbook": "^1.6.5",
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "@opentelemetry/api-logs": "^0.214.0",
+ "@opentelemetry/core": "^2.6.1",
+ "@opentelemetry/resources": "^2.6.1",
+ "@opentelemetry/sdk-logs": "^0.214.0",
+ "@opentelemetry/sdk-metrics": "^2.6.1",
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
+ "@opentelemetry/semantic-conventions": "^1.40.0",
+ "ajv": "^8.18.0",
+ "asciichart": "^1.5.25",
+ "auto-bind": "^5.0.1",
+ "axios": "^1.14.0",
+ "bidi-js": "^1.0.3",
+ "chalk": "^5.6.2",
+ "chokidar": "^5.0.0",
+ "cli-boxes": "^4.0.1",
+ "code-excerpt": "^4.0.0",
+ "diff": "^8.0.4",
+ "emoji-regex": "^10.6.0",
+ "env-paths": "^4.0.0",
+ "execa": "^9.6.1",
+ "figures": "^6.1.0",
+ "fuse.js": "^7.1.0",
+ "get-east-asian-width": "^1.5.0",
+ "google-auth-library": "^10.6.2",
+ "highlight.js": "^11.11.1",
+ "https-proxy-agent": "^8.0.0",
+ "ignore": "^7.0.5",
+ "indent-string": "^5.0.0",
+ "ink": "^6.8.0",
+ "jsonc-parser": "^3.3.1",
+ "lodash-es": "^4.17.23",
+ "lru-cache": "^11.2.7",
+ "marked": "^17.0.5",
+ "p-map": "^7.0.4",
+ "picomatch": "^4.0.4",
+ "proper-lockfile": "^4.1.2",
+ "qrcode": "^1.5.4",
+ "react": "^19.2.4",
+ "react-reconciler": "^0.33.0",
+ "semver": "^7.7.4",
+ "shell-quote": "^1.8.3",
+ "signal-exit": "^4.1.0",
+ "stack-utils": "^2.0.6",
+ "strip-ansi": "^7.2.0",
+ "supports-hyperlinks": "^4.4.0",
+ "tree-kill": "^1.2.2",
+ "type-fest": "^5.5.0",
+ "undici": "^7.24.6",
+ "usehooks-ts": "^3.1.1",
+ "vscode-jsonrpc": "^8.2.1",
+ "vscode-languageserver-types": "^3.17.5",
+ "wrap-ansi": "^10.0.0",
+ "ws": "^8.20.0",
+ "xss": "^1.0.15",
+ "yaml": "^2.8.3",
+ "zod": "^4.3.6",
+ },
+ },
+ "packages/computer-use-input": {
+ "name": "@ant/computer-use-input",
+ "version": "0.1.0",
+ },
+ "packages/computer-use-swift": {
+ "name": "@ant/computer-use-swift",
+ "version": "0.1.0",
+ },
+ },
+ "packages": {
+ "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.5", "https://registry.npmmirror.com/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.5.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw=="],
+
+ "@ant/computer-use-input": ["@ant/computer-use-input@workspace:packages/computer-use-input"],
+
+ "@ant/computer-use-swift": ["@ant/computer-use-swift@workspace:packages/computer-use-swift"],
+
+ "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.44", "https://registry.npmmirror.com/@anthropic-ai/sandbox-runtime/-/sandbox-runtime-0.0.44.tgz", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "@types/lodash-es": "^4.17.12", "commander": "^12.1.0", "lodash-es": "^4.17.23", "shell-quote": "^1.8.3", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-mmyjq0mzsHnQZyiU+FGYyaiJcPckuQpP78VB8iqFi2IOu8rcb9i5SmaOKyJENJNfY8l/1grzLMQgWq4Apvmozw=="],
+
+ "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.80.0", "https://registry.npmmirror.com/@anthropic-ai/sdk/-/sdk-0.80.0.tgz", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g=="],
+
+ "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "https://registry.npmmirror.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
+
+ "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
+
+ "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "https://registry.npmmirror.com/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="],
+
+ "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "https://registry.npmmirror.com/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="],
+
+ "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "https://registry.npmmirror.com/@aws-crypto/util/-/util-5.2.0.tgz", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
+
+ "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1020.0", "https://registry.npmmirror.com/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1020.0.tgz", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-node": "^3.972.28", "@aws-sdk/eventstream-handler-node": "^3.972.12", "@aws-sdk/middleware-eventstream": "^3.972.8", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/middleware-websocket": "^3.972.14", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/token-providers": "3.1020.0", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/eventstream-serde-config-resolver": "^4.3.12", "@smithy/eventstream-serde-node": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.45", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-nqDCbaB05gRc3FuIEN74Mo04+k8RNI0YT2YBAU/9nioqgDyoqzMx8Ia2QWaw9UhUyIHMBjcFEfKIPfCZx7caCw=="],
+
+ "@aws-sdk/core": ["@aws-sdk/core@3.973.26", "https://registry.npmmirror.com/@aws-sdk/core/-/core-3.973.26.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/xml-builder": "^3.972.16", "@smithy/core": "^3.23.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ=="],
+
+ "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.24", "https://registry.npmmirror.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.24.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w=="],
+
+ "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.26", "https://registry.npmmirror.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.26.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA=="],
+
+ "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.27", "https://registry.npmmirror.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.27.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-login": "^3.972.27", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.27", "@aws-sdk/credential-provider-web-identity": "^3.972.27", "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Um26EsNSUfVUX0wUXnUA1W3wzKhVy6nviEElsh5lLZUYj9bk6DXOPnpte0gt+WHubcVfVsRk40bbm4KaroTEag=="],
+
+ "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.27", "https://registry.npmmirror.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.27.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-t3ehEtHomGZwg5Gixw4fYbYtG9JBnjfAjSDabxhPEu/KLLUp0BB37/APX7MSKXQhX6ZH7pseuACFJ19NrAkNdg=="],
+
+ "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.28", "https://registry.npmmirror.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.28.tgz", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.24", "@aws-sdk/credential-provider-http": "^3.972.26", "@aws-sdk/credential-provider-ini": "^3.972.27", "@aws-sdk/credential-provider-process": "^3.972.24", "@aws-sdk/credential-provider-sso": "^3.972.27", "@aws-sdk/credential-provider-web-identity": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-rren+P6k5rShG5PX61iVi40kKdueyuMLBRTctQbyR5LooO9Ygr5L6R7ilG7RF1957NSH3KC3TU206fZuKwjSpQ=="],
+
+ "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.24", "https://registry.npmmirror.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.24.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw=="],
+
+ "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.27", "https://registry.npmmirror.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.27.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/token-providers": "3.1020.0", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWXeGjlbBuHcm9appZUgXKP2zHDyTti0/+gXpSFJ2J3CnSwf1KWjicjN0qG2ozkMH6blrrzMrimeIOEYNl238Q=="],
+
+ "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.27", "https://registry.npmmirror.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.27.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CUY4hQIFswdQNEsRGEzGBUKGMK5KpqmNDdu2ROMgI+45PLFS8H0y3Tm7kvM16uvvw3n1pVxk85tnRVUTgtaa1w=="],
+
+ "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.12", "https://registry.npmmirror.com/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.12.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ruyc/MNR6e+cUrGCth7fLQ12RXBZDy/bV06tgqB9Z5n/0SN/C0m6bsQEV8FF9zPI6VSAOaRd0rNgmpYVnGawrQ=="],
+
+ "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.8", "https://registry.npmmirror.com/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.8.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ=="],
+
+ "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.8", "https://registry.npmmirror.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ=="],
+
+ "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.8", "https://registry.npmmirror.com/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA=="],
+
+ "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.9", "https://registry.npmmirror.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ=="],
+
+ "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.27", "https://registry.npmmirror.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.27.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-TIRLO5UR2+FVUGmhYoAwVkKhcVzywEDX/5LzR9tjy1h8FQAXOtFg2IqgmwvxU7y933rkTn9rl6AdgcAUgQ1/Kg=="],
+
+ "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.14", "https://registry.npmmirror.com/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.14.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-format-url": "^3.972.8", "@smithy/eventstream-codec": "^4.2.12", "@smithy/eventstream-serde-browser": "^4.2.12", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/protocol-http": "^5.3.12", "@smithy/signature-v4": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-qnfDlIHjm6DrTYNvWOUbnZdVKgtoKbO/Qzj+C0Wp5Y7VUrsvBRQtGKxD+hc+mRTS4N0kBJ6iZ3+zxm4N1OSyjg=="],
+
+ "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.996.17", "https://registry.npmmirror.com/@aws-sdk/nested-clients/-/nested-clients-3.996.17.tgz", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.26", "@aws-sdk/middleware-host-header": "^3.972.8", "@aws-sdk/middleware-logger": "^3.972.8", "@aws-sdk/middleware-recursion-detection": "^3.972.9", "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/region-config-resolver": "^3.972.10", "@aws-sdk/types": "^3.973.6", "@aws-sdk/util-endpoints": "^3.996.5", "@aws-sdk/util-user-agent-browser": "^3.972.8", "@aws-sdk/util-user-agent-node": "^3.973.13", "@smithy/config-resolver": "^4.4.13", "@smithy/core": "^3.23.13", "@smithy/fetch-http-handler": "^5.3.15", "@smithy/hash-node": "^4.2.12", "@smithy/invalid-dependency": "^4.2.12", "@smithy/middleware-content-length": "^4.2.12", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-retry": "^4.4.45", "@smithy/middleware-serde": "^4.2.16", "@smithy/middleware-stack": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/node-http-handler": "^4.5.1", "@smithy/protocol-http": "^5.3.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.44", "@smithy/util-defaults-mode-node": "^4.2.48", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.12", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-7B0HIX0tEFmOSJuWzdHZj1WhMXSryM+h66h96ZkqSncoY7J6wq61KOu4Kr57b/YnJP3J/EeQYVFulgR281h+7A=="],
+
+ "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.10", "https://registry.npmmirror.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.10.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/config-resolver": "^4.4.13", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ=="],
+
+ "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1020.0", "https://registry.npmmirror.com/@aws-sdk/token-providers/-/token-providers-3.1020.0.tgz", { "dependencies": { "@aws-sdk/core": "^3.973.26", "@aws-sdk/nested-clients": "^3.996.17", "@aws-sdk/types": "^3.973.6", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-T61KA/VKl0zVUubdxigr1ut7SEpwE1/4CIKb14JDLyTAOne2yWKtQE1dDCSHl0UqrZNwW/bTt+EBHfQbslZJdw=="],
+
+ "@aws-sdk/types": ["@aws-sdk/types@3.973.6", "https://registry.npmmirror.com/@aws-sdk/types/-/types-3.973.6.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw=="],
+
+ "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.5", "https://registry.npmmirror.com/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" } }, "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw=="],
+
+ "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.8", "https://registry.npmmirror.com/@aws-sdk/util-format-url/-/util-format-url-3.972.8.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A=="],
+
+ "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.5", "https://registry.npmmirror.com/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ=="],
+
+ "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.8", "https://registry.npmmirror.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", { "dependencies": { "@aws-sdk/types": "^3.973.6", "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA=="],
+
+ "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.973.13", "https://registry.npmmirror.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.13.tgz", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.27", "@aws-sdk/types": "^3.973.6", "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-s1dCJ0J9WU9UPkT3FFqhKTSquYTkqWXGRaapHFyWwwJH86ZussewhNST5R5TwXVL1VSHq4aJVl9fWK+svaRVCQ=="],
+
+ "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.16", "https://registry.npmmirror.com/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" } }, "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A=="],
+
+ "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "https://registry.npmmirror.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.2", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
+
+ "@commander-js/extra-typings": ["@commander-js/extra-typings@14.0.0", "https://registry.npmmirror.com/@commander-js/extra-typings/-/extra-typings-14.0.0.tgz", { "peerDependencies": { "commander": "~14.0.0" } }, "sha512-hIn0ncNaJRLkZrxBIp5AsW/eXEHNKYQBh0aPdoUqNgD+Io3NIykQqpKFyKcuasZhicGaEZJX/JBSIkZ4e5x8Dg=="],
+
+ "@growthbook/growthbook": ["@growthbook/growthbook@1.6.5", "https://registry.npmmirror.com/@growthbook/growthbook/-/growthbook-1.6.5.tgz", { "dependencies": { "dom-mutator": "^0.6.0" } }, "sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A=="],
+
+ "@hono/node-server": ["@hono/node-server@1.19.12", "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.12.tgz", { "peerDependencies": { "hono": "^4" } }, "sha512-txsUW4SQ1iilgE0l9/e9VQWmELXifEFvmdA1j6WFh/aFPj99hIntrSsq/if0UWyGVkmrRPKA1wCeP+UCr1B9Uw=="],
+
+ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "https://registry.npmmirror.com/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
+
+ "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.1.tgz", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
+
+ "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "https://registry.npmmirror.com/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
+
+ "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "https://registry.npmmirror.com/@opentelemetry/core/-/core-2.6.1.tgz", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
+
+ "@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "https://registry.npmmirror.com/@opentelemetry/resources/-/resources-2.6.1.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
+
+ "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "https://registry.npmmirror.com/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
+
+ "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "https://registry.npmmirror.com/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="],
+
+ "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "https://registry.npmmirror.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
+
+ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "https://registry.npmmirror.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
+
+ "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "https://registry.npmmirror.com/@pondwader/socks5-server/-/socks5-server-1.0.10.tgz", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="],
+
+ "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
+
+ "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "https://registry.npmmirror.com/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="],
+
+ "@smithy/config-resolver": ["@smithy/config-resolver@4.4.13", "https://registry.npmmirror.com/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "@smithy/util-config-provider": "^4.2.2", "@smithy/util-endpoints": "^3.3.3", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg=="],
+
+ "@smithy/core": ["@smithy/core@3.23.13", "https://registry.npmmirror.com/@smithy/core/-/core-3.23.13.tgz", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-stream": "^4.5.21", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q=="],
+
+ "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.12", "https://registry.npmmirror.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg=="],
+
+ "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.12", "https://registry.npmmirror.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA=="],
+
+ "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.12", "https://registry.npmmirror.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A=="],
+
+ "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.12", "https://registry.npmmirror.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q=="],
+
+ "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.12", "https://registry.npmmirror.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA=="],
+
+ "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.12", "https://registry.npmmirror.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", { "dependencies": { "@smithy/eventstream-codec": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ=="],
+
+ "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.15", "https://registry.npmmirror.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" } }, "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A=="],
+
+ "@smithy/hash-node": ["@smithy/hash-node@4.2.12", "https://registry.npmmirror.com/@smithy/hash-node/-/hash-node-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w=="],
+
+ "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.12", "https://registry.npmmirror.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g=="],
+
+ "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.2", "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow=="],
+
+ "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.12", "https://registry.npmmirror.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA=="],
+
+ "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.28", "https://registry.npmmirror.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.28.tgz", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-serde": "^4.2.16", "@smithy/node-config-provider": "^4.3.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "@smithy/url-parser": "^4.2.12", "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" } }, "sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ=="],
+
+ "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.46", "https://registry.npmmirror.com/@smithy/middleware-retry/-/middleware-retry-4.4.46.tgz", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/service-error-classification": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "@smithy/util-middleware": "^4.2.12", "@smithy/util-retry": "^4.2.13", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" } }, "sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow=="],
+
+ "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.16", "https://registry.npmmirror.com/@smithy/middleware-serde/-/middleware-serde-4.2.16.tgz", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA=="],
+
+ "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.12", "https://registry.npmmirror.com/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw=="],
+
+ "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.12", "https://registry.npmmirror.com/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw=="],
+
+ "@smithy/node-http-handler": ["@smithy/node-http-handler@4.5.1", "https://registry.npmmirror.com/@smithy/node-http-handler/-/node-http-handler-4.5.1.tgz", { "dependencies": { "@smithy/protocol-http": "^5.3.12", "@smithy/querystring-builder": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw=="],
+
+ "@smithy/property-provider": ["@smithy/property-provider@4.2.12", "https://registry.npmmirror.com/@smithy/property-provider/-/property-provider-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A=="],
+
+ "@smithy/protocol-http": ["@smithy/protocol-http@5.3.12", "https://registry.npmmirror.com/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw=="],
+
+ "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.12", "https://registry.npmmirror.com/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg=="],
+
+ "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "https://registry.npmmirror.com/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="],
+
+ "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.12", "https://registry.npmmirror.com/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1" } }, "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ=="],
+
+ "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.7", "https://registry.npmmirror.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw=="],
+
+ "@smithy/signature-v4": ["@smithy/signature-v4@5.3.12", "https://registry.npmmirror.com/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-middleware": "^4.2.12", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw=="],
+
+ "@smithy/smithy-client": ["@smithy/smithy-client@4.12.8", "https://registry.npmmirror.com/@smithy/smithy-client/-/smithy-client-4.12.8.tgz", { "dependencies": { "@smithy/core": "^3.23.13", "@smithy/middleware-endpoint": "^4.4.28", "@smithy/middleware-stack": "^4.2.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "@smithy/util-stream": "^4.5.21", "tslib": "^2.6.2" } }, "sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA=="],
+
+ "@smithy/types": ["@smithy/types@4.13.1", "https://registry.npmmirror.com/@smithy/types/-/types-4.13.1.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="],
+
+ "@smithy/url-parser": ["@smithy/url-parser@4.2.12", "https://registry.npmmirror.com/@smithy/url-parser/-/url-parser-4.2.12.tgz", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="],
+
+ "@smithy/util-base64": ["@smithy/util-base64@4.3.2", "https://registry.npmmirror.com/@smithy/util-base64/-/util-base64-4.3.2.tgz", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ=="],
+
+ "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.2", "https://registry.npmmirror.com/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ=="],
+
+ "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.3", "https://registry.npmmirror.com/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g=="],
+
+ "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="],
+
+ "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.2", "https://registry.npmmirror.com/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ=="],
+
+ "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.44", "https://registry.npmmirror.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.44.tgz", { "dependencies": { "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA=="],
+
+ "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.48", "https://registry.npmmirror.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.48.tgz", { "dependencies": { "@smithy/config-resolver": "^4.4.13", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/node-config-provider": "^4.3.12", "@smithy/property-provider": "^4.2.12", "@smithy/smithy-client": "^4.12.8", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg=="],
+
+ "@smithy/util-endpoints": ["@smithy/util-endpoints@3.3.3", "https://registry.npmmirror.com/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", { "dependencies": { "@smithy/node-config-provider": "^4.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig=="],
+
+ "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.2", "https://registry.npmmirror.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg=="],
+
+ "@smithy/util-middleware": ["@smithy/util-middleware@4.2.12", "https://registry.npmmirror.com/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ=="],
+
+ "@smithy/util-retry": ["@smithy/util-retry@4.2.13", "https://registry.npmmirror.com/@smithy/util-retry/-/util-retry-4.2.13.tgz", { "dependencies": { "@smithy/service-error-classification": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ=="],
+
+ "@smithy/util-stream": ["@smithy/util-stream@4.5.21", "https://registry.npmmirror.com/@smithy/util-stream/-/util-stream-4.5.21.tgz", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="],
+
+ "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.2", "https://registry.npmmirror.com/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw=="],
+
+ "@smithy/util-utf8": ["@smithy/util-utf8@4.2.2", "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", { "dependencies": { "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw=="],
+
+ "@smithy/uuid": ["@smithy/uuid@1.1.2", "https://registry.npmmirror.com/@smithy/uuid/-/uuid-1.1.2.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g=="],
+
+ "@types/lodash": ["@types/lodash@4.17.24", "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", {}, "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ=="],
+
+ "@types/lodash-es": ["@types/lodash-es@4.17.12", "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
+
+ "accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
+
+ "agent-base": ["agent-base@8.0.0", "https://registry.npmmirror.com/agent-base/-/agent-base-8.0.0.tgz", {}, "sha512-QT8i0hCz6C/KQ+KTAbSNwCHDGdmUJl2tp2ZpNlGSWCfhUNVbYG2WLE3MdZGBAgXPV4GAvjGMxo+C1hroyxmZEg=="],
+
+ "ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
+
+ "ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
+
+ "ansi-escapes": ["ansi-escapes@7.3.0", "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
+
+ "ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
+
+ "ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "asciichart": ["asciichart@1.5.25", "https://registry.npmmirror.com/asciichart/-/asciichart-1.5.25.tgz", {}, "sha512-PNxzXIPPOtWq8T7bgzBtk9cI2lgS4SJZthUHEiQ1aoIc3lNzGfUvIvo9LiAnq26TACo9t1/4qP6KTGAUbzX9Xg=="],
+
+ "asynckit": ["asynckit@0.4.0", "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "auto-bind": ["auto-bind@5.0.1", "https://registry.npmmirror.com/auto-bind/-/auto-bind-5.0.1.tgz", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
+
+ "axios": ["axios@1.14.0", "https://registry.npmmirror.com/axios/-/axios-1.14.0.tgz", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
+
+ "base64-js": ["base64-js@1.5.1", "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
+
+ "bidi-js": ["bidi-js@1.0.3", "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
+
+ "bignumber.js": ["bignumber.js@9.3.1", "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
+
+ "body-parser": ["body-parser@2.2.2", "https://registry.npmmirror.com/body-parser/-/body-parser-2.2.2.tgz", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
+
+ "bowser": ["bowser@2.14.1", "https://registry.npmmirror.com/bowser/-/bowser-2.14.1.tgz", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="],
+
+ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
+
+ "bytes": ["bytes@3.1.2", "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
+
+ "camelcase": ["camelcase@5.3.1", "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
+
+ "chalk": ["chalk@5.6.2", "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+
+ "chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
+
+ "cli-boxes": ["cli-boxes@4.0.1", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-4.0.1.tgz", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="],
+
+ "cli-cursor": ["cli-cursor@4.0.0", "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-4.0.0.tgz", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
+
+ "cli-truncate": ["cli-truncate@5.2.0", "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
+
+ "cliui": ["cliui@6.0.0", "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
+
+ "code-excerpt": ["code-excerpt@4.0.0", "https://registry.npmmirror.com/code-excerpt/-/code-excerpt-4.0.0.tgz", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
+
+ "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
+
+ "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "commander": ["commander@12.1.0", "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
+
+ "content-disposition": ["content-disposition@1.0.1", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.1.tgz", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
+
+ "content-type": ["content-type@1.0.5", "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
+
+ "convert-to-spaces": ["convert-to-spaces@2.0.1", "https://registry.npmmirror.com/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
+
+ "cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
+
+ "cookie-signature": ["cookie-signature@1.2.2", "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.2.2.tgz", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
+
+ "cors": ["cors@2.8.6", "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
+
+ "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
+
+ "cssfilter": ["cssfilter@0.0.10", "https://registry.npmmirror.com/cssfilter/-/cssfilter-0.0.10.tgz", {}, "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw=="],
+
+ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
+
+ "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decamelize": ["decamelize@1.2.0", "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
+
+ "diff": ["diff@8.0.4", "https://registry.npmmirror.com/diff/-/diff-8.0.4.tgz", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="],
+
+ "dijkstrajs": ["dijkstrajs@1.0.3", "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="],
+
+ "dom-mutator": ["dom-mutator@0.6.0", "https://registry.npmmirror.com/dom-mutator/-/dom-mutator-0.6.0.tgz", {}, "sha512-iCt9o0aYfXMUkz/43ZOAUFQYotjGB+GNbYJiJdz4TgXkyToXbbRy5S6FbTp72lRBtfpUMwEc1KmpFEU4CZeoNg=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
+
+ "ee-first": ["ee-first@1.1.1", "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
+
+ "emoji-regex": ["emoji-regex@10.6.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
+
+ "encodeurl": ["encodeurl@2.0.0", "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
+
+ "env-paths": ["env-paths@4.0.0", "https://registry.npmmirror.com/env-paths/-/env-paths-4.0.0.tgz", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="],
+
+ "environment": ["environment@1.1.0", "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "es-toolkit": ["es-toolkit@1.45.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.45.1.tgz", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
+
+ "escape-html": ["escape-html@1.0.3", "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
+
+ "escape-string-regexp": ["escape-string-regexp@2.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
+
+ "etag": ["etag@1.8.1", "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
+
+ "eventsource": ["eventsource@3.0.7", "https://registry.npmmirror.com/eventsource/-/eventsource-3.0.7.tgz", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
+
+ "eventsource-parser": ["eventsource-parser@3.0.6", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
+
+ "execa": ["execa@9.6.1", "https://registry.npmmirror.com/execa/-/execa-9.6.1.tgz", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="],
+
+ "express": ["express@5.2.1", "https://registry.npmmirror.com/express/-/express-5.2.1.tgz", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
+
+ "express-rate-limit": ["express-rate-limit@8.3.2", "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-8.3.2.tgz", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg=="],
+
+ "extend": ["extend@3.0.2", "https://registry.npmmirror.com/extend/-/extend-3.0.2.tgz", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
+
+ "fast-xml-builder": ["fast-xml-builder@1.1.4", "https://registry.npmmirror.com/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", { "dependencies": { "path-expression-matcher": "^1.1.3" } }, "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg=="],
+
+ "fast-xml-parser": ["fast-xml-parser@5.5.8", "https://registry.npmmirror.com/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", { "dependencies": { "fast-xml-builder": "^1.1.4", "path-expression-matcher": "^1.2.0", "strnum": "^2.2.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ=="],
+
+ "fetch-blob": ["fetch-blob@3.2.0", "https://registry.npmmirror.com/fetch-blob/-/fetch-blob-3.2.0.tgz", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
+
+ "figures": ["figures@6.1.0", "https://registry.npmmirror.com/figures/-/figures-6.1.0.tgz", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="],
+
+ "finalhandler": ["finalhandler@2.1.1", "https://registry.npmmirror.com/finalhandler/-/finalhandler-2.1.1.tgz", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
+
+ "find-up": ["find-up@4.1.0", "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "form-data": ["form-data@4.0.5", "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "formdata-polyfill": ["formdata-polyfill@4.0.10", "https://registry.npmmirror.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
+
+ "forwarded": ["forwarded@0.2.0", "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
+
+ "fresh": ["fresh@2.0.0", "https://registry.npmmirror.com/fresh/-/fresh-2.0.0.tgz", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
+
+ "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "fuse.js": ["fuse.js@7.1.0", "https://registry.npmmirror.com/fuse.js/-/fuse.js-7.1.0.tgz", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
+
+ "gaxios": ["gaxios@7.1.4", "https://registry.npmmirror.com/gaxios/-/gaxios-7.1.4.tgz", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="],
+
+ "gcp-metadata": ["gcp-metadata@8.1.2", "https://registry.npmmirror.com/gcp-metadata/-/gcp-metadata-8.1.2.tgz", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="],
+
+ "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
+
+ "get-east-asian-width": ["get-east-asian-width@1.5.0", "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "get-stream": ["get-stream@9.0.1", "https://registry.npmmirror.com/get-stream/-/get-stream-9.0.1.tgz", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
+
+ "google-auth-library": ["google-auth-library@10.6.2", "https://registry.npmmirror.com/google-auth-library/-/google-auth-library-10.6.2.tgz", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="],
+
+ "google-logging-utils": ["google-logging-utils@1.1.3", "https://registry.npmmirror.com/google-logging-utils/-/google-logging-utils-1.1.3.tgz", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
+
+ "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
+
+ "has-flag": ["has-flag@5.0.1", "https://registry.npmmirror.com/has-flag/-/has-flag-5.0.1.tgz", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "highlight.js": ["highlight.js@11.11.1", "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
+
+ "hono": ["hono@4.12.9", "https://registry.npmmirror.com/hono/-/hono-4.12.9.tgz", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
+
+ "http-errors": ["http-errors@2.0.1", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
+
+ "https-proxy-agent": ["https-proxy-agent@8.0.0", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-8.0.0.tgz", { "dependencies": { "agent-base": "8.0.0", "debug": "^4.3.4" } }, "sha512-YYeW+iCnAS3xhvj2dvVoWgsbca3RfQy/IlaNHHOtDmU0jMqPI9euIq3Y9BJETdxk16h9NHHCKqp/KB9nIMStCQ=="],
+
+ "human-signals": ["human-signals@8.0.1", "https://registry.npmmirror.com/human-signals/-/human-signals-8.0.1.tgz", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="],
+
+ "iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
+
+ "ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "indent-string": ["indent-string@5.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-5.0.0.tgz", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
+
+ "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
+
+ "ink": ["ink@6.8.0", "https://registry.npmmirror.com/ink/-/ink-6.8.0.tgz", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.4", "ansi-escapes": "^7.3.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^5.1.1", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.33.0", "scheduler": "^0.27.0", "signal-exit": "^3.0.7", "slice-ansi": "^8.0.0", "stack-utils": "^2.0.6", "string-width": "^8.1.1", "terminal-size": "^4.0.1", "type-fest": "^5.4.1", "widest-line": "^6.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": ">=6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-sbl1RdLOgkO9isK42WCZlJCFN9hb++sX9dsklOvfd1YQ3bQ2AiFu12Q6tFlr0HvEUvzraJntQCCpfEoUe9DSzA=="],
+
+ "ip-address": ["ip-address@10.1.0", "https://registry.npmmirror.com/ip-address/-/ip-address-10.1.0.tgz", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
+
+ "ipaddr.js": ["ipaddr.js@1.9.1", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
+
+ "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
+
+ "is-in-ci": ["is-in-ci@2.0.0", "https://registry.npmmirror.com/is-in-ci/-/is-in-ci-2.0.0.tgz", { "bin": "cli.js" }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-promise": ["is-promise@4.0.0", "https://registry.npmmirror.com/is-promise/-/is-promise-4.0.0.tgz", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
+
+ "is-safe-filename": ["is-safe-filename@0.1.1", "https://registry.npmmirror.com/is-safe-filename/-/is-safe-filename-0.1.1.tgz", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="],
+
+ "is-stream": ["is-stream@4.0.1", "https://registry.npmmirror.com/is-stream/-/is-stream-4.0.1.tgz", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
+
+ "is-unicode-supported": ["is-unicode-supported@2.1.0", "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
+
+ "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
+
+ "jose": ["jose@6.2.2", "https://registry.npmmirror.com/jose/-/jose-6.2.2.tgz", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
+
+ "json-bigint": ["json-bigint@1.0.0", "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
+
+ "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "https://registry.npmmirror.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
+
+ "json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
+
+ "json-schema-typed": ["json-schema-typed@8.0.2", "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
+
+ "jsonc-parser": ["jsonc-parser@3.3.1", "https://registry.npmmirror.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="],
+
+ "jwa": ["jwa@2.0.1", "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
+
+ "jws": ["jws@4.0.1", "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
+
+ "locate-path": ["locate-path@5.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
+
+ "lodash-es": ["lodash-es@4.17.23", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
+
+ "lodash.debounce": ["lodash.debounce@4.0.8", "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="],
+
+ "lru-cache": ["lru-cache@11.2.7", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.7.tgz", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="],
+
+ "marked": ["marked@17.0.5", "https://registry.npmmirror.com/marked/-/marked-17.0.5.tgz", { "bin": "bin/marked.js" }, "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "media-typer": ["media-typer@1.1.0", "https://registry.npmmirror.com/media-typer/-/media-typer-1.1.0.tgz", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
+
+ "merge-descriptors": ["merge-descriptors@2.0.0", "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
+
+ "mime-db": ["mime-db@1.54.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
+
+ "mime-types": ["mime-types@3.0.2", "https://registry.npmmirror.com/mime-types/-/mime-types-3.0.2.tgz", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
+
+ "mimic-fn": ["mimic-fn@2.1.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-2.1.0.tgz", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
+
+ "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "negotiator": ["negotiator@1.0.0", "https://registry.npmmirror.com/negotiator/-/negotiator-1.0.0.tgz", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
+
+ "node-domexception": ["node-domexception@1.0.0", "https://registry.npmmirror.com/node-domexception/-/node-domexception-1.0.0.tgz", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
+
+ "node-fetch": ["node-fetch@3.3.2", "https://registry.npmmirror.com/node-fetch/-/node-fetch-3.3.2.tgz", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+
+ "npm-run-path": ["npm-run-path@6.0.0", "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-6.0.0.tgz", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="],
+
+ "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
+
+ "object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
+
+ "on-finished": ["on-finished@2.4.1", "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
+
+ "once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
+
+ "onetime": ["onetime@5.1.2", "https://registry.npmmirror.com/onetime/-/onetime-5.1.2.tgz", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
+
+ "p-limit": ["p-limit@2.3.0", "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
+
+ "p-locate": ["p-locate@4.1.0", "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
+
+ "p-map": ["p-map@7.0.4", "https://registry.npmmirror.com/p-map/-/p-map-7.0.4.tgz", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="],
+
+ "p-try": ["p-try@2.2.0", "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
+
+ "parse-ms": ["parse-ms@4.0.0", "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="],
+
+ "parseurl": ["parseurl@1.3.3", "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
+
+ "patch-console": ["patch-console@2.0.0", "https://registry.npmmirror.com/patch-console/-/patch-console-2.0.0.tgz", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
+
+ "path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-expression-matcher": ["path-expression-matcher@1.2.0", "https://registry.npmmirror.com/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", {}, "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ=="],
+
+ "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "path-to-regexp": ["path-to-regexp@8.4.1", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-8.4.1.tgz", {}, "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw=="],
+
+ "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "pkce-challenge": ["pkce-challenge@5.0.1", "https://registry.npmmirror.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
+
+ "pngjs": ["pngjs@5.0.0", "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
+
+ "pretty-ms": ["pretty-ms@9.3.0", "https://registry.npmmirror.com/pretty-ms/-/pretty-ms-9.3.0.tgz", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="],
+
+ "proper-lockfile": ["proper-lockfile@4.1.2", "https://registry.npmmirror.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
+
+ "proxy-addr": ["proxy-addr@2.0.7", "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
+
+ "proxy-from-env": ["proxy-from-env@2.1.0", "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-2.1.0.tgz", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
+
+ "qrcode": ["qrcode@1.5.4", "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": "bin/qrcode" }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="],
+
+ "qs": ["qs@6.15.0", "https://registry.npmmirror.com/qs/-/qs-6.15.0.tgz", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
+
+ "range-parser": ["range-parser@1.2.1", "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
+
+ "raw-body": ["raw-body@3.0.2", "https://registry.npmmirror.com/raw-body/-/raw-body-3.0.2.tgz", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
+
+ "react": ["react@19.2.4", "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-reconciler": ["react-reconciler@0.33.0", "https://registry.npmmirror.com/react-reconciler/-/react-reconciler-0.33.0.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.0" } }, "sha512-KetWRytFv1epdpJc3J4G75I4WrplZE5jOL7Yq0p34+OVOKF4Se7WrdIdVC45XsSSmUTlht2FM/fM1FZb1mfQeA=="],
+
+ "readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
+
+ "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
+
+ "require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
+
+ "require-main-filename": ["require-main-filename@2.0.0", "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
+
+ "restore-cursor": ["restore-cursor@4.0.0", "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-4.0.0.tgz", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
+
+ "retry": ["retry@0.12.0", "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
+
+ "router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
+
+ "safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "send": ["send@1.2.1", "https://registry.npmmirror.com/send/-/send-1.2.1.tgz", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
+
+ "serve-static": ["serve-static@2.2.1", "https://registry.npmmirror.com/serve-static/-/serve-static-2.2.1.tgz", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
+
+ "set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
+
+ "setprototypeof": ["setprototypeof@1.2.0", "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
+
+ "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
+
+ "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
+
+ "shell-quote": ["shell-quote@1.8.3", "https://registry.npmmirror.com/shell-quote/-/shell-quote-1.8.3.tgz", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
+
+ "side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
+
+ "side-channel-list": ["side-channel-list@1.0.0", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
+
+ "side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
+
+ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
+
+ "signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
+
+ "slice-ansi": ["slice-ansi@8.0.0", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
+
+ "stack-utils": ["stack-utils@2.0.6", "https://registry.npmmirror.com/stack-utils/-/stack-utils-2.0.6.tgz", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
+
+ "statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
+
+ "string-width": ["string-width@8.2.0", "https://registry.npmmirror.com/string-width/-/string-width-8.2.0.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw=="],
+
+ "strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
+
+ "strip-final-newline": ["strip-final-newline@4.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="],
+
+ "strnum": ["strnum@2.2.2", "https://registry.npmmirror.com/strnum/-/strnum-2.2.2.tgz", {}, "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA=="],
+
+ "supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
+
+ "supports-hyperlinks": ["supports-hyperlinks@4.4.0", "https://registry.npmmirror.com/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg=="],
+
+ "tagged-tag": ["tagged-tag@1.0.0", "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="],
+
+ "terminal-size": ["terminal-size@4.0.1", "https://registry.npmmirror.com/terminal-size/-/terminal-size-4.0.1.tgz", {}, "sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ=="],
+
+ "toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
+
+ "tree-kill": ["tree-kill@1.2.2", "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", { "bin": "cli.js" }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
+
+ "ts-algebra": ["ts-algebra@2.0.0", "https://registry.npmmirror.com/ts-algebra/-/ts-algebra-2.0.0.tgz", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
+
+ "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "type-fest": ["type-fest@5.5.0", "https://registry.npmmirror.com/type-fest/-/type-fest-5.5.0.tgz", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="],
+
+ "type-is": ["type-is@2.0.1", "https://registry.npmmirror.com/type-is/-/type-is-2.0.1.tgz", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
+
+ "undici": ["undici@7.24.6", "https://registry.npmmirror.com/undici/-/undici-7.24.6.tgz", {}, "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA=="],
+
+ "unicorn-magic": ["unicorn-magic@0.3.0", "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="],
+
+ "unpipe": ["unpipe@1.0.0", "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
+
+ "usehooks-ts": ["usehooks-ts@3.1.1", "https://registry.npmmirror.com/usehooks-ts/-/usehooks-ts-3.1.1.tgz", { "dependencies": { "lodash.debounce": "^4.0.8" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA=="],
+
+ "vary": ["vary@1.1.2", "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
+
+ "vscode-jsonrpc": ["vscode-jsonrpc@8.2.1", "https://registry.npmmirror.com/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz", {}, "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ=="],
+
+ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "https://registry.npmmirror.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="],
+
+ "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "https://registry.npmmirror.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
+
+ "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
+
+ "which-module": ["which-module@2.0.1", "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
+
+ "widest-line": ["widest-line@6.0.0", "https://registry.npmmirror.com/widest-line/-/widest-line-6.0.0.tgz", { "dependencies": { "string-width": "^8.1.0" } }, "sha512-U89AsyEeAsyoF0zVJBkG9zBgekjgjK7yk9sje3F4IQpXBJ10TF6ByLlIfjMhcmHMJgHZI4KHt4rdNfktzxIAMA=="],
+
+ "wrap-ansi": ["wrap-ansi@10.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
+
+ "wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
+
+ "ws": ["ws@8.20.0", "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
+
+ "xss": ["xss@1.0.15", "https://registry.npmmirror.com/xss/-/xss-1.0.15.tgz", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": "bin/xss" }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="],
+
+ "y18n": ["y18n@4.0.3", "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
+
+ "yaml": ["yaml@2.8.3", "https://registry.npmmirror.com/yaml/-/yaml-2.8.3.tgz", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
+
+ "yargs": ["yargs@15.4.1", "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
+
+ "yargs-parser": ["yargs-parser@18.1.3", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
+
+ "yoctocolors": ["yoctocolors@2.1.2", "https://registry.npmmirror.com/yoctocolors/-/yoctocolors-2.1.2.tgz", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="],
+
+ "yoga-layout": ["yoga-layout@3.2.1", "https://registry.npmmirror.com/yoga-layout/-/yoga-layout-3.2.1.tgz", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
+
+ "zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "https://registry.npmmirror.com/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
+
+ "@anthropic-ai/sandbox-runtime/zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
+
+ "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
+
+ "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "https://registry.npmmirror.com/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
+
+ "@commander-js/extra-typings/commander": ["commander@14.0.3", "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
+
+ "cliui/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "cliui/strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "cliui/wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
+
+ "form-data/mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "ink/cli-boxes": ["cli-boxes@3.0.0", "https://registry.npmmirror.com/cli-boxes/-/cli-boxes-3.0.0.tgz", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
+
+ "ink/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+
+ "ink/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
+
+ "npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
+
+ "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+
+ "restore-cursor/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
+
+ "xss/commander": ["commander@2.20.3", "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
+
+ "yargs/string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
+
+ "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
+
+ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "https://registry.npmmirror.com/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
+
+ "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
+
+ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ink/wrap-ansi/string-width": ["string-width@7.2.0", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
+
+ "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
+
+ "yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
+
+ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
+
+ "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
+
+ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "https://registry.npmmirror.com/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
+
+ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ }
+}
diff --git a/docs/computer-use.en.md b/docs/computer-use.en.md
new file mode 100644
index 00000000..031a0930
--- /dev/null
+++ b/docs/computer-use.en.md
@@ -0,0 +1,241 @@
+# Computer Use Guide
+
+
中文 | English
+
+> **Modified Version**: This feature is a **heavily modified version** of the Computer Use (internal codename "Chicago") found in the leaked Claude Code source. The official implementation relies on Anthropic's private native modules (`@ant/computer-use-swift`, `@ant/computer-use-input`) that are not publicly available. We **replaced the entire underlying operation layer** with a Python bridge (`pyautogui` + `mss` + `pyobjc`), enabling anyone to run Computer Use on macOS.
+
+---
+
+## Table of Contents
+
+- [Overview](#overview)
+- [Supported Platforms](#supported-platforms)
+- [How It Works](#how-it-works)
+- [Quick Start](#quick-start)
+- [Usage](#usage)
+- [Security](#security)
+- [Environment Variables](#environment-variables)
+- [Technical Architecture](#technical-architecture)
+- [Approaches We Tried](#approaches-we-tried)
+- [Known Limitations](#known-limitations)
+- [References and Credits](#references-and-credits)
+
+---
+
+## Overview
+
+Computer Use allows AI models to **directly control your computer** — taking screenshots, moving the mouse, clicking buttons, typing text, and managing application windows.
+
+24 MCP tools are available:
+
+| Category | Tools |
+|----------|-------|
+| Screenshot | `screenshot`, `zoom` |
+| Mouse | `left_click`, `right_click`, `middle_click`, `double_click`, `triple_click`, `left_click_drag`, `mouse_move`, `left_mouse_down`, `left_mouse_up`, `cursor_position`, `scroll` |
+| Keyboard | `type`, `key`, `hold_key` |
+| Apps | `open_application`, `switch_display` |
+| Permissions | `request_access`, `list_granted_applications` |
+| Clipboard | `read_clipboard`, `write_clipboard` |
+| Other | `wait`, `computer_batch` |
+
+---
+
+## Supported Platforms
+
+| Platform | Architecture | Status | Notes |
+|----------|-------------|--------|-------|
+| macOS | Apple Silicon (M1/M2/M3/M4) | ✅ Fully supported | Recommended |
+| macOS | Intel x86_64 | ✅ Fully supported | |
+| Windows | Any | ⚠️ Theoretically possible | Core libs (`pyautogui` + `mss`) are cross-platform, but `pyobjc` parts (app management) need to be replaced with `win32com`. Not yet adapted |
+| Linux | Any | ⚠️ Theoretically possible | Same as above — `pyobjc` needs to be replaced with `wmctrl` + `xdotool`. Not yet adapted |
+
+### Requirements
+
+- [Bun](https://bun.sh) >= 1.1.0
+- Python >= 3.8 (venv and dependencies are auto-installed on first use)
+- macOS permissions: Accessibility + Screen Recording
+
+---
+
+## How It Works
+
+Computer Use operates through a **screenshot → analyze → act** feedback loop:
+
+```
+┌────────────────────────────────────────────────────┐
+│ AI Model (Claude / any Anthropic-protocol model) │
+│ │
+│ 1. Receives user request: "open Music app" │
+│ 2. Calls screenshot tool → receives screen image │
+│ 3. Model analyzes pixels, identifies UI elements │
+│ → "search box is at (756, 342)" │
+│ 4. Calls left_click { coordinate: [756, 342] } │
+│ 5. Calls type { text: "search query" } │
+│ 6. Calls screenshot again → verify → next step... │
+└───────────────┬────────────────────────────────────┘
+ │ MCP Tool Call
+ ▼
+┌────────────────────────────────────────────────────┐
+│ TypeScript Tool Layer (vendor/computer-use-mcp) │
+│ - Security checks (app allowlist, TCC permissions) │
+│ - Coordinate transformation │
+│ - Tool dispatch → executor │
+└───────────────┬────────────────────────────────────┘
+ │ callPythonHelper()
+ ▼
+┌────────────────────────────────────────────────────┐
+│ Python Bridge (runtime/mac_helper.py) │
+│ pyautogui.click(756, 342) ← mouse control │
+│ mss.grab(monitor) ← screenshot │
+│ NSWorkspace.open(bundleId) ← app management │
+└────────────────────────────────────────────────────┘
+```
+
+**Key**: Coordinate analysis is performed entirely by the model's vision capabilities — it "sees" the screenshot like a human sees a screen, identifying buttons, text fields, and other UI elements directly from pixels.
+
+---
+
+## Quick Start
+
+### 1. Install dependencies
+
+```bash
+bun install
+```
+
+### 2. Ensure Python 3 is available
+
+```bash
+python3 --version # >= 3.8 required
+```
+
+> Python dependencies are **automatically installed** into `.runtime/venv/` on first Computer Use invocation.
+
+### 3. Grant macOS permissions
+
+**Accessibility:**
+
+```bash
+open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
+```
+
+Add your **terminal app** (iTerm, Terminal, Ghostty, etc.) to the allow list.
+
+**Screen Recording:**
+
+```bash
+open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
+```
+
+Add your terminal app as well. You may need to **restart your terminal** after granting permission.
+
+### 4. Start
+
+```bash
+./bin/claude-haha
+```
+
+### 5. Use
+
+Just ask in natural language:
+
+```
+> Take a screenshot of my desktop
+> Open Safari and search for something
+> Type "hello" in the text editor
+```
+
+---
+
+## Security
+
+| Mechanism | Description |
+|-----------|-------------|
+| **App allowlist** | Each session requires explicit authorization for which apps Claude can interact with |
+| **Concurrency lock** | Only one Claude session can use Computer Use at a time (file lock) |
+| **Clipboard guard** | Original clipboard content is saved and restored when typing via clipboard |
+| **Sensitive action gates** | System keyboard shortcuts require additional authorization |
+
+> Note: Since we replaced the native modules with Python bridge, the global Escape hotkey abort and auto-hide features from the original implementation are not available. Use `Ctrl+C` to abort instead.
+
+---
+
+## Environment Variables
+
+| Variable | Default | Description |
+|----------|---------|-------------|
+| `CLAUDE_COMPUTER_USE_ENABLED` | `1` | Set to `0` to disable Computer Use |
+| `CLAUDE_COMPUTER_USE_COORDINATE_MODE` | `pixels` | Coordinate mode: `pixels` or `normalized_0_100` |
+| `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE` | `1` | Enable clipboard-based text input |
+| `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION` | `1` | Enable mouse animation |
+| `CLAUDE_COMPUTER_USE_DEBUG` | `0` | Debug mode |
+
+---
+
+## Technical Architecture
+
+### Gate Bypass
+
+The official Claude Code gates Computer Use behind three layers:
+
+| Layer | Original Mechanism | Our Approach |
+|-------|-------------------|--------------|
+| Compile-time | `feature('CHICAGO_MCP')` (Bun macro) | Replaced with `true` |
+| Subscription | `hasRequiredSubscription()` (Max/Pro only) | `getChicagoEnabled()` returns `true` directly |
+| Remote config | GrowthBook `tengu_malort_pedway` | Same — no remote dependency |
+| Default-disabled | `isDefaultDisabledBuiltin('computer-use')` | Returns `false` |
+
+### Python Bridge
+
+On first invocation, the bridge automatically:
+1. Creates a Python virtual environment (`.runtime/venv/`)
+2. Installs pip
+3. Installs dependencies (`mss`, `Pillow`, `pyautogui`, `pyobjc-*`)
+4. Validates via SHA256 hash (only reinstalls when `requirements.txt` changes)
+
+---
+
+## Approaches We Tried
+
+### Approach 1: Extract native .node modules from Claude Code binary ❌
+
+Extracted `computer-use-swift.node` and `computer-use-input.node` from the installed Claude Code Mach-O binary. Synchronous methods worked, but async Swift methods (screenshot) hung due to N-API async incompatibility between Bun versions.
+
+### Approach 2: Create empty stub packages ❌
+
+Stub packages allowed compilation but provided no actual functionality.
+
+### Approach 3: Python Bridge ✅ (current)
+
+Replaced all native module calls with Python subprocess calls via `callPythonHelper()`. Zero binary dependencies, auto-bootstrapping, full functionality on any macOS.
+
+---
+
+## Known Limitations
+
+| Limitation | Description |
+|------------|-------------|
+| macOS only | Windows/Linux need `pyobjc` replacements |
+| No global Escape abort | Original used CGEventTap; use `Ctrl+C` instead |
+| No auto-hide windows | Original's `prepareDisplay` relied on Swift |
+| Slightly higher latency | ~100ms Python process startup overhead per call |
+
+---
+
+## References and Credits
+
+| Project | License | Contribution |
+|---------|---------|-------------|
+| [wimi321/macos-computer-use-skill](https://github.com/wimi321/macos-computer-use-skill) | MIT | Python bridge architecture, `mac_helper.py` runtime, executor adaptation |
+| [domdomegg/computer-use-mcp](https://github.com/domdomegg/computer-use-mcp) | MIT | Independent Computer Use MCP server (nut.js based), used as reference |
+| [paoloanzn/free-code](https://github.com/paoloanzn/free-code) | - | Feature flag system analysis |
+| [oboard/claude-code-rev](https://github.com/oboard/claude-code-rev) | - | Early leaked source restoration, stub package reference |
+
+### Underlying Libraries
+
+| Library | Purpose |
+|---------|---------|
+| [pyautogui](https://github.com/asweigart/pyautogui) | Mouse and keyboard control |
+| [mss](https://github.com/BoboTiG/python-mss) | Screenshot capture |
+| [Pillow](https://github.com/python-pillow/Pillow) | Image processing and compression |
+| [pyobjc](https://github.com/ronaldoussoren/pyobjc) | macOS Cocoa/Quartz framework bindings |
diff --git a/docs/computer-use.md b/docs/computer-use.md
new file mode 100644
index 00000000..ee43ef90
--- /dev/null
+++ b/docs/computer-use.md
@@ -0,0 +1,312 @@
+# Computer Use 功能指南
+
+中文 | English
+
+> **魔改说明**:本功能是基于 Claude Code 泄露源码中的 Computer Use(内部代号 "Chicago")进行的**深度改造版本**。官方实现依赖 Anthropic 内部私有原生模块(`@ant/computer-use-swift`、`@ant/computer-use-input`),无法公开获取。我们**替换了整个底层操作层**,使用 Python bridge(`pyautogui` + `mss` + `pyobjc`)实现所有系统交互,使得任何人都可以在 macOS 上运行 Computer Use 功能。
+
+---
+
+## 目录
+
+- [功能简介](#功能简介)
+- [支持的设备与平台](#支持的设备与平台)
+- [工作原理](#工作原理)
+- [快速开始](#快速开始)
+- [使用方式](#使用方式)
+- [安全机制](#安全机制)
+- [环境变量](#环境变量)
+- [技术架构详解](#技术架构详解)
+- [我们尝试过的方案](#我们尝试过的方案)
+- [已知限制](#已知限制)
+- [参考项目与致谢](#参考项目与致谢)
+
+---
+
+## 功能简介
+
+Computer Use 让 AI 模型能够**直接控制你的电脑**——截屏、移动鼠标、点击按钮、输入文字、管理应用窗口。
+
+支持的操作(共 24 个 MCP 工具):
+
+| 类别 | 工具 |
+|------|------|
+| 截屏 | `screenshot`、`zoom` |
+| 鼠标 | `left_click`、`right_click`、`middle_click`、`double_click`、`triple_click`、`left_click_drag`、`mouse_move`、`left_mouse_down`、`left_mouse_up`、`cursor_position`、`scroll` |
+| 键盘 | `type`、`key`、`hold_key` |
+| 应用 | `open_application`、`switch_display` |
+| 权限 | `request_access`、`list_granted_applications` |
+| 剪贴板 | `read_clipboard`、`write_clipboard` |
+| 其他 | `wait`、`computer_batch` |
+
+---
+
+## 支持的设备与平台
+
+| 平台 | 芯片 | 状态 | 说明 |
+|------|------|------|------|
+| macOS | Apple Silicon (M1/M2/M3/M4) | ✅ 完整支持 | 推荐平台 |
+| macOS | Intel x86_64 | ✅ 完整支持 | |
+| Windows | 任意 | ⚠️ 理论可行 | `pyautogui` + `mss` 跨平台,但 `pyobjc` 部分(应用管理)需替换为 `win32com`,当前未适配 |
+| Linux | 任意 | ⚠️ 理论可行 | 同上,需替换 `pyobjc` 为 `wmctrl` + `xdotool`,当前未适配 |
+
+### 运行环境要求
+
+- [Bun](https://bun.sh) >= 1.1.0
+- Python >= 3.8(首次运行自动创建 venv 并安装依赖)
+- macOS 系统权限:Accessibility(辅助功能)+ Screen Recording(屏幕录制)
+
+---
+
+## 工作原理
+
+Computer Use 的核心是一个**截图-分析-操作**的闭环:
+
+```
+┌──────────────────────────────────────────────┐
+│ AI 模型(Claude / 其他 Anthropic 协议模型) │
+│ │
+│ 1. 收到用户请求 "打开网易云搜索喜欢你" │
+│ 2. 调用 screenshot 工具 → 收到屏幕截图 │
+│ 3. 模型分析截图像素,识别 UI 元素位置 │
+│ → "搜索框在 (756, 342)" │
+│ 4. 调用 left_click { coordinate: [756, 342] } │
+│ 5. 调用 type { text: "喜欢你" } │
+│ 6. 再次 screenshot → 确认结果 → 下一步... │
+└──────────────┬───────────────────────────────┘
+ │ MCP Tool Call
+ ▼
+┌──────────────────────────────────────────────┐
+│ TypeScript 工具层 │
+│ (vendor/computer-use-mcp) │
+│ │
+│ - 安全检查(应用白名单、TCC 权限) │
+│ - 坐标模式转换(pixels / normalized) │
+│ - 工具分发 → executor │
+└──────────────┬───────────────────────────────┘
+ │ callPythonHelper()
+ ▼
+┌──────────────────────────────────────────────┐
+│ Python Bridge │
+│ (runtime/mac_helper.py) │
+│ │
+│ pyautogui.click(756, 342) ← 鼠标控制 │
+│ mss.grab(monitor) ← 截图 │
+│ NSWorkspace.open(bundleId) ← 应用管理 │
+└──────────────────────────────────────────────┘
+```
+
+**关键:坐标分析完全由模型的视觉能力完成**——模型"看"截图就像人看屏幕一样,直接从像素中识别按钮、输入框等 UI 元素的位置。
+
+---
+
+## 快速开始
+
+### 1. 安装依赖
+
+```bash
+bun install
+```
+
+### 2. 确保 Python 3 可用
+
+```bash
+python3 --version # 需要 >= 3.8
+```
+
+> Python 依赖会在首次使用 Computer Use 时**自动安装**到 `.runtime/venv/`,无需手动操作。
+
+### 3. 授予 macOS 权限
+
+#### Accessibility(辅助功能)
+
+```bash
+open "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility"
+```
+
+将你的**终端应用**(如 iTerm、Terminal、Ghostty 等)添加到允许列表。
+
+#### Screen Recording(屏幕录制)
+
+```bash
+open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"
+```
+
+同样添加你的终端应用。授权后可能需要**重启终端**。
+
+### 4. 启动
+
+```bash
+./bin/claude-haha
+```
+
+### 5. 使用
+
+在对话中用自然语言请求即可:
+
+```
+> 帮我打开网易云音乐,搜索一首歌
+> 截个屏看看当前桌面
+> 帮我在 VS Code 里打开终端
+```
+
+---
+
+## 使用方式
+
+首次使用 Computer Use 时,系统会弹出**应用授权对话框**,你需要选择允许 Claude 操作的应用。
+
+- 模型会先调用 `request_access` 请求权限
+- 你在终端中确认允许哪些应用
+- 之后模型就可以截图、点击、输入了
+
+---
+
+## 安全机制
+
+| 机制 | 说明 |
+|------|------|
+| **应用白名单** | 每次会话需要明确授权允许操作的应用 |
+| **并发保护** | 同一时间只有一个 Claude 会话可使用 Computer Use(文件锁机制) |
+| **剪贴板保护** | 通过剪贴板输入文本时会自动保存和恢复原始剪贴板内容 |
+| **操作确认** | 敏感操作(如系统快捷键)需要额外授权 |
+
+> 注意:由于底层改为 Python bridge,原生方案中的全局 Escape 快捷键中止和操作前自动隐藏应用功能暂不可用。可使用 `Ctrl+C` 中止。
+
+---
+
+## 环境变量
+
+| 变量 | 默认值 | 说明 |
+|------|--------|------|
+| `CLAUDE_COMPUTER_USE_ENABLED` | `1` | 设为 `0` 可禁用 Computer Use |
+| `CLAUDE_COMPUTER_USE_COORDINATE_MODE` | `pixels` | 坐标模式:`pixels` 或 `normalized_0_100` |
+| `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE` | `1` | 是否启用剪贴板粘贴输入 |
+| `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION` | `1` | 是否启用鼠标动画 |
+| `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION` | `0` | 操作前是否隐藏其他窗口 |
+| `CLAUDE_COMPUTER_USE_DEBUG` | `0` | 调试模式 |
+
+---
+
+## 技术架构详解
+
+### 整体分层
+
+```
+src/
+├── vendor/computer-use-mcp/ ← MCP 工具定义与分发(12 个文件)
+│ ├── tools.ts ← 24 个工具的 schema 定义
+│ ├── toolCalls.ts ← 安全检查 + 工具分发
+│ ├── mcpServer.ts ← MCP 服务器创建
+│ ├── types.ts ← 全部类型定义
+│ └── ...
+├── utils/computerUse/
+│ ├── executor.ts ← 执行器(调用 Python bridge)
+│ ├── pythonBridge.ts ← Python 子进程管理
+│ ├── hostAdapter.ts ← 权限检查适配器
+│ ├── gates.ts ← 功能开关(已绕过灰度)
+│ ├── wrapper.tsx ← MCP 工具覆写层
+│ ├── setup.ts ← MCP 配置初始化
+│ └── ...
+└── runtime/
+ ├── mac_helper.py ← Python 实现(659 行)
+ └── requirements.txt ← Python 依赖
+```
+
+### 灰度控制绕过
+
+官方 Claude Code 中 Computer Use 通过三层门控限制访问:
+
+| 层级 | 原始机制 | 我们的处理 |
+|------|----------|-----------|
+| 编译时 | `feature('CHICAGO_MCP')` (Bun 编译宏) | 替换为 `true` |
+| 订阅检查 | `hasRequiredSubscription()` (Max/Pro) | `getChicagoEnabled()` 直接返回 `true` |
+| 远程配置 | GrowthBook `tengu_malort_pedway` | 同上,不再依赖远程配置 |
+| 默认禁用 | `isDefaultDisabledBuiltin('computer-use')` | `isDefaultDisabledBuiltin()` 返回 `false` |
+
+### Python Bridge 工作机制
+
+```typescript
+// pythonBridge.ts
+async function callPythonHelper(command: string, payload: object): Promise {
+ await ensureBootstrapped() // 首次调用自动创建 venv + pip install
+
+ // 调用: python3 runtime/mac_helper.py --payload '{...}'
+ const result = execFile(pythonBin, ['mac_helper.py', command, '--payload', JSON.stringify(payload)])
+
+ return JSON.parse(result.stdout) // { ok: true, result: T }
+}
+```
+
+首次运行自动完成:
+1. 创建 Python 虚拟环境 (`.runtime/venv/`)
+2. 安装 pip
+3. 安装依赖 (`mss`, `Pillow`, `pyautogui`, `pyobjc-*`)
+4. SHA256 哈希验证(仅 requirements.txt 变更时重新安装)
+
+---
+
+## 我们尝试过的方案
+
+### 方案一:从 Claude Code 二进制提取原生 .node 模块 ❌
+
+**思路**:从已安装的 Claude Code 二进制 (`~/.local/share/claude/versions/2.1.91`,189MB Mach-O) 中定位并提取嵌入的原生 NAPI 模块。
+
+**实施**:
+- 成功从 Bun `$bunfs` 虚拟文件系统中提取了 `computer-use-swift.node` (ARM64 424KB + x64 430KB) 和 `computer-use-input.node` (ARM64 836KB + x64 821KB)
+- 同步方法(TCC 权限检查、显示枚举)正常工作
+- 创建了 npm 包装包并通过 workspace 注册
+
+**失败原因**:
+- Swift 异步方法(`screenshot.captureExcluding`)的 continuation 永远不会 resume
+- 根因:提取的 .node 文件是针对 Claude Code 内置的 Bun 运行时编译的,与用户系统的 Bun 版本的 N-API 异步实现不兼容
+- 错误信息:`SWIFT TASK CONTINUATION MISUSE: captureScreenWithExclusion leaked its continuation without resuming it`
+
+### 方案二:创建空 Stub 包 ❌
+
+**思路**:为 `@ant/computer-use-mcp`、`@ant/computer-use-input`、`@ant/computer-use-swift` 创建最小化的 stub 包,使代码能编译加载。
+
+**失败原因**:代码能编译但 MCP 服务器注册后无法执行任何实际操作——截图、点击等全部报错。
+
+### 方案三:Python Bridge 替代原生模块 ✅(当前方案)
+
+**思路**:参考 [wimi321/macos-computer-use-skill](https://github.com/wimi321/macos-computer-use-skill),用 Python 子进程替代所有原生模块调用。
+
+**优势**:
+- 零二进制依赖,不依赖特定 Bun/Node 版本
+- 纯 Python 实现,首次运行自动安装
+- 截图、鼠标、键盘、应用管理全部可用
+- macOS ARM64 + x86_64 均支持
+
+---
+
+## 已知限制
+
+| 限制 | 说明 |
+|------|------|
+| 仅 macOS | Windows/Linux 需要适配 `pyobjc` 部分 |
+| 无全局 Escape 中止 | 原生方案用 CGEventTap 实现,Python 版暂不支持,用 `Ctrl+C` 代替 |
+| 操作前不自动隐藏窗口 | 原生方案的 `prepareDisplay` 依赖 Swift,Python 版未实现 |
+| 性能略低 | Python 进程启动 ~100ms 开销,但模型思考时间通常是秒级,用户感知不到 |
+| 像素验证关闭 | `pixelValidation` 默认关闭 |
+
+---
+
+## 参考项目与致谢
+
+本功能的实现参考了以下开源项目,在此致以感谢:
+
+| 项目 | 许可证 | 贡献 |
+|------|--------|------|
+| [wimi321/macos-computer-use-skill](https://github.com/wimi321/macos-computer-use-skill) | MIT | Python bridge 架构、`mac_helper.py` 运行时、`executor.ts` 适配方案。该项目从 Claude Code 工作流中提取了可复用的 TypeScript 逻辑,并用完全公开的 Python 库替代了私有原生模块 |
+| [domdomegg/computer-use-mcp](https://github.com/domdomegg/computer-use-mcp) | MIT | 独立的 Computer Use MCP 服务器实现(基于 nut.js),跨平台可用。在方案调研阶段提供了参考 |
+| [paoloanzn/free-code](https://github.com/paoloanzn/free-code) | - | Feature flag 系统分析和构建系统参考 |
+| [oboard/claude-code-rev](https://github.com/oboard/claude-code-rev) | - | 泄露源码的早期恢复工作,提供了 stub 包的参考实现 |
+
+### 底层依赖
+
+| 库 | 用途 |
+|----|------|
+| [pyautogui](https://github.com/asweigart/pyautogui) | 鼠标和键盘控制 |
+| [mss](https://github.com/BoboTiG/python-mss) | 屏幕截图 |
+| [Pillow](https://github.com/python-pillow/Pillow) | 图像处理和压缩 |
+| [pyobjc](https://github.com/ronaldoussoren/pyobjc) | macOS Cocoa/Quartz 框架绑定(应用管理、显示枚举) |
diff --git a/runtime/mac_helper.py b/runtime/mac_helper.py
new file mode 100755
index 00000000..20c7a52f
--- /dev/null
+++ b/runtime/mac_helper.py
@@ -0,0 +1,659 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import base64
+import json
+import os
+import subprocess
+import sys
+import time
+from io import BytesIO
+from pathlib import Path
+from typing import Any
+
+import mss
+from AppKit import NSWorkspace, NSPasteboard, NSPasteboardTypeString, NSURL
+from PIL import Image
+from Quartz import (
+ CGDisplayBounds,
+ CGDisplayIsMain,
+ CGDisplayModeGetPixelHeight,
+ CGDisplayModeGetPixelWidth,
+ CGDisplayPixelsHigh,
+ CGDisplayPixelsWide,
+ CGGetActiveDisplayList,
+ CGMainDisplayID,
+ CGWindowListCopyWindowInfo,
+ CGRectContainsPoint,
+ CGRectIntersection,
+ CGPointMake,
+ kCGNullWindowID,
+ kCGWindowBounds,
+ kCGWindowIsOnscreen,
+ kCGWindowLayer,
+ kCGWindowListExcludeDesktopElements,
+ kCGWindowListOptionOnScreenOnly,
+ kCGWindowName,
+ kCGWindowOwnerName,
+)
+
+os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
+os.environ.setdefault("PYAUTOGUI_HIDE_SUPPORT_PROMPT", "1")
+
+import pyautogui # noqa: E402
+
+pyautogui.FAILSAFE = False
+pyautogui.PAUSE = 0
+
+KEY_MAP = {
+ "a": "a",
+ "b": "b",
+ "c": "c",
+ "d": "d",
+ "e": "e",
+ "f": "f",
+ "g": "g",
+ "h": "h",
+ "i": "i",
+ "j": "j",
+ "k": "k",
+ "l": "l",
+ "m": "m",
+ "n": "n",
+ "o": "o",
+ "p": "p",
+ "q": "q",
+ "r": "r",
+ "s": "s",
+ "t": "t",
+ "u": "u",
+ "v": "v",
+ "w": "w",
+ "x": "x",
+ "y": "y",
+ "z": "z",
+ "0": "0",
+ "1": "1",
+ "2": "2",
+ "3": "3",
+ "4": "4",
+ "5": "5",
+ "6": "6",
+ "7": "7",
+ "8": "8",
+ "9": "9",
+ "cmd": "command",
+ "command": "command",
+ "meta": "command",
+ "super": "command",
+ "ctrl": "ctrl",
+ "control": "ctrl",
+ "shift": "shift",
+ "alt": "option",
+ "option": "option",
+ "opt": "option",
+ "fn": "fn",
+ "escape": "esc",
+ "esc": "esc",
+ "enter": "enter",
+ "return": "enter",
+ "tab": "tab",
+ "space": "space",
+ "backspace": "backspace",
+ "delete": "delete",
+ "forwarddelete": "delete",
+ "up": "up",
+ "down": "down",
+ "left": "left",
+ "right": "right",
+ "home": "home",
+ "end": "end",
+ "pageup": "pageup",
+ "pagedown": "pagedown",
+ "capslock": "capslock",
+ "f1": "f1",
+ "f2": "f2",
+ "f3": "f3",
+ "f4": "f4",
+ "f5": "f5",
+ "f6": "f6",
+ "f7": "f7",
+ "f8": "f8",
+ "f9": "f9",
+ "f10": "f10",
+ "f11": "f11",
+ "f12": "f12",
+ "-": "minus",
+ "=": "equals",
+ "[": "[",
+ "]": "]",
+ "\\": "\\",
+ ";": ";",
+ "'": "'",
+ ",": ",",
+ ".": ".",
+ "/": "/",
+ "`": "`",
+}
+
+
+def normalize_key(name: str) -> str:
+ key = name.strip().lower()
+ if key not in KEY_MAP:
+ raise ValueError(f"Unsupported key: {name}")
+ return KEY_MAP[key]
+
+
+def json_output(payload: dict[str, Any]) -> None:
+ sys.stdout.write(json.dumps(payload, ensure_ascii=False))
+ sys.stdout.write("\n")
+ sys.stdout.flush()
+
+
+def error_output(message: str, code: str = "runtime_error") -> None:
+ json_output({"ok": False, "error": {"code": code, "message": message}})
+
+
+def bool_env(name: str, default: bool = False) -> bool:
+ value = os.environ.get(name)
+ if value is None:
+ return default
+ return value not in {"0", "false", "False", ""}
+
+
+def run_osascript(script: str) -> str:
+ result = subprocess.run(
+ ["osascript", "-e", script],
+ text=True,
+ capture_output=True,
+ check=False,
+ )
+ if result.returncode != 0:
+ raise RuntimeError(result.stderr.strip() or result.stdout.strip() or "osascript failed")
+ return result.stdout.strip()
+
+
+def get_displays() -> list[dict[str, Any]]:
+ max_displays = 32
+ err, active, count = CGGetActiveDisplayList(max_displays, None, None)
+ if err != 0:
+ raise RuntimeError(f"CGGetActiveDisplayList failed: {err}")
+ displays: list[dict[str, Any]] = []
+ main_id = CGMainDisplayID()
+ for idx, display_id in enumerate(active[:count]):
+ bounds = CGDisplayBounds(display_id)
+ mode = None
+ try:
+ from Quartz import CGDisplayCopyDisplayMode
+ mode = CGDisplayCopyDisplayMode(display_id)
+ except Exception:
+ mode = None
+ physical_width = int(CGDisplayPixelsWide(display_id))
+ physical_height = int(CGDisplayPixelsHigh(display_id))
+ logical_width = int(bounds.size.width)
+ logical_height = int(bounds.size.height)
+ if mode is not None:
+ mode_w = int(CGDisplayModeGetPixelWidth(mode))
+ mode_h = int(CGDisplayModeGetPixelHeight(mode))
+ physical_width = mode_w or physical_width
+ physical_height = mode_h or physical_height
+ scale_factor = physical_width / logical_width if logical_width else 1
+ name = f"Display {idx + 1}"
+ displays.append(
+ {
+ "id": int(display_id),
+ "displayId": int(display_id),
+ "width": logical_width,
+ "height": logical_height,
+ "scaleFactor": scale_factor,
+ "originX": int(bounds.origin.x),
+ "originY": int(bounds.origin.y),
+ "isPrimary": bool(display_id == main_id or CGDisplayIsMain(display_id)),
+ "name": name,
+ "label": name,
+ }
+ )
+ return displays
+
+
+def choose_display(display_id: int | None) -> dict[str, Any]:
+ displays = get_displays()
+ if not displays:
+ raise RuntimeError("No active displays found")
+ if display_id is None:
+ for display in displays:
+ if display["isPrimary"]:
+ return display
+ return displays[0]
+ for display in displays:
+ if display["displayId"] == display_id or display["id"] == display_id:
+ return display
+ raise RuntimeError(f"Unknown display: {display_id}")
+
+
+def capture_display(display_id: int | None, resize: tuple[int, int] | None = None) -> dict[str, Any]:
+ display = choose_display(display_id)
+ monitor = {
+ "left": display["originX"],
+ "top": display["originY"],
+ "width": display["width"],
+ "height": display["height"],
+ }
+ with mss.mss() as sct:
+ raw = sct.grab(monitor)
+ image = Image.frombytes("RGB", raw.size, raw.rgb)
+ if resize:
+ image = image.resize(resize, Image.Resampling.LANCZOS)
+ buffer = BytesIO()
+ image.save(buffer, format="JPEG", quality=75, optimize=True)
+ base64_data = base64.b64encode(buffer.getvalue()).decode("ascii")
+ return {
+ "base64": base64_data,
+ "width": image.width,
+ "height": image.height,
+ "displayWidth": display["width"],
+ "displayHeight": display["height"],
+ "displayId": display["displayId"],
+ "originX": display["originX"],
+ "originY": display["originY"],
+ "display": display,
+ }
+
+
+def capture_region(region: dict[str, int], resize: tuple[int, int] | None = None) -> dict[str, Any]:
+ with mss.mss() as sct:
+ raw = sct.grab(region)
+ image = Image.frombytes("RGB", raw.size, raw.rgb)
+ if resize:
+ image = image.resize(resize, Image.Resampling.LANCZOS)
+ buffer = BytesIO()
+ image.save(buffer, format="JPEG", quality=75, optimize=True)
+ base64_data = base64.b64encode(buffer.getvalue()).decode("ascii")
+ return {"base64": base64_data, "width": image.width, "height": image.height}
+
+
+def list_windows() -> list[dict[str, Any]]:
+ windows = CGWindowListCopyWindowInfo(
+ kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
+ kCGNullWindowID,
+ )
+ out: list[dict[str, Any]] = []
+ for window in windows or []:
+ if int(window.get(kCGWindowLayer, 0)) != 0:
+ continue
+ if not bool(window.get(kCGWindowIsOnscreen, True)):
+ continue
+ bounds = window.get(kCGWindowBounds) or {}
+ width = int(bounds.get("Width", 0))
+ height = int(bounds.get("Height", 0))
+ if width <= 1 or height <= 1:
+ continue
+ out.append(
+ {
+ "ownerName": window.get(kCGWindowOwnerName, "") or "",
+ "title": window.get(kCGWindowName, "") or "",
+ "bounds": {
+ "x": int(bounds.get("X", 0)),
+ "y": int(bounds.get("Y", 0)),
+ "width": width,
+ "height": height,
+ },
+ }
+ )
+ return out
+
+
+def bundle_id_to_app(bundle_id: str):
+ return NSWorkspace.sharedWorkspace().URLForApplicationWithBundleIdentifier_(bundle_id)
+
+
+def installed_apps() -> list[dict[str, Any]]:
+ search_roots = [
+ Path("/Applications"),
+ Path.home() / "Applications",
+ Path("/System/Applications"),
+ Path("/System/Applications/Utilities"),
+ ]
+ results: dict[str, dict[str, Any]] = {}
+ workspace = NSWorkspace.sharedWorkspace()
+ for root in search_roots:
+ if not root.exists():
+ continue
+ for app in root.rglob("*.app"):
+ try:
+ bundle = workspace.bundleIdentifierForURL_(NSURL.fileURLWithPath_(str(app)))
+ except Exception:
+ bundle = None
+ if not bundle:
+ try:
+ url = workspace.URLForApplicationWithBundleIdentifier_(str(app))
+ bundle = workspace.bundleIdentifierForURL_(url) if url else None
+ except Exception:
+ bundle = None
+ info_plist = app / "Contents/Info.plist"
+ display_name = app.stem
+ if info_plist.exists():
+ try:
+ import plistlib
+ with info_plist.open("rb") as f:
+ plist = plistlib.load(f)
+ bundle = bundle or plist.get("CFBundleIdentifier")
+ display_name = plist.get("CFBundleDisplayName") or plist.get("CFBundleName") or display_name
+ except Exception:
+ pass
+ if not bundle or bundle in results:
+ continue
+ results[bundle] = {
+ "bundleId": str(bundle),
+ "displayName": str(display_name),
+ "path": str(app),
+ }
+ return sorted(results.values(), key=lambda item: item["displayName"].lower())
+
+
+def running_apps() -> list[dict[str, Any]]:
+ apps = []
+ seen = set()
+ for app in NSWorkspace.sharedWorkspace().runningApplications() or []:
+ bundle_id = app.bundleIdentifier()
+ if not bundle_id or bundle_id in seen:
+ continue
+ seen.add(bundle_id)
+ name = app.localizedName() or bundle_id
+ apps.append({"bundleId": str(bundle_id), "displayName": str(name)})
+ return sorted(apps, key=lambda item: item["displayName"].lower())
+
+
+def app_display_name(bundle_id: str) -> str | None:
+ for app in NSWorkspace.sharedWorkspace().runningApplications() or []:
+ if app.bundleIdentifier() == bundle_id:
+ return str(app.localizedName() or bundle_id)
+ for app in installed_apps():
+ if app["bundleId"] == bundle_id:
+ return str(app["displayName"])
+ return None
+
+
+def frontmost_app() -> dict[str, str] | None:
+ app = NSWorkspace.sharedWorkspace().frontmostApplication()
+ if not app:
+ return None
+ bundle_id = app.bundleIdentifier()
+ if not bundle_id:
+ return None
+ return {
+ "bundleId": str(bundle_id),
+ "displayName": str(app.localizedName() or bundle_id),
+ }
+
+
+def app_under_point(x: int, y: int) -> dict[str, str] | None:
+ point = CGPointMake(x, y)
+ running_by_name = {
+ str(app.localizedName() or app.bundleIdentifier()): str(app.bundleIdentifier())
+ for app in NSWorkspace.sharedWorkspace().runningApplications() or []
+ if app.bundleIdentifier()
+ }
+ for window in list_windows():
+ bounds = window["bounds"]
+ rect = ((bounds["x"], bounds["y"]), (bounds["width"], bounds["height"]))
+ if CGRectContainsPoint(rect, point):
+ owner = window["ownerName"]
+ bundle = running_by_name.get(owner)
+ if bundle:
+ return {"bundleId": bundle, "displayName": str(owner)}
+ return frontmost_app()
+
+
+def find_window_displays(bundle_ids: list[str]) -> list[dict[str, Any]]:
+ if not bundle_ids:
+ return []
+ displays = get_displays()
+ names_by_bundle = {
+ bundle_id: app_display_name(bundle_id) or bundle_id for bundle_id in bundle_ids
+ }
+ windows = list_windows()
+ result = []
+ for bundle_id in bundle_ids:
+ target_name = names_by_bundle.get(bundle_id)
+ display_ids: set[int] = set()
+ for window in windows:
+ owner = window["ownerName"]
+ if not owner:
+ continue
+ if target_name and owner != target_name:
+ continue
+ if not target_name and owner != bundle_id:
+ continue
+ wx = window["bounds"]["x"]
+ wy = window["bounds"]["y"]
+ ww = window["bounds"]["width"]
+ wh = window["bounds"]["height"]
+ window_rect = ((wx, wy), (ww, wh))
+ for display in displays:
+ display_rect = ((display["originX"], display["originY"]), (display["width"], display["height"]))
+ intersection = CGRectIntersection(window_rect, display_rect)
+ if intersection.size.width > 0 and intersection.size.height > 0:
+ display_ids.add(int(display["displayId"]))
+ result.append({"bundleId": bundle_id, "displayIds": sorted(display_ids)})
+ return result
+
+
+def open_app(bundle_id: str) -> None:
+ url = bundle_id_to_app(bundle_id)
+ if not url:
+ raise RuntimeError(f"App not found for bundle identifier: {bundle_id}")
+ ok, err = NSWorkspace.sharedWorkspace().launchApplicationAtURL_options_configuration_error_(url, 0, {}, None)
+ if not ok:
+ raise RuntimeError(str(err) if err else f"Failed to open app {bundle_id}")
+
+
+def read_clipboard() -> str:
+ pb = NSPasteboard.generalPasteboard()
+ value = pb.stringForType_(NSPasteboardTypeString)
+ return "" if value is None else str(value)
+
+
+def write_clipboard(text: str) -> None:
+ pb = NSPasteboard.generalPasteboard()
+ pb.clearContents()
+ pb.setString_forType_(text, NSPasteboardTypeString)
+
+
+def check_permissions() -> dict[str, bool]:
+ accessibility = True
+ try:
+ run_osascript('tell application "System Events" to get name of first process')
+ except Exception:
+ accessibility = False
+ screen_recording = True
+ try:
+ capture_display(None)
+ except Exception:
+ screen_recording = False
+ return {
+ "accessibility": accessibility,
+ "screenRecording": screen_recording,
+ }
+
+
+def click(x: int, y: int, button: str, count: int, modifiers: list[str] | None) -> None:
+ pyautogui.moveTo(x, y)
+ if modifiers:
+ normalized = [normalize_key(m) for m in modifiers]
+ for key in normalized:
+ pyautogui.keyDown(key)
+ try:
+ pyautogui.click(x=x, y=y, button=button, clicks=count, interval=0.08)
+ finally:
+ for key in reversed(normalized):
+ pyautogui.keyUp(key)
+ else:
+ pyautogui.click(x=x, y=y, button=button, clicks=count, interval=0.08)
+
+
+def scroll(x: int, y: int, delta_x: int, delta_y: int) -> None:
+ pyautogui.moveTo(x, y)
+ if delta_y:
+ pyautogui.scroll(int(delta_y), x=x, y=y)
+ if delta_x:
+ pyautogui.hscroll(int(delta_x), x=x, y=y)
+
+
+def key_action(sequence: str, repeat: int = 1) -> None:
+ parts = [normalize_key(part) for part in sequence.split("+") if part.strip()]
+ for _ in range(max(1, repeat)):
+ if len(parts) == 1:
+ pyautogui.press(parts[0])
+ else:
+ pyautogui.hotkey(*parts, interval=0.02)
+ time.sleep(0.01)
+
+
+def hold_keys(keys: list[str], duration_ms: int) -> None:
+ normalized = [normalize_key(k) for k in keys]
+ for key in normalized:
+ pyautogui.keyDown(key)
+ try:
+ time.sleep(max(duration_ms, 0) / 1000)
+ finally:
+ for key in reversed(normalized):
+ pyautogui.keyUp(key)
+
+
+def type_text(text: str) -> None:
+ pyautogui.write(text, interval=0.008)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument("command")
+ parser.add_argument("--payload", default="{}")
+ args = parser.parse_args()
+ payload = json.loads(args.payload)
+
+ try:
+ command = args.command
+ if command == "check_permissions":
+ perms = check_permissions()
+ json_output({"ok": True, "result": perms})
+ return 0
+ if command == "list_displays":
+ json_output({"ok": True, "result": get_displays()})
+ return 0
+ if command == "get_display_size":
+ json_output({"ok": True, "result": choose_display(payload.get("displayId"))})
+ return 0
+ if command == "screenshot":
+ resize = None
+ if payload.get("targetWidth") and payload.get("targetHeight"):
+ resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
+ result = capture_display(payload.get("displayId"), resize)
+ json_output({"ok": True, "result": result})
+ return 0
+ if command == "resolve_prepare_capture":
+ resize = None
+ if payload.get("targetWidth") and payload.get("targetHeight"):
+ resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
+ result = capture_display(payload.get("preferredDisplayId"), resize)
+ result["hidden"] = []
+ result["resolvedDisplayId"] = result["displayId"]
+ json_output({"ok": True, "result": result})
+ return 0
+ if command == "zoom":
+ resize = None
+ if payload.get("targetWidth") and payload.get("targetHeight"):
+ resize = (int(payload["targetWidth"]), int(payload["targetHeight"]))
+ region = {
+ "left": int(payload["x"]),
+ "top": int(payload["y"]),
+ "width": int(payload["width"]),
+ "height": int(payload["height"]),
+ }
+ json_output({"ok": True, "result": capture_region(region, resize)})
+ return 0
+ if command == "prepare_for_action":
+ json_output({"ok": True, "result": []})
+ return 0
+ if command == "preview_hide_set":
+ json_output({"ok": True, "result": []})
+ return 0
+ if command == "find_window_displays":
+ json_output({"ok": True, "result": find_window_displays(list(payload.get("bundleIds") or []))})
+ return 0
+ if command == "key":
+ key_action(str(payload["keySequence"]), int(payload.get("repeat") or 1))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "hold_key":
+ hold_keys(list(payload.get("keyNames") or []), int(payload.get("durationMs") or 0))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "type":
+ type_text(str(payload.get("text") or ""))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "click":
+ click(int(payload["x"]), int(payload["y"]), str(payload.get("button") or "left"), int(payload.get("count") or 1), payload.get("modifiers"))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "drag":
+ from_point = payload.get("from")
+ if from_point:
+ pyautogui.moveTo(int(from_point["x"]), int(from_point["y"]))
+ pyautogui.dragTo(int(payload["to"]["x"]), int(payload["to"]["y"]), duration=0.2, button="left")
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "move_mouse":
+ pyautogui.moveTo(int(payload["x"]), int(payload["y"]))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "scroll":
+ scroll(int(payload["x"]), int(payload["y"]), int(payload.get("deltaX") or 0), int(payload.get("deltaY") or 0))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "mouse_down":
+ pyautogui.mouseDown(button="left")
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "mouse_up":
+ pyautogui.mouseUp(button="left")
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "cursor_position":
+ x, y = pyautogui.position()
+ json_output({"ok": True, "result": {"x": int(x), "y": int(y)}})
+ return 0
+ if command == "frontmost_app":
+ json_output({"ok": True, "result": frontmost_app()})
+ return 0
+ if command == "app_under_point":
+ json_output({"ok": True, "result": app_under_point(int(payload["x"]), int(payload["y"]))})
+ return 0
+ if command == "list_installed_apps":
+ json_output({"ok": True, "result": installed_apps()})
+ return 0
+ if command == "list_running_apps":
+ json_output({"ok": True, "result": running_apps()})
+ return 0
+ if command == "open_app":
+ open_app(str(payload["bundleId"]))
+ json_output({"ok": True, "result": True})
+ return 0
+ if command == "read_clipboard":
+ json_output({"ok": True, "result": read_clipboard()})
+ return 0
+ if command == "write_clipboard":
+ write_clipboard(str(payload.get("text") or ""))
+ json_output({"ok": True, "result": True})
+ return 0
+ error_output(f"Unknown command: {command}", code="bad_command")
+ return 2
+ except Exception as exc:
+ error_output(str(exc))
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/runtime/requirements.txt b/runtime/requirements.txt
new file mode 100644
index 00000000..06466fae
--- /dev/null
+++ b/runtime/requirements.txt
@@ -0,0 +1,6 @@
+mss>=10.1.0
+Pillow>=11.3.0
+pyautogui>=0.9.54
+pyobjc-core>=11.1
+pyobjc-framework-Cocoa>=11.1
+pyobjc-framework-Quartz>=11.1
diff --git a/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx b/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx
index 9d85595d..d260fe61 100644
--- a/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx
+++ b/src/components/permissions/ComputerUseApproval/ComputerUseApproval.tsx
@@ -1,7 +1,7 @@
import { c as _c } from "react/compiler-runtime";
-import { getSentinelCategory } from '@ant/computer-use-mcp/sentinelApps';
-import type { CuPermissionRequest, CuPermissionResponse } from '@ant/computer-use-mcp/types';
-import { DEFAULT_GRANT_FLAGS } from '@ant/computer-use-mcp/types';
+import { getSentinelCategory } from '../../../vendor/computer-use-mcp/sentinelApps.js';
+import type { CuPermissionRequest, CuPermissionResponse } from '../../../vendor/computer-use-mcp/types.js';
+import { DEFAULT_GRANT_FLAGS } from '../../../vendor/computer-use-mcp/types.js';
import figures from 'figures';
import * as React from 'react';
import { useMemo, useState } from 'react';
diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx
index 67326f89..e633fd96 100644
--- a/src/entrypoints/cli.tsx
+++ b/src/entrypoints/cli.tsx
@@ -83,7 +83,7 @@ async function main(): Promise {
} = await import('../utils/claudeInChrome/chromeNativeHost.js');
await runChromeNativeHost();
return;
- } else if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') {
+ } else if (process.argv[2] === '--computer-use-mcp') {
profileCheckpoint('cli_computer_use_mcp_path');
const {
runComputerUseMcpServer
diff --git a/src/main.tsx b/src/main.tsx
index 3668c328..aa88a150 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1474,7 +1474,7 @@ async function run(): Promise {
let reservedNameError: string | null = null;
if (nonSdkConfigNames.some(isClaudeInChromeMCPServer)) {
reservedNameError = `Invalid MCP configuration: "${CLAUDE_IN_CHROME_MCP_SERVER_NAME}" is a reserved MCP name.`;
- } else if (feature('CHICAGO_MCP')) {
+ } else {
const {
isComputerUseMCPServer,
COMPUTER_USE_MCP_SERVER_NAME
@@ -1605,7 +1605,7 @@ async function run(): Promise {
// `type: 'stdio'`. An enterprise-config ant with the GB gate on would
// otherwise process.exit(1). Chrome has the same latent issue but has
// shipped without incident; chicago places itself correctly.
- if (feature('CHICAGO_MCP') && getPlatform() === 'macos' && !getIsNonInteractiveSession()) {
+ if (getPlatform() === 'macos' && !getIsNonInteractiveSession()) {
try {
const {
getChicagoEnabled
diff --git a/src/query.ts b/src/query.ts
index 07e8b6fa..6491c1a1 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -1030,7 +1030,7 @@ async function* queryLoop(
// chicago MCP: auto-unhide + lock release on interrupt. Same cleanup
// as the natural turn-end path in stopHooks.ts. Main thread only —
// see stopHooks.ts for the subagent-releasing-main's-lock rationale.
- if (feature('CHICAGO_MCP') && !toolUseContext.agentId) {
+ if (!toolUseContext.agentId) {
try {
const { cleanupComputerUseAfterTurn } = await import(
'./utils/computerUse/cleanup.js'
@@ -1486,7 +1486,7 @@ async function* queryLoop(
// chicago MCP: auto-unhide + lock release when aborted mid-tool-call.
// This is the most likely Ctrl+C path for CU (e.g. slow screenshot).
// Main thread only — see stopHooks.ts for the subagent rationale.
- if (feature('CHICAGO_MCP') && !toolUseContext.agentId) {
+ if (!toolUseContext.agentId) {
try {
const { cleanupComputerUseAfterTurn } = await import(
'./utils/computerUse/cleanup.js'
diff --git a/src/query/stopHooks.ts b/src/query/stopHooks.ts
index 1118086c..6105485d 100644
--- a/src/query/stopHooks.ts
+++ b/src/query/stopHooks.ts
@@ -161,7 +161,7 @@ export async function* handleStopHooks(
// so a subagent's stopHooks releasing it leaves the main thread's cleanup
// seeing isLockHeldLocally()===false → no exit notification, and unhides
// mid-turn. Subagents don't start CU sessions so this is a pure skip.
- if (feature('CHICAGO_MCP') && !toolUseContext.agentId) {
+ if (!toolUseContext.agentId) {
try {
const { cleanupComputerUseAfterTurn } = await import(
'../utils/computerUse/cleanup.js'
diff --git a/src/services/analytics/metadata.ts b/src/services/analytics/metadata.ts
index b83e96aa..ef58f708 100644
--- a/src/services/analytics/metadata.ts
+++ b/src/services/analytics/metadata.ts
@@ -126,15 +126,11 @@ export function isAnalyticsToolDetailsLoggingEnabled(
* a user-configured 'computer-use' is possible in builds without the feature.
*/
/* eslint-disable @typescript-eslint/no-require-imports */
-const BUILTIN_MCP_SERVER_NAMES: ReadonlySet = new Set(
- feature('CHICAGO_MCP')
- ? [
- (
- require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
- ).COMPUTER_USE_MCP_SERVER_NAME,
- ]
- : [],
-)
+const BUILTIN_MCP_SERVER_NAMES: ReadonlySet = new Set([
+ (
+ require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
+ ).COMPUTER_USE_MCP_SERVER_NAME,
+])
/* eslint-enable @typescript-eslint/no-require-imports */
/**
diff --git a/src/services/mcp/client.ts b/src/services/mcp/client.ts
index 0b3afc6a..e7d6b6ab 100644
--- a/src/services/mcp/client.ts
+++ b/src/services/mcp/client.ts
@@ -238,15 +238,12 @@ const claudeInChromeToolRendering =
// Lazy: wrapper.tsx → hostAdapter.ts → executor.ts pulls both native modules
// (@ant/computer-use-input + @ant/computer-use-swift). Runtime-gated by
// GrowthBook tengu_malort_pedway (see gates.ts).
-const computerUseWrapper = feature('CHICAGO_MCP')
- ? (): typeof import('../../utils/computerUse/wrapper.js') =>
- require('../../utils/computerUse/wrapper.js')
- : undefined
-const isComputerUseMCPServer = feature('CHICAGO_MCP')
- ? (
- require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
- ).isComputerUseMCPServer
- : undefined
+const computerUseWrapper =
+ (): typeof import('../../utils/computerUse/wrapper.js') =>
+ require('../../utils/computerUse/wrapper.js')
+const isComputerUseMCPServer = (
+ require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
+).isComputerUseMCPServer
import { mkdir, readFile, unlink, writeFile } from 'fs/promises'
import { dirname, join } from 'path'
@@ -923,9 +920,8 @@ export const connectToServer = memoize(
transport = clientTransport
logMCPDebug(name, `In-process Chrome MCP server started`)
} else if (
- feature('CHICAGO_MCP') &&
(serverRef.type === 'stdio' || !serverRef.type) &&
- isComputerUseMCPServer!(name)
+ isComputerUseMCPServer(name)
) {
// Run the Computer Use MCP server in-process — same rationale as
// Chrome above. The package's CallTool handler is a stub; real
@@ -1980,10 +1976,9 @@ export const fetchToolsForClient = memoizeWithLRU(
tool.name,
)
: {}),
- ...(feature('CHICAGO_MCP') &&
- (client.config.type === 'stdio' || !client.config.type) &&
- isComputerUseMCPServer!(client.name)
- ? computerUseWrapper!().getComputerUseMCPToolOverrides(tool.name)
+ ...((client.config.type === 'stdio' || !client.config.type) &&
+ isComputerUseMCPServer(client.name)
+ ? computerUseWrapper().getComputerUseMCPToolOverrides(tool.name)
: {}),
}
})
diff --git a/src/services/mcp/config.ts b/src/services/mcp/config.ts
index c5cee9cd..4b4c1edd 100644
--- a/src/services/mcp/config.ts
+++ b/src/services/mcp/config.ts
@@ -638,13 +638,11 @@ export async function addMcpConfig(
throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
- if (feature('CHICAGO_MCP')) {
- const { isComputerUseMCPServer } = await import(
- '../../utils/computerUse/common.js'
- )
- if (isComputerUseMCPServer(name)) {
- throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
- }
+ const { isComputerUseMCPServer } = await import(
+ '../../utils/computerUse/common.js'
+ )
+ if (isComputerUseMCPServer(name)) {
+ throw new Error(`Cannot add MCP server "${name}": this name is reserved.`)
}
// Block adding servers when enterprise MCP config exists (it has exclusive control)
@@ -1509,15 +1507,13 @@ export function areMcpConfigsAllowedWithEnterpriseMcpConfig(
* enabledMcpServers. Shows up in /mcp as disabled until the user enables it.
*/
/* eslint-disable @typescript-eslint/no-require-imports */
-const DEFAULT_DISABLED_BUILTIN = feature('CHICAGO_MCP')
- ? (
- require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
- ).COMPUTER_USE_MCP_SERVER_NAME
- : null
+const DEFAULT_DISABLED_BUILTIN = (
+ require('../../utils/computerUse/common.js') as typeof import('../../utils/computerUse/common.js')
+).COMPUTER_USE_MCP_SERVER_NAME
/* eslint-enable @typescript-eslint/no-require-imports */
-function isDefaultDisabledBuiltin(name: string): boolean {
- return DEFAULT_DISABLED_BUILTIN !== null && name === DEFAULT_DISABLED_BUILTIN
+function isDefaultDisabledBuiltin(_name: string): boolean {
+ return false // Computer Use 默认启用,无需用户手动 enable
}
/**
diff --git a/src/utils/computerUse/drainRunLoop.ts b/src/utils/computerUse/drainRunLoop.ts
index e5df3caa..5aea8913 100644
--- a/src/utils/computerUse/drainRunLoop.ts
+++ b/src/utils/computerUse/drainRunLoop.ts
@@ -1,79 +1,11 @@
-import { logForDebugging } from '../debug.js'
-import { withResolvers } from '../withResolvers.js'
-import { requireComputerUseSwift } from './swiftLoader.js'
-
/**
- * Shared CFRunLoop pump. Swift's four `@MainActor` async methods
- * (captureExcluding, captureRegion, apps.listInstalled, resolvePrepareCapture)
- * and `@ant/computer-use-input`'s key()/keys() all dispatch to
- * DispatchQueue.main. Under libuv (Node/bun) that queue never drains — the
- * promises hang. Electron drains it via CFRunLoop so Cowork doesn't need this.
- *
- * One refcounted setInterval calls `_drainMainRunLoop` (RunLoop.main.run)
- * every 1ms while any main-queue-dependent call is pending. Multiple
- * concurrent drainRunLoop() calls share the single pump via retain/release.
- */
-
-let pump: ReturnType | undefined
-let pending = 0
-
-function drainTick(cu: ReturnType): void {
- cu._drainMainRunLoop()
-}
-
-function retain(): void {
- pending++
- if (pump === undefined) {
- pump = setInterval(drainTick, 1, requireComputerUseSwift())
- logForDebugging('[drainRunLoop] pump started', { level: 'verbose' })
- }
-}
-
-function release(): void {
- pending--
- if (pending <= 0 && pump !== undefined) {
- clearInterval(pump)
- pump = undefined
- logForDebugging('[drainRunLoop] pump stopped', { level: 'verbose' })
- pending = 0
- }
-}
-
-const TIMEOUT_MS = 30_000
-
-function timeoutReject(reject: (e: Error) => void): void {
- reject(new Error(`computer-use native call exceeded ${TIMEOUT_MS}ms`))
-}
-
-/**
- * Hold a pump reference for the lifetime of a long-lived registration
- * (e.g. the CGEventTap Escape handler). Unlike `drainRunLoop(fn)` this has
- * no timeout — the caller is responsible for calling `releasePump()`. Same
- * refcount as drainRunLoop calls, so nesting is safe.
- */
-export const retainPump = retain
-export const releasePump = release
-
-/**
- * Await `fn()` with the shared drain pump running. Safe to nest — multiple
- * concurrent drainRunLoop() calls share one setInterval.
+ * No-op replacement. The Python bridge is a synchronous subprocess call —
+ * there is no CFRunLoop to pump. All former callers can just await their
+ * promises directly.
*/
export async function drainRunLoop(fn: () => Promise): Promise {
- retain()
- let timer: ReturnType | undefined
- try {
- // If the timeout wins the race, fn()'s promise is orphaned — a late
- // rejection from the native layer would become an unhandledRejection.
- // Attaching a no-op catch swallows it; the timeout error is what surfaces.
- // fn() sits inside try so a synchronous throw (e.g. NAPI argument
- // validation) still reaches release() — otherwise the pump leaks.
- const work = fn()
- work.catch(() => {})
- const timeout = withResolvers()
- timer = setTimeout(timeoutReject, TIMEOUT_MS, timeout.reject)
- return await Promise.race([work, timeout.promise])
- } finally {
- clearTimeout(timer)
- release()
- }
+ return fn()
}
+
+export const retainPump = () => {}
+export const releasePump = () => {}
diff --git a/src/utils/computerUse/escHotkey.ts b/src/utils/computerUse/escHotkey.ts
index 9aa882a9..5dae040f 100644
--- a/src/utils/computerUse/escHotkey.ts
+++ b/src/utils/computerUse/escHotkey.ts
@@ -1,54 +1,12 @@
-import { logForDebugging } from '../debug.js'
-import { releasePump, retainPump } from './drainRunLoop.js'
-import { requireComputerUseSwift } from './swiftLoader.js'
-
/**
- * Global Escape → abort. Mirrors Cowork's `escAbort.ts` but without Electron:
- * CGEventTap via `@ant/computer-use-swift`. While registered, Escape is
- * consumed system-wide (PI defense — a prompt-injected action can't dismiss
- * a dialog with Escape).
- *
- * Lifecycle: register on fresh lock acquire (`wrapper.tsx` `acquireCuLock`),
- * unregister on lock release (`cleanup.ts`). The tap's CFRunLoopSource sits
- * in .defaultMode on CFRunLoopGetMain(), so we hold a drainRunLoop pump
- * retain for the registration's lifetime — same refcounted setInterval as
- * the `@MainActor` methods.
- *
- * `notifyExpectedEscape()` punches a hole for model-synthesized Escapes: the
- * executor's `key("escape")` calls it before posting the CGEvent. Swift
- * schedules a 100ms decay so a CGEvent that never reaches the tap callback
- * doesn't eat the next user ESC.
+ * No-op replacement. The Python bridge does not support global Escape hotkey
+ * via CGEventTap. The original implementation required native Swift module
+ * for CGEventTap-based system-wide key interception.
*/
-
-let registered = false
-
-export function registerEscHotkey(onEscape: () => void): boolean {
- if (registered) return true
- const cu = requireComputerUseSwift()
- if (!cu.hotkey.registerEscape(onEscape)) {
- // CGEvent.tapCreate failed — typically missing Accessibility permission.
- // CU still works, just without ESC abort. Mirrors Cowork's escAbort.ts:81.
- logForDebugging('[cu-esc] registerEscape returned false', { level: 'warn' })
- return false
- }
- retainPump()
- registered = true
- logForDebugging('[cu-esc] registered')
- return true
+export function registerEscHotkey(_onEscape: () => void): boolean {
+ return false
}
-export function unregisterEscHotkey(): void {
- if (!registered) return
- try {
- requireComputerUseSwift().hotkey.unregister()
- } finally {
- releasePump()
- registered = false
- logForDebugging('[cu-esc] unregistered')
- }
-}
+export function unregisterEscHotkey(): void {}
-export function notifyExpectedEscape(): void {
- if (!registered) return
- requireComputerUseSwift().hotkey.notifyExpectedEscape()
-}
+export function notifyExpectedEscape(): void {}
diff --git a/src/utils/computerUse/executor.ts b/src/utils/computerUse/executor.ts
index 6e221941..7f6b4f59 100644
--- a/src/utils/computerUse/executor.ts
+++ b/src/utils/computerUse/executor.ts
@@ -1,30 +1,8 @@
/**
- * CLI `ComputerExecutor` implementation. Wraps two native modules:
- * - `@ant/computer-use-input` (Rust/enigo) — mouse, keyboard, frontmost app
- * - `@ant/computer-use-swift` — SCContentFilter screenshots, NSWorkspace apps, TCC
+ * CLI `ComputerExecutor` implementation — Python bridge variant.
*
- * Contract: `packages/desktop/computer-use-mcp/src/executor.ts` in the apps
- * repo. The reference impl is Cowork's `apps/desktop/src/main/nest-only/
- * computer-use/executor.ts` — see notable deviations under "CLI deltas" below.
- *
- * ── CLI deltas from Cowork ─────────────────────────────────────────────────
- *
- * No `withClickThrough`. Cowork wraps every mouse op in
- * `BrowserWindow.setIgnoreMouseEvents(true)` so clicks fall through the
- * overlay. We're a terminal — no window — so the click-through bracket is
- * a no-op. The sentinel `CLI_HOST_BUNDLE_ID` never matches frontmost.
- *
- * Terminal as surrogate host. `getTerminalBundleId()` detects the emulator
- * we're running inside. It's passed as `hostBundleId` to `prepareDisplay`/
- * `resolvePrepareCapture` so the Swift side exempts it from hide AND skips
- * it in the activate z-order walk (so the terminal being frontmost doesn't
- * eat clicks meant for the target app). Also stripped from `allowedBundleIds`
- * via `withoutTerminal()` so screenshots don't capture it (Swift 0.2.1's
- * captureExcluding takes an allow-list despite the name — apps#30355).
- * `capabilities.hostBundleId` stays as the sentinel — the package's
- * frontmost gate uses that, and the terminal being frontmost is fine.
- *
- * Clipboard via `pbcopy`/`pbpaste`. No Electron `clipboard` module.
+ * Replaces the native Swift/Rust modules with a Python subprocess bridge
+ * (pyautogui + mss + pyobjc). See `pythonBridge.ts` and `runtime/mac_helper.py`.
*/
import type {
@@ -35,28 +13,22 @@ import type {
ResolvePrepareCaptureResult,
RunningApp,
ScreenshotResult,
-} from '@ant/computer-use-mcp'
-
-import { API_RESIZE_PARAMS, targetImageSize } from '@ant/computer-use-mcp'
-import { logForDebugging } from '../debug.js'
-import { errorMessage } from '../errors.js'
+} from '../../vendor/computer-use-mcp/index.js'
+import { API_RESIZE_PARAMS, targetImageSize } from '../../vendor/computer-use-mcp/index.js'
import { execFileNoThrow } from '../execFileNoThrow.js'
import { sleep } from '../sleep.js'
-import {
- CLI_CU_CAPABILITIES,
- CLI_HOST_BUNDLE_ID,
- getTerminalBundleId,
-} from './common.js'
-import { drainRunLoop } from './drainRunLoop.js'
-import { notifyExpectedEscape } from './escHotkey.js'
-import { requireComputerUseInput } from './inputLoader.js'
-import { requireComputerUseSwift } from './swiftLoader.js'
-
-// ── Helpers ───────────────────────────────────────────────────────────────────
+import { CLI_CU_CAPABILITIES, CLI_HOST_BUNDLE_ID } from './common.js'
+import { callPythonHelper } from './pythonBridge.js'
const SCREENSHOT_JPEG_QUALITY = 0.75
+const MOVE_SETTLE_MS = 50
+
+type PythonDisplayGeometry = DisplayGeometry
+
+type PythonResolvePrepareCaptureResult = ResolvePrepareCaptureResult & {
+ displayId?: number
+}
-/** Logical → physical → API target dims. See `targetImageSize` + COORDINATES.md. */
function computeTargetDims(
logicalW: number,
logicalH: number,
@@ -67,592 +39,192 @@ function computeTargetDims(
return targetImageSize(physW, physH, API_RESIZE_PARAMS)
}
-async function readClipboardViaPbpaste(): Promise {
- const { stdout, code } = await execFileNoThrow('pbpaste', [], {
- useCwd: false,
- })
- if (code !== 0) {
- throw new Error(`pbpaste exited with code ${code}`)
+function normalizeDisplayGeometry(display: PythonDisplayGeometry): DisplayGeometry {
+ return {
+ ...display,
+ displayId: display.displayId ?? display.id,
+ label: display.label ?? display.name,
}
+}
+
+async function readClipboardViaPbpaste(): Promise {
+ const { stdout, code } = await execFileNoThrow('pbpaste', [], { useCwd: false })
+ if (code !== 0) throw new Error(`pbpaste exited with code ${code}`)
return stdout
}
async function writeClipboardViaPbcopy(text: string): Promise {
- const { code } = await execFileNoThrow('pbcopy', [], {
- input: text,
- useCwd: false,
- })
- if (code !== 0) {
- throw new Error(`pbcopy exited with code ${code}`)
- }
+ const { code } = await execFileNoThrow('pbcopy', [], { input: text, useCwd: false })
+ if (code !== 0) throw new Error(`pbcopy exited with code ${code}`)
}
-type Input = ReturnType
-
-/**
- * Single-element key sequence matching "escape" or "esc" (case-insensitive).
- * Used to hole-punch the CGEventTap abort for model-synthesized Escape — enigo
- * accepts both spellings, so the tap must too.
- */
-function isBareEscape(parts: readonly string[]): boolean {
- if (parts.length !== 1) return false
- const lower = parts[0]!.toLowerCase()
- return lower === 'escape' || lower === 'esc'
-}
-
-/**
- * Instant move, then 50ms — an input→HID→AppKit→NSEvent round-trip before the
- * caller reads `NSEvent.mouseLocation` or dispatches a click. Used for click,
- * scroll, and drag-from; `animatedMove` is reserved for drag-to only. The
- * intermediate animation frames were triggering hover states and, on the
- * decomposed mouseDown/moveMouse path, emitting stray `.leftMouseDragged`
- * events (toolCalls.ts handleScroll's mouse_full workaround).
- */
-const MOVE_SETTLE_MS = 50
-
-async function moveAndSettle(
- input: Input,
- x: number,
- y: number,
-): Promise {
- await input.moveMouse(x, y, false)
- await sleep(MOVE_SETTLE_MS)
-}
-
-/**
- * Release `pressed` in reverse (last pressed = first released). Errors are
- * swallowed so a release failure never masks the real error.
- *
- * Drains via pop() rather than snapshotting length: if a drainRunLoop-
- * orphaned press lambda resolves an in-flight input.key() AFTER finally
- * calls us, that late push is still released on the next iteration. The
- * orphaned flag stops the lambda at its NEXT check, not the current await.
- */
-async function releasePressed(input: Input, pressed: string[]): Promise {
- let k: string | undefined
- while ((k = pressed.pop()) !== undefined) {
- try {
- await input.key(k, 'release')
- } catch {
- // Swallow — best-effort release.
- }
- }
-}
-
-/**
- * Bracket `fn()` with modifier press/release. `pressed` tracks which presses
- * actually landed, so a mid-press throw only releases what was pressed — no
- * stuck modifiers. The finally covers both press-phase and fn() throws.
- *
- * Caller must already be inside drainRunLoop() — key() dispatches to the
- * main queue and needs the pump to resolve.
- */
-async function withModifiers(
- input: Input,
- mods: string[],
- fn: () => Promise,
-): Promise {
- const pressed: string[] = []
- try {
- for (const m of mods) {
- await input.key(m, 'press')
- pressed.push(m)
- }
- return await fn()
- } finally {
- await releasePressed(input, pressed)
- }
-}
-
-/**
- * Port of Cowork's `typeViaClipboard`. Sequence:
- * 1. Save the user's clipboard.
- * 2. Write our text.
- * 3. READ-BACK VERIFY — clipboard writes can silently fail. If the
- * read-back doesn't match, never press Cmd+V (would paste junk).
- * 4. Cmd+V via keys().
- * 5. Sleep 100ms — battle-tested threshold for the paste-effect vs
- * clipboard-restore race. Restoring too soon means the target app
- * pastes the RESTORED content.
- * 6. Restore — in a `finally`, so a throw between 2-5 never leaves the
- * user's clipboard clobbered. Restore failures are swallowed.
- */
-async function typeViaClipboard(input: Input, text: string): Promise {
+async function typeViaClipboard(text: string): Promise {
let saved: string | undefined
try {
saved = await readClipboardViaPbpaste()
- } catch {
- logForDebugging(
- '[computer-use] pbpaste before paste failed; proceeding without restore',
- )
- }
+ } catch {}
try {
await writeClipboardViaPbcopy(text)
- if ((await readClipboardViaPbpaste()) !== text) {
- throw new Error('Clipboard write did not round-trip.')
- }
- await input.keys(['command', 'v'])
+ await callPythonHelper('key', { keySequence: 'command+v', repeat: 1 })
await sleep(100)
} finally {
if (typeof saved === 'string') {
try {
await writeClipboardViaPbcopy(saved)
- } catch {
- logForDebugging('[computer-use] clipboard restore after paste failed')
- }
+ } catch {}
}
}
}
-/**
- * Port of Cowork's `animateMouseMovement` + `animatedMove`. Ease-out-cubic at
- * 60fps; distance-proportional duration at 2000 px/sec, capped at 0.5s. When
- * the sub-gate is off (or distance < ~2 frames), falls through to
- * `moveAndSettle`. Called only from `drag` for the press→to motion — target
- * apps may watch for `.leftMouseDragged` specifically (not just "button down +
- * position changed") and the slow motion gives them time to process
- * intermediate positions (scrollbars, window resizes).
- */
-async function animatedMove(
- input: Input,
- targetX: number,
- targetY: number,
- mouseAnimationEnabled: boolean,
-): Promise {
- if (!mouseAnimationEnabled) {
- await moveAndSettle(input, targetX, targetY)
- return
- }
- const start = await input.mouseLocation()
- const deltaX = targetX - start.x
- const deltaY = targetY - start.y
- const distance = Math.hypot(deltaX, deltaY)
- if (distance < 1) return
- const durationSec = Math.min(distance / 2000, 0.5)
- if (durationSec < 0.03) {
- await moveAndSettle(input, targetX, targetY)
- return
- }
- const frameRate = 60
- const frameIntervalMs = 1000 / frameRate
- const totalFrames = Math.floor(durationSec * frameRate)
- for (let frame = 1; frame <= totalFrames; frame++) {
- const t = frame / totalFrames
- const eased = 1 - Math.pow(1 - t, 3)
- await input.moveMouse(
- Math.round(start.x + deltaX * eased),
- Math.round(start.y + deltaY * eased),
- false,
- )
- if (frame < totalFrames) {
- await sleep(frameIntervalMs)
- }
- }
- // Last frame has no trailing sleep — same HID round-trip before the
- // caller's mouseButton reads NSEvent.mouseLocation.
- await sleep(MOVE_SETTLE_MS)
-}
-
-// ── Factory ───────────────────────────────────────────────────────────────
-
-export function createCliExecutor(opts: {
+export function createCliExecutor(_opts: {
getMouseAnimationEnabled: () => boolean
getHideBeforeActionEnabled: () => boolean
}): ComputerExecutor {
if (process.platform !== 'darwin') {
- throw new Error(
- `createCliExecutor called on ${process.platform}. Computer control is macOS-only.`,
- )
+ throw new Error(`createCliExecutor called on ${process.platform}. Computer control is macOS-only.`)
}
- // Swift loaded once at factory time — every executor method needs it.
- // Input loaded lazily via requireComputerUseInput() on first mouse/keyboard
- // call — it caches internally, so screenshot-only flows never pull the
- // enigo .node.
- const cu = requireComputerUseSwift()
-
- const { getMouseAnimationEnabled, getHideBeforeActionEnabled } = opts
- const terminalBundleId = getTerminalBundleId()
- const surrogateHost = terminalBundleId ?? CLI_HOST_BUNDLE_ID
- // Swift 0.2.1's captureExcluding/captureRegion take an ALLOW list despite the
- // name (apps#30355 — complement computed Swift-side against running apps).
- // The terminal isn't in the user's grants so it's naturally excluded, but if
- // the package ever passes it through we strip it here so the terminal never
- // photobombs a screenshot.
- const withoutTerminal = (allowed: readonly string[]): string[] =>
- terminalBundleId === null
- ? [...allowed]
- : allowed.filter(id => id !== terminalBundleId)
-
- logForDebugging(
- terminalBundleId
- ? `[computer-use] terminal ${terminalBundleId} → surrogate host (hide-exempt, activate-skip, screenshot-excluded)`
- : '[computer-use] terminal not detected; falling back to sentinel host',
- )
-
return {
capabilities: {
...CLI_CU_CAPABILITIES,
hostBundleId: CLI_HOST_BUNDLE_ID,
},
- // ── Pre-action sequence (hide + defocus) ────────────────────────────
-
- async prepareForAction(
- allowlistBundleIds: string[],
- displayId?: number,
- ): Promise {
- if (!getHideBeforeActionEnabled()) {
- return []
- }
- // prepareDisplay isn't @MainActor (plain Task{}), but its .hide() calls
- // trigger window-manager events that queue on CFRunLoop. Without the
- // pump, those pile up during Swift's ~1s of usleeps and flush all at
- // once when the next pumped call runs — visible window flashing.
- // Electron drains CFRunLoop continuously so Cowork doesn't see this.
- // Worst-case 100ms + 5×200ms safety-net ≈ 1.1s, well under the 30s
- // drainRunLoop ceiling.
- //
- // "Continue with action execution even if switching fails" — the
- // frontmost gate in toolCalls.ts catches any actual unsafe state.
- return drainRunLoop(async () => {
- try {
- const result = await cu.apps.prepareDisplay(
- allowlistBundleIds,
- surrogateHost,
- displayId,
- )
- if (result.activated) {
- logForDebugging(
- `[computer-use] prepareForAction: activated ${result.activated}`,
- )
- }
- return result.hidden
- } catch (err) {
- logForDebugging(
- `[computer-use] prepareForAction failed; continuing to action: ${errorMessage(err)}`,
- { level: 'warn' },
- )
- return []
- }
- })
+ async prepareForAction(): Promise {
+ return callPythonHelper('prepare_for_action', {})
},
- async previewHideSet(
- allowlistBundleIds: string[],
- displayId?: number,
- ): Promise> {
- return cu.apps.previewHideSet(
- [...allowlistBundleIds, surrogateHost],
- displayId,
- )
+ async previewHideSet() {
+ return callPythonHelper('preview_hide_set', {})
},
- // ── Display ──────────────────────────────────────────────────────────
-
async getDisplaySize(displayId?: number): Promise {
- return cu.display.getSize(displayId)
+ return normalizeDisplayGeometry(await callPythonHelper('get_display_size', { displayId }))
},
async listDisplays(): Promise {
- return cu.display.listAll()
+ const displays = await callPythonHelper('list_displays', {})
+ return displays.map(display => normalizeDisplayGeometry(display))
},
- async findWindowDisplays(
- bundleIds: string[],
- ): Promise> {
- return cu.apps.findWindowDisplays(bundleIds)
+ async findWindowDisplays(bundleIds: string[]) {
+ return callPythonHelper('find_window_displays', { bundleIds })
},
- async resolvePrepareCapture(opts: {
- allowedBundleIds: string[]
- preferredDisplayId?: number
- autoResolve: boolean
- doHide?: boolean
- }): Promise {
- const d = cu.display.getSize(opts.preferredDisplayId)
- const [targetW, targetH] = computeTargetDims(
- d.width,
- d.height,
- d.scaleFactor,
- )
- return drainRunLoop(() =>
- cu.resolvePrepareCapture(
- withoutTerminal(opts.allowedBundleIds),
- surrogateHost,
- SCREENSHOT_JPEG_QUALITY,
- targetW,
- targetH,
- opts.preferredDisplayId,
- opts.autoResolve,
- opts.doHide,
- ),
- )
+ async resolvePrepareCapture(opts): Promise {
+ const display = await this.getDisplaySize(opts.preferredDisplayId)
+ const [targetW, targetH] = computeTargetDims(display.width, display.height, display.scaleFactor)
+ const result = await callPythonHelper('resolve_prepare_capture', {
+ preferredDisplayId: opts.preferredDisplayId,
+ targetWidth: targetW,
+ targetHeight: targetH,
+ jpegQuality: SCREENSHOT_JPEG_QUALITY,
+ })
+ return {
+ ...result,
+ display: normalizeDisplayGeometry(result.display),
+ resolvedDisplayId: result.resolvedDisplayId ?? result.displayId,
+ }
},
- /**
- * Pre-size to `targetImageSize` output so the API transcoder's early-return
- * fires — no server-side resize, `scaleCoord` stays coherent. See
- * packages/desktop/computer-use-mcp/COORDINATES.md.
- */
- async screenshot(opts: {
- allowedBundleIds: string[]
- displayId?: number
- }): Promise {
- const d = cu.display.getSize(opts.displayId)
- const [targetW, targetH] = computeTargetDims(
- d.width,
- d.height,
- d.scaleFactor,
- )
- return drainRunLoop(() =>
- cu.screenshot.captureExcluding(
- withoutTerminal(opts.allowedBundleIds),
- SCREENSHOT_JPEG_QUALITY,
- targetW,
- targetH,
- opts.displayId,
- ),
- )
+ async screenshot(opts): Promise {
+ const display = await this.getDisplaySize(opts.displayId)
+ const [targetW, targetH] = computeTargetDims(display.width, display.height, display.scaleFactor)
+ const result = await callPythonHelper('screenshot', {
+ displayId: opts.displayId,
+ targetWidth: targetW,
+ targetHeight: targetH,
+ jpegQuality: SCREENSHOT_JPEG_QUALITY,
+ })
+ return result
},
- async zoom(
- regionLogical: { x: number; y: number; w: number; h: number },
- allowedBundleIds: string[],
- displayId?: number,
- ): Promise<{ base64: string; width: number; height: number }> {
- const d = cu.display.getSize(displayId)
- const [outW, outH] = computeTargetDims(
- regionLogical.w,
- regionLogical.h,
- d.scaleFactor,
- )
- return drainRunLoop(() =>
- cu.screenshot.captureRegion(
- withoutTerminal(allowedBundleIds),
- regionLogical.x,
- regionLogical.y,
- regionLogical.w,
- regionLogical.h,
- outW,
- outH,
- SCREENSHOT_JPEG_QUALITY,
- displayId,
- ),
- )
- },
-
- // ── Keyboard ─────────────────────────────────────────────────────────
-
- /**
- * xdotool-style sequence e.g. "ctrl+shift+a" → split on '+' and pass to
- * keys(). keys() dispatches to DispatchQueue.main — drainRunLoop pumps
- * CFRunLoop so it resolves. Rust's error-path cleanup (enigo_wrap.rs)
- * releases modifiers on each invocation, so a mid-loop throw leaves
- * nothing stuck. 8ms between iterations — 125Hz USB polling cadence.
- */
- async key(keySequence: string, repeat?: number): Promise {
- const input = requireComputerUseInput()
- const parts = keySequence.split('+').filter(p => p.length > 0)
- // Bare-only: the CGEventTap checks event.flags.isEmpty so ctrl+escape
- // etc. pass through without aborting.
- const isEsc = isBareEscape(parts)
- const n = repeat ?? 1
- await drainRunLoop(async () => {
- for (let i = 0; i < n; i++) {
- if (i > 0) {
- await sleep(8)
- }
- if (isEsc) {
- notifyExpectedEscape()
- }
- await input.keys(parts)
- }
+ async zoom(regionLogical, _allowedBundleIds, displayId) {
+ const display = await this.getDisplaySize(displayId)
+ const [outW, outH] = computeTargetDims(regionLogical.w, regionLogical.h, display.scaleFactor)
+ return callPythonHelper('zoom', {
+ x: regionLogical.x,
+ y: regionLogical.y,
+ width: regionLogical.w,
+ height: regionLogical.h,
+ targetWidth: outW,
+ targetHeight: outH,
})
},
- async holdKey(keyNames: string[], durationMs: number): Promise {
- const input = requireComputerUseInput()
- // Press/release each wrapped in drainRunLoop; the sleep sits outside so
- // durationMs isn't bounded by drainRunLoop's 30s timeout. `pressed`
- // tracks which presses landed so a mid-press throw still releases
- // everything that was actually pressed.
- //
- // `orphaned` guards against a timeout-orphan race: if the press-phase
- // drainRunLoop times out while the esc-hotkey pump-retain keeps the
- // pump running, the orphaned lambda would continue pushing to `pressed`
- // after finally's releasePressed snapshotted the length — leaving keys
- // stuck. The flag stops the lambda at the next iteration.
- const pressed: string[] = []
- let orphaned = false
- try {
- await drainRunLoop(async () => {
- for (const k of keyNames) {
- if (orphaned) return
- // Bare Escape: notify the CGEventTap so it doesn't fire the
- // abort callback for a model-synthesized press. Same as key().
- if (isBareEscape([k])) {
- notifyExpectedEscape()
- }
- await input.key(k, 'press')
- pressed.push(k)
- }
- })
- await sleep(durationMs)
- } finally {
- orphaned = true
- await drainRunLoop(() => releasePressed(input, pressed))
- }
+ async key(keySequence: string, repeat?: number): Promise {
+ await callPythonHelper('key', { keySequence, repeat: repeat ?? 1 })
},
- async type(text: string, opts: { viaClipboard: boolean }): Promise {
- const input = requireComputerUseInput()
- if (opts.viaClipboard) {
- // keys(['command','v']) inside needs the pump.
- await drainRunLoop(() => typeViaClipboard(input, text))
+ async holdKey(keyNames: string[], durationMs: number): Promise {
+ await callPythonHelper('hold_key', { keyNames, durationMs })
+ },
+
+ async type(text: string, opts2: { viaClipboard: boolean }): Promise {
+ if (opts2.viaClipboard) {
+ await typeViaClipboard(text)
return
}
- // `toolCalls.ts` handles the grapheme loop + 8ms sleeps and calls this
- // once per grapheme. typeText doesn't dispatch to the main queue.
- await input.typeText(text)
+ await callPythonHelper('type', { text })
},
readClipboard: readClipboardViaPbpaste,
-
writeClipboard: writeClipboardViaPbcopy,
- // ── Mouse ────────────────────────────────────────────────────────────
-
- async moveMouse(x: number, y: number): Promise {
- await moveAndSettle(requireComputerUseInput(), x, y)
- },
-
- /**
- * Move, then click. Modifiers are press/release bracketed via withModifiers
- * — same pattern as Cowork. AppKit computes NSEvent.clickCount from timing
- * + position proximity, so double/triple click work without setting the
- * CGEvent clickState field. key() inside withModifiers needs the pump;
- * the modifier-less path doesn't.
- */
- async click(
- x: number,
- y: number,
- button: 'left' | 'right' | 'middle',
- count: 1 | 2 | 3,
- modifiers?: string[],
- ): Promise {
- const input = requireComputerUseInput()
- await moveAndSettle(input, x, y)
- if (modifiers && modifiers.length > 0) {
- await drainRunLoop(() =>
- withModifiers(input, modifiers, () =>
- input.mouseButton(button, 'click', count),
- ),
- )
- } else {
- await input.mouseButton(button, 'click', count)
- }
+ async click(x, y, button, count, modifiers): Promise {
+ await callPythonHelper('click', { x, y, button, count, modifiers })
+ await sleep(MOVE_SETTLE_MS)
},
async mouseDown(): Promise {
- await requireComputerUseInput().mouseButton('left', 'press')
+ await callPythonHelper('mouse_down', {})
},
async mouseUp(): Promise {
- await requireComputerUseInput().mouseButton('left', 'release')
+ await callPythonHelper('mouse_up', {})
},
async getCursorPosition(): Promise<{ x: number; y: number }> {
- return requireComputerUseInput().mouseLocation()
+ return callPythonHelper('cursor_position', {})
},
- /**
- * `from === undefined` → drag from current cursor (training's
- * left_click_drag with start_coordinate omitted). Inner `finally`: the
- * button is ALWAYS released even if the move throws — otherwise the
- * user's left button is stuck-pressed until they physically click.
- * 50ms sleep after press: enigo's move_mouse reads NSEvent.pressedMouseButtons
- * to decide .leftMouseDragged vs .mouseMoved; the synthetic leftMouseDown
- * needs a HID-tap round-trip to show up there.
- */
- async drag(
- from: { x: number; y: number } | undefined,
- to: { x: number; y: number },
- ): Promise {
- const input = requireComputerUseInput()
- if (from !== undefined) {
- await moveAndSettle(input, from.x, from.y)
- }
- await input.mouseButton('left', 'press')
+ async drag(from, to): Promise {
+ await callPythonHelper('drag', { from, to })
await sleep(MOVE_SETTLE_MS)
- try {
- await animatedMove(input, to.x, to.y, getMouseAnimationEnabled())
- } finally {
- await input.mouseButton('left', 'release')
- }
},
- /**
- * Move first, then scroll each axis. Vertical-first — it's the common
- * axis; a horizontal failure shouldn't lose the vertical.
- */
- async scroll(x: number, y: number, dx: number, dy: number): Promise {
- const input = requireComputerUseInput()
- await moveAndSettle(input, x, y)
- if (dy !== 0) {
- await input.mouseScroll(dy, 'vertical')
- }
- if (dx !== 0) {
- await input.mouseScroll(dx, 'horizontal')
- }
+ async moveMouse(x, y): Promise {
+ await callPythonHelper('move_mouse', { x, y })
+ await sleep(MOVE_SETTLE_MS)
},
- // ── App management ───────────────────────────────────────────────────
+ async scroll(x, y, dx, dy): Promise {
+ await callPythonHelper('scroll', { x, y, deltaX: dx, deltaY: dy })
+ },
async getFrontmostApp(): Promise {
- const info = requireComputerUseInput().getFrontmostAppInfo()
- if (!info || !info.bundleId) return null
- return { bundleId: info.bundleId, displayName: info.appName }
+ return callPythonHelper('frontmost_app', {})
},
- async appUnderPoint(
- x: number,
- y: number,
- ): Promise<{ bundleId: string; displayName: string } | null> {
- return cu.apps.appUnderPoint(x, y)
+ async appUnderPoint(x, y) {
+ return callPythonHelper('app_under_point', { x, y })
},
async listInstalledApps(): Promise {
- // `ComputerUseInstalledApp` is `{bundleId, displayName, path}`.
- // `InstalledApp` adds optional `iconDataUrl` — left unpopulated;
- // the approval dialog fetches lazily via getAppIcon() below.
- return drainRunLoop(() => cu.apps.listInstalled())
- },
-
- async getAppIcon(path: string): Promise {
- return cu.apps.iconDataUrl(path) ?? undefined
+ return callPythonHelper('list_installed_apps', {})
},
async listRunningApps(): Promise {
- return cu.apps.listRunning()
+ return callPythonHelper('list_running_apps', {})
},
async openApp(bundleId: string): Promise {
- await cu.apps.open(bundleId)
+ await callPythonHelper('open_app', { bundleId })
},
}
}
-/**
- * Module-level export (not on the executor object) — called at turn-end from
- * `stopHooks.ts` / `query.ts`, outside the executor lifecycle. Fire-and-forget
- * at the call site; the caller `.catch()`es.
- */
-export async function unhideComputerUseApps(
- bundleIds: readonly string[],
-): Promise {
- if (bundleIds.length === 0) return
- const cu = requireComputerUseSwift()
- await cu.apps.unhide([...bundleIds])
+export async function unhideComputerUseApps(_bundleIds: readonly string[]): Promise {
+ return
}
diff --git a/src/utils/computerUse/gates.ts b/src/utils/computerUse/gates.ts
index 6563a480..c94603d1 100644
--- a/src/utils/computerUse/gates.ts
+++ b/src/utils/computerUse/gates.ts
@@ -1,8 +1,6 @@
-import type { CoordinateMode, CuSubGates } from '@ant/computer-use-mcp/types'
+import type { CoordinateMode, CuSubGates } from '../../vendor/computer-use-mcp/types.js'
import { getDynamicConfig_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
-import { getSubscriptionType } from '../auth.js'
-import { isEnvTruthy } from '../envUtils.js'
type ChicagoConfig = CuSubGates & {
enabled: boolean
@@ -10,7 +8,7 @@ type ChicagoConfig = CuSubGates & {
}
const DEFAULTS: ChicagoConfig = {
- enabled: false,
+ enabled: true,
pixelValidation: false,
clipboardPasteMultiline: true,
mouseAnimation: true,
@@ -33,28 +31,8 @@ function readConfig(): ChicagoConfig {
}
}
-// Max/Pro only for external rollout. Ant bypass so dogfooding continues
-// regardless of subscription tier — not all ants are max/pro, and per
-// CLAUDE.md:281, USER_TYPE !== 'ant' branches get zero antfooding.
-function hasRequiredSubscription(): boolean {
- if (process.env.USER_TYPE === 'ant') return true
- const tier = getSubscriptionType()
- return tier === 'max' || tier === 'pro'
-}
-
export function getChicagoEnabled(): boolean {
- // Disable for ants whose shell inherited monorepo dev config.
- // MONOREPO_ROOT_DIR is exported by config/local/zsh/zshrc, which
- // laptop-setup.sh wires into ~/.zshrc — its presence is the cheap
- // proxy for "has monorepo access". Override: ALLOW_ANT_COMPUTER_USE_MCP=1.
- if (
- process.env.USER_TYPE === 'ant' &&
- process.env.MONOREPO_ROOT_DIR &&
- !isEnvTruthy(process.env.ALLOW_ANT_COMPUTER_USE_MCP)
- ) {
- return false
- }
- return hasRequiredSubscription() && readConfig().enabled
+ return true
}
export function getChicagoSubGates(): CuSubGates {
diff --git a/src/utils/computerUse/hostAdapter.ts b/src/utils/computerUse/hostAdapter.ts
index d9e78fae..3c24da4d 100644
--- a/src/utils/computerUse/hostAdapter.ts
+++ b/src/utils/computerUse/hostAdapter.ts
@@ -1,13 +1,13 @@
import type {
ComputerUseHostAdapter,
Logger,
-} from '@ant/computer-use-mcp/types'
+} from '../../vendor/computer-use-mcp/types.js'
import { format } from 'util'
import { logForDebugging } from '../debug.js'
import { COMPUTER_USE_MCP_SERVER_NAME } from './common.js'
import { createCliExecutor } from './executor.js'
import { getChicagoEnabled, getChicagoSubGates } from './gates.js'
-import { requireComputerUseSwift } from './swiftLoader.js'
+import { callPythonHelper } from './pythonBridge.js'
class DebugLogger implements Logger {
silly(message: string, ...args: unknown[]): void {
@@ -29,12 +29,6 @@ class DebugLogger implements Logger {
let cached: ComputerUseHostAdapter | undefined
-/**
- * Process-lifetime singleton. Built once on first CU tool call; native modules
- * (both `@ant/computer-use-input` and `@ant/computer-use-swift`) are loaded
- * here via the executor factory, which throws on load failure — there is no
- * degraded mode.
- */
export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
if (cached) return cached
cached = {
@@ -45,24 +39,14 @@ export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
getHideBeforeActionEnabled: () => getChicagoSubGates().hideBeforeAction,
}),
ensureOsPermissions: async () => {
- const cu = requireComputerUseSwift()
- const accessibility = cu.tcc.checkAccessibility()
- const screenRecording = cu.tcc.checkScreenRecording()
- return accessibility && screenRecording
- ? { granted: true }
- : { granted: false, accessibility, screenRecording }
+ const perms = await callPythonHelper<{ accessibility: boolean; screenRecording: boolean }>('check_permissions', {})
+ return perms.accessibility && perms.screenRecording
+ ? { granted: true as const }
+ : { granted: false as const, accessibility: perms.accessibility, screenRecording: perms.screenRecording }
},
isDisabled: () => !getChicagoEnabled(),
getSubGates: getChicagoSubGates,
- // cleanup.ts always unhides at turn end — no user preference to disable it.
getAutoUnhideEnabled: () => true,
-
- // Pixel-validation JPEG decode+crop. MUST be synchronous (the package
- // does `patch1.equals(patch2)` directly on the return value). Cowork uses
- // Electron's `nativeImage` (sync); our `image-processor-napi` is
- // sharp-compatible and async-only. Returning null → validation skipped,
- // click proceeds — the designed fallback per `PixelCompareResult.skipped`.
- // The sub-gate defaults to false anyway.
cropRawPatch: () => null,
}
return cached
diff --git a/src/utils/computerUse/inputLoader.ts b/src/utils/computerUse/inputLoader.ts
index 2dd6e29c..f0f2c275 100644
--- a/src/utils/computerUse/inputLoader.ts
+++ b/src/utils/computerUse/inputLoader.ts
@@ -1,30 +1,3 @@
-import type {
- ComputerUseInput,
- ComputerUseInputAPI,
-} from '@ant/computer-use-input'
-
-let cached: ComputerUseInputAPI | undefined
-
-/**
- * Package's js/index.js reads COMPUTER_USE_INPUT_NODE_PATH (baked by
- * build-with-plugins.ts on darwin targets, unset otherwise — falls through to
- * the node_modules prebuilds/ path).
- *
- * The package exports a discriminated union on `isSupported` — narrowed here
- * once so callers get the bare `ComputerUseInputAPI` without re-checking.
- *
- * key()/keys() dispatch enigo work onto DispatchQueue.main via
- * dispatch2::run_on_main, then block a tokio worker on a channel. Under
- * Electron (CFRunLoop drains the main queue) this works; under libuv
- * (Node/bun) the main queue never drains and the promise hangs. The executor
- * calls these inside drainRunLoop().
- */
-export function requireComputerUseInput(): ComputerUseInputAPI {
- if (cached) return cached
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- const input = require('@ant/computer-use-input') as ComputerUseInput
- if (!input.isSupported) {
- throw new Error('@ant/computer-use-input is not supported on this platform')
- }
- return (cached = input)
+export function requireComputerUseInput(): any {
+ throw new Error('Native input module replaced by Python bridge. See pythonBridge.ts')
}
diff --git a/src/utils/computerUse/mcpServer.ts b/src/utils/computerUse/mcpServer.ts
index d51d80ab..6b5100bf 100644
--- a/src/utils/computerUse/mcpServer.ts
+++ b/src/utils/computerUse/mcpServer.ts
@@ -1,7 +1,7 @@
import {
buildComputerUseTools,
createComputerUseMcpServer,
-} from '@ant/computer-use-mcp'
+} from '../../vendor/computer-use-mcp/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { homedir } from 'os'
diff --git a/src/utils/computerUse/pythonBridge.ts b/src/utils/computerUse/pythonBridge.ts
new file mode 100644
index 00000000..bfeb3231
--- /dev/null
+++ b/src/utils/computerUse/pythonBridge.ts
@@ -0,0 +1,110 @@
+import { createHash } from 'node:crypto'
+import { readFile, mkdir, access, writeFile } from 'node:fs/promises'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import { execFileNoThrow } from '../execFileNoThrow.js'
+import { logForDebugging } from '../debug.js'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const projectRoot = path.resolve(__dirname, '../../..')
+const runtimeRoot = path.join(projectRoot, 'runtime')
+const runtimeStateRoot = path.join(projectRoot, '.runtime')
+const venvRoot = path.join(runtimeStateRoot, 'venv')
+const requirementsPath = path.join(runtimeRoot, 'requirements.txt')
+const helperPath = path.join(runtimeRoot, 'mac_helper.py')
+const installStampPath = path.join(runtimeStateRoot, 'requirements.sha256')
+
+let bootstrapPromise: Promise | undefined
+
+function pythonBinPath(): string {
+ return path.join(venvRoot, 'bin', 'python3')
+}
+
+async function pathExists(target: string): Promise {
+ try {
+ await access(target)
+ return true
+ } catch {
+ return false
+ }
+}
+
+async function runOrThrow(file: string, args: string[], label: string): Promise {
+ const { code, stdout, stderr } = await execFileNoThrow(file, args, { useCwd: false })
+ if (code !== 0) {
+ throw new Error(`${label} failed with code ${code}: ${stderr || stdout || 'unknown error'}`)
+ }
+ return stdout
+}
+
+export async function ensureBootstrapped(): Promise {
+ if (bootstrapPromise) return bootstrapPromise
+ bootstrapPromise = (async () => {
+ await mkdir(runtimeStateRoot, { recursive: true })
+
+ if (!(await pathExists(pythonBinPath()))) {
+ logForDebugging('creating runtime venv at %s', { level: 'debug' })
+ await runOrThrow('python3', ['-m', 'venv', venvRoot], 'python venv creation')
+ }
+
+ if (!(await pathExists(path.join(venvRoot, 'bin', 'pip')))) {
+ logForDebugging('bootstrapping pip with ensurepip', { level: 'debug' })
+ await runOrThrow(pythonBinPath(), ['-m', 'ensurepip', '--upgrade'], 'ensurepip')
+ }
+
+ const requirements = await readFile(requirementsPath, 'utf8')
+ const digest = createHash('sha256').update(requirements).digest('hex')
+ let installedDigest = ''
+ try {
+ installedDigest = (await readFile(installStampPath, 'utf8')).trim()
+ } catch {}
+
+ if (installedDigest !== digest) {
+ logForDebugging('installing python runtime dependencies', { level: 'debug' })
+ await runOrThrow(pythonBinPath(), ['-m', 'pip', 'install', '--upgrade', 'pip'], 'pip upgrade')
+ await runOrThrow(
+ pythonBinPath(),
+ ['-m', 'pip', 'install', '-r', requirementsPath],
+ 'python dependency install',
+ )
+ await writeFile(installStampPath, `${digest}\n`, 'utf8')
+ }
+ })()
+
+ try {
+ await bootstrapPromise
+ } catch (error) {
+ bootstrapPromise = undefined
+ throw error
+ }
+}
+
+export async function callPythonHelper(command: string, payload: Record = {}): Promise {
+ await ensureBootstrapped()
+ const { code, stdout, stderr } = await execFileNoThrow(
+ pythonBinPath(),
+ [helperPath, command, '--payload', JSON.stringify(payload)],
+ { useCwd: false },
+ )
+
+ if (code !== 0 && !stdout.trim()) {
+ throw new Error(stderr || `Python helper ${command} failed with code ${code}`)
+ }
+
+ let parsed: { ok: boolean; result?: T; error?: { message?: string } }
+ try {
+ parsed = JSON.parse(stdout)
+ } catch {
+ throw new Error(stderr || stdout || `Python helper ${command} returned invalid JSON`)
+ }
+
+ if (!parsed.ok) {
+ throw new Error(parsed.error?.message || `Python helper ${command} failed`)
+ }
+
+ return parsed.result as T
+}
+
+export function getRuntimePaths(): { projectRoot: string; runtimeRoot: string; venvRoot: string } {
+ return { projectRoot, runtimeRoot, venvRoot }
+}
diff --git a/src/utils/computerUse/setup.ts b/src/utils/computerUse/setup.ts
index 8355e9f2..b4f8c63c 100644
--- a/src/utils/computerUse/setup.ts
+++ b/src/utils/computerUse/setup.ts
@@ -1,4 +1,4 @@
-import { buildComputerUseTools } from '@ant/computer-use-mcp'
+import { buildComputerUseTools } from '../../vendor/computer-use-mcp/index.js'
import { join } from 'path'
import { fileURLToPath } from 'url'
import { buildMcpToolName } from '../../services/mcp/mcpStringUtils.js'
diff --git a/src/utils/computerUse/swiftLoader.ts b/src/utils/computerUse/swiftLoader.ts
index 1a8a9b25..9cb110a3 100644
--- a/src/utils/computerUse/swiftLoader.ts
+++ b/src/utils/computerUse/swiftLoader.ts
@@ -1,23 +1,3 @@
-import type { ComputerUseAPI } from '@ant/computer-use-swift'
-
-let cached: ComputerUseAPI | undefined
-
-/**
- * Package's js/index.js reads COMPUTER_USE_SWIFT_NODE_PATH (baked by
- * build-with-plugins.ts on darwin targets, unset otherwise — falls through to
- * the node_modules prebuilds/ path). We cache the loaded native module.
- *
- * The four @MainActor methods (captureExcluding, captureRegion,
- * apps.listInstalled, resolvePrepareCapture) dispatch to DispatchQueue.main
- * and will hang under libuv unless CFRunLoop is pumped — call sites wrap
- * these in drainRunLoop().
- */
-export function requireComputerUseSwift(): ComputerUseAPI {
- if (process.platform !== 'darwin') {
- throw new Error('@ant/computer-use-swift is macOS-only')
- }
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- return (cached ??= require('@ant/computer-use-swift') as ComputerUseAPI)
+export function requireComputerUseSwift(): any {
+ throw new Error('Native Swift module replaced by Python bridge. See pythonBridge.ts')
}
-
-export type { ComputerUseAPI }
diff --git a/src/utils/computerUse/wrapper.tsx b/src/utils/computerUse/wrapper.tsx
index 217015ba..65d46f5e 100644
--- a/src/utils/computerUse/wrapper.tsx
+++ b/src/utils/computerUse/wrapper.tsx
@@ -16,7 +16,7 @@
* GrowthBook gate `tengu_malort_pedway` (see gates.ts).
*/
-import { bindSessionContext, type ComputerUseSessionContext, type CuCallToolResult, type CuPermissionRequest, type CuPermissionResponse, DEFAULT_GRANT_FLAGS, type ScreenshotDims } from '@ant/computer-use-mcp';
+import { bindSessionContext, type ComputerUseSessionContext, type CuCallToolResult, type CuPermissionRequest, type CuPermissionResponse, DEFAULT_GRANT_FLAGS, type ScreenshotDims } from '../../vendor/computer-use-mcp/index.js';
import * as React from 'react';
import { getSessionId } from '../../bootstrap/state.js';
import { ComputerUseApproval } from '../../components/permissions/ComputerUseApproval/ComputerUseApproval.js';
diff --git a/src/vendor/computer-use-mcp/deniedApps.ts b/src/vendor/computer-use-mcp/deniedApps.ts
new file mode 100644
index 00000000..92f14e0b
--- /dev/null
+++ b/src/vendor/computer-use-mcp/deniedApps.ts
@@ -0,0 +1,553 @@
+/**
+ * App category lookup for tiered CU permissions. Three categories land at a
+ * restricted tier instead of `"full"`:
+ *
+ * - **browser** → `"read"` tier — visible in screenshots, NO interaction.
+ * The model can read an already-open page but must use the Claude-in-Chrome
+ * MCP for navigation/clicking/typing.
+ * - **terminal** → `"click"` tier — visible + clickable, NO typing. The
+ * model can click a Run button or scroll test output in an IDE, but can't
+ * type into the integrated terminal. Use the Bash tool for shell work.
+ * - **trading** → `"read"` tier — same restrictions as browsers, but no
+ * CiC-MCP alternative exists. For platforms where a stray click can
+ * execute a trade or send a message to a counterparty.
+ *
+ * Uncategorized apps default to `"full"`. See `getDefaultTierForApp`.
+ *
+ * Identification is two-layered:
+ * 1. Bundle ID match (macOS-only; `InstalledApp.bundleId` is a
+ * CFBundleIdentifier and meaningless on Windows). Fast, exact, the
+ * primary mechanism while CU is darwin-gated.
+ * 2. Display-name substring match (cross-platform fallback). Catches
+ * unresolved requests ("Chrome" when Chrome isn't installed) AND will
+ * be the primary mechanism on Windows/Linux where there's no bundle ID.
+ * Windows-relevant names (PowerShell, cmd, Windows Terminal) are
+ * included now so they activate the moment the darwin gate lifts.
+ *
+ * Keep this file **import-free** (like sentinelApps.ts) — the renderer may
+ * import it via a package.json subpath export, and pulling in
+ * `@modelcontextprotocol/sdk` (a devDep) through the index → mcpServer chain
+ * would fail module resolution in Next.js. The `CuAppPermTier` type is
+ * duplicated as a string literal below rather than imported.
+ */
+
+export type DeniedCategory = "browser" | "terminal" | "trading";
+
+/**
+ * Map a category to its hardcoded tier. Return-type is the string-literal
+ * union inline (this file is import-free; see header comment). The
+ * authoritative type is `CuAppPermTier` in types.ts — keep in sync.
+ *
+ * Not bijective — both `"browser"` and `"trading"` map to `"read"`. Copy
+ * that differs by category (the "use CiC" hint is browser-only) must check
+ * the category, not just the tier.
+ */
+export function categoryToTier(
+ category: DeniedCategory | null,
+): "read" | "click" | "full" {
+ if (category === "browser" || category === "trading") return "read";
+ if (category === "terminal") return "click";
+ return "full";
+}
+
+// ─── Bundle-ID deny sets (macOS) ─────────────────────────────────────────
+
+const BROWSER_BUNDLE_IDS: ReadonlySet = new Set([
+ // Apple
+ "com.apple.Safari",
+ "com.apple.SafariTechnologyPreview",
+ // Google
+ "com.google.Chrome",
+ "com.google.Chrome.beta",
+ "com.google.Chrome.dev",
+ "com.google.Chrome.canary",
+ // Microsoft
+ "com.microsoft.edgemac",
+ "com.microsoft.edgemac.Beta",
+ "com.microsoft.edgemac.Dev",
+ "com.microsoft.edgemac.Canary",
+ // Mozilla
+ "org.mozilla.firefox",
+ "org.mozilla.firefoxdeveloperedition",
+ "org.mozilla.nightly",
+ // Chromium-based
+ "org.chromium.Chromium",
+ "com.brave.Browser",
+ "com.brave.Browser.beta",
+ "com.brave.Browser.nightly",
+ "com.operasoftware.Opera",
+ "com.operasoftware.OperaGX",
+ "com.operasoftware.OperaDeveloper",
+ "com.vivaldi.Vivaldi",
+ // The Browser Company
+ "company.thebrowser.Browser", // Arc
+ "company.thebrowser.dia", // Dia (agentic)
+ // Privacy-focused
+ "org.torproject.torbrowser",
+ "com.duckduckgo.macos.browser",
+ "ru.yandex.desktop.yandex-browser",
+ // Agentic / AI browsers — newer entrants with LLM integrations
+ "ai.perplexity.comet",
+ "com.sigmaos.sigmaos.macos", // SigmaOS
+ // Webkit-based misc
+ "com.kagi.kagimacOS", // Orion
+]);
+
+/**
+ * Terminals + IDEs with integrated terminals. Supersets
+ * `SHELL_ACCESS_BUNDLE_IDS` from sentinelApps.ts — terminals proceed to the
+ * approval dialog at tier "click", and the sentinel warning renders
+ * alongside the tier badge.
+ */
+const TERMINAL_BUNDLE_IDS: ReadonlySet = new Set([
+ // Dedicated terminals
+ "com.apple.Terminal",
+ "com.googlecode.iterm2",
+ "dev.warp.Warp-Stable",
+ "dev.warp.Warp-Beta",
+ "com.github.wez.wezterm",
+ "org.alacritty",
+ "io.alacritty", // pre-v0.11.0 (renamed 2022-07) — kept for legacy installs
+ "net.kovidgoyal.kitty",
+ "co.zeit.hyper",
+ "com.mitchellh.ghostty",
+ "org.tabby",
+ "com.termius-dmg.mac", // Termius
+ // IDEs with integrated terminals — we can't distinguish "type in the
+ // editor" from "type in the integrated terminal" via screenshot+click.
+ // VS Code family
+ "com.microsoft.VSCode",
+ "com.microsoft.VSCodeInsiders",
+ "com.vscodium", // VSCodium
+ "com.todesktop.230313mzl4w4u92", // Cursor
+ "com.exafunction.windsurf", // Windsurf / Codeium
+ "dev.zed.Zed",
+ "dev.zed.Zed-Preview",
+ // JetBrains family (all have integrated terminals)
+ "com.jetbrains.intellij",
+ "com.jetbrains.intellij.ce",
+ "com.jetbrains.pycharm",
+ "com.jetbrains.pycharm.ce",
+ "com.jetbrains.WebStorm",
+ "com.jetbrains.CLion",
+ "com.jetbrains.goland",
+ "com.jetbrains.rubymine",
+ "com.jetbrains.PhpStorm",
+ "com.jetbrains.datagrip",
+ "com.jetbrains.rider",
+ "com.jetbrains.AppCode",
+ "com.jetbrains.rustrover",
+ "com.jetbrains.fleet",
+ "com.google.android.studio", // Android Studio (JetBrains-based)
+ // Other IDEs
+ "com.axosoft.gitkraken", // GitKraken has an integrated terminal panel. Also keeps the "kraken" trading-substring from miscategorizing it — bundle-ID wins.
+ "com.sublimetext.4",
+ "com.sublimetext.3",
+ "org.vim.MacVim",
+ "com.neovim.neovim",
+ "org.gnu.Emacs",
+ // Xcode's previous carve-out (full tier for Interface Builder / simulator)
+ // was reversed — at tier "click" IB and simulator taps still work (both are
+ // plain clicks) while the integrated terminal is blocked from keyboard input.
+ "com.apple.dt.Xcode",
+ "org.eclipse.platform.ide",
+ "org.netbeans.ide",
+ "com.microsoft.visual-studio", // Visual Studio for Mac
+ // AppleScript/automation execution surfaces — same threat as terminals:
+ // type(script) → key("cmd+r") runs arbitrary code. Added after #28011
+ // removed the osascript MCP server, making CU the only tool-call route
+ // to AppleScript.
+ "com.apple.ScriptEditor2",
+ "com.apple.Automator",
+ "com.apple.shortcuts",
+]);
+
+/**
+ * Trading / crypto platforms — granted at tier `"read"` so the agent can see
+ * balances and prices but can't click into an order, transfer, or IB chat.
+ * Bundle IDs populated from Homebrew cask `uninstall.quit` stanzas as they're
+ * verified; the name-substring fallback below is the primary check. Bloomberg
+ * Terminal has no native macOS build per their FAQ (web/Citrix only).
+ *
+ * Budgeting/accounting apps (Quicken, YNAB, QuickBooks, etc.) are NOT listed
+ * here — they default to tier `"full"`. The risk model for brokerage/crypto
+ * (a stray click can execute a trade) doesn't apply to budgeting apps; the
+ * Cowork system prompt carries the soft instruction to never execute trades
+ * or transfer money on the user's behalf.
+ */
+const TRADING_BUNDLE_IDS: ReadonlySet = new Set([
+ // Verified via Homebrew quit/zap stanzas + mdls + electron-builder source.
+ // Trading
+ "com.webull.desktop.v1", // Webull (direct download, Qt)
+ "com.webull.trade.mac.v1", // Webull (Mac App Store)
+ "com.tastytrade.desktop",
+ "com.tradingview.tradingviewapp.desktop",
+ "com.fidelity.activetrader", // Fidelity Trader+ (new)
+ "com.fmr.activetrader", // Fidelity Active Trader Pro (legacy)
+ // Interactive Brokers TWS — install4j wrapper; Homebrew quit stanza is
+ // authoritative for this exact value but install4j IDs can drift across
+ // major versions — name-substring "trader workstation" is the fallback.
+ "com.install4j.5889-6375-8446-2021",
+ // Crypto
+ "com.binance.BinanceDesktop",
+ "com.electron.exodus",
+ // Electrum uses PyInstaller with bundle_identifier=None → defaults to
+ // org.pythonmac.unspecified.. Confirmed in spesmilo/electrum
+ // source + Homebrew zap. IntuneBrew's "org.electrum.electrum" is a fork.
+ "org.pythonmac.unspecified.Electrum",
+ "com.ledger.live",
+ "io.trezor.TrezorSuite",
+ // No native macOS app (name-substring only): Schwab, E*TRADE, TradeStation,
+ // Robinhood, NinjaTrader, Coinbase, Kraken, Bloomberg. thinkorswim
+ // install4j ID drifts per-install — substring safer.
+]);
+
+// ─── Policy-deny (not a tier — cannot be granted at all) ─────────────────
+//
+// Streaming / ebook / music apps and a handful of publisher apps. These
+// are auto-denied before the approval dialog — no tier can be granted.
+// Rationale is copyright / content-control (the agent has no legitimate
+// need to screenshot Netflix or click Play on Spotify).
+//
+// Sourced from the ACP CU-apps blocklist xlsx ("Full block" tab). See
+// /tmp/extract_cu_blocklist.py for the extraction script.
+
+const POLICY_DENIED_BUNDLE_IDS: ReadonlySet = new Set([
+ // Verified via Homebrew quit/zap + mdls /System/Applications + IntuneBrew.
+ // Apple built-ins
+ "com.apple.TV",
+ "com.apple.Music",
+ "com.apple.iBooksX",
+ "com.apple.podcasts",
+ // Music
+ "com.spotify.client",
+ "com.amazon.music",
+ "com.tidal.desktop",
+ "com.deezer.deezer-desktop",
+ "com.pandora.desktop",
+ "com.electron.pocket-casts", // direct-download Electron wrapper
+ "au.com.shiftyjelly.PocketCasts", // Mac App Store
+ // Video
+ "tv.plex.desktop",
+ "tv.plex.htpc",
+ "tv.plex.plexamp",
+ "com.amazon.aiv.AIVApp", // Prime Video (iOS-on-Apple-Silicon)
+ // Ebooks
+ "net.kovidgoyal.calibre",
+ "com.amazon.Kindle", // legacy desktop, discontinued
+ "com.amazon.Lassen", // current Mac App Store (iOS-on-Mac)
+ "com.kobo.desktop.Kobo",
+ // No native macOS app (name-substring only): Netflix, Disney+, Hulu,
+ // HBO Max, Peacock, Paramount+, YouTube, Crunchyroll, Tubi, Vudu,
+ // Audible, Reddit, NYTimes. Their iOS apps don't opt into iPad-on-Mac.
+]);
+
+const POLICY_DENIED_NAME_SUBSTRINGS: readonly string[] = [
+ // Video streaming
+ "netflix",
+ "disney+",
+ "hulu",
+ "prime video",
+ "apple tv",
+ "peacock",
+ "paramount+",
+ // "plex" is too generic — would match "Perplexity". Covered by
+ // tv.plex.* bundle IDs on macOS.
+ "tubi",
+ "crunchyroll",
+ "vudu",
+ // E-readers / audiobooks
+ "kindle",
+ "apple books",
+ "kobo",
+ "play books",
+ "calibre",
+ "libby",
+ "readium",
+ "audible",
+ "libro.fm",
+ "speechify",
+ // Music
+ "spotify",
+ "apple music",
+ "amazon music",
+ "youtube music",
+ "tidal",
+ "deezer",
+ "pandora",
+ "pocket casts",
+ // Publisher / social apps (from the same blocklist tab)
+ "naver",
+ "reddit",
+ "sony music",
+ "vegas pro",
+ "pitchfork",
+ "economist",
+ "nytimes",
+ // Skipped (too generic for substring matching — need bundle ID):
+ // HBO Max / Max, YouTube (non-Music), Nook, Sony Catalyst, Wired
+];
+
+/**
+ * Policy-level auto-deny. Unlike `userDeniedBundleIds` (per-user Settings
+ * page), this is baked into the build. `buildAccessRequest` strips these
+ * before the approval dialog with "blocked by policy" guidance; the agent
+ * is told to not retry.
+ */
+export function isPolicyDenied(
+ bundleId: string | undefined,
+ displayName: string,
+): boolean {
+ if (bundleId && POLICY_DENIED_BUNDLE_IDS.has(bundleId)) return true;
+ const lower = displayName.toLowerCase();
+ for (const sub of POLICY_DENIED_NAME_SUBSTRINGS) {
+ if (lower.includes(sub)) return true;
+ }
+ return false;
+}
+
+export function getDeniedCategory(bundleId: string): DeniedCategory | null {
+ if (BROWSER_BUNDLE_IDS.has(bundleId)) return "browser";
+ if (TERMINAL_BUNDLE_IDS.has(bundleId)) return "terminal";
+ if (TRADING_BUNDLE_IDS.has(bundleId)) return "trading";
+ return null;
+}
+
+// ─── Display-name fallback (cross-platform) ──────────────────────────────
+
+/**
+ * Lowercase substrings checked against the requested display name. Catches:
+ * - Unresolved requests (app not installed, Spotlight miss)
+ * - Future Windows/Linux support where bundleId is meaningless
+ *
+ * Matched via `.includes()` on `name.toLowerCase()`. Entries are ordered
+ * by specificity (more-specific first is irrelevant since we return on
+ * first match, but groupings are by category for readability).
+ */
+const BROWSER_NAME_SUBSTRINGS: readonly string[] = [
+ "safari",
+ "chrome",
+ "firefox",
+ "microsoft edge",
+ "brave",
+ "opera",
+ "vivaldi",
+ "chromium",
+ // Arc/Dia: the canonical display name is just "Arc"/"Dia" — too short for
+ // substring matching (false-positives: "Arcade", "Diagram"). Covered by
+ // bundle ID on macOS. The "... browser" entries below catch natural-language
+ // phrasings ("the arc browser") but NOT the canonical short name.
+ "arc browser",
+ "tor browser",
+ "duckduckgo",
+ "yandex",
+ "orion browser",
+ // Agentic / AI browsers
+ "comet", // Perplexity's browser — "Comet" substring risks false positives
+ // but leaving for now; "comet" in an app name is rare
+ "sigmaos",
+ "dia browser",
+];
+
+const TERMINAL_NAME_SUBSTRINGS: readonly string[] = [
+ // macOS / cross-platform terminals
+ "terminal", // catches Terminal, Windows Terminal (NOT iTerm — separate entry)
+ "iterm",
+ "wezterm",
+ "alacritty",
+ "kitty",
+ "ghostty",
+ "tabby",
+ "termius",
+ // AppleScript runners — see bundle-ID comment above. "shortcuts" is too
+ // generic for substring matching (many apps have "shortcuts" in the name);
+ // covered by bundle ID only, like warp/hyper.
+ "script editor",
+ "automator",
+ // NOTE: "warp" and "hyper" are too generic for substring matching —
+ // they'd false-positive on "Warpaint" or "Hyperion". Covered by bundle ID
+ // (dev.warp.Warp-Stable, co.zeit.hyper) for macOS; Windows exe-name
+ // matching can be added when Windows CU ships.
+ // Windows shells (activate when the darwin gate lifts)
+ "powershell",
+ "cmd.exe",
+ "command prompt",
+ "git bash",
+ "conemu",
+ "cmder",
+ // IDEs (VS Code family)
+ "visual studio code",
+ "visual studio", // catches VS for Mac + Windows
+ "vscode",
+ "vs code",
+ "vscodium",
+ "cursor", // Cursor IDE — "cursor" is generic but IDE is the only common app
+ "windsurf",
+ // Zed: display name is just "Zed" — too short for substring matching
+ // (false-positives). Covered by bundle ID (dev.zed.Zed) on macOS.
+ // IDEs (JetBrains family)
+ "intellij",
+ "pycharm",
+ "webstorm",
+ "clion",
+ "goland",
+ "rubymine",
+ "phpstorm",
+ "datagrip",
+ "rider",
+ "appcode",
+ "rustrover",
+ "fleet",
+ "android studio",
+ // Other IDEs
+ "sublime text",
+ "macvim",
+ "neovim",
+ "emacs",
+ "xcode",
+ "eclipse",
+ "netbeans",
+];
+
+const TRADING_NAME_SUBSTRINGS: readonly string[] = [
+ // Trading — brokerage apps. Sourced from the ACP CU-apps blocklist xlsx
+ // ("Read Only" tab). Name-substring safe for proper nouns below; generic
+ // names (IG, Delta, HTX) are skipped and need bundle-ID matching once
+ // verified.
+ "bloomberg",
+ "ameritrade",
+ "thinkorswim",
+ "schwab",
+ "fidelity",
+ "e*trade",
+ "interactive brokers",
+ "trader workstation", // Interactive Brokers TWS
+ "tradestation",
+ "webull",
+ "robinhood",
+ "tastytrade",
+ "ninjatrader",
+ "tradingview",
+ "moomoo",
+ "tradezero",
+ "prorealtime",
+ "plus500",
+ "saxotrader",
+ "oanda",
+ "metatrader",
+ "forex.com",
+ "avaoptions",
+ "ctrader",
+ "jforex",
+ "iq option",
+ "olymp trade",
+ "binomo",
+ "pocket option",
+ "raceoption",
+ "expertoption",
+ "quotex",
+ "naga",
+ "morgan stanley",
+ "ubs neo",
+ "eikon", // Thomson Reuters / LSEG Workspace
+ // Crypto — exchanges, wallets, portfolio trackers
+ "coinbase",
+ "kraken",
+ "binance",
+ "okx",
+ "bybit",
+ // "gate.io" is too generic — the ".io" TLD suffix is common in app names
+ // (e.g., "Draw.io"). Needs bundle-ID matching once verified.
+ "phemex",
+ "stormgain",
+ "crypto.com",
+ // "exodus" is too generic — it's a common noun and would match unrelated
+ // apps/games. Needs bundle-ID matching once verified.
+ "electrum",
+ "ledger live",
+ "trezor",
+ "guarda",
+ "atomic wallet",
+ "bitpay",
+ "bisq",
+ "koinly",
+ "cointracker",
+ "blockfi",
+ "stripe cli",
+ // Crypto games / metaverse (same trade-execution risk model)
+ "decentraland",
+ "axie infinity",
+ "gods unchained",
+];
+
+/**
+ * Display-name substring match. Called when bundle-ID resolution returned
+ * nothing (`resolved === undefined`) or when no bundle-ID deny-list entry
+ * matched. Returns the category for the first matching substring, or null.
+ *
+ * Case-insensitive, substring — so `"Google Chrome"`, `"chrome"`, and
+ * `"Chrome Canary"` all match the `"chrome"` entry.
+ */
+export function getDeniedCategoryByDisplayName(
+ name: string,
+): DeniedCategory | null {
+ const lower = name.toLowerCase();
+ // Trading first — proper-noun-only set, most specific. "Bloomberg Terminal"
+ // contains "terminal" and would miscategorize if TERMINAL_NAME_SUBSTRINGS
+ // ran first.
+ for (const sub of TRADING_NAME_SUBSTRINGS) {
+ if (lower.includes(sub)) return "trading";
+ }
+ for (const sub of BROWSER_NAME_SUBSTRINGS) {
+ if (lower.includes(sub)) return "browser";
+ }
+ for (const sub of TERMINAL_NAME_SUBSTRINGS) {
+ if (lower.includes(sub)) return "terminal";
+ }
+ return null;
+}
+
+/**
+ * Combined check — bundle ID first (exact, fast), then display-name
+ * fallback. This is the function tool-call handlers should use.
+ *
+ * `bundleId` may be undefined (unresolved request — model asked for an app
+ * that isn't installed or Spotlight didn't find). In that case only the
+ * display-name check runs.
+ */
+export function getDeniedCategoryForApp(
+ bundleId: string | undefined,
+ displayName: string,
+): DeniedCategory | null {
+ if (bundleId) {
+ const byId = getDeniedCategory(bundleId);
+ if (byId) return byId;
+ }
+ return getDeniedCategoryByDisplayName(displayName);
+}
+
+/**
+ * Default tier for an app at grant time. Wraps `getDeniedCategoryForApp` +
+ * `categoryToTier`. Browsers → `"read"`, terminals/IDEs → `"click"`,
+ * everything else → `"full"`.
+ *
+ * Called by `buildAccessRequest` to populate `ResolvedAppRequest.proposedTier`
+ * before the approval dialog shows.
+ */
+export function getDefaultTierForApp(
+ bundleId: string | undefined,
+ displayName: string,
+): "read" | "click" | "full" {
+ return categoryToTier(getDeniedCategoryForApp(bundleId, displayName));
+}
+
+export const _test = {
+ BROWSER_BUNDLE_IDS,
+ TERMINAL_BUNDLE_IDS,
+ TRADING_BUNDLE_IDS,
+ POLICY_DENIED_BUNDLE_IDS,
+ BROWSER_NAME_SUBSTRINGS,
+ TERMINAL_NAME_SUBSTRINGS,
+ TRADING_NAME_SUBSTRINGS,
+ POLICY_DENIED_NAME_SUBSTRINGS,
+};
diff --git a/src/vendor/computer-use-mcp/executor.ts b/src/vendor/computer-use-mcp/executor.ts
new file mode 100644
index 00000000..9d7f5f8c
--- /dev/null
+++ b/src/vendor/computer-use-mcp/executor.ts
@@ -0,0 +1,100 @@
+export interface DisplayGeometry {
+ id?: number
+ displayId?: number
+ width: number
+ height: number
+ scaleFactor: number
+ originX: number
+ originY: number
+ isPrimary?: boolean
+ name?: string
+ label?: string
+}
+
+export interface ScreenshotResult {
+ base64: string
+ width: number
+ height: number
+ displayWidth: number
+ displayHeight: number
+ displayId?: number
+ originX: number
+ originY: number
+}
+
+export interface FrontmostApp {
+ bundleId: string
+ displayName: string
+}
+
+export interface RunningApp {
+ bundleId: string
+ displayName: string
+}
+
+export interface InstalledApp {
+ bundleId: string
+ displayName: string
+ path: string
+ iconDataUrl?: string
+}
+
+export interface ResolvePrepareCaptureResult extends ScreenshotResult {
+ hidden: string[]
+ display: DisplayGeometry
+ resolvedDisplayId?: number
+ captureError?: string
+}
+
+export interface ComputerExecutor {
+ capabilities: {
+ screenshotFiltering: 'native' | 'none'
+ platform: 'darwin' | 'win32'
+ hostBundleId: string
+ teachMode?: boolean
+ }
+ prepareForAction(allowlistBundleIds: string[], displayId?: number): Promise
+ previewHideSet(allowlistBundleIds: string[], displayId?: number): Promise>
+ getDisplaySize(displayId?: number): Promise
+ listDisplays(): Promise
+ findWindowDisplays(bundleIds: string[]): Promise>
+ resolvePrepareCapture(opts: {
+ allowedBundleIds: string[]
+ preferredDisplayId?: number
+ autoResolve: boolean
+ doHide?: boolean
+ }): Promise
+ screenshot(opts: { allowedBundleIds: string[]; displayId?: number }): Promise
+ zoom(
+ regionLogical: { x: number; y: number; w: number; h: number },
+ allowedBundleIds: string[],
+ displayId?: number,
+ ): Promise<{ base64: string; width: number; height: number }>
+ key(keySequence: string, repeat?: number): Promise
+ holdKey(keyNames: string[], durationMs: number): Promise
+ type(text: string, opts: { viaClipboard: boolean }): Promise
+ click(
+ x: number,
+ y: number,
+ button: 'left' | 'right' | 'middle',
+ count: 1 | 2 | 3,
+ modifiers?: string[],
+ ): Promise
+ drag(
+ from: { x: number; y: number } | undefined,
+ to: { x: number; y: number },
+ ): Promise
+ moveMouse(x: number, y: number): Promise
+ scroll(x: number, y: number, deltaX: number, deltaY: number): Promise
+ mouseDown(): Promise
+ mouseUp(): Promise
+ getCursorPosition(): Promise<{ x: number; y: number }>
+ getFrontmostApp(): Promise
+ appUnderPoint(x: number, y: number): Promise<{ bundleId: string; displayName: string } | null>
+ listInstalledApps(): Promise
+ getAppIcon?(path: string): Promise
+ listRunningApps(): Promise
+ openApp(bundleId: string): Promise
+ readClipboard(): Promise
+ writeClipboard(text: string): Promise
+}
diff --git a/src/vendor/computer-use-mcp/imageResize.ts b/src/vendor/computer-use-mcp/imageResize.ts
new file mode 100644
index 00000000..fc529714
--- /dev/null
+++ b/src/vendor/computer-use-mcp/imageResize.ts
@@ -0,0 +1,108 @@
+/**
+ * Port of the API's image transcoder target-size algorithm. Pre-sizing
+ * screenshots to this function's output means the API's early-return fires
+ * (tokens ≤ max) and the image is NOT resized server-side — so the model
+ * sees exactly the dimensions in `ScreenshotResult.width/height` and
+ * `scaleCoord` stays coherent.
+ *
+ * Rust reference: api/api/image_transcoder/rust_transcoder/src/utils/resize.rs
+ * Sibling TS port: apps/claude-browser-use/src/utils/imageResize.ts (identical
+ * algorithm, lives in the Chrome extension tree — not a shared package).
+ *
+ * See COORDINATES.md for why this matters for click accuracy.
+ */
+
+export interface ResizeParams {
+ pxPerToken: number;
+ maxTargetPx: number;
+ maxTargetTokens: number;
+}
+
+/**
+ * Production defaults — match `resize.rs:160-164` and Chrome's
+ * `CDPService.ts:638-642`. Vision encoder uses 28px tiles; 1568 is both
+ * the long-edge cap (56 tiles) AND the token budget.
+ */
+export const API_RESIZE_PARAMS: ResizeParams = {
+ pxPerToken: 28,
+ maxTargetPx: 1568,
+ maxTargetTokens: 1568,
+};
+
+/** ceil(px / pxPerToken). Matches resize.rs:74-76 (which uses integer ceil-div). */
+export function nTokensForPx(px: number, pxPerToken: number): number {
+ return Math.floor((px - 1) / pxPerToken) + 1;
+}
+
+function nTokensForImg(
+ width: number,
+ height: number,
+ pxPerToken: number,
+): number {
+ return nTokensForPx(width, pxPerToken) * nTokensForPx(height, pxPerToken);
+}
+
+/**
+ * Binary-search along the width dimension for the largest image that:
+ * - preserves the input aspect ratio
+ * - has long edge ≤ maxTargetPx
+ * - has ceil(w/pxPerToken) × ceil(h/pxPerToken) ≤ maxTargetTokens
+ *
+ * Returns [width, height]. No-op if input already satisfies all three.
+ *
+ * The long-edge constraint alone (what we used to use) is insufficient on
+ * squarer-than-16:9 displays: 1568×1014 (MBP 16" AR) is 56×37 = 2072 tokens,
+ * over budget, and gets server-resized to 1372×887 — model then clicks in
+ * 1372-space but scaleCoord assumed 1568-space → ~14% coord error.
+ *
+ * Matches resize.rs:91-155 exactly (verified against its test vectors).
+ */
+export function targetImageSize(
+ width: number,
+ height: number,
+ params: ResizeParams,
+): [number, number] {
+ const { pxPerToken, maxTargetPx, maxTargetTokens } = params;
+
+ if (
+ width <= maxTargetPx &&
+ height <= maxTargetPx &&
+ nTokensForImg(width, height, pxPerToken) <= maxTargetTokens
+ ) {
+ return [width, height];
+ }
+
+ // Normalize to landscape for the search; transpose result back.
+ if (height > width) {
+ const [w, h] = targetImageSize(height, width, params);
+ return [h, w];
+ }
+
+ const aspectRatio = width / height;
+
+ // Loop invariant: lowerBoundWidth is always valid, upperBoundWidth is
+ // always invalid. ~12 iterations for a 4000px image.
+ let upperBoundWidth = width;
+ let lowerBoundWidth = 1;
+
+ for (;;) {
+ if (lowerBoundWidth + 1 === upperBoundWidth) {
+ return [
+ lowerBoundWidth,
+ Math.max(Math.round(lowerBoundWidth / aspectRatio), 1),
+ ];
+ }
+
+ const middleWidth = Math.floor((lowerBoundWidth + upperBoundWidth) / 2);
+ const middleHeight = Math.max(Math.round(middleWidth / aspectRatio), 1);
+
+ if (
+ middleWidth <= maxTargetPx &&
+ nTokensForImg(middleWidth, middleHeight, pxPerToken) <= maxTargetTokens
+ ) {
+ lowerBoundWidth = middleWidth;
+ } else {
+ upperBoundWidth = middleWidth;
+ }
+ }
+}
diff --git a/src/vendor/computer-use-mcp/index.ts b/src/vendor/computer-use-mcp/index.ts
new file mode 100644
index 00000000..1e012cb2
--- /dev/null
+++ b/src/vendor/computer-use-mcp/index.ts
@@ -0,0 +1,69 @@
+export type {
+ ComputerExecutor,
+ DisplayGeometry,
+ FrontmostApp,
+ InstalledApp,
+ ResolvePrepareCaptureResult,
+ RunningApp,
+ ScreenshotResult,
+} from "./executor.js";
+
+export type {
+ AppGrant,
+ CuAppPermTier,
+ ComputerUseHostAdapter,
+ ComputerUseOverrides,
+ ComputerUseSessionContext,
+ CoordinateMode,
+ CuGrantFlags,
+ CuPermissionRequest,
+ CuPermissionResponse,
+ CuSubGates,
+ CuTeachPermissionRequest,
+ Logger,
+ ResolvedAppRequest,
+ ScreenshotDims,
+ TeachStepRequest,
+ TeachStepResult,
+} from "./types.js";
+
+export { DEFAULT_GRANT_FLAGS } from "./types.js";
+
+export {
+ SENTINEL_BUNDLE_IDS,
+ getSentinelCategory,
+} from "./sentinelApps.js";
+export type { SentinelCategory } from "./sentinelApps.js";
+
+export {
+ categoryToTier,
+ getDefaultTierForApp,
+ getDeniedCategory,
+ getDeniedCategoryByDisplayName,
+ getDeniedCategoryForApp,
+ isPolicyDenied,
+} from "./deniedApps.js";
+export type { DeniedCategory } from "./deniedApps.js";
+
+export { isSystemKeyCombo, normalizeKeySequence } from "./keyBlocklist.js";
+
+export { ALL_SUB_GATES_OFF, ALL_SUB_GATES_ON } from "./subGates.js";
+
+export { API_RESIZE_PARAMS, targetImageSize } from "./imageResize.js";
+export type { ResizeParams } from "./imageResize.js";
+
+export { defersLockAcquire, handleToolCall } from "./toolCalls.js";
+export type {
+ CuCallTelemetry,
+ CuCallToolResult,
+ CuErrorKind,
+} from "./toolCalls.js";
+
+export { bindSessionContext, createComputerUseMcpServer } from "./mcpServer.js";
+export { buildComputerUseTools } from "./tools.js";
+
+export {
+ comparePixelAtLocation,
+ validateClickTarget,
+} from "./pixelCompare.js";
+export type { CropRawPatchFn, PixelCompareResult } from "./pixelCompare.js";
diff --git a/src/vendor/computer-use-mcp/keyBlocklist.ts b/src/vendor/computer-use-mcp/keyBlocklist.ts
new file mode 100644
index 00000000..1373e150
--- /dev/null
+++ b/src/vendor/computer-use-mcp/keyBlocklist.ts
@@ -0,0 +1,153 @@
+/**
+ * Key combos that cross app boundaries or terminate processes. Gated behind
+ * the `systemKeyCombos` grant flag. When that flag is off, the `key` tool
+ * rejects these and returns a tool error telling the model to request the
+ * flag; all other combos work normally.
+ *
+ * Matching is canonicalized: every modifier alias the Rust executor accepts
+ * collapses to one canonical name. Without this, `command+q` / `meta+q` /
+ * `cmd+alt+escape` bypass the gate — see keyBlocklist.test.ts for the three
+ * bypass forms and the Rust parity check that catches future alias drift.
+ */
+
+/**
+ * Every modifier alias enigo_wrap.rs accepts (two copies: :351-359, :564-572),
+ * mapped to one canonical per Key:: variant. Left/right variants collapse —
+ * the blocklist doesn't distinguish which Ctrl.
+ *
+ * Canonical names are Rust's own variant names lowercased. Blocklist entries
+ * below use ONLY these. "meta" reads odd for Cmd+Q but it's honest: Rust
+ * sends Key::Meta, which is Cmd on darwin and Win on win32.
+ */
+const CANONICAL_MODIFIER: Readonly> = {
+ // Key::Meta — "meta"|"super"|"command"|"cmd"|"windows"|"win"
+ meta: "meta",
+ super: "meta",
+ command: "meta",
+ cmd: "meta",
+ windows: "meta",
+ win: "meta",
+ // Key::Control + LControl + RControl
+ ctrl: "ctrl",
+ control: "ctrl",
+ lctrl: "ctrl",
+ lcontrol: "ctrl",
+ rctrl: "ctrl",
+ rcontrol: "ctrl",
+ // Key::Shift + LShift + RShift
+ shift: "shift",
+ lshift: "shift",
+ rshift: "shift",
+ // Key::Alt and Key::Option — distinct Rust variants but same keycode on
+ // darwin (kVK_Option). Collapse: cmd+alt+escape and cmd+option+escape
+ // both Force Quit.
+ alt: "alt",
+ option: "alt",
+};
+
+/** Sort order for canonicals. ctrl < alt < shift < meta. */
+const MODIFIER_ORDER = ["ctrl", "alt", "shift", "meta"];
+
+/**
+ * Canonical-form entries only. Every modifier must be a CANONICAL_MODIFIER
+ * *value* (not key), modifiers must be in MODIFIER_ORDER, non-modifier last.
+ * The self-consistency test enforces this.
+ */
+const BLOCKED_DARWIN = new Set([
+ "meta+q", // Cmd+Q — quit frontmost app
+ "shift+meta+q", // Cmd+Shift+Q — log out
+ "alt+meta+escape", // Cmd+Option+Esc — Force Quit dialog
+ "meta+tab", // Cmd+Tab — app switcher
+ "meta+space", // Cmd+Space — Spotlight
+ "ctrl+meta+q", // Ctrl+Cmd+Q — lock screen
+]);
+
+const BLOCKED_WIN32 = new Set([
+ "ctrl+alt+delete", // Secure Attention Sequence
+ "alt+f4", // close window
+ "alt+tab", // window switcher
+ "meta+l", // Win+L — lock
+ "meta+d", // Win+D — show desktop
+]);
+
+/**
+ * Partition into sorted-canonical modifiers and non-modifier keys.
+ * Shared by normalizeKeySequence (join for display) and isSystemKeyCombo
+ * (check mods+each-key to catch the cmd+q+a suffix bypass).
+ */
+function partitionKeys(seq: string): { mods: string[]; keys: string[] } {
+ const parts = seq
+ .toLowerCase()
+ .split("+")
+ .map((p) => p.trim())
+ .filter(Boolean);
+ const mods: string[] = [];
+ const keys: string[] = [];
+ for (const p of parts) {
+ const canonical = CANONICAL_MODIFIER[p];
+ if (canonical !== undefined) {
+ mods.push(canonical);
+ } else {
+ keys.push(p);
+ }
+ }
+ // Dedupe: "cmd+command+q" → "meta+q", not "meta+meta+q".
+ const uniqueMods = [...new Set(mods)];
+ uniqueMods.sort(
+ (a, b) => MODIFIER_ORDER.indexOf(a) - MODIFIER_ORDER.indexOf(b),
+ );
+ return { mods: uniqueMods, keys };
+}
+
+/**
+ * Normalize "Cmd + Shift + Q" → "shift+meta+q": lowercase, trim, alias →
+ * canonical, dedupe, sort modifiers, non-modifiers last.
+ */
+export function normalizeKeySequence(seq: string): string {
+ const { mods, keys } = partitionKeys(seq);
+ return [...mods, ...keys].join("+");
+}
+
+/**
+ * True if the sequence would fire a blocked OS shortcut.
+ *
+ * Checks mods + EACH non-modifier key individually, not just the full
+ * joined string. `cmd+q+a` → Rust presses Cmd, then Q (Cmd+Q fires here),
+ * then A. Exact-match against "meta+q+a" misses; checking "meta+q" and
+ * "meta+a" separately catches the Q.
+ *
+ * Modifiers-only sequences ("cmd+shift") are checked as-is — no key to
+ * pair with, and no blocklist entry is modifier-only, so this is a no-op
+ * that falls through to false. Covers the click-modifier case where
+ * `left_click(text="cmd")` is legitimate.
+ */
+export function isSystemKeyCombo(
+ seq: string,
+ platform: "darwin" | "win32",
+): boolean {
+ const blocklist = platform === "darwin" ? BLOCKED_DARWIN : BLOCKED_WIN32;
+ const { mods, keys } = partitionKeys(seq);
+ const prefix = mods.length > 0 ? mods.join("+") + "+" : "";
+
+ // No non-modifier keys (e.g. "cmd+shift" as click-modifiers) — check the
+ // whole thing. Never matches (no blocklist entry is modifier-only) but
+ // keeps the contract simple: every call reaches a .has().
+ if (keys.length === 0) {
+ return blocklist.has(mods.join("+"));
+ }
+
+ // mods + each key. Any hit blocks the whole sequence.
+ for (const key of keys) {
+ if (blocklist.has(prefix + key)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export const _test = {
+ CANONICAL_MODIFIER,
+ BLOCKED_DARWIN,
+ BLOCKED_WIN32,
+ MODIFIER_ORDER,
+};
diff --git a/src/vendor/computer-use-mcp/mcpServer.ts b/src/vendor/computer-use-mcp/mcpServer.ts
new file mode 100644
index 00000000..4b1f0ca2
--- /dev/null
+++ b/src/vendor/computer-use-mcp/mcpServer.ts
@@ -0,0 +1,313 @@
+/**
+ * MCP server factory + session-context binder.
+ *
+ * Two entry points:
+ *
+ * `bindSessionContext` — the wrapper closure. Takes a `ComputerUseSessionContext`
+ * (getters + callbacks backed by host session state), returns a dispatcher.
+ * Reusable by both the MCP CallTool handler here AND Cowork's
+ * `InternalServerDefinition.handleToolCall` (which doesn't go through MCP).
+ * This replaces the duplicated wrapper closures in apps/desktop/…/serverDef.ts
+ * and the Claude Code CLI's CU host wrapper — both did the same thing: build `ComputerUseOverrides`
+ * fresh from getters, call `handleToolCall`, stash screenshot, merge permissions.
+ *
+ * `createComputerUseMcpServer` — the Server object. When `context` is provided,
+ * the CallTool handler is real (uses `bindSessionContext`). When not, it's the
+ * legacy stub that returns a not-wired error. The tool-schema ListTools handler
+ * is the same either way.
+ */
+
+import { Server } from "@modelcontextprotocol/sdk/server/index.js";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ CallToolRequestSchema,
+ ListToolsRequestSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+
+import type { ScreenshotResult } from "./executor.js";
+import type { CuCallToolResult } from "./toolCalls.js";
+import {
+ defersLockAcquire,
+ handleToolCall,
+ resetMouseButtonHeld,
+} from "./toolCalls.js";
+import { buildComputerUseTools } from "./tools.js";
+import type {
+ AppGrant,
+ ComputerUseHostAdapter,
+ ComputerUseOverrides,
+ ComputerUseSessionContext,
+ CoordinateMode,
+ CuGrantFlags,
+ CuPermissionResponse,
+} from "./types.js";
+import { DEFAULT_GRANT_FLAGS } from "./types.js";
+
+const DEFAULT_LOCK_HELD_MESSAGE =
+ "Another Claude session is currently using the computer. Wait for that " +
+ "session to finish, or find a non-computer-use approach.";
+
+/**
+ * Dedupe `granted` into `existing` on bundleId, spread truthy-only flags over
+ * defaults+existing. Truthy-only: a subsequent `request_access` that doesn't
+ * request clipboard can't revoke an earlier clipboard grant — revocation lives
+ * in a Settings page, not here.
+ *
+ * Same merge both hosts implemented independently today.
+ */
+function mergePermissionResponse(
+ existing: readonly AppGrant[],
+ existingFlags: CuGrantFlags,
+ response: CuPermissionResponse,
+): { apps: AppGrant[]; flags: CuGrantFlags } {
+ const seen = new Set(existing.map((a) => a.bundleId));
+ const apps = [
+ ...existing,
+ ...response.granted.filter((g) => !seen.has(g.bundleId)),
+ ];
+ const truthyFlags = Object.fromEntries(
+ Object.entries(response.flags).filter(([, v]) => v === true),
+ );
+ const flags: CuGrantFlags = {
+ ...DEFAULT_GRANT_FLAGS,
+ ...existingFlags,
+ ...truthyFlags,
+ };
+ return { apps, flags };
+}
+
+/**
+ * Bind session state to a reusable dispatcher. The returned function is the
+ * wrapper closure: async lock gate → build overrides fresh → `handleToolCall`
+ * → stash screenshot → strip piggybacked fields.
+ *
+ * The last-screenshot blob is held in a closure cell here (not on `ctx`), so
+ * hosts don't need to guarantee `ctx` object identity across calls — they just
+ * need to hold onto the returned dispatcher. Cowork caches per
+ * `InternalServerContext` in a WeakMap; the CLI host constructs once at server creation.
+ */
+export function bindSessionContext(
+ adapter: ComputerUseHostAdapter,
+ coordinateMode: CoordinateMode,
+ ctx: ComputerUseSessionContext,
+): (name: string, args: unknown) => Promise {
+ const { logger, serverName } = adapter;
+
+ // Screenshot blob persists here across calls — NOT on `ctx`. Hosts hold
+ // onto the returned dispatcher; that's the identity that matters.
+ let lastScreenshot: ScreenshotResult | undefined;
+
+ const wrapPermission = ctx.onPermissionRequest
+ ? async (
+ req: Parameters>[0],
+ signal: AbortSignal,
+ ): Promise => {
+ const response = await ctx.onPermissionRequest!(req, signal);
+ const { apps, flags } = mergePermissionResponse(
+ ctx.getAllowedApps(),
+ ctx.getGrantFlags(),
+ response,
+ );
+ logger.debug(
+ `[${serverName}] permission result: granted=${response.granted.length} denied=${response.denied.length}`,
+ );
+ ctx.onAllowedAppsChanged?.(apps, flags);
+ return response;
+ }
+ : undefined;
+
+ const wrapTeachPermission = ctx.onTeachPermissionRequest
+ ? async (
+ req: Parameters>[0],
+ signal: AbortSignal,
+ ): Promise => {
+ const response = await ctx.onTeachPermissionRequest!(req, signal);
+ logger.debug(
+ `[${serverName}] teach permission result: granted=${response.granted.length} denied=${response.denied.length}`,
+ );
+ // Teach doesn't request grant flags — preserve existing.
+ const { apps } = mergePermissionResponse(
+ ctx.getAllowedApps(),
+ ctx.getGrantFlags(),
+ response,
+ );
+ ctx.onAllowedAppsChanged?.(apps, {
+ ...DEFAULT_GRANT_FLAGS,
+ ...ctx.getGrantFlags(),
+ });
+ return response;
+ }
+ : undefined;
+
+ return async (name, args) => {
+ // ─── Async lock gate ─────────────────────────────────────────────────
+ // Replaces the sync Gate-3 in `handleToolCall` — we pass
+ // `checkCuLock: undefined` below so it no-ops. Hosts with
+ // cross-process locks (O_EXCL file) await the real primitive here
+ // instead of pre-computing + feeding a fake sync result.
+ if (ctx.checkCuLock) {
+ const lock = await ctx.checkCuLock();
+ if (lock.holder !== undefined && !lock.isSelf) {
+ const text =
+ ctx.formatLockHeldMessage?.(lock.holder) ?? DEFAULT_LOCK_HELD_MESSAGE;
+ return {
+ content: [{ type: "text", text }],
+ isError: true,
+ telemetry: { error_kind: "cu_lock_held" },
+ };
+ }
+ if (lock.holder === undefined && !defersLockAcquire(name)) {
+ await ctx.acquireCuLock?.();
+ // Re-check: the awaits above yield the microtask queue, so another
+ // session's check+acquire can interleave with ours. Hosts where
+ // acquire is a no-op when already held (Cowork's CuLockManager) give
+ // no signal that we lost — verify we're now the holder before
+ // proceeding. The CLI's O_EXCL file lock would surface this as a throw from
+ // acquire instead; this re-check is a belt-and-suspenders for that
+ // path too.
+ const recheck = await ctx.checkCuLock();
+ if (recheck.holder !== undefined && !recheck.isSelf) {
+ const text =
+ ctx.formatLockHeldMessage?.(recheck.holder) ??
+ DEFAULT_LOCK_HELD_MESSAGE;
+ return {
+ content: [{ type: "text", text }],
+ isError: true,
+ telemetry: { error_kind: "cu_lock_held" },
+ };
+ }
+ // Fresh holder → any prior session's mouseButtonHeld is stale.
+ // Mirrors what Gate-3 does on the acquire branch. After the
+ // re-check so we only clear module state when we actually won.
+ resetMouseButtonHeld();
+ }
+ }
+
+ // ─── Build overrides fresh ───────────────────────────────────────────
+ // Blob-first; dims-fallback with base64:"" when the closure cell is
+ // unset (cross-respawn). scaleCoord reads dims; pixelCompare sees "" →
+ // isEmpty → skip.
+ const dimsFallback = lastScreenshot
+ ? undefined
+ : ctx.getLastScreenshotDims?.();
+
+ // Per-call AbortController for dialog dismissal. Aborted in `finally` —
+ // if handleToolCall finishes (MCP timeout, throw) before the user
+ // answers, the host's dialog handler sees the abort and tears down.
+ const dialogAbort = new AbortController();
+
+ const overrides: ComputerUseOverrides = {
+ allowedApps: [...ctx.getAllowedApps()],
+ grantFlags: ctx.getGrantFlags(),
+ userDeniedBundleIds: ctx.getUserDeniedBundleIds(),
+ coordinateMode,
+ selectedDisplayId: ctx.getSelectedDisplayId(),
+ displayPinnedByModel: ctx.getDisplayPinnedByModel?.(),
+ displayResolvedForApps: ctx.getDisplayResolvedForApps?.(),
+ lastScreenshot:
+ lastScreenshot ??
+ (dimsFallback ? { ...dimsFallback, base64: "" } : undefined),
+ onPermissionRequest: wrapPermission
+ ? (req) => wrapPermission(req, dialogAbort.signal)
+ : undefined,
+ onTeachPermissionRequest: wrapTeachPermission
+ ? (req) => wrapTeachPermission(req, dialogAbort.signal)
+ : undefined,
+ onAppsHidden: ctx.onAppsHidden,
+ getClipboardStash: ctx.getClipboardStash,
+ onClipboardStashChanged: ctx.onClipboardStashChanged,
+ onResolvedDisplayUpdated: ctx.onResolvedDisplayUpdated,
+ onDisplayPinned: ctx.onDisplayPinned,
+ onDisplayResolvedForApps: ctx.onDisplayResolvedForApps,
+ onTeachModeActivated: ctx.onTeachModeActivated,
+ onTeachStep: ctx.onTeachStep,
+ onTeachWorking: ctx.onTeachWorking,
+ getTeachModeActive: ctx.getTeachModeActive,
+ // Undefined → handleToolCall's sync Gate-3 no-ops. The async gate
+ // above already ran.
+ checkCuLock: undefined,
+ acquireCuLock: undefined,
+ isAborted: ctx.isAborted,
+ };
+
+ logger.debug(
+ `[${serverName}] tool=${name} allowedApps=${overrides.allowedApps.length} coordMode=${coordinateMode}`,
+ );
+
+ // ─── Dispatch ────────────────────────────────────────────────────────
+ try {
+ const result = await handleToolCall(adapter, name, args, overrides);
+
+ if (result.screenshot) {
+ lastScreenshot = result.screenshot;
+ const { base64: _blob, ...dims } = result.screenshot;
+ logger.debug(`[${serverName}] screenshot dims: ${JSON.stringify(dims)}`);
+ ctx.onScreenshotCaptured?.(dims);
+ }
+
+ return result;
+ } finally {
+ dialogAbort.abort();
+ }
+ };
+}
+
+export function createComputerUseMcpServer(
+ adapter: ComputerUseHostAdapter,
+ coordinateMode: CoordinateMode,
+ context?: ComputerUseSessionContext,
+): Server {
+ const { serverName, logger } = adapter;
+
+ const server = new Server(
+ { name: serverName, version: "0.1.3" },
+ { capabilities: { tools: {}, logging: {} } },
+ );
+
+ const tools = buildComputerUseTools(
+ adapter.executor.capabilities,
+ coordinateMode,
+ );
+
+ server.setRequestHandler(ListToolsRequestSchema, async () =>
+ adapter.isDisabled() ? { tools: [] } : { tools },
+ );
+
+ if (context) {
+ const dispatch = bindSessionContext(adapter, coordinateMode, context);
+ server.setRequestHandler(
+ CallToolRequestSchema,
+ async (request): Promise => {
+ const { screenshot: _s, telemetry: _t, ...result } = await dispatch(
+ request.params.name,
+ request.params.arguments ?? {},
+ );
+ return result;
+ },
+ );
+ return server;
+ }
+
+ // Legacy: no context → stub handler. Reached only if something calls the
+ // server over MCP transport WITHOUT going through a binder (a wiring
+ // regression). Clear error instead of silent failure.
+ server.setRequestHandler(
+ CallToolRequestSchema,
+ async (request): Promise => {
+ logger.warn(
+ `[${serverName}] tool call "${request.params.name}" reached the stub handler — no session context bound. Per-session state unavailable.`,
+ );
+ return {
+ content: [
+ {
+ type: "text",
+ text: "This computer-use server instance is not wired to a session. Per-session app permissions are not available on this code path.",
+ },
+ ],
+ isError: true,
+ };
+ },
+ );
+
+ return server;
+}
diff --git a/src/vendor/computer-use-mcp/pixelCompare.ts b/src/vendor/computer-use-mcp/pixelCompare.ts
new file mode 100644
index 00000000..05153f60
--- /dev/null
+++ b/src/vendor/computer-use-mcp/pixelCompare.ts
@@ -0,0 +1,171 @@
+/**
+ * Staleness guard ported from the Vercept acquisition.
+ *
+ * Compares the model's last-seen screenshot against a fresh-right-now
+ * screenshot at the click target, so the model never clicks pixels it hasn't
+ * seen. If the 9×9 patch around the target differs, the click is aborted and
+ * the model is told to re-screenshot. This is NOT a popup detector.
+ *
+ * Semantics preserved exactly:
+ * - Skip on no `lastScreenshot` (cold start) — click proceeds.
+ * - Skip on any internal error (crop throws, screenshot fails, etc.) —
+ * click proceeds. Validation failure must never block the action.
+ * - 9×9 exact byte equality on raw pixel bytes. No fuzzing, no tolerance.
+ * - Compare in percentage coords so Retina scale doesn't matter.
+ *
+ * JPEG decode + crop is INJECTED via `ComputerUseHostAdapter.cropRawPatch`.
+ * The original used `sharp` (LGPL, native `.node` addon); we inject Electron's
+ * `nativeImage` (Chromium decoders, BSD, nothing to bundle) from the host, so
+ * this package never imports it — the crop is a function parameter.
+ */
+
+import type { ScreenshotResult } from "./executor.js";
+import type { Logger } from "./types.js";
+
+/** Injected by the host. See `ComputerUseHostAdapter.cropRawPatch`. */
+export type CropRawPatchFn = (
+ jpegBase64: string,
+ rect: { x: number; y: number; width: number; height: number },
+) => Buffer | null;
+
+/** 9×9 is empirically the sweet spot — large enough to catch a tooltip
+ * appearing, small enough to not false-positive on surrounding animation.
+ **/
+const DEFAULT_GRID_SIZE = 9;
+
+export interface PixelCompareResult {
+ /** true → click may proceed. false → patch changed, abort the click. */
+ valid: boolean;
+ /** true → validation did not run (cold start, sub-gate off, or internal
+ * error). The caller MUST treat this identically to `valid: true`. */
+ skipped: boolean;
+ /** Populated when valid === false. Returned to the model verbatim. */
+ warning?: string;
+}
+
+/**
+ * Compute the crop rect for a patch centered on (xPercent, yPercent).
+ *
+ * Dimensions come from ScreenshotResult.width/height (physical pixels). Both
+ * screenshots have the same dimensions (same display, consecutive captures),
+ * so the rect is the same for both.
+ */
+function computeCropRect(
+ imgW: number,
+ imgH: number,
+ xPercent: number,
+ yPercent: number,
+ gridSize: number,
+): { x: number; y: number; width: number; height: number } | null {
+ if (!imgW || !imgH) return null;
+
+ const clampedX = Math.max(0, Math.min(100, xPercent));
+ const clampedY = Math.max(0, Math.min(100, yPercent));
+
+ const centerX = Math.round((clampedX / 100.0) * imgW);
+ const centerY = Math.round((clampedY / 100.0) * imgH);
+
+ const halfGrid = Math.floor(gridSize / 2);
+ const cropX = Math.max(0, centerX - halfGrid);
+ const cropY = Math.max(0, centerY - halfGrid);
+ const cropW = Math.min(gridSize, imgW - cropX);
+ const cropH = Math.min(gridSize, imgH - cropY);
+ if (cropW <= 0 || cropH <= 0) return null;
+
+ return { x: cropX, y: cropY, width: cropW, height: cropH };
+}
+
+/**
+ * Compare the same patch location between two screenshots.
+ *
+ * @returns true when the raw pixel bytes are identical. false on any
+ * difference, or on any internal error (the caller treats an error here as
+ * `skipped`, so the false is harmless).
+ */
+export function comparePixelAtLocation(
+ crop: CropRawPatchFn,
+ lastScreenshot: ScreenshotResult,
+ freshScreenshot: ScreenshotResult,
+ xPercent: number,
+ yPercent: number,
+ gridSize: number = DEFAULT_GRID_SIZE,
+): boolean {
+ // Both screenshots are of the same display — use the fresh one's
+ // dimensions (less likely to be stale than last's).
+ const rect = computeCropRect(
+ freshScreenshot.width,
+ freshScreenshot.height,
+ xPercent,
+ yPercent,
+ gridSize,
+ );
+ if (!rect) return false;
+
+ const patch1 = crop(lastScreenshot.base64, rect);
+ const patch2 = crop(freshScreenshot.base64, rect);
+ if (!patch1 || !patch2) return false;
+
+ // Direct buffer equality. Note: nativeImage.toBitmap() gives BGRA, sharp's
+ // .raw() gave RGB.
+ // Doesn't matter — we're comparing two same-format buffers for equality.
+ return patch1.equals(patch2);
+}
+
+/**
+ * Battle-tested click-target validation ported from the Vercept acquisition,
+ * with the fresh-screenshot capture delegated to the caller (we don't have
+ * a global `SystemActions.takeScreenshot()` — the executor is injected).
+ *
+ * Skip conditions (any of these → `{ valid: true, skipped: true }`):
+ * - `lastScreenshot` is undefined (cold start).
+ * - `takeFreshScreenshot()` throws or returns null.
+ * - Injected crop function returns null (decode failure).
+ * - Any other exception.
+ *
+ * The caller decides whether to invoke this at all (sub-gate check lives
+ * in toolCalls.ts, not here).
+ */
+export async function validateClickTarget(
+ crop: CropRawPatchFn,
+ lastScreenshot: ScreenshotResult | undefined,
+ xPercent: number,
+ yPercent: number,
+ takeFreshScreenshot: () => Promise,
+ logger: Logger,
+ gridSize: number = DEFAULT_GRID_SIZE,
+): Promise {
+ if (!lastScreenshot) {
+ return { valid: true, skipped: true };
+ }
+
+ try {
+ const fresh = await takeFreshScreenshot();
+ if (!fresh) {
+ return { valid: true, skipped: true };
+ }
+
+ const pixelsMatch = comparePixelAtLocation(
+ crop,
+ lastScreenshot,
+ fresh,
+ xPercent,
+ yPercent,
+ gridSize,
+ );
+
+ if (pixelsMatch) {
+ return { valid: true, skipped: false };
+ }
+ return {
+ valid: false,
+ skipped: false,
+ warning:
+ "Screen content at the target location changed since the last screenshot. Take a new screenshot before clicking.",
+ };
+ } catch (err) {
+ // Skip validation on technical errors, execute action anyway.
+ // Battle-tested: validation failure must never block the click.
+ logger.debug("[pixelCompare] validation error, skipping", err);
+ return { valid: true, skipped: true };
+ }
+}
diff --git a/src/vendor/computer-use-mcp/sentinelApps.ts b/src/vendor/computer-use-mcp/sentinelApps.ts
new file mode 100644
index 00000000..0d26de60
--- /dev/null
+++ b/src/vendor/computer-use-mcp/sentinelApps.ts
@@ -0,0 +1,43 @@
+/**
+ * Bundle IDs that are escalations-in-disguise. The approval UI shows a warning
+ * badge for these; they are NOT blocked. Power users may legitimately want the
+ * model controlling a terminal.
+ *
+ * Imported by the renderer via the `./sentinelApps` subpath (package.json
+ * `exports`), which keeps Next.js from reaching index.ts → mcpServer.ts →
+ * @modelcontextprotocol/sdk (devDep, would fail module resolution). Keep
+ * this file import-free so the subpath stays clean.
+ */
+
+/** These apps can execute arbitrary shell commands. */
+const SHELL_ACCESS_BUNDLE_IDS = new Set([
+ "com.apple.Terminal",
+ "com.googlecode.iterm2",
+ "com.microsoft.VSCode",
+ "dev.warp.Warp-Stable",
+ "com.github.wez.wezterm",
+ "io.alacritty",
+ "net.kovidgoyal.kitty",
+ "com.jetbrains.intellij",
+ "com.jetbrains.pycharm",
+]);
+
+/** Finder in the allowlist ≈ browse + open-any-file. */
+const FILESYSTEM_ACCESS_BUNDLE_IDS = new Set(["com.apple.finder"]);
+
+const SYSTEM_SETTINGS_BUNDLE_IDS = new Set(["com.apple.systempreferences"]);
+
+export const SENTINEL_BUNDLE_IDS: ReadonlySet = new Set([
+ ...SHELL_ACCESS_BUNDLE_IDS,
+ ...FILESYSTEM_ACCESS_BUNDLE_IDS,
+ ...SYSTEM_SETTINGS_BUNDLE_IDS,
+]);
+
+export type SentinelCategory = "shell" | "filesystem" | "system_settings";
+
+export function getSentinelCategory(bundleId: string): SentinelCategory | null {
+ if (SHELL_ACCESS_BUNDLE_IDS.has(bundleId)) return "shell";
+ if (FILESYSTEM_ACCESS_BUNDLE_IDS.has(bundleId)) return "filesystem";
+ if (SYSTEM_SETTINGS_BUNDLE_IDS.has(bundleId)) return "system_settings";
+ return null;
+}
diff --git a/src/vendor/computer-use-mcp/subGates.ts b/src/vendor/computer-use-mcp/subGates.ts
new file mode 100644
index 00000000..c2cacdef
--- /dev/null
+++ b/src/vendor/computer-use-mcp/subGates.ts
@@ -0,0 +1,19 @@
+import type { CuSubGates } from './types.js'
+
+export const ALL_SUB_GATES_ON: CuSubGates = {
+ pixelValidation: false,
+ clipboardPasteMultiline: true,
+ mouseAnimation: true,
+ hideBeforeAction: true,
+ autoTargetDisplay: true,
+ clipboardGuard: true,
+}
+
+export const ALL_SUB_GATES_OFF: CuSubGates = {
+ pixelValidation: false,
+ clipboardPasteMultiline: false,
+ mouseAnimation: false,
+ hideBeforeAction: false,
+ autoTargetDisplay: false,
+ clipboardGuard: false,
+}
diff --git a/src/vendor/computer-use-mcp/toolCalls.ts b/src/vendor/computer-use-mcp/toolCalls.ts
new file mode 100644
index 00000000..1c982e00
--- /dev/null
+++ b/src/vendor/computer-use-mcp/toolCalls.ts
@@ -0,0 +1,3652 @@
+/**
+ * Tool dispatch. Every security decision from plan §2 is enforced HERE,
+ * before any executor method is called.
+ *
+ * Enforcement order, every call:
+ * 1. Kill switch (`adapter.isDisabled()`).
+ * 2. TCC gate (`adapter.ensureOsPermissions()`). `request_access` is
+ * exempted — it threads the ungranted state to the renderer so the
+ * user can grant TCC perms from inside the approval dialog.
+ * 3. Tool-specific gates (see dispatch table) — ANY exception in a gate
+ * returns a tool error, executor never called.
+ * 4. Executor call.
+ *
+ * For input actions (click/type/key/scroll/drag/move_mouse) the tool-specific
+ * gates are, in order:
+ * a. `prepareForAction` — hide every non-allowlisted app, then defocus us
+ * (battle-tested pre-action sequence from the Vercept acquisition).
+ * Sub-gated via `hideBeforeAction`. After this runs the screenshot is
+ * TRUE (what the
+ * model sees IS what's at each pixel) and we are not keyboard-focused.
+ * b. Frontmost gate — branched by actionKind:
+ * mouse: frontmost ∈ allowlist ∪ {hostBundleId, Finder} → pass.
+ * hostBundleId passes because the executor's
+ * `withClickThrough` bracket makes us click-through.
+ * keyboard: frontmost ∈ allowlist ∪ {Finder} → pass.
+ * hostBundleId → ERROR (safety net — defocus should have
+ * moved us off; if it didn't, typing would go into our
+ * own chat box).
+ * After step (a) this gate fires RARELY — only when something popped
+ * up between prepare and action, or the 5-try hide loop gave up.
+ * Checked FRESH on every call, not cached across calls.
+ *
+ * For click variants only, AFTER the above gates but BEFORE the executor call:
+ * c. Pixel-validation staleness check (sub-gated).
+ */
+
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { randomUUID } from "node:crypto";
+
+import { getDefaultTierForApp, getDeniedCategoryForApp, isPolicyDenied } from "./deniedApps.js";
+import type {
+ ComputerExecutor,
+ DisplayGeometry,
+ InstalledApp,
+ ScreenshotResult,
+} from "./executor.js";
+import { isSystemKeyCombo } from "./keyBlocklist.js";
+import { validateClickTarget } from "./pixelCompare.js";
+import { SENTINEL_BUNDLE_IDS } from "./sentinelApps.js";
+import type {
+ AppGrant,
+ ComputerUseHostAdapter,
+ ComputerUseOverrides,
+ CoordinateMode,
+ CuAppPermTier,
+ CuGrantFlags,
+ CuPermissionRequest,
+ CuSubGates,
+ CuTeachPermissionRequest,
+ Logger,
+ ResolvedAppRequest,
+ TeachStepRequest,
+} from "./types.js";
+
+/**
+ * Finder is never hidden by the hide loop (hiding Finder kills the Desktop),
+ * so it's always a valid frontmost.
+ */
+const FINDER_BUNDLE_ID = "com.apple.finder";
+
+/**
+ * Categorical error classes for the cu_tool_call telemetry event. Never
+ * free text — error messages may contain file paths / app content (PII).
+ */
+export type CuErrorKind =
+ | "allowlist_empty"
+ | "tcc_not_granted"
+ | "cu_lock_held"
+ | "teach_mode_conflict"
+ | "teach_mode_not_active"
+ | "executor_threw"
+ | "capture_failed"
+ | "app_denied" // no longer emitted (tiered model replaced hard-deny); kept for schema compat
+ | "bad_args" // malformed tool args (type/shape/range/unknown value)
+ | "app_not_granted" // target app not in session allowlist (distinct from allowlist_empty)
+ | "tier_insufficient" // app in allowlist but at a tier too low for the action
+ | "feature_unavailable" // tool callable but session not wired for it
+ | "state_conflict" // wrong state for action (call sequence, mouse already held)
+ | "grant_flag_required" // action needs a grant flag (systemKeyCombos, clipboard*) from request_access
+ | "display_error" // display enumeration failed (platform)
+ | "other";
+
+/**
+ * Telemetry payload piggybacked on the result — populated by handlers,
+ * consumed and stripped by the host wrapper (serverDef.ts) before the
+ * result goes to the SDK. Same pattern as `screenshot`.
+ */
+export interface CuCallTelemetry {
+ /** request_access / request_teach_access: apps NEWLY granted in THIS call
+ * (does NOT include idempotent re-grants of already-allowed apps). */
+ granted_count?: number;
+ /** request_access / request_teach_access: apps denied in THIS call */
+ denied_count?: number;
+ /** request_access / request_teach_access: apps safety-denied (browser) this call */
+ denied_browser_count?: number;
+ /** request_access / request_teach_access: apps safety-denied (terminal) this call */
+ denied_terminal_count?: number;
+ /** Categorical error class (only set when isError) */
+ error_kind?: CuErrorKind;
+}
+
+/**
+ * `CallToolResult` augmented with the screenshot payload. `bindSessionContext`
+ * reads `result.screenshot` after a `screenshot` tool call and stashes it in a
+ * closure cell for the next pixel-validation. MCP clients never see this
+ * field — the host wrapper strips it before returning to the SDK.
+ */
+export type CuCallToolResult = CallToolResult & {
+ screenshot?: ScreenshotResult;
+ /** Piggybacked telemetry — stripped by the host wrapper before SDK return. */
+ telemetry?: CuCallTelemetry;
+};
+
+// ---------------------------------------------------------------------------
+// Small result helpers (mirror of chrome-mcp's inline `{content, isError}`)
+// ---------------------------------------------------------------------------
+
+function errorResult(text: string, errorKind?: CuErrorKind): CuCallToolResult {
+ return {
+ content: [{ type: "text", text }],
+ isError: true,
+ telemetry: errorKind ? { error_kind: errorKind } : undefined,
+ };
+}
+
+function okText(text: string): CuCallToolResult {
+ return { content: [{ type: "text", text }] };
+}
+
+function okJson(obj: unknown, telemetry?: CuCallTelemetry): CuCallToolResult {
+ return {
+ content: [{ type: "text", text: JSON.stringify(obj) }],
+ telemetry,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Arg validation — lightweight, no zod (mirrors chrome-mcp's cast-and-check)
+// ---------------------------------------------------------------------------
+
+function asRecord(args: unknown): Record {
+ if (typeof args === "object" && args !== null) {
+ return args as Record;
+ }
+ return {};
+}
+
+function requireNumber(
+ args: Record,
+ key: string,
+): number | Error {
+ const v = args[key];
+ if (typeof v !== "number" || !Number.isFinite(v)) {
+ return new Error(`"${key}" must be a finite number.`);
+ }
+ return v;
+}
+
+function requireString(
+ args: Record,
+ key: string,
+): string | Error {
+ const v = args[key];
+ if (typeof v !== "string") {
+ return new Error(`"${key}" must be a string.`);
+ }
+ return v;
+}
+
+/**
+ * Extract (x, y) from `coordinate: [x, y]` tuple.
+ * array of length 2, both non-negative numbers.
+ */
+function extractCoordinate(
+ args: Record,
+ paramName: string = "coordinate",
+): [number, number] | Error {
+ const coord = args[paramName];
+ if (coord === undefined) {
+ return new Error(`${paramName} is required`);
+ }
+ if (!Array.isArray(coord) || coord.length !== 2) {
+ return new Error(`${paramName} must be an array of length 2`);
+ }
+ const [x, y] = coord;
+ if (typeof x !== "number" || typeof y !== "number" || x < 0 || y < 0) {
+ return new Error(`${paramName} must be a tuple of non-negative numbers`);
+ }
+ return [x, y];
+}
+
+// ---------------------------------------------------------------------------
+// Coordinate scaling
+// ---------------------------------------------------------------------------
+
+/**
+ * Convert model-space coordinates to the logical points that enigo expects.
+ *
+ * - `normalized_0_100`: (x / 100) * display.width. `display` is fetched
+ * fresh per tool call — never cached across calls —
+ * so a mid-session display-settings change doesn't leave us stale.
+ * - `pixels`: the model sent image-space pixel coords (it read them off the
+ * last screenshot). With the 1568-px long-edge downsample, the
+ * screenshot-px → logical-pt ratio is `displayWidth / screenshotWidth`,
+ * NOT `1/scaleFactor`. Uses the display geometry stashed at CAPTURE time
+ * (`lastScreenshot.displayWidth`), not fresh — so the transform matches
+ * what the model actually saw even if the user changed display settings
+ * since. (Chrome's ScreenshotContext pattern — CDPService.ts:1486-1493.)
+ */
+function scaleCoord(
+ rawX: number,
+ rawY: number,
+ mode: CoordinateMode,
+ display: DisplayGeometry,
+ lastScreenshot: ScreenshotResult | undefined,
+ logger: Logger,
+): { x: number; y: number } {
+ if (mode === "normalized_0_100") {
+ // Origin offset targets the selected display in virtual-screen space.
+ return {
+ x: Math.round((rawX / 100) * display.width) + display.originX,
+ y: Math.round((rawY / 100) * display.height) + display.originY,
+ };
+ }
+
+ // mode === "pixels": model sent image-space pixel coords.
+ if (lastScreenshot) {
+ // The transform. Chrome coordinateScaling.ts:22-34 + claude-in-a-box
+ // ComputerTool.swift:70-80 — two independent convergent impls.
+ // Uses the display geometry stashed AT CAPTURE TIME, not fresh.
+ // Origin from the same snapshot keeps clicks coherent with the captured display.
+ return {
+ x:
+ Math.round(
+ rawX * (lastScreenshot.displayWidth / lastScreenshot.width),
+ ) + lastScreenshot.originX,
+ y:
+ Math.round(
+ rawY * (lastScreenshot.displayHeight / lastScreenshot.height),
+ ) + lastScreenshot.originY,
+ };
+ }
+
+ // Cold start: model sent pixel coords without having taken a screenshot.
+ // Degenerate — fall back to the old /sf behavior and warn.
+ logger.warn(
+ "[computer-use] pixels-mode coordinate received with no prior screenshot; " +
+ "falling back to /scaleFactor. Click may be off if downsample is active.",
+ );
+ return {
+ x: Math.round(rawX / display.scaleFactor) + display.originX,
+ y: Math.round(rawY / display.scaleFactor) + display.originY,
+ };
+}
+
+/**
+ * Convert model-space coordinates to the 0–100 percentage that
+ * pixelCompare.ts works in. The staleness check operates in screenshot-image
+ * space; comparing by percentage lets us crop both last and fresh screenshots
+ * at the same relative location without caring about their absolute dims.
+ *
+ * With the 1568-px downsample, `screenshot.width != display.width * sf`, so
+ * the old `rawX / (display.width * sf)` formula is wrong. The correct
+ * denominator is just `lastScreenshot.width` — the model's raw pixel coord is
+ * already in that image's coordinate space. `DisplayGeometry` is no longer
+ * consumed at all.
+ */
+function coordToPercentageForPixelCompare(
+ rawX: number,
+ rawY: number,
+ mode: CoordinateMode,
+ lastScreenshot: ScreenshotResult | undefined,
+): { xPct: number; yPct: number } {
+ if (mode === "normalized_0_100") {
+ // Unchanged — already a percentage.
+ return { xPct: rawX, yPct: rawY };
+ }
+
+ // mode === "pixels"
+ if (!lastScreenshot) {
+ // validateClickTarget at pixelCompare.ts:141-143 already skips when
+ // lastScreenshot is undefined, so this return value never reaches a crop.
+ return { xPct: 0, yPct: 0 };
+ }
+ return {
+ xPct: (rawX / lastScreenshot.width) * 100,
+ yPct: (rawY / lastScreenshot.height) * 100,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Shared input-action gates
+// ---------------------------------------------------------------------------
+
+/**
+ * Tier needed to perform a given action class. `undefined` → `"full"`.
+ *
+ * - `"mouse_position"` — mouse_move only. Passes at any tier including
+ * `"read"`. Pure cursor positioning, no app interaction. Still runs
+ * prepareForAction (hide non-allowed apps).
+ * - `"mouse"` — plain left click, double/triple, scroll, drag-from.
+ * Requires tier `"click"` or `"full"`.
+ * - `"mouse_full"` — right/middle click, any click with modifiers,
+ * drag-drop (the `to` endpoint of left_click_drag). Requires tier
+ * `"full"`. Right-click → context menu Paste, modifier chords →
+ * keystrokes before click, drag-drop → text insertion at the drop
+ * point. All escalate a click-tier grant to keyboard-equivalent input.
+ * Blunt: also rejects same-app drags (scrollbar, panel resize) onto
+ * click-tier apps; `scroll` is the tier-"click" way to scroll.
+ * - `"keyboard"` — type, key, hold_key. Requires tier `"full"`.
+ */
+type CuActionKind = "mouse_position" | "mouse" | "mouse_full" | "keyboard";
+
+function tierSatisfies(
+ grantTier: CuAppPermTier | undefined,
+ actionKind: CuActionKind,
+): boolean {
+ const tier = grantTier ?? "full";
+ if (actionKind === "mouse_position") return true;
+ if (actionKind === "keyboard" || actionKind === "mouse_full") {
+ return tier === "full";
+ }
+ // mouse
+ return tier === "click" || tier === "full";
+}
+
+// Appended to every tier_insufficient error. The model may try to route
+// around the gate (osascript, System Events, cliclick via Bash) — this
+// closes that door explicitly. Leading space so it concatenates cleanly.
+const TIER_ANTI_SUBVERSION =
+ " Do not attempt to work around this restriction — never use AppleScript, " +
+ "System Events, shell commands, or any other method to send clicks or " +
+ "keystrokes to this app.";
+
+// ---------------------------------------------------------------------------
+// Clipboard guard — stash+clear while a click-tier app is frontmost
+// ---------------------------------------------------------------------------
+//
+// Threat: tier "click" blocks type/key/right-click-Paste, but a click-tier
+// terminal/IDE may have a UI Paste button that's plain-left-clickable. If the
+// clipboard holds `rm -rf /` — from the user, from a prior full-tier paste,
+// OR from the agent's own write_clipboard call (which doesn't route through
+// runInputActionGates) — a left_click on that button injects it.
+//
+// Mitigation: stash the user's clipboard on first entry to click-tier, then
+// RE-CLEAR before every input action while click-tier stays frontmost. The
+// re-clear is the load-bearing part — a stash-on-transition-only design
+// leaves a gap between an agent write_clipboard and the next left_click.
+// When frontmost becomes anything else, restore. Turn-end restore is inlined
+// in the host's result-handler + leavingRunning (same dual-location as
+// cuHiddenDuringTurn unhide) — reads `session.cuClipboardStash` directly and
+// writes via Electron's `clipboard.writeText`, so no nest-only import.
+//
+// State lives on the session (via `overrides.getClipboardStash` /
+// `onClipboardStashChanged`), not module-level. The CU lock still guarantees
+// one session at a time, but session-scoped state means the host's turn-end
+// restore doesn't need to reach back into this package.
+
+async function syncClipboardStash(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ frontmostIsClickTier: boolean,
+): Promise {
+ const current = overrides.getClipboardStash?.();
+ if (!frontmostIsClickTier) {
+ // Restore + clear. Idempotent — if nothing is stashed, no-op.
+ if (current === undefined) return;
+ try {
+ await adapter.executor.writeClipboard(current);
+ // Clear only after a successful write — a transient pasteboard
+ // failure must not irrecoverably drop the stash.
+ overrides.onClipboardStashChanged?.(undefined);
+ } catch {
+ // Best effort — stash held, next non-click action retries.
+ }
+ return;
+ }
+ // Stash the user's clipboard on FIRST entry to click-tier only.
+ if (current === undefined) {
+ try {
+ const read = await adapter.executor.readClipboard();
+ overrides.onClipboardStashChanged?.(read);
+ } catch {
+ // readClipboard failed — use empty sentinel so we don't retry the stash
+ // on the next action; restore becomes a harmless writeClipboard("").
+ overrides.onClipboardStashChanged?.("");
+ }
+ }
+ // Re-clear on EVERY click-tier action, not just the first. Defeats the
+ // bypass where the agent calls write_clipboard (which doesn't route
+ // through runInputActionGates) between stash and a left_click on a UI
+ // Paste button — the next action's clear clobbers the agent's write
+ // before the click lands.
+ try {
+ await adapter.executor.writeClipboard("");
+ } catch {
+ // Transient pasteboard failure. The tier-"click" right-click/modifier
+ // block still holds; this is a net, not a promise.
+ }
+}
+
+/** Every click/type/key/scroll/drag/move_mouse runs through this before
+ * touching the executor. Returns null on pass, error-result on block.
+ * Any throw inside → caught by handleToolCall's outer try → tool error. */
+async function runInputActionGates(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+ actionKind: CuActionKind,
+): Promise {
+ // Step A+B — hide non-allowlisted apps + defocus us. Sub-gated. After this
+ // runs, the frontmost gate below becomes a rare edge-case detector (something
+ // popped up between prepare and action) rather than a normal-path blocker.
+ // ALL grant tiers stay visible — visibility is the baseline (tier "read").
+ if (subGates.hideBeforeAction) {
+ const hidden = await adapter.executor.prepareForAction(
+ overrides.allowedApps.map((a) => a.bundleId),
+ overrides.selectedDisplayId,
+ );
+ // Empty-check so we don't spam the callback on every action when nothing
+ // was hidden (the common case after the first action of a turn).
+ if (hidden.length > 0) {
+ overrides.onAppsHidden?.(hidden);
+ }
+ }
+
+ // Frontmost gate. Check FRESH on every call.
+ const frontmost = await adapter.executor.getFrontmostApp();
+
+ const tierByBundleId = new Map(
+ overrides.allowedApps.map((a) => [a.bundleId, a.tier] as const),
+ );
+
+ // After handleToolCall's tier backfill, every grant has a concrete tier —
+ // .get() returning undefined means the app is not in the allowlist at all.
+ const frontmostTier = frontmost
+ ? tierByBundleId.get(frontmost.bundleId)
+ : undefined;
+
+ // Clipboard guard. Per-action, not per-tool-call — runs for every sub-action
+ // inside computer_batch and teach_step/teach_batch, so clicking into a
+ // click-tier app mid-batch stashes+clears before the next click lands.
+ // Lives here (not in handleToolCall) so deferAcquire tools (request_access,
+ // list_granted_applications), `wait`, and the teach_step blocking-dialog
+ // phase don't trigger a sync — only input actions do.
+ if (subGates.clipboardGuard) {
+ await syncClipboardStash(adapter, overrides, frontmostTier === "click");
+ }
+
+ if (!frontmost) {
+ // No frontmost app (rare — login window?). Let it through; the click
+ // will land somewhere and PixelCompare catches staleness.
+ return null;
+ }
+
+ const { hostBundleId } = adapter.executor.capabilities;
+
+ if (frontmostTier !== undefined) {
+ if (tierSatisfies(frontmostTier, actionKind)) return null;
+ // In the allowlist but tier doesn't cover this action. Tailor the
+ // guidance to the actual tier — at "read", suggesting left_click or Bash
+ // is wrong (nothing is allowed; use Chrome MCP). At "click", the
+ // mouse_full/keyboard-specific messages apply.
+ if (frontmostTier === "read") {
+ // tier "read" is not category-unique (browser AND trading map to it) —
+ // re-look-up so the CiC hint only shows for actual browsers.
+ const isBrowser =
+ getDeniedCategoryForApp(frontmost.bundleId, frontmost.displayName) ===
+ "browser";
+ return errorResult(
+ `"${frontmost.displayName}" is granted at tier "read" — ` +
+ `visible in screenshots only, no clicks or typing.` +
+ (isBrowser
+ ? " Use the Claude-in-Chrome MCP for browser interaction (tools " +
+ "named `mcp__Claude_in_Chrome__*`; load via ToolSearch if " +
+ "deferred)."
+ : " No interaction is permitted; ask the user to take any " +
+ "actions in this app themselves.") +
+ TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+ }
+ // frontmostTier === "click" (tier === "full" would have passed tierSatisfies)
+ if (actionKind === "keyboard") {
+ return errorResult(
+ `"${frontmost.displayName}" is granted at tier "click" — ` +
+ `typing, key presses, and paste require tier "full". The keys ` +
+ `would go to this app's text fields or integrated terminal. To ` +
+ `type into a different app, click it first to bring it forward. ` +
+ `For shell commands, use the Bash tool.` + TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+ }
+ // actionKind === "mouse_full" ("mouse" and "mouse_position" pass at "click")
+ return errorResult(
+ `"${frontmost.displayName}" is granted at tier "click" — ` +
+ `right-click, middle-click, and clicks with modifier keys require ` +
+ `tier "full". Right-click opens a context menu with Paste/Cut, and ` +
+ `modifier chords fire as keystrokes before the click. Plain ` +
+ `left_click is allowed here.` + TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+ }
+ // Finder is never-hide, always allowed.
+ if (frontmost.bundleId === FINDER_BUNDLE_ID) return null;
+
+ if (frontmost.bundleId === hostBundleId) {
+ if (actionKind !== "keyboard") {
+ // mouse and mouse_full are both click events — click-through works.
+ // We're click-through (executor's withClickThrough). Pass.
+ return null;
+ }
+ // Keyboard safety net — defocus (prepareForAction step B) should have
+ // moved us off. If we're still here, typing would go to our chat box.
+ return errorResult(
+ "Claude's own window still has keyboard focus. This should not happen " +
+ "after the pre-action defocus. Click on the target application first.",
+ "state_conflict",
+ );
+ }
+
+ // Non-allowlisted, non-us, non-Finder. RARE after the hide loop — means
+ // something popped up between prepare and action, or the 5-try loop gave up.
+ return errorResult(
+ `"${frontmost.displayName}" is not in the allowed applications and is ` +
+ `currently in front. Take a new screenshot — it may have appeared ` +
+ `since your last one.`,
+ "app_not_granted",
+ );
+}
+
+/**
+ * Hit-test gate: reject a mouse action if the window under (x, y) belongs
+ * to an app whose tier doesn't cover mouse input. Closes the gap where a
+ * tier-"full" app is frontmost but the click lands on a tier-"read" window
+ * overlapping it — `runInputActionGates` passes (frontmost is fine), but the
+ * click actually goes to the read-tier app.
+ *
+ * Runs AFTER `scaleCoord` (needs global coords) and BEFORE the executor call.
+ * Returns null on pass (target is tier-"click"/"full", or desktop/Finder/us),
+ * error-result on block.
+ *
+ * When `appUnderPoint` returns null (desktop, or platform without hit-test),
+ * falls through — the frontmost check in `runInputActionGates` already ran.
+ */
+async function runHitTestGate(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+ x: number,
+ y: number,
+ actionKind: CuActionKind,
+): Promise {
+ const target = await adapter.executor.appUnderPoint(x, y);
+ if (!target) return null; // desktop / nothing under point / platform no-op
+
+ // Finder (desktop, file dialogs) is always clickable — same exemption as
+ // runInputActionGates. Our own overlay is filtered by Swift (pid != self).
+ if (target.bundleId === FINDER_BUNDLE_ID) return null;
+
+ const tierByBundleId = new Map(
+ overrides.allowedApps.map((a) => [a.bundleId, a.tier] as const),
+ );
+
+ if (!tierByBundleId.has(target.bundleId)) {
+ // Not in the allowlist at all. The frontmost check would catch this if
+ // the target were frontmost, but here a different app is in front. This
+ // is the "something popped up" edge case — a new window appeared between
+ // screenshot and click, or a background app's window overlaps the target.
+ return errorResult(
+ `Click at these coordinates would land on "${target.displayName}", ` +
+ `which is not in the allowed applications. Take a fresh screenshot ` +
+ `to see the current window layout.`,
+ "app_not_granted",
+ );
+ }
+
+ const targetTier = tierByBundleId.get(target.bundleId);
+
+ // Frontmost-based sync (runInputActionGates) misses the case where
+ // the click lands on a NON-FRONTMOST click-tier window. Re-sync by
+ // the hit-test target's tier — if target is click-tier, stash+clear
+ // before the click lands, regardless of what's frontmost.
+ if (subGates.clipboardGuard && targetTier === "click") {
+ await syncClipboardStash(adapter, overrides, true);
+ }
+
+ if (tierSatisfies(targetTier, actionKind)) return null;
+
+ // Target is in the allowlist but tier doesn't cover this action.
+ // runHitTestGate is only called with mouse/mouse_full (keyboard routes to
+ // frontmost, not window-under-cursor). The branch above catches
+ // mouse_full ∧ click; the only remaining fall-through is tier "read".
+ if (actionKind === "mouse_full" && targetTier === "click") {
+ return errorResult(
+ `Click at these coordinates would land on "${target.displayName}", ` +
+ `which is granted at tier "click" — right-click, middle-click, and ` +
+ `clicks with modifier keys require tier "full" (they can Paste via ` +
+ `the context menu or fire modifier-chord keystrokes). Plain ` +
+ `left_click is allowed here.` + TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+ }
+ const isBrowser =
+ getDeniedCategoryForApp(target.bundleId, target.displayName) === "browser";
+ return errorResult(
+ `Click at these coordinates would land on "${target.displayName}", ` +
+ `which is granted at tier "read" (screenshots only, no interaction). ` +
+ (isBrowser
+ ? "Use the Claude-in-Chrome MCP for browser interaction."
+ : "Ask the user to take any actions in this app themselves.") +
+ TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Screenshot helpers
+// ---------------------------------------------------------------------------
+
+/**
+ * §6 item 9 — screenshot retry on implausibly-small buffer. Battle-tested
+ * threshold (1024 bytes). We retry exactly once.
+ */
+const MIN_SCREENSHOT_BYTES = 1024;
+
+function decodedByteLength(base64: string): number {
+ // 3 bytes per 4 chars, minus padding. Good enough for a threshold check.
+ const padding = base64.endsWith("==") ? 2 : base64.endsWith("=") ? 1 : 0;
+ return Math.floor((base64.length * 3) / 4) - padding;
+}
+
+async function takeScreenshotWithRetry(
+ executor: ComputerExecutor,
+ allowedBundleIds: string[],
+ logger: ComputerUseHostAdapter["logger"],
+ displayId?: number,
+): Promise {
+ let shot = await executor.screenshot({ allowedBundleIds, displayId });
+ if (decodedByteLength(shot.base64) < MIN_SCREENSHOT_BYTES) {
+ logger.warn(
+ `[computer-use] screenshot implausibly small (${decodedByteLength(shot.base64)} bytes decoded), retrying once`,
+ );
+ shot = await executor.screenshot({ allowedBundleIds, displayId });
+ }
+ return shot;
+}
+
+// ---------------------------------------------------------------------------
+// Grapheme iteration — §6 item 7, ported from the Vercept acquisition
+// ---------------------------------------------------------------------------
+
+const INTER_GRAPHEME_SLEEP_MS = 8; // §6 item 4 — 125 Hz USB polling
+
+function segmentGraphemes(text: string): string[] {
+ try {
+ // Node 18+ has Intl.Segmenter; the try is defence against a stripped-
+ // -down runtime (falls back to code points).
+ const Segmenter = (
+ Intl as typeof Intl & {
+ Segmenter?: new (
+ locale?: string,
+ options?: { granularity: "grapheme" | "word" | "sentence" },
+ ) => { segment: (s: string) => Iterable<{ segment: string }> };
+ }
+ ).Segmenter;
+ if (typeof Segmenter === "function") {
+ const seg = new Segmenter(undefined, { granularity: "grapheme" });
+ return Array.from(seg.segment(text), (s) => s.segment);
+ }
+ } catch {
+ // fall through
+ }
+ // Code-point iteration. Keeps surrogate pairs together but splits ZWJ.
+ return Array.from(text);
+}
+
+function sleep(ms: number): Promise {
+ return new Promise((r) => setTimeout(r, ms));
+}
+
+/**
+ * Split a chord string like "ctrl+shift" into individual key names.
+ * Same parsing as `key` tool / executor.key / keyBlocklist.normalizeKeySequence.
+ */
+function parseKeyChord(text: string): string[] {
+ return text
+ .split("+")
+ .map((s) => s.trim())
+ .filter(Boolean);
+}
+
+// ---------------------------------------------------------------------------
+// left_mouse_down / left_mouse_up held-state tracking
+// ---------------------------------------------------------------------------
+
+/**
+ * Errors on double-down but not on up-without-down. Module-level, but
+ * reset on every lock acquire (handleToolCall → acquireCuLock branch) so
+ * a session interrupted mid-drag (overlay stop during left_mouse_down)
+ * doesn't leave the flag true for the next lock holder.
+ *
+ * Still scoped wrong within a single lock cycle if sessions could interleave
+ * tool calls, but the lock enforces at-most-one-session-uses-CU so they
+ * can't. The per-turn reset is the correctness boundary.
+ */
+let mouseButtonHeld = false;
+/** Whether mouse_move occurred between left_mouse_down and left_mouse_up.
+ * When false at mouseUp, the decomposed sequence is a click-release (not a
+ * drop) — hit-test at "mouse", not "mouse_full". */
+let mouseMoved = false;
+
+/** Clears the cross-call drag flags. Called from Gate-3 on lock-acquire and
+ * from `bindSessionContext` in mcpServer.ts — a fresh lock holder must not
+ * inherit a prior session's mid-drag state. */
+export function resetMouseButtonHeld(): void {
+ mouseButtonHeld = false;
+ mouseMoved = false;
+}
+
+/** If a left_mouse_down set the OS button without a matching left_mouse_up
+ * ever getting its turn, release it now. Same release-before-return as
+ * handleClick. No-op when not held — callers don't need to check. */
+async function releaseHeldMouse(
+ adapter: ComputerUseHostAdapter,
+): Promise {
+ if (!mouseButtonHeld) return;
+ await adapter.executor.mouseUp();
+ mouseButtonHeld = false;
+ mouseMoved = false;
+}
+
+/**
+ * Tools that check the lock but don't acquire it. `request_access` and
+ * `list_granted_applications` hit the CHECK (so a blocked session doesn't
+ * show an approval dialog for access it can't use) but defer ACQUIRE — the
+ * enter-CU notification/overlay only fires on the first action tool.
+ *
+ * `request_teach_access` is NOT here: approving teach mode hides the main
+ * window, and the lock must be held before that. See Gate-3 block in
+ * `handleToolCall` for the full explanation.
+ *
+ * Exported for `bindSessionContext` in mcpServer.ts so the async lock gate
+ * uses the same set as the sync one.
+ */
+export function defersLockAcquire(toolName: string): boolean {
+ return (
+ toolName === "request_access" ||
+ toolName === "list_granted_applications"
+ );
+}
+
+// ---------------------------------------------------------------------------
+// request_access helpers
+// ---------------------------------------------------------------------------
+
+/** Reverse-DNS-ish: contains at least one dot, no spaces, no slashes. Lets
+ * raw bundle IDs pass through resolution. */
+const REVERSE_DNS_RE = /^[A-Za-z0-9][\w.-]*\.[A-Za-z0-9][\w.-]*$/;
+
+function looksLikeBundleId(s: string): boolean {
+ return REVERSE_DNS_RE.test(s) && !s.includes(" ");
+}
+
+function resolveRequestedApps(
+ requestedNames: string[],
+ installed: InstalledApp[],
+ alreadyGrantedBundleIds: ReadonlySet,
+): ResolvedAppRequest[] {
+ const byLowerDisplayName = new Map();
+ const byBundleId = new Map();
+ for (const app of installed) {
+ byBundleId.set(app.bundleId, app);
+ // Last write wins on collisions. Ambiguous-name handling (multiple
+ // candidates in the dialog) is plan-documented but deferred — the
+ // InstalledApps enumerator dedupes by bundle ID, so true display-name
+ // collisions are rare. TODO(chicago, post-P1): surface all candidates.
+ byLowerDisplayName.set(app.displayName.toLowerCase(), app);
+ }
+
+ return requestedNames.map((requested): ResolvedAppRequest => {
+ let resolved: InstalledApp | undefined;
+ if (looksLikeBundleId(requested)) {
+ resolved = byBundleId.get(requested);
+ }
+ if (!resolved) {
+ resolved = byLowerDisplayName.get(requested.toLowerCase());
+ }
+ const bundleId = resolved?.bundleId;
+ // When unresolved AND the requested string looks like a bundle ID, use it
+ // directly for tier lookup (e.g. "company.thebrowser.Browser" with Arc not
+ // installed — the reverse-DNS string won't match any display-name substring).
+ const bundleIdCandidate =
+ bundleId ?? (looksLikeBundleId(requested) ? requested : undefined);
+ return {
+ requestedName: requested,
+ resolved,
+ isSentinel: bundleId ? SENTINEL_BUNDLE_IDS.has(bundleId) : false,
+ alreadyGranted: bundleId ? alreadyGrantedBundleIds.has(bundleId) : false,
+ proposedTier: getDefaultTierForApp(
+ bundleIdCandidate,
+ resolved?.displayName ?? requested,
+ ),
+ };
+ });
+}
+
+// ---------------------------------------------------------------------------
+// Individual tool handlers
+// ---------------------------------------------------------------------------
+
+async function handleRequestAccess(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ tccState: { accessibility: boolean; screenRecording: boolean } | undefined,
+): Promise {
+ if (!overrides.onPermissionRequest) {
+ return errorResult(
+ "This session was not wired with a permission handler. Computer control is not available here.",
+ "feature_unavailable",
+ );
+ }
+
+ // Teach mode hides the main window; permission dialogs render in that
+ // window. Without this, handleToolPermission blocks on an invisible
+ // prompt and the overlay spins forever. Tell the model to exit teach
+ // mode, request access, then re-enter.
+ if (overrides.getTeachModeActive?.()) {
+ return errorResult(
+ "Cannot request additional permissions during teach mode — the permission dialog would be hidden. End teach mode (finish the tour or let the turn complete), then call request_access, then start a new tour.",
+ "teach_mode_conflict",
+ );
+ }
+
+ const reason = requireString(args, "reason");
+ if (reason instanceof Error) return errorResult(reason.message, "bad_args");
+
+ // TCC-ungranted branch. The renderer shows a toggle panel INSTEAD OF the
+ // app list when `tccState` is present on the request, so we skip app
+ // resolution entirely (listInstalledApps() may fail without Screen
+ // Recording anyway). The user grants the OS perms from inside the dialog,
+ // then clicks "Ask again" — both buttons resolve with deny by design
+ // (ComputerUseApproval.tsx) so the model re-calls request_access and
+ // gets the app list on the next call.
+ if (tccState) {
+ const req: CuPermissionRequest = {
+ requestId: randomUUID(),
+ reason,
+ apps: [],
+ requestedFlags: {},
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ tccState,
+ };
+ await overrides.onPermissionRequest(req);
+
+ // Re-check: the user may have granted in System Settings while the
+ // dialog was up. The `tccState` arg is a pre-dialog snapshot — reading
+ // it here would tell the model "not yet granted" even after the user
+ // granted, and the model waits for confirmation instead of retrying.
+ // The renderer's TCC panel already live-polls (computerUseTccStore);
+ // this is the same re-check on the tool-result side.
+ const recheck = await adapter.ensureOsPermissions();
+ if (recheck.granted) {
+ return errorResult(
+ "macOS Accessibility and Screen Recording are now both granted. " +
+ "Call request_access again immediately — the next call will show " +
+ "the app selection list.",
+ );
+ }
+
+ const missing: string[] = [];
+ if (!recheck.accessibility) missing.push("Accessibility");
+ if (!recheck.screenRecording) missing.push("Screen Recording");
+ return errorResult(
+ `macOS ${missing.join(" and ")} permission(s) not yet granted. ` +
+ `The permission panel has been shown. Once the user grants the ` +
+ `missing permission(s), call request_access again.`,
+ "tcc_not_granted",
+ );
+ }
+
+ const rawApps = args.apps;
+ if (!Array.isArray(rawApps) || !rawApps.every((a) => typeof a === "string")) {
+ return errorResult('"apps" must be an array of strings.', "bad_args");
+ }
+ const apps = rawApps as string[];
+
+ const requestedFlags: Partial = {};
+ if (typeof args.clipboardRead === "boolean") {
+ requestedFlags.clipboardRead = args.clipboardRead;
+ }
+ if (typeof args.clipboardWrite === "boolean") {
+ requestedFlags.clipboardWrite = args.clipboardWrite;
+ }
+ if (typeof args.systemKeyCombos === "boolean") {
+ requestedFlags.systemKeyCombos = args.systemKeyCombos;
+ }
+
+ const {
+ needDialog,
+ skipDialogGrants,
+ willHide,
+ tieredApps,
+ userDenied,
+ policyDenied,
+ } = await buildAccessRequest(
+ adapter,
+ apps,
+ overrides.allowedApps,
+ new Set(overrides.userDeniedBundleIds),
+ overrides.selectedDisplayId,
+ );
+
+ let dialogGranted: AppGrant[] = [];
+ let dialogDenied: Array<{
+ bundleId: string;
+ reason: "user_denied" | "not_installed";
+ }> = [];
+ let dialogFlags: CuGrantFlags = overrides.grantFlags;
+
+ if (needDialog.length > 0 || Object.keys(requestedFlags).length > 0) {
+ const req: CuPermissionRequest = {
+ requestId: randomUUID(),
+ reason,
+ apps: needDialog,
+ requestedFlags,
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ // Undefined when empty so the renderer skips the section cleanly.
+ ...(willHide.length > 0 && {
+ willHide,
+ autoUnhideEnabled: adapter.getAutoUnhideEnabled(),
+ }),
+ };
+ const response = await overrides.onPermissionRequest(req);
+ dialogGranted = response.granted;
+ dialogDenied = response.denied;
+ dialogFlags = response.flags;
+ }
+
+ // Do NOT return display geometry or coordinateMode. See COORDINATES.md
+ // ("Never give the model a number that invites rescaling"). scaleCoord
+ // already transforms server-side; the coordinate convention is baked into
+ // the tool param descriptions at server-construction time.
+ const allGranted = [...skipDialogGrants, ...dialogGranted];
+ // Filter tieredApps to what was actually granted — if the user unchecked
+ // Chrome in the dialog, don't explain Chrome's tier.
+ const grantedBundleIds = new Set(allGranted.map((g) => g.bundleId));
+ const grantedTieredApps = tieredApps.filter((t) =>
+ grantedBundleIds.has(t.bundleId),
+ );
+ // Best-effort — grants are already persisted by wrappedPermissionHandler;
+ // a listDisplays/findWindowDisplays failure (monitor hot-unplug, NAPI
+ // error) must not tank the grant response. Same discipline as
+ // buildMonitorNote's listDisplays try/catch.
+ let windowLocations: Awaited> = [];
+ try {
+ windowLocations = await buildWindowLocations(adapter, allGranted);
+ } catch (e) {
+ adapter.logger.warn(
+ `[computer-use] buildWindowLocations failed: ${String(e)}`,
+ );
+ }
+ return okJson(
+ {
+ granted: allGranted,
+ denied: dialogDenied,
+ // Policy blocklist — precedes userDenied in precedence and response
+ // order. No escape hatch; the agent is told to find another approach.
+ ...(policyDenied.length > 0 && {
+ policyDenied: {
+ apps: policyDenied,
+ guidance: buildPolicyDeniedGuidance(policyDenied),
+ },
+ }),
+ // User-configured auto-deny — stripped before the dialog; this is the
+ // agent's only signal that these apps exist but are user-blocked.
+ ...(userDenied.length > 0 && {
+ userDenied: {
+ apps: userDenied,
+ guidance: buildUserDeniedGuidance(userDenied),
+ },
+ }),
+ // Upfront guidance so the model knows what each tier allows BEFORE
+ // hitting the gate. Only included when something was tier-restricted.
+ ...(grantedTieredApps.length > 0 && {
+ tierGuidance: buildTierGuidanceMessage(grantedTieredApps),
+ }),
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ // Where each granted app currently has open windows, across monitors.
+ // Omitted when the app isn't running or has no normal windows.
+ ...(windowLocations.length > 0 ? { windowLocations } : {}),
+ },
+ {
+ // dialogGranted only — skipDialogGrants are idempotent re-grants of
+ // apps already in the allowlist (no user action, dialog skips them).
+ // Matching denied_count's this-call-only semantics.
+ granted_count: dialogGranted.length,
+ denied_count: dialogDenied.length,
+ ...tierAssignmentTelemetry(grantedTieredApps),
+ },
+ );
+}
+
+/**
+ * For each granted app with open windows, which displays those windows are
+ * on. Single-monitor setups return an empty array (no multi-monitor signal
+ * to give). Apps not running, or running with no normal windows, are omitted.
+ */
+async function buildWindowLocations(
+ adapter: ComputerUseHostAdapter,
+ granted: AppGrant[],
+): Promise<
+ Array<{
+ bundleId: string;
+ displayName: string;
+ displays: Array<{ id: number; label?: string; isPrimary?: boolean }>;
+ }>
+> {
+ if (granted.length === 0) return [];
+
+ const displays = await adapter.executor.listDisplays();
+ if (displays.length <= 1) return [];
+
+ const grantedBundleIds = granted.map((g) => g.bundleId);
+ const windowLocs = await adapter.executor.findWindowDisplays(grantedBundleIds);
+ const displayById = new Map(displays.map((d) => [d.displayId, d]));
+ const idsByBundle = new Map(windowLocs.map((w) => [w.bundleId, w.displayIds]));
+
+ const out = [];
+ for (const g of granted) {
+ const displayIds = idsByBundle.get(g.bundleId);
+ if (!displayIds || displayIds.length === 0) continue;
+ out.push({
+ bundleId: g.bundleId,
+ displayName: g.displayName,
+ displays: displayIds.map((id) => {
+ const d = displayById.get(id);
+ return { id, label: d?.label, isPrimary: d?.isPrimary };
+ }),
+ });
+ }
+ return out;
+}
+
+/**
+ * Shared app-resolution + partition + hide-preview pipeline. Extracted from
+ * `handleRequestAccess` so `handleRequestTeachAccess` can call the same path.
+ *
+ * Does the full app-name→InstalledApp resolution, assigns each a tier
+ * (browser→"read", terminal/IDE→"click", else "full" — see deniedApps.ts),
+ * splits into already-granted (skip the dialog, preserve grantedAt+tier) vs
+ * need-dialog, and computes the willHide preview. Unlike the previous
+ * hard-deny model, ALL apps proceed to the dialog; the tier just constrains
+ * what actions are allowed once granted.
+ */
+/** An app assigned a restricted tier (not `"full"`). Used to build the
+ * guidance message telling the model what it can/can't do. */
+interface TieredApp {
+ bundleId: string;
+ displayName: string;
+ /** Never `"full"` — only restricted tiers are collected. */
+ tier: "read" | "click";
+}
+
+interface AccessRequestParts {
+ needDialog: ResolvedAppRequest[];
+ skipDialogGrants: AppGrant[];
+ willHide: Array<{ bundleId: string; displayName: string }>;
+ /** Resolved apps with `proposedTier !== "full"` — for the guidance text.
+ * Unresolved apps are omitted (they go to `denied` with `not_installed`). */
+ tieredApps: TieredApp[];
+ /** Apps stripped by the user's Settings auto-deny list. Surfaced in the
+ * response with guidance; never reach the dialog. */
+ userDenied: Array<{ requestedName: string; displayName: string }>;
+ /** Apps stripped by the baked-in policy blocklist (streaming/music/ebooks,
+ * etc. — `deniedApps.isPolicyDenied`). Precedence over userDenied. */
+ policyDenied: Array<{ requestedName: string; displayName: string }>;
+}
+
+async function buildAccessRequest(
+ adapter: ComputerUseHostAdapter,
+ apps: string[],
+ allowedApps: AppGrant[],
+ userDeniedBundleIds: ReadonlySet,
+ selectedDisplayId?: number,
+): Promise {
+ const alreadyGranted = new Set(allowedApps.map((g) => g.bundleId));
+ const installed = await adapter.executor.listInstalledApps();
+ const resolved = resolveRequestedApps(apps, installed, alreadyGranted);
+
+ // Policy-level auto-deny (baked-in, not user-configurable). Stripped
+ // before userDenied — checks bundle ID AND display name (covers
+ // unresolved requests). Precedence: policy > user setting > tier.
+ const policyDenied: Array<{ requestedName: string; displayName: string }> =
+ [];
+ const afterPolicy: typeof resolved = [];
+ for (const r of resolved) {
+ const displayName = r.resolved?.displayName ?? r.requestedName;
+ if (isPolicyDenied(r.resolved?.bundleId, displayName)) {
+ policyDenied.push({ requestedName: r.requestedName, displayName });
+ } else {
+ afterPolicy.push(r);
+ }
+ }
+
+ // User-configured auto-deny (Settings → Desktop app → Computer Use).
+ // Stripped BEFORE
+ // tier assignment — these never reach the dialog regardless of category.
+ // Bundle-ID match only (the Settings UI picks from installed apps, which
+ // always have a bundle ID). Unresolved requests pass through to the tier
+ // system; the user can't preemptively deny an app that isn't installed.
+ const userDenied: Array<{ requestedName: string; displayName: string }> = [];
+ const surviving: typeof afterPolicy = [];
+ for (const r of afterPolicy) {
+ if (r.resolved && userDeniedBundleIds.has(r.resolved.bundleId)) {
+ userDenied.push({
+ requestedName: r.requestedName,
+ displayName: r.resolved.displayName,
+ });
+ } else {
+ surviving.push(r);
+ }
+ }
+
+ // Collect resolved apps with a restricted tier for the guidance message.
+ // Unresolved apps with a restricted tier (e.g. model asks for "Chrome" but
+ // it's not installed) are omitted — they'll end up in the `denied` list
+ // with reason "not_installed" and the model will see that instead.
+ const tieredApps: TieredApp[] = [];
+ for (const r of surviving) {
+ if (r.proposedTier === "full" || !r.resolved) continue;
+ tieredApps.push({
+ bundleId: r.resolved.bundleId,
+ displayName: r.resolved.displayName,
+ tier: r.proposedTier,
+ });
+ }
+
+ // Idempotence: apps that are already granted skip the dialog and are
+ // merged into the `granted` response. Existing grants keep their tier
+ // (which may differ from the current proposedTier if policy changed).
+ const skipDialog = surviving.filter((r) => r.alreadyGranted);
+ const needDialog = surviving.filter((r) => !r.alreadyGranted);
+
+ // Populate icons only for what the dialog will actually show. Sequential
+ // awaits are fine — the Swift module is cached (listInstalledApps above
+ // loaded it), each N-API call is synchronous, and the darwin executor
+ // memoizes by path. Failures leave iconDataUrl undefined; renderer falls
+ // back to a grey box.
+ for (const r of needDialog) {
+ if (!r.resolved) continue;
+ try {
+ r.resolved.iconDataUrl = await adapter.executor.getAppIcon(
+ r.resolved.path,
+ );
+ } catch {
+ // leave undefined
+ }
+ }
+
+ const now = Date.now();
+ const skipDialogGrants: AppGrant[] = skipDialog
+ .filter((r) => r.resolved)
+ .map((r) => {
+ // Reuse the existing grant (preserving grantedAt + tier) rather than
+ // synthesizing a new one — keeps Settings-page "Granted 3m ago" honest.
+ const existing = allowedApps.find(
+ (g) => g.bundleId === r.resolved!.bundleId,
+ );
+ return (
+ existing ?? {
+ bundleId: r.resolved!.bundleId,
+ displayName: r.resolved!.displayName,
+ grantedAt: now,
+ tier: r.proposedTier,
+ }
+ );
+ });
+
+ // Preview what will be hidden if the user approves exactly the requested
+ // set plus what they already have. All tiers are visible, so everything
+ // resolved goes in the exempt set.
+ const exemptForPreview = [
+ ...allowedApps.map((a) => a.bundleId),
+ ...surviving.filter((r) => r.resolved).map((r) => r.resolved!.bundleId),
+ ];
+ const willHide = await adapter.executor.previewHideSet(
+ exemptForPreview,
+ selectedDisplayId,
+ );
+
+ return {
+ needDialog,
+ skipDialogGrants,
+ willHide,
+ tieredApps,
+ userDenied,
+ policyDenied,
+ };
+}
+
+/**
+ * Build guidance text for apps granted at a restricted tier. Returned
+ * inline in the okJson response so the model knows upfront what it can
+ * do with each app, instead of learning by hitting the tier gate.
+ */
+function buildTierGuidanceMessage(tiered: TieredApp[]): string {
+ // tier "read" is not category-unique — split so browsers get the CiC hint
+ // and trading platforms get "ask the user" instead.
+ const readBrowsers = tiered.filter(
+ (t) =>
+ t.tier === "read" &&
+ getDeniedCategoryForApp(t.bundleId, t.displayName) === "browser",
+ );
+ const readOther = tiered.filter(
+ (t) =>
+ t.tier === "read" &&
+ getDeniedCategoryForApp(t.bundleId, t.displayName) !== "browser",
+ );
+ const clickTier = tiered.filter((t) => t.tier === "click");
+
+ const parts: string[] = [];
+
+ if (readBrowsers.length > 0) {
+ const names = readBrowsers.map((b) => `"${b.displayName}"`).join(", ");
+ parts.push(
+ `${names} ${readBrowsers.length === 1 ? "is a browser" : "are browsers"} — ` +
+ `granted at tier "read" (visible in screenshots only; no clicks or ` +
+ `typing). You can read what's on screen but cannot navigate, click, ` +
+ `or type into ${readBrowsers.length === 1 ? "it" : "them"}. For browser ` +
+ `interaction, use the Claude-in-Chrome MCP (tools named ` +
+ `\`mcp__Claude_in_Chrome__*\`; load via ToolSearch if deferred).`,
+ );
+ }
+
+ if (readOther.length > 0) {
+ const names = readOther.map((t) => `"${t.displayName}"`).join(", ");
+ parts.push(
+ `${names} ${readOther.length === 1 ? "is" : "are"} granted at tier ` +
+ `"read" (visible in screenshots only; no clicks or typing). You can ` +
+ `read what's on screen but cannot interact. Ask the user to take any ` +
+ `actions in ${readOther.length === 1 ? "this app" : "these apps"} ` +
+ `themselves.`,
+ );
+ }
+
+ if (clickTier.length > 0) {
+ const names = clickTier.map((t) => `"${t.displayName}"`).join(", ");
+ parts.push(
+ `${names} ${clickTier.length === 1 ? "has" : "have"} terminal or IDE ` +
+ `capabilities — granted at tier "click" (visible + plain left-click ` +
+ `only; NO typing, key presses, right-click, modifier-clicks, or ` +
+ `drag-drop). You can click buttons and scroll output, but ` +
+ `${clickTier.length === 1 ? "its" : "their"} integrated terminal and ` +
+ `editor are off-limits to keyboard input. Right-click (context-menu ` +
+ `Paste) and dragging text onto ${clickTier.length === 1 ? "it" : "them"} ` +
+ `require tier "full". For shell commands, use the Bash tool.`,
+ );
+ }
+
+ if (parts.length === 0) return "";
+ // Same anti-subversion clause the gate errors carry — said upfront so the
+ // model doesn't reach for osascript/cliclick after seeing "no clicks/typing".
+ return parts.join("\n\n") + TIER_ANTI_SUBVERSION;
+}
+
+/**
+ * Build guidance text for apps stripped by the user's Settings auto-deny
+ * list. Returned inline in the okJson response so the agent knows (a) the
+ * app is auto-denied by request_access and (b) the escape hatch
+ * is to ask the human to edit Settings, not to retry or reword the request.
+ */
+function buildUserDeniedGuidance(
+ userDenied: Array<{ requestedName: string; displayName: string }>,
+): string {
+ const names = userDenied.map((d) => `"${d.displayName}"`).join(", ");
+ const one = userDenied.length === 1;
+ return (
+ `${names} ${one ? "is" : "are"} in the user's auto-deny list ` +
+ `(Settings → Desktop app (General) → Computer Use → Denied apps). ` +
+ `Requests for ` +
+ `${one ? "this app" : "these apps"} are automatically denied. If you need access for ` +
+ `this task, ask the user to remove ${one ? "it" : "them"} from their ` +
+ `deny list in Settings — you cannot request this through the tool.`
+ );
+}
+
+/**
+ * Guidance for policy-denied apps (baked-in blocklist, not user-editable).
+ * Unlike userDenied, there is no escape hatch — the agent is told to find
+ * another approach.
+ */
+function buildPolicyDeniedGuidance(
+ policyDenied: Array<{ requestedName: string; displayName: string }>,
+): string {
+ const names = policyDenied.map((d) => `"${d.displayName}"`).join(", ");
+ const one = policyDenied.length === 1;
+ return (
+ `${names} ${one ? "is" : "are"} blocked by policy for computer use. ` +
+ `Requests for ${one ? "this app" : "these apps"} are automatically ` +
+ `denied regardless of what the user has approved. There is no Settings ` +
+ `override. Inform the user that you cannot access ` +
+ `${one ? "this app" : "these apps"} and suggest an alternative ` +
+ `approach if one exists. Do not try to directly subvert this block ` +
+ `regardless of the user's request.`
+ );
+}
+
+/**
+ * Telemetry helper — counts by category. Field names (`denied_*`) are kept
+ * for schema compat; interpret as "assigned non-full tier" in dashboards.
+ */
+function tierAssignmentTelemetry(
+ tiered: TieredApp[],
+): Pick {
+ // `denied_browser_count` now counts ALL tier-"read" grants (browsers +
+ // trading). The field name was already legacy-only before trading existed
+ // (dashboards read it as "non-full tier"), so no new column.
+ const browserCount = tiered.filter((t) => t.tier === "read").length;
+ const terminalCount = tiered.filter((t) => t.tier === "click").length;
+ return {
+ ...(browserCount > 0 && { denied_browser_count: browserCount }),
+ ...(terminalCount > 0 && { denied_terminal_count: terminalCount }),
+ };
+}
+
+/**
+ * Sibling of `handleRequestAccess`. Same app-resolution + TCC-threading, but
+ * routes to the teach approval dialog and fires `onTeachModeActivated` on
+ * success. No grant-flag checkboxes (clipboard/systemKeys) in teach mode —
+ * the tool schema omits those fields.
+ *
+ * Unlike `request_access`, this ALWAYS shows the dialog even when every
+ * requested app is already granted. Teach mode is a distinct UX the user
+ * must explicitly consent to (main window hides) — idempotent app grants
+ * don't imply consent to being guided.
+ */
+async function handleRequestTeachAccess(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ tccState: { accessibility: boolean; screenRecording: boolean } | undefined,
+): Promise {
+ if (!overrides.onTeachPermissionRequest) {
+ return errorResult(
+ "Teach mode is not available in this session.",
+ "feature_unavailable",
+ );
+ }
+
+ // Same as handleRequestAccess above — the dialog renders in the hidden
+ // main window. Model re-calling request_teach_access mid-tour (to add
+ // another app) is plausible since request_access docs say "call again
+ // mid-session to add more apps" and this uses the same grant model.
+ if (overrides.getTeachModeActive?.()) {
+ return errorResult(
+ "Teach mode is already active. To add more apps, end the current tour first, then call request_teach_access again with the full app list.",
+ "teach_mode_conflict",
+ );
+ }
+
+ const reason = requireString(args, "reason");
+ if (reason instanceof Error) return errorResult(reason.message, "bad_args");
+
+ // TCC-ungranted branch — identical to handleRequestAccess's. The renderer
+ // shows the same TCC toggle panel regardless of which request tool got here.
+ if (tccState) {
+ const req: CuTeachPermissionRequest = {
+ requestId: randomUUID(),
+ reason,
+ apps: [],
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ tccState,
+ };
+ await overrides.onTeachPermissionRequest(req);
+
+ // Same re-check as handleRequestAccess — user may have granted while the
+ // dialog was up, and the pre-dialog snapshot would mislead the model.
+ const recheck = await adapter.ensureOsPermissions();
+ if (recheck.granted) {
+ return errorResult(
+ "macOS Accessibility and Screen Recording are now both granted. " +
+ "Call request_teach_access again immediately — the next call will " +
+ "show the app selection list.",
+ );
+ }
+
+ const missing: string[] = [];
+ if (!recheck.accessibility) missing.push("Accessibility");
+ if (!recheck.screenRecording) missing.push("Screen Recording");
+ return errorResult(
+ `macOS ${missing.join(" and ")} permission(s) not yet granted. ` +
+ `The permission panel has been shown. Once the user grants the ` +
+ `missing permission(s), call request_teach_access again.`,
+ "tcc_not_granted",
+ );
+ }
+
+ const rawApps = args.apps;
+ if (!Array.isArray(rawApps) || !rawApps.every((a) => typeof a === "string")) {
+ return errorResult('"apps" must be an array of strings.', "bad_args");
+ }
+ const apps = rawApps as string[];
+
+ const {
+ needDialog,
+ skipDialogGrants,
+ willHide,
+ tieredApps,
+ userDenied,
+ policyDenied,
+ } = await buildAccessRequest(
+ adapter,
+ apps,
+ overrides.allowedApps,
+ new Set(overrides.userDeniedBundleIds),
+ overrides.selectedDisplayId,
+ );
+
+ // All requested apps were user-denied (or unresolvable) and none pre-granted
+ // — skip the dialog entirely. Without this, onTeachPermissionRequest fires
+ // with apps:[] and the user sees an empty approval dialog where Allow and
+ // Deny produce the same result (granted=[] → teachModeActive stays false).
+ // handleRequestAccess has the equivalent guard at the needDialog.length
+ // check; teach didn't need one before user-deny because needDialog=[]
+ // previously implied skipDialogGrants.length > 0 (all-already-granted).
+ if (needDialog.length === 0 && skipDialogGrants.length === 0) {
+ return okJson(
+ {
+ granted: [],
+ denied: [],
+ ...(policyDenied.length > 0 && {
+ policyDenied: {
+ apps: policyDenied,
+ guidance: buildPolicyDeniedGuidance(policyDenied),
+ },
+ }),
+ ...(userDenied.length > 0 && {
+ userDenied: {
+ apps: userDenied,
+ guidance: buildUserDeniedGuidance(userDenied),
+ },
+ }),
+ teachModeActive: false,
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ },
+ { granted_count: 0, denied_count: 0 },
+ );
+ }
+
+ const req: CuTeachPermissionRequest = {
+ requestId: randomUUID(),
+ reason,
+ apps: needDialog,
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ ...(willHide.length > 0 && {
+ willHide,
+ autoUnhideEnabled: adapter.getAutoUnhideEnabled(),
+ }),
+ };
+ const response = await overrides.onTeachPermissionRequest(req);
+
+ const granted = [...skipDialogGrants, ...response.granted];
+ // Gate on explicit dialog consent, NOT on merged grant length.
+ // skipDialogGrants are pre-existing idempotent app grants — they don't
+ // imply the user said yes to THIS dialog. Without the userConsented
+ // check, Deny would still activate teach mode whenever any requested
+ // app was previously granted (worst case: needDialog=[] → Allow and
+ // Deny payloads are structurally identical).
+ const teachModeActive = response.userConsented === true && granted.length > 0;
+ if (teachModeActive) {
+ overrides.onTeachModeActivated?.();
+ }
+
+ const grantedBundleIds = new Set(granted.map((g) => g.bundleId));
+ const grantedTieredApps = tieredApps.filter((t) =>
+ grantedBundleIds.has(t.bundleId),
+ );
+
+ return okJson(
+ {
+ granted,
+ denied: response.denied,
+ ...(policyDenied.length > 0 && {
+ policyDenied: {
+ apps: policyDenied,
+ guidance: buildPolicyDeniedGuidance(policyDenied),
+ },
+ }),
+ ...(userDenied.length > 0 && {
+ userDenied: {
+ apps: userDenied,
+ guidance: buildUserDeniedGuidance(userDenied),
+ },
+ }),
+ ...(grantedTieredApps.length > 0 && {
+ tierGuidance: buildTierGuidanceMessage(grantedTieredApps),
+ }),
+ teachModeActive,
+ screenshotFiltering: adapter.executor.capabilities.screenshotFiltering,
+ },
+ {
+ // response.granted only — skipDialogGrants are idempotent re-grants.
+ // See handleRequestAccess's parallel comment.
+ granted_count: response.granted.length,
+ denied_count: response.denied.length,
+ ...tierAssignmentTelemetry(grantedTieredApps),
+ },
+ );
+}
+
+// ---------------------------------------------------------------------------
+// teach_step + teach_batch — shared step primitives
+// ---------------------------------------------------------------------------
+
+/** A fully-validated teach step, anchor already scaled to logical points. */
+interface ValidatedTeachStep {
+ explanation: string;
+ nextPreview: string;
+ anchorLogical: TeachStepRequest["anchorLogical"];
+ actions: Array>;
+}
+
+/**
+ * Validate one raw step record and scale its anchor. `label` is prefixed to
+ * error messages so teach_batch can say `steps[2].actions[0]` instead of
+ * just `actions[0]`.
+ *
+ * The anchor transform is the whole coordinate story: model sends image-pixel
+ * coords (same space as click coords, per COORDINATES.md), `scaleCoord` turns
+ * them into logical points against `overrides.lastScreenshot`. For
+ * teach_batch, lastScreenshot stays at its pre-call value for the entire
+ * batch — same invariant as computer_batch's "coordinates refer to the
+ * PRE-BATCH screenshot". Anchors for step 2+ must therefore target elements
+ * the model can predict will be at those coordinates after step 1's actions.
+ */
+async function validateTeachStepArgs(
+ raw: Record,
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ label: string,
+): Promise {
+ const explanation = requireString(raw, "explanation");
+ if (explanation instanceof Error) {
+ return new Error(`${label}: ${explanation.message}`);
+ }
+ const nextPreview = requireString(raw, "next_preview");
+ if (nextPreview instanceof Error) {
+ return new Error(`${label}: ${nextPreview.message}`);
+ }
+
+ const actions = raw.actions;
+ if (!Array.isArray(actions)) {
+ return new Error(
+ `${label}: "actions" must be an array (empty is allowed).`,
+ );
+ }
+ for (const [i, act] of actions.entries()) {
+ if (typeof act !== "object" || act === null) {
+ return new Error(`${label}: actions[${i}] must be an object`);
+ }
+ const action = (act as Record).action;
+ if (typeof action !== "string") {
+ return new Error(`${label}: actions[${i}].action must be a string`);
+ }
+ if (!BATCHABLE_ACTIONS.has(action)) {
+ return new Error(
+ `${label}: actions[${i}].action="${action}" is not allowed. ` +
+ `Allowed: ${[...BATCHABLE_ACTIONS].join(", ")}.`,
+ );
+ }
+ }
+
+ let anchorLogical: TeachStepRequest["anchorLogical"];
+ if (raw.anchor !== undefined) {
+ const anchor = raw.anchor;
+ if (
+ !Array.isArray(anchor) ||
+ anchor.length !== 2 ||
+ typeof anchor[0] !== "number" ||
+ typeof anchor[1] !== "number" ||
+ !Number.isFinite(anchor[0]) ||
+ !Number.isFinite(anchor[1])
+ ) {
+ return new Error(
+ `${label}: "anchor" must be a [x, y] number tuple or omitted.`,
+ );
+ }
+ const display = await adapter.executor.getDisplaySize(
+ overrides.selectedDisplayId,
+ );
+ anchorLogical = scaleCoord(
+ anchor[0],
+ anchor[1],
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+ }
+
+ return {
+ explanation,
+ nextPreview,
+ anchorLogical,
+ actions: actions as Array>,
+ };
+}
+
+/** Outcome of showing one tooltip + running its actions. */
+type TeachStepOutcome =
+ | { kind: "exit" }
+ | { kind: "ok"; results: BatchActionResult[] }
+ | {
+ kind: "action_error";
+ executed: number;
+ failed: BatchActionResult;
+ remaining: number;
+ /** The inner action's telemetry (error_kind), forwarded so the
+ * caller can pass it to okJson and keep cu_tool_call accurate
+ * when the failure happened inside a batch. */
+ telemetry: CuCallTelemetry | undefined;
+ };
+
+/**
+ * Show the tooltip, block for Next/Exit, run actions on Next.
+ *
+ * Action execution is a straight lift from `handleComputerBatch`:
+ * prepareForAction ONCE per step (the user clicked Next — they consented to
+ * that step's sequence), pixelValidation OFF (committed sequence), frontmost
+ * gate still per-action, stop-on-first-error with partial results.
+ *
+ * Empty `actions` is valid — "read this, click Next to continue" steps.
+ * Assumes `overrides.onTeachStep` is set (caller guards).
+ */
+async function executeTeachStep(
+ step: ValidatedTeachStep,
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ // Block until Next or Exit. Same pending-promise pattern as
+ // onPermissionRequest — host stores the resolver, overlay IPC fires it.
+ // `!` is safe: both callers guard on overrides.onTeachStep before reaching here.
+ const stepResult = await overrides.onTeachStep!({
+ explanation: step.explanation,
+ nextPreview: step.nextPreview,
+ anchorLogical: step.anchorLogical,
+ });
+
+ if (stepResult.action === "exit") {
+ // The host's Exit handler also calls stopSession, so the turn is
+ // already unwinding. Caller decides what to return for the transcript.
+ // A PREVIOUS step's left_mouse_down may have left the OS button held.
+ await releaseHeldMouse(adapter);
+ return { kind: "exit" };
+ }
+
+ // Next clicked. Flip overlay to spinner before we start driving.
+ overrides.onTeachWorking?.();
+
+ if (step.actions.length === 0) {
+ return { kind: "ok", results: [] };
+ }
+
+ if (subGates.hideBeforeAction) {
+ const hidden = await adapter.executor.prepareForAction(
+ overrides.allowedApps.map((a) => a.bundleId),
+ overrides.selectedDisplayId,
+ );
+ if (hidden.length > 0) {
+ overrides.onAppsHidden?.(hidden);
+ }
+ }
+
+ const stepSubGates: CuSubGates = {
+ ...subGates,
+ hideBeforeAction: false,
+ pixelValidation: false,
+ // Anchors are pre-computed against the display at batch start.
+ // A mid-batch resolver switch would break tooltip positioning.
+ autoTargetDisplay: false,
+ };
+
+ const results: BatchActionResult[] = [];
+ for (const [i, act] of step.actions.entries()) {
+ // Same abort check as handleComputerBatch — Exit calls stopSession so
+ // this IS the exit path, just caught mid-dispatch instead of at the
+ // onTeachStep await above. Callers already handle { kind: "exit" }.
+ if (overrides.isAborted?.()) {
+ await releaseHeldMouse(adapter);
+ return { kind: "exit" };
+ }
+ // Same inter-step settle as handleComputerBatch.
+ if (i > 0) await sleep(10);
+ const action = act.action as string;
+
+ // Drop mid-step screenshot piggyback — same invariant as computer_batch.
+ // Click coords stay anchored to the screenshot the model took BEFORE
+ // calling teach_step/teach_batch.
+ const { screenshot: _dropped, ...inner } = await dispatchAction(
+ action,
+ act,
+ adapter,
+ overrides,
+ stepSubGates,
+ );
+
+ const text = firstTextContent(inner);
+ const result = { action, ok: !inner.isError, output: text };
+ results.push(result);
+
+ if (inner.isError) {
+ await releaseHeldMouse(adapter);
+ return {
+ kind: "action_error",
+ executed: results.length - 1,
+ failed: result,
+ remaining: step.actions.length - results.length,
+ telemetry: inner.telemetry,
+ };
+ }
+ }
+
+ return { kind: "ok", results };
+}
+
+/**
+ * Fold a fresh screenshot into the result. Eliminates the separate
+ * screenshot tool call the model would otherwise make before the next
+ * teach_step (one fewer API round trip per step). handleScreenshot
+ * runs its own prepareForAction — that's correct: actions may have
+ * opened something outside the allowlist. The .screenshot piggyback
+ * flows through to serverDef.ts's stash → lastScreenshot updates →
+ * the next teach_step.anchor scales against THIS image, which is what
+ * the model is now looking at.
+ */
+async function appendTeachScreenshot(
+ resultJson: unknown,
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const shotResult = await handleScreenshot(adapter, overrides, subGates);
+ if (shotResult.isError) {
+ // Hide+screenshot failed (rare — e.g. SCContentFilter error). Don't
+ // tank the step; just omit the image. Model will call screenshot
+ // itself and see the real error.
+ return okJson(resultJson);
+ }
+ return {
+ content: [
+ { type: "text", text: JSON.stringify(resultJson) },
+ // handleScreenshot's content is [maybeMonitorNote, maybeHiddenNote,
+ // image]. Spread all — both notes are useful context and the model
+ // expects them alongside screenshots.
+ ...shotResult.content,
+ ],
+ // For serverDef.ts to stash. Next teach_step.anchor scales against this.
+ screenshot: shotResult.screenshot,
+ };
+}
+
+/**
+ * Show one guided-tour tooltip and block until the user clicks Next or Exit.
+ * On Next, execute `actions[]` with `computer_batch` semantics.
+ */
+async function handleTeachStep(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ if (!overrides.onTeachStep) {
+ return errorResult(
+ "Teach mode is not active. Call request_teach_access first.",
+ "teach_mode_not_active",
+ );
+ }
+
+ const step = await validateTeachStepArgs(
+ args,
+ adapter,
+ overrides,
+ "teach_step",
+ );
+ if (step instanceof Error) return errorResult(step.message, "bad_args");
+
+ const outcome = await executeTeachStep(step, adapter, overrides, subGates);
+
+ if (outcome.kind === "exit") {
+ return okJson({ exited: true });
+ }
+ if (outcome.kind === "action_error") {
+ return okJson(
+ {
+ executed: outcome.executed,
+ failed: outcome.failed,
+ remaining: outcome.remaining,
+ },
+ outcome.telemetry,
+ );
+ }
+
+ // ok. No screenshot for empty actions — screen didn't change, model's
+ // existing screenshot is still accurate.
+ if (step.actions.length === 0) {
+ return okJson({ executed: 0, results: [] });
+ }
+ return appendTeachScreenshot(
+ { executed: outcome.results.length, results: outcome.results },
+ adapter,
+ overrides,
+ subGates,
+ );
+}
+
+/**
+ * Queue a whole guided tour in one tool call. Parallels `computer_batch`: N
+ * steps → one model→API round trip instead of N. Each step still blocks for
+ * its own Next click (the user paces the tour), but the model doesn't wait
+ * for a round trip between steps.
+ *
+ * Validates ALL steps upfront so a typo in step 5 doesn't surface after the
+ * user has already clicked through steps 1–4.
+ *
+ * Anchors for every step scale against the pre-call `lastScreenshot` — same
+ * PRE-BATCH invariant as computer_batch. Steps 2+ should either omit anchor
+ * (centered tooltip) or target elements the model predicts won't have moved.
+ *
+ * Result shape:
+ * {exited: true, stepsCompleted: N} — user clicked Exit
+ * {stepsCompleted, stepFailed, executed, failed, …} — action error at step N
+ * {stepsCompleted, results: [...]} + screenshot — all steps ran
+ */
+async function handleTeachBatch(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ if (!overrides.onTeachStep) {
+ return errorResult(
+ "Teach mode is not active. Call request_teach_access first.",
+ "teach_mode_not_active",
+ );
+ }
+
+ const rawSteps = args.steps;
+ if (!Array.isArray(rawSteps) || rawSteps.length < 1) {
+ return errorResult('"steps" must be a non-empty array.', "bad_args");
+ }
+
+ // Validate upfront — fail fast before showing any tooltip.
+ const steps: ValidatedTeachStep[] = [];
+ for (const [i, raw] of rawSteps.entries()) {
+ if (typeof raw !== "object" || raw === null) {
+ return errorResult(`steps[${i}] must be an object`, "bad_args");
+ }
+ const v = await validateTeachStepArgs(
+ raw as Record,
+ adapter,
+ overrides,
+ `steps[${i}]`,
+ );
+ if (v instanceof Error) return errorResult(v.message, "bad_args");
+ steps.push(v);
+ }
+
+ const allResults: BatchActionResult[][] = [];
+ for (const [i, step] of steps.entries()) {
+ const outcome = await executeTeachStep(step, adapter, overrides, subGates);
+
+ if (outcome.kind === "exit") {
+ return okJson({ exited: true, stepsCompleted: i });
+ }
+ if (outcome.kind === "action_error") {
+ return okJson(
+ {
+ stepsCompleted: i,
+ stepFailed: i,
+ executed: outcome.executed,
+ failed: outcome.failed,
+ remaining: outcome.remaining,
+ results: allResults,
+ },
+ outcome.telemetry,
+ );
+ }
+ allResults.push(outcome.results);
+ }
+
+ // Final screenshot only if any step ran actions (screen changed).
+ const screenChanged = steps.some((s) => s.actions.length > 0);
+ const resultJson = { stepsCompleted: steps.length, results: allResults };
+ if (!screenChanged) {
+ return okJson(resultJson);
+ }
+ return appendTeachScreenshot(resultJson, adapter, overrides, subGates);
+}
+
+/**
+ * Build the hidden-apps note that accompanies a screenshot. Tells the model
+ * which apps got hidden (not in allowlist) and how to add them. Returns
+ * undefined when nothing was hidden since the last screenshot.
+ */
+async function buildHiddenNote(
+ adapter: ComputerUseHostAdapter,
+ hiddenSinceLastSeen: string[],
+): Promise {
+ if (hiddenSinceLastSeen.length === 0) return undefined;
+ const running = await adapter.executor.listRunningApps();
+ const nameOf = new Map(running.map((a) => [a.bundleId, a.displayName]));
+ const names = hiddenSinceLastSeen.map((id) => nameOf.get(id) ?? id);
+ const list = names.map((n) => `"${n}"`).join(", ");
+ const one = names.length === 1;
+ return (
+ `${list} ${one ? "was" : "were"} open and got hidden before this screenshot ` +
+ `(not in the session allowlist). If a previous action was meant to open ` +
+ `${one ? "it" : "one of them"}, that's why you don't see it — call ` +
+ `request_access to add ${one ? "it" : "them"} to the allowlist.`
+ );
+}
+
+/**
+ * Assign a human-readable label to each display. Falls back to `display N`
+ * when NSScreen.localizedName is undefined; disambiguates identical labels
+ * (matched-pair external monitors) with a `(2)` suffix. Used by both
+ * buildMonitorNote and handleSwitchDisplay so the name the model sees in a
+ * screenshot note is the same name it can pass back to switch_display.
+ */
+function uniqueDisplayLabels(
+ displays: readonly DisplayGeometry[],
+): Map {
+ // Sort by displayId so the (N) suffix is stable regardless of
+ // NSScreen.screens iteration order — same label always maps to same
+ // physical display across buildMonitorNote → switch_display round-trip,
+ // even if display configuration reorders between the two calls.
+ const sorted = [...displays].sort((a, b) => a.displayId - b.displayId);
+ const counts = new Map();
+ const out = new Map();
+ for (const d of sorted) {
+ const base = d.label ?? `display ${d.displayId}`;
+ const n = (counts.get(base) ?? 0) + 1;
+ counts.set(base, n);
+ out.set(d.displayId, n === 1 ? base : `${base} (${n})`);
+ }
+ return out;
+}
+
+/**
+ * Build the monitor-context text that accompanies a screenshot. Tells the
+ * model which monitor it's looking at (by human name), lists other attached
+ * monitors, and flags when the monitor changed vs. the previous screenshot.
+ *
+ * Only emitted when there are 2+ displays AND (first screenshot OR the
+ * display changed). Single-monitor setups and steady-state same-monitor
+ * screenshots get no text — avoids noise.
+ */
+async function buildMonitorNote(
+ adapter: ComputerUseHostAdapter,
+ shotDisplayId: number,
+ lastDisplayId: number | undefined,
+ canSwitchDisplay: boolean,
+): Promise {
+ // listDisplays failure (e.g. Swift returns zero screens during monitor
+ // hot-unplug) must not tank the screenshot — this note is optional context.
+ let displays;
+ try {
+ displays = await adapter.executor.listDisplays();
+ } catch (e) {
+ adapter.logger.warn(`[computer-use] listDisplays failed: ${String(e)}`);
+ return undefined;
+ }
+ if (displays.length < 2) return undefined;
+
+ const labels = uniqueDisplayLabels(displays);
+ const nameOf = (id: number): string => labels.get(id) ?? `display ${id}`;
+
+ const current = nameOf(shotDisplayId);
+ const others = displays
+ .filter((d) => d.displayId !== shotDisplayId)
+ .map((d) => nameOf(d.displayId));
+ const switchHint = canSwitchDisplay
+ ? " Use switch_display to capture a different monitor."
+ : "";
+ const othersList =
+ others.length > 0
+ ? ` Other attached monitors: ${others.map((n) => `"${n}"`).join(", ")}.` +
+ switchHint
+ : "";
+
+ // 0 is kCGNullDirectDisplay (sentinel from old sessions persisted
+ // pre-multimon) — treat same as undefined.
+ if (lastDisplayId === undefined || lastDisplayId === 0) {
+ return `This screenshot was taken on monitor "${current}".` + othersList;
+ }
+ if (lastDisplayId !== shotDisplayId) {
+ const prev = nameOf(lastDisplayId);
+ return (
+ `This screenshot was taken on monitor "${current}", which is different ` +
+ `from your previous screenshot (taken on "${prev}").` +
+ othersList
+ );
+ }
+ return undefined;
+}
+
+async function handleScreenshot(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ // §2 — empty allowlist → tool error, no screenshot.
+ if (overrides.allowedApps.length === 0) {
+ return errorResult(
+ "No applications are granted for this session. Call request_access first.",
+ "allowlist_empty",
+ );
+ }
+
+ // Atomic resolve→prepare→capture (one Swift call, no scheduler gap).
+ // Off → fall through to separate-calls path below.
+ if (subGates.autoTargetDisplay) {
+ // Model's explicit switch_display pin overrides everything — Swift's
+ // straight cuDisplayInfo(forDisplayID:) passthrough, no chase chain.
+ // Otherwise sticky display: only auto-resolve when the allowed-app
+ // set has changed since the display was last resolved. Prevents the
+ // resolver yanking the display on every screenshot.
+ const allowedBundleIds = overrides.allowedApps.map((a) => a.bundleId);
+ const currentAppSetKey = allowedBundleIds.slice().sort().join(",");
+ const appSetChanged = currentAppSetKey !== overrides.displayResolvedForApps;
+ const autoResolve = !overrides.displayPinnedByModel && appSetChanged;
+
+ const result = await adapter.executor.resolvePrepareCapture({
+ allowedBundleIds,
+ preferredDisplayId: overrides.selectedDisplayId,
+ autoResolve,
+ // Keep the hideBeforeAction sub-gate independently rollable —
+ // atomic path honors the same toggle the non-atomic path checks
+ // at the prepareForAction call site.
+ doHide: subGates.hideBeforeAction,
+ });
+
+ // Non-atomic path's takeScreenshotWithRetry has a MIN_SCREENSHOT_BYTES
+ // check + retry. The atomic call is expensive (resolve+prepare+capture),
+ // so no retry here — just a warning when the result is implausibly
+ // small (transient display state like sleep wake). Skip when
+ // captureError is set (base64 is intentionally empty then).
+ if (
+ result.captureError === undefined &&
+ decodedByteLength(result.base64) < MIN_SCREENSHOT_BYTES
+ ) {
+ adapter.logger.warn(
+ `[computer-use] resolvePrepareCapture result implausibly small (${decodedByteLength(result.base64)} bytes decoded) — possible transient display state`,
+ );
+ }
+
+ // Resolver picked a different display than the session had selected
+ // (host window moved, or allowed app on a different display). Write
+ // the pick back to session so teach overlay positioning and subsequent
+ // non-resolver calls track the same display. Fire-and-forget.
+ if (result.displayId !== overrides.selectedDisplayId) {
+ adapter.logger.debug(
+ `[computer-use] resolver: preferred=${overrides.selectedDisplayId} resolved=${result.displayId}`,
+ );
+ overrides.onResolvedDisplayUpdated?.(result.displayId);
+ }
+ // Record the app set this display was resolved for, so the next
+ // screenshot skips auto-resolve until the set changes again. Gated on
+ // autoResolve (not just appSetChanged) — when pinned, we didn't
+ // actually resolve, so don't update the key.
+ if (autoResolve) {
+ overrides.onDisplayResolvedForApps?.(currentAppSetKey);
+ }
+
+ // Report hidden apps only when the model has already seen the screen.
+ let hiddenSinceLastSeen: string[] = [];
+ if (overrides.lastScreenshot !== undefined) {
+ hiddenSinceLastSeen = result.hidden;
+ }
+ if (result.hidden.length > 0) {
+ overrides.onAppsHidden?.(result.hidden);
+ }
+
+ // Partial-success case: hide succeeded, capture failed (SCK perm
+ // revoked mid-session). onAppsHidden fired above so auto-unhide will
+ // restore hidden apps at turn end. Now surface the error to the model.
+ if (result.captureError !== undefined) {
+ return errorResult(result.captureError, "capture_failed");
+ }
+
+ const hiddenNote = await buildHiddenNote(adapter, hiddenSinceLastSeen);
+
+ // Cherry-pick — don't spread `result` (would leak resolver fields into lastScreenshot).
+ const shot: ScreenshotResult = {
+ base64: result.base64,
+ width: result.width,
+ height: result.height,
+ displayWidth: result.displayWidth,
+ displayHeight: result.displayHeight,
+ displayId: result.displayId,
+ originX: result.originX,
+ originY: result.originY,
+ };
+
+ const monitorNote = await buildMonitorNote(
+ adapter,
+ shot.displayId,
+ overrides.lastScreenshot?.displayId,
+ overrides.onDisplayPinned !== undefined,
+ );
+
+ return {
+ content: [
+ ...(monitorNote ? [{ type: "text" as const, text: monitorNote }] : []),
+ ...(hiddenNote ? [{ type: "text" as const, text: hiddenNote }] : []),
+ {
+ type: "image",
+ data: shot.base64,
+ mimeType: "image/jpeg",
+ },
+ ],
+ screenshot: shot,
+ };
+ }
+
+ // Same hide+defocus sequence as input actions. Screenshot needs hide too
+ // — if a non-allowlisted app is on top, SCContentFilter would composite it
+ // out, but the pixels BELOW it are what the model would see, and those are
+ // NOT what's actually there. Hiding first makes the screenshot TRUE.
+ let hiddenSinceLastSeen: string[] = [];
+ if (subGates.hideBeforeAction) {
+ const hidden = await adapter.executor.prepareForAction(
+ overrides.allowedApps.map((a) => a.bundleId),
+ overrides.selectedDisplayId,
+ );
+ // "Something appeared since the model last looked." Report whenever:
+ // (a) prepare hid something AND
+ // (b) the model has ALREADY SEEN the screen (lastScreenshot is set).
+ //
+ // (b) is the discriminator that silences the first screenshot's
+ // expected-noise hide. NOT a delta against a cumulative set — that was
+ // the earlier bug: cuHiddenDuringTurn only grows, so once Preview is in
+ // it (from the first screenshot's hide), subsequent re-hides of Preview
+ // delta to zero. The double-click → Preview opens → re-hide → silent
+ // loop never breaks.
+ //
+ // With this check: every re-hide fires. If the model loops "click → file
+ // opens in Preview → screenshot → Preview hidden", it gets told EVERY
+ // time. Eventually it'll request_access for Preview (or give up).
+ //
+ // False positive: user alt-tabs mid-turn → Safari re-hidden → reported.
+ // Rare, and "Safari appeared" is at worst mild noise — far better than
+ // the false-negative of never explaining why the file vanished.
+ if (overrides.lastScreenshot !== undefined) {
+ hiddenSinceLastSeen = hidden;
+ }
+ if (hidden.length > 0) {
+ overrides.onAppsHidden?.(hidden);
+ }
+ }
+
+ const allowedBundleIds = overrides.allowedApps.map((g) => g.bundleId);
+ const shot = await takeScreenshotWithRetry(
+ adapter.executor,
+ allowedBundleIds,
+ adapter.logger,
+ overrides.selectedDisplayId,
+ );
+
+ const hiddenNote = await buildHiddenNote(adapter, hiddenSinceLastSeen);
+
+ const monitorNote = await buildMonitorNote(
+ adapter,
+ shot.displayId,
+ overrides.lastScreenshot?.displayId,
+ overrides.onDisplayPinned !== undefined,
+ );
+
+ return {
+ content: [
+ ...(monitorNote ? [{ type: "text" as const, text: monitorNote }] : []),
+ ...(hiddenNote ? [{ type: "text" as const, text: hiddenNote }] : []),
+ {
+ type: "image",
+ data: shot.base64,
+ mimeType: "image/jpeg",
+ },
+ ],
+ // Piggybacked for serverDef.ts to stash on InternalServerContext.
+ screenshot: shot,
+ };
+}
+
+/**
+ * Region-crop upscaled screenshot. Coord invariant (computer_use_v2.py:1092):
+ * click coords ALWAYS refer to the full-screen screenshot, never the zoom.
+ * Enforced structurally: this handler's return has NO `.screenshot` field,
+ * so serverDef.ts's `if (result.screenshot)` branch cannot fire and
+ * `cuLastScreenshot` is never touched. `executor.zoom()`'s return type also
+ * lacks displayWidth/displayHeight, so it's not assignable to
+ * `ScreenshotResult` even by accident.
+ */
+async function handleZoom(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+): Promise {
+ // region: [x0, y0, x1, y1] in IMAGE-PX of lastScreenshot — same space the
+ // model reads click coords from.
+ const region = args.region;
+ if (!Array.isArray(region) || region.length !== 4) {
+ return errorResult(
+ "region must be an array of length 4: [x0, y0, x1, y1]",
+ "bad_args",
+ );
+ }
+ const [x0, y0, x1, y1] = region;
+ if (![x0, y0, x1, y1].every((v) => typeof v === "number" && v >= 0)) {
+ return errorResult(
+ "region values must be non-negative numbers",
+ "bad_args",
+ );
+ }
+ if (x1 <= x0)
+ return errorResult("region x1 must be greater than x0", "bad_args");
+ if (y1 <= y0)
+ return errorResult("region y1 must be greater than y0", "bad_args");
+
+ const last = overrides.lastScreenshot;
+ if (!last) {
+ return errorResult(
+ "take a screenshot before zooming (region coords are relative to it)",
+ "state_conflict",
+ );
+ }
+ if (x1 > last.width || y1 > last.height) {
+ return errorResult(
+ `region exceeds screenshot bounds (${last.width}×${last.height})`,
+ "bad_args",
+ );
+ }
+
+ // image-px → logical-pt. Same ratio as scaleCoord (:198-199) —
+ // displayWidth / width, not 1/scaleFactor. The ratio is folded.
+ const ratioX = last.displayWidth / last.width;
+ const ratioY = last.displayHeight / last.height;
+ const regionLogical = {
+ x: x0 * ratioX,
+ y: y0 * ratioY,
+ w: (x1 - x0) * ratioX,
+ h: (y1 - y0) * ratioY,
+ };
+
+ const allowedIds = overrides.allowedApps.map((g) => g.bundleId);
+ // Crop from the same display as lastScreenshot so the zoom region
+ // matches the image the model is reading coords from.
+ const zoomed = await adapter.executor.zoom(
+ regionLogical,
+ allowedIds,
+ last.displayId,
+ );
+
+ // Return the image. NO `.screenshot` piggyback — this is the invariant.
+ return {
+ content: [{ type: "image", data: zoomed.base64, mimeType: "image/jpeg" }],
+ };
+}
+
+/** Shared handler for all five click variants. */
+async function handleClickVariant(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+ button: "left" | "right" | "middle",
+ count: 1 | 2 | 3,
+): Promise {
+ // A prior left_mouse_down may have set mouseButtonHeld without a matching
+ // left_mouse_up (e.g. drag rejected by a tier gate, model falls back to
+ // left_click). executor.click() does its own mouseDown+mouseUp, releasing
+ // the OS button — but without this, the JS flag stays true and all
+ // subsequent mouse_move calls take the held-button path ("mouse"/
+ // "mouse_full" actionKind + hit-test), causing spurious rejections on
+ // click-tier and read-tier windows. Release first so click() gets a clean
+ // slate.
+ if (mouseButtonHeld) {
+ await adapter.executor.mouseUp();
+ mouseButtonHeld = false;
+ mouseMoved = false;
+ }
+
+ const coord = extractCoordinate(args);
+ if (coord instanceof Error) return errorResult(coord.message, "bad_args");
+ const [rawX, rawY] = coord;
+
+ // left_click(coordinate=[x,y], text="shift") — hold modifiers
+ // during the click. Same chord parsing as the key tool.
+ let modifiers: string[] | undefined;
+ if (args.text !== undefined) {
+ if (typeof args.text !== "string") {
+ return errorResult("text must be a string", "bad_args");
+ }
+ // Same gate as handleKey/handleHoldKey. withModifiers presses each name
+ // via native.key(m, "press") — a non-modifier like "q" in text="cmd+q"
+ // gets pressed while Cmd is held → Cmd+Q fires before the click.
+ if (
+ isSystemKeyCombo(args.text, adapter.executor.capabilities.platform) &&
+ !overrides.grantFlags.systemKeyCombos
+ ) {
+ return errorResult(
+ `The modifier chord "${args.text}" would fire a system shortcut. ` +
+ "Request the systemKeyCombos grant flag via request_access, or use " +
+ "only modifier keys (shift, ctrl, alt, cmd) in the text parameter.",
+ "grant_flag_required",
+ );
+ }
+ modifiers = parseKeyChord(args.text);
+ }
+
+ // Right/middle-click and any click with a modifier chord escalate to
+ // keyboard-equivalent input at tier "click" (context-menu Paste, chord
+ // keystrokes). Compute once, pass to both gates.
+ const clickActionKind: CuActionKind =
+ button !== "left" || (modifiers !== undefined && modifiers.length > 0)
+ ? "mouse_full"
+ : "mouse";
+
+ const gate = await runInputActionGates(
+ adapter,
+ overrides,
+ subGates,
+ clickActionKind,
+ );
+ if (gate) return gate;
+
+ const display = await adapter.executor.getDisplaySize(
+ overrides.selectedDisplayId,
+ );
+
+ // §6 item P — pixel-validation staleness check. Sub-gated.
+ // Runs AFTER the gates (no point validating if we're about to refuse
+ // anyway) but BEFORE the executor call.
+ if (subGates.pixelValidation) {
+ const { xPct, yPct } = coordToPercentageForPixelCompare(
+ rawX,
+ rawY,
+ overrides.coordinateMode,
+ overrides.lastScreenshot,
+ );
+ const validation = await validateClickTarget(
+ adapter.cropRawPatch,
+ overrides.lastScreenshot,
+ xPct,
+ yPct,
+ async () => {
+ // The fresh screenshot for validation uses the SAME allow-set as
+ // the model's last screenshot did, so we compare like with like.
+ const allowedIds = overrides.allowedApps.map((g) => g.bundleId);
+ try {
+ // Fresh shot must match lastScreenshot's display, not the current
+ // selection — pixel-compare is against the model's last image.
+ return await adapter.executor.screenshot({
+ allowedBundleIds: allowedIds,
+ displayId: overrides.lastScreenshot?.displayId,
+ });
+ } catch {
+ return null;
+ }
+ },
+ adapter.logger,
+ );
+ if (!validation.valid && validation.warning) {
+ // Warning result — model told to re-screenshot.
+ return okText(validation.warning);
+ }
+ }
+
+ const { x, y } = scaleCoord(
+ rawX,
+ rawY,
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+
+ const hitGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ x,
+ y,
+ clickActionKind,
+ );
+ if (hitGate) return hitGate;
+
+ await adapter.executor.click(x, y, button, count, modifiers);
+ return okText("Clicked.");
+}
+
+async function handleType(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const text = requireString(args, "text");
+ if (text instanceof Error) return errorResult(text.message, "bad_args");
+
+ const gate = await runInputActionGates(
+ adapter,
+ overrides,
+ subGates,
+ "keyboard",
+ );
+ if (gate) return gate;
+
+ // §6 item 3 — clipboard-paste fast path. On macOS we also prefer the
+ // clipboard path for ordinary text when clipboardWrite is granted, because
+ // IME/input-source state can corrupt keystroke-by-keystroke typing even for
+ // plain ASCII. The save/restore + read-back-verify lives in the EXECUTOR,
+ // not here. Here we just route.
+ const viaClipboard =
+ (text.includes("\n") ||
+ (adapter.executor.capabilities.platform === "darwin" && text.length > 1)) &&
+ overrides.grantFlags.clipboardWrite &&
+ subGates.clipboardPasteMultiline;
+
+ if (viaClipboard) {
+ await adapter.executor.type(text, { viaClipboard: true });
+ return okText("Typed (via clipboard).");
+ }
+
+ // §6 item 7 — grapheme-cluster iteration. Prevents ZWJ emoji → �.
+ // §6 item 4 — 8ms between graphemes (125 Hz USB polling). Battle-tested:
+ // sleep BEFORE each keystroke, not after.
+ //
+ // \n, \r, \t MUST route through executor.key(), not type(). Two reasons:
+ // 1. enigo.text("\n") on macOS posts a stale CGEvent with virtualKey=0
+ // after stripping the newline — virtualKey 0 is the 'a' key, so a
+ // ghost 'a' gets typed. Upstream bug in enigo 0.6.1 fast_text().
+ // 2. Unicode text-insertion of '\n' is not a Return key press. URL bars
+ // and terminals ignore it; the model's intent (submit/execute) is lost.
+ // CRLF (\r\n) is one grapheme cluster (UAX #29 GB3), so check for it too.
+ const graphemes = segmentGraphemes(text);
+ for (const [i, g] of graphemes.entries()) {
+ // Same abort check as handleComputerBatch. At 8ms/grapheme a 50-char
+ // type() runs ~400ms; this is where an in-flight batch actually
+ // spends its time.
+ if (overrides.isAborted?.()) {
+ return errorResult(
+ `Typing aborted after ${i} of ${graphemes.length} graphemes (user interrupt).`,
+ );
+ }
+ await sleep(INTER_GRAPHEME_SLEEP_MS);
+ if (g === "\n" || g === "\r" || g === "\r\n") {
+ await adapter.executor.key("return");
+ } else if (g === "\t") {
+ await adapter.executor.key("tab");
+ } else {
+ await adapter.executor.type(g, { viaClipboard: false });
+ }
+ }
+ return okText(`Typed ${graphemes.length} grapheme(s).`);
+}
+
+async function handleKey(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const keySequence = requireString(args, "text");
+ if (keySequence instanceof Error)
+ return errorResult("text is required", "bad_args");
+
+ // Cap 100, error strings match.
+ let repeat: number | undefined;
+ if (args.repeat !== undefined) {
+ if (
+ typeof args.repeat !== "number" ||
+ !Number.isInteger(args.repeat) ||
+ args.repeat < 1
+ ) {
+ return errorResult("repeat must be a positive integer", "bad_args");
+ }
+ if (args.repeat > 100) {
+ return errorResult("repeat exceeds maximum of 100", "bad_args");
+ }
+ repeat = args.repeat;
+ }
+
+ // §2 — blocklist check BEFORE gates. A blocked combo with an ungranted
+ // app frontmost should return the blocklist error, not the frontmost
+ // error — the model's fix is to request the flag, not change focus.
+ if (
+ isSystemKeyCombo(keySequence, adapter.executor.capabilities.platform) &&
+ !overrides.grantFlags.systemKeyCombos
+ ) {
+ return errorResult(
+ `"${keySequence}" is a system-level shortcut. Request the \`systemKeyCombos\` grant via request_access to use it.`,
+ "grant_flag_required",
+ );
+ }
+
+ const gate = await runInputActionGates(
+ adapter,
+ overrides,
+ subGates,
+ "keyboard",
+ );
+ if (gate) return gate;
+
+ await adapter.executor.key(keySequence, repeat);
+ return okText("Key pressed.");
+}
+
+async function handleScroll(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const coord = extractCoordinate(args);
+ if (coord instanceof Error) return errorResult(coord.message, "bad_args");
+ const [rawX, rawY] = coord;
+
+ // Uses scroll_direction + scroll_amount.
+ // Map to our dx/dy executor interface.
+ const dir = args.scroll_direction;
+ if (dir !== "up" && dir !== "down" && dir !== "left" && dir !== "right") {
+ return errorResult(
+ "scroll_direction must be 'up', 'down', 'left', or 'right'",
+ "bad_args",
+ );
+ }
+ const amount = args.scroll_amount;
+ if (typeof amount !== "number" || !Number.isInteger(amount) || amount < 0) {
+ return errorResult("scroll_amount must be a non-negative int", "bad_args");
+ }
+ if (amount > 100) {
+ return errorResult("scroll_amount exceeds maximum of 100", "bad_args");
+ }
+ // up → dy = -amount; down → dy = +amount; left → dx = -amount; right → dx = +amount.
+ const dx = dir === "left" ? -amount : dir === "right" ? amount : 0;
+ const dy = dir === "up" ? -amount : dir === "down" ? amount : 0;
+
+ const gate = await runInputActionGates(adapter, overrides, subGates, "mouse");
+ if (gate) return gate;
+
+ const display = await adapter.executor.getDisplaySize(
+ overrides.selectedDisplayId,
+ );
+ const { x, y } = scaleCoord(
+ rawX,
+ rawY,
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+
+ // When the button is held, executor.scroll's internal moveMouse generates
+ // a leftMouseDragged event (enigo reads NSEvent.pressedMouseButtons) —
+ // same mechanism as handleMoveMouse's held-button path. Upgrade the
+ // hit-test to "mouse_full" so scroll can't be used to drag-drop text onto
+ // a click-tier terminal, and mark mouseMoved so the subsequent
+ // left_mouse_up hit-tests as a drop not a click-release.
+ const hitGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ x,
+ y,
+ mouseButtonHeld ? "mouse_full" : "mouse",
+ );
+ if (hitGate) return hitGate;
+ if (mouseButtonHeld) mouseMoved = true;
+
+ await adapter.executor.scroll(x, y, dx, dy);
+ return okText("Scrolled.");
+}
+
+async function handleDrag(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ // executor.drag() does its own press+release internally. Without this
+ // defensive clear, a prior left_mouse_down leaves mouseButtonHeld=true
+ // across the drag and desyncs the flag from OS state — same mechanism as
+ // the handleClickVariant clear above. Release first so drag() gets a
+ // clean slate.
+ if (mouseButtonHeld) {
+ await adapter.executor.mouseUp();
+ mouseButtonHeld = false;
+ mouseMoved = false;
+ }
+
+ // `coordinate` is the END point
+ // (required). `start_coordinate` is OPTIONAL — when omitted, drag from
+ // current cursor position.
+ const endCoord = extractCoordinate(args, "coordinate");
+ if (endCoord instanceof Error)
+ return errorResult(endCoord.message, "bad_args");
+ const rawTo = endCoord;
+
+ let rawFrom: [number, number] | undefined;
+ if (args.start_coordinate !== undefined) {
+ const startCoord = extractCoordinate(args, "start_coordinate");
+ if (startCoord instanceof Error)
+ return errorResult(startCoord.message, "bad_args");
+ rawFrom = startCoord;
+ }
+ // else: rawFrom stays undefined → executor drags from current cursor.
+
+ const gate = await runInputActionGates(adapter, overrides, subGates, "mouse");
+ if (gate) return gate;
+
+ const display = await adapter.executor.getDisplaySize(
+ overrides.selectedDisplayId,
+ );
+ const from =
+ rawFrom === undefined
+ ? undefined
+ : scaleCoord(
+ rawFrom[0],
+ rawFrom[1],
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+ const to = scaleCoord(
+ rawTo[0],
+ rawTo[1],
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+
+ // Check both drag endpoints. `from` is where the mouseDown happens (picks
+ // up), `to` is where mouseUp happens (drops). When start_coordinate is
+ // omitted the drag begins at the cursor — same bypass as mouse_move →
+ // left_mouse_down, so read the cursor and hit-test it (mirrors
+ // handleLeftMouseDown).
+ //
+ // The `to` endpoint uses "mouse_full" (not "mouse"): dropping text onto a
+ // terminal inserts it as if typed (macOS text drag-drop). Same threat as
+ // right-click→Paste. `from` stays "mouse" — picking up is a read.
+ const fromPoint = from ?? (await adapter.executor.getCursorPosition());
+ const fromGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ fromPoint.x,
+ fromPoint.y,
+ "mouse",
+ );
+ if (fromGate) return fromGate;
+ const toGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ to.x,
+ to.y,
+ "mouse_full",
+ );
+ if (toGate) return toGate;
+
+ await adapter.executor.drag(from, to);
+ return okText("Dragged.");
+}
+
+async function handleMoveMouse(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const coord = extractCoordinate(args);
+ if (coord instanceof Error) return errorResult(coord.message, "bad_args");
+ const [rawX, rawY] = coord;
+
+ // When the button is held, moveMouse generates leftMouseDragged events on
+ // the window under the cursor — that's interaction, not positioning.
+ // Upgrade to "mouse" and hit-test the destination. When the button is NOT
+ // held: pure positioning, passes at any tier, no hit-test (mouseDown/Up
+ // hit-test the cursor to close the mouse_move→left_mouse_down decomposition).
+ const actionKind: CuActionKind = mouseButtonHeld ? "mouse" : "mouse_position";
+ const gate = await runInputActionGates(
+ adapter,
+ overrides,
+ subGates,
+ actionKind,
+ );
+ if (gate) return gate;
+
+ const display = await adapter.executor.getDisplaySize(
+ overrides.selectedDisplayId,
+ );
+ const { x, y } = scaleCoord(
+ rawX,
+ rawY,
+ overrides.coordinateMode,
+ display,
+ overrides.lastScreenshot,
+ adapter.logger,
+ );
+
+ if (mouseButtonHeld) {
+ // "mouse_full" — same as left_click_drag's to-endpoint. Dragging onto a
+ // click-tier terminal is text injection regardless of which primitive
+ // (atomic drag vs. decomposed down/move/up) delivers the events.
+ const hitGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ x,
+ y,
+ "mouse_full",
+ );
+ if (hitGate) return hitGate;
+ }
+
+ await adapter.executor.moveMouse(x, y);
+ if (mouseButtonHeld) mouseMoved = true;
+ return okText("Moved.");
+}
+
+async function handleOpenApplication(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+): Promise {
+ const app = requireString(args, "app");
+ if (app instanceof Error) return errorResult(app.message, "bad_args");
+
+ // Resolve display-name → bundle ID. Same logic as request_access.
+ const allowed = new Set(overrides.allowedApps.map((g) => g.bundleId));
+ let targetBundleId: string | undefined;
+
+ if (looksLikeBundleId(app) && allowed.has(app)) {
+ targetBundleId = app;
+ } else {
+ // Try display name → bundle ID, but ONLY against the allowlist itself.
+ // Avoids paying the listInstalledApps() cost on the hot path and is
+ // arguably more correct: if the user granted "Slack", the model asking
+ // to open "Slack" should match THAT grant.
+ const match = overrides.allowedApps.find(
+ (g) => g.displayName.toLowerCase() === app.toLowerCase(),
+ );
+ targetBundleId = match?.bundleId;
+ }
+
+ if (!targetBundleId || !allowed.has(targetBundleId)) {
+ return errorResult(
+ `"${app}" is not granted for this session. Call request_access first.`,
+ "app_not_granted",
+ );
+ }
+
+ // open_application works at any tier — bringing an app forward is exactly
+ // what tier "read" enables (you need it on screen to screenshot it). The
+ // tier gates on click/type catch any follow-up interaction.
+
+ await adapter.executor.openApp(targetBundleId);
+
+ // On multi-monitor setups, macOS may place the opened window on a monitor
+ // the resolver won't pick (e.g. Claude + another allowed app are co-located
+ // elsewhere). Nudge the model toward switch_display BEFORE it wastes steps
+ // clicking on dock icons. Single-monitor → no hint. listDisplays failure is
+ // non-fatal — the hint is advisory.
+ if (overrides.onDisplayPinned !== undefined) {
+ let displayCount = 1;
+ try {
+ displayCount = (await adapter.executor.listDisplays()).length;
+ } catch {
+ // hint skipped
+ }
+ if (displayCount >= 2) {
+ return okText(
+ `Opened "${app}". If it isn't visible in the next screenshot, it may ` +
+ `have opened on a different monitor — use switch_display to check.`,
+ );
+ }
+ }
+
+ return okText(`Opened "${app}".`);
+}
+
+async function handleSwitchDisplay(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+): Promise {
+ const display = requireString(args, "display");
+ if (display instanceof Error) return errorResult(display.message, "bad_args");
+
+ if (!overrides.onDisplayPinned) {
+ return errorResult(
+ "Display switching is not available in this session.",
+ "feature_unavailable",
+ );
+ }
+
+ if (display.toLowerCase() === "auto") {
+ overrides.onDisplayPinned(undefined);
+ return okText(
+ "Returned to automatic monitor selection. Call screenshot to continue.",
+ );
+ }
+
+ // Resolve label → displayId fresh. Same source buildMonitorNote reads,
+ // so whatever name the model saw in a screenshot note resolves here.
+ let displays;
+ try {
+ displays = await adapter.executor.listDisplays();
+ } catch (e) {
+ return errorResult(
+ `Failed to enumerate displays: ${String(e)}`,
+ "display_error",
+ );
+ }
+
+ if (displays.length < 2) {
+ return errorResult(
+ "Only one monitor is connected. There is nothing to switch to.",
+ "bad_args",
+ );
+ }
+
+ const labels = uniqueDisplayLabels(displays);
+ const wanted = display.toLowerCase();
+ const target = displays.find(
+ (d) => labels.get(d.displayId)?.toLowerCase() === wanted,
+ );
+ if (!target) {
+ const available = displays
+ .map((d) => `"${labels.get(d.displayId)}"`)
+ .join(", ");
+ return errorResult(
+ `No monitor named "${display}" is connected. Available monitors: ${available}.`,
+ "bad_args",
+ );
+ }
+
+ overrides.onDisplayPinned(target.displayId);
+ return okText(
+ `Switched to monitor "${labels.get(target.displayId)}". Call screenshot to see it.`,
+ );
+}
+
+function handleListGrantedApplications(
+ overrides: ComputerUseOverrides,
+): CuCallToolResult {
+ return okJson({
+ allowedApps: overrides.allowedApps,
+ grantFlags: overrides.grantFlags,
+ });
+}
+
+async function handleReadClipboard(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ if (!overrides.grantFlags.clipboardRead) {
+ return errorResult(
+ "Clipboard read is not granted. Request `clipboardRead` via request_access.",
+ "grant_flag_required",
+ );
+ }
+
+ // read_clipboard doesn't route through runInputActionGates — sync here so
+ // reading after clicking into a click-tier app sees the cleared clipboard
+ // (same as what the app's own Paste would see).
+ if (subGates.clipboardGuard) {
+ const frontmost = await adapter.executor.getFrontmostApp();
+ const tierByBundleId = new Map(
+ overrides.allowedApps.map((a) => [a.bundleId, a.tier] as const),
+ );
+ const frontmostTier = frontmost
+ ? tierByBundleId.get(frontmost.bundleId)
+ : undefined;
+ await syncClipboardStash(adapter, overrides, frontmostTier === "click");
+ }
+
+ // clipboardGuard may have stashed+cleared — read the actual (possibly
+ // empty) clipboard. The agent sees what the app would see.
+ const text = await adapter.executor.readClipboard();
+ return okJson({ text });
+}
+
+async function handleWriteClipboard(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ if (!overrides.grantFlags.clipboardWrite) {
+ return errorResult(
+ "Clipboard write is not granted. Request `clipboardWrite` via request_access.",
+ "grant_flag_required",
+ );
+ }
+ const text = requireString(args, "text");
+ if (text instanceof Error) return errorResult(text.message, "bad_args");
+
+ if (subGates.clipboardGuard) {
+ const frontmost = await adapter.executor.getFrontmostApp();
+ const tierByBundleId = new Map(
+ overrides.allowedApps.map((a) => [a.bundleId, a.tier] as const),
+ );
+ const frontmostTier = frontmost
+ ? tierByBundleId.get(frontmost.bundleId)
+ : undefined;
+
+ // Defense-in-depth for the clipboardGuard bypass: write_clipboard +
+ // left_click on a click-tier app's UI Paste button. The re-clear in
+ // syncClipboardStash already defeats it (the next action clobbers the
+ // write), but rejecting here gives the agent a clear signal instead of
+ // silently voiding its write.
+ if (frontmost && frontmostTier === "click") {
+ return errorResult(
+ `"${frontmost.displayName}" is a tier-"click" app and currently ` +
+ `frontmost. write_clipboard is blocked because the next action ` +
+ `would clear the clipboard anyway — a UI Paste button in this ` +
+ `app cannot be used to inject text. Bring a tier-"full" app ` +
+ `forward before writing to the clipboard.` +
+ TIER_ANTI_SUBVERSION,
+ "tier_insufficient",
+ );
+ }
+
+ // write_clipboard doesn't route through runInputActionGates — sync here
+ // so clicking away from a click-tier app then writing restores the user's
+ // stash before the agent's text lands.
+ await syncClipboardStash(adapter, overrides, frontmostTier === "click");
+ }
+
+ await adapter.executor.writeClipboard(text);
+ return okText("Clipboard written.");
+}
+
+/**
+ * wait(duration=N). Sleeps N seconds, capped at 100.
+ * No frontmost gate — no input, nothing to protect. Kill-switch + TCC
+ * are checked in handleToolCall before dispatch reaches here.
+ */
+async function handleWait(
+ args: Record,
+): Promise {
+ const duration = args.duration;
+ if (typeof duration !== "number" || !Number.isFinite(duration)) {
+ return errorResult("duration must be a number", "bad_args");
+ }
+ if (duration < 0) {
+ return errorResult("duration must be non-negative", "bad_args");
+ }
+ if (duration > 100) {
+ return errorResult(
+ "duration is too long. Duration is in seconds.",
+ "bad_args",
+ );
+ }
+ await sleep(duration * 1000);
+ return okText(`Waited ${duration}s.`);
+}
+
+/**
+ * Returns "X=...,Y=..." plain text. We return richer JSON with
+ * coordinateSpace annotation — the model handles both shapes.
+ *
+ * When lastScreenshot is present: inverse of scaleCoord — logical points →
+ * image-pixels via `imageX = logicalX × (screenshotWidth / displayWidth)`.
+ * Uses capture-time dims so the returned coords match what the model would
+ * read off that screenshot.
+ *
+ * No frontmost gate — read-only, no input.
+ */
+async function handleCursorPosition(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+): Promise {
+ const logical = await adapter.executor.getCursorPosition();
+ const shot = overrides.lastScreenshot;
+ if (shot) {
+ // Inverse of scaleCoord: subtract capture-time origin to go from
+ // virtual-screen to display-relative before the image-px transform.
+ const localX = logical.x - shot.originX;
+ const localY = logical.y - shot.originY;
+ // Cursor off the captured display (multi-monitor): local coords go
+ // negative or exceed display dims. Return logical_points + hint rather
+ // than garbage image-px.
+ if (
+ localX < 0 ||
+ localX > shot.displayWidth ||
+ localY < 0 ||
+ localY > shot.displayHeight
+ ) {
+ return okJson({
+ x: logical.x,
+ y: logical.y,
+ coordinateSpace: "logical_points",
+ note: "cursor is on a different monitor than your last screenshot; take a fresh screenshot",
+ });
+ }
+ const x = Math.round(localX * (shot.width / shot.displayWidth));
+ const y = Math.round(localY * (shot.height / shot.displayHeight));
+ return okJson({ x, y, coordinateSpace: "image_pixels" });
+ }
+ return okJson({
+ x: logical.x,
+ y: logical.y,
+ coordinateSpace: "logical_points",
+ note: "take a screenshot first for image-pixel coordinates",
+ });
+}
+
+/**
+ * Presses each key in the
+ * chord, sleeps duration seconds, releases in reverse. Same duration bounds
+ * as wait. Keyboard action → frontmost gate applies; same systemKeyCombos
+ * blocklist check as key.
+ */
+async function handleHoldKey(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const text = requireString(args, "text");
+ if (text instanceof Error) return errorResult(text.message, "bad_args");
+
+ const duration = args.duration;
+ if (typeof duration !== "number" || !Number.isFinite(duration)) {
+ return errorResult("duration must be a number", "bad_args");
+ }
+ if (duration < 0) {
+ return errorResult("duration must be non-negative", "bad_args");
+ }
+ if (duration > 100) {
+ return errorResult(
+ "duration is too long. Duration is in seconds.",
+ "bad_args",
+ );
+ }
+
+ // Blocklist check BEFORE gates — same reasoning as handleKey. Holding
+ // cmd+q is just as dangerous as tapping it.
+ if (
+ isSystemKeyCombo(text, adapter.executor.capabilities.platform) &&
+ !overrides.grantFlags.systemKeyCombos
+ ) {
+ return errorResult(
+ `"${text}" is a system-level shortcut. Request the \`systemKeyCombos\` grant via request_access to use it.`,
+ "grant_flag_required",
+ );
+ }
+
+ const gate = await runInputActionGates(
+ adapter,
+ overrides,
+ subGates,
+ "keyboard",
+ );
+ if (gate) return gate;
+
+ const keyNames = parseKeyChord(text);
+ await adapter.executor.holdKey(keyNames, duration * 1000);
+ return okText("Key held.");
+}
+
+/**
+ * Raw press at current cursor, no coordinate.
+ * Move first with mouse_move. Errors if already held.
+ */
+async function handleLeftMouseDown(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ if (mouseButtonHeld) {
+ return errorResult(
+ "mouse button already held, call left_mouse_up first",
+ "state_conflict",
+ );
+ }
+
+ const gate = await runInputActionGates(adapter, overrides, subGates, "mouse");
+ if (gate) return gate;
+
+ // macOS routes mouseDown to the window under the cursor, not the frontmost
+ // app. Without this hit-test, mouse_move (positioning, passes at any tier)
+ // + left_mouse_down decomposes a click that lands on a tier-"read" window
+ // overlapping a tier-"full" frontmost app — bypassing runHitTestGate's
+ // whole purpose. All three are batchable, so the bypass is atomic.
+ const cursor = await adapter.executor.getCursorPosition();
+ const hitGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ cursor.x,
+ cursor.y,
+ "mouse",
+ );
+ if (hitGate) return hitGate;
+
+ await adapter.executor.mouseDown();
+ mouseButtonHeld = true;
+ mouseMoved = false;
+ return okText("Mouse button pressed.");
+}
+
+/**
+ * Raw release at current cursor. Does NOT error
+ * if not held (idempotent release).
+ */
+async function handleLeftMouseUp(
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ // Any gate rejection here must release the button FIRST — otherwise the
+ // OS button stays pressed and mouseButtonHeld stays true. Recovery
+ // attempts (mouse_move back to a safe app) would generate leftMouseDragged
+ // events into whatever window is under the cursor, including the very
+ // read-tier window the gate was protecting. A single mouseUp on a
+ // restricted window is one event; a stuck button is cascading damage.
+ //
+ // This includes the frontmost gate: focus can change between mouseDown and
+ // mouseUp (something else grabbed focus), in which case runInputActionGates
+ // rejects here even though it passed at mouseDown.
+ const releaseFirst = async (
+ err: CuCallToolResult,
+ ): Promise => {
+ await adapter.executor.mouseUp();
+ mouseButtonHeld = false;
+ mouseMoved = false;
+ return err;
+ };
+
+ const gate = await runInputActionGates(adapter, overrides, subGates, "mouse");
+ if (gate) return releaseFirst(gate);
+
+ // When the cursor moved since mouseDown, this is a drop (text-injection
+ // vector) — hit-test at "mouse_full" same as left_click_drag's `to`. When
+ // NO move happened, this is a click-release — same semantics as the atomic
+ // left_click, hit-test at "mouse". Without this distinction, a decomposed
+ // click on a click-tier app fails here while the atomic left_click works,
+ // and releaseFirst fires mouseUp anyway so the OS sees a complete click
+ // while the model gets a misleading error.
+ const cursor = await adapter.executor.getCursorPosition();
+ const hitGate = await runHitTestGate(
+ adapter,
+ overrides,
+ subGates,
+ cursor.x,
+ cursor.y,
+ mouseMoved ? "mouse_full" : "mouse",
+ );
+ if (hitGate) return releaseFirst(hitGate);
+
+ await adapter.executor.mouseUp();
+ mouseButtonHeld = false;
+ mouseMoved = false;
+ return okText("Mouse button released.");
+}
+
+// ---------------------------------------------------------------------------
+// Batch dispatch
+// ---------------------------------------------------------------------------
+
+/**
+ * Actions allowed inside a computer_batch call. Excludes request_access,
+ * open_application, clipboard, list_granted (no latency benefit, complicates
+ * security model).
+ */
+const BATCHABLE_ACTIONS: ReadonlySet = new Set([
+ "key",
+ "type",
+ "mouse_move",
+ "left_click",
+ "left_click_drag",
+ "right_click",
+ "middle_click",
+ "double_click",
+ "triple_click",
+ "scroll",
+ "hold_key",
+ "screenshot",
+ "cursor_position",
+ "left_mouse_down",
+ "left_mouse_up",
+ "wait",
+]);
+
+interface BatchActionResult {
+ action: string;
+ ok: boolean;
+ output: string;
+}
+
+/**
+ * Executes `actions: [{action, …}, …]`
+ * sequentially in ONE model→API round trip — the dominant latency cost
+ * (seconds, vs. ~50ms local overhead per action).
+ *
+ * Gate semantics (the security model):
+ * - Kill-switch + TCC: checked ONCE by handleToolCall before reaching here.
+ * - prepareForAction: run ONCE at the top. The user approved "do this
+ * sequence"; hiding apps per-action is wasted work and fast-pathed anyway.
+ * - Frontmost gate: checked PER ACTION. State can change mid-batch — a
+ * click might open a non-allowed app. This is the safety net: if action
+ * 3 of 5 opened Safari (not allowed), action 4's frontmost check fires
+ * and stops the batch there.
+ * - PixelCompare: SKIPPED inside batch. The model committed to the full
+ * sequence without intermediate screenshots; validating mid-batch clicks
+ * against a pre-batch screenshot would false-positive constantly.
+ *
+ * Both skips are implemented by passing `{...subGates, hideBeforeAction:
+ * false, pixelValidation: false}` to each inner dispatch — the handlers'
+ * existing gate logic does the right thing, no new code paths.
+ *
+ * Stop-on-first-error: accumulate results, on
+ * first `isError` stop executing, return everything so far + the error. The
+ * model sees exactly where the batch broke and what succeeded before it.
+ *
+ * Mid-batch screenshots are allowed (for inspection) but NEVER piggyback —
+ * their `.screenshot` field is dropped. Same invariant as zoom: click coords
+ * always refer to the PRE-BATCH `lastScreenshot`. If the model wants to click
+ * based on a new screenshot, it ends the batch and screenshots separately.
+ */
+async function handleComputerBatch(
+ adapter: ComputerUseHostAdapter,
+ args: Record,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ const actions = args.actions;
+ if (!Array.isArray(actions) || actions.length === 0) {
+ return errorResult("actions must be a non-empty array", "bad_args");
+ }
+
+ for (const [i, act] of actions.entries()) {
+ if (typeof act !== "object" || act === null) {
+ return errorResult(`actions[${i}] must be an object`, "bad_args");
+ }
+ const action = (act as Record).action;
+ if (typeof action !== "string") {
+ return errorResult(`actions[${i}].action must be a string`, "bad_args");
+ }
+ if (!BATCHABLE_ACTIONS.has(action)) {
+ return errorResult(
+ `actions[${i}].action="${action}" is not allowed in a batch. ` +
+ `Allowed: ${[...BATCHABLE_ACTIONS].join(", ")}.`,
+ "bad_args",
+ );
+ }
+ }
+
+ // prepareForAction ONCE. After this, inner dispatches skip it via
+ // hideBeforeAction:false.
+ if (subGates.hideBeforeAction) {
+ const hidden = await adapter.executor.prepareForAction(
+ overrides.allowedApps.map((a) => a.bundleId),
+ overrides.selectedDisplayId,
+ );
+ if (hidden.length > 0) {
+ overrides.onAppsHidden?.(hidden);
+ }
+ }
+
+ // Inner actions: skip prepare (already ran), skip pixelCompare (stale by
+ // design). Frontmost still checked — runInputActionGates does it
+ // unconditionally.
+ const batchSubGates: CuSubGates = {
+ ...subGates,
+ hideBeforeAction: false,
+ pixelValidation: false,
+ // Batch already took its screenshot (appended at end); a mid-batch
+ // resolver switch would make that screenshot inconsistent with
+ // earlier clicks' lastScreenshot-based scaleCoord targeting.
+ autoTargetDisplay: false,
+ };
+
+ const results: BatchActionResult[] = [];
+ for (const [i, act] of actions.entries()) {
+ // Overlay Stop → host's stopSession → lifecycleState leaves "running"
+ // synchronously before query.interrupt(). The SDK abort tears down the
+ // host's await but not this loop — without this check the remaining
+ // actions fire into a dead session.
+ if (overrides.isAborted?.()) {
+ await releaseHeldMouse(adapter);
+ return errorResult(
+ `Batch aborted after ${results.length} of ${actions.length} actions (user interrupt).`,
+ );
+ }
+
+ // Small inter-step settle. Synthetic CGEvents post instantly; some apps
+ // need a tick to process step N's input before step N+1 lands (e.g. a
+ // click opening a menu before the next click targets a menu item).
+ if (i > 0) await sleep(10);
+
+ const actionArgs = act as Record;
+ const action = actionArgs.action as string;
+
+ // Drop mid-batch screenshot piggyback (strip .screenshot). Click coords
+ // stay anchored to the pre-batch lastScreenshot.
+ const { screenshot: _dropped, ...inner } = await dispatchAction(
+ action,
+ actionArgs,
+ adapter,
+ overrides,
+ batchSubGates,
+ );
+
+ const text = firstTextContent(inner);
+ const result = { action, ok: !inner.isError, output: text };
+ results.push(result);
+
+ if (inner.isError) {
+ // Stop-on-first-error. Return everything so far + the error.
+ // Forward the inner action's telemetry (error_kind) so cu_tool_call
+ // reflects the actual failure — without this, batch-internal errors
+ // emit error_kind: undefined despite the inner handler tagging it.
+ // Release held mouse: the error may be a mid-grapheme abort in
+ // handleType, or a frontmost gate, landing between mouse_down and
+ // mouse_up.
+ await releaseHeldMouse(adapter);
+ return okJson(
+ {
+ completed: results.slice(0, -1),
+ failed: result,
+ remaining: actions.length - results.length,
+ },
+ inner.telemetry,
+ );
+ }
+ }
+
+ return okJson({ completed: results });
+}
+
+function firstTextContent(r: CuCallToolResult): string {
+ const first = r.content[0];
+ return first && first.type === "text" ? first.text : "";
+}
+
+/**
+ * Action dispatch shared by handleToolCall and handleComputerBatch. Called
+ * AFTER kill-switch + TCC gates have passed. Never sees request_access — it's
+ * special-cased in handleToolCall for the tccState thread-through.
+ */
+async function dispatchAction(
+ name: string,
+ a: Record,
+ adapter: ComputerUseHostAdapter,
+ overrides: ComputerUseOverrides,
+ subGates: CuSubGates,
+): Promise {
+ switch (name) {
+ case "screenshot":
+ return handleScreenshot(adapter, overrides, subGates);
+
+ case "zoom":
+ return handleZoom(adapter, a, overrides);
+
+ case "left_click":
+ return handleClickVariant(adapter, a, overrides, subGates, "left", 1);
+ case "double_click":
+ return handleClickVariant(adapter, a, overrides, subGates, "left", 2);
+ case "triple_click":
+ return handleClickVariant(adapter, a, overrides, subGates, "left", 3);
+ case "right_click":
+ return handleClickVariant(adapter, a, overrides, subGates, "right", 1);
+ case "middle_click":
+ return handleClickVariant(adapter, a, overrides, subGates, "middle", 1);
+
+ case "type":
+ return handleType(adapter, a, overrides, subGates);
+
+ case "key":
+ return handleKey(adapter, a, overrides, subGates);
+
+ case "scroll":
+ return handleScroll(adapter, a, overrides, subGates);
+
+ case "left_click_drag":
+ return handleDrag(adapter, a, overrides, subGates);
+
+ case "mouse_move":
+ return handleMoveMouse(adapter, a, overrides, subGates);
+
+ case "wait":
+ return handleWait(a);
+
+ case "cursor_position":
+ return handleCursorPosition(adapter, overrides);
+
+ case "hold_key":
+ return handleHoldKey(adapter, a, overrides, subGates);
+
+ case "left_mouse_down":
+ return handleLeftMouseDown(adapter, overrides, subGates);
+
+ case "left_mouse_up":
+ return handleLeftMouseUp(adapter, overrides, subGates);
+
+ case "open_application":
+ return handleOpenApplication(adapter, a, overrides);
+
+ case "switch_display":
+ return handleSwitchDisplay(adapter, a, overrides);
+
+ case "list_granted_applications":
+ return handleListGrantedApplications(overrides);
+
+ case "read_clipboard":
+ return handleReadClipboard(adapter, overrides, subGates);
+
+ case "write_clipboard":
+ return handleWriteClipboard(adapter, a, overrides, subGates);
+
+ case "computer_batch":
+ return handleComputerBatch(adapter, a, overrides, subGates);
+
+ default:
+ return errorResult(`Unknown tool "${name}".`, "bad_args");
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Main dispatch
+// ---------------------------------------------------------------------------
+
+export async function handleToolCall(
+ adapter: ComputerUseHostAdapter,
+ name: string,
+ args: unknown,
+ rawOverrides: ComputerUseOverrides,
+): Promise {
+ const { logger, serverName } = adapter;
+
+ // Normalize the allowlist before any gate runs:
+ //
+ // (a) Strip user-denied. A grant from a previous session (before the user
+ // added the app to Settings → Desktop app → Computer Use → Denied apps)
+ // must not survive. Without
+ // this, a stale grant bypasses the auto-deny. Stripped silently — the
+ // agent already saw the userDenied guidance at request_access time, and
+ // a live frontmost-gate rejection cites "not in allowed applications".
+ //
+ // (b) Strip policy-denied. Same story as (a) for a grant that predates a
+ // blocklist addition. buildAccessRequest denies these up front for new
+ // requests; this catches stale persisted grants.
+ //
+ // (c) Backfill tier. A grant persisted before the tier field existed has
+ // `tier: undefined`, which `tierSatisfies` treats as `"full"` — wrong
+ // for a legacy Chrome grant. Assign the hardcoded tier based on
+ // bundle-ID category. Modern grants already have a tier.
+ //
+ // `.some()` guard keeps the hot path (empty deny list, no legacy grants)
+ // zero-alloc.
+ const userDeniedSet = new Set(rawOverrides.userDeniedBundleIds);
+ const overrides: ComputerUseOverrides = rawOverrides.allowedApps.some(
+ (a) =>
+ a.tier === undefined ||
+ userDeniedSet.has(a.bundleId) ||
+ isPolicyDenied(a.bundleId, a.displayName),
+ )
+ ? {
+ ...rawOverrides,
+ allowedApps: rawOverrides.allowedApps
+ .filter((a) => !userDeniedSet.has(a.bundleId))
+ .filter((a) => !isPolicyDenied(a.bundleId, a.displayName))
+ .map((a) =>
+ a.tier !== undefined
+ ? a
+ : { ...a, tier: getDefaultTierForApp(a.bundleId, a.displayName) },
+ ),
+ }
+ : rawOverrides;
+
+ // ─── Gate 1: kill switch ─────────────────────────────────────────────
+ if (adapter.isDisabled()) {
+ return errorResult(
+ "Computer control is disabled in Settings. Enable it and try again.",
+ "other",
+ );
+ }
+
+ // ─── Gate 2: TCC ─────────────────────────────────────────────────────
+ // Accessibility + Screen Recording on macOS. Pure check — no dialog,
+ // no relaunch. `request_access` is exempted: it threads the ungranted
+ // state through to the renderer, which shows a TCC toggle panel instead
+ // of the app list. Every other tool short-circuits here.
+ const osPerms = await adapter.ensureOsPermissions();
+ let tccState:
+ | { accessibility: boolean; screenRecording: boolean }
+ | undefined;
+ if (!osPerms.granted) {
+ // Both request_* tools thread tccState through to the renderer's
+ // TCC toggle panel. Every other tool short-circuits.
+ if (name !== "request_access" && name !== "request_teach_access") {
+ return errorResult(
+ "Accessibility and Screen Recording permissions are required. " +
+ "Call request_access to show the permission panel.",
+ "tcc_not_granted",
+ );
+ }
+ tccState = {
+ accessibility: osPerms.accessibility,
+ screenRecording: osPerms.screenRecording,
+ };
+ }
+
+ // ─── Gate 3: global CU lock ──────────────────────────────────────────
+ // At most one session uses CU at a time. Every tool including
+ // request_access hits the CHECK — even showing the approval dialog while
+ // another session holds the lock would be confusing ("why approve access
+ // that can't be used?").
+ //
+ // But ACQUIRE is split: request_access and list_granted_applications
+ // check-without-acquire (the overlay + notifications are driven by
+ // cuLockChanged, and showing "Claude is using your computer" while the
+ // agent is only ASKING for access is premature). First action tool
+ // acquires and the overlay appears. If the user denies and no action
+ // follows, the overlay never shows.
+ //
+ // request_teach_access is NOT in this set — approving teach mode HIDES
+ // the main window (via onTeachModeActivated), and the lock must be held
+ // before that happens. Otherwise a concurrent session's request_access
+ // would render its dialog in an invisible main window during the gap
+ // between hide and the first teach_step (seconds of model inference).
+ // The old acquire-always-at-Gate-3 behavior was correct for teach; only
+ // the non-teach permission tools benefit from deferral.
+ //
+ // Host releases on idle/stop/archive; this package never releases. Both
+ // Cowork (LAM) and CCD (LSM) wire checkCuLock via the shared cuLock
+ // singleton. When undefined (tests/future hosts), no gate — absence of
+ // the mechanism ≠ locked out.
+ const deferAcquire = defersLockAcquire(name);
+ const lock = overrides.checkCuLock?.();
+ if (lock) {
+ if (lock.holder !== undefined && !lock.isSelf) {
+ return errorResult(
+ "Another Claude session is currently using the computer. Wait for " +
+ "the user to acknowledge it is finished (stop button in the Claude " +
+ "window), or find a non-computer-use approach if one is readily " +
+ "apparent.",
+ "cu_lock_held",
+ );
+ }
+ if (lock.holder === undefined && !deferAcquire) {
+ // Acquire. Emits cuLockChanged → overlay shows. Idempotent — if
+ // someone else acquired between check and here (won't happen on a
+ // single-threaded event loop, but defensive), this is a no-op.
+ overrides.acquireCuLock?.();
+ // Fresh lock holder → any prior session's mouseButtonHeld is stale
+ // (e.g. overlay stop mid-drag). Clear it so this session doesn't get
+ // a spurious "already held" error. resetMouseButtonHeld is file-local;
+ // this is the one non-test callsite.
+ resetMouseButtonHeld();
+ }
+ // lock.isSelf → already held by us, proceed.
+ // lock.holder === undefined && deferAcquire →
+ // checked but not acquired — proceed, first action will acquire.
+ }
+
+ // Sub-gates read FRESH every call so a GrowthBook flip takes effect
+ // mid-session (plan §3).
+ const subGates = adapter.getSubGates();
+
+ // Clipboard guard runs per-action inside runInputActionGates + inline in
+ // handleReadClipboard/handleWriteClipboard. NOT here — per-tool-call sync
+ // would run once for computer_batch and miss sub-actions 2..N, and would
+ // fire during deferAcquire tools / `wait` / teach_step's blocking-dialog
+ // phase where no input is happening.
+
+ const a = asRecord(args);
+
+ logger.silly(
+ `[${serverName}] tool=${name} args=${JSON.stringify(a).slice(0, 200)}`,
+ );
+
+ // ─── Fail-closed dispatch ────────────────────────────────────────────
+ // ANY exception below → tool error, executor never left in a half-called
+ // state. Explicit inversion of the prior `catch → return true` fail-open.
+ try {
+ // request_access / request_teach_access: need tccState thread-through;
+ // dispatchAction never sees them (not batchable).
+ // teach_step: blocking UI tool, also not batchable; needs subGates for
+ // its action-execution phase.
+ if (name === "request_access") {
+ return await handleRequestAccess(adapter, a, overrides, tccState);
+ }
+ if (name === "request_teach_access") {
+ return await handleRequestTeachAccess(adapter, a, overrides, tccState);
+ }
+ if (name === "teach_step") {
+ return await handleTeachStep(adapter, a, overrides, subGates);
+ }
+ if (name === "teach_batch") {
+ return await handleTeachBatch(adapter, a, overrides, subGates);
+ }
+ return await dispatchAction(name, a, adapter, overrides, subGates);
+ } catch (err) {
+ // Fail-closed. If the gate machinery itself throws (e.g.
+ // getFrontmostApp() rejects), the executor has NOT been called yet for
+ // the gated tools — the gates run before the executor in every handler.
+ // For ungated tools, the executor may have been mid-call; that's fine —
+ // the result is still a tool error, never an implicit success.
+ const msg = err instanceof Error ? err.message : String(err);
+ logger.error(`[${serverName}] tool=${name} threw: ${msg}`, err);
+ return errorResult(`Tool "${name}" failed: ${msg}`, "executor_threw");
+ }
+}
+
+export const _test = {
+ scaleCoord,
+ coordToPercentageForPixelCompare,
+ segmentGraphemes,
+ decodedByteLength,
+ resolveRequestedApps,
+ buildAccessRequest,
+ buildTierGuidanceMessage,
+ buildUserDeniedGuidance,
+ tierSatisfies,
+ looksLikeBundleId,
+ extractCoordinate,
+ parseKeyChord,
+ buildMonitorNote,
+ handleSwitchDisplay,
+ uniqueDisplayLabels,
+};
diff --git a/src/vendor/computer-use-mcp/tools.ts b/src/vendor/computer-use-mcp/tools.ts
new file mode 100644
index 00000000..c744a232
--- /dev/null
+++ b/src/vendor/computer-use-mcp/tools.ts
@@ -0,0 +1,706 @@
+/**
+ * MCP tool schemas for the computer-use server. Mirrors
+ * claude-for-chrome-mcp/src/browserTools.ts in shape (plain `Tool`-shaped
+ * object literals, no zod).
+ *
+ * Coordinate descriptions are baked in at tool-list build time from the
+ * `chicago_coordinate_mode` gate. The model sees exactly ONE coordinate
+ * convention in the param descriptions and never learns the other exists.
+ * The host (`serverDef.ts`) reads the same frozen gate value for
+ * `scaleCoord` — both must agree or clicks land in the wrong space.
+ */
+
+import type { Tool } from "@modelcontextprotocol/sdk/types.js";
+
+import type { CoordinateMode } from "./types.js";
+
+// See packages/desktop/computer-use-mcp/COORDINATES.md before touching any
+// model-facing coordinate text. Chrome's browserTools.ts:143 is the reference
+// phrasing — "pixels from the left edge", no geometry, no number to do math with.
+const COORD_DESC: Record = {
+ pixels: {
+ x: "Horizontal pixel position read directly from the most recent screenshot image, measured from the left edge. The server handles all scaling.",
+ y: "Vertical pixel position read directly from the most recent screenshot image, measured from the top edge. The server handles all scaling.",
+ },
+ normalized_0_100: {
+ x: "Horizontal position as a percentage of screen width, 0.0–100.0 (0 = left edge, 100 = right edge).",
+ y: "Vertical position as a percentage of screen height, 0.0–100.0 (0 = top edge, 100 = bottom edge).",
+ },
+};
+
+const FRONTMOST_GATE_DESC =
+ "The frontmost application must be in the session allowlist at the time of this call, or this tool returns an error and does nothing.";
+
+/**
+ * Item schema for the `actions` array in `computer_batch`, `teach_step`, and
+ * `teach_batch`. All three dispatch through the same `dispatchAction` path
+ * with the same validation — keep this enum in sync with `BATCHABLE_ACTIONS`
+ * in toolCalls.ts.
+ */
+const BATCH_ACTION_ITEM_SCHEMA = {
+ type: "object",
+ properties: {
+ action: {
+ type: "string",
+ enum: [
+ "key",
+ "type",
+ "mouse_move",
+ "left_click",
+ "left_click_drag",
+ "right_click",
+ "middle_click",
+ "double_click",
+ "triple_click",
+ "scroll",
+ "hold_key",
+ "screenshot",
+ "cursor_position",
+ "left_mouse_down",
+ "left_mouse_up",
+ "wait",
+ ],
+ description: "The action to perform.",
+ },
+ coordinate: {
+ type: "array",
+ items: { type: "number" },
+ minItems: 2,
+ maxItems: 2,
+ description:
+ "(x, y) for click/mouse_move/scroll/left_click_drag end point.",
+ },
+ start_coordinate: {
+ type: "array",
+ items: { type: "number" },
+ minItems: 2,
+ maxItems: 2,
+ description:
+ "(x, y) drag start — left_click_drag only. Omit to drag from current cursor.",
+ },
+ text: {
+ type: "string",
+ description:
+ "For type: the text. For key/hold_key: the chord string. For click/scroll: modifier keys to hold.",
+ },
+ scroll_direction: {
+ type: "string",
+ enum: ["up", "down", "left", "right"],
+ },
+ scroll_amount: { type: "integer", minimum: 0, maximum: 100 },
+ duration: {
+ type: "number",
+ description: "Seconds (0–100). For hold_key/wait.",
+ },
+ repeat: {
+ type: "integer",
+ minimum: 1,
+ maximum: 100,
+ description: "For key: repeat count.",
+ },
+ },
+ required: ["action"],
+};
+
+/**
+ * Build the tool list. Parameterized by capabilities and coordinate mode so
+ * descriptions are honest and unambiguous (plan §1 — "Unfiltered + honest").
+ *
+ * `coordinateMode` MUST match what the host passes to `scaleCoord` at tool-
+ * -call time. Both should read the same frozen-at-load gate constant.
+ *
+ * `installedAppNames` — optional pre-sanitized list of app display names to
+ * enumerate in the `request_access` description. The caller is responsible
+ * for sanitization (length cap, character allowlist, sort, count cap) —
+ * this function just splices the list into the description verbatim. Omit
+ * to fall back to the generic "display names or bundle IDs" wording.
+ */
+export function buildComputerUseTools(
+ caps: {
+ screenshotFiltering: "native" | "none";
+ platform: "darwin" | "win32";
+ /** Include request_teach_access + teach_step. Read once at server construction. */
+ teachMode?: boolean;
+ },
+ coordinateMode: CoordinateMode,
+ installedAppNames?: string[],
+): Tool[] {
+ const coord = COORD_DESC[coordinateMode];
+
+ // Shared hint suffix for BOTH request_access and request_teach_access —
+ // they use the same resolveRequestedApps path, so the model should get
+ // the same enumeration for both.
+ const installedAppsHint =
+ installedAppNames && installedAppNames.length > 0
+ ? ` Available applications on this machine: ${installedAppNames.join(", ")}.`
+ : "";
+
+ // [x, y]` tuple — param shape for all
+ // click/move/scroll tools.
+ const coordinateTuple = {
+ type: "array",
+ items: { type: "number" },
+ minItems: 2,
+ maxItems: 2,
+ description: `(x, y): ${coord.x}`,
+ };
+ // Modifier hold during click. Shared across all 5 click variants.
+ const clickModifierText = {
+ type: "string",
+ description:
+ 'Modifier keys to hold during the click (e.g. "shift", "ctrl+shift"). Supports the same syntax as the key tool.',
+ };
+
+ const screenshotDesc =
+ caps.screenshotFiltering === "native"
+ ? "Take a screenshot of the primary display. Applications not in the session allowlist are excluded at the compositor level — only granted apps and the desktop are visible."
+ : "Take a screenshot of the primary display. On this platform, screenshots are NOT filtered — all open windows are visible. Input actions targeting apps not in the session allowlist are rejected.";
+
+ return [
+ {
+ name: "request_access",
+ description:
+ "Request user permission to control a set of applications for this session. Must be called before any other tool in this server. " +
+ "The user sees a single dialog listing all requested apps and either allows the whole set or denies it. " +
+ "Call this again mid-session to add more apps; previously granted apps remain granted. " +
+ "Returns the granted apps, denied apps, and screenshot filtering capability.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ apps: {
+ type: "array",
+ items: { type: "string" },
+ description:
+ "Application display names (e.g. \"Slack\", \"Calendar\") or bundle identifiers (e.g. \"com.tinyspeck.slackmacgap\"). Display names are resolved case-insensitively against installed apps." +
+ installedAppsHint,
+ },
+ reason: {
+ type: "string",
+ description:
+ "One-sentence explanation shown to the user in the approval dialog. Explain the task, not the mechanism.",
+ },
+ clipboardRead: {
+ type: "boolean",
+ description:
+ "Also request permission to read the user's clipboard (separate checkbox in the dialog).",
+ },
+ clipboardWrite: {
+ type: "boolean",
+ description:
+ "Also request permission to write the user's clipboard. When granted, multi-line `type` calls use the clipboard fast path.",
+ },
+ systemKeyCombos: {
+ type: "boolean",
+ description:
+ "Also request permission to send system-level key combos (quit app, switch app, lock screen). Without this, those specific combos are blocked.",
+ },
+ },
+ required: ["apps", "reason"],
+ },
+ },
+
+ {
+ name: "screenshot",
+ description:
+ screenshotDesc +
+ " Returns an error if the allowlist is empty. The returned image is what subsequent click coordinates are relative to.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ save_to_disk: {
+ type: "boolean",
+ description:
+ "Save the image to disk so it can be attached to a message for the user. Returns the saved path in the tool result. Only set this when you intend to share the image — screenshots you're just looking at don't need saving.",
+ },
+ },
+ required: [],
+ },
+ },
+
+ {
+ name: "zoom",
+ description:
+ "Take a higher-resolution screenshot of a specific region of the last full-screen screenshot. Use this liberally to inspect small text, button labels, or fine UI details that are hard to read in the downsampled full-screen image. " +
+ "IMPORTANT: Coordinates in subsequent click calls always refer to the full-screen screenshot, never the zoomed image. This tool is read-only for inspecting detail.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ region: {
+ type: "array",
+ items: { type: "integer" },
+ minItems: 4,
+ maxItems: 4,
+ description:
+ "(x0, y0, x1, y1): Rectangle to zoom into, in the coordinate space of the most recent full-screen screenshot. x0,y0 = top-left, x1,y1 = bottom-right.",
+ },
+ save_to_disk: {
+ type: "boolean",
+ description:
+ "Save the image to disk so it can be attached to a message for the user. Returns the saved path in the tool result. Only set this when you intend to share the image.",
+ },
+ },
+ required: ["region"],
+ },
+ },
+
+ {
+ name: "left_click",
+ description: `Left-click at the given coordinates. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ text: clickModifierText,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "double_click",
+ description: `Double-click at the given coordinates. Selects a word in most text editors. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ text: clickModifierText,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "triple_click",
+ description: `Triple-click at the given coordinates. Selects a line in most text editors. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ text: clickModifierText,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "right_click",
+ description: `Right-click at the given coordinates. Opens a context menu in most applications. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ text: clickModifierText,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "middle_click",
+ description: `Middle-click (scroll-wheel click) at the given coordinates. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ text: clickModifierText,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "type",
+ description: `Type text into whatever currently has keyboard focus. ${FRONTMOST_GATE_DESC} Newlines are supported. For keyboard shortcuts use \`key\` instead.`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ text: { type: "string", description: "Text to type." },
+ },
+ required: ["text"],
+ },
+ },
+
+ {
+ name: "key",
+ description:
+ `Press a key or key combination (e.g. "return", "escape", "cmd+a", "ctrl+shift+tab"). ${FRONTMOST_GATE_DESC} ` +
+ "System-level combos (quit app, switch app, lock screen) require the `systemKeyCombos` grant — without it they return an error. All other combos work.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ text: {
+ type: "string",
+ description: 'Modifiers joined with "+", e.g. "cmd+shift+a".',
+ },
+ repeat: {
+ type: "integer",
+ minimum: 1,
+ maximum: 100,
+ description: "Number of times to repeat the key press. Default is 1.",
+ },
+ },
+ required: ["text"],
+ },
+ },
+
+ {
+ name: "scroll",
+ description: `Scroll at the given coordinates. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ scroll_direction: {
+ type: "string",
+ enum: ["up", "down", "left", "right"],
+ description: "Direction to scroll.",
+ },
+ scroll_amount: {
+ type: "integer",
+ minimum: 0,
+ maximum: 100,
+ description: "Number of scroll ticks.",
+ },
+ },
+ required: ["coordinate", "scroll_direction", "scroll_amount"],
+ },
+ },
+
+ {
+ name: "left_click_drag",
+ description: `Press, move to target, and release. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: {
+ ...coordinateTuple,
+ description: `(x, y) end point: ${coord.x}`,
+ },
+ start_coordinate: {
+ ...coordinateTuple,
+ description: `(x, y) start point. If omitted, drags from the current cursor position. ${coord.x}`,
+ },
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "mouse_move",
+ description: `Move the mouse cursor without clicking. Useful for triggering hover states. ${FRONTMOST_GATE_DESC}`,
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ coordinate: coordinateTuple,
+ },
+ required: ["coordinate"],
+ },
+ },
+
+ {
+ name: "open_application",
+ description:
+ "Bring an application to the front, launching it if necessary. The target application must already be in the session allowlist — call request_access first.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ app: {
+ type: "string",
+ description:
+ "Display name (e.g. \"Slack\") or bundle identifier (e.g. \"com.tinyspeck.slackmacgap\").",
+ },
+ },
+ required: ["app"],
+ },
+ },
+
+ {
+ name: "switch_display",
+ description:
+ "Switch which monitor subsequent screenshots capture. Use this when the " +
+ "application you need is on a different monitor than the one shown. " +
+ "The screenshot tool tells you which monitor it captured and lists " +
+ "other attached monitors by name — pass one of those names here. " +
+ "After switching, call screenshot to see the new monitor. " +
+ 'Pass "auto" to return to automatic monitor selection.',
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ display: {
+ type: "string",
+ description:
+ 'Monitor name from the screenshot note (e.g. "Built-in Retina Display", ' +
+ '"LG UltraFine"), or "auto" to re-enable automatic selection.',
+ },
+ },
+ required: ["display"],
+ },
+ },
+
+ {
+ name: "list_granted_applications",
+ description:
+ "List the applications currently in the session allowlist, plus the active grant flags and coordinate mode. No side effects.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {},
+ required: [],
+ },
+ },
+
+ {
+ name: "read_clipboard",
+ description:
+ "Read the current clipboard contents as text. Requires the `clipboardRead` grant.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {},
+ required: [],
+ },
+ },
+
+ {
+ name: "write_clipboard",
+ description:
+ "Write text to the clipboard. Requires the `clipboardWrite` grant.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ text: { type: "string" },
+ },
+ required: ["text"],
+ },
+ },
+
+ {
+ name: "wait",
+ description: "Wait for a specified duration.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ duration: {
+ type: "number",
+ description: "Duration in seconds (0–100).",
+ },
+ },
+ required: ["duration"],
+ },
+ },
+
+ {
+ name: "cursor_position",
+ description:
+ "Get the current mouse cursor position. Returns image-pixel coordinates relative to the most recent screenshot, or logical points if no screenshot has been taken.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {},
+ required: [],
+ },
+ },
+
+ {
+ name: "hold_key",
+ description:
+ `Press and hold a key or key combination for the specified duration, then release. ${FRONTMOST_GATE_DESC} ` +
+ "System-level combos require the `systemKeyCombos` grant.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ text: {
+ type: "string",
+ description: 'Key or chord to hold, e.g. "space", "shift+down".',
+ },
+ duration: {
+ type: "number",
+ description: "Duration in seconds (0–100).",
+ },
+ },
+ required: ["text", "duration"],
+ },
+ },
+
+ {
+ name: "left_mouse_down",
+ description:
+ `Press the left mouse button at the current cursor position and leave it held. ${FRONTMOST_GATE_DESC} ` +
+ "Use mouse_move first to position the cursor. Call left_mouse_up to release. Errors if the button is already held.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {},
+ required: [],
+ },
+ },
+
+ {
+ name: "left_mouse_up",
+ description:
+ `Release the left mouse button at the current cursor position. ${FRONTMOST_GATE_DESC} ` +
+ "Pairs with left_mouse_down. Safe to call even if the button is not currently held.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {},
+ required: [],
+ },
+ },
+
+ {
+ name: "computer_batch",
+ description:
+ "Execute a sequence of actions in ONE tool call. Each individual tool call requires a model→API round trip (seconds); " +
+ "batching a predictable sequence eliminates all but one. Use this whenever you can predict the outcome of several actions ahead — " +
+ "e.g. click a field, type into it, press Return. Actions execute sequentially and stop on the first error. " +
+ `${FRONTMOST_GATE_DESC} The frontmost check runs before EACH action inside the batch — if an action opens a non-allowed app, the next action's gate fires and the batch stops there. ` +
+ "Mid-batch screenshot actions are allowed for inspection but coordinates in subsequent clicks always refer to the PRE-BATCH full-screen screenshot.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ actions: {
+ type: "array",
+ minItems: 1,
+ items: BATCH_ACTION_ITEM_SCHEMA,
+ description:
+ 'List of actions. Example: [{"action":"left_click","coordinate":[100,200]},{"action":"type","text":"hello"},{"action":"key","text":"Return"}]',
+ },
+ },
+ required: ["actions"],
+ },
+ },
+
+ ...(caps.teachMode ? buildTeachTools(coord, installedAppsHint) : []),
+ ];
+}
+
+/**
+ * Teach-mode tools. Split out so the spread above stays a single expression;
+ * takes `coord` so `teach_step.anchor`'s description uses the same
+ * frozen coordinate-mode phrasing as click coords, and `installedAppsHint`
+ * so `request_teach_access.apps` gets the same enumeration as
+ * `request_access.apps` (same resolution path → same hint).
+ */
+function buildTeachTools(
+ coord: { x: string; y: string },
+ installedAppsHint: string,
+): Tool[] {
+ // Shared between teach_step (top-level) and teach_batch (inside steps[]
+ // items). Depends on coord, so it lives inside this factory.
+ const teachStepProperties = {
+ explanation: {
+ type: "string",
+ description:
+ "Tooltip body text. Explain what the user is looking at and why it matters. " +
+ "This is the ONLY place the user sees your words — be complete but concise.",
+ },
+ next_preview: {
+ type: "string",
+ description:
+ "One line describing exactly what will happen when the user clicks Next. " +
+ 'Example: "Next: I\'ll click Create Bucket and type the name." ' +
+ "Shown below the explanation in a smaller font.",
+ },
+ anchor: {
+ type: "array",
+ items: { type: "number" },
+ minItems: 2,
+ maxItems: 2,
+ description:
+ `(x, y) — where the tooltip arrow points. ${coord.x} ` +
+ "Omit to center the tooltip with no arrow (for general-context steps).",
+ },
+ actions: {
+ type: "array",
+ // Empty allowed — "read this, click Next" steps.
+ items: BATCH_ACTION_ITEM_SCHEMA,
+ description:
+ "Actions to execute when the user clicks Next. Same item schema as computer_batch.actions. " +
+ "Empty array is valid for purely explanatory steps. Actions run sequentially and stop on first error.",
+ },
+ } as const;
+
+ return [
+ {
+ name: "request_teach_access",
+ description:
+ "Request permission to guide the user through a task step-by-step with on-screen tooltips. " +
+ "Use this INSTEAD OF request_access when the user wants to LEARN how to do something " +
+ '(phrases like "teach me", "walk me through", "show me how", "help me learn"). ' +
+ "On approval the main Claude window hides and a fullscreen tooltip overlay appears. " +
+ "You then call teach_step repeatedly; each call shows one tooltip and waits for the user to click Next. " +
+ "Same app-allowlist semantics as request_access, but no clipboard/system-key flags. " +
+ "Teach mode ends automatically when your turn ends.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ apps: {
+ type: "array",
+ items: { type: "string" },
+ description:
+ 'Application display names (e.g. "Slack", "Calendar") or bundle identifiers. Resolved case-insensitively against installed apps.' +
+ installedAppsHint,
+ },
+ reason: {
+ type: "string",
+ description:
+ 'What you will be teaching. Shown in the approval dialog as "Claude wants to guide you through {reason}". Keep it short and task-focused.',
+ },
+ },
+ required: ["apps", "reason"],
+ },
+ },
+
+ {
+ name: "teach_step",
+ description:
+ "Show one guided-tour tooltip and wait for the user to click Next. On Next, execute the actions, " +
+ "take a fresh screenshot, and return both — you do NOT need a separate screenshot call between steps. " +
+ "The returned image shows the state after your actions ran; anchor the next teach_step against it. " +
+ "IMPORTANT — the user only sees the tooltip during teach mode. Put ALL narration in `explanation`. " +
+ "Text you emit outside teach_step calls is NOT visible until teach mode ends. " +
+ "Pack as many actions as possible into each step's `actions` array — the user waits through " +
+ "the whole round trip between clicks, so one step that fills a form beats five steps that fill one field each. " +
+ "Returns {exited:true} if the user clicks Exit — do not call teach_step again after that. " +
+ "Take an initial screenshot before your FIRST teach_step to anchor it.",
+ inputSchema: {
+ type: "object" as const,
+ properties: teachStepProperties,
+ required: ["explanation", "next_preview", "actions"],
+ },
+ },
+
+ {
+ name: "teach_batch",
+ description:
+ "Queue multiple teach steps in one tool call. Parallels computer_batch: " +
+ "N steps → one model↔API round trip instead of N. Each step still shows a tooltip " +
+ "and waits for the user's Next click, but YOU aren't waiting for a round trip between steps. " +
+ "You can call teach_batch multiple times in one tour — treat each batch as one predictable " +
+ "SEGMENT (typically: all the steps on one page). The returned screenshot shows the state " +
+ "after the batch's final actions; anchor the NEXT teach_batch against it. " +
+ "WITHIN a batch, all anchors and click coordinates refer to the PRE-BATCH screenshot " +
+ "(same invariant as computer_batch) — for steps 2+ in a batch, either omit anchor " +
+ "(centered tooltip) or target elements you know won't have moved. " +
+ "Good pattern: batch 5 tooltips on page A (last step navigates) → read returned screenshot → " +
+ "batch 3 tooltips on page B → done. " +
+ "Returns {exited:true, stepsCompleted:N} if the user clicks Exit — do NOT call again after that; " +
+ "{stepsCompleted, stepFailed, ...} if an action errors mid-batch; " +
+ "otherwise {stepsCompleted, results:[...]} plus a final screenshot. " +
+ "Fall back to individual teach_step calls when you need to react to each intermediate screenshot.",
+ inputSchema: {
+ type: "object" as const,
+ properties: {
+ steps: {
+ type: "array",
+ minItems: 1,
+ items: {
+ type: "object",
+ properties: teachStepProperties,
+ required: ["explanation", "next_preview", "actions"],
+ },
+ description:
+ "Ordered steps. Validated upfront — a typo in step 5 errors before any tooltip shows.",
+ },
+ },
+ required: ["steps"],
+ },
+ },
+ ];
+}
diff --git a/src/vendor/computer-use-mcp/types.ts b/src/vendor/computer-use-mcp/types.ts
new file mode 100644
index 00000000..3519a733
--- /dev/null
+++ b/src/vendor/computer-use-mcp/types.ts
@@ -0,0 +1,621 @@
+import type {
+ ComputerExecutor,
+ InstalledApp,
+ ScreenshotResult,
+} from "./executor.js";
+
+/** `ScreenshotResult` without the base64 blob. The shape hosts persist for
+ * cross-respawn `scaleCoord` survival. */
+export type ScreenshotDims = Omit;
+
+/** Shape mirrors claude-for-chrome-mcp/src/types.ts:1-7 */
+export interface Logger {
+ info: (message: string, ...args: unknown[]) => void;
+ error: (message: string, ...args: unknown[]) => void;
+ warn: (message: string, ...args: unknown[]) => void;
+ debug: (message: string, ...args: unknown[]) => void;
+ silly: (message: string, ...args: unknown[]) => void;
+}
+
+/**
+ * Per-app permission tier. Hardcoded by category at grant time — the
+ * approval dialog displays the tier but the user cannot change it (for now).
+ *
+ * - `"read"` — visible in screenshots, NO interaction (no clicks, no typing).
+ * Browsers land here: the model can read a page that's already open, but
+ * must use the Claude-in-Chrome MCP for any navigation/clicking. Trading
+ * platforms land here too (no CiC alternative — the model asks the user).
+ * - `"click"` — visible + plain left-click, scroll. NO typing/keys,
+ * NO right/middle-click, NO modifier-clicks, NO drag-drop (all text-
+ * injection vectors). Terminals/IDEs land here: the model can click a
+ * Run button or scroll test output, but `type("rm -rf /")` is blocked
+ * and so is right-click→Paste and dragging text onto the terminal.
+ * - `"full"` — visible + click + type/key/paste. Everything else.
+ *
+ * Enforced in `runInputActionGates` via the frontmost-app check: keyboard
+ * actions require `"full"`, mouse actions require `"click"` or higher.
+ */
+export type CuAppPermTier = "read" | "click" | "full";
+
+/**
+ * A single app the user has approved for the current session. Session-scoped
+ * only — there is no "once" or "forever" scope (unlike Chrome's per-domain
+ * three-way). CU has no natural "once" unit; one task = hundreds of clicks.
+ * Mirrors how `chromeAllowedDomains` is a plain `string[]` with no per-item
+ * scope.
+ */
+export interface AppGrant {
+ bundleId: string;
+ displayName: string;
+ /** Epoch ms. For Settings-page display ("Granted 3m ago"). */
+ grantedAt: number;
+ /** Undefined → `"full"` (back-compat for pre-tier grants persisted in
+ * session state). */
+ tier?: CuAppPermTier;
+}
+
+/** Orthogonal to the app allowlist. */
+export interface CuGrantFlags {
+ clipboardRead: boolean;
+ clipboardWrite: boolean;
+ /**
+ * When false, the `key` tool rejects combos in `keyBlocklist.ts`
+ * (cmd+q, cmd+tab, cmd+space, cmd+shift+q, ctrl+alt+delete). All other
+ * key sequences work regardless.
+ */
+ systemKeyCombos: boolean;
+}
+
+export const DEFAULT_GRANT_FLAGS: CuGrantFlags = {
+ clipboardRead: false,
+ clipboardWrite: false,
+ systemKeyCombos: false,
+};
+
+/**
+ * Host picks via GrowthBook JSON feature `chicago_coordinate_mode`, baked
+ * into tool param descriptions at server-construction time. The model sees
+ * ONE convention and never learns the other exists. `normalized_0_100`
+ * sidesteps the Retina scaleFactor bug class entirely.
+ */
+export type CoordinateMode = "pixels" | "normalized_0_100";
+
+/**
+ * Independent kill switches for subtle/risky ported behaviors. Read from
+ * GrowthBook by the host adapter, consulted in `toolCalls.ts`.
+ */
+export interface CuSubGates {
+ /** 9×9 exact-byte staleness guard before click. */
+ pixelValidation: boolean;
+ /** Route `type("foo\nbar")` through clipboard instead of keystroke-by-keystroke. */
+ clipboardPasteMultiline: boolean;
+ /**
+ * Ease-out-cubic mouse glide at 60fps, distance-proportional duration
+ * (2000 px/sec, capped at 0.5s). Adds up to ~0.5s latency
+ * per click. When off, cursor teleports instantly.
+ */
+ mouseAnimation: boolean;
+ /**
+ * Pre-action sequence: hide non-allowlisted apps, then defocus us (from the
+ * Vercept acquisition). When off, the
+ * frontmost gate fires in the normal case and the model gets stuck — this
+ * is the A/B-test-the-old-broken-behavior switch.
+ */
+ hideBeforeAction: boolean;
+ /**
+ * Auto-resolve the target display before each screenshot when the
+ * selected display has no allowed-app windows. When on, `handleScreenshot`
+ * uses the atomic Swift path; off → sticks with `selectedDisplayId`.
+ */
+ autoTargetDisplay: boolean;
+ /**
+ * Stash+clear the clipboard while a tier-"click" app is frontmost.
+ * Closes the gap where a click-tier terminal/IDE has a UI Paste button
+ * that's plain-left-clickable — without this, the tier "click"
+ * keyboard block can be routed around by clicking Paste. Restored when
+ * a non-"click" app becomes frontmost, or at turn end.
+ */
+ clipboardGuard: boolean;
+}
+
+// ----------------------------------------------------------------------------
+// Permission request/response (mirror of BridgePermissionRequest, types.ts:77-94)
+// ----------------------------------------------------------------------------
+
+/** One entry per app the model asked for, after name → bundle ID resolution. */
+export interface ResolvedAppRequest {
+ /** What the model asked for (e.g. "Slack", "com.tinyspeck.slackmacgap"). */
+ requestedName: string;
+ /** The resolved InstalledApp if found, else undefined (shown greyed in the UI). */
+ resolved?: InstalledApp;
+ /** Shell-access-equivalent bundle IDs get a UI warning. See sentinelApps.ts. */
+ isSentinel: boolean;
+ /** Already in the allowlist → skip the checkbox, return in `granted` immediately. */
+ alreadyGranted: boolean;
+ /** Hardcoded tier for this app (browser→"read", terminal→"click", else "full").
+ * The dialog displays this read-only; the renderer passes it through
+ * verbatim in the AppGrant. */
+ proposedTier: CuAppPermTier;
+}
+
+/**
+ * Payload for the renderer approval dialog. Rides through the existing
+ * `ToolPermissionRequest.input: unknown` field
+ * (packages/utils/desktop/bridge/common/claude.web.ts:1262) — no IPC schema
+ * change needed.
+ */
+export interface CuPermissionRequest {
+ requestId: string;
+ /** Model-provided reason string. Shown prominently in the approval UI. */
+ reason: string;
+ apps: ResolvedAppRequest[];
+ /** What the model asked for. User can toggle independently of apps. */
+ requestedFlags: Partial;
+ /**
+ * For the "On Windows, Claude can see all apps..." footnote. Taken from
+ * `executor.capabilities.screenshotFiltering` so the renderer doesn't
+ * need to know about platforms.
+ */
+ screenshotFiltering: "native" | "none";
+ /**
+ * Present only when TCC permissions are NOT yet granted. When present,
+ * the renderer shows a TCC toggle panel (two rows: Accessibility, Screen
+ * Recording) INSTEAD OF the app list. Clicking a row's "Request" button
+ * triggers the OS prompt; the store polls on window-focus and flips the
+ * toggle when the grant is detected. macOS itself prompts the user to
+ * restart after granting Screen Recording — we don't.
+ */
+ tccState?: {
+ accessibility: boolean;
+ screenRecording: boolean;
+ };
+ /**
+ * Apps with windows on the CU display that aren't in the requested
+ * allowlist. These will be hidden the first time Claude takes an action.
+ * Computed at request_access time — may be slightly stale by the time the
+ * user clicks Allow, but it's a preview, not a contract. Absent when
+ * empty so the renderer can skip the section cleanly.
+ */
+ willHide?: Array<{ bundleId: string; displayName: string }>;
+ /**
+ * `chicagoAutoUnhide` app preference at request time. The renderer picks
+ * between "...then restored when Claude is done" and "...will be hidden"
+ * copy. Absent when `willHide` is absent (same condition).
+ */
+ autoUnhideEnabled?: boolean;
+}
+
+/**
+ * What the renderer stuffs into `updatedInput._cuGrants` when the user clicks
+ * "Allow for this session" (mirror of the `_allowAllSites` sentinel at
+ * LocalAgentModeSessionManager.ts:2794).
+ */
+export interface CuPermissionResponse {
+ granted: AppGrant[];
+ /** Bundle IDs the user unchecked, or apps that weren't installed. */
+ denied: Array<{ bundleId: string; reason: "user_denied" | "not_installed" }>;
+ flags: CuGrantFlags;
+ /**
+ * Whether the user clicked Allow in THIS dialog. Only set by the
+ * teach-mode handler — regular request_access doesn't need it (the
+ * session manager's `result.behavior` gates the merge there). Needed
+ * because when all requested apps are already granted (skipDialogGrants
+ * non-empty, needDialog empty), Allow and Deny produce identical
+ * `{granted:[], denied:[]}` payloads and the tool handler can't tell
+ * them apart without this. Undefined → legacy/regular path, do not
+ * gate on it.
+ */
+ userConsented?: boolean;
+}
+
+// ----------------------------------------------------------------------------
+// Host adapter (mirror of ClaudeForChromeContext, types.ts:33-62)
+// ----------------------------------------------------------------------------
+
+/**
+ * Process-lifetime singleton dependencies. Everything that does NOT vary per
+ * tool call. Built once by `apps/desktop/src/main/nest-only/chicago/hostAdapter.ts`.
+ * No Electron imports in this package — the host injects everything.
+ */
+export interface ComputerUseHostAdapter {
+ serverName: string;
+ logger: Logger;
+ executor: ComputerExecutor;
+
+ /**
+ * TCC state check — Accessibility + Screen Recording on macOS. Pure check,
+ * no dialog, no relaunch. When either is missing, `request_access` threads
+ * the state through to the renderer which shows a toggle panel; all other
+ * tools return a tool error.
+ */
+ ensureOsPermissions(): Promise<
+ { granted: boolean; accessibility?: boolean; screenRecording?: boolean }
+ >;
+
+ /** The Settings-page kill switch (`chicagoEnabled` app preference). */
+ isDisabled(): boolean;
+
+ /**
+ * The `chicagoAutoUnhide` app preference. Consumed by `buildAccessRequest`
+ * to populate `CuPermissionRequest.autoUnhideEnabled` so the renderer's
+ * "will be hidden" copy can say "then restored" only when true.
+ */
+ getAutoUnhideEnabled(): boolean;
+
+ /**
+ * Sub-gates re-read on every tool call so GrowthBook flips take effect
+ * mid-session without restart.
+ */
+ getSubGates(): CuSubGates;
+
+ /**
+ * JPEG decode + crop + raw pixel bytes, for the PixelCompare staleness guard.
+ * Injected so this package stays Electron-free. The host implements it via
+ * `nativeImage.createFromBuffer(jpeg).crop(rect).toBitmap()` — Chromium's
+ * decoders, BSD-licensed, no `.node` binary.
+ *
+ * Returns null on decode/crop failure — caller treats null as `skipped`,
+ * click proceeds (validation failure must never block the action).
+ */
+ cropRawPatch(
+ jpegBase64: string,
+ rect: { x: number; y: number; width: number; height: number },
+ ): Buffer | null;
+}
+
+// ----------------------------------------------------------------------------
+// Session context (getter/callback bag for bindSessionContext)
+// ----------------------------------------------------------------------------
+
+/**
+ * Per-session state binding for `bindSessionContext`. Hosts build this once
+ * per session with getters that read fresh from their session store and
+ * callbacks that write back. The returned dispatcher builds
+ * `ComputerUseOverrides` from these getters on every call.
+ *
+ * Callbacks must be set at construction time — `bindSessionContext` reads
+ * them once at bind, not per call.
+ *
+ * The lock hooks are **async** — `bindSessionContext` awaits them before
+ * `handleToolCall`, then passes `checkCuLock: undefined` in overrides so the
+ * sync Gate-3 in `handleToolCall` no-ops. Hosts with in-memory sync locks
+ * (Cowork) wrap them trivially; hosts with cross-process locks (the CLI's
+ * O_EXCL file) call the real async primitive directly.
+ */
+export interface ComputerUseSessionContext {
+ // ── Read state fresh per call ──────────────────────────────────────
+
+ getAllowedApps(): readonly AppGrant[];
+ getGrantFlags(): CuGrantFlags;
+ /** Per-user auto-deny list (Settings page). Empty array = none. */
+ getUserDeniedBundleIds(): readonly string[];
+ getSelectedDisplayId(): number | undefined;
+ getDisplayPinnedByModel?(): boolean;
+ getDisplayResolvedForApps?(): string | undefined;
+ getTeachModeActive?(): boolean;
+ /** Dims-only fallback when `lastScreenshot` is unset (cross-respawn).
+ * `bindSessionContext` reconstructs `{...dims, base64: ""}` so scaleCoord
+ * works and pixelCompare correctly skips. */
+ getLastScreenshotDims?(): ScreenshotDims | undefined;
+
+ // ── Write-back callbacks ───────────────────────────────────────────
+
+ /** Shows the approval dialog. Host routes to its UI, awaits user. The
+ * signal is aborted if the tool call finishes before the user answers
+ * (MCP timeout, etc.) — hosts dismiss the dialog on abort. */
+ onPermissionRequest?(
+ req: CuPermissionRequest,
+ signal: AbortSignal,
+ ): Promise;
+ /** Teach-mode sibling of `onPermissionRequest`. */
+ onTeachPermissionRequest?(
+ req: CuTeachPermissionRequest,
+ signal: AbortSignal,
+ ): Promise;
+ /** Called by `bindSessionContext` after merging a permission response into
+ * the allowlist (dedupe on bundleId, truthy-only flag spread). Host
+ * persists for resume survival. */
+ onAllowedAppsChanged?(apps: readonly AppGrant[], flags: CuGrantFlags): void;
+ onAppsHidden?(bundleIds: string[]): void;
+ /** Reads the session's clipboardGuard stash. undefined → no stash held. */
+ getClipboardStash?(): string | undefined;
+ /** Writes the clipboardGuard stash. undefined clears it. */
+ onClipboardStashChanged?(stash: string | undefined): void;
+ onResolvedDisplayUpdated?(displayId: number): void;
+ onDisplayPinned?(displayId: number | undefined): void;
+ onDisplayResolvedForApps?(sortedBundleIdsKey: string): void;
+ /** Called after each screenshot. Host persists for respawn survival. */
+ onScreenshotCaptured?(dims: ScreenshotDims): void;
+ onTeachModeActivated?(): void;
+ onTeachStep?(req: TeachStepRequest): Promise;
+ onTeachWorking?(): void;
+
+ // ── Lock (async) ───────────────────────────────────────────────────
+
+ /** At most one session uses CU at a time. Awaited by `bindSessionContext`
+ * before dispatch. Undefined → no lock gating (proceed). */
+ checkCuLock?(): Promise<{ holder: string | undefined; isSelf: boolean }>;
+ /** Take the lock. Called when `checkCuLock` returned `holder: undefined`
+ * on a non-deferring tool. Host emits enter-CU signals here. */
+ acquireCuLock?(): Promise;
+ /** Host-specific lock-held error text. Default is the package's generic
+ * message. The CLI host includes the holder session-ID prefix. */
+ formatLockHeldMessage?(holder: string): string;
+
+ /** User-abort signal. Passed through to `ComputerUseOverrides.isAborted`
+ * for the mid-loop checks in handleComputerBatch / handleType. See that
+ * field for semantics. */
+ isAborted?(): boolean;
+}
+
+// ----------------------------------------------------------------------------
+// Per-call overrides (mirror of PermissionOverrides, types.ts:97-102)
+// ----------------------------------------------------------------------------
+
+/**
+ * Built FRESH on every tool call by `bindSessionContext` from
+ * `ComputerUseSessionContext` getters. This is what lets a singleton MCP
+ * server carry per-session state — the state lives on the host's session
+ * store, not the server.
+ */
+export interface ComputerUseOverrides {
+ allowedApps: AppGrant[];
+ grantFlags: CuGrantFlags;
+ coordinateMode: CoordinateMode;
+
+ /**
+ * User-configured auto-deny list (Settings → Desktop app → Computer Use).
+ * Bundle IDs
+ * here are stripped from request_access BEFORE the approval dialog — they
+ * never reach the user for approval regardless of tier. The response tells
+ * the agent to ask the user to remove the app from their deny list in
+ * Settings if access is genuinely needed.
+ *
+ * Per-USER, persists across restarts (read from appPreferences per call,
+ * not session state). Contrast with `allowedApps` which is per-session.
+ * Empty array = no user-configured denies (the default).
+ */
+ userDeniedBundleIds: readonly string[];
+
+ /**
+ * Display CU operates on; read fresh per call. `scaleCoord` uses the
+ * `originX/Y` snapshotted in `lastScreenshot`, so mid-session switches
+ * only affect the NEXT screenshot/prepare call.
+ */
+ selectedDisplayId?: number;
+
+ /**
+ * The `request_access` tool handler calls this and awaits. The wrapper
+ * closure in serverDef.ts (mirroring InternalMcpServerManager.ts:131-177)
+ * routes through `handleToolPermission` → IPC → renderer ChicagoApproval.
+ * When it resolves, the wrapper side-effectfully mutates
+ * `InternalServerContext.cuAllowedApps` BEFORE returning here.
+ *
+ * Undefined when the session wasn't wired with a permission handler (e.g.
+ * a future headless mode). `request_access` returns a tool error in that case.
+ */
+ onPermissionRequest?: (req: CuPermissionRequest) => Promise;
+
+ /**
+ * For the pixel-validation staleness guard. The model's-last-screenshot,
+ * stashed by serverDef.ts after each `screenshot` tool call. Undefined on
+ * cold start → pixel validation skipped (click proceeds).
+ */
+ lastScreenshot?: ScreenshotResult;
+
+ /**
+ * Fired after every `prepareForAction` with the bundle IDs it just hid.
+ * The wrapper closure in serverDef.ts accumulates these into
+ * `Session.cuHiddenDuringTurn` via a write-through callback (same pattern
+ * as `onCuPermissionUpdated`). At turn end (`sdkMessage.type === "result"`),
+ * if the `chicagoAutoUnhide` setting is on, everything in the set is
+ * unhidden. Set is cleared regardless of the setting so it doesn't leak
+ * across turns.
+ *
+ * Undefined when the session wasn't wired with a tracker — unhide just
+ * doesn't happen.
+ */
+ onAppsHidden?: (bundleIds: string[]) => void;
+
+ /**
+ * Reads the clipboardGuard stash from session state. `undefined` means no
+ * stash is held — `syncClipboardStash` stashes on first entry to click-tier
+ * and clears on restore. Sibling of the `cuHiddenDuringTurn` getter pattern
+ * — state lives on the host's session, not module-level here.
+ */
+ getClipboardStash?: () => string | undefined;
+
+ /**
+ * Writes the clipboardGuard stash to session state. `undefined` clears.
+ * Sibling of `onAppsHidden` — the wrapper closure writes through to
+ * `Session.cuClipboardStash`. At turn end the host reads + clears it
+ * directly and restores via Electron's `clipboard.writeText` (no nest-only
+ * import surface).
+ */
+ onClipboardStashChanged?: (stash: string | undefined) => void;
+
+ /**
+ * Write the resolver's picked display back to session so teach overlay
+ * positioning and subsequent non-resolver calls use the same display.
+ * Fired by `handleScreenshot` in the atomic `autoTargetDisplay` path when
+ * `resolvePrepareCapture`'s pick differs from `selectedDisplayId`.
+ * Fire-and-forget.
+ */
+ onResolvedDisplayUpdated?: (displayId: number) => void;
+
+ /**
+ * Set when the model explicitly picked a display via `switch_display`.
+ * When true, `handleScreenshot` passes `autoResolve: false` so the Swift
+ * resolver honors `selectedDisplayId` directly (straight cuDisplayInfo
+ * passthrough) instead of running the co-location/chase chain. The
+ * resolver's Step 2 ("host + allowed co-located → host") otherwise
+ * overrides any `selectedDisplayId` whenever an allowed app shares the
+ * host's monitor.
+ */
+ displayPinnedByModel?: boolean;
+
+ /**
+ * Write the model's explicit display pick to session. `displayId:
+ * undefined` clears both `selectedDisplayId` and the pin (back to auto).
+ * Sibling of `onResolvedDisplayUpdated` but also sets the pin flag —
+ * the two are semantically distinct (resolver-picked vs model-picked).
+ */
+ onDisplayPinned?: (displayId: number | undefined) => void;
+
+ /**
+ * Sorted comma-joined bundle-ID set the display was last auto-resolved
+ * for. `handleScreenshot` compares this to the current allowed set and
+ * only passes `autoResolve: true` when they differ — so the resolver
+ * doesn't yank the display on every screenshot, only when the app set
+ * has changed since the last resolve (or manual switch).
+ */
+ displayResolvedForApps?: string;
+
+ /**
+ * Records which app set the current display selection was made for. Fired
+ * alongside `onResolvedDisplayUpdated` when the resolver picks, so the next
+ * screenshot sees a matching set and skips auto-resolve.
+ */
+ onDisplayResolvedForApps?: (sortedBundleIdsKey: string) => void;
+
+ /**
+ * Global CU lock — at most one session actively uses CU at a time. Checked
+ * in `handleToolCall` after kill-switch/TCC, before dispatch. Every CU tool
+ * including `request_access` goes through it.
+ *
+ * - `holder === undefined` → lock is free, safe to acquire
+ * - `isSelf === true` → this session already holds it (no-op, proceed)
+ * - `holder !== undefined && !isSelf` → blocked, return tool error
+ *
+ * `undefined` callback → lock system not wired (e.g. CCD). Proceed without
+ * gating — absence of the mechanism ≠ locked out.
+ *
+ * The host manages release (on session idle/stop/archive) — this package
+ * never releases.
+ */
+ checkCuLock?: () => { holder: string | undefined; isSelf: boolean };
+
+ /**
+ * Take the lock for this session. `handleToolCall` calls this exactly once
+ * per turn, on the FIRST CU tool call when `checkCuLock().holder` is
+ * undefined. No-op if already held (defensive — the check should have
+ * short-circuited). Host emits an event the overlay listens to.
+ */
+ acquireCuLock?: () => void;
+
+ /**
+ * User-abort signal. Checked mid-iteration inside `handleComputerBatch`
+ * and `handleType`'s grapheme loop so an in-flight batch/type stops
+ * promptly on overlay Stop instead of running to completion after the
+ * host has already abandoned the tool result.
+ *
+ * Undefined → never aborts (e.g. unwired host). Live per-check read —
+ * same lazy-getter pattern as `checkCuLock`.
+ */
+ isAborted?: () => boolean;
+
+ // ── Teach mode ───────────────────────────────────────────────────────
+ // Wired only when the host's teachModeEnabled gate is on. All five
+ // undefined → `request_teach_access` / `teach_step` return tool errors
+ // and teach mode is effectively off.
+
+ /**
+ * Sibling of `onPermissionRequest`. Same blocking-await-on-renderer-dialog
+ * semantics, but routes to ComputerUseTeachApproval.tsx (which explains
+ * the window-hides-during-guide behavior) instead of ComputerUseApproval.
+ * The wrapper closure in serverDef.ts writes grants through to session state
+ * via `onCuPermissionUpdated` exactly as `onPermissionRequest` does.
+ */
+ onTeachPermissionRequest?: (
+ req: CuTeachPermissionRequest,
+ ) => Promise;
+
+ /**
+ * Called by `handleRequestTeachAccess` after the user approves and at least
+ * one app was granted. Host sets `session.teachModeActive = true`, emits
+ * `teachModeChanged` → teach controller hides the main window and shows the
+ * fullscreen overlay. Cleared by the host on turn end (`transitionTo("idle")`)
+ * alongside the CU lock release.
+ */
+ onTeachModeActivated?: () => void;
+
+ /**
+ * Read by `handleRequestAccess` and `handleRequestTeachAccess` to
+ * short-circuit with a clear tool error when teach mode is active. The
+ * main window is hidden during teach mode, so permission dialogs render
+ * invisibly and handleToolPermission blocks forever on an invisible
+ * prompt. Better to tell the model to exit teach mode first. Getter
+ * (not a boolean field) because teach mode state lives on the session,
+ * not on this per-call overrides object.
+ */
+ getTeachModeActive?: () => boolean;
+
+ /**
+ * Called by `handleTeachStep` with the scaled anchor + text. Host stores
+ * the resolver, emits `teachStepRequested` → teach controller pushes the
+ * payload to the overlay → user reads, clicks Next → IPC → host calls the
+ * stored resolver → this promise resolves. `{action: "exit"}` when the user
+ * clicks Exit (or the turn is interrupted) — `handleTeachStep` short-circuits
+ * without executing actions.
+ *
+ * Same blocking-promise pattern as `onPermissionRequest`, but resolved by
+ * the teach overlay's own preload (not the main renderer's tool-approval UI).
+ */
+ onTeachStep?: (req: TeachStepRequest) => Promise;
+
+ /**
+ * Called immediately after `onTeachStep` resolves with "next", before
+ * action dispatch begins. Host emits `teachStepWorking` → overlay flips to
+ * the spinner state (Next button gone, Exit stays, "Working…" + rotating
+ * notch). The next `onTeachStep` call replaces the spinner with the new
+ * tooltip content.
+ */
+ onTeachWorking?: () => void;
+}
+
+// ----------------------------------------------------------------------------
+// Teach mode (guided-tour tooltips with Next-button action execution)
+// ----------------------------------------------------------------------------
+
+/**
+ * Payload the host pushes to the teach overlay BrowserWindow. Built by
+ * `handleTeachStep` in toolCalls.ts from the model's `teach_step` args.
+ *
+ * `anchorLogical` here is POST-`scaleCoord` — **full-display** logical
+ * macOS points (origin = monitor top-left, menu bar included, since
+ * cuDisplayInfo returns CGDisplayBounds). The overlay window is positioned
+ * at `workArea.{x,y}` (excludes menu bar/Dock), so `updateTeachStep` in
+ * teach/window.ts subtracts the workArea offset before IPC so the HTML's
+ * CSS coords match.
+ */
+export interface TeachStepRequest {
+ explanation: string;
+ nextPreview: string;
+ /** Full-display logical points. Undefined → overlay centers the tooltip, hides the arrow. */
+ anchorLogical?: { x: number; y: number };
+}
+
+export type TeachStepResult = { action: "next" } | { action: "exit" };
+
+/**
+ * Payload for the renderer's ComputerUseTeachApproval dialog. Rides through
+ * `ToolPermissionRequest.input: unknown` same as `CuPermissionRequest`.
+ * Separate type (not a flag on `CuPermissionRequest`) so the two approval
+ * components can narrow independently and the teach dialog is free to drop
+ * fields it doesn't render (no grant-flag checkboxes in teach mode).
+ */
+export interface CuTeachPermissionRequest {
+ requestId: string;
+ /** Model-provided reason. Shown in the dialog headline ("guide you through {reason}"). */
+ reason: string;
+ apps: ResolvedAppRequest[];
+ screenshotFiltering: "native" | "none";
+ /** Present only when TCC is ungranted — same semantics as `CuPermissionRequest.tccState`. */
+ tccState?: {
+ accessibility: boolean;
+ screenRecording: boolean;
+ };
+ willHide?: Array<{ bundleId: string; displayName: string }>;
+ /** Same semantics as `CuPermissionRequest.autoUnhideEnabled`. */
+ autoUnhideEnabled?: boolean;
+}