Skill: wimi321/cross-platform-computer-use-skill (version 0.1.0)

--- name: cross-platform-computer-use-skill version: 0.1.0 description: Top-level cross-platform computer-use skill that bundles standalone macOS, Windows, and Linux runtimes with zero local Claude dependency and selects the correct platform payload at install/use time. tags: - skill - computer-use - automation - mcp - macos - windows - linux --- # Cross-Platform Computer-Use Skill Use this skill when the task needs a top-level portable computer-use skill that works across macOS, Windows, and Linux without depending on any local Claude installation, private native modules, or extracted app assets. ## What this skill does - bundles standalone platform runtimes for `macOS`, `Windows`, and `Linux` - installs one top-level skill that contains all three platform payloads - selects the correct project for the current host platform - preserves the standalone bootstrap model: each platform runtime creates its own virtualenv and installs public Python dependencies on first real run - keeps platform-specific limitations explicit instead of pretending the hosts behave identically ## Installed layout After installation, assume the top-level skill lives at: ```bash ~/.codex/skills/cross-platform-computer-use-skill ``` The bundled projects are stored under: ```bash ~/.codex/skills/cross-platform-computer-use-skill/project/platforms/macos ~/.codex/skills/cross-platform-computer-use-skill/project/platforms/windows ~/.codex/skills/cross-platform-computer-use-skill/project/platforms/linux ``` ## Platform selection Use the helper script from the installed skill root to resolve the active platform project: ```bash bash ~/.codex/skills/cross-platform-computer-use-skill/scripts/current-project.sh ``` On PowerShell: ```powershell powershell -ExecutionPolicy Bypass -File $HOME/.codex/skills/cross-platform-computer-use-skill/scripts/current-project.ps1 ``` ## Build Always build the selected platform project, not all three at once: ```bash cd "$(bash ~/.codex/skills/cross-platform-computer-use-skill/scripts/current-project.sh)" npm install npm run build ``` ## Run ```bash cd "$(bash ~/.codex/skills/cross-platform-computer-use-skill/scripts/current-project.sh)" node dist/cli.js ``` ## Validation notes - `macOS` has been real-device validated in this workspace, including GUI typing round-trip through the MCP `type` tool. - `Windows` and `Linux` are implemented, built, packaged, and published, but still need end-to-end runtime validation on real hosts. - `Linux` currently targets `X11` first; Wayland can restrict screenshots, focus inspection, clipboard, and synthetic input. ## Guardrails - Treat this host as trusted-local only. - Do not tell the user to search a local Claude install for binaries or hidden assets. - Be explicit about the current host platform and its validation status before claiming something is verified. - Mention that current runtimes report `screenshotFiltering: none`, so action gating is handled at the MCP layer.

path: agents/openai.yaml
content: interface:
  display_name: "Computer-Use Skill"
  short_description: "Use one top-level cross-platform computer-use skill with bundled macOS, Windows, and Linux runtimes"
  default_prompt: "Use the bundled top-level cross-platform computer-use skill, detect the current host platform, build the matching standalone runtime if needed, let it self-bootstrap, and avoid any dependency on a local Claude installation or hidden app assets."


path: project/manifest.json
content: {
  "name": "cross-platform-computer-use-skill",
  "version": "0.1.0",
  "platforms": {
    "macos": {
      "repo": "https://github.com/wimi321/macos-computer-use-skill",
      "version": "0.2.2",
      "clawhub": "https://clawhub.ai/wimi321/computer-use-macos",
      "validation": "real-device-validated"
    },
    "windows": {
      "repo": "https://github.com/wimi321/windows-computer-use-skill",
      "version": "0.1.1",
      "clawhub": "https://clawhub.ai/wimi321/computer-use-windows",
      "validation": "static-build-validated"
    },
    "linux": {
      "repo": "https://github.com/wimi321/linux-computer-use-skill",
      "version": "0.1.1",
      "clawhub": "https://clawhub.ai/wimi321/computer-use-linux",
      "validation": "static-build-validated"
    }
  }
}


path: project/platforms/linux/README.ja.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="linux-computer-use-skill hero" width="100%" />
  <h1>Linux Computer-Use Skill</h1>
  <p><strong>Linux 向けのトップレベル skill。standalone runtime と MCP server を同梱しています。</strong></p>
  <p>
    <a href="https://github.com/wimi321/linux-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-linux">ClawHub</a>
    ·
    <a href="./README.md">English</a>
    ·
    <a href="./README.zh-CN.md">简体中文</a>
  </p>
</div>

## ClawHub からインストール

この skill は ClawHub に [`computer-use-linux`](https://clawhub.ai/wimi321/computer-use-linux) として公開済みです。

```bash
clawhub install computer-use-linux
```

## このプロジェクトの位置づけ

このリポジトリは同時に:

- トップレベルの `skill`
- 独立した Linux デスクトップ制御 runtime
- agent エコシステム向けの computer-use MCP server

として設計されています。ローカル Claude インストールには依存しません。

## このプロジェクトの目的

目標は次のとおりです。

- ローカル Claude に依存しない
- private な `.node` バイナリに依存しない
- 抽出済みの隠し資産に依存しない
- skill を入れて server を build すれば、そのまま使える

## できること

- トップレベル Linux computer-use skill
- スクリーンショット、マウス、キーボード、アプリ起動、ウィンドウ/ディスプレイ対応付け、クリップボードを扱う standalone MCP server
- 公開依存のみ: `Node.js + Python + pyautogui + mss + Pillow + psutil + python-xlib`
- 初回起動時に virtualenv を自動作成し、Python 依存を自動導入
- `~/.codex/skills/computer-use-linux/project` に本体まで配置される skill install
- 抽出した TypeScript tool layer を Linux-native Python backend に接続

## 現在の状態

このリポジトリで実装済み:

- Linux Python helper と runtime bootstrap
- ディスプレイ列挙とスクリーンショット経路
- マウス、キーボード、ドラッグ、スクロール、クリップボード
- 最前面アプリ、ポインタ下アプリ、実行中アプリ、インストール済みアプリ、ウィンドウ表示先ディスプレイの取得
- Linux skill packaging と bundled project payload
- TypeScript build 成功

本番投入前に推奨されること:

- 実機 Linux での検証
- 複数の desktop environment と monitor layout の確認
- フォーカス、クリップボード、権限制限の境界テスト

このセッションには実際の Linux マシンが接続されていないため、ここでの状態は「実装済み・build 済み」であり、「Linux 実機で end-to-end 検証済み」ではありません。

## 0.1.1 で修正したこと

`0.1.1` では、Linux 向けパッケージング移行時に壊れていた共有 platform ロジックを修正しました。system-key blocklist と tool capability typing がコピー時に崩れており、Linux 用の system shortcut 保護が明示的に定義されていませんでした。

このリリースで `linux` 向け分岐を明示的に戻し、修正はソース本体と bundled skill payload の両方に同期されています。

## 重要なスコープ

現在の desktop-control は主に `X11` セッションを対象にしています。

つまり:

- X11 desktop session が主対象
- Wayland は compositor policy により screenshot、focus query、clipboard、synthetic input が制限される場合がある
- distro や desktop environment によって挙動差がある

## アーキテクチャ

```mermaid
flowchart LR
    A[Agent / MCP Client] --> B[linux-computer-use-skill]
    B --> C[抽出した TypeScript MCP ツール層]
    B --> D[Standalone Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[psutil + python-xlib]
    E --> H[マウス / キーボード]
    F --> I[スクリーンショット]
    G --> J[アプリ / ウィンドウ / ディスプレイ / クリップボード]
```

## インストール

### 1. クローンして Node 依存を入れる

```bash
git clone https://github.com/wimi321/linux-computer-use-skill.git
cd linux-computer-use-skill
npm install
npm run build
```

### 2. サーバーを起動

```bash
node dist/cli.js
```

初回起動時に自動で以下を実行します。

- `.runtime/venv` の作成
- 必要なら `pip` の bootstrap
- `runtime/requirements.txt` に基づく Python 依存の導入

## MCP 設定

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/linux-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

参考: [`examples/mcp-config.json`](./examples/mcp-config.json)

## Skill インストール

同梱 skill: [`skill/computer-use-linux`](./skill/computer-use-linux)

### Option A: ClawHub からインストール

```bash
clawhub install computer-use-linux
```

```bash
bash skill/computer-use-linux/scripts/install.sh
```

インストール後の bundled project 既定パス:

```text
~/.codex/skills/computer-use-linux/project
```

`CODEX_HOME` が設定されている場合はその配下を使います。

## 検証マトリクス

このセッションで完了したもの:

- `npm run check`
- `npm run build`
- `runtime/linux_helper.py` の Python compile check
- bundled skill ソース整合性チェック
- bundled project の version sync チェック
- Linux/X11 runtime における display discovery / screenshot / clipboard / frontmost app / app enumeration / window-display lookup 経路のコードレビュー

このセッションでは未実施:

- 実機 Linux での GUI 制御
- 実機 Linux での screenshot capture
- 実アプリに対する foreground-window gating
- Wayland compositor ごとの差異
- デスクトップ環境 / マルチモニタの境界検証

## 実行メモ

### 権限

Linux desktop control は次の条件で制限されることがあります。

- Wayland compositor restrictions
- sandboxed app isolation
- session / remote desktop boundaries
- desktop environment ごとの focus / clipboard 挙動差

### Screenshot Filtering

この runtime は `screenshotFiltering: none` を返します。

つまり screenshot filtering は compositor-native ではなく、gating は MCP レイヤーで行われます。

### 対応プラットフォーム

この実装は現在 `Linux-only` です。

## リポジトリ構成

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  linux_helper.py
  requirements.txt
skill/
  computer-use-linux/
examples/
assets/
```

## Roadmap

- 実機 Linux での検証とハードニング
- Linux 向け app identity / icon 抽出の改善
- 自動 Linux integration test の追加
- Wayland 制限と代替パスの整理

## License

MIT

## Credits

Claude Code computer-use ワークフローから再利用可能な TypeScript ロジックを抽出し、そこに完全独立の公開 Linux runtime を接続したプロジェクトです。


path: project/platforms/linux/README.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="linux-computer-use-skill hero" width="100%" />
  <h1>Linux Computer-Use Skill</h1>
  <p><strong>A top-level Linux skill with a bundled standalone runtime and MCP server.</strong></p>
  <p>
    <a href="https://github.com/wimi321/linux-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-linux">ClawHub</a>
    ·
    <a href="./README.zh-CN.md">简体中文</a>
    ·
    <a href="./README.ja.md">日本語</a>
  </p>
</div>

## Install From ClawHub

Published on ClawHub as [`computer-use-linux`](https://clawhub.ai/wimi321/computer-use-linux).

```bash
clawhub install computer-use-linux
```

## Positioning

This repository is:

- a top-level `skill`
- a standalone Linux desktop-control runtime
- a computer-use MCP server for agent ecosystems

It is packaged skill-first instead of depending on any local Claude install.

## Why This Exists

The requirement is stricter than "wrap an existing install":

- no dependency on a local Claude app
- no private `.node` binaries
- no extracted hidden assets
- install the skill, build the server, and use it

This project follows that rule on Linux.

## What You Get

- top-level Linux computer-use skill
- standalone MCP server for screenshots, mouse, keyboard, app launch, window/display mapping, and clipboard
- public dependency chain only: `Node.js + Python + pyautogui + mss + Pillow + psutil + python-xlib`
- first-run runtime bootstrap: the server creates its own virtualenv and installs dependencies automatically
- bundled skill install that copies the full project into `~/.codex/skills/computer-use-linux/project`
- extracted TypeScript tool layer wired to a Linux-native Python backend

## Status

Implemented in this repository:

- Linux Python helper and runtime bootstrap
- display enumeration and screenshot pipeline
- mouse, keyboard, drag, scroll, and clipboard primitives
- frontmost app, app-under-point, running app, installed app, and window-display lookup paths
- Linux-first skill packaging and bundled project payload
- TypeScript build passing

Still recommended before production use:

- validate on a real Linux host
- test multiple desktop environments and monitor layouts
- test focus, clipboard, and permission edge cases

This session did not have a live Linux machine attached, so runtime behavior on Linux has been implemented and built, but not end-to-end verified on a real Linux desktop.

## What Was Fixed In 0.1.1

Version `0.1.1` fixes a Linux packaging regression in the shared system-key blocklist and platform typing. The migrated shared files had been edited into an invalid copied branch, which meant Linux builds were not using a clearly defined Linux shortcut denylist.

This release restores explicit Linux handling for system-level shortcut checks and syncs the fix into both the source tree and the bundled skill payload.

## Important Scope

Current desktop-control support is aimed at `X11` sessions.

Notable implications:

- X11 desktop sessions are the primary target
- Wayland may block or limit screenshots, focus inspection, clipboard access, and synthetic input depending on compositor policy
- distro / desktop-environment differences can affect behavior

## Architecture

```mermaid
flowchart LR
    A[Agent / MCP Client] --> B[linux-computer-use-skill]
    B --> C[Extracted TypeScript MCP tools]
    B --> D[Standalone Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[psutil + python-xlib]
    E --> H[Mouse / Keyboard]
    F --> I[Screenshots]
    G --> J[Apps / Windows / Displays / Clipboard]
```

## Install

### 1. Clone and install Node deps

```bash
git clone https://github.com/wimi321/linux-computer-use-skill.git
cd linux-computer-use-skill
npm install
npm run build
```

### 2. Start the server

```bash
node dist/cli.js
```

On first launch, the project will automatically:

- create `.runtime/venv`
- bootstrap `pip` if needed
- install the Python runtime dependencies from `runtime/requirements.txt`

## MCP Configuration

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/linux-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

See [`examples/mcp-config.json`](./examples/mcp-config.json).

## Skill Install

This repo ships a top-level skill at [`skill/computer-use-linux`](./skill/computer-use-linux).

### Option A: Install from ClawHub

```bash
clawhub install computer-use-linux
```

```bash
bash skill/computer-use-linux/scripts/install.sh
```

After installation, the bundled project lives at:

```text
~/.codex/skills/computer-use-linux/project
```

If `CODEX_HOME` is set, use that location instead.

## Validation Matrix

Validated in this session:

- `npm run check`
- `npm run build`
- Python syntax compile check for `runtime/linux_helper.py`
- bundled skill source integrity checks
- bundled project version sync checks
- review of Linux-specific runtime paths for X11 display discovery, screenshots, clipboard, frontmost app, app enumeration, and window/display lookup

Not yet validated in this session:

- real Linux GUI control
- live screenshot capture on Linux
- foreground-window enforcement against real Linux apps
- Wayland behavior under different compositors
- mixed desktop-environment and multi-monitor edge cases

## Runtime Notes

### Permissions

Linux desktop control can still be limited by:

- Wayland compositor restrictions
- sandboxed app isolation
- session / remote desktop boundaries
- desktop-environment specific focus and clipboard behavior

### Screenshot Filtering

This standalone runtime reports `screenshotFiltering: none`.

That means screenshot filtering is not compositor-native; gating still happens at the MCP layer.

### Platform Scope

This repository is intentionally `Linux-only`.

Covered capabilities:

- screenshots
- mouse control
- keyboard input
- frontmost app inspection
- installed / running app discovery
- window-to-display mapping
- clipboard access
- app launch

## Example Commands

```bash
npm run build
node dist/cli.js
```

```bash
node --input-type=module -e "import { callPythonHelper } from './dist/computer-use/pythonBridge.js'; console.log(await callPythonHelper('list_displays', {}));"
```

## Repository Layout

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  linux_helper.py
  requirements.txt
skill/
  computer-use-linux/
examples/
assets/
```

## Environment Flags

- `CLAUDE_COMPUTER_USE_DEBUG=1`
- `CLAUDE_COMPUTER_USE_COORDINATE_MODE=pixels`
- `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE=1`
- `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION=1`
- `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION=0`

## Roadmap

- validate and harden on real Linux hardware
- improve app identity and icon extraction on Linux
- add automated Linux integration tests
- document Wayland-specific limitations and alternatives

## License

MIT

## Credits

This project preserves and adapts reusable TypeScript computer-use logic recovered from the Claude Code workflow, then replaces the missing private runtime with a fully standalone public Linux implementation.


path: project/platforms/linux/README.zh-CN.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="linux-computer-use-skill hero" width="100%" />
  <h1>Linux Computer-Use Skill</h1>
  <p><strong>一个面向 Linux 的顶级 skill，内置独立 runtime 与 MCP server。</strong></p>
  <p>
    <a href="https://github.com/wimi321/linux-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-linux">ClawHub</a>
    ·
    <a href="./README.md">English</a>
    ·
    <a href="./README.ja.md">日本語</a>
  </p>
</div>

## ClawHub 安装

这个 skill 已发布到 ClawHub，slug 是 [`computer-use-linux`](https://clawhub.ai/wimi321/computer-use-linux)。

```bash
clawhub install computer-use-linux
```

## 项目定位

这个仓库同时是：

- 一个顶级 `skill`
- 一套独立的 Linux 桌面控制 runtime
- 一个给 agent 生态使用的 computer-use MCP server

它是 skill-first，不依赖本机 Claude 安装。

## 这个项目解决什么问题

目标仍然是：

- 不依赖本机 Claude
- 不依赖私有 `.node` 二进制
- 不依赖提取内部隐藏资产
- skill 装上、server 构建好，就能直接用

## 你现在拿到的能力

- 顶级 Linux computer-use skill
- 独立 MCP server：截图、鼠标、键盘、应用启动、窗口/显示器映射、剪贴板
- 只使用公开依赖：`Node.js + Python + pyautogui + mss + Pillow + psutil + python-xlib`
- 首次运行自动自举：自动创建虚拟环境并安装 Python 依赖
- 安装 skill 时会把完整项目一起复制到 `~/.codex/skills/computer-use-linux/project`
- 提取出来的 TypeScript 工具层已经接到 Linux 原生 Python backend

## 当前状态

这个仓库里已经完成：

- Linux Python helper 和 runtime 自举链路
- 显示器枚举与截图链路
- 鼠标、键盘、拖拽、滚动、剪贴板能力
- 前台应用、指针下应用、运行中应用、已安装应用、窗口归属显示器查询
- Linux skill 打包与 bundled project 分发
- TypeScript 构建通过

上线前仍然建议：

- 在真实 Linux 机器上做验证
- 覆盖不同桌面环境、多显示器和焦点边界情况
- 补测剪贴板、窗口聚焦和权限限制细节

这一轮会话没有接入真实 Linux 主机，所以这里是“实现完成并已构建”，但还不是“已在 Linux 实机全链路验证”。

## 0.1.1 修复了什么

`0.1.1` 修复了 Linux 打包迁移中的共享平台逻辑问题：系统快捷键黑名单和工具 platform 类型被改坏后，Linux 构建并没有明确使用 Linux 自己的系统级快捷键保护分支。

现在这部分逻辑已经恢复为显式支持 `linux`，并且源码树与 bundled skill payload 都已同步修复。

## 重要范围

当前桌面控制能力主要面向 `X11` 会话。

含义是：

- X11 桌面会话是当前主目标
- Wayland 可能因为 compositor 策略限制截图、焦点查询、剪贴板和模拟输入
- 不同发行版 / 桌面环境之间也可能存在差异

## 架构

```mermaid
flowchart LR
    A[Agent / MCP Client] --> B[linux-computer-use-skill]
    B --> C[提取出的 TypeScript MCP 工具层]
    B --> D[独立 Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[psutil + python-xlib]
    E --> H[鼠标 / 键盘]
    F --> I[截图]
    G --> J[应用 / 窗口 / 显示器 / 剪贴板]
```

## 安装

### 1. 克隆并安装 Node 依赖

```bash
git clone https://github.com/wimi321/linux-computer-use-skill.git
cd linux-computer-use-skill
npm install
npm run build
```

### 2. 启动 server

```bash
node dist/cli.js
```

首次启动时项目会自动：

- 创建 `.runtime/venv`
- 必要时自动补 `pip`
- 根据 `runtime/requirements.txt` 安装 Python 依赖

## MCP 配置

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/linux-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

参考 [`examples/mcp-config.json`](./examples/mcp-config.json)。

## Skill 安装

仓库自带顶级 skill：[`skill/computer-use-linux`](./skill/computer-use-linux)

### 方式 A：从 ClawHub 安装

```bash
clawhub install computer-use-linux
```

```bash
bash skill/computer-use-linux/scripts/install.sh
```

安装后 bundled project 默认位于：

```text
~/.codex/skills/computer-use-linux/project
```

如果设置了 `CODEX_HOME`，则使用对应路径。

## 验证矩阵

这次会话里已经完成：

- `npm run check`
- `npm run build`
- `runtime/linux_helper.py` 的 Python 编译检查
- bundled skill 源码完整性检查
- bundled project 版本同步检查
- 对 Linux/X11 运行时中显示器发现、截图、剪贴板、前台应用、应用枚举、窗口/显示器映射路径的代码审视

这次还没有真实完成：

- Linux 实机 GUI 控制
- Linux 实机截图采集
- 面向真实 Linux 应用的前台窗口 gating
- 不同 Wayland compositor 下的行为
- 不同桌面环境和多显示器边界情况

## 运行说明

### 权限

Linux 桌面控制仍可能受这些因素限制：

- Wayland compositor 限制
- sandbox 应用隔离
- 会话 / 远程桌面边界
- 桌面环境在焦点和剪贴板上的差异行为

### 截图过滤

当前 runtime 声明的是 `screenshotFiltering: none`。

也就是说，截图过滤不是 compositor 原生级别，动作 gating 仍由 MCP 层处理。

### 平台范围

当前仓库明确是 `Linux-only`。

覆盖能力：

- 截图
- 鼠标控制
- 键盘输入
- 前台应用识别
- 已安装 / 运行中应用发现
- 窗口到显示器映射
- 剪贴板访问
- 应用启动

## 仓库结构

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  linux_helper.py
  requirements.txt
skill/
  computer-use-linux/
examples/
assets/
```

## 路线图

- 在真实 Linux 硬件上继续打磨和加固
- 改进 Linux 应用身份识别与图标提取
- 增加自动化 Linux 集成测试
- 补齐 Wayland 限制和替代路径说明

## License

MIT

## Credits

这个项目保留了从 Claude Code computer-use 工作流中提炼出来的可复用 TypeScript 逻辑，并用一套完全独立、公开可安装的 Linux runtime 替换了缺失的私有执行层。


path: project/platforms/linux/assets/hero.svg
content: <svg width="1280" height="640" viewBox="0 0 1280 640" fill="none" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="bg" x1="54" y1="42" x2="1190" y2="582" gradientUnits="userSpaceOnUse">
      <stop stop-color="#0B1220"/>
      <stop offset="0.5" stop-color="#111C32"/>
      <stop offset="1" stop-color="#1D3557"/>
    </linearGradient>
    <linearGradient id="accent" x1="218" y1="116" x2="1112" y2="476" gradientUnits="userSpaceOnUse">
      <stop stop-color="#7EE787"/>
      <stop offset="0.45" stop-color="#5BC0EB"/>
      <stop offset="1" stop-color="#FFB703"/>
    </linearGradient>
    <linearGradient id="panel" x1="802" y1="157" x2="1107" y2="434" gradientUnits="userSpaceOnUse">
      <stop stop-color="#111927" stop-opacity="0.96"/>
      <stop offset="1" stop-color="#0B1220" stop-opacity="0.8"/>
    </linearGradient>
    <filter id="blur" x="0" y="0" width="1280" height="640" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
      <feGaussianBlur stdDeviation="60"/>
    </filter>
  </defs>

  <rect width="1280" height="640" rx="28" fill="url(#bg)"/>
  <g filter="url(#blur)" opacity="0.34">
    <circle cx="225" cy="147" r="132" fill="#7EE787"/>
    <circle cx="1022" cy="158" r="110" fill="#5BC0EB"/>
    <circle cx="952" cy="514" r="164" fill="#FFB703"/>
  </g>

  <rect x="62" y="58" width="1156" height="524" rx="26" fill="#0B1220" fill-opacity="0.25" stroke="rgba(255,255,255,0.12)"/>

  <g opacity="0.95">
    <rect x="94" y="92" width="394" height="34" rx="17" fill="#0F1728" stroke="#25324A"/>
    <circle cx="117" cy="109" r="5" fill="#FF6B6B"/>
    <circle cx="136" cy="109" r="5" fill="#FFD166"/>
    <circle cx="155" cy="109" r="5" fill="#7EE787"/>
    <text x="182" y="115" fill="#B9C6DA" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="14">linux-computer-use-skill</text>
  </g>

  <g>
    <text x="98" y="204" fill="white" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="64" font-weight="700">Standalone</text>
    <text x="98" y="282" fill="url(#accent)" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="70" font-weight="800">Computer Use</text>
    <text x="98" y="352" fill="#DFF5FF" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="60" font-weight="760">Skill + MCP</text>
    <text x="98" y="404" fill="#D6E2F0" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="21">Recovered from Claude Code and rebuilt as a portable public Linux runtime.</text>
    <text x="98" y="438" fill="#8EA3BC" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="19">TS core, Python bridge, runtime bootstrap, and an installable top-level skill.</text>
  </g>

  <g>
    <rect x="96" y="482" width="160" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="132" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">stdio MCP</text>

    <rect x="272" y="482" width="204" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="304" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">Linux automation</text>

    <rect x="494" y="482" width="162" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="526" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">Codex skill</text>
  </g>

  <g>
    <rect x="770" y="134" width="370" height="336" rx="24" fill="url(#panel)" stroke="#314561"/>
    <text x="803" y="180" fill="#E8F0FB" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">System Shape</text>

    <rect x="803" y="208" width="304" height="58" rx="16" fill="#122035" stroke="#29496F"/>
    <text x="827" y="243" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">Codex / MCP client</text>

    <path d="M955 267V294" stroke="#5BC0EB" stroke-width="3" stroke-linecap="round"/>

    <rect x="803" y="296" width="304" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="827" y="331" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">linux-computer-use-skill</text>

    <path d="M955 355V382" stroke="#7EE787" stroke-width="3" stroke-linecap="round"/>

    <rect x="803" y="384" width="140" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="826" y="419" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">TS core</text>

    <rect x="968" y="384" width="139" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="991" y="419" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">py runtime</text>
  </g>
</svg>


path: project/platforms/linux/examples/mcp-config.json
content: {
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/linux-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}


path: project/platforms/linux/package-lock.json
content: {
  "name": "linux-computer-use-skill",
  "version": "0.1.1",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "linux-computer-use-skill",
      "version": "0.1.1",
      "license": "MIT",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.18.0"
      },
      "bin": {
        "linux-computer-use-skill": "dist/cli.js"
      },
      "devDependencies": {
        "@types/node": "^24.5.2",
        "tsx": "^4.20.5",
        "typescript": "^5.9.2"
      },
      "engines": {
        "node": ">=20"
      }
    },
    "node_modules/@esbuild/aix-ppc64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "aix"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/android-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "android"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/freebsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "freebsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-arm": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ia32": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ia32"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-loong64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "loong64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-mips64el": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "mips64el"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-ppc64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "ppc64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-riscv64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "riscv64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/linux-s390x": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "s390x"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/netbsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "netbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openbsd-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openbsd"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/openharmony-arm64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "arm64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "openharmony"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@esbuild/sunos-x64": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "cpu": [
        "x64"
      ],
      "dev": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "sunos"
      ],
      "engines": {
        "node": ">=18"
      }
    },
    "node_modules/@hono/node-server": {
      "version": "1.19.12",
      "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.12.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.14.1"
      },
      "peerDependencies": {
        "hono": "^4"
      }
    },
    "node_modules/@modelcontextprotocol/sdk": {
      "version": "1.29.0",
      "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">=18"
      },
      "peerDependencies": {
        "@cfworker/json-schema": "^4.1.1",
        "zod": "^3.25 || ^4.0"
      },
      "peerDependenciesMeta": {
        "@cfworker/json-schema": {
          "optional": true
        },
        "zod": {
          "optional": false
        }
      }
    },
    "node_modules/@types/node": {
      "version": "24.12.0",
      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "undici-types": "~7.16.0"
      }
    },
    "node_modules/accepts": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-types": "^3.0.0",
        "negotiator": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/ajv": {
      "version": "8.18.0",
      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "fast-deep-equal": "^3.1.3",
        "fast-uri": "^3.0.1",
        "json-schema-traverse": "^1.0.0",
        "require-from-string": "^2.0.2"
      },
      "funding": {
        "type": "github",
        "url": "https://github.com/sponsors/epoberezkin"
      }
    },
    "node_modules/ajv-formats": {
      "version": "3.0.1",
      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependencies": {
        "ajv": "^8.0.0"
      },
      "peerDependenciesMeta": {
        "ajv": {
          "optional": true
        }
      }
    },
    "node_modules/body-parser": {
      "version": "2.2.2",
      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/bytes": {
      "version": "3.1.2",
      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/call-bound": {
      "version": "1.0.4",
      "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "get-intrinsic": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/content-disposition": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/content-type": {
      "version": "1.0.5",
      "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/cookie-signature": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=6.6.0"
      }
    },
    "node_modules/cors": {
      "version": "2.8.6",
      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "object-assign": "^4",
        "vary": "^1"
      },
      "engines": {
        "node": ">= 0.10"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/cross-spawn": {
      "version": "7.0.6",
      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "path-key": "^3.1.0",
        "shebang-command": "^2.0.0",
        "which": "^2.0.1"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/debug": {
      "version": "4.4.3",
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ms": "^2.1.3"
      },
      "engines": {
        "node": ">=6.0"
      },
      "peerDependenciesMeta": {
        "supports-color": {
          "optional": true
        }
      }
    },
    "node_modules/depd": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/ee-first": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/encodeurl": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/esbuild": {
      "version": "0.27.4",
      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "bin": {
        "esbuild": "bin/esbuild"
      },
      "engines": {
        "node": ">=18"
      },
      "optionalDependencies": {
        "@esbuild/aix-ppc64": "0.27.4",
        "@esbuild/android-arm": "0.27.4",
        "@esbuild/android-arm64": "0.27.4",
        "@esbuild/android-x64": "0.27.4",
        "@esbuild/linux-arm64": "0.27.4",
        "@esbuild/linux-x64": "0.27.4",
        "@esbuild/freebsd-arm64": "0.27.4",
        "@esbuild/freebsd-x64": "0.27.4",
        "@esbuild/linux-arm": "0.27.4",
        "@esbuild/linux-ia32": "0.27.4",
        "@esbuild/linux-loong64": "0.27.4",
        "@esbuild/linux-mips64el": "0.27.4",
        "@esbuild/linux-ppc64": "0.27.4",
        "@esbuild/linux-riscv64": "0.27.4",
        "@esbuild/linux-s390x": "0.27.4",
        "@esbuild/netbsd-arm64": "0.27.4",
        "@esbuild/netbsd-x64": "0.27.4",
        "@esbuild/openbsd-arm64": "0.27.4",
        "@esbuild/openbsd-x64": "0.27.4",
        "@esbuild/openharmony-arm64": "0.27.4",
        "@esbuild/sunos-x64": "0.27.4"
      }
    },
    "node_modules/escape-html": {
      "version": "1.0.3",
      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/etag": {
      "version": "1.8.1",
      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/eventsource": {
      "version": "3.0.7",
      "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "eventsource-parser": "^3.0.1"
      },
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/eventsource-parser": {
      "version": "3.0.6",
      "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18.0.0"
      }
    },
    "node_modules/express": {
      "version": "5.2.1",
      "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/express-rate-limit": {
      "version": "8.3.2",
      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ip-address": "10.1.0"
      },
      "engines": {
        "node": ">= 16"
      },
      "funding": {
        "url": "https://github.com/sponsors/express-rate-limit"
      },
      "peerDependencies": {
        "express": ">= 4.11"
      }
    },
    "node_modules/fast-deep-equal": {
      "version": "3.1.3",
      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/fast-uri": {
      "version": "3.1.0",
      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "github",
          "url": "https://github.com/sponsors/fastify"
        },
        {
          "type": "opencollective",
          "url": "https://opencollective.com/fastify"
        }
      ],
      "license": "BSD-3-Clause"
    },
    "node_modules/finalhandler": {
      "version": "2.1.1",
      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 18.0.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/forwarded": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/fresh": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/fsevents": {
      "version": "2.3.3",
      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "hasInstallScript": true,
      "license": "MIT",
      "optional": true,
      "os": [
        "linux"
      ],
      "engines": {
        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/get-tsconfig": {
      "version": "4.13.7",
      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "resolve-pkg-maps": "^1.0.0"
      },
      "funding": {
        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/hono": {
      "version": "4.12.9",
      "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.9.0"
      }
    },
    "node_modules/http-errors": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "depd": "~2.0.0",
        "inherits": "~2.0.4",
        "setprototypeof": "~1.2.0",
        "statuses": "~2.0.2",
        "toidentifier": "~1.0.1"
      },
      "engines": {
        "node": ">= 0.8"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/iconv-lite": {
      "version": "0.7.2",
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "safer-buffer": ">= 2.1.2 < 3.0.0"
      },
      "engines": {
        "node": ">=0.10.0"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/inherits": {
      "version": "2.0.4",
      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/ip-address": {
      "version": "10.1.0",
      "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 12"
      }
    },
    "node_modules/ipaddr.js": {
      "version": "1.9.1",
      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/is-promise": {
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/isexe": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/jose": {
      "version": "6.2.2",
      "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/panva"
      }
    },
    "node_modules/json-schema-traverse": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/json-schema-typed": {
      "version": "8.0.2",
      "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause"
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/media-typer": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/merge-descriptors": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "url": "https://github.com/sponsors/sindresorhus"
      }
    },
    "node_modules/mime-db": {
      "version": "1.54.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "^1.54.0"
      },
      "engines": {
        "node": ">=18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/ms": {
      "version": "2.1.3",
      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/negotiator": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/object-assign": {
      "version": "4.1.1",
      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/object-inspect": {
      "version": "1.13.4",
      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/on-finished": {
      "version": "2.4.1",
      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "ee-first": "1.1.1"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/once": {
      "version": "1.4.0",
      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "wrappy": "1"
      }
    },
    "node_modules/parseurl": {
      "version": "1.3.3",
      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/path-key": {
      "version": "3.1.1",
      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/path-to-regexp": {
      "version": "8.4.1",
      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/pkce-challenge": {
      "version": "5.0.1",
      "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=16.20.0"
      }
    },
    "node_modules/proxy-addr": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "forwarded": "0.2.0",
        "ipaddr.js": "1.9.1"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/qs": {
      "version": "6.15.0",
      "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-3-Clause",
      "dependencies": {
        "side-channel": "^1.1.0"
      },
      "engines": {
        "node": ">=0.6"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/range-parser": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/raw-body": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "bytes": "~3.1.2",
        "http-errors": "~2.0.1",
        "iconv-lite": "~0.7.0",
        "unpipe": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.10"
      }
    },
    "node_modules/require-from-string": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.10.0"
      }
    },
    "node_modules/resolve-pkg-maps": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "funding": {
        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
      }
    },
    "node_modules/router": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "debug": "^4.4.0",
        "depd": "^2.0.0",
        "is-promise": "^4.0.0",
        "parseurl": "^1.3.3",
        "path-to-regexp": "^8.0.0"
      },
      "engines": {
        "node": ">= 18"
      }
    },
    "node_modules/safer-buffer": {
      "version": "2.1.2",
      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/send": {
      "version": "1.2.1",
      "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/serve-static": {
      "version": "2.2.1",
      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "encodeurl": "^2.0.0",
        "escape-html": "^1.0.3",
        "parseurl": "^1.3.3",
        "send": "^1.2.0"
      },
      "engines": {
        "node": ">= 18"
      },
      "funding": {
        "type": "opencollective",
        "url": "https://opencollective.com/express"
      }
    },
    "node_modules/setprototypeof": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/shebang-command": {
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "shebang-regex": "^3.0.0"
      },
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/shebang-regex": {
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=8"
      }
    },
    "node_modules/side-channel": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-list": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-map": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bound": "^1.0.2",
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.5",
        "object-inspect": "^1.13.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/side-channel-weakmap": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "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"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/statuses": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/toidentifier": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.6"
      }
    },
    "node_modules/tsx": {
      "version": "4.21.0",
      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT",
      "dependencies": {
        "esbuild": "~0.27.0",
        "get-tsconfig": "^4.7.5"
      },
      "bin": {
        "tsx": "dist/cli.mjs"
      },
      "engines": {
        "node": ">=18.0.0"
      },
      "optionalDependencies": {
        "fsevents": "~2.3.3"
      }
    },
    "node_modules/type-is": {
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "content-type": "^1.0.5",
        "media-typer": "^1.1.0",
        "mime-types": "^3.0.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/typescript": {
      "version": "5.9.3",
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "Apache-2.0",
      "bin": {
        "tsc": "bin/tsc",
        "tsserver": "bin/tsserver"
      },
      "engines": {
        "node": ">=14.17"
      }
    },
    "node_modules/undici-types": {
      "version": "7.16.0",
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
      "integrity": [REDACTED_SECRET],
      "dev": true,
      "license": "MIT"
    },
    "node_modules/unpipe": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/vary": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/which": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "dependencies": {
        "isexe": "^2.0.0"
      },
      "bin": {
        "node-which": "bin/node-which"
      },
      "engines": {
        "node": ">= 8"
      }
    },
    "node_modules/wrappy": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC"
    },
    "node_modules/zod": {
      "version": "4.3.6",
      "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/colinhacks"
      }
    },
    "node_modules/zod-to-json-schema": {
      "version": "3.25.2",
      "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "ISC",
      "peerDependencies": {
        "zod": "^3.25.28 || ^4"
      }
    }
  }
}


path: project/platforms/linux/package.json
content: {
  "name": "linux-computer-use-skill",
  "version": "0.1.1",
  "description": "Top-level Linux computer-use skill with a bundled standalone runtime and MCP server, zero local Claude dependency.",
  "homepage": "https://github.com/wimi321/linux-computer-use-skill",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/wimi321/linux-computer-use-skill.git"
  },
  "bugs": {
    "url": "https://github.com/wimi321/linux-computer-use-skill/issues"
  },
  "type": "module",
  "bin": {
    "linux-computer-use-skill": "dist/cli.js"
  },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "dev": "tsx src/cli.ts",
    "start": "node dist/cli.js",
    "check": "tsc -p tsconfig.json --noEmit"
  },
  "keywords": [
    "skill",
    "portable-skill",
    "computer-use",
    "linux",
    "automation",
    "desktop-automation",
    "mcp",
    "ai-agents",
    "x11"
  ],
  "author": "haoc",
  "license": "MIT",
  "engines": {
    "node": ">=20"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.18.0"
  },
  "devDependencies": {
    "@types/node": "^24.5.2",
    "tsx": "^4.20.5",
    "typescript": "^5.9.2"
  }
}


path: project/platforms/linux/runtime/linux_helper.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import base64
import json
import os
import shutil
import subprocess
import sys
import time
from io import BytesIO
from pathlib import Path
from tkinter import Tk
from typing import Any

import mss
import psutil
import pyautogui
from PIL import Image
from Xlib import X, display as xdisplay
from Xlib.error import XError

os.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1")
os.environ.setdefault("PYAUTOGUI_HIDE_SUPPORT_PROMPT", "1")

pyautogui.FAILSAFE = False
pyautogui.PAUSE = 0

KEY_MAP = {
    **{chr(i): chr(i) for i in range(ord("a"), ord("z") + 1)},
    **{str(i): str(i) for i in range(10)},
    "cmd": "winleft",
    "command": "winleft",
    "meta": "winleft",
    "super": "winleft",
    "win": "winleft",
    "ctrl": "ctrl",
    "control": "ctrl",
    "shift": "shift",
    "alt": "alt",
    "option": "alt",
    "opt": "alt",
    "escape": "esc",
    "esc": "esc",
    "enter": "enter",
    "return": "enter",
    "tab": "tab",
    "space": "space",
    "backspace": "backspace",
    "delete": "delete",
    "insert": "insert",
    "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 require_x11() -> xdisplay.Display:
    if not os.environ.get("DISPLAY"):
        raise RuntimeError("DISPLAY is not set. Linux computer-use currently requires an X11 desktop session.")
    try:
        return xdisplay.Display()
    except Exception as exc:
        raise RuntimeError(f"Unable to open X display: {exc}") from exc


def atom(disp: xdisplay.Display, name: str):
    return disp.intern_atom(name, only_if_exists=True)


def get_displays() -> list[dict[str, Any]]:
    displays: list[dict[str, Any]] = []
    with mss.mss() as sct:
        for idx, monitor in enumerate(sct.monitors[1:], start=1):
            displays.append(
                {
                    "id": idx,
                    "displayId": idx,
                    "width": int(monitor["width"]),
                    "height": int(monitor["height"]),
                    "scaleFactor": 1,
                    "originX": int(monitor["left"]),
                    "originY": int(monitor["top"]),
                    "isPrimary": idx == 1,
                    "name": f"Display {idx}",
                    "label": f"Display {idx}",
                }
            )
    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:
        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_monitor(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)
    return {
        "base64": base64.b64encode(buffer.getvalue()).decode("ascii"),
        "width": image.width,
        "height": image.height,
    }


def capture_display(display_id: int | None, resize: tuple[int, int] | None = None) -> dict[str, Any]:
    display = choose_display(display_id)
    region = {
        "left": display["originX"],
        "top": display["originY"],
        "width": display["width"],
        "height": display["height"],
    }
    result = capture_monitor(region, resize)
    result.update(
        {
            "displayWidth": display["width"],
            "displayHeight": display["height"],
            "displayId": display["displayId"],
            "originX": display["originX"],
            "originY": display["originY"],
            "display": display,
        }
    )
    return result


def window_name(win) -> str:
    for prop in ["_NET_WM_NAME", "WM_NAME"]:
        try:
            value = win.get_full_property(atom(win.display, prop), X.AnyPropertyType)
            if value and getattr(value, "value", None):
                raw = value.value
                if isinstance(raw, bytes):
                    return raw.decode("utf-8", errors="ignore")
                if isinstance(raw, str):
                    return raw
                if hasattr(raw, "tolist"):
                    data = bytes(raw.tolist())
                    return data.decode("utf-8", errors="ignore")
        except XError:
            continue
        except Exception:
            continue
    return ""


def get_window_pid(win) -> int | None:
    try:
        value = win.get_full_property(atom(win.display, "_NET_WM_PID"), X.AnyPropertyType)
        if value and getattr(value, "value", None):
            return int(value.value[0])
    except Exception:
        return None
    return None


def get_process_path(pid: int | None) -> str | None:
    if not pid:
        return None
    try:
        return psutil.Process(pid).exe()
    except Exception:
        return None


def display_name_from_path(path: str | None, fallback: str) -> str:
    if path:
        stem = Path(path).stem
        if stem:
            return stem
    return fallback


def list_windows() -> list[dict[str, Any]]:
    disp = require_x11()
    root = disp.screen().root
    client_list = root.get_full_property(atom(disp, "_NET_CLIENT_LIST"), X.AnyPropertyType)
    windows: list[dict[str, Any]] = []
    if not client_list:
        return windows
    for window_id in client_list.value:
        try:
            win = disp.create_resource_object("window", int(window_id))
            geom = win.get_geometry()
            abs_pos = win.translate_coords(root, 0, 0)
            width = int(geom.width)
            height = int(geom.height)
            if width <= 1 or height <= 1:
                continue
            pid = get_window_pid(win)
            process_path = get_process_path(pid)
            title = window_name(win)
            windows.append(
                {
                    "windowId": int(window_id),
                    "pid": pid or 0,
                    "processPath": process_path or "",
                    "displayName": display_name_from_path(process_path, title or f"pid-{pid or 0}"),
                    "title": title,
                    "bounds": {"x": int(abs_pos.x), "y": int(abs_pos.y), "width": width, "height": height},
                }
            )
        except Exception:
            continue
    return windows


def frontmost_window() -> dict[str, Any] | None:
    disp = require_x11()
    root = disp.screen().root
    active = root.get_full_property(atom(disp, "_NET_ACTIVE_WINDOW"), X.AnyPropertyType)
    if not active or not active.value or int(active.value[0]) == 0:
        return None
    win = disp.create_resource_object("window", int(active.value[0]))
    pid = get_window_pid(win)
    process_path = get_process_path(pid)
    return {
        "bundleId": process_path or (window_name(win) or f"pid-{pid or 0}"),
        "displayName": display_name_from_path(process_path, window_name(win) or f"pid-{pid or 0}"),
    }


def point_in_bounds(x: int, y: int, bounds: dict[str, int]) -> bool:
    return bounds["x"] <= x < bounds["x"] + bounds["width"] and bounds["y"] <= y < bounds["y"] + bounds["height"]


def app_under_point(x: int, y: int) -> dict[str, str] | None:
    windows = list_windows()
    for window in reversed(windows):
        if point_in_bounds(x, y, window["bounds"]):
            bundle_id = window["processPath"] or window["title"] or f"pid-{window['pid']}"
            return {"bundleId": bundle_id, "displayName": window["displayName"]}
    return frontmost_window()


def intersects(a: dict[str, int], b: dict[str, int]) -> bool:
    return max(a["x"], b["x"]) < min(a["x"] + a["width"], b["x"] + b["width"]) and max(a["y"], b["y"]) < min(a["y"] + a["height"], b["y"] + b["height"])


def find_window_displays(bundle_ids: list[str]) -> list[dict[str, Any]]:
    windows = list_windows()
    displays = get_displays()
    result: list[dict[str, Any]] = []
    for bundle_id in bundle_ids:
        display_ids: set[int] = set()
        for window in windows:
            if (window["processPath"] or "") != bundle_id:
                continue
            for display in displays:
                display_bounds = {
                    "x": display["originX"],
                    "y": display["originY"],
                    "width": display["width"],
                    "height": display["height"],
                }
                if intersects(window["bounds"], display_bounds):
                    display_ids.add(int(display["displayId"]))
        result.append({"bundleId": bundle_id, "displayIds": sorted(display_ids)})
    return result


def list_installed_apps() -> list[dict[str, Any]]:
    results: dict[str, dict[str, Any]] = {}
    desktop_files = []
    for root in [Path("/usr/share/applications"), Path.home() / ".local/share/applications"]:
        if root.exists():
            desktop_files.extend(root.rglob("*.desktop"))
    for desktop_file in desktop_files:
        try:
            text = desktop_file.read_text(errors="ignore")
        except Exception:
            continue
        name = ""
        exec_path = ""
        for line in text.splitlines():
            if line.startswith("Name=") and not name:
                name = line.split("=", 1)[1].strip()
            if line.startswith("Exec=") and not exec_path:
                exec_value = line.split("=", 1)[1].strip().split()[0]
                exec_path = exec_value.replace("%u", "").replace("%U", "")
        if not exec_path:
            continue
        if not exec_path.startswith("/"):
            resolved = shutil.which(exec_path)
            exec_path = resolved or exec_path
        bundle_id = exec_path
        results.setdefault(bundle_id, {"bundleId": bundle_id, "displayName": name or Path(exec_path).stem, "path": exec_path})
    return sorted(results.values(), key=lambda item: item["displayName"].lower())


def list_running_apps() -> list[dict[str, Any]]:
    results: dict[str, dict[str, Any]] = {}
    for proc in psutil.process_iter(["pid", "name", "exe"]):
        exe = proc.info.get("exe")
        if not exe:
            continue
        results.setdefault(exe, {"bundleId": exe, "displayName": display_name_from_path(exe, proc.info.get("name") or exe)})
    return sorted(results.values(), key=lambda item: item["displayName"].lower())


def open_app(bundle_id: str) -> None:
    target = bundle_id.strip()
    if not target:
        raise RuntimeError("Missing app identifier")
    if target.endswith(".desktop") and os.path.exists(target):
        subprocess.Popen(["gtk-launch", Path(target).stem])
        return
    if os.path.exists(target):
        subprocess.Popen([target])
        return
    subprocess.Popen([target])


def read_clipboard() -> str:
    root = Tk()
    root.withdraw()
    try:
        return root.clipboard_get()
    finally:
        root.destroy()


def write_clipboard(text: str) -> None:
    root = Tk()
    root.withdraw()
    try:
        root.clipboard_clear()
        root.clipboard_append(text)
        root.update()
    finally:
        root.destroy()


def check_permissions() -> dict[str, bool]:
    has_display = bool(os.environ.get("DISPLAY"))
    return {"accessibility": has_display, "screenRecording": has_display}


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":
            json_output({"ok": True, "result": check_permissions()})
            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"]))
            json_output({"ok": True, "result": capture_display(payload.get("displayId"), resize)})
            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_monitor(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_window()})
            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": list_installed_apps()})
            return 0
        if command == "list_running_apps":
            json_output({"ok": True, "result": list_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())


path: project/platforms/linux/runtime/requirements.txt
content: mss>=10.1.0
Pillow>=11.3.0
pyautogui>=0.9.54
psutil>=7.0.0
python-xlib>=0.33


path: project/platforms/linux/src/cli.ts
content: #!/usr/bin/env node
import { runServer } from './server.js'
import { errorMessage } from './lib/errors.js'

try {
  await runServer()
} catch (error) {
  process.stderr.write(`linux-computer-use-skill failed: ${errorMessage(error)}\n`)
  process.exit(1)
}


path: project/platforms/linux/src/computer-use/appNames.ts
content: type InstalledAppLike = {
  readonly bundleId: string
  readonly displayName: string
  readonly path: string
}

const PATH_ALLOWLIST: readonly string[] = [
  '/Applications/',
  '/System/Applications/',
]

const NAME_PATTERN_BLOCKLIST: readonly RegExp[] = [
  /Helper(?:$|\s\()/,
  /Agent(?:$|\s\()/,
  /Service(?:$|\s\()/,
  /Uninstaller(?:$|\s\()/,
  /Updater(?:$|\s\()/,
  /^\./,
]

const ALWAYS_KEEP_BUNDLE_IDS: ReadonlySet<string> = new Set([
  'com.apple.Safari',
  'com.google.Chrome',
  'com.microsoft.edgemac',
  'org.mozilla.firefox',
  'company.thebrowser.Browser',
  'com.tinyspeck.slackmacgap',
  'us.zoom.xos',
  'com.microsoft.teams2',
  'com.microsoft.teams',
  'com.apple.MobileSMS',
  'com.apple.mail',
  'com.microsoft.Word',
  'com.microsoft.Excel',
  'com.microsoft.Powerpoint',
  'com.microsoft.Outlook',
  'com.apple.iWork.Pages',
  'com.apple.iWork.Numbers',
  'com.apple.iWork.Keynote',
  'notion.id',
  'com.apple.Notes',
  'md.obsidian',
  'com.linear',
  'com.figma.Desktop',
  'com.microsoft.VSCode',
  'com.apple.Terminal',
  'com.googlecode.iterm2',
  'com.github.GitHubDesktop',
  'com.apple.finder',
  'com.apple.iCal',
  'com.apple.systempreferences',
])

const APP_NAME_ALLOWED = /^[\p{L}\p{M}\p{N}_ .&'()+-]+$/u
const APP_NAME_MAX_LEN = 40
const APP_NAME_MAX_COUNT = 50

function isUserFacingPath(path: string, homeDir: string | undefined): boolean {
  if (PATH_ALLOWLIST.some(root => path.startsWith(root))) return true
  if (homeDir) {
    const userApps = homeDir.endsWith('/') ? `${homeDir}Applications/` : `${homeDir}/Applications/`
    if (path.startsWith(userApps)) return true
  }
  return false
}

function sanitizeCore(raw: readonly string[], applyCharFilter: boolean): string[] {
  const seen = new Set<string>()
  return raw
    .map(name => name.trim())
    .filter(trimmed => {
      if (!trimmed || trimmed.length > APP_NAME_MAX_LEN) return false
      if (applyCharFilter && !APP_NAME_ALLOWED.test(trimmed)) return false
      if (seen.has(trimmed)) return false
      seen.add(trimmed)
      return true
    })
    .sort((a, b) => a.localeCompare(b))
}

function sanitizeAppNames(raw: readonly string[]): string[] {
  const filtered = sanitizeCore(raw, true)
  if (filtered.length <= APP_NAME_MAX_COUNT) return filtered
  return [
    ...filtered.slice(0, APP_NAME_MAX_COUNT),
    `... and ${filtered.length - APP_NAME_MAX_COUNT} more`,
  ]
}

export function filterAppsForDescription(
  installed: readonly InstalledAppLike[],
  homeDir: string | undefined,
): string[] {
  const alwaysKept: string[] = []
  const rest: string[] = []

  for (const app of installed) {
    if (ALWAYS_KEEP_BUNDLE_IDS.has(app.bundleId)) {
      alwaysKept.push(app.displayName)
      continue
    }
    if (isUserFacingPath(app.path, homeDir) && !NAME_PATTERN_BLOCKLIST.some(re => re.test(app.displayName))) {
      rest.push(app.displayName)
    }
  }

  const sanitizedAlways = sanitizeCore(alwaysKept, false)
  const alwaysSet = new Set(sanitizedAlways)
  return [
    ...sanitizedAlways,
    ...sanitizeAppNames(rest).filter(name => !alwaysSet.has(name)),
  ]
}


path: project/platforms/linux/src/computer-use/common.ts
content: export const COMPUTER_USE_MCP_SERVER_NAME = 'computer-use'
export const CLI_HOST_BUNDLE_ID = 'io.github.wimi321.linux-computer-use-skill'

export const CLI_CU_CAPABILITIES = {
  screenshotFiltering: 'none' as const,
  platform: 'linux' as const,
}


path: project/platforms/linux/src/computer-use/computerUseLock.ts
content: import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { homedir } from 'node:os'
import { getErrnoCode } from '../lib/errors.js'
import { logDebug } from '../lib/log.js'

const LOCK_FILENAME = 'computer-use.lock'
const CONFIG_DIR = join(homedir(), '.linux-computer-use-skill')

let heldLocally = false

export type AcquireResult =
  | { kind: 'acquired'; fresh: boolean }
  | { kind: 'blocked'; by: string }

export type CheckResult =
  | { kind: 'free' }
  | { kind: 'held_by_self' }
  | { kind: 'blocked'; by: string }

function getSessionId(): string {
  return process.env.CODEX_THREAD_ID ?? `pid-${process.pid}`
}

function getLockPath(): string {
  return join(CONFIG_DIR, LOCK_FILENAME)
}

async function readLock(): Promise<{ sessionId: string; pid: number } | undefined> {
  try {
    const parsed = JSON.parse(await readFile(getLockPath(), 'utf8'))
    if (typeof parsed?.sessionId === 'string' && typeof parsed?.pid === 'number') return parsed
  } catch {}
  return undefined
}

function isProcessRunning(pid: number): boolean {
  try {
    process.kill(pid, 0)
    return true
  } catch {
    return false
  }
}

async function tryCreateExclusive(lock: { sessionId: string; pid: number }): Promise<boolean> {
  try {
    await writeFile(getLockPath(), JSON.stringify(lock), { flag: 'wx' })
    return true
  } catch (error) {
    if (getErrnoCode(error) === 'EEXIST') return false
    throw error
  }
}

export async function checkComputerUseLock(): Promise<CheckResult> {
  const existing = await readLock()
  if (!existing) return { kind: 'free' }
  if (existing.sessionId === getSessionId()) return { kind: 'held_by_self' }
  if (isProcessRunning(existing.pid)) return { kind: 'blocked', by: existing.sessionId }
  await unlink(getLockPath()).catch(() => {})
  return { kind: 'free' }
}

export async function tryAcquireComputerUseLock(): Promise<AcquireResult> {
  const lock = { sessionId: getSessionId(), pid: process.pid }
  await mkdir(CONFIG_DIR, { recursive: true })
  if (await tryCreateExclusive(lock)) {
    heldLocally = true
    return { kind: 'acquired', fresh: true }
  }
  const existing = await readLock()
  if (!existing) {
    await unlink(getLockPath()).catch(() => {})
    if (await tryCreateExclusive(lock)) {
      heldLocally = true
      return { kind: 'acquired', fresh: true }
    }
    return { kind: 'blocked', by: 'unknown' }
  }
  if (existing.sessionId === lock.sessionId) {
    heldLocally = true
    return { kind: 'acquired', fresh: false }
  }
  if (isProcessRunning(existing.pid)) return { kind: 'blocked', by: existing.sessionId }
  logDebug('recovering stale computer-use lock from %s', existing.sessionId)
  await unlink(getLockPath()).catch(() => {})
  if (await tryCreateExclusive(lock)) {
    heldLocally = true
    return { kind: 'acquired', fresh: true }
  }
  return { kind: 'blocked', by: 'unknown' }
}

export async function releaseComputerUseLock(): Promise<boolean> {
  const existing = await readLock()
  heldLocally = false
  if (!existing || existing.sessionId !== getSessionId()) return false
  try {
    await unlink(getLockPath())
    return true
  } catch {
    return false
  }
}

export function isLockHeldLocally(): boolean {
  return heldLocally
}


path: project/platforms/linux/src/computer-use/drainRunLoop.ts
content: import { logDebug } from '../lib/log.js'
import { withResolvers } from '../lib/withResolvers.js'
import { requireComputerUseSwift } from './swiftLoader.js'

let pump: ReturnType<typeof setInterval> | undefined
let pending = 0
const TIMEOUT_MS = 30_000

function retain(): void {
  pending += 1
  if (!pump) {
    pump = setInterval(() => requireComputerUseSwift()._drainMainRunLoop(), 1)
    logDebug('drain run loop started')
  }
}

function release(): void {
  pending -= 1
  if (pending <= 0 && pump) {
    clearInterval(pump)
    pump = undefined
    pending = 0
    logDebug('drain run loop stopped')
  }
}

export const retainPump = retain
export const releasePump = release

export async function drainRunLoop<T>(fn: () => Promise<T>): Promise<T> {
  retain()
  let timer: ReturnType<typeof setTimeout> | undefined
  try {
    const work = fn()
    work.catch(() => {})
    const timeout = withResolvers<never>()
    timer = setTimeout(() => timeout.reject(new Error(`computer-use native call exceeded ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
    return await Promise.race([work, timeout.promise])
  } finally {
    if (timer) clearTimeout(timer)
    release()
  }
}


path: project/platforms/linux/src/computer-use/escHotkey.ts
content: import { logDebug, logWarn } from '../lib/log.js'
import { releasePump, retainPump } from './drainRunLoop.js'
import { requireComputerUseSwift } from './swiftLoader.js'

let registered = false

export function registerEscHotkey(onEscape: () => void): boolean {
  if (registered) return true
  const cu = requireComputerUseSwift()
  if (!cu.hotkey?.registerEscape?.(onEscape)) {
    logWarn('escape hotkey registration failed')
    return false
  }
  retainPump()
  registered = true
  logDebug('escape hotkey registered')
  return true
}

export function unregisterEscHotkey(): void {
  if (!registered) return
  try {
    requireComputerUseSwift().hotkey.unregister()
  } finally {
    releasePump()
    registered = false
    logDebug('escape hotkey unregistered')
  }
}

export function notifyExpectedEscape(): void {
  if (!registered) return
  requireComputerUseSwift().hotkey.notifyExpectedEscape()
}


path: project/platforms/linux/src/computer-use/executor.ts
content: import type {
  ComputerExecutor,
  DisplayGeometry,
  FrontmostApp,
  InstalledApp,
  ResolvePrepareCaptureResult,
  RunningApp,
  ScreenshotResult,
} from '../vendor/computer-use-mcp/executor.js'
import { API_RESIZE_PARAMS, targetImageSize } from '../vendor/computer-use-mcp/imageResize.js'
import { sleep } from '../lib/sleep.js'
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

function computeTargetDims(
  logicalW: number,
  logicalH: number,
  scaleFactor: number,
): [number, number] {
  const physW = Math.round(logicalW * scaleFactor)
  const physH = Math.round(logicalH * scaleFactor)
  return targetImageSize(physW, physH, API_RESIZE_PARAMS)
}

function normalizeDisplayGeometry(display: PythonDisplayGeometry): DisplayGeometry {
  return {
    ...display,
    displayId: display.displayId ?? display.id,
    label: display.label ?? display.name,
  }
}

async function readClipboard(): Promise<string> {
  return callPythonHelper('read_clipboard', {})
}

async function writeClipboard(text: string): Promise<void> {
  await callPythonHelper('write_clipboard', { text })
}

async function typeViaClipboard(text: string): Promise<void> {
  let saved: string | undefined
  try {
    saved = await readClipboard()
  } catch {}

  try {
    await writeClipboard(text)
    await callPythonHelper('key', { keySequence: 'ctrl+v', repeat: 1 })
    await sleep(100)
  } finally {
    if (typeof saved === 'string') {
      try {
        await writeClipboard(saved)
      } catch {}
    }
  }
}

export function createCliExecutor(_opts: {
  getMouseAnimationEnabled: () => boolean
  getHideBeforeActionEnabled: () => boolean
}): ComputerExecutor {
  if (process.platform !== 'linux') {
    throw new Error(`createCliExecutor called on ${process.platform}. Computer control is Linux-only.`)
  }

  return {
    capabilities: {
      ...CLI_CU_CAPABILITIES,
      hostBundleId: CLI_HOST_BUNDLE_ID,
    },

    async prepareForAction(): Promise<string[]> {
      return callPythonHelper('prepare_for_action', {})
    },

    async previewHideSet() {
      return callPythonHelper('preview_hide_set', {})
    },

    async getDisplaySize(displayId?: number): Promise<DisplayGeometry> {
      return normalizeDisplayGeometry(await callPythonHelper('get_display_size', { displayId }))
    },

    async listDisplays(): Promise<DisplayGeometry[]> {
      const displays = await callPythonHelper<PythonDisplayGeometry[]>('list_displays', {})
      return displays.map(display => normalizeDisplayGeometry(display))
    },

    async findWindowDisplays(bundleIds: string[]) {
      return callPythonHelper('find_window_displays', { bundleIds })
    },

    async resolvePrepareCapture(opts): Promise<ResolvePrepareCaptureResult> {
      const display = await this.getDisplaySize(opts.preferredDisplayId)
      const [targetW, targetH] = computeTargetDims(display.width, display.height, display.scaleFactor)
      const result = await callPythonHelper<PythonResolvePrepareCaptureResult>('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,
      }
    },

    async screenshot(opts): Promise<ScreenshotResult> {
      const display = await this.getDisplaySize(opts.displayId)
      const [targetW, targetH] = computeTargetDims(display.width, display.height, display.scaleFactor)
      const result = await callPythonHelper<ScreenshotResult>('screenshot', {
        displayId: opts.displayId,
        targetWidth: targetW,
        targetHeight: targetH,
        jpegQuality: SCREENSHOT_JPEG_QUALITY,
      })
      return result
    },

    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 key(keySequence: string, repeat?: number): Promise<void> {
      await callPythonHelper('key', { keySequence, repeat: repeat ?? 1 })
    },

    async holdKey(keyNames: string[], durationMs: number): Promise<void> {
      await callPythonHelper('hold_key', { keyNames, durationMs })
    },

    async type(text: string, opts2: { viaClipboard: boolean }): Promise<void> {
      if (opts2.viaClipboard) {
        await typeViaClipboard(text)
        return
      }
      await callPythonHelper('type', { text })
    },

    readClipboard,
    writeClipboard,

    async click(x, y, button, count, modifiers): Promise<void> {
      await callPythonHelper('click', { x, y, button, count, modifiers })
      await sleep(MOVE_SETTLE_MS)
    },

    async mouseDown(): Promise<void> {
      await callPythonHelper('mouse_down', {})
    },

    async mouseUp(): Promise<void> {
      await callPythonHelper('mouse_up', {})
    },

    async getCursorPosition(): Promise<{ x: number; y: number }> {
      return callPythonHelper('cursor_position', {})
    },

    async drag(from, to): Promise<void> {
      await callPythonHelper('drag', { from, to })
      await sleep(MOVE_SETTLE_MS)
    },

    async moveMouse(x, y): Promise<void> {
      await callPythonHelper('move_mouse', { x, y })
      await sleep(MOVE_SETTLE_MS)
    },

    async scroll(x, y, dx, dy): Promise<void> {
      await callPythonHelper('scroll', { x, y, deltaX: dx, deltaY: dy })
    },

    async getFrontmostApp(): Promise<FrontmostApp | null> {
      return callPythonHelper('frontmost_app', {})
    },

    async appUnderPoint(x, y) {
      return callPythonHelper('app_under_point', { x, y })
    },

    async listInstalledApps(): Promise<InstalledApp[]> {
      return callPythonHelper('list_installed_apps', {})
    },

    async listRunningApps(): Promise<RunningApp[]> {
      return callPythonHelper('list_running_apps', {})
    },

    async openApp(bundleId: string): Promise<void> {
      await callPythonHelper('open_app', { bundleId })
    },
  }
}

export async function unhideComputerUseApps(_bundleIds: readonly string[]): Promise<void> {
  return
}


path: project/platforms/linux/src/computer-use/gates.ts
content: import type { CoordinateMode, CuSubGates } from '../vendor/computer-use-mcp/types.js'

const enabled = process.env.CLAUDE_COMPUTER_USE_ENABLED !== '0'
const coordinateMode = (process.env.CLAUDE_COMPUTER_USE_COORDINATE_MODE as CoordinateMode | undefined) ?? 'pixels'

export function getChicagoEnabled(): boolean {
  return enabled
}

export function getChicagoCoordinateMode(): CoordinateMode {
  return coordinateMode
}

export function getChicagoSubGates(): CuSubGates {
  return {
    pixelValidation: process.env.CLAUDE_COMPUTER_USE_PIXEL_VALIDATION === '1',
    clipboardPasteMultiline: process.env.CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE !== '0',
    mouseAnimation: process.env.CLAUDE_COMPUTER_USE_MOUSE_ANIMATION !== '0',
    hideBeforeAction: process.env.CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION !== '0',
    autoTargetDisplay: process.env.CLAUDE_COMPUTER_USE_AUTO_TARGET_DISPLAY !== '0',
    clipboardGuard: process.env.CLAUDE_COMPUTER_USE_CLIPBOARD_GUARD !== '0',
  }
}


path: project/platforms/linux/src/computer-use/hostAdapter.ts
content: import type { ComputerUseHostAdapter, Logger } from '../vendor/computer-use-mcp/types.js'
import { format } from 'node:util'
import { logDebug, logWarn } from '../lib/log.js'
import { COMPUTER_USE_MCP_SERVER_NAME } from './common.js'
import { createCliExecutor } from './executor.js'
import { getChicagoEnabled, getChicagoSubGates } from './gates.js'
import { callPythonHelper } from './pythonBridge.js'

class DebugLogger implements Logger {
  silly(message: string, ...args: unknown[]): void { logDebug(format(message, ...args)) }
  debug(message: string, ...args: unknown[]): void { logDebug(format(message, ...args)) }
  info(message: string, ...args: unknown[]): void { logDebug(format(message, ...args)) }
  warn(message: string, ...args: unknown[]): void { logWarn(format(message, ...args)) }
  error(message: string, ...args: unknown[]): void { logWarn(format(message, ...args)) }
}

let cached: ComputerUseHostAdapter | undefined

export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
  if (cached) return cached
  cached = {
    serverName: COMPUTER_USE_MCP_SERVER_NAME,
    logger: new DebugLogger(),
    executor: createCliExecutor({
      getMouseAnimationEnabled: () => getChicagoSubGates().mouseAnimation,
      getHideBeforeActionEnabled: () => getChicagoSubGates().hideBeforeAction,
    }),
    ensureOsPermissions: async () => {
      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,
    getAutoUnhideEnabled: () => true,
    cropRawPatch: () => null,
  }
  return cached
}


path: project/platforms/linux/src/computer-use/inputLoader.ts
content: import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
let cached: any

export function requireComputerUseInput(): any {
  if (cached) return cached
  throw new Error(
    'Legacy native input loader was retained only for source-history reference. ' +
    'The standalone runtime now uses the Python bridge in src/computer-use/pythonBridge.ts instead.',
  )
}


path: project/platforms/linux/src/computer-use/pythonBridge.ts
content: 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 '../lib/execFileNoThrow.js'
import { logDebug } from '../lib/log.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, 'linux_helper.py')
const installStampPath = path.join(runtimeStateRoot, 'requirements.sha256')

let bootstrapPromise: Promise<void> | undefined

function pythonBinPath(): string {
  return process.platform === 'win32'
    ? path.join(venvRoot, 'Scripts', 'python.exe')
    : path.join(venvRoot, 'bin', 'python3')
}

function pipBinPath(): string {
  return process.platform === 'win32'
    ? path.join(venvRoot, 'Scripts', 'pip.exe')
    : path.join(venvRoot, 'bin', 'pip')
}

async function createVenv(): Promise<void> {
  if (process.platform === 'win32') {
    const pyLauncher = await execFileNoThrow('py', ['-3', '-m', 'venv', venvRoot], { useCwd: false })
    if (pyLauncher.code === 0) return
    await runOrThrow('python', ['-m', 'venv', venvRoot], 'python venv creation')
    return
  }
  await runOrThrow('python3', ['-m', 'venv', venvRoot], 'python venv creation')
}

async function pathExists(target: string): Promise<boolean> {
  try {
    await access(target)
    return true
  } catch {
    return false
  }
}

async function runOrThrow(file: string, args: string[], label: string): Promise<string> {
  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
}

async function ensureBootstrapped(): Promise<void> {
  if (bootstrapPromise) return bootstrapPromise
  bootstrapPromise = (async () => {
    await mkdir(runtimeStateRoot, { recursive: true })

    if (!(await pathExists(pythonBinPath()))) {
      logDebug('creating runtime venv at %s', venvRoot)
      await createVenv()
    }

    if (!(await pathExists(pipBinPath()))) {
      logDebug('bootstrapping pip with ensurepip')
      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) {
      logDebug('installing python runtime dependencies')
      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<T>(command: string, payload: Record<string, unknown> = {}): Promise<T> {
  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 }
}


path: project/platforms/linux/src/computer-use/swiftLoader.ts
content: import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
let cached: any

export function requireComputerUseSwift(): any {
  if (cached) return cached
  throw new Error(
    'Legacy native Swift loader was retained only for source-history reference. ' +
    'The standalone runtime now uses the Python bridge in src/computer-use/pythonBridge.ts instead.',
  )
}


path: project/platforms/linux/src/lib/errors.ts
content: export function errorMessage(error: unknown): string {
  if (error instanceof Error) return error.message
  return String(error)
}

export function getErrnoCode(error: unknown): string | undefined {
  if (typeof error === 'object' && error !== null && 'code' in error) {
    const code = (error as { code?: unknown }).code
    return typeof code === 'string' ? code : undefined
  }
  return undefined
}


path: project/platforms/linux/src/lib/execFileNoThrow.ts
content: import { execFile } from 'node:child_process'

export function execFileNoThrow(
  file: string,
  args: string[],
  options: { input?: string; useCwd?: boolean } = {},
): Promise<{ code: number; stdout: string; stderr: string }> {
  return new Promise(resolve => {
    const child = execFile(
      file,
      args,
      {
        cwd: options.useCwd === false ? undefined : process.cwd(),
        encoding: 'utf8',
        maxBuffer: 32 * 1024 * 1024,
      },
      (error, stdout, stderr) => {
        const code = typeof (error as NodeJS.ErrnoException | null)?.code === 'number'
          ? (error as NodeJS.ErrnoException).code as unknown as number
          : error
            ? 1
            : 0
        resolve({ code, stdout: stdout ?? '', stderr: stderr ?? '' })
      },
    )

    if (options.input !== undefined) {
      child.stdin?.end(options.input)
    }
  })
}


path: project/platforms/linux/src/lib/log.ts
content: import { format } from 'node:util'

const enabled = process.env.CLAUDE_COMPUTER_USE_DEBUG === '1'

export function logDebug(message: string, ...args: unknown[]): void {
  if (!enabled) return
  process.stderr.write(`[claude-computer-use] ${format(message, ...args)}\n`)
}

export function logWarn(message: string, ...args: unknown[]): void {
  process.stderr.write(`[claude-computer-use][warn] ${format(message, ...args)}\n`)
}


path: project/platforms/linux/src/lib/sleep.ts
content: export function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms))
}


path: project/platforms/linux/src/lib/withResolvers.ts
content: export function withResolvers<T>() {
  let resolve!: (value: T | PromiseLike<T>) => void
  let reject!: (reason?: unknown) => void
  const promise = new Promise<T>((res, rej) => {
    resolve = res
    reject = rej
  })
  return { promise, resolve, reject }
}


path: project/platforms/linux/src/server.ts
content: import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { homedir } from 'node:os'
import { createComputerUseMcpServer } from './vendor/computer-use-mcp/mcpServer.js'
import { buildComputerUseTools } from './vendor/computer-use-mcp/tools.js'
import { filterAppsForDescription } from './computer-use/appNames.js'
import { getComputerUseHostAdapter } from './computer-use/hostAdapter.js'
import { getChicagoCoordinateMode } from './computer-use/gates.js'
import { createSessionContext } from './session.js'
import { logDebug } from './lib/log.js'

async function getInstalledAppNames(): Promise<string[] | undefined> {
  try {
    const installed = await getComputerUseHostAdapter().executor.listInstalledApps()
    return filterAppsForDescription(installed, homedir())
  } catch {
    return undefined
  }
}

export async function runServer(): Promise<void> {
  const adapter = getComputerUseHostAdapter()
  const coordinateMode = getChicagoCoordinateMode()
  const context = createSessionContext()
  const server = createComputerUseMcpServer(adapter, coordinateMode, context)
  const installedAppNames = await getInstalledAppNames()
  const tools = buildComputerUseTools(adapter.executor.capabilities, coordinateMode, installedAppNames)
  server.setRequestHandler((await import('@modelcontextprotocol/sdk/types.js')).ListToolsRequestSchema, async () =>
    adapter.isDisabled() ? { tools: [] } : { tools },
  )
  const transport = new StdioServerTransport()
  logDebug('starting stdio MCP server')
  await server.connect(transport)
}


path: project/platforms/linux/src/session.ts
content: import type {
  AppGrant,
  ComputerUseSessionContext,
  CuGrantFlags,
  CuPermissionRequest,
  CuPermissionResponse,
  ScreenshotDims,
} from './vendor/computer-use-mcp/types.js'
import { DEFAULT_GRANT_FLAGS } from './vendor/computer-use-mcp/types.js'
import { checkComputerUseLock, tryAcquireComputerUseLock } from './computer-use/computerUseLock.js'

type State = {
  allowedApps: AppGrant[]
  grantFlags: CuGrantFlags
  hiddenDuringTurn: Set<string>
  clipboardStash?: string
  selectedDisplayId?: number
  displayPinnedByModel?: boolean
  displayResolvedForApps?: string
  lastScreenshotDims?: ScreenshotDims
}

function autoApprovePermission(req: CuPermissionRequest): CuPermissionResponse {
  const granted = req.apps
    .filter(app => app.resolved && !app.alreadyGranted)
    .map(app => ({
      bundleId: app.resolved!.bundleId,
      displayName: app.resolved!.displayName,
      grantedAt: Date.now(),
      tier: app.proposedTier,
    }))

  const denied = req.apps
    .filter(app => !app.resolved)
    .map(app => ({
      bundleId: app.requestedName,
      reason: 'not_installed' as const,
    }))

  return {
    granted,
    denied,
    flags: {
      ...DEFAULT_GRANT_FLAGS,
      ...req.requestedFlags,
    },
  }
}

export function createSessionContext(): ComputerUseSessionContext {
  const state: State = {
    allowedApps: [],
    grantFlags: { ...DEFAULT_GRANT_FLAGS },
    hiddenDuringTurn: new Set<string>(),
  }

  return {
    getAllowedApps: () => state.allowedApps,
    getGrantFlags: () => state.grantFlags,
    getUserDeniedBundleIds: () => [],
    getSelectedDisplayId: () => state.selectedDisplayId,
    getDisplayPinnedByModel: () => state.displayPinnedByModel ?? false,
    getDisplayResolvedForApps: () => state.displayResolvedForApps,
    getLastScreenshotDims: () => state.lastScreenshotDims,
    onPermissionRequest: async req => autoApprovePermission(req),
    onAllowedAppsChanged: (apps, flags) => {
      state.allowedApps = [...apps]
      state.grantFlags = flags
    },
    onAppsHidden: ids => {
      for (const id of ids) state.hiddenDuringTurn.add(id)
    },
    getClipboardStash: () => state.clipboardStash,
    onClipboardStashChanged: stash => {
      state.clipboardStash = stash
    },
    onResolvedDisplayUpdated: displayId => {
      state.selectedDisplayId = displayId
      state.displayPinnedByModel = false
    },
    onDisplayPinned: displayId => {
      state.selectedDisplayId = displayId
      state.displayPinnedByModel = displayId !== undefined
      if (displayId === undefined) state.displayResolvedForApps = undefined
    },
    onDisplayResolvedForApps: key => {
      state.displayResolvedForApps = key
    },
    onScreenshotCaptured: dims => {
      state.lastScreenshotDims = dims
    },
    checkCuLock: async () => {
      const result = await checkComputerUseLock()
      switch (result.kind) {
        case 'free':
          return { holder: undefined, isSelf: false }
        case 'held_by_self':
          return { holder: process.env.CODEX_THREAD_ID ?? `pid-${process.pid}`, isSelf: true }
        case 'blocked':
          return { holder: result.by, isSelf: false }
      }
    },
    acquireCuLock: async () => {
      const result = await tryAcquireComputerUseLock()
      if (result.kind === 'blocked') {
        throw new Error(`Computer use is locked by another session (${result.by}).`)
      }
    },
    formatLockHeldMessage: holder =>
      `Computer use is already in use by another session (${holder.slice(0, 8)}...).`,
  }
}


path: project/platforms/linux/src/vendor/computer-use-mcp/deniedApps.ts
content: /**
 * 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 (Linux-only; `InstalledApp.bundleId` is a
 *      CFBundleIdentifier and meaningless on Linux). Fast, exact, the
 *      primary mechanism while CU is linux-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 Linux/Linux where there's no bundle ID.
 *      Linux-relevant names (PowerShell, cmd, Linux Terminal) are
 *      included now so they activate the moment the linux 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 (Linux) ─────────────────────────────────────────

const BROWSER_BUNDLE_IDS: ReadonlySet<string> = 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.kagiLinux", // 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<string> = 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 Linux 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<string> = 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.<AppName>. 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 Linux 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<string> = 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 Linux 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 Linux.
  "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 Linux/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 Linux. 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[] = [
  // Linux / cross-platform terminals
  "terminal", // catches Terminal, Linux 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 Linux; Linux exe-name
  // matching can be added when Linux CU ships.
  // Linux shells (activate when the linux gate lifts)
  "powershell",
  "cmd.exe",
  "command prompt",
  "git bash",
  "conemu",
  "cmder",
  // IDEs (VS Code family)
  "visual studio code",
  "visual studio", // catches VS for Mac + Linux
  "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 Linux.
  // 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,
};


path: project/platforms/linux/src/vendor/computer-use-mcp/executor.ts
content: 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: 'linux'
    hostBundleId: string
    teachMode?: boolean
  }
  prepareForAction(allowlistBundleIds: string[], displayId?: number): Promise<string[]>
  previewHideSet(allowlistBundleIds: string[], displayId?: number): Promise<Array<{ bundleId: string; displayName: string }>>
  getDisplaySize(displayId?: number): Promise<DisplayGeometry>
  listDisplays(): Promise<DisplayGeometry[]>
  findWindowDisplays(bundleIds: string[]): Promise<Array<{ bundleId: string; displayIds: number[] }>>
  resolvePrepareCapture(opts: {
    allowedBundleIds: string[]
    preferredDisplayId?: number
    autoResolve: boolean
    doHide?: boolean
  }): Promise<ResolvePrepareCaptureResult>
  screenshot(opts: { allowedBundleIds: string[]; displayId?: number }): Promise<ScreenshotResult>
  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<void>
  holdKey(keyNames: string[], durationMs: number): Promise<void>
  type(text: string, opts: { viaClipboard: boolean }): Promise<void>
  click(
    x: number,
    y: number,
    button: 'left' | 'right' | 'middle',
    count: 1 | 2 | 3,
    modifiers?: string[],
  ): Promise<void>
  drag(
    from: { x: number; y: number } | undefined,
    to: { x: number; y: number },
  ): Promise<void>
  moveMouse(x: number, y: number): Promise<void>
  scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<void>
  mouseDown(): Promise<void>
  mouseUp(): Promise<void>
  getCursorPosition(): Promise<{ x: number; y: number }>
  getFrontmostApp(): Promise<FrontmostApp | null>
  appUnderPoint(x: number, y: number): Promise<{ bundleId: string; displayName: string } | null>
  listInstalledApps(): Promise<InstalledApp[]>
  getAppIcon?(path: string): Promise<string | undefined>
  listRunningApps(): Promise<RunningApp[]>
  openApp(bundleId: string): Promise<void>
  readClipboard(): Promise<string>
  writeClipboard(text: string): Promise<void>
}


path: project/platforms/linux/src/vendor/computer-use-mcp/imageResize.ts
content: /**
 * 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;
    }
  }
}


path: project/platforms/linux/src/vendor/computer-use-mcp/index.ts
content: 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";


path: project/platforms/linux/src/vendor/computer-use-mcp/keyBlocklist.ts
content: /**
 * 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/Super on win32/linux.
 */
const CANONICAL_MODIFIER: Readonly<Record<string, string>> = {
  // 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. We collapse aliases
  // so platform-specific blocklists can reason over one canonical modifier.
  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
]);

const BLOCKED_LINUX = new Set([
  "ctrl+alt+delete", // logout / system dialog on many desktops
  "alt+f4", // close window
  "alt+tab", // window switcher
  "meta+tab", // shell/app switcher on some desktops
  "meta+l", // lock screen
  "meta+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" | "linux",
): boolean {
  const blocklist =
    platform === "darwin"
      ? BLOCKED_DARWIN
      : platform === "linux"
        ? BLOCKED_LINUX
        : 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,
  BLOCKED_LINUX,
  MODIFIER_ORDER,
};


path: project/platforms/linux/src/vendor/computer-use-mcp/mcpServer.ts
content: /**
 * 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<CuCallToolResult> {
  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<NonNullable<typeof ctx.onPermissionRequest>>[0],
        signal: AbortSignal,
      ): Promise<CuPermissionResponse> => {
        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<NonNullable<typeof ctx.onTeachPermissionRequest>>[0],
        signal: AbortSignal,
      ): Promise<CuPermissionResponse> => {
        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<CallToolResult> => {
        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<CallToolResult> => {
      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;
}


path: project/platforms/linux/src/vendor/computer-use-mcp/pixelCompare.ts
content: /**
 * 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<ScreenshotResult | null>,
  logger: Logger,
  gridSize: number = DEFAULT_GRID_SIZE,
): Promise<PixelCompareResult> {
  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 };
  }
}


path: project/platforms/linux/src/vendor/computer-use-mcp/sentinelApps.ts
content: /**
 * 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<string> = 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;
}


path: project/platforms/linux/src/vendor/computer-use-mcp/subGates.ts
content: 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,
}


path: project/platforms/linux/src/vendor/computer-use-mcp/tools.ts
content: /**
 * 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<CoordinateMode, { x: string; y: string }> = {
  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" | "linux";
    /** 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"],
      },
    },
  ];
}


path: project/platforms/linux/src/vendor/computer-use-mcp/types.ts
content: 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<ScreenshotResult, "base64">;

/** 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<CuGrantFlags>;
  /**
   * For the "On Linux, 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. Linux 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 Linux. 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<CuPermissionResponse>;
  /** Teach-mode sibling of `onPermissionRequest`. */
  onTeachPermissionRequest?(
    req: CuTeachPermissionRequest,
    signal: AbortSignal,
  ): Promise<CuPermissionResponse>;
  /** 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<TeachStepResult>;
  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<void>;
  /** 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<CuPermissionResponse>;

  /**
   * 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<CuPermissionResponse>;

  /**
   * 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<TeachStepResult>;

  /**
   * 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
 * Linux 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;
}


path: project/platforms/linux/tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "dist",
    "rootDir": "src",
    "strict": false,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,
    "declaration": true,
    "sourceMap": true,
    "types": ["node"]
  },
  "include": ["src/**/*.ts"],
  "exclude": ["dist", "node_modules"]
}


path: project/platforms/macos/README.ja.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="macos-computer-use-skill hero" width="100%" />
  <h1>macOS Computer-Use Skill</h1>
  <p><strong>macOS 向けのトップレベル portable skill。standalone runtime と MCP server を同梱しています。</strong></p>
  <p>
    <a href="https://github.com/wimi321/macos-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-macos">ClawHub</a>
    ·
    <a href="./README.md">English</a>
    ·
    <a href="./README.zh-CN.md">简体中文</a>
  </p>
</div>

## ClawHub からインストール

この skill は ClawHub に [`computer-use-macos`](https://clawhub.ai/wimi321/computer-use-macos) として公開済みです。

```bash
clawhub install computer-use-macos
```

ソース一式も欲しい場合は、このまま GitHub セットアップを参照してください。

## このプロジェクトの位置づけ

このリポジトリは同時に:

- トップレベルの `skill`
- 独立した macOS runtime
- agent エコシステム向けの computer-use MCP server

として設計されています。Codex 専用ではなく、skill という配布形態そのものをポータブルにしています。

## このプロジェクトの目的

目標は明確です。

- ローカル Claude に依存しない
- private な `.node` バイナリに依存しない
- どこかのインストール済み Claude から内部資産を拾わない
- skill を入れたら、そのまま computer use が動く

このリポジトリはその前提で再構成されています。

## できること

- トップレベル macOS computer-use skill
- スクリーンショット、マウス、キーボード、アプリ起動、ウィンドウ/ディスプレイ情報、クリップボードを扱う standalone MCP server
- 公開依存のみ: `Node.js + Python + pyautogui + mss + Pillow + pyobjc`
- 初回起動時に `.runtime/venv` を自動作成し、Python 依存を自動インストール
- `~/.codex/skills/computer-use-macos/project` に本体まで配置される skill install
- 抽出した TypeScript computer-use ツール層を維持しつつ、実行バックエンドを完全独立化

## ローカル検証済み

実機の macOS で以下を確認しました。

- runtime bootstrap
- 権限チェック
- ディスプレイ列挙
- スクリーンショット取得
- 最前面アプリ検出
- ポインタ下アプリ検出
- ウィンドウとディスプレイの対応付け
- クリップボード読み書き
- MCP `type` ツール経由の GUI 入力 smoke test
- MCP server 起動

## 0.2.2 で修正したこと

実機テスト中に重要な不具合を確認しました。中国語 IME / 入力ソースが有効な状態では、通常の ASCII テキストでもキーを 1 文字ずつ送る方式だと内容が壊れることがありました。

`0.2.2` では、macOS 上でクリップボード書き込み権限がある場合、通常の複数字入力も優先的にクリップボード貼り付け経路へルーティングします。これにより、英字入力が IME 状態に引きずられて壊れるケースを避けられます。

## アーキテクチャ

```mermaid
flowchart LR
    A[Codex / MCP Client] --> B[macos-computer-use-skill]
    B --> C[抽出した TypeScript MCP ツール層]
    B --> D[Standalone Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[pyobjc Cocoa + Quartz]
    E --> H[マウス / キーボード]
    F --> I[スクリーンショット]
    G --> J[アプリ / ディスプレイ / クリップボード / ウィンドウ]
```

## インストール

### 1. クローンして Node 依存を入れる

```bash
git clone https://github.com/wimi321/macos-computer-use-skill.git
cd macos-computer-use-skill
npm install
npm run build
```

### 2. サーバーを起動

```bash
node dist/cli.js
```

初回起動時に自動で以下を実行します。

- `.runtime/venv` の作成
- 必要なら `pip` の bootstrap
- `runtime/requirements.txt` に基づく Python 依存の導入

Claude Desktop も private native module も不要です。

## MCP 設定

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/macos-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

参考: [`examples/mcp-config.json`](./examples/mcp-config.json)

## Skill インストール

同梱 skill: [`skill/computer-use-macos`](./skill/computer-use-macos)

ClawHub から直接入れる方法と、このリポジトリから入れる方法の両方があります。

### Option A: ClawHub からインストール

```bash
clawhub install computer-use-macos
```

### Option B: リポジトリからインストール

```bash
bash skill/computer-use-macos/scripts/install.sh
```

インストーラは以下をまとめてコピーします。

- skill メタデータ
- standalone プロジェクト本体
- runtime bootstrap ファイル

インストール後の既定パス:

```bash
~/.codex/skills/computer-use-macos/project
```

つまり元の clone が消えても、インストール済み skill 側のプロジェクトで動かせます。

## 実行メモ

### 権限

macOS では引き続き以下が必要です。

- Accessibility
- Screen Recording

この standalone host は MCP フローの中で両方を確認します。

### スクリーンショット filtering

この runtime は `screenshotFiltering: none` を返します。

つまり:

- スクリーンショット自体は compositor レベルでは絞り込まれない
- その代わり allowlist / tier / action gate は MCP 側で継続して適用される

### 対応プラットフォーム

この実装は現在 `macOS only` です。Windows や Linux 向け backend はまだ含まれていません。

## 検証マトリクス

今回この Mac で完了した実テスト:

- `npm run check`
- `npm run build`
- `runtime/mac_helper.py` の Python compile check
- Accessibility / Screen Recording の権限確認
- アクティブディスプレイの列挙
- 実デスクトップのスクリーンショット取得
- 実行中 / インストール済みアプリの列挙
- 最前面アプリの検出
- 新しい `CODEX_HOME` への skill インストール
- インストール済み `project/` での `npm install && npm run build`
- TextEdit を使った MCP `type` ツールの GUI 入力ラウンドトリップ検証

## 主要コマンド

```bash
npm run build
node dist/cli.js
```

```bash
node --input-type=module -e "import { callPythonHelper } from './dist/computer-use/pythonBridge.js'; console.log(await callPythonHelper('list_displays', {}));"
```

## リポジトリ構成

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  mac_helper.py
  requirements.txt
skill/
  computer-use-macos/
examples/
assets/
```

## 任意の環境変数

- `CLAUDE_COMPUTER_USE_DEBUG=1`
- `CLAUDE_COMPUTER_USE_COORDINATE_MODE=pixels`
- `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE=1`
- `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION=1`
- `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION=0`

## Roadmap

- app icon 抽出の改善
- nested helper app のフィルタ精度向上
- MCP 統合テストの拡充
- 配布しやすいリリース成果物の追加

## License

MIT

## Credits

Claude Code computer-use ワークフローから再利用可能な TypeScript ロジックを抽出し、そこに完全独立の公開 macOS runtime を接続したプロジェクトです。


path: project/platforms/macos/README.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="macos-computer-use-skill hero" width="100%" />
  <h1>macOS Computer-Use Skill</h1>
  <p><strong>A top-level portable skill for macOS with a bundled standalone runtime and MCP server.</strong></p>
  <p>
    <a href="https://github.com/wimi321/macos-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-macos">ClawHub</a>
    ·
    <a href="./README.zh-CN.md">简体中文</a>
    ·
    <a href="./README.ja.md">日本語</a>
  </p>
</div>

## Install From ClawHub

Published on ClawHub as [`computer-use-macos`](https://clawhub.ai/wimi321/computer-use-macos).

```bash
clawhub install computer-use-macos
```

If you want the source repo as well, keep reading for the full GitHub setup.

## Positioning

This repository is best understood as:

- a top-level `skill`
- a bundled standalone macOS runtime
- a computer-use MCP server for agent ecosystems

It is not just for Codex. The skill packaging is intentionally portable, so the same project can be adapted for ecosystems that consume skill-style distributions.

## Why This Project Exists

The original Claude Code computer-use stack was excellent, but the user requirement here was stricter:

- no piggybacking on a local Claude install
- no private `.node` binaries
- no "works if you already extracted internal assets"
- install the skill, launch the server, and use it

This repository now delivers exactly that on macOS.

## What You Get

- top-level macOS computer-use skill
- standalone MCP server for screenshots, mouse, keyboard, app launch, display switching context, and clipboard
- public dependency chain only: `Node.js + Python + pyautogui + mss + Pillow + pyobjc`
- first-run runtime bootstrap: the server creates its own virtualenv and installs dependencies automatically
- bundled skill install that copies the full project into `~/.codex/skills/computer-use-macos/project`
- extracted TypeScript tool layer from the original computer-use workflow, re-wired to a fully independent backend

## Current Status

This repository has been validated locally on a real macOS machine with:

- runtime bootstrap
- permission checks
- display enumeration
- screenshot capture
- frontmost app detection
- app-under-point lookup
- window-to-display resolution
- clipboard read/write
- MCP `type` tool GUI typing smoke tests
- MCP server startup

## What Was Fixed In 0.2.2

During real-device testing, we hit a macOS-specific bug: under a Chinese IME/input source, ordinary ASCII text could be corrupted when the tool typed one key at a time.

Version `0.2.2` fixes that by preferring clipboard-routed typing on macOS for normal multi-character text when clipboard write is available. That keeps the standalone skill usable even when the current input source is not plain U.S. keyboard mode.

## Architecture

```mermaid
flowchart LR
    A[Codex / MCP Client] --> B[macos-computer-use-skill]
    B --> C[Extracted TypeScript MCP tools]
    B --> D[Standalone Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[pyobjc Cocoa + Quartz]
    E --> H[Mouse / Keyboard]
    F --> I[Screenshots]
    G --> J[Apps / Displays / Clipboard / Windows]
```

## Install

### 1. Clone and install Node deps

```bash
git clone https://github.com/wimi321/macos-computer-use-skill.git
cd macos-computer-use-skill
npm install
npm run build
```

### 2. Start the server

```bash
node dist/cli.js
```

On first launch, the project will automatically:

- create `.runtime/venv`
- bootstrap `pip` if needed
- install the Python runtime dependencies from `runtime/requirements.txt`

No Claude desktop app. No private native modules. No local extraction path required.

## MCP Configuration

Example config:

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/macos-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

See [`examples/mcp-config.json`](./examples/mcp-config.json).

## Skill Install

This repo ships a top-level skill at [`skill/computer-use-macos`](./skill/computer-use-macos).

You can install it either from ClawHub or from this repository.

### Option A: Install from ClawHub

```bash
clawhub install computer-use-macos
```

### Option B: Install from the repo

Install it with:

```bash
bash skill/computer-use-macos/scripts/install.sh
```

The installer copies:

- the skill metadata
- the bundled standalone project
- the runtime bootstrap files

After installation, the default project path becomes:

```bash
~/.codex/skills/computer-use-macos/project
```

That means the installed skill can work even if the original clone disappears.

## Runtime Notes

### Permissions

macOS still requires:

- Accessibility
- Screen Recording

The standalone host checks both and reports them through the MCP flow.

### Screenshot Filtering

This standalone runtime reports `screenshotFiltering: none`.

That means:

- screenshots are not compositor-filtered
- the original allowlist / permission / tier logic still protects actions at the MCP layer

### Platform Scope

This project is intentionally focused on `macOS` desktop computer use. The current runtime is not a Windows or Linux backend.

Covered capabilities:

- screenshots
- mouse control
- keyboard input
- frontmost app inspection
- installed/running app discovery
- window-to-display mapping
- clipboard access
- app launch

## Validation Matrix

Real tests completed on this Mac:

- `npm run check`
- `npm run build`
- Python helper compile check for `runtime/mac_helper.py`
- permission probe: Accessibility + Screen Recording both granted
- display discovery on the active display
- real screenshot capture from the desktop
- running / installed app enumeration
- frontmost-app detection
- bundled skill install into a clean `CODEX_HOME`
- bundled project `npm install && npm run build`
- real GUI typing round-trip through the MCP `type` tool into TextEdit with exact clipboard verification

## Example Commands

```bash
npm run build
node dist/cli.js
```

```bash
node --input-type=module -e "import { callPythonHelper } from './dist/computer-use/pythonBridge.js'; console.log(await callPythonHelper('list_displays', {}));"
```

## Repository Layout

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  mac_helper.py
  requirements.txt
skill/
  computer-use-macos/
examples/
assets/
```

## Environment Flags

Optional knobs:

- `CLAUDE_COMPUTER_USE_DEBUG=1`
- `CLAUDE_COMPUTER_USE_COORDINATE_MODE=pixels`
- `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE=1`
- `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION=1`
- `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION=0`

## Roadmap

- richer app-icon extraction without private APIs
- stronger app filtering for nested helper bundles
- broader automated MCP integration tests
- optional packaged release artifacts for easier distribution

## License

MIT

## Credits

This project preserves and adapts reusable TypeScript computer-use logic recovered from the Claude Code workflow, then replaces the missing private runtime with a fully standalone public macOS implementation.


path: project/platforms/macos/README.zh-CN.md
content: <div align="center">
  <img src="./assets/hero.svg" alt="macos-computer-use-skill hero" width="100%" />
  <h1>macOS Computer-Use Skill</h1>
  <p><strong>一个面向 macOS 的顶级可移植 skill，内置独立 runtime 与 MCP server。</strong></p>
  <p>
    <a href="https://github.com/wimi321/macos-computer-use-skill">GitHub</a>
    ·
    <a href="https://clawhub.ai/wimi321/computer-use-macos">ClawHub</a>
    ·
    <a href="./README.md">English</a>
    ·
    <a href="./README.ja.md">日本語</a>
  </p>
</div>

## ClawHub 安装

这个 skill 已发布到 ClawHub，slug 是 [`computer-use-macos`](https://clawhub.ai/wimi321/computer-use-macos)。

```bash
clawhub install computer-use-macos
```

如果你还想拿到完整源码仓库，下面继续看 GitHub 方式。

## 项目定位

这个仓库本质上同时是：

- 一个顶级 `skill`
- 一套独立的 macOS runtime
- 一个给 agent 生态使用的 computer-use MCP server

它不只是给 Codex 用，skill 这种交付方式本身就是为了跨 agent 生态可移植。

## 这个项目解决什么问题

目标只有一个：

- 不依赖本机 Claude
- 不依赖私有 `.node` 二进制
- 不依赖“先从某个安装目录里捞内部资产”
- skill 装上之后就能把 computer-use 跑起来

这版仓库已经按这个目标重构完成。

## 你现在拿到的能力

- 顶级 macOS computer-use skill
- 独立 MCP server：截图、鼠标、键盘、应用启动、窗口/显示器信息、剪贴板
- 只使用公开依赖：`Node.js + Python + pyautogui + mss + Pillow + pyobjc`
- 首次运行自动自举：自动创建 `.runtime/venv` 并安装 Python 依赖
- 安装 skill 时会把完整项目一起复制到 `~/.codex/skills/computer-use-macos/project`
- 保留并复用提取出来的 TypeScript computer-use 工具链，但底层执行器已换成真正独立的 runtime

## 已完成的本地验证

已经在一台真实 macOS 机器上实际验证：

- runtime 自举成功
- 权限检测成功
- 显示器枚举成功
- 截图成功
- 前台应用识别成功
- 鼠标位置对应应用识别成功
- 窗口到显示器的归属解析成功
- 剪贴板读写成功
- 走 MCP `type` 工具链路的 GUI 输入 smoke test 成功
- MCP server 成功启动

## 0.2.2 修复了什么

这次真机测试里抓到了一个关键问题：在中文输入法环境下，普通英文文本如果按“逐键输入”执行，内容会被输入法状态污染，连纯 ASCII 都可能打错。

`0.2.2` 已修复这一点：在 macOS 上，只要具备剪贴板写权限，普通多字符文本会优先走剪贴板粘贴路径，而不是一键一键敲。这样 skill 安装后在真实中文工作环境里会稳定得多。

## 架构

```mermaid
flowchart LR
    A[Codex / MCP Client] --> B[macos-computer-use-skill]
    B --> C[提取出的 TypeScript MCP 工具层]
    B --> D[独立 Python bridge]
    D --> E[pyautogui]
    D --> F[mss + Pillow]
    D --> G[pyobjc Cocoa + Quartz]
    E --> H[鼠标 / 键盘]
    F --> I[截图]
    G --> J[应用 / 显示器 / 剪贴板 / 窗口]
```

## 安装

### 1. 克隆并安装 Node 依赖

```bash
git clone https://github.com/wimi321/macos-computer-use-skill.git
cd macos-computer-use-skill
npm install
npm run build
```

### 2. 启动 server

```bash
node dist/cli.js
```

首次启动时项目会自动：

- 创建 `.runtime/venv`
- 必要时自动补 `pip`
- 根据 `runtime/requirements.txt` 安装 Python 运行时依赖

不需要 Claude Desktop。也不需要任何私有 native 模块。

## MCP 配置

示例：

```json
{
  "mcpServers": {
    "computer-use": {
      "command": "node",
      "args": [
        "/absolute/path/to/macos-computer-use-skill/dist/cli.js"
      ],
      "env": {
        "CLAUDE_COMPUTER_USE_DEBUG": "0",
        "CLAUDE_COMPUTER_USE_COORDINATE_MODE": "pixels"
      }
    }
  }
}
```

参考 [`examples/mcp-config.json`](./examples/mcp-config.json)。

## Skill 安装

仓库自带顶级 skill：[`skill/computer-use-macos`](./skill/computer-use-macos)

你现在可以通过 ClawHub 直接安装，也可以从这个仓库本地安装。

### 方式 A：从 ClawHub 安装

```bash
clawhub install computer-use-macos
```

### 方式 B：从仓库安装

安装：

```bash
bash skill/computer-use-macos/scripts/install.sh
```

安装脚本会一起复制：

- skill 元数据
- 独立项目本体
- runtime 自举文件

安装后默认项目路径为：

```bash
~/.codex/skills/computer-use-macos/project
```

也就是说，即使原始 clone 被删掉，已安装的 skill 仍然有自己的可运行项目副本。

## 运行说明

### 权限

macOS 仍然需要：

- Accessibility
- Screen Recording

这个 standalone host 会在 MCP 流程中检测并反馈这两项状态。

### 截图过滤

当前 standalone runtime 声明的是 `screenshotFiltering: none`。

含义是：

- 截图本身不是 compositor 级过滤
- 但操作权限、allowlist、tier 限制仍由 MCP 层逻辑继续执行

### 平台范围

当前项目明确只支持 `macOS` 桌面 computer use，这一版还不是 Windows 或 Linux backend。

当前覆盖：

- 截图
- 鼠标控制
- 键盘输入
- 前台应用识别
- 已安装 / 运行中应用枚举
- 窗口到显示器映射
- 剪贴板访问
- 应用启动

## 验证矩阵

这次在本机完成的真实测试包括：

- `npm run check`
- `npm run build`
- `runtime/mac_helper.py` 的 Python 编译检查
- 权限探测：Accessibility 与 Screen Recording 均已授权
- 当前显示器枚举
- 真实桌面截图采集
- 运行中 / 已安装应用枚举
- 前台应用识别
- 将 skill 安装到全新 `CODEX_HOME`
- 在已安装 skill 的 `project/` 内执行 `npm install && npm run build`
- 通过 TextEdit 对 MCP `type` 工具做真实 GUI 输入回读校验，结果精确匹配

## 常用命令

```bash
npm run build
node dist/cli.js
```

```bash
node --input-type=module -e "import { callPythonHelper } from './dist/computer-use/pythonBridge.js'; console.log(await callPythonHelper('list_displays', {}));"
```

## 仓库结构

```text
src/
  computer-use/
    executor.ts
    hostAdapter.ts
    pythonBridge.ts
  vendor/computer-use-mcp/
runtime/
  mac_helper.py
  requirements.txt
skill/
  computer-use-macos/
examples/
assets/
```

## 可选环境变量

- `CLAUDE_COMPUTER_USE_DEBUG=1`
- `CLAUDE_COMPUTER_USE_COORDINATE_MODE=pixels`
- `CLAUDE_COMPUTER_USE_CLIPBOARD_PASTE=1`
- `CLAUDE_COMPUTER_USE_MOUSE_ANIMATION=1`
- `CLAUDE_COMPUTER_USE_HIDE_BEFORE_ACTION=0`

## 路线图

- 更好的 app icon 提取
- 更稳的嵌套 helper app 过滤
- 更完整的 MCP 集成测试
- 提供更易分发的打包产物

## License

MIT

## Credits

这个项目保留了从 Claude Code computer-use 工作流中提炼出来的可复用 TypeScript 逻辑，并用一套完全独立、公开可安装的 macOS runtime 替换了缺失的私有执行层。


path: project/platforms/macos/assets/hero.svg
content: <svg width="1280" height="640" viewBox="0 0 1280 640" fill="none" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <linearGradient id="bg" x1="54" y1="42" x2="1190" y2="582" gradientUnits="userSpaceOnUse">
      <stop stop-color="#0B1220"/>
      <stop offset="0.5" stop-color="#111C32"/>
      <stop offset="1" stop-color="#1D3557"/>
    </linearGradient>
    <linearGradient id="accent" x1="218" y1="116" x2="1112" y2="476" gradientUnits="userSpaceOnUse">
      <stop stop-color="#7EE787"/>
      <stop offset="0.45" stop-color="#5BC0EB"/>
      <stop offset="1" stop-color="#FFB703"/>
    </linearGradient>
    <linearGradient id="panel" x1="802" y1="157" x2="1107" y2="434" gradientUnits="userSpaceOnUse">
      <stop stop-color="#111927" stop-opacity="0.96"/>
      <stop offset="1" stop-color="#0B1220" stop-opacity="0.8"/>
    </linearGradient>
    <filter id="blur" x="0" y="0" width="1280" height="640" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
      <feGaussianBlur stdDeviation="60"/>
    </filter>
  </defs>

  <rect width="1280" height="640" rx="28" fill="url(#bg)"/>
  <g filter="url(#blur)" opacity="0.34">
    <circle cx="225" cy="147" r="132" fill="#7EE787"/>
    <circle cx="1022" cy="158" r="110" fill="#5BC0EB"/>
    <circle cx="952" cy="514" r="164" fill="#FFB703"/>
  </g>

  <rect x="62" y="58" width="1156" height="524" rx="26" fill="#0B1220" fill-opacity="0.25" stroke="rgba(255,255,255,0.12)"/>

  <g opacity="0.95">
    <rect x="94" y="92" width="394" height="34" rx="17" fill="#0F1728" stroke="#25324A"/>
    <circle cx="117" cy="109" r="5" fill="#FF6B6B"/>
    <circle cx="136" cy="109" r="5" fill="#FFD166"/>
    <circle cx="155" cy="109" r="5" fill="#7EE787"/>
    <text x="182" y="115" fill="#B9C6DA" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="14">macos-computer-use-skill</text>
  </g>

  <g>
    <text x="98" y="204" fill="white" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="64" font-weight="700">Standalone</text>
    <text x="98" y="282" fill="url(#accent)" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="70" font-weight="800">Computer Use</text>
    <text x="98" y="352" fill="#DFF5FF" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="60" font-weight="760">Skill + MCP</text>
    <text x="98" y="404" fill="#D6E2F0" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="21">Recovered from Claude Code and rebuilt as a portable public macOS runtime.</text>
    <text x="98" y="438" fill="#8EA3BC" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="19">TS core, Python bridge, runtime bootstrap, and an installable top-level skill.</text>
  </g>

  <g>
    <rect x="96" y="482" width="160" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="132" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">stdio MCP</text>

    <rect x="272" y="482" width="204" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="304" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">macOS automation</text>

    <rect x="494" y="482" width="162" height="44" rx="22" fill="#13233D" stroke="#2C4E7A"/>
    <text x="526" y="510" fill="#DCEBFF" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">Codex skill</text>
  </g>

  <g>
    <rect x="770" y="134" width="370" height="336" rx="24" fill="url(#panel)" stroke="#314561"/>
    <text x="803" y="180" fill="#E8F0FB" font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-size="22" font-weight="700">System Shape</text>

    <rect x="803" y="208" width="304" height="58" rx="16" fill="#122035" stroke="#29496F"/>
    <text x="827" y="243" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">Codex / MCP client</text>

    <path d="M955 267V294" stroke="#5BC0EB" stroke-width="3" stroke-linecap="round"/>

    <rect x="803" y="296" width="304" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="827" y="331" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">macos-computer-use-skill</text>

    <path d="M955 355V382" stroke="#7EE787" stroke-width="3" stroke-linecap="round"/>

    <rect x="803" y="384" width="140" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="826" y="419" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">TS core</text>

    <rect x="968" y="384" width="139" height="58" rx="16" fill="#132840" stroke="#37618F"/>
    <text x="991" y="419" fill="#DBE8F9" font-family="SFMono-Regular, Menlo, Monaco, Consolas, monospace" font-size="18">py runtime</text>
  </g>
</svg>


path: project/platforms/macos/dist/cli.d.ts
content: #!/usr/bin/env node
export {};


path: project/platforms/macos/dist/cli.js
content: #!/usr/bin/env node
import { runServer } from './server.js';
import { errorMessage } from './lib/errors.js';
try {
    await runServer();
}
catch (error) {
    process.stderr.write(`macos-computer-use-skill failed: ${errorMessage(error)}\n`);
    process.exit(1);
}
//# sourceMappingURL=cli.js.map

path: project/platforms/macos/dist/computer-use/appNames.d.ts
content: type InstalledAppLike = {
    readonly bundleId: string;
    readonly displayName: string;
    readonly path: string;
};
export declare function filterAppsForDescription(installed: readonly InstalledAppLike[], homeDir: string | undefined): string[];
export {};


path: project/platforms/macos/dist/computer-use/common.d.ts
content: export declare const COMPUTER_USE_MCP_SERVER_NAME = "computer-use";
export declare const CLI_HOST_BUNDLE_ID = "io.github.wimi321.macos-computer-use-skill";
export declare const CLI_CU_CAPABILITIES: {
    screenshotFiltering: "none";
    platform: "darwin";
};


path: project/platforms/macos/dist/computer-use/common.js
content: export const COMPUTER_USE_MCP_SERVER_NAME = 'computer-use';
export const CLI_HOST_BUNDLE_ID = 'io.github.wimi321.macos-computer-use-skill';
export const CLI_CU_CAPABILITIES = {
    screenshotFiltering: 'none',
    platform: 'darwin',
};
//# sourceMappingURL=common.js.map

path: project/platforms/macos/dist/computer-use/computerUseLock.d.ts
content: export type AcquireResult = {
    kind: 'acquired';
    fresh: boolean;
} | {
    kind: 'blocked';
    by: string;
};
export type CheckResult = {
    kind: 'free';
} | {
    kind: 'held_by_self';
} | {
    kind: 'blocked';
    by: string;
};
export declare function checkComputerUseLock(): Promise<CheckResult>;
export declare function tryAcquireComputerUseLock(): Promise<AcquireResult>;
export declare function releaseComputerUseLock(): Promise<boolean>;
export declare function isLockHeldLocally(): boolean;


path: project/platforms/macos/dist/computer-use/drainRunLoop.d.ts
content: declare function retain(): void;
declare function release(): void;
export declare const retainPump: typeof retain;
export declare const releasePump: typeof release;
export declare function drainRunLoop<T>(fn: () => Promise<T>): Promise<T>;
export {};


path: project/platforms/macos/dist/computer-use/drainRunLoop.js
content: import { logDebug } from '../lib/log.js';
import { withResolvers } from '../lib/withResolvers.js';
import { requireComputerUseSwift } from './swiftLoader.js';
let pump;
let pending = 0;
const TIMEOUT_MS = 30_000;
function retain() {
    pending += 1;
    if (!pump) {
        pump = setInterval(() => requireComputerUseSwift()._drainMainRunLoop(), 1);
        logDebug('drain run loop started');
    }
}
function release() {
    pending -= 1;
    if (pending <= 0 && pump) {
        clearInterval(pump);
        pump = undefined;
        pending = 0;
        logDebug('drain run loop stopped');
    }
}
export const retainPump = retain;
export const releasePump = release;
export async function drainRunLoop(fn) {
    retain();
    let timer;
    try {
        const work = fn();
        work.catch(() => { });
        const timeout = withResolvers();
        timer = setTimeout(() => timeout.reject(new Error(`computer-use native call exceeded ${TIMEOUT_MS}ms`)), TIMEOUT_MS);
        return await Promise.race([work, timeout.promise]);
    }
    finally {
        if (timer)
            clearTimeout(timer);
        release();
    }
}
//# sourceMappingURL=drainRunLoop.js.map

path: project/platforms/macos/dist/computer-use/escHotkey.d.ts
content: export declare function registerEscHotkey(onEscape: () => void): boolean;
export declare function unregisterEscHotkey(): void;
export declare function notifyExpectedEscape(): void;


path: project/platforms/macos/dist/computer-use/hostAdapter.d.ts
content: import type { ComputerUseHostAdapter } from '../vendor/computer-use-mcp/types.js';
export declare function getComputerUseHostAdapter(): ComputerUseHostAdapter;


path: project/platforms/macos/dist/computer-use/inputLoader.d.ts
content: export declare function requireComputerUseInput(): any;


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)