From 5cf7f60d04dd1c2c9cdd5573bd7ab1cfb0a51716 Mon Sep 17 00:00:00 2001
From: yyf <2358929807@qq.com>
Date: Fri, 21 Aug 2026 12:57:32 +0800
Subject: [PATCH 1/8] feat: add Windows support for Codex Micro
---
README.md | 44 ++-
README_zh.md | 40 +-
docs/architecture.md | 8 +-
docs/configuration.md | 8 +-
docs/setup-and-operations.md | 165 ++++-----
.../de_DE.json | 14 +-
.../en.json | 18 +-
.../es_ES.json | 14 +-
.../ja_JP.json | 14 +-
.../ko_KR.json | 14 +-
.../manifest.json | 8 +-
.../package-lock.json | 4 +-
.../package.json | 2 +-
.../plugin/app.js | 4 +
.../plugin/bridge-installer.js | 342 ++++++++++--------
.../property-inspector/setup.html | 192 ++++------
.../pt_PT.json | 14 +-
.../test/smoke.mjs | 56 +--
.../zh_CN.json | 18 +-
.../zh_HK.json | 14 +-
package-lock.json | 4 +-
package.json | 6 +-
scripts/install-plugin.mjs | 17 +-
scripts/install.mjs | 202 +----------
scripts/launch.mjs | 13 +
scripts/uninstall.mjs | 49 +--
skills/install-ulanzi-studio-plugin/SKILL.md | 19 +-
skills/setup-codex-bridge/SKILL.md | 23 +-
skills/setup-codex-bridge/agents/openai.yaml | 4 +-
src/bridge/auth.mjs | 9 +
src/bridge/codex-cdp.mjs | 24 +-
src/bridge/platform.mjs | 197 ++++++++++
src/bridge/server.mjs | 26 +-
test/bridge-installer_test.mjs | 87 +++++
test/bridge_test.mjs | 46 +++
35 files changed, 896 insertions(+), 823 deletions(-)
create mode 100644 scripts/launch.mjs
create mode 100644 src/bridge/auth.mjs
create mode 100644 src/bridge/platform.mjs
diff --git a/README.md b/README.md
index d8ba799..d06aa3d 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,8 @@ Steer, Mic, Submit, and Latest Task & Scroll Encoder actions.
| Local-only transport | Bind CDP and the Bridge API to loopback addresses only |
This Ulanzi-maintained project is an unofficial integration with Codex Desktop
-for **macOS, Ulanzi Studio, and Ulanzi D200 Series**. It is not affiliated with or
-endorsed by OpenAI. The entire project was vibe-coded with Codex.
+for **Windows or macOS, Ulanzi Studio, and Ulanzi D200 Series**. It is not
+affiliated with or endorsed by OpenAI.
Ulanzi's implementation and maintenance scope is limited to
`integration/com.ulanzi.codexmicro.ulanziPlugin/`. The plugin only consumes
@@ -38,8 +38,8 @@ responsibility boundary.
### Installation environment
-- macOS 13 or later;
-- Codex Desktop;
+- Windows 10 or later, or macOS 13 or later;
+- Codex Desktop Stable or Beta;
- Ulanzi Studio 3.0.1 or later;
- Ulanzi D200 Series connected to Ulanzi Studio;
- Node.js 20 or newer only for repository-based manual installation.
@@ -49,7 +49,8 @@ responsibility boundary.
An LLM or coding agent must follow these steps:
1. Read [AGENTS.md](AGENTS.md) and inspect the installed Ulanzi Studio plugin
- manifests under `~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/`.
+ manifests under `%APPDATA%\Ulanzi\UlanziDeck\Plugins` on Windows or
+ `~/Library/Application Support/Ulanzi/UlanziDeck/Plugins` on macOS.
2. Treat Codex Micro as installed only when manifest UUID
`com.ulanzi.ulanzistudio.codexmicro` is present and its `CodePath` exists.
3. If it is missing or invalid, load
@@ -57,7 +58,7 @@ An LLM or coding agent must follow these steps:
install the plugin, and verify its manifest and entry point.
4. After the plugin is verified, load
[`$setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md), install or repair
- `Codex Bridge.app` and its sidecar, and verify the local health endpoint.
+ Codex Bridge and its sidecar, and verify the local health endpoint.
5. Report plugin installation and Bridge connectivity separately. Do not
reinstall a valid plugin unless an update or repair was requested.
@@ -66,13 +67,14 @@ An LLM or coding agent must follow these steps:
After installing the Codex Micro plugin, drag any Codex Micro action onto a
key and select it. Its shared **Codex Bridge Setup** page can:
-- show whether `Codex Bridge.app`, the Bridge service, and CDP are available;
+- show whether Codex Bridge, its user-level service, and CDP are available;
- install or repair the bundled Bridge without a repository or npm directory;
-- launch `~/Applications/Codex Bridge.app`; and
+- launch Codex with loopback-only CDP arguments; and
- recheck the connection or open the full installation guide.
-The installer writes only to the current user's Applications, Application
-Support, and LaunchAgents directories and does not require `sudo`.
+The installer uses `%LOCALAPPDATA%\OpenCodexMicro` on Windows. On macOS it uses
+the current user's Applications, Application Support, and LaunchAgents
+directories. Neither path requires administrator access.
### 3. Manual installation
@@ -87,11 +89,12 @@ npm run setup
Quit Ulanzi Studio before running `npm run install:plugin`. The command
validates and atomically installs the prebuilt plugin. `npm run setup` builds
the loopback Bridge sidecar, registers its user LaunchAgent, and installs
-`Codex Bridge.app` in `~/Applications`.
+the platform-specific user-level Bridge runtime.
-Reopen Ulanzi Studio. Quit Codex Desktop, then open
-`~/Applications/Codex Bridge.app`; the wrapper starts Codex with a
-loopback-only CDP endpoint. Confirm the connection with:
+Reopen Ulanzi Studio and use **Launch Codex Bridge** in any Action setup page,
+or run `npm run bridge:start`. On Windows the launcher discovers the current
+Stable or Beta Appx package dynamically; it never pins a versioned WindowsApps
+directory. Confirm the connection with:
```bash
curl http://127.0.0.1:17373/health
@@ -102,14 +105,9 @@ In Ulanzi Studio, drag the Codex Micro actions onto the desired keys. See
[Setup and operations](docs/setup-and-operations.md) for installed paths,
diagnostics, updates, and uninstall instructions.
-> **Important:** When using Codex Micro, always launch Codex through
-> `~/Applications/Codex Bridge.app`. Do not open Codex Desktop directly.
->
-> Launch command:
->
-> ```bash
-> open ~/Applications/Codex\ Bridge.app
-> ```
+> **Important:** Codex must be started with the loopback CDP arguments shown in
+> the setup page. If `9222` is already available, Launch only focuses the current
+> Codex instance; otherwise it starts the configured Stable or Beta channel.
## Configure
@@ -132,7 +130,7 @@ The repository includes two reusable Codex skills:
| Skill | Purpose |
| --- | --- |
-| [`setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md) | Install, update, verify, or repair `Codex Bridge.app` and its sidecar |
+| [`setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md) | Install, update, verify, or repair Codex Bridge and its sidecar |
| [`install-ulanzi-studio-plugin`](skills/install-ulanzi-studio-plugin/SKILL.md) | Install the repository's prebuilt plugin directory into Ulanzi Studio |
## Documentation
diff --git a/README_zh.md b/README_zh.md
index 4543475..0e68de5 100644
--- a/README_zh.md
+++ b/README_zh.md
@@ -34,8 +34,8 @@ Ulanzi 的实现与维护范围仅限
### 安装环境
-- macOS 13 或更高版本;
-- Codex Desktop;
+- Windows 10 或更高版本,或 macOS 13 或更高版本;
+- Codex Desktop Stable 或 Beta;
- Ulanzi Studio 3.0.1 或更高版本;
- 已在 Ulanzi Studio 中连接 Ulanzi D200 Series;
- 仅使用仓库手动安装时需要 Node.js 20 或更高版本。
@@ -44,9 +44,9 @@ Ulanzi 的实现与维护范围仅限
LLM 或代码 Agent 必须按以下步骤执行:
-1. 阅读 [AGENTS.md](AGENTS.md),检查
- `~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/` 下已经安装的插件
- manifest。
+1. 阅读 [AGENTS.md](AGENTS.md),检查 Windows 的
+ `%APPDATA%\Ulanzi\UlanziDeck\Plugins` 或 macOS 的
+ `~/Library/Application Support/Ulanzi/UlanziDeck/Plugins` 下已经安装的插件 manifest。
2. 仅当 manifest UUID 为 `com.ulanzi.ulanzistudio.codexmicro`,且其 `CodePath`
确实存在时,才判断 Codex Micro 已安装。
3. 如果插件缺失或无效,加载
@@ -54,7 +54,7 @@ LLM 或代码 Agent 必须按以下步骤执行:
安装插件并验证 manifest 和入口文件。
4. 插件验证通过后,加载
[`$setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md),安装或修复
- `Codex Bridge.app` 与 sidecar,并验证本机 health 接口。
+ Codex Bridge 与 sidecar,并验证本机 health 接口。
5. 分别报告插件安装状态和 Bridge 连接状态。除非用户要求更新或修复,否则不要重复
安装有效插件。
@@ -63,13 +63,13 @@ LLM 或代码 Agent 必须按以下步骤执行:
安装 Codex Micro 插件后,把任意 Codex Micro Action 拖到按键并选中。所有 Action
共用的 **Codex Bridge 设置**页可以:
-- 显示 `Codex Bridge.app`、Bridge 后台服务和 CDP 的状态;
+- 显示 Codex Bridge、用户级后台进程和 CDP 的状态;
- 使用插件内置资源安装或修复 Bridge,不需要仓库目录或 npm 项目目录;
-- 启动 `~/Applications/Codex Bridge.app`;
+- 使用仅限回环地址的 CDP 参数启动 Codex;
- 重新检测连接或打开完整安装说明。
-安装器只写入当前用户的 Applications、Application Support 和 LaunchAgents 目录,
-不需要 `sudo`。
+Windows 安装器写入 `%LOCALAPPDATA%\OpenCodexMicro`;macOS 安装器写入当前用户的
+Applications、Application Support 和 LaunchAgents 目录。两者都不需要管理员权限。
### 3. 手动安装
@@ -82,11 +82,11 @@ npm run setup
```
运行 `npm run install:plugin` 前请退出 Ulanzi Studio。该命令会校验并原子安装仓库
-中的预构建插件;`npm run setup` 会构建本机 Bridge sidecar、注册用户级
-LaunchAgent,并把 `Codex Bridge.app` 安装到 `~/Applications`。
+中的预构建插件;`npm run setup` 会构建并安装当前平台的用户级 Bridge sidecar。
-重新打开 Ulanzi Studio。完全退出 Codex Desktop,再打开
-`~/Applications/Codex Bridge.app`。可用以下命令确认连接:
+重新打开 Ulanzi Studio,在任意 Action 设置页点击“启动 Codex Bridge”,或运行
+`npm run bridge:start`。Windows 会从 Appx 包元数据动态发现 Stable 或 Beta,
+不会固定带版本号的 WindowsApps 目录。可用以下命令确认连接:
```bash
curl http://127.0.0.1:17373/health
@@ -96,14 +96,8 @@ curl http://127.0.0.1:17373/state
最后在 Ulanzi Studio 中把 Codex Micro actions 拖到需要的按键。安装路径、诊断、
更新与卸载方法见 [安装与运行](docs/setup-and-operations.md)。
-> **注意事项:** 使用 Codex Micro 时,必须通过
-> `~/Applications/Codex Bridge.app` 启动 Codex,请勿直接打开 Codex Desktop。
->
-> 启动命令:
->
-> ```bash
-> open ~/Applications/Codex\ Bridge.app
-> ```
+> **注意事项:** Codex 必须带设置页所示的本机回环 CDP 参数启动。如果 `9222`
+> 已在线,“启动”只会聚焦当前 Codex;否则会启动所选 Stable 或 Beta 通道。
## 配置
@@ -123,7 +117,7 @@ Action 行为和布局建议见[配置详解](docs/configuration.md)。
| Skill | 用途 |
| --- | --- |
-| [`setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md) | 安装、更新、验证或修复 `Codex Bridge.app` 与 sidecar |
+| [`setup-codex-bridge`](skills/setup-codex-bridge/SKILL.md) | 安装、更新、验证或修复 Codex Bridge 与 sidecar |
| [`install-ulanzi-studio-plugin`](skills/install-ulanzi-studio-plugin/SKILL.md) | 把仓库中的预构建插件目录安装到 Ulanzi Studio |
## 文档
diff --git a/docs/architecture.md b/docs/architecture.md
index fe3e9f7..c5a5425 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -19,8 +19,10 @@ Codex renderer Micro store
## Codex Bridge
-`Codex Bridge.app` launches the real Codex executable with CDP restricted to
-`127.0.0.1:9222`. The sidecar keeps a persistent renderer connection and
+Codex Bridge launches the real Codex executable with CDP restricted to
+`127.0.0.1:9222`. Windows discovers Stable or Beta through Appx metadata and the
+Ulanzi plugin supervises the user-level sidecar; macOS retains the wrapper app
+and user LaunchAgent. The sidecar keeps a persistent renderer connection and
refreshes an in-memory snapshot every 500 ms. `/state` reads that cache rather
than triggering a fresh renderer scan for every plugin poll.
@@ -52,6 +54,6 @@ development, or maintenance of CDP or Codex's CDP implementation.
## Installation boundary
- `scripts/install.mjs` installs only the Bridge sidecar and
- `~/Applications/Codex Bridge.app`.
+ its platform-specific user-level launcher/lifecycle files.
- `scripts/install-plugin.mjs` installs only the Ulanzi Studio plugin directory.
- `scripts/uninstall.mjs` removes those two installed components.
diff --git a/docs/configuration.md b/docs/configuration.md
index 62ef190..886b45c 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -37,10 +37,12 @@ placed on more than one key.
## Runtime requirements
-- Start Codex through `~/Applications/Codex Bridge.app`.
+- Start Codex from the Bridge setup page or with `npm run bridge:start`; Windows
+ resolves the current Stable or Beta Appx package dynamically.
- Keep the Bridge sidecar running at `127.0.0.1:17373`.
-- Allow Ulanzi Studio under macOS System Settings > Privacy & Security >
- Accessibility so the Encoder can emit mouse-wheel events.
+- On macOS, allow Ulanzi Studio under System Settings > Privacy & Security >
+ Accessibility so the Encoder can emit mouse-wheel events. On Windows, confirm
+ Ulanzi Studio is allowed to send the configured hotkey events.
- Restart Ulanzi Studio after installing a new plugin build.
The plugin does not require a separate device service, shortcut file, or theme
diff --git a/docs/setup-and-operations.md b/docs/setup-and-operations.md
index f21d2b6..fa65b80 100644
--- a/docs/setup-and-operations.md
+++ b/docs/setup-and-operations.md
@@ -8,72 +8,63 @@ Codex Desktop <-> Codex Bridge sidecar <-> Ulanzi Studio plugin <-> D200
## Dependencies
-- macOS;
-- Codex Desktop;
-- Ulanzi Studio with a supported keypad device;
-- Node.js 20 or newer only for repository-based manual installation.
+- Windows 10+ or macOS 13+;
+- Codex Desktop Stable or Beta;
+- Ulanzi Studio 3.0.1+ with a supported D200 Series device;
+- Node.js 20+ only for repository-based installation.
-## 1. Set up Codex Bridge.app in Ulanzi Studio
+## Install from Ulanzi Studio
-Drag any Codex Micro action onto a key and select it. In the shared
-**Codex Bridge Setup** page, choose **Install / Repair**. The plugin installs
-its bundled Bridge runtime, wrapper app, and user LaunchAgent without requiring
-a repository path, npm, or administrator access. Then choose
-**Launch Codex Bridge** and wait for the CDP status to become connected.
+Drag any Codex Micro Action onto a key and select it. In the shared **Codex
+Bridge Setup** page, choose **Install / Repair**, then **Launch Codex Bridge**.
+The bundled installer requires no repository path, npm directory, administrator
+service, or fixed Codex package version.
-## 2. Set up Codex Bridge.app from the repository
+Windows installs Bridge data under:
-From the repository root:
-
-```bash
-npm install
-npm run setup
+```text
+%LOCALAPPDATA%\OpenCodexMicro
```
-The installer builds the sidecar, writes and starts
-`io.opencodexmicro.bridge`, and ad-hoc signs:
+The Ulanzi plugin starts and supervises the user-level Bridge process. The
+launcher reads Appx metadata for `OpenAI.Codex` and `OpenAI.CodexBeta`, preferring
+Stable unless `CODEX_DESKTOP_CHANNEL=beta` is configured.
+
+macOS keeps the wrapper and LaunchAgent flow:
```text
~/Applications/Codex Bridge.app
+~/Library/Application Support/OpenCodexMicro
+~/Library/LaunchAgents/io.opencodexmicro.bridge.plist
```
-To install without starting the sidecar LaunchAgent:
-
-```bash
-npm run setup -- --no-start
-```
-
-## 3. Install the Ulanzi Studio plugin
+## Install from the repository
-Quit Ulanzi Studio before replacing a plugin that is currently loaded, then
-run:
+From `plugins/codex-console`:
-```bash
+```text
+npm install
npm run install:plugin
+npm run setup
```
-The repository ships the prebuilt `dist/app.js`. The command validates that
-the manifest entry point exists and atomically replaces:
+Quit Ulanzi Studio before replacing a loaded plugin. `install:plugin` validates
+the manifest, entry point, Property Inspector, icons, banners, locales, and
+bundled Bridge resources before atomically replacing the installed directory.
+
+Plugin destinations are:
```text
-~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/com.ulanzi.codexmicro.ulanziPlugin
+Windows: %APPDATA%\Ulanzi\UlanziDeck\Plugins\com.ulanzi.codexmicro.ulanziPlugin
+macOS: ~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/com.ulanzi.codexmicro.ulanziPlugin
```
-Restart Ulanzi Studio and confirm the **Codex Micro** category and actions are
-visible.
-
-Plugin development still uses `npm run build:plugin` and `npm run check`; commit
-the updated `dist/app.js` and `dist/package.json` with source changes.
+Use `npm run setup -- --no-start` to install Bridge without launching Codex, or
+`npm run bridge:start` to launch an existing installation.
-## Starting Codex
-
-Quit a normally launched Codex instance, then open:
-
-```text
-~/Applications/Codex Bridge.app
-```
+## Codex and CDP lifecycle
-The wrapper launches `/Applications/ChatGPT.app/Contents/MacOS/ChatGPT` with:
+Codex must be launched with:
```text
--remote-debugging-address=127.0.0.1
@@ -81,77 +72,63 @@ The wrapper launches `/Applications/ChatGPT.app/Contents/MacOS/ChatGPT` with:
--remote-allow-origins=http://127.0.0.1:9222
```
-CDP binds to `127.0.0.1:9222` and the sidecar API to `127.0.0.1:17373`.
-Neither endpoint is reachable from the LAN.
+Bridge probes `127.0.0.1:9222` first. If it is already available, Launch only
+focuses Codex. If it is unavailable on Windows, Launch dynamically resolves the
+selected Appx package, stops only that channel's main process, and starts its
+current executable with the arguments above. No versioned WindowsApps path is
+stored.
-## Installed files and service
+CDP binds to `127.0.0.1:9222` and Bridge to `127.0.0.1:17373`. Bridge `POST`
+actions require the random capability token stored in the user data directory;
+the plugin reads it locally and never sends it to the Property Inspector.
-```text
-~/Applications/Codex Bridge.app
-~/Library/Application Support/OpenCodexMicro/bridge.mjs
-~/Library/Application Support/OpenCodexMicro/bridge.log
-~/Library/Application Support/OpenCodexMicro/bridge-error.log
-~/Library/LaunchAgents/io.opencodexmicro.bridge.plist
-~/Library/Application Support/Ulanzi/UlanziDeck/Plugins/com.ulanzi.codexmicro.ulanziPlugin
-```
+## Diagnostics
-Inspect or restart the Bridge sidecar:
+Read-only health endpoints remain available on loopback:
-```bash
-launchctl print "gui/$(id -u)/io.opencodexmicro.bridge"
-launchctl kickstart -k "gui/$(id -u)/io.opencodexmicro.bridge"
+```text
+http://127.0.0.1:17373/health
+http://127.0.0.1:17373/state
+http://127.0.0.1:9222/json/version
+http://127.0.0.1:9222/json/list
```
-## Diagnostics
-
-```bash
-curl http://127.0.0.1:17373/health
-curl http://127.0.0.1:17373/state
-tail -f "$HOME/Library/Application Support/OpenCodexMicro/bridge.log"
-tail -f "$HOME/Library/Application Support/OpenCodexMicro/bridge-error.log"
-```
+Windows logs can be inspected from the Ulanzi Studio plugin log and the Bridge
+status page. macOS sidecar logs remain under
+`~/Library/Application Support/OpenCodexMicro`.
| Symptom | Check |
| --- | --- |
-| Plugin category is missing | Confirm the installed plugin directory contains `manifest.json` and `dist/app.js`, then restart Ulanzi Studio |
-| Plugin keys show offline | Start Codex with `Codex Bridge.app` and check `/health` and `/state` |
-| A task key does not switch | Check `bridge-error.log` and confirm `/state` contains the displayed thread |
-| Steer does nothing | Confirm a running task exposes the visible composer Steer action |
-| An action has no effect | Confirm the plugin can reach `127.0.0.1:17373` and inspect the Bridge log |
+| Plugin category is missing | Confirm the platform plugin directory contains `manifest.json` and `dist/app.js`, then restart Ulanzi Studio |
+| Bridge is not installed | Use **Install / Repair** and confirm the user data directory contains `bridge.mjs`, `bridge-token`, and `install.json` |
+| Bridge is offline | Use **Launch Codex Bridge**; on Windows confirm no endpoint security product blocked the user-level Node process |
+| CDP is disconnected | Confirm Codex was launched with the three loopback arguments and `/json/version` responds |
+| A task key does not switch | Confirm `/state` contains the displayed thread and the current Codex renderer still exposes Micro handlers |
+| Usage is blank | Treat `rate-limit-status: null` as unavailable account data, not as a Windows transport failure |
-## Update
+## Update and uninstall
-```bash
-git pull
+```text
npm install
npm run check
npm run setup
npm run install:plugin
```
-Restart Ulanzi Studio after updating the plugin.
-
-## Uninstall
+Restart Ulanzi Studio after updating the plugin. Remove Bridge and the installed
+plugin only when explicitly intended:
-```bash
+```text
npm run uninstall
```
-This stops and removes the Bridge LaunchAgent, Bridge runtime, wrapper app, and
-installed Codex Micro plugin directory.
+The Windows uninstaller stops only the Bridge process whose command line points
+to the installed `bridge.mjs`; it does not remove or terminate Codex Desktop.
## Development
-```bash
-npm install
-npm run check
-```
-
-Main entry points:
-
-- `src/bridge/`: Codex renderer bridge;
-- `scripts/build-bridge.mjs`: bundle the loopback sidecar;
-- `scripts/install.mjs`: install the sidecar and wrapper app;
-- `scripts/install-plugin.mjs`: validate and install the prebuilt Ulanzi plugin;
-- `integration/com.ulanzi.codexmicro.ulanziPlugin/`: Ulanzi Studio plugin;
-- `scripts/uninstall.mjs`: remove both installed components.
+The committed release entry points are
+`integration/com.ulanzi.codexmicro.ulanziPlugin/dist/app.js` and
+`integration/com.ulanzi.codexmicro.ulanziPlugin/installer/bridge.mjs`. Run the
+repository verification gate after platform, installer, manifest, localization,
+or build changes.
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/de_DE.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/de_DE.json
index fb0a678..fcdd217 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/de_DE.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/de_DE.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Codex-Desktop-Aufgaben und -Aktionen mit der Ulanzi D200 Series in Ulanzi Studio steuern.",
- "Description": "Verwandeln Sie die Ulanzi D200 Series über Ulanzi Studio in eine dedizierte Steueroberfläche für Codex Desktop.\n\nFUNKTIONEN\n- Zeigt fünf aktuelle Codex-Aufgaben mit Live-Status und öffnet exakt die ausgewählte Aufgabe.\n- Bietet Fast, Usage, Pin, New, Fork, Steer, Mic und Submit.\n- Mit Latest Task & Scroll öffnen Sie task1 und scrollen Codex über den Encoder.\n- Codex Bridge über die Einstellungsseite jeder Action installieren, reparieren, starten und prüfen.\n- Hält die Kommunikation über Codex Bridge.app und ausschließlich lokale Loopback-Endpunkte auf dem Mac.\n\nINSTALLATIONSUMGEBUNG\n- macOS 13 oder neuer\n- Codex Desktop\n- Ulanzi Studio 3.0.1 oder neuer\n- In Ulanzi Studio verbundene Ulanzi D200 Series\n- Node.js 20 oder neuer nur für die manuelle Einrichtung aus dem Repository\n\nLLM-/AGENT-INSTALLATION\nGeben Sie in Ihrem Agent ein:\nInstalliere bitte das Ulanzi-Studio-Plugin für mich: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\nMANUELLE INSTALLATION\n1. https://github.com/UlanziTechnology/OpenCodexMicro klonen und das Repository öffnen.\n2. Ulanzi Studio beenden.\n3. Ausführen: npm install\n4. Ausführen: npm run install:plugin\n5. Ausführen: npm run setup\n6. Ulanzi Studio erneut öffnen, Codex Desktop beenden und ~/Applications/Codex Bridge.app starten.\n7. Die Codex-Micro-Aktionen in Ulanzi Studio den gewünschten Tasten hinzufügen.\n8. Eine Action auswählen und Installation, Dienst sowie CDP auf der Codex-Bridge-Seite prüfen.\n\nWICHTIG\nStarten Sie Codex bei Verwendung von Codex Micro immer über ~/Applications/Codex Bridge.app. Öffnen Sie Codex Desktop nicht direkt.\nStartbefehl: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Steuert Codex Desktop mit der Ulanzi D200 Series in Ulanzi Studio. Unterstützt fünf aktuelle Aufgaben, Fast, Usage, Pin, New, Fork, Steer, Mic, Submit und Encoder-Navigation unter Windows 10 oder neuer sowie macOS 13 oder neuer. Unter Windows erkennt die Bridge Stable oder Beta dynamisch; Schreibaktionen sind durch ein lokales Capability-Token geschützt und CDP bleibt auf Loopback. Für die Repository-Einrichtung: npm install, npm run install:plugin und npm run setup.",
"Actions": [
{ "Name": "Codex-Aufgabe 1", "Tooltip": "Öffnet den letzten Codex-Aufgabenplatz 1 und zeigt seinen Live-Status." },
{ "Name": "Codex-Aufgabe 2", "Tooltip": "Öffnet den letzten Codex-Aufgabenplatz 2 und zeigt seinen Live-Status." },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge einrichten",
"BridgeSetupDescription": "Lokale Codex-Verbindung installieren, starten und prüfen.",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge-Dienst",
"CDPConnection": "CDP-Verbindung",
"InstallRepair": "Installieren / Reparieren",
@@ -30,14 +30,14 @@
"UninstallingBridge": "Wird deinstalliert…",
"UninstallBridgeDone": "Codex Bridge wurde deinstalliert. Das Ulanzi-Plugin und Codex Desktop bleiben erhalten.",
"UninstallBridgeConfirm": "Codex Bridge und den Hintergrunddienst deinstallieren? Das Ulanzi-Plugin und Codex Desktop werden nicht entfernt.",
- "BridgeAppInfoTitle": "Was ist Codex Bridge.app?",
- "BridgeAppDescription": "Codex Bridge.app ist ein lokaler Starter. Falls Codex bereits läuft, wird es regulär beendet und anschließend die offizielle Codex-Desktop-Datei mit ausschließlich lokalen CDP-Startparametern gestartet.",
- "BridgeAppSafety": "Der Starter verändert weder Codex Desktop noch Konten, Projekte oder Benutzerdateien und gibt CDP nicht außerhalb dieses Macs frei. Er führt keine weiteren privilegierten oder destruktiven Aktionen aus. Der separate Bridge-Dienst übernimmt lokalen Status und Action-Kommunikation.",
+ "BridgeAppInfoTitle": "Wie startet Codex Bridge Codex?",
+ "BridgeAppDescription": "Unter Windows erkennt Codex Bridge Stable oder Beta dynamisch und startet die offizielle Anwendung mit Loopback-CDP-Argumenten; unter macOS verwendet es die lokale Wrapper-App.",
+ "BridgeAppSafety": "Der Starter verändert weder Codex Desktop noch Konten, Projekte oder Benutzerdateien. CDP bleibt lokal und Bridge-Schreibaktionen benötigen ein lokales Capability-Token.",
"BackgroundActivityTitle": "Hintergrundaktivität und Node.js",
- "LaunchAgentDescription": "Der LaunchAgent startet den lokalen Codex-Bridge-Dienst bei der Anmeldung und startet ihn nach einem Beenden neu. Der Dienst lauscht nur über eine Loopback-Adresse auf diesem Mac.",
+ "LaunchAgentDescription": "Unter Windows überwacht das Ulanzi-Plugin den Bridge-Prozess auf Benutzerebene, unter macOS ein Benutzer-LaunchAgent. Der Dienst lauscht nur auf Loopback.",
"NodeSelectionStrategy": "Installieren / Reparieren bevorzugt Node.js 20 oder neuer aus Anmeldeumgebung, PATH, Homebrew oder üblichen Installationspfaden. Nur ersatzweise wird das in Ulanzi Studio enthaltene Node verwendet.",
"NodeInstallRecommendation": "Empfohlen: Installieren Sie die neueste Node.js-Version im Terminal und führen Sie Installieren / Reparieren erneut aus:",
- "UlanziNodeSignatureNotice": "Beim Rückgriff auf Ulanzi Studio Node kann macOS unter Hintergrundobjekte eine Aktivität mit Ulanzi-Studio-Signatur anzeigen. Dabei läuft der Codex-Bridge-Dienst mit der Node-Datei von Ulanzi Studio.",
+ "UlanziNodeSignatureNotice": "Die Bridge läuft mit Benutzerrechten und installiert keinen Windows-Dienst auf Administratorebene.",
"CurrentNodeRuntime": "Aktuelle Laufzeit: {runtime} {version}",
"SystemNode": "System-Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/en.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/en.json
index 4df2379..b8f3c3f 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/en.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/en.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Control Codex Desktop tasks and actions from Ulanzi D200 Series in Ulanzi Studio.",
- "Description": "Turn Ulanzi D200 Series into a dedicated Codex Desktop control surface through Ulanzi Studio.\n\nFEATURES\n- View five recent Codex tasks with live status and open the exact selected task.\n- Use Fast, Usage, Pin, New, Fork, Steer, Mic, and Submit actions.\n- Use the Latest Task & Scroll Encoder action to open task 1 and scroll Codex.\n- Install, repair, launch, and check Codex Bridge from every Action setup page.\n- Keep communication local through Codex Bridge.app and loopback-only endpoints.\n\nINSTALLATION ENVIRONMENT\n- macOS 13 or later\n- Codex Desktop\n- Ulanzi Studio 3.0.1 or later\n- Ulanzi D200 Series connected in Ulanzi Studio\n- Node.js 20 or later only for manual repository setup\n\nLLM / AGENT INSTALLATION\nIn your Agent, enter:\nInstall the Ulanzi Studio plugin for me: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\nMANUAL INSTALLATION\n1. Clone https://github.com/UlanziTechnology/OpenCodexMicro and open the repository directory.\n2. Quit Ulanzi Studio.\n3. Run: npm install\n4. Run: npm run install:plugin\n5. Run: npm run setup\n6. Reopen Ulanzi Studio, quit Codex Desktop, and launch ~/Applications/Codex Bridge.app.\n7. Add the Codex Micro actions to your keys in Ulanzi Studio.\n8. Select any Action to check Bridge installation, service, and CDP status.\n\nIMPORTANT\nWhen using Codex Micro, always launch Codex through ~/Applications/Codex Bridge.app. Do not open Codex Desktop directly.\nLaunch command: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Control Codex Desktop from Ulanzi D200 Series through Ulanzi Studio. View five recent tasks, use Fast, Usage, Pin, New, Fork, Steer, Mic and Submit, and navigate with the Encoder. Supports Windows 10 or later and macOS 13 or later. Codex Bridge dynamically locates Windows Stable or Beta and keeps authenticated Bridge actions and CDP on loopback. For repository setup run npm install, npm run install:plugin, and npm run setup.",
"Actions": [
{ "Name": "Codex Task 1", "Tooltip": "Open recent Codex task slot 1 and show its live status." },
{ "Name": "Codex Task 2", "Tooltip": "Open recent Codex task slot 2 and show its live status." },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge Setup",
"BridgeSetupDescription": "Install, launch, and check the local Codex connection.",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge service",
"CDPConnection": "CDP connection",
"InstallRepair": "Install / Repair",
@@ -30,14 +30,14 @@
"UninstallingBridge": "Uninstalling…",
"UninstallBridgeDone": "Codex Bridge was uninstalled. The Ulanzi plugin and Codex Desktop were kept.",
"UninstallBridgeConfirm": "Uninstall Codex Bridge and its background service? The Ulanzi plugin and Codex Desktop will not be removed.",
- "BridgeAppInfoTitle": "What is Codex Bridge.app?",
- "BridgeAppDescription": "Codex Bridge.app is a local launcher. If Codex is already running, it asks Codex to quit, then starts the official Codex Desktop executable with loopback-only CDP arguments.",
- "BridgeAppSafety": "The launcher does not modify Codex Desktop, accounts, projects, or user files, and it does not expose CDP outside this Mac. It performs no other privileged or destructive operation. The separate Bridge service handles local status and Action communication.",
+ "BridgeAppInfoTitle": "How does Codex Bridge start Codex?",
+ "BridgeAppDescription": "On Windows, Codex Bridge dynamically locates Stable or Beta and starts the official executable with loopback-only CDP arguments. On macOS it uses the local wrapper app.",
+ "BridgeAppSafety": "The launcher does not modify Codex Desktop, accounts, projects, or user files. CDP stays on this computer, and Bridge write actions require a local capability token.",
"BackgroundActivityTitle": "Background activity and Node.js",
- "LaunchAgentDescription": "The LaunchAgent starts the local Codex Bridge service when you sign in and restarts it if it exits. The service listens only on this Mac through a loopback address.",
- "NodeSelectionStrategy": "Install / Repair first selects a system Node.js 20 or later from your login environment, PATH, Homebrew, or common install paths. It uses Ulanzi Studio's bundled Node only as a fallback.",
- "NodeInstallRecommendation": "Recommended: install the latest Node.js from Terminal, then run Install / Repair again:",
- "UlanziNodeSignatureNotice": "If the Ulanzi Studio Node fallback is used, macOS Background Items may show an activity carrying the Ulanzi Studio signature. This is the Codex Bridge service running with Ulanzi Studio's Node executable.",
+ "LaunchAgentDescription": "The Ulanzi plugin supervises the user-level Bridge process on Windows; a user LaunchAgent supervises it on macOS. The service listens only on loopback.",
+ "NodeSelectionStrategy": "Install / Repair selects Node.js 20 or later from PATH, then uses Ulanzi Studio's bundled Node as a fallback.",
+ "NodeInstallRecommendation": "For repository setup, verify that Node.js 20 or later is available:",
+ "UlanziNodeSignatureNotice": "The Bridge runs with user permissions and does not install an administrator-level service.",
"CurrentNodeRuntime": "Current runtime: {runtime} {version}",
"SystemNode": "System Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/es_ES.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/es_ES.json
index 85f7856..6af8c85 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/es_ES.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/es_ES.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Controla tareas y acciones de Codex Desktop con Ulanzi D200 Series en Ulanzi Studio.",
- "Description": "Convierte Ulanzi D200 Series en una superficie de control dedicada para Codex Desktop mediante Ulanzi Studio.\n\nFUNCIONES\n- Muestra cinco tareas recientes de Codex con estado en tiempo real y abre exactamente la tarea seleccionada.\n- Incluye las acciones Fast, Usage, Pin, New, Fork, Steer, Mic y Submit.\n- Usa el Encoder Latest Task & Scroll para abrir task1 y desplazarte por Codex.\n- Instala, repara, inicia y comprueba Codex Bridge desde la página de configuración de cada Action.\n- Mantiene la comunicación local mediante Codex Bridge.app y endpoints exclusivos de loopback.\n\nENTORNO DE INSTALACIÓN\n- macOS 13 o posterior\n- Codex Desktop\n- Ulanzi Studio 3.0.1 o posterior\n- Ulanzi D200 Series conectada en Ulanzi Studio\n- Node.js 20 o posterior solo para la configuración manual desde el repositorio\n\nINSTALACIÓN CON LLM / AGENT\nEscribe en tu Agent:\nInstálame el plugin de Ulanzi Studio: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\nINSTALACIÓN MANUAL\n1. Clona https://github.com/UlanziTechnology/OpenCodexMicro y abre la carpeta del repositorio.\n2. Cierra Ulanzi Studio.\n3. Ejecuta: npm install\n4. Ejecuta: npm run install:plugin\n5. Ejecuta: npm run setup\n6. Vuelve a abrir Ulanzi Studio, cierra Codex Desktop e inicia ~/Applications/Codex Bridge.app.\n7. Añade las acciones de Codex Micro a las teclas deseadas en Ulanzi Studio.\n8. Selecciona una Action y comprueba la instalación, el servicio y CDP en la página de Codex Bridge.\n\nIMPORTANTE\nAl usar Codex Micro, inicia siempre Codex mediante ~/Applications/Codex Bridge.app. No abras Codex Desktop directamente.\nComando de inicio: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Controla Codex Desktop con Ulanzi D200 Series en Ulanzi Studio. Incluye cinco tareas recientes, Fast, Usage, Pin, New, Fork, Steer, Mic, Submit y navegación con Encoder en Windows 10 o posterior y macOS 13 o posterior. En Windows, Bridge encuentra Stable o Beta dinámicamente; las acciones de escritura usan un token local y CDP permanece en loopback. Para configurar desde el repositorio ejecuta npm install, npm run install:plugin y npm run setup.",
"Actions": [
{ "Name": "Tarea Codex 1", "Tooltip": "Abre la posición 1 de las tareas recientes de Codex y muestra su estado en tiempo real." },
{ "Name": "Tarea Codex 2", "Tooltip": "Abre la posición 2 de las tareas recientes de Codex y muestra su estado en tiempo real." },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Configuración de Codex Bridge",
"BridgeSetupDescription": "Instala, inicia y comprueba la conexión local de Codex.",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Servicio Bridge",
"CDPConnection": "Conexión CDP",
"InstallRepair": "Instalar / Reparar",
@@ -30,14 +30,14 @@
"UninstallingBridge": "Desinstalando…",
"UninstallBridgeDone": "Codex Bridge se ha desinstalado. El plugin de Ulanzi y Codex Desktop se conservaron.",
"UninstallBridgeConfirm": "¿Desinstalar Codex Bridge y su servicio en segundo plano? El plugin de Ulanzi y Codex Desktop no se eliminarán.",
- "BridgeAppInfoTitle": "¿Qué es Codex Bridge.app?",
- "BridgeAppDescription": "Codex Bridge.app es un iniciador local. Si Codex ya está abierto, solicita su cierre normal y después inicia el ejecutable oficial de Codex Desktop con argumentos CDP limitados al bucle local.",
- "BridgeAppSafety": "El iniciador no modifica Codex Desktop, cuentas, proyectos ni archivos del usuario, y no expone CDP fuera de este Mac. No realiza ninguna otra operación privilegiada o destructiva. El servicio Bridge independiente gestiona el estado local y la comunicación con las Actions.",
+ "BridgeAppInfoTitle": "¿Cómo inicia Codex Bridge a Codex?",
+ "BridgeAppDescription": "En Windows, Codex Bridge encuentra Stable o Beta dinámicamente e inicia la aplicación oficial con argumentos CDP de loopback; en macOS usa la aplicación envoltorio local.",
+ "BridgeAppSafety": "El iniciador no modifica Codex Desktop, cuentas, proyectos ni archivos del usuario. CDP permanece local y las acciones de escritura de Bridge requieren un token local.",
"BackgroundActivityTitle": "Actividad en segundo plano y Node.js",
- "LaunchAgentDescription": "LaunchAgent inicia el servicio local Codex Bridge al entrar en la sesión y lo reinicia si termina. El servicio solo escucha en este Mac mediante una dirección de bucle local.",
+ "LaunchAgentDescription": "En Windows, el plugin de Ulanzi supervisa Bridge a nivel de usuario; en macOS lo hace un LaunchAgent del usuario. El servicio solo escucha en loopback.",
"NodeSelectionStrategy": "Instalar / Reparar prioriza Node.js 20 o posterior del entorno de inicio, PATH, Homebrew o rutas habituales. Solo usa el Node incluido en Ulanzi Studio como alternativa.",
"NodeInstallRecommendation": "Recomendado: instala la versión más reciente de Node.js desde Terminal y vuelve a ejecutar Instalar / Reparar:",
- "UlanziNodeSignatureNotice": "Si se usa el Node de Ulanzi Studio como alternativa, los elementos en segundo plano de macOS pueden mostrar una actividad con la firma de Ulanzi Studio. Es el servicio Codex Bridge ejecutándose con el Node de Ulanzi Studio.",
+ "UlanziNodeSignatureNotice": "Bridge se ejecuta con permisos de usuario y no instala un servicio de Windows a nivel de administrador.",
"CurrentNodeRuntime": "Entorno actual: {runtime} {version}",
"SystemNode": "Node.js del sistema",
"UlanziNode": "Node de Ulanzi Studio",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/ja_JP.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/ja_JP.json
index a3c82cd..46b9174 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/ja_JP.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/ja_JP.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Ulanzi Studio の Ulanzi D200 Series から Codex Desktop のタスクと操作を制御します。",
- "Description": "Ulanzi Studio を通じて、Ulanzi D200 Series を Codex Desktop 専用のコントロールサーフェスにします。\n\n機能\n- 最近の 5 件の Codex タスクをリアルタイム状態付きで表示し、選択したタスクを正確に開きます。\n- Fast、Usage、Pin、New、Fork、Steer、Mic、Submit を操作できます。\n- Latest Task & Scroll Encoder で task1 を開き、Codex を上下にスクロールできます。\n- 各 Action の設定ページから Codex Bridge のインストール、修復、起動、確認ができます。\n- Codex Bridge.app とループバック限定エンドポイントを使用して通信をローカルに保ちます。\n\nインストール環境\n- macOS 13 以降\n- Codex Desktop\n- Ulanzi Studio 3.0.1 以降\n- Ulanzi Studio に接続された Ulanzi D200 Series\n- リポジトリから手動設定する場合のみ Node.js 20 以降\n\nLLM / AGENT インストール\nAgent に次のように入力します:\nUlanzi Studio プラグインをインストールしてください:[https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\n手動インストール\n1. https://github.com/UlanziTechnology/OpenCodexMicro をクローンしてリポジトリを開きます。\n2. Ulanzi Studio を終了します。\n3. 実行:npm install\n4. 実行:npm run install:plugin\n5. 実行:npm run setup\n6. Ulanzi Studio を再度開き、Codex Desktop を終了して ~/Applications/Codex Bridge.app を起動します。\n7. Ulanzi Studio で Codex Micro actions をキーに追加します。\n8. 任意の Action を選択し、Codex Bridge 設定ページでインストール、サービス、CDP の状態を確認します。\n\n重要事項\nCodex Micro を使用する際は、必ず ~/Applications/Codex Bridge.app から Codex を起動してください。Codex Desktop を直接開かないでください。\n起動コマンド: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Ulanzi Studio と Ulanzi D200 Series から Codex Desktop を操作します。最近の 5 タスク、Fast、Usage、Pin、New、Fork、Steer、Mic、Submit、Encoder ナビゲーションに対応します。Windows 10 以降と macOS 13 以降をサポートします。Windows Bridge は Stable または Beta を動的に検出し、Bridge の書き込み操作はローカル能力トークンで保護され、CDP はループバックだけを待ち受けます。リポジトリ設定では npm install、npm run install:plugin、npm run setup を実行します。",
"Actions": [
{ "Name": "Codex タスク 1", "Tooltip": "最近の Codex タスクスロット 1 を開き、リアルタイム状態を表示します。" },
{ "Name": "Codex タスク 2", "Tooltip": "最近の Codex タスクスロット 2 を開き、リアルタイム状態を表示します。" },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge 設定",
"BridgeSetupDescription": "ローカルの Codex 接続をインストール、起動、確認します。",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge サービス",
"CDPConnection": "CDP 接続",
"InstallRepair": "インストール / 修復",
@@ -30,14 +30,14 @@
"UninstallingBridge": "アンインストール中…",
"UninstallBridgeDone": "Codex Bridge をアンインストールしました。Ulanzi プラグインと Codex Desktop は保持されます。",
"UninstallBridgeConfirm": "Codex Bridge とバックグラウンドサービスをアンインストールしますか?Ulanzi プラグインと Codex Desktop は削除されません。",
- "BridgeAppInfoTitle": "Codex Bridge.app とは?",
- "BridgeAppDescription": "Codex Bridge.app はローカルランチャーです。Codex が実行中の場合は正常終了を要求し、ループバック専用の CDP 起動引数で公式 Codex Desktop 実行ファイルを起動します。",
- "BridgeAppSafety": "このランチャーは Codex Desktop、アカウント、プロジェクト、ユーザーファイルを変更せず、CDP をこの Mac の外部に公開しません。その他の権限昇格や破壊的操作も行いません。別の Bridge サービスがローカル状態と Action 通信を処理します。",
+ "BridgeAppInfoTitle": "Codex Bridge はどのように Codex を起動しますか?",
+ "BridgeAppDescription": "Windows では Stable または Beta を動的に検出し、ループバック専用の CDP 引数で公式プログラムを起動します。macOS ではローカルラッパーアプリを使用します。",
+ "BridgeAppSafety": "ランチャーは Codex Desktop、アカウント、プロジェクト、ユーザーファイルを変更しません。CDP はローカル限定で、Bridge の書き込み操作にはローカル能力トークンが必要です。",
"BackgroundActivityTitle": "バックグラウンド動作と Node.js",
- "LaunchAgentDescription": "LaunchAgent はログイン時にローカル Codex Bridge サービスを起動し、終了した場合は再起動します。サービスはループバックアドレスでこの Mac 内だけを待ち受けます。",
+ "LaunchAgentDescription": "Windows では Ulanzi プラグイン、macOS ではユーザー LaunchAgent がユーザー権限の Bridge を監視します。サービスはループバックだけを待ち受けます。",
"NodeSelectionStrategy": "インストール / 修復では、ログイン環境、PATH、Homebrew、一般的なパスから Node.js 20 以降を優先し、見つからない場合のみ Ulanzi Studio 内蔵 Node を使用します。",
"NodeInstallRecommendation": "推奨:Terminal で最新の Node.js をインストールし、インストール / 修復を再実行してください:",
- "UlanziNodeSignatureNotice": "Ulanzi Studio Node にフォールバックすると、macOS のバックグラウンド項目に Ulanzi Studio の署名付きアクティビティが表示されることがあります。これは Codex Bridge サービスが Ulanzi Studio の Node 実行ファイルで動作しているためです。",
+ "UlanziNodeSignatureNotice": "Bridge は現在のユーザー権限で動作し、管理者レベルの Windows サービスはインストールしません。",
"CurrentNodeRuntime": "現在のランタイム:{runtime} {version}",
"SystemNode": "システム Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/ko_KR.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/ko_KR.json
index 2cbe818..a6eb22a 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/ko_KR.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/ko_KR.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Ulanzi Studio의 Ulanzi D200 Series에서 Codex Desktop 작업과 동작을 제어합니다.",
- "Description": "Ulanzi Studio를 통해 Ulanzi D200 Series를 Codex Desktop 전용 컨트롤 표면으로 사용합니다.\n\n기능\n- 최근 Codex 작업 5개의 실시간 상태를 표시하고 선택한 작업을 정확히 엽니다.\n- Fast, Usage, Pin, New, Fork, Steer, Mic, Submit 동작을 제공합니다.\n- Latest Task & Scroll Encoder로 task1을 열고 Codex를 위아래로 스크롤합니다.\n- 각 Action 설정 페이지에서 Codex Bridge를 설치, 복구, 실행하고 상태를 확인합니다.\n- Codex Bridge.app과 루프백 전용 엔드포인트를 통해 통신을 로컬로 유지합니다.\n\n설치 환경\n- macOS 13 이상\n- Codex Desktop\n- Ulanzi Studio 3.0.1 이상\n- Ulanzi Studio에 연결된 Ulanzi D200 Series\n- 저장소에서 수동 설정할 때만 Node.js 20 이상 필요\n\nLLM / AGENT 설치\nAgent에 다음과 같이 입력하세요:\nUlanzi Studio 플러그인을 설치해 주세요: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\n수동 설치\n1. https://github.com/UlanziTechnology/OpenCodexMicro 저장소를 복제하고 디렉터리를 엽니다.\n2. Ulanzi Studio를 종료합니다.\n3. 실행: npm install\n4. 실행: npm run install:plugin\n5. 실행: npm run setup\n6. Ulanzi Studio를 다시 열고 Codex Desktop을 종료한 다음 ~/Applications/Codex Bridge.app을 실행합니다.\n7. Ulanzi Studio에서 Codex Micro actions를 원하는 키에 추가합니다.\n8. Action을 선택하고 Codex Bridge 설정 페이지에서 설치, 서비스, CDP 상태를 확인합니다.\n\n중요 사항\nCodex Micro를 사용할 때는 항상 ~/Applications/Codex Bridge.app을 통해 Codex를 실행하세요. Codex Desktop을 직접 열지 마세요.\n실행 명령: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Ulanzi Studio의 Ulanzi D200 Series에서 Codex Desktop을 제어합니다. 최근 작업 5개, Fast, Usage, Pin, New, Fork, Steer, Mic, Submit 및 Encoder 탐색을 Windows 10 이상과 macOS 13 이상에서 지원합니다. Windows Bridge는 Stable 또는 Beta를 동적으로 찾고, 쓰기 작업은 로컬 기능 토큰으로 보호되며 CDP는 루프백에서만 수신합니다. 저장소 설정은 npm install, npm run install:plugin, npm run setup을 실행하세요.",
"Actions": [
{ "Name": "Codex 작업 1", "Tooltip": "최근 Codex 작업 슬롯 1을 열고 실시간 상태를 표시합니다." },
{ "Name": "Codex 작업 2", "Tooltip": "최근 Codex 작업 슬롯 2를 열고 실시간 상태를 표시합니다." },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge 설정",
"BridgeSetupDescription": "로컬 Codex 연결을 설치하고 실행한 뒤 확인합니다.",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge 서비스",
"CDPConnection": "CDP 연결",
"InstallRepair": "설치 / 복구",
@@ -30,14 +30,14 @@
"UninstallingBridge": "제거 중…",
"UninstallBridgeDone": "Codex Bridge가 제거되었습니다. Ulanzi 플러그인과 Codex Desktop은 유지됩니다.",
"UninstallBridgeConfirm": "Codex Bridge와 백그라운드 서비스를 제거할까요? Ulanzi 플러그인과 Codex Desktop은 삭제되지 않습니다.",
- "BridgeAppInfoTitle": "Codex Bridge.app이란?",
- "BridgeAppDescription": "Codex Bridge.app은 로컬 실행기입니다. Codex가 이미 실행 중이면 정상 종료를 요청한 뒤, 로컬 루프백 전용 CDP 실행 인자로 공식 Codex Desktop 실행 파일을 시작합니다.",
- "BridgeAppSafety": "이 실행기는 Codex Desktop, 계정, 프로젝트 또는 사용자 파일을 수정하지 않으며 CDP를 이 Mac 외부에 노출하지 않습니다. 그 밖의 권한 상승이나 파괴적 작업도 수행하지 않습니다. 별도의 Bridge 서비스가 로컬 상태와 Action 통신을 처리합니다.",
+ "BridgeAppInfoTitle": "Codex Bridge는 Codex를 어떻게 실행하나요?",
+ "BridgeAppDescription": "Windows에서는 Codex Bridge가 Stable 또는 Beta를 동적으로 찾아 루프백 전용 CDP 인자로 공식 앱을 실행하고, macOS에서는 로컬 래퍼 앱을 사용합니다.",
+ "BridgeAppSafety": "실행기는 Codex Desktop, 계정, 프로젝트 또는 사용자 파일을 수정하지 않습니다. CDP는 로컬로 유지되며 Bridge 쓰기 작업에는 로컬 기능 토큰이 필요합니다.",
"BackgroundActivityTitle": "백그라운드 활동 및 Node.js",
- "LaunchAgentDescription": "LaunchAgent는 로그인할 때 로컬 Codex Bridge 서비스를 시작하고 종료되면 다시 시작합니다. 서비스는 루프백 주소를 통해 이 Mac에서만 수신합니다.",
+ "LaunchAgentDescription": "Windows에서는 Ulanzi 플러그인이 사용자 수준 Bridge를 감독하고 macOS에서는 사용자 LaunchAgent가 감독합니다. 서비스는 루프백에서만 수신합니다.",
"NodeSelectionStrategy": "설치 / 복구는 로그인 환경, PATH, Homebrew 및 일반 설치 경로의 Node.js 20 이상을 우선 선택하고, 적합한 버전이 없을 때만 Ulanzi Studio 내장 Node를 사용합니다.",
"NodeInstallRecommendation": "권장: Terminal에서 최신 Node.js를 설치한 다음 설치 / 복구를 다시 실행하세요:",
- "UlanziNodeSignatureNotice": "Ulanzi Studio Node로 대체하면 macOS 백그라운드 항목에 Ulanzi Studio 서명이 포함된 활동이 표시될 수 있습니다. 이는 Codex Bridge 서비스가 Ulanzi Studio의 Node 실행 파일로 동작하기 때문입니다.",
+ "UlanziNodeSignatureNotice": "Bridge는 현재 사용자 권한으로 실행되며 관리자 수준 Windows 서비스를 설치하지 않습니다.",
"CurrentNodeRuntime": "현재 런타임: {runtime} {version}",
"SystemNode": "시스템 Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/manifest.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/manifest.json
index 74a06af..afc7709 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/manifest.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/manifest.json
@@ -2,7 +2,7 @@
"Author": "OpenCodexMicro",
"Name": "Codex Micro",
"Overview": "Control Codex Desktop tasks and actions from Ulanzi D200 Series in Ulanzi Studio.",
- "Description": "Turn Ulanzi D200 Series into a dedicated Codex Desktop control surface through Ulanzi Studio.\n\nFEATURES\n- View five recent Codex tasks with live status and open the exact selected task.\n- Use Fast, Usage, Pin, New, Fork, Steer, Mic, and Submit actions.\n- Use the Latest Task & Scroll Encoder action to open task 1 and scroll Codex.\n- Install, repair, launch, and check Codex Bridge from every Action setup page.\n- Keep communication local through Codex Bridge.app and loopback-only endpoints.\n\nINSTALLATION ENVIRONMENT\n- macOS 13 or later\n- Codex Desktop\n- Ulanzi Studio 3.0.1 or later\n- Ulanzi D200 Series connected in Ulanzi Studio\n- Node.js 20 or later only for manual repository setup\n\nLLM / AGENT INSTALLATION\nIn your Agent, enter:\nInstall the Ulanzi Studio plugin for me: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\nMANUAL INSTALLATION\n1. Clone https://github.com/UlanziTechnology/OpenCodexMicro and open the repository directory.\n2. Quit Ulanzi Studio.\n3. Run: npm install\n4. Run: npm run install:plugin\n5. Run: npm run setup\n6. Reopen Ulanzi Studio, quit Codex Desktop, and launch ~/Applications/Codex Bridge.app.\n7. Add the Codex Micro actions to your keys in Ulanzi Studio.\n8. Select any Action to check Bridge installation, service, and CDP status.\n\nIMPORTANT\nWhen using Codex Micro, always launch Codex through ~/Applications/Codex Bridge.app. Do not open Codex Desktop directly.\nLaunch command: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Turn Ulanzi D200 Series into a dedicated Codex Desktop control surface through Ulanzi Studio.\n\nFEATURES\n- View five recent Codex tasks with live status and open the exact selected task.\n- Use Fast, Usage, Pin, New, Fork, Steer, Mic, and Submit actions.\n- Use the Latest Task & Scroll Encoder action to open task 1 and scroll Codex.\n- Install, repair, launch, and check Codex Bridge from every Action setup page.\n- Keep Bridge and CDP communication on authenticated loopback-only endpoints.\n\nINSTALLATION ENVIRONMENT\n- Windows 10 or later, or macOS 13 or later\n- Codex Desktop Stable or Beta\n- Ulanzi Studio 3.0.1 or later\n- Ulanzi D200 Series connected in Ulanzi Studio\n- Node.js 20 or later only for manual repository setup\n\nMANUAL INSTALLATION\n1. Clone the repository and open plugins/codex-console.\n2. Quit Ulanzi Studio.\n3. Run: npm install\n4. Run: npm run install:plugin\n5. Run: npm run setup\n6. Reopen Ulanzi Studio and select any Action to check Bridge, service, and CDP status.\n\nIMPORTANT\nOn Windows, Codex Bridge dynamically locates the installed Stable or Beta Appx package and starts Codex with loopback-only CDP arguments. It does not use a version-pinned WindowsApps path.",
"Icon": "assets/icons/plugin.png",
"Category": "AI",
"CategoryIcon": "assets/icons/plugin.png",
@@ -11,7 +11,7 @@
"assets/banners/codex-micro-banner-2.png",
"assets/banners/codex-micro-banner-3.png"
],
- "Version": "0.4.0",
+ "Version": "0.5.0",
"UUID": "com.ulanzi.ulanzistudio.codexmicro",
"Type": "JavaScript",
"CodePath": "dist/app.js",
@@ -22,6 +22,10 @@
{
"Platform": "mac",
"MinimumVersion": "13.0"
+ },
+ {
+ "Platform": "windows",
+ "MinimumVersion": "10.0"
}
],
"Actions": [
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/package-lock.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/package-lock.json
index e0c483d..7fae435 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/package-lock.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "open-codex-micro-ulanzi-plugin",
- "version": "0.4.0",
+ "version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-codex-micro-ulanzi-plugin",
- "version": "0.4.0",
+ "version": "0.5.0",
"dependencies": {
"ws": "^8.18.0"
},
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/package.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/package.json
index c2b4613..2c2c389 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/package.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/package.json
@@ -1,6 +1,6 @@
{
"name": "open-codex-micro-ulanzi-plugin",
- "version": "0.4.0",
+ "version": "0.5.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/app.js b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/app.js
index 7c93e76..d1d9623 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/app.js
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/app.js
@@ -310,6 +310,7 @@ function renderAll() {
async function bridgeRequest(path, method = "GET") {
const response = await fetch(`${BRIDGE_URL}${path}`, {
method,
+ headers: await bridgeSetup.authorizationHeaders(),
signal: AbortSignal.timeout(1200)
});
const payload = await response.json();
@@ -451,6 +452,9 @@ function connect() {
socket = new WebSocket(HOST_URL);
socket.on("open", () => {
send({ code: 0, cmd: "connected", uuid: PLUGIN_UUID });
+ if (process.env.CODEX_BRIDGE_AUTOSTART !== "0") {
+ void bridgeSetup.ensureService().catch(() => {});
+ }
clearInterval(pollTimer);
pollTimer = setInterval(() => void pollBridge(), 500);
pollTimer.unref();
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
index 801203a..55ecd07 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
@@ -9,10 +9,16 @@ import {
rm,
writeFile
} from "node:fs/promises";
-import { execFile } from "node:child_process";
+import { execFile, spawn } from "node:child_process";
+import { randomBytes } from "node:crypto";
import { promisify } from "node:util";
import { homedir } from "node:os";
-import { delimiter, join, resolve } from "node:path";
+import { delimiter, isAbsolute, join, resolve } from "node:path";
+import {
+ discoverDebugPort,
+ focusCodex,
+ launchWindowsCodex
+} from "../../../src/bridge/platform.mjs";
const execFileAsync = promisify(execFile);
@@ -53,6 +59,17 @@ async function nodeVersion(executable, execute) {
}
}
+export function bridgeDataRoot({
+ platform = process.platform,
+ home = homedir(),
+ localAppData = process.env.LOCALAPPDATA
+} = {}) {
+ if (platform === "win32") {
+ return join(localAppData || join(home, "AppData", "Local"), "OpenCodexMicro");
+ }
+ return join(home, "Library", "Application Support", "OpenCodexMicro");
+}
+
export async function selectBridgeNodeRuntime({
home = homedir(),
fallbackNodeExecutable = process.execPath,
@@ -66,44 +83,47 @@ export async function selectBridgeNodeRuntime({
const { stdout = "" } = await execute("/bin/zsh", ["-lic", "node -p process.execPath"]);
const discovered = String(stdout)
.split(/\r?\n/)
- .map(line => line.trim())
- .find(line => line.startsWith("/") && line.split("/").at(-1) === "node");
+ .map((line) => line.trim())
+ .find((line) => line.startsWith("/") && line.split("/").at(-1) === "node");
if (discovered) candidates.push(await realpath(discovered));
} catch {
- // Login-shell discovery is best effort; fixed paths and fallback remain.
+ // Login-shell discovery is best effort; PATH and fallback remain.
}
}
+ const nodeName = platform === "win32" ? "node.exe" : "node";
for (const directory of environmentPath.split(delimiter).filter(Boolean)) {
- candidates.push(join(directory, "node"));
+ candidates.push(join(directory, nodeName));
+ }
+ if (platform === "darwin") {
+ candidates.push(
+ "/opt/homebrew/bin/node",
+ "/usr/local/bin/node",
+ "/usr/bin/node",
+ join(home, ".local", "bin", "node")
+ );
}
- candidates.push(
- "/opt/homebrew/bin/node",
- "/usr/local/bin/node",
- "/usr/bin/node",
- join(home, ".local", "bin", "node")
- );
let resolvedFallback = fallbackNodeExecutable;
try {
resolvedFallback = await realpath(fallbackNodeExecutable);
} catch {
- // The fallback availability check below will provide the final result.
+ // The fallback availability check below provides the final result.
}
for (const executable of [...new Set(candidates)]) {
- if (!executable.startsWith("/")) continue;
- if (!await exists(executable, fsConstants.X_OK)) continue;
+ if (!isAbsolute(executable)) continue;
+ if (!await exists(executable, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK)) continue;
const resolvedExecutable = await realpath(executable);
if (resolvedExecutable === resolvedFallback) continue;
- const version = await nodeVersion(resolvedExecutable, execute);
- if (version?.major >= 20) {
- return { executable: resolvedExecutable, version: version.text, source: "system" };
+ const candidateVersion = await nodeVersion(resolvedExecutable, execute);
+ if (candidateVersion?.major >= 20) {
+ return { executable: resolvedExecutable, version: candidateVersion.text, source: "system" };
}
}
- if (await exists(resolvedFallback, fsConstants.X_OK)) {
- const version = await nodeVersion(resolvedFallback, execute);
- if (version?.major >= 20) {
- return { executable: resolvedFallback, version: version.text, source: "ulanzi" };
+ if (await exists(resolvedFallback, platform === "win32" ? fsConstants.F_OK : fsConstants.X_OK)) {
+ const fallbackVersion = await nodeVersion(resolvedFallback, execute);
+ if (fallbackVersion?.major >= 20) {
+ return { executable: resolvedFallback, version: fallbackVersion.text, source: "ulanzi" };
}
}
throw new Error("No compatible Node.js 20 or later runtime was found.");
@@ -114,13 +134,24 @@ export function createBridgeInstaller({
bridgeUrl,
version,
home = homedir(),
+ localAppData = process.env.LOCALAPPDATA,
uid = process.getuid?.(),
platform = process.platform,
nodeExecutable = process.execPath,
environmentPath = process.env.PATH || "",
- execute = execFileAsync
+ codexChannel = process.env.CODEX_DESKTOP_CHANNEL || "stable",
+ execute = execFileAsync,
+ spawnProcess = spawn,
+ fetchImpl = fetch
}) {
- const appRoot = join(home, "Library", "Application Support", "OpenCodexMicro");
+ const appRoot = bridgeDataRoot({ platform, home, localAppData });
+ const bridgeRuntime = join(appRoot, "bridge.mjs");
+ const tokenPath = join(appRoot, "bridge-token");
+ const installMetadata = join(appRoot, "install.json");
+ const installerRoot = resolve(pluginRoot, "installer");
+ const bundledRuntime = join(installerRoot, "bridge.mjs");
+ const bundledIcon = join(installerRoot, "CodexBridge.png");
+
const userApplications = join(home, "Applications");
const bridgeApp = join(userApplications, "Codex Bridge.app");
const bridgeContents = join(bridgeApp, "Contents");
@@ -129,17 +160,23 @@ export function createBridgeInstaller({
const bridgeLicenses = join(bridgeResources, "licenses");
const bridgeExecutable = join(bridgeMacOS, "Codex Bridge");
const bridgeIcon = join(bridgeResources, "CodexBridge.icns");
- const bridgeRuntime = join(appRoot, "bridge.mjs");
- const installMetadata = join(appRoot, "install.json");
const agentsRoot = join(home, "Library", "LaunchAgents");
const bridgeAgent = join(agentsRoot, "io.opencodexmicro.bridge.plist");
- const installerRoot = resolve(pluginRoot, "installer");
- const bundledRuntime = join(installerRoot, "bridge.mjs");
- const bundledIcon = join(installerRoot, "CodexBridge.png");
+ let lastWindowsServiceStart = 0;
+
+ async function authorizationHeaders() {
+ try {
+ const token = (await readFile(tokenPath, "utf8")).trim();
+ return token ? { Authorization: `Bearer ${token}` } : {};
+ } catch {
+ return {};
+ }
+ }
async function probeBridge() {
try {
- const response = await fetch(`${bridgeUrl}/health`, {
+ const response = await fetchImpl(`${bridgeUrl}/health`, {
+ headers: await authorizationHeaders(),
signal: AbortSignal.timeout(1200)
});
const payload = await response.json();
@@ -151,50 +188,48 @@ export function createBridgeInstaller({
}
async function status() {
- const [appInstalled, runtimeInstalled, agentInstalled, metadata, probe] = await Promise.all([
- exists(bridgeExecutable, fsConstants.X_OK),
+ const [runtimeInstalled, tokenInstalled, metadata, probe] = await Promise.all([
exists(bridgeRuntime),
- exists(bridgeAgent),
+ exists(tokenPath),
readJson(installMetadata),
probeBridge()
]);
- const installed = appInstalled && runtimeInstalled && agentInstalled;
+ const macAppInstalled = platform === "darwin"
+ ? await exists(bridgeExecutable, fsConstants.X_OK)
+ : runtimeInstalled;
+ const agentInstalled = platform === "darwin" ? await exists(bridgeAgent) : true;
+ const installed = macAppInstalled && runtimeInstalled && tokenInstalled && agentInstalled;
return {
- supported: platform === "darwin" && Number.isInteger(uid),
+ supported: platform === "win32" || (platform === "darwin" && Number.isInteger(uid)),
+ platform,
installed,
- appInstalled,
- serviceInstalled: runtimeInstalled && agentInstalled,
+ appInstalled: macAppInstalled,
+ serviceInstalled: runtimeInstalled && tokenInstalled && agentInstalled,
installedVersion: metadata?.version || null,
bundledVersion: version,
needsUpdate: !installed || metadata?.version !== version,
- appPath: bridgeApp,
+ appPath: platform === "darwin" ? bridgeApp : appRoot,
nodeExecutable: metadata?.nodeExecutable || null,
nodeVersion: metadata?.nodeVersion || null,
nodeSource: metadata?.nodeSource || null,
+ codexChannel: metadata?.codexChannel || codexChannel,
...probe
};
}
- async function buildIcon() {
+ async function buildMacIcon() {
const iconset = join(appRoot, "CodexBridge.iconset");
await rm(iconset, { recursive: true, force: true });
await mkdir(iconset, { recursive: true });
try {
for (const [name, size] of [
- ["icon_16x16.png", 16],
- ["icon_16x16@2x.png", 32],
- ["icon_32x32.png", 32],
- ["icon_32x32@2x.png", 64],
- ["icon_128x128.png", 128],
- ["icon_128x128@2x.png", 256],
- ["icon_256x256.png", 256],
- ["icon_256x256@2x.png", 512],
- ["icon_512x512.png", 512],
- ["icon_512x512@2x.png", 1024]
+ ["icon_16x16.png", 16], ["icon_16x16@2x.png", 32],
+ ["icon_32x32.png", 32], ["icon_32x32@2x.png", 64],
+ ["icon_128x128.png", 128], ["icon_128x128@2x.png", 256],
+ ["icon_256x256.png", 256], ["icon_256x256@2x.png", 512],
+ ["icon_512x512.png", 512], ["icon_512x512@2x.png", 1024]
]) {
- await execute("/usr/bin/sips", [
- "-z", String(size), String(size), bundledIcon, "--out", join(iconset, name)
- ]);
+ await execute("/usr/bin/sips", ["-z", String(size), String(size), bundledIcon, "--out", join(iconset, name)]);
}
await execute("/usr/bin/iconutil", ["-c", "icns", iconset, "-o", bridgeIcon]);
} finally {
@@ -202,38 +237,17 @@ export function createBridgeInstaller({
}
}
- async function install() {
- if (platform !== "darwin" || !Number.isInteger(uid)) {
- throw new Error("Codex Bridge installation is supported on macOS only.");
- }
- if (!await exists(bundledRuntime) || !await exists(bundledIcon)) {
- throw new Error("The plugin does not contain the Codex Bridge installation resources.");
- }
- const nodeRuntime = await selectBridgeNodeRuntime({
- home,
- fallbackNodeExecutable: nodeExecutable,
- environmentPath,
- platform,
- execute
- });
-
- await mkdir(appRoot, { recursive: true, mode: 0o700 });
- await chmod(appRoot, 0o700);
+ async function installMac(nodeRuntime) {
await mkdir(userApplications, { recursive: true });
await mkdir(agentsRoot, { recursive: true });
- await copyFile(bundledRuntime, bridgeRuntime);
-
await rm(bridgeApp, { recursive: true, force: true });
await mkdir(bridgeMacOS, { recursive: true });
await mkdir(bridgeLicenses, { recursive: true });
for (const notice of ["LICENSE", "NOTICE.md", "THIRD_PARTY_NOTICES.md"]) {
- const source = join(installerRoot, notice);
- await copyFile(source, join(bridgeLicenses, notice));
- await copyFile(source, join(appRoot, notice));
+ await copyFile(join(installerRoot, notice), join(bridgeLicenses, notice));
}
- await buildIcon();
-
- const info = `
+ await buildMacIcon();
+ await writeFile(join(bridgeContents, "Info.plist"), `
CFBundleDisplayNameCodex Bridge
@@ -246,116 +260,140 @@ export function createBridgeInstaller({
CFBundleVersion${xml(version)}LSMinimumSystemVersion13.0LSUIElement
- NSHighResolutionCapable
-`;
- await writeFile(join(bridgeContents, "Info.plist"), info);
-
- const launcher = `#!/bin/zsh
+`);
+ await writeFile(bridgeExecutable, `#!/bin/zsh
set -u
-unsetopt BG_NICE
-
codex_binary="/Applications/ChatGPT.app/Contents/MacOS/ChatGPT"
-bridge_log="$HOME/Library/Logs/OpenCodexMicro-codex-bridge.log"
-
-if [[ ! -x "$codex_binary" ]]; then
- /usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex was not found at /Applications/ChatGPT.app." as critical'
- exit 1
-fi
-
+if [[ ! -x "$codex_binary" ]]; then exit 1; fi
if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
/usr/bin/osascript -e 'tell application id "com.openai.codex" to quit'
- for attempt in {1..80}; do
- /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1 || break
- /bin/sleep 0.1
- done
-fi
-
-if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
- /usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex did not quit, so bridge parameters could not be applied. Quit Codex and try again." as critical'
- exit 1
+ for attempt in {1..80}; do /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1 || break; /bin/sleep 0.1; done
fi
-
-/usr/bin/nohup "$codex_binary" \\
- --remote-debugging-address=127.0.0.1 \\
- --remote-debugging-port=9222 \\
- --remote-allow-origins=http://127.0.0.1:9222 \\
- >>"$bridge_log" 2>&1 &
-
-for attempt in {1..300}; do
- if /usr/bin/curl --noproxy '*' --silent --fail --max-time 0.2 \\
- http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
- exit 0
- fi
- if ! /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
- break
- fi
- /bin/sleep 0.1
-done
-
-/usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex started, but the bridge endpoint is unavailable. Quit Codex and launch Codex Bridge again." as critical'
-exit 1
-`;
- await writeFile(bridgeExecutable, launcher, { mode: 0o755 });
+if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then exit 1; fi
+/usr/bin/nohup "$codex_binary" ${
+ ["--remote-debugging-address=127.0.0.1", "--remote-debugging-port=9222", "--remote-allow-origins=http://127.0.0.1:9222"].join(" ")
+} >/dev/null 2>&1 &
+`, { mode: 0o755 });
await chmod(bridgeExecutable, 0o755);
await execute("/usr/bin/codesign", ["--force", "--deep", "--sign", "-", bridgeApp]);
-
- const plist = `
+ await writeFile(bridgeAgent, `
Labelio.opencodexmicro.bridge
- ProgramArguments
- ${xml(nodeRuntime.executable)}
- ${xml(bridgeRuntime)}
-
- RunAtLoad
- KeepAlive
- ProcessTypeBackground
- ThrottleInterval2
+ ProgramArguments${xml(nodeRuntime.executable)}${xml(bridgeRuntime)}
+ RunAtLoadKeepAlive
+ ProcessTypeBackgroundThrottleInterval2StandardOutPath${xml(join(appRoot, "bridge.log"))}StandardErrorPath${xml(join(appRoot, "bridge-error.log"))}
-`;
- await writeFile(bridgeAgent, plist, { mode: 0o644 });
+`, { mode: 0o644 });
+ try { await execute("/bin/launchctl", ["bootout", `gui/${uid}`, bridgeAgent]); } catch {}
+ await execute("/bin/launchctl", ["bootstrap", `gui/${uid}`, bridgeAgent]);
+ }
+
+ async function install() {
+ if (!(platform === "win32" || (platform === "darwin" && Number.isInteger(uid)))) {
+ throw new Error("Codex Bridge installation is supported on Windows and macOS only.");
+ }
+ if (!await exists(bundledRuntime) || !await exists(bundledIcon)) {
+ throw new Error("The plugin does not contain the Codex Bridge installation resources.");
+ }
+ const nodeRuntime = await selectBridgeNodeRuntime({
+ home,
+ fallbackNodeExecutable: nodeExecutable,
+ environmentPath,
+ platform,
+ execute
+ });
+ await mkdir(appRoot, { recursive: true, mode: 0o700 });
+ if (platform !== "win32") await chmod(appRoot, 0o700);
+ await copyFile(bundledRuntime, bridgeRuntime);
+ for (const notice of ["LICENSE", "NOTICE.md", "THIRD_PARTY_NOTICES.md"]) {
+ await copyFile(join(installerRoot, notice), join(appRoot, notice));
+ }
+ if (!await exists(tokenPath)) {
+ await writeFile(tokenPath, `${randomBytes(32).toString("base64url")}\n`, { mode: 0o600 });
+ }
await writeFile(installMetadata, `${JSON.stringify({
version,
+ platform,
+ codexChannel,
nodeExecutable: nodeRuntime.executable,
nodeVersion: nodeRuntime.version,
nodeSource: nodeRuntime.source,
installedAt: new Date().toISOString()
}, null, 2)}\n`, { mode: 0o600 });
+ if (platform === "darwin") await installMac(nodeRuntime);
+ if (platform === "win32") await ensureService();
+ return status();
+ }
- try {
- await execute("/bin/launchctl", ["bootout", `gui/${uid}`, bridgeAgent]);
- } catch {
- // The service may not be loaded yet.
+ async function ensureService() {
+ if (platform !== "win32") return status();
+ const probe = await probeBridge();
+ if (probe.serviceOnline || Date.now() - lastWindowsServiceStart < 2000) return { ...await status(), ...probe };
+ const metadata = await readJson(installMetadata);
+ if (!metadata?.nodeExecutable || !await exists(bridgeRuntime) || !await exists(tokenPath)) {
+ throw new Error("Codex Bridge is not installed. Use Install / Repair first.");
}
- await execute("/bin/launchctl", ["bootstrap", `gui/${uid}`, bridgeAgent]);
+ const token = (await readFile(tokenPath, "utf8")).trim();
+ const child = spawnProcess(metadata.nodeExecutable, [bridgeRuntime], {
+ detached: true,
+ stdio: "ignore",
+ windowsHide: true,
+ env: {
+ ...process.env,
+ CODEX_BRIDGE_DATA_ROOT: appRoot,
+ CODEX_BRIDGE_TOKEN: token
+ }
+ });
+ child.unref?.();
+ lastWindowsServiceStart = Date.now();
return status();
}
async function launch() {
- if (!await exists(bridgeExecutable, fsConstants.X_OK)) {
- throw new Error("Codex Bridge.app is not installed.");
+ const current = await status();
+ if (!current.installed) throw new Error("Codex Bridge is not installed. Use Install / Repair first.");
+ if (platform === "darwin") {
+ await execute("/usr/bin/open", [bridgeApp]);
+ return status();
}
- await execute("/usr/bin/open", [bridgeApp]);
- return status();
+ if (platform === "win32") {
+ await ensureService();
+ try {
+ await discoverDebugPort({ platform, execute, fetchImpl });
+ await focusCodex({ platform, execute });
+ } catch (error) {
+ if (!/local debug bridge/.test(error.message)) throw error;
+ await launchWindowsCodex({ channel: codexChannel, execute, spawnProcess });
+ }
+ return status();
+ }
+ throw new Error(`Codex Bridge launch is not supported on ${platform}.`);
}
async function uninstall() {
- if (platform !== "darwin" || !Number.isInteger(uid)) {
- throw new Error("Codex Bridge uninstallation is supported on macOS only.");
+ if (!(platform === "win32" || (platform === "darwin" && Number.isInteger(uid)))) {
+ throw new Error("Codex Bridge uninstallation is supported on Windows and macOS only.");
}
- try {
- await execute("/bin/launchctl", ["bootout", `gui/${uid}`, bridgeAgent]);
- } catch {
- // The service may already be stopped or absent.
+ if (platform === "darwin") {
+ try { await execute("/bin/launchctl", ["bootout", `gui/${uid}`, bridgeAgent]); } catch {}
+ await rm(bridgeAgent, { force: true });
+ await rm(bridgeApp, { recursive: true, force: true });
+ } else {
+ const stopScript = "$target=$env:CODEX_BRIDGE_TARGET_RUNTIME; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $PID -and $_.Name -like 'node*' -and $_.CommandLine -like ('*' + $target + '*') } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }";
+ try {
+ await execute("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", stopScript], {
+ timeout: 5000,
+ env: { ...process.env, CODEX_BRIDGE_TARGET_RUNTIME: bridgeRuntime }
+ });
+ } catch {}
}
- await rm(bridgeAgent, { force: true });
await rm(appRoot, { recursive: true, force: true });
- await rm(bridgeApp, { recursive: true, force: true });
return status();
}
- return { status, install, launch, uninstall };
+ return { status, install, launch, uninstall, ensureService, authorizationHeaders };
}
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/property-inspector/setup.html b/integration/com.ulanzi.codexmicro.ulanziPlugin/property-inspector/setup.html
index 038564d..f9caef1 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/property-inspector/setup.html
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/property-inspector/setup.html
@@ -9,37 +9,26 @@
html, body { margin: 0; background: transparent; color: #e8e8e8; font-size: 13px; }
* { box-sizing: border-box; }
main { padding: 12px 14px 10px; }
- h1 { margin: 0 0 5px; font-size: 15px; font-weight: 650; }
- .intro { margin: 0 0 10px; color: #aeb3ba; line-height: 1.35; }
- .panel { border: 1px solid rgba(255,255,255,.12); border-radius: 8px; overflow: hidden; background: rgba(255,255,255,.035); }
- .row { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; min-height: 37px; padding: 7px 10px; border-bottom: 1px solid rgba(255,255,255,.08); }
+ h1 { margin: 0 0 5px; font-size: 15px; }
+ .intro, .message, .info-body { color: #aeb3ba; line-height: 1.4; }
+ .intro { margin: 0 0 10px; }
+ .panel, details { border: 1px solid rgba(255,255,255,.12); border-radius: 8px; background: rgba(255,255,255,.035); }
+ .row { display: grid; grid-template-columns: 1fr auto; gap: 12px; padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,.08); }
.row:last-child { border-bottom: 0; }
- .value { display: inline-flex; align-items: center; gap: 6px; color: #aeb3ba; text-align: right; }
- .dot { width: 8px; height: 8px; border-radius: 50%; background: #72777f; box-shadow: 0 0 0 2px rgba(114,119,127,.15); }
- .value.good { color: #75d6a3; }
- .value.good .dot { background: #42c985; box-shadow: 0 0 0 2px rgba(66,201,133,.15); }
- .value.warn { color: #f0bc68; }
- .value.warn .dot { background: #e6a84b; box-shadow: 0 0 0 2px rgba(230,168,75,.15); }
- .value.bad { color: #ef8585; }
- .value.bad .dot { background: #e45f5f; box-shadow: 0 0 0 2px rgba(228,95,95,.15); }
- .message { min-height: 18px; margin: 8px 1px 4px; color: #aeb3ba; line-height: 1.35; }
- .message.error { color: #ef8585; }
- .message.success { color: #75d6a3; }
+ .value { display: inline-flex; align-items: center; gap: 6px; text-align: right; }
+ .dot { width: 8px; height: 8px; border-radius: 50%; background: #72777f; }
+ .good { color: #75d6a3; } .good .dot { background: #42c985; }
+ .warn { color: #f0bc68; } .warn .dot { background: #e6a84b; }
+ .bad, .error { color: #ef8585; } .bad .dot { background: #e45f5f; }
+ .success { color: #75d6a3; }
+ .message { min-height: 18px; margin: 8px 1px 4px; }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin-top: 8px; }
button { min-height: 33px; border: 1px solid rgba(255,255,255,.14); border-radius: 6px; color: #f3f3f3; background: rgba(255,255,255,.08); cursor: pointer; }
- button:hover:not(:disabled) { background: rgba(255,255,255,.13); }
- button.primary { border-color: #3f78bd; background: #2867ad; }
- button.primary:hover:not(:disabled) { background: #3276bf; }
- button.danger { grid-column: 1 / -1; color: #ef9a9a; border-color: rgba(228,95,95,.35); background: rgba(228,95,95,.08); }
- button.danger:hover:not(:disabled) { background: rgba(228,95,95,.16); }
- button:disabled { cursor: default; opacity: .45; }
- details.info { margin-top: 10px; border: 1px solid rgba(255,255,255,.1); border-radius: 7px; background: rgba(255,255,255,.025); }
- details.info summary { padding: 9px 10px; color: #d5d8dc; cursor: pointer; font-weight: 600; }
- .info-body { padding: 0 10px 10px; color: #9fa5ad; line-height: 1.45; }
- .info-body p { margin: 7px 0; }
- .info-body code { display: block; margin: 7px 0; padding: 7px 8px; overflow-wrap: anywhere; border-radius: 5px; color: #dce7f6; background: rgba(0,0,0,.28); user-select: text; }
- .runtime { color: #c7d3e2; }
- .signature-note { color: #e3b971; }
+ button.primary { background: #2867ad; } button.danger { grid-column: 1 / -1; color: #ef9a9a; }
+ button:disabled { opacity: .45; cursor: default; }
+ details { margin-top: 10px; } summary { padding: 9px 10px; cursor: pointer; font-weight: 600; }
+ .info-body { padding: 0 10px 10px; }
+ .info-body code { display: block; margin: 7px 0; padding: 7px 8px; border-radius: 5px; background: rgba(0,0,0,.28); user-select: text; }
.version { margin-top: 7px; color: #777e87; font-size: 11px; text-align: right; }
@@ -54,13 +43,13 @@
-
+
-
+
@@ -68,153 +57,94 @@
-
+
-
- brew install node
-
-
+
+
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/pt_PT.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/pt_PT.json
index d62042b..86ca6a5 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/pt_PT.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/pt_PT.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "Controle tarefas e ações do Codex Desktop com a Ulanzi D200 Series no Ulanzi Studio.",
- "Description": "Transforme a Ulanzi D200 Series numa superfície de controlo dedicada ao Codex Desktop através do Ulanzi Studio.\n\nFUNCIONALIDADES\n- Mostra cinco tarefas recentes do Codex com estado em tempo real e abre exatamente a tarefa selecionada.\n- Disponibiliza as ações Fast, Usage, Pin, New, Fork, Steer, Mic e Submit.\n- Use o Encoder Latest Task & Scroll para abrir a task1 e deslocar o Codex.\n- Instale, repare, inicie e verifique o Codex Bridge na página de configuração de cada Action.\n- Mantém a comunicação local através da Codex Bridge.app e de endpoints apenas de loopback.\n\nAMBIENTE DE INSTALAÇÃO\n- macOS 13 ou posterior\n- Codex Desktop\n- Ulanzi Studio 3.0.1 ou posterior\n- Ulanzi D200 Series ligada no Ulanzi Studio\n- Node.js 20 ou posterior apenas para configuração manual pelo repositório\n\nINSTALAÇÃO COM LLM / AGENT\nIntroduza no seu Agent:\nInstala o plugin do Ulanzi Studio por mim: [https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\nINSTALAÇÃO MANUAL\n1. Clone https://github.com/UlanziTechnology/OpenCodexMicro e abra a pasta do repositório.\n2. Feche o Ulanzi Studio.\n3. Execute: npm install\n4. Execute: npm run install:plugin\n5. Execute: npm run setup\n6. Reabra o Ulanzi Studio, feche o Codex Desktop e inicie ~/Applications/Codex Bridge.app.\n7. Adicione as ações Codex Micro às teclas pretendidas no Ulanzi Studio.\n8. Selecione uma Action e verifique a instalação, o serviço e o CDP na página do Codex Bridge.\n\nIMPORTANTE\nAo usar o Codex Micro, inicie sempre o Codex através de ~/Applications/Codex Bridge.app. Não abra diretamente o Codex Desktop.\nComando de início: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "Controle o Codex Desktop com a Ulanzi D200 Series no Ulanzi Studio. Suporta cinco tarefas recentes, Fast, Usage, Pin, New, Fork, Steer, Mic, Submit e navegação por Encoder no Windows 10 ou posterior e macOS 13 ou posterior. No Windows, a Bridge encontra Stable ou Beta dinamicamente; as ações de escrita usam um token local e o CDP permanece em loopback. Para configurar pelo repositório execute npm install, npm run install:plugin e npm run setup.",
"Actions": [
{ "Name": "Tarefa Codex 1", "Tooltip": "Abre a posição 1 das tarefas recentes do Codex e mostra o estado em tempo real." },
{ "Name": "Tarefa Codex 2", "Tooltip": "Abre a posição 2 das tarefas recentes do Codex e mostra o estado em tempo real." },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Configuração do Codex Bridge",
"BridgeSetupDescription": "Instale, inicie e verifique a ligação local ao Codex.",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Serviço Bridge",
"CDPConnection": "Ligação CDP",
"InstallRepair": "Instalar / Reparar",
@@ -30,14 +30,14 @@
"UninstallingBridge": "A desinstalar…",
"UninstallBridgeDone": "O Codex Bridge foi desinstalado. O plugin Ulanzi e o Codex Desktop foram mantidos.",
"UninstallBridgeConfirm": "Desinstalar o Codex Bridge e o respetivo serviço em segundo plano? O plugin Ulanzi e o Codex Desktop não serão removidos.",
- "BridgeAppInfoTitle": "O que é a Codex Bridge.app?",
- "BridgeAppDescription": "A Codex Bridge.app é um iniciador local. Se o Codex já estiver aberto, pede-lhe que termine normalmente e inicia o executável oficial do Codex Desktop com argumentos CDP limitados ao endereço de loopback.",
- "BridgeAppSafety": "O iniciador não altera o Codex Desktop, contas, projetos ou ficheiros do utilizador e não expõe o CDP fora deste Mac. Não executa outras operações privilegiadas ou destrutivas. O serviço Bridge separado trata do estado local e da comunicação com as Actions.",
+ "BridgeAppInfoTitle": "Como é que o Codex Bridge inicia o Codex?",
+ "BridgeAppDescription": "No Windows, o Codex Bridge encontra Stable ou Beta dinamicamente e inicia a aplicação oficial com argumentos CDP de loopback; no macOS usa a aplicação wrapper local.",
+ "BridgeAppSafety": "O iniciador não altera o Codex Desktop, contas, projetos ou ficheiros do utilizador. O CDP permanece local e as ações de escrita da Bridge exigem um token local.",
"BackgroundActivityTitle": "Atividade em segundo plano e Node.js",
- "LaunchAgentDescription": "O LaunchAgent inicia o serviço local Codex Bridge ao iniciar sessão e reinicia-o se terminar. O serviço escuta apenas neste Mac através de um endereço de loopback.",
+ "LaunchAgentDescription": "No Windows, o plugin Ulanzi supervisiona a Bridge ao nível do utilizador; no macOS, um LaunchAgent do utilizador. O serviço escuta apenas em loopback.",
"NodeSelectionStrategy": "Instalar / Reparar dá prioridade ao Node.js 20 ou posterior do ambiente de início de sessão, PATH, Homebrew ou caminhos comuns. Só usa o Node incluído no Ulanzi Studio como alternativa.",
"NodeInstallRecommendation": "Recomendado: instale a versão mais recente do Node.js no Terminal e execute novamente Instalar / Reparar:",
- "UlanziNodeSignatureNotice": "Ao usar o Node do Ulanzi Studio como alternativa, os Itens em segundo plano do macOS podem mostrar uma atividade com a assinatura do Ulanzi Studio. É o serviço Codex Bridge a usar o executável Node do Ulanzi Studio.",
+ "UlanziNodeSignatureNotice": "A Bridge é executada com permissões do utilizador e não instala um serviço Windows de administrador.",
"CurrentNodeRuntime": "Runtime atual: {runtime} {version}",
"SystemNode": "Node.js do sistema",
"UlanziNode": "Node do Ulanzi Studio",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/test/smoke.mjs b/integration/com.ulanzi.codexmicro.ulanziPlugin/test/smoke.mjs
index d0e09e2..554c0f8 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/test/smoke.mjs
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/test/smoke.mjs
@@ -11,9 +11,10 @@ const navigateAction = manifest.Actions.find(action =>
);
assert.deepEqual(navigateAction?.Controllers, ["Encoder"]);
assert.equal(navigateAction?.Encoder?.layout, "$UA1");
-assert.equal(manifest.Version, "0.4.0");
+assert.equal(manifest.Version, "0.5.0");
assert.equal(manifest.Software?.MinVersion, "3.0.1");
assert.equal(manifest.OS?.find(item => item.Platform === "mac")?.MinimumVersion, "13.0");
+assert.equal(manifest.OS?.find(item => item.Platform === "windows")?.MinimumVersion, "10.0");
assert.ok(
manifest.Actions.every(action => action.PropertyInspectorPath === "property-inspector/setup.html"),
"every action must expose the shared Bridge setup inspector"
@@ -34,17 +35,15 @@ for (const asset of [
]) {
assert.ok((await readFile(new URL(asset, packageRootUrl))).length > 0, `${asset} must be bundled`);
}
+for (const banner of manifest.Banner || []) {
+ assert.ok((await readFile(new URL(banner, packageRootUrl))).length > 0, `${banner} must be bundled`);
+}
assert.match(manifest.Overview, /Ulanzi D200 Series/);
-assert.match(manifest.Description, /INSTALLATION ENVIRONMENT/);
-assert.match(manifest.Description, /LLM \/ AGENT INSTALLATION/);
assert.match(manifest.Description, /MANUAL INSTALLATION/);
assert.match(manifest.Description, /Latest Task & Scroll Encoder/);
-assert.match(
- manifest.Description,
- /Install the Ulanzi Studio plugin for me: \[https:\/\/github\.com\/UlanziTechnology\/OpenCodexMicro#1-llm--agent-installation\]\(https:\/\/github\.com\/UlanziTechnology\/OpenCodexMicro#1-llm--agent-installation\)/
-);
-assert.match(manifest.Description, /always launch Codex through ~\/Applications\/Codex Bridge\.app/i);
-assert.match(manifest.Description, /open ~\/Applications\/Codex\\ Bridge\.app$/);
+assert.match(manifest.Description, /Windows 10/);
+assert.match(manifest.Description, /Stable or Beta/);
+assert.match(manifest.Description, /does not use a version-pinned WindowsApps path/i);
const localizedActionNames = {
"en.json": ["Codex Task 1", "Latest Task & Scroll", "Submit to Codex"],
@@ -57,21 +56,6 @@ const localizedActionNames = {
"es_ES.json": ["Tarea Codex 1", "Tarea reciente y desplazamiento", "Enviar a Codex"]
};
-const localizedInstallPrompts = {
- "en.json": "Install the Ulanzi Studio plugin for me:",
- "zh_CN.json": "替我安装一下UlanziStudio插件:",
- "zh_HK.json": "請替我安裝 Ulanzi Studio 外掛程式:",
- "ja_JP.json": "Ulanzi Studio プラグインをインストールしてください:",
- "de_DE.json": "Installiere bitte das Ulanzi-Studio-Plugin für mich:",
- "ko_KR.json": "Ulanzi Studio 플러그인을 설치해 주세요:",
- "pt_PT.json": "Instala o plugin do Ulanzi Studio por mim:",
- "es_ES.json": "Instálame el plugin de Ulanzi Studio:"
-};
-
-const llmInstallationLink =
- "[https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation]" +
- "(https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)";
-
for (const locale of [
"en.json",
"zh_CN.json",
@@ -85,10 +69,9 @@ for (const locale of [
const messages = JSON.parse(await readFile(new URL(locale, packageRootUrl)));
assert.equal(messages.Name, "Codex Micro", `${locale} must localize Name`);
assert.ok(messages.Overview?.length > 20, `${locale} must localize Overview`);
- assert.match(messages.Description, /https:\/\/github\.com\/UlanziTechnology\/OpenCodexMicro/);
assert.match(messages.Description, /npm run install:plugin/);
assert.match(messages.Description, /npm run setup/);
- assert.match(messages.Description, /Latest Task & Scroll/);
+ assert.doesNotMatch(messages.Description, /Codex Bridge\.app/);
assert.ok(messages.Localization?.BridgeSetup?.length > 0, `${locale} must localize the setup page`);
assert.ok(messages.Localization?.InstallRepair?.length > 0, `${locale} must localize Bridge installation`);
assert.ok(messages.Localization?.UninstallBridge?.length > 0, `${locale} must localize Bridge uninstallation`);
@@ -96,12 +79,6 @@ for (const locale of [
assert.ok(messages.Localization?.BackgroundActivityTitle?.length > 0, `${locale} must explain background activity`);
assert.ok(messages.Localization?.NodeSelectionStrategy?.length > 0, `${locale} must explain Node selection`);
assert.ok(messages.Localization?.UlanziNodeSignatureNotice?.length > 0, `${locale} must explain the Ulanzi Node signature`);
- assert.ok(
- messages.Description.includes(`${localizedInstallPrompts[locale]}${
- locale.startsWith("zh_") || locale === "ja_JP.json" ? "" : " "
- }${llmInstallationLink}`),
- `${locale} must localize the anchored LLM installation prompt`
- );
assert.equal(messages.Actions?.length, manifest.Actions.length, `${locale} must localize every action`);
assert.ok(
messages.Actions.every(action => action.Name?.length > 0 && action.Tooltip?.length > 0),
@@ -112,11 +89,8 @@ for (const locale of [
localizedActionNames[locale],
`${locale} action localization must follow manifest action order`
);
- assert.match(
- messages.Description.split("\n\n").at(-1),
- /open ~\/Applications\/Codex\\ Bridge\.app$/,
- `${locale} must end with the Bridge launch command`
- );
+ assert.equal(messages.Localization.BridgeApp, "Codex Bridge");
+ assert.doesNotMatch(messages.Localization.BridgeAppDescription, /Codex Bridge\.app/);
}
const bridgeRequests = [];
@@ -155,7 +129,11 @@ const nodeBinary = process.env.PLUGIN_NODE_BINARY || process.execPath;
const pluginRoot = process.env.PLUGIN_RUNTIME_ROOT || new URL("..", import.meta.url);
const child = spawn(nodeBinary, ["dist/app.js", "127.0.0.1", String(hostPort), "en-US", "3.0.0"], {
cwd: pluginRoot,
- env: { ...process.env, CODEX_BRIDGE_URL: `http://127.0.0.1:${bridgePort}` },
+ env: {
+ ...process.env,
+ CODEX_BRIDGE_URL: `http://127.0.0.1:${bridgePort}`,
+ CODEX_BRIDGE_AUTOSTART: "0"
+ },
stdio: ["ignore", "pipe", "pipe"]
});
@@ -184,7 +162,7 @@ try {
);
assert.equal(setupStatus?.payload?.status?.serviceOnline, true);
assert.equal(setupStatus?.payload?.status?.cdpConnected, true);
- assert.equal(setupStatus?.payload?.status?.bundledVersion, "0.4.0");
+ assert.equal(setupStatus?.payload?.status?.bundledVersion, "0.5.0");
const taskPaths = [
"assets/icons/task-working.png",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_CN.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_CN.json
index e055a1f..6aa0a7a 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_CN.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_CN.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "通过 Ulanzi Studio,在 Ulanzi D200 Series 上查看并控制 Codex Desktop。",
- "Description": "通过 Ulanzi Studio,将 Ulanzi D200 Series 变成 Codex Desktop 专用控制台。\n\n功能\n- 实时显示最近五个 Codex 任务的状态,并精确打开所选任务。\n- 提供 Fast、Usage、Pin、New、Fork、Steer、Mic 和 Submit 操作。\n- 使用 Latest Task & Scroll 旋钮打开 task1,并上下滚动 Codex。\n- 通过每个 Action 的配置页安装、修复、启动并检测 Codex Bridge。\n- 通过 Codex Bridge.app 和仅限回环地址的接口保持本机通信。\n\n安装环境\n- macOS 13 或更高版本\n- Codex Desktop\n- Ulanzi Studio 3.0.1 或更高版本\n- 已在 Ulanzi Studio 中连接 Ulanzi D200 Series\n- 仅使用仓库手动设置 Bridge 时需要 Node.js 20 或更高版本\n\nLLM / AGENT 安装\n在 Agent 中输入:\n替我安装一下UlanziStudio插件:[https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\n手动安装\n1. 克隆 https://github.com/UlanziTechnology/OpenCodexMicro 并进入仓库目录。\n2. 退出 Ulanzi Studio。\n3. 运行:npm install\n4. 运行:npm run install:plugin\n5. 运行:npm run setup\n6. 重新打开 Ulanzi Studio,退出 Codex Desktop,再启动 ~/Applications/Codex Bridge.app。\n7. 在 Ulanzi Studio 中把 Codex Micro actions 添加到按键。\n8. 选中任意 Action,在 Codex Bridge 设置页检查安装、服务和 CDP 状态。\n\n注意事项\n使用 Codex Micro 时,必须通过 ~/Applications/Codex Bridge.app 启动 Codex,请勿直接打开 Codex Desktop。\n启动命令: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "通过 Ulanzi Studio,在 Ulanzi D200 Series 上查看和控制 Codex Desktop。支持最近五个任务、Fast、Usage、Pin、New、Fork、Steer、Mic、Submit 和旋钮导航。支持 Windows 10 或更高版本以及 macOS 13 或更高版本。Windows 版 Bridge 会动态查找 Stable 或 Beta,Bridge 写操作使用本机能力令牌,CDP 仅监听回环地址。仓库安装请运行 npm install、npm run install:plugin 和 npm run setup。",
"Actions": [
{ "Name": "Codex 任务 1", "Tooltip": "打开最近的 Codex 任务槽位 1,并显示其实时状态。" },
{ "Name": "Codex 任务 2", "Tooltip": "打开最近的 Codex 任务槽位 2,并显示其实时状态。" },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge 设置",
"BridgeSetupDescription": "安装、启动并检查本机 Codex 连接。",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge 后台服务",
"CDPConnection": "CDP 连接",
"InstallRepair": "安装 / 修复",
@@ -30,14 +30,14 @@
"UninstallingBridge": "正在卸载…",
"UninstallBridgeDone": "Codex Bridge 已卸载,Ulanzi 插件和 Codex Desktop 保留不变。",
"UninstallBridgeConfirm": "是否卸载 Codex Bridge 及其后台服务?不会删除 Ulanzi 插件和 Codex Desktop。",
- "BridgeAppInfoTitle": "什么是 Codex Bridge.app?",
- "BridgeAppDescription": "Codex Bridge.app 是一个本地启动器。如果 Codex 已在运行,它会先请求 Codex 正常退出,再使用仅限本机回环地址的 CDP 启动参数打开官方 Codex Desktop 可执行文件。",
- "BridgeAppSafety": "该启动器不会修改 Codex Desktop、账户、项目或用户文件,也不会把 CDP 开放到本机之外;它不执行其他提权或破坏性操作。独立的 Bridge 服务负责本地状态和 Action 通信。",
+ "BridgeAppInfoTitle": "Codex Bridge 如何启动 Codex?",
+ "BridgeAppDescription": "Windows 上会动态查找 Stable 或 Beta,并使用仅限回环地址的 CDP 参数启动官方程序;macOS 上使用本地包装应用。",
+ "BridgeAppSafety": "启动器不会修改 Codex Desktop、账户、项目或用户文件。CDP 仅限本机,Bridge 写操作需要本机能力令牌。",
"BackgroundActivityTitle": "后台活动与 Node.js",
- "LaunchAgentDescription": "LaunchAgent 会在用户登录时启动本机 Codex Bridge 服务,并在服务退出后自动重启。该服务只通过回环地址在本机监听,不向外部网络开放。",
- "NodeSelectionStrategy": "“安装 / 修复”会优先从登录环境、PATH、Homebrew 和常见安装路径选择系统 Node.js 20 或更高版本,仅在找不到合适版本时使用 Ulanzi Studio 内置 Node。",
- "NodeInstallRecommendation": "建议通过 Terminal 安装最新 Node.js,然后再次执行“安装 / 修复”:",
- "UlanziNodeSignatureNotice": "如果降级使用 Ulanzi Studio Node,macOS“后台项目”中可能显示一个携带 Ulanzi Studio 签名的活动。这是 Codex Bridge 服务正在使用 Ulanzi Studio 的 Node 可执行文件。",
+ "LaunchAgentDescription": "Windows 由 Ulanzi 插件监督用户级 Bridge 进程,macOS 由用户 LaunchAgent 监督;服务只监听回环地址。",
+ "NodeSelectionStrategy": "“安装 / 修复”优先选择 PATH 中的 Node.js 20 或更高版本,找不到时使用 Ulanzi Studio 内置 Node。",
+ "NodeInstallRecommendation": "从仓库安装时,请确认 Node.js 20 或更高版本可用:",
+ "UlanziNodeSignatureNotice": "Bridge 以当前用户权限运行,不安装管理员级 Windows 服务。",
"CurrentNodeRuntime": "当前运行环境:{runtime} {version}",
"SystemNode": "系统 Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_HK.json b/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_HK.json
index c3c21e1..d7752b4 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_HK.json
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/zh_HK.json
@@ -1,7 +1,7 @@
{
"Name": "Codex Micro",
"Overview": "透過 Ulanzi Studio,在 Ulanzi D200 Series 上查看並控制 Codex Desktop。",
- "Description": "透過 Ulanzi Studio,將 Ulanzi D200 Series 變成 Codex Desktop 專用控制台。\n\n功能\n- 即時顯示最近五個 Codex 任務的狀態,並準確開啟所選任務。\n- 提供 Fast、Usage、Pin、New、Fork、Steer、Mic 和 Submit 操作。\n- 使用 Latest Task & Scroll 旋鈕開啟 task1,並上下捲動 Codex。\n- 透過每個 Action 的設定頁安裝、修復、啟動並檢查 Codex Bridge。\n- 透過 Codex Bridge.app 及僅限迴環位址的介面維持本機通訊。\n\n安裝環境\n- macOS 13 或更新版本\n- Codex Desktop\n- Ulanzi Studio 3.0.1 或更新版本\n- 已在 Ulanzi Studio 中連接 Ulanzi D200 Series\n- 僅使用儲存庫手動設定 Bridge 時需要 Node.js 20 或更新版本\n\nLLM / AGENT 安裝\n在 Agent 中輸入:\n請替我安裝 Ulanzi Studio 外掛程式:[https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation](https://github.com/UlanziTechnology/OpenCodexMicro#1-llm--agent-installation)\n\n手動安裝\n1. 複製 https://github.com/UlanziTechnology/OpenCodexMicro 並進入儲存庫目錄。\n2. 結束 Ulanzi Studio。\n3. 執行:npm install\n4. 執行:npm run install:plugin\n5. 執行:npm run setup\n6. 重新開啟 Ulanzi Studio,結束 Codex Desktop,再啟動 ~/Applications/Codex Bridge.app。\n7. 在 Ulanzi Studio 中將 Codex Micro actions 加到按鍵。\n8. 選取任意 Action,在 Codex Bridge 設定頁檢查安裝、服務和 CDP 狀態。\n\n注意事項\n使用 Codex Micro 時,必須透過 ~/Applications/Codex Bridge.app 啟動 Codex,請勿直接開啟 Codex Desktop。\n啟動指令: open ~/Applications/Codex\\ Bridge.app",
+ "Description": "透過 Ulanzi Studio,在 Ulanzi D200 Series 上查看及控制 Codex Desktop。支援最近五個任務、Fast、Usage、Pin、New、Fork、Steer、Mic、Submit 及旋鈕導覽。支援 Windows 10 或更新版本及 macOS 13 或更新版本。Windows Bridge 會動態尋找 Stable 或 Beta,Bridge 寫入操作使用本機能力權杖,CDP 只監聽迴環位址。儲存庫安裝請執行 npm install、npm run install:plugin 及 npm run setup。",
"Actions": [
{ "Name": "Codex 任務 1", "Tooltip": "開啟最近的 Codex 任務欄位 1,並顯示即時狀態。" },
{ "Name": "Codex 任務 2", "Tooltip": "開啟最近的 Codex 任務欄位 2,並顯示即時狀態。" },
@@ -21,7 +21,7 @@
"Localization": {
"BridgeSetup": "Codex Bridge 設定",
"BridgeSetupDescription": "安裝、啟動並檢查本機 Codex 連線。",
- "BridgeApp": "Codex Bridge.app",
+ "BridgeApp": "Codex Bridge",
"BridgeService": "Bridge 背景服務",
"CDPConnection": "CDP 連線",
"InstallRepair": "安裝 / 修復",
@@ -30,14 +30,14 @@
"UninstallingBridge": "正在卸載…",
"UninstallBridgeDone": "Codex Bridge 已卸載,Ulanzi 插件和 Codex Desktop 保留不變。",
"UninstallBridgeConfirm": "是否卸載 Codex Bridge 及其背景服務?不會刪除 Ulanzi 插件和 Codex Desktop。",
- "BridgeAppInfoTitle": "甚麼是 Codex Bridge.app?",
- "BridgeAppDescription": "Codex Bridge.app 是本機啟動器。如果 Codex 已在執行,它會先要求 Codex 正常結束,再使用僅限本機迴環位址的 CDP 啟動參數開啟官方 Codex Desktop 執行檔。",
- "BridgeAppSafety": "此啟動器不會修改 Codex Desktop、帳戶、專案或使用者檔案,也不會向本機以外開放 CDP;它不執行其他提權或破壞性操作。獨立 Bridge 服務負責本機狀態及 Action 通訊。",
+ "BridgeAppInfoTitle": "Codex Bridge 如何啟動 Codex?",
+ "BridgeAppDescription": "Windows 會動態尋找 Stable 或 Beta,並以僅限迴環位址的 CDP 參數啟動官方程式;macOS 使用本機包裝應用程式。",
+ "BridgeAppSafety": "啟動器不會修改 Codex Desktop、帳戶、專案或使用者檔案。CDP 只限本機,Bridge 寫入操作需要本機能力權杖。",
"BackgroundActivityTitle": "背景活動與 Node.js",
- "LaunchAgentDescription": "LaunchAgent 會在使用者登入時啟動本機 Codex Bridge 服務,並在服務結束後自動重新啟動。服務只透過迴環位址在本機監聽。",
+ "LaunchAgentDescription": "Windows 由 Ulanzi 插件監督使用者級 Bridge 程序,macOS 由使用者 LaunchAgent 監督;服務只監聽迴環位址。",
"NodeSelectionStrategy": "「安裝 / 修復」會優先從登入環境、PATH、Homebrew 及常見安裝路徑選擇系統 Node.js 20 或以上版本,找不到合適版本時才使用 Ulanzi Studio 內置 Node。",
"NodeInstallRecommendation": "建議透過 Terminal 安裝最新 Node.js,然後再次執行「安裝 / 修復」:",
- "UlanziNodeSignatureNotice": "若使用 Ulanzi Studio Node 後備方案,macOS「背景項目」可能顯示帶有 Ulanzi Studio 簽名的活動。這是 Codex Bridge 服務使用 Ulanzi Studio Node 執行檔所致。",
+ "UlanziNodeSignatureNotice": "Bridge 以目前使用者權限執行,不安裝管理員級 Windows 服務。",
"CurrentNodeRuntime": "目前執行環境:{runtime} {version}",
"SystemNode": "系統 Node.js",
"UlanziNode": "Ulanzi Studio Node",
diff --git a/package-lock.json b/package-lock.json
index e9f9590..1368a2b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "open-codex-micro",
- "version": "0.4.0",
+ "version": "0.5.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "open-codex-micro",
- "version": "0.4.0",
+ "version": "0.5.0",
"license": "MIT",
"devDependencies": {
"esbuild": "^0.25.12",
diff --git a/package.json b/package.json
index baad80d..b9cf5b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-codex-micro",
- "version": "0.4.0",
+ "version": "0.5.0",
"private": true,
"type": "module",
"description": "Control Codex Desktop from Ulanzi Studio through a local bridge",
@@ -12,12 +12,12 @@
"test": "node --test test/*_test.mjs",
"build:bridge": "node scripts/build-bridge.mjs",
"build:plugin": "npm --prefix integration/com.ulanzi.codexmicro.ulanziPlugin run build",
- "check:node": "node --check scripts/install.mjs && node --check scripts/install-plugin.mjs && node --check scripts/uninstall.mjs && node --check scripts/build-bridge.mjs && node --check src/bridge/thread-key.mjs && node --check src/bridge/codex-cdp.mjs && node --check src/bridge/server.mjs",
+ "check:node": "node --check scripts/install.mjs && node --check scripts/install-plugin.mjs && node --check scripts/launch.mjs && node --check scripts/uninstall.mjs && node --check scripts/build-bridge.mjs && node --check src/bridge/thread-key.mjs && node --check src/bridge/auth.mjs && node --check src/bridge/platform.mjs && node --check src/bridge/codex-cdp.mjs && node --check src/bridge/server.mjs",
"check:plugin": "npm --prefix integration/com.ulanzi.codexmicro.ulanziPlugin run check && npm --prefix integration/com.ulanzi.codexmicro.ulanziPlugin run smoke",
"check": "npm run test && npm run check:node && npm run build:bridge && npm run build:plugin && npm run check:plugin",
"setup": "node scripts/install.mjs",
"install:plugin": "node scripts/install-plugin.mjs",
- "bridge:start": "open \"$HOME/Applications/Codex Bridge.app\"",
+ "bridge:start": "node scripts/launch.mjs",
"uninstall": "node scripts/uninstall.mjs"
},
"devDependencies": {
diff --git a/scripts/install-plugin.mjs b/scripts/install-plugin.mjs
index 329b881..4e0e3e0 100644
--- a/scripts/install-plugin.mjs
+++ b/scripts/install-plugin.mjs
@@ -23,14 +23,9 @@ const localizationFiles = [
"es_ES.json"
];
const pluginName = basename(source);
-const pluginsRoot = join(
- homedir(),
- "Library",
- "Application Support",
- "Ulanzi",
- "UlanziDeck",
- "Plugins"
-);
+const pluginsRoot = process.platform === "win32"
+ ? join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "Ulanzi", "UlanziDeck", "Plugins")
+ : join(homedir(), "Library", "Application Support", "Ulanzi", "UlanziDeck", "Plugins");
const destination = join(pluginsRoot, pluginName);
const staging = join(pluginsRoot, `.${pluginName}.installing-${process.pid}`);
const backup = join(pluginsRoot, `.${pluginName}.backup-${process.pid}`);
@@ -59,6 +54,11 @@ for (const action of manifest.Actions || []) {
throw new Error(`Action PropertyInspectorPath is missing: ${action.PropertyInspectorPath}`);
}
}
+for (const banner of manifest.Banner || []) {
+ if (!await exists(join(source, banner))) {
+ throw new Error(`Manifest Banner is missing: ${banner}`);
+ }
+}
for (const relative of [
"installer/bridge.mjs",
"installer/CodexBridge.png",
@@ -88,6 +88,7 @@ for (const relative of [
"manifest.json",
...localizationFiles,
"assets/icons",
+ "assets/banners",
"dist",
"installer",
"property-inspector"
diff --git a/scripts/install.mjs b/scripts/install.mjs
index ffbf3f1..57ad3b5 100644
--- a/scripts/install.mjs
+++ b/scripts/install.mjs
@@ -1,197 +1,27 @@
-import {
- chmod,
- cp,
- mkdir,
- readFile,
- rm,
- writeFile
-} from "node:fs/promises";
import { execFileSync } from "node:child_process";
-import { homedir } from "node:os";
-import { join, resolve } from "node:path";
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { createBridgeInstaller } from "../integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js";
-const home = homedir();
-const uid = process.getuid();
+const pluginRoot = resolve("integration/com.ulanzi.codexmicro.ulanziPlugin");
const packageMetadata = JSON.parse(await readFile(resolve("package.json"), "utf8"));
-const releaseVersion = String(packageMetadata.version);
const start = !process.argv.slice(2).includes("--no-start");
-const appRoot = join(home, "Library", "Application Support", "OpenCodexMicro");
-const userApplications = join(home, "Applications");
-const bridgeApp = join(userApplications, "Codex Bridge.app");
-const agentsRoot = join(home, "Library", "LaunchAgents");
-const bridgeAgent = join(
- agentsRoot,
- "io.opencodexmicro.bridge.plist"
-);
-const xml = (value) => String(value)
- .replaceAll("&", "&")
- .replaceAll("<", "<")
- .replaceAll(">", ">")
- .replaceAll('"', """)
- .replaceAll("'", "'");
-await mkdir(appRoot, { recursive: true, mode: 0o700 });
-await chmod(appRoot, 0o700);
-await mkdir(agentsRoot, { recursive: true });
-await mkdir(userApplications, { recursive: true });
-execFileSync(process.execPath, [resolve("scripts/build-bridge.mjs")], {
+execFileSync(process.execPath, [resolve(pluginRoot, "build.mjs")], {
+ cwd: pluginRoot,
stdio: "inherit"
});
-await cp(resolve("dist/bridge.mjs"), join(appRoot, "bridge.mjs"));
-const bridgeContents = join(bridgeApp, "Contents");
-const bridgeMacOS = join(bridgeContents, "MacOS");
-const bridgeResources = join(bridgeContents, "Resources");
-const bridgeLicenses = join(bridgeResources, "licenses");
-const bridgeExecutable = join(bridgeMacOS, "Codex Bridge");
-const bridgeIcon = join(bridgeResources, "CodexBridge.icns");
-const iconset = join(appRoot, "CodexBridge.iconset");
-await rm(bridgeApp, { recursive: true, force: true });
-await rm(iconset, { recursive: true, force: true });
-await mkdir(bridgeMacOS, { recursive: true });
-await mkdir(bridgeResources, { recursive: true });
-await mkdir(bridgeLicenses, { recursive: true });
-await mkdir(iconset, { recursive: true });
-for (const notice of ["LICENSE", "NOTICE.md", "THIRD_PARTY_NOTICES.md"]) {
- await cp(resolve(notice), join(bridgeLicenses, notice));
- await cp(resolve(notice), join(appRoot, notice));
-}
-
-const iconSource = resolve("bridge/CodexBridge.png");
-for (const [name, size] of [
- ["icon_16x16.png", 16],
- ["icon_16x16@2x.png", 32],
- ["icon_32x32.png", 32],
- ["icon_32x32@2x.png", 64],
- ["icon_128x128.png", 128],
- ["icon_128x128@2x.png", 256],
- ["icon_256x256.png", 256],
- ["icon_256x256@2x.png", 512],
- ["icon_512x512.png", 512],
- ["icon_512x512@2x.png", 1024]
-]) {
- execFileSync("/usr/bin/sips", [
- "-z",
- String(size),
- String(size),
- iconSource,
- "--out",
- join(iconset, name)
- ], { stdio: "ignore" });
-}
-execFileSync("/usr/bin/iconutil", [
- "-c",
- "icns",
- iconset,
- "-o",
- bridgeIcon
-]);
-await rm(iconset, { recursive: true, force: true });
-
-const bridgeInfo = `
-
-
- CFBundleDisplayNameCodex Bridge
- CFBundleExecutableCodex Bridge
- CFBundleIconFileCodexBridge
- CFBundleIdentifierio.opencodexmicro.bridge
- CFBundleNameCodex Bridge
- CFBundlePackageTypeAPPL
- CFBundleShortVersionString${xml(releaseVersion)}
- CFBundleVersion${xml(releaseVersion)}
- LSMinimumSystemVersion13.0
- LSUIElement
- NSHighResolutionCapable
-
-`;
-await writeFile(join(bridgeContents, "Info.plist"), bridgeInfo);
-
-const bridgeLauncher = `#!/bin/zsh
-set -u
-unsetopt BG_NICE
-
-codex_binary="/Applications/ChatGPT.app/Contents/MacOS/ChatGPT"
-bridge_log="$HOME/Library/Logs/OpenCodexMicro-codex-bridge.log"
-
-if [[ ! -x "$codex_binary" ]]; then
- /usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex was not found at /Applications/ChatGPT.app." as critical'
- exit 1
-fi
-
-if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
- /usr/bin/osascript -e 'tell application id "com.openai.codex" to quit'
- for attempt in {1..80}; do
- /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1 || break
- /bin/sleep 0.1
- done
-fi
-
-if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
- /usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex did not quit, so bridge parameters could not be applied. Quit Codex and try again." as critical'
- exit 1
-fi
-
-/usr/bin/nohup "$codex_binary" \\
- --remote-debugging-address=127.0.0.1 \\
- --remote-debugging-port=9222 \\
- --remote-allow-origins=http://127.0.0.1:9222 \\
- >>"$bridge_log" 2>&1 &
-
-for attempt in {1..300}; do
- if /usr/bin/curl --noproxy '*' --silent --fail --max-time 0.2 \\
- http://127.0.0.1:9222/json/version >/dev/null 2>&1; then
- exit 0
- fi
- if ! /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then
- break
- fi
- /bin/sleep 0.1
-done
-
-/usr/bin/osascript -e 'display alert "Codex Bridge" message "Codex started, but the bridge endpoint is unavailable. Task keys will use the slower native fallback. Quit Codex and launch Codex Bridge again." as critical'
-exit 1
-`;
-await writeFile(bridgeExecutable, bridgeLauncher, { mode: 0o755 });
-await chmod(bridgeExecutable, 0o755);
-execFileSync("/usr/bin/codesign", [
- "--force",
- "--deep",
- "--sign",
- "-",
- bridgeApp
-], { stdio: "ignore" });
-console.log(`Codex Bridge app installed at: ${bridgeApp}`);
-
-const bridgePlist = `
-
-
- Labelio.opencodexmicro.bridge
- ProgramArguments
- ${xml(process.execPath)}
- ${xml(join(appRoot, "bridge.mjs"))}
-
- RunAtLoad
- KeepAlive
- ProcessTypeBackground
- ThrottleInterval2
- StandardOutPath${xml(join(appRoot, "bridge.log"))}
- StandardErrorPath${xml(join(appRoot, "bridge-error.log"))}
-
-`;
-await writeFile(bridgeAgent, bridgePlist, { mode: 0o644 });
-
-for (const agent of [bridgeAgent]) {
- try {
- execFileSync("/bin/launchctl", ["bootout", `gui/${uid}`, agent], {
- stdio: "ignore"
- });
- } catch {
- // The service may not be installed.
- }
-}
+const installer = createBridgeInstaller({
+ pluginRoot,
+ bridgeUrl: process.env.CODEX_BRIDGE_URL || "http://127.0.0.1:17373",
+ version: String(packageMetadata.version)
+});
+const installed = await installer.install();
+console.log(`Codex Bridge installed at: ${installed.appPath}`);
if (start) {
- execFileSync("/bin/launchctl", ["bootstrap", `gui/${uid}`, bridgeAgent]);
- console.log("Codex Bridge installed and started.");
+ await installer.launch();
+ console.log("Codex Bridge and Codex Desktop launch requested.");
} else {
- console.log("Codex Bridge installed; daemon start skipped.");
+ console.log("Codex Bridge installed; launch skipped.");
}
diff --git a/scripts/launch.mjs b/scripts/launch.mjs
new file mode 100644
index 0000000..b0bbfc3
--- /dev/null
+++ b/scripts/launch.mjs
@@ -0,0 +1,13 @@
+import { readFile } from "node:fs/promises";
+import { resolve } from "node:path";
+import { createBridgeInstaller } from "../integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js";
+
+const pluginRoot = resolve("integration/com.ulanzi.codexmicro.ulanziPlugin");
+const packageMetadata = JSON.parse(await readFile(resolve("package.json"), "utf8"));
+const installer = createBridgeInstaller({
+ pluginRoot,
+ bridgeUrl: process.env.CODEX_BRIDGE_URL || "http://127.0.0.1:17373",
+ version: String(packageMetadata.version)
+});
+await installer.launch();
+console.log("Codex Bridge and Codex Desktop launch requested.");
diff --git a/scripts/uninstall.mjs b/scripts/uninstall.mjs
index f2e8761..c2a5cc6 100644
--- a/scripts/uninstall.mjs
+++ b/scripts/uninstall.mjs
@@ -1,38 +1,19 @@
-import { mkdir, rm } from "node:fs/promises";
-import { execFileSync } from "node:child_process";
+import { readFile, rm } from "node:fs/promises";
import { homedir } from "node:os";
-import { join } from "node:path";
+import { join, resolve } from "node:path";
+import { createBridgeInstaller } from "../integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js";
-const home = homedir();
-const uid = process.getuid();
-const appRoot = join(home, "Library", "Application Support", "OpenCodexMicro");
-const bridgeApp = join(home, "Applications", "Codex Bridge.app");
-const agentsRoot = join(home, "Library", "LaunchAgents");
-const plugin = join(
- home,
- "Library",
- "Application Support",
- "Ulanzi",
- "UlanziDeck",
- "Plugins",
- "com.ulanzi.codexmicro.ulanziPlugin"
-);
-await mkdir(agentsRoot, { recursive: true });
-const agents = [
- join(agentsRoot, "io.opencodexmicro.bridge.plist")
-];
+const pluginRoot = resolve("integration/com.ulanzi.codexmicro.ulanziPlugin");
+const packageMetadata = JSON.parse(await readFile(resolve("package.json"), "utf8"));
+const installer = createBridgeInstaller({
+ pluginRoot,
+ bridgeUrl: process.env.CODEX_BRIDGE_URL || "http://127.0.0.1:17373",
+ version: String(packageMetadata.version)
+});
+await installer.uninstall();
-for (const agent of agents) {
- try {
- execFileSync("/bin/launchctl", ["bootout", `gui/${uid}`, agent], {
- stdio: "ignore"
- });
- } catch {
- // Already stopped or never installed.
- }
- await rm(agent, { force: true });
-}
-await rm(appRoot, { recursive: true, force: true });
-await rm(bridgeApp, { recursive: true, force: true });
-await rm(plugin, { recursive: true, force: true });
+const pluginsRoot = process.platform === "win32"
+ ? join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "Ulanzi", "UlanziDeck", "Plugins")
+ : join(homedir(), "Library", "Application Support", "Ulanzi", "UlanziDeck", "Plugins");
+await rm(join(pluginsRoot, "com.ulanzi.codexmicro.ulanziPlugin"), { recursive: true, force: true });
console.log("OpenCodexMicro removed.");
diff --git a/skills/install-ulanzi-studio-plugin/SKILL.md b/skills/install-ulanzi-studio-plugin/SKILL.md
index 9e9cb49..6a2859c 100644
--- a/skills/install-ulanzi-studio-plugin/SKILL.md
+++ b/skills/install-ulanzi-studio-plugin/SKILL.md
@@ -1,6 +1,6 @@
---
name: install-ulanzi-studio-plugin
-description: Install, update, verify, or diagnose the repository's prebuilt Codex Micro .ulanziPlugin directory in Ulanzi Studio on macOS. Use for local Ulanzi Studio plugin installation, not plugin development or Codex Bridge.app setup.
+description: Install, update, verify, or diagnose the prebuilt Codex Micro .ulanziPlugin directory in Ulanzi Studio on Windows or macOS. Use for local plugin installation, not plugin development or Codex Bridge setup.
---
# Install the Ulanzi Studio Plugin
@@ -13,7 +13,7 @@ Resolve the project root as two directories above this file. Require
1. Inspect `git status --short`, the plugin `manifest.json`, and its committed
`CodePath`. Preserve unrelated worktree changes.
-2. Verify macOS, Node.js 20+, and `/Applications/Ulanzi Studio.app`.
+2. Verify Windows 10+ or macOS 13+, Node.js 20+, and Ulanzi Studio 3.0.1+.
3. If Ulanzi Studio is running, ask the user to quit it before replacing the
loaded plugin. Do not terminate the app without permission.
4. From the project root, run:
@@ -25,17 +25,10 @@ Resolve the project root as two directories above this file. Require
The installer validates the prebuilt manifest entry point, then atomically
replaces the installed directory. It must not rebuild the plugin during
installation.
-5. Verify:
-
- ```bash
- plugin="$HOME/Library/Application Support/Ulanzi/UlanziDeck/Plugins/com.ulanzi.codexmicro.ulanziPlugin"
- test -f "$plugin/manifest.json"
- test -f "$plugin/dist/app.js"
- test -f "$plugin/dist/package.json"
- for locale in en zh_CN zh_HK ja_JP de_DE ko_KR pt_PT es_ES; do
- test -f "$plugin/$locale.json"
- done
- ```
+5. Verify the platform-specific destination: `%APPDATA%\Ulanzi\UlanziDeck\Plugins`
+ on Windows or `~/Library/Application Support/Ulanzi/UlanziDeck/Plugins` on
+ macOS. Confirm `manifest.json`, `dist/app.js`, `dist/package.json`, all eight
+ locale files, icons, banners, Property Inspector, and installer resources.
6. Ask the user to reopen Ulanzi Studio and confirm that the **Codex Micro**
category and actions appear. If actions show offline, verify the Bridge with
diff --git a/skills/setup-codex-bridge/SKILL.md b/skills/setup-codex-bridge/SKILL.md
index e43ea6d..335cb0a 100644
--- a/skills/setup-codex-bridge/SKILL.md
+++ b/skills/setup-codex-bridge/SKILL.md
@@ -1,6 +1,6 @@
---
name: setup-codex-bridge
-description: Install, update, verify, diagnose, or remove OpenCodexMicro's Codex Bridge.app and loopback sidecar on macOS. Use when the request concerns Bridge setup or its local LaunchAgent, not Ulanzi Studio plugin installation.
+description: Install, update, verify, diagnose, or remove OpenCodexMicro's local Codex Bridge on Windows or macOS. Use for Bridge setup and lifecycle, not Ulanzi Studio plugin installation.
---
# Setup Codex Bridge
@@ -13,8 +13,9 @@ OpenCodexMicro checkout.
1. Inspect `git status --short`, `package.json`, and the current Bridge state.
Preserve unrelated worktree changes.
-2. Verify macOS, Node.js 20+, and
- `/Applications/ChatGPT.app/Contents/MacOS/ChatGPT`.
+2. Verify Windows 10+ or macOS 13+, Node.js 20+, and an installed Codex Desktop.
+ On Windows, discover `OpenAI.Codex` or `OpenAI.CodexBeta` through Appx package
+ metadata; never pin a versioned `WindowsApps` path.
3. Install or update the Bridge:
```bash
@@ -24,24 +25,24 @@ OpenCodexMicro checkout.
Use `npm run setup -- --no-start` only when the user explicitly wants the
sidecar installed but stopped.
-4. Verify the installed artifacts and service:
+4. Verify the installed artifacts and service. Windows stores Bridge data under
+ `%LOCALAPPDATA%\OpenCodexMicro`; macOS uses
+ `~/Library/Application Support/OpenCodexMicro` and a user LaunchAgent.
```bash
- test -x "$HOME/Applications/Codex Bridge.app/Contents/MacOS/Codex Bridge"
- launchctl print "gui/$(id -u)/io.opencodexmicro.bridge"
- curl --fail http://127.0.0.1:17373/health
+ curl --fail http://127.0.0.1:17373/health
```
-5. Do not launch `Codex Bridge.app` merely to verify installation. Launching it
- quits any running Codex instance; do so only when the user asks and has had
+5. Do not launch Codex merely to verify installation. Applying CDP arguments may
+ restart the selected Codex channel; do so only when the user asks and has had
a chance to save current work.
6. When launched, verify `/state` and report whether `connected` is true. If it
is false, inspect `bridge-error.log` and confirm Codex was opened through the
- wrapper app.
+ Bridge launcher.
## Boundaries
- Keep CDP at `127.0.0.1:9222` and the sidecar at `127.0.0.1:17373`.
-- Do not modify Codex shortcuts or Ulanzi Studio plugins in this skill.
+- Do not create version-pinned Codex shortcuts or modify Ulanzi Studio plugins in this skill.
- Run `npm run uninstall` only when the user explicitly requests removal; it
also removes the installed Codex Micro plugin.
diff --git a/skills/setup-codex-bridge/agents/openai.yaml b/skills/setup-codex-bridge/agents/openai.yaml
index f9ded0a..1eae316 100644
--- a/skills/setup-codex-bridge/agents/openai.yaml
+++ b/skills/setup-codex-bridge/agents/openai.yaml
@@ -1,4 +1,4 @@
interface:
display_name: "Setup Codex Bridge"
- short_description: "Install and verify Codex Bridge.app on macOS"
- default_prompt: "Use $setup-codex-bridge to install and verify Codex Bridge.app."
+ short_description: "Install Codex Bridge on Windows or macOS"
+ default_prompt: "Use $setup-codex-bridge to install and verify the local Codex Bridge."
diff --git a/src/bridge/auth.mjs b/src/bridge/auth.mjs
new file mode 100644
index 0000000..51ed5f9
--- /dev/null
+++ b/src/bridge/auth.mjs
@@ -0,0 +1,9 @@
+import { timingSafeEqual } from "node:crypto";
+
+export function bridgeRequestAuthorized(token, authorizationHeader) {
+ if (!token) return false;
+ const candidate = String(authorizationHeader || "").replace(/^Bearer\s+/i, "");
+ const actual = Buffer.from(token);
+ const supplied = Buffer.from(candidate);
+ return actual.length === supplied.length && timingSafeEqual(actual, supplied);
+}
diff --git a/src/bridge/codex-cdp.mjs b/src/bridge/codex-cdp.mjs
index b6d6cc1..32a0587 100644
--- a/src/bridge/codex-cdp.mjs
+++ b/src/bridge/codex-cdp.mjs
@@ -1,9 +1,7 @@
-import { execFile } from "node:child_process";
-import { promisify } from "node:util";
+import { discoverDebugPort, fetchJson } from "./platform.mjs";
import WebSocket from "ws";
import { localThreadKey } from "./thread-key.mjs";
-const execFileAsync = promisify(execFile);
const USAGE_REFRESH_MS = Math.max(
15000,
Number(process.env.CODEX_KEYBOARD_USAGE_REFRESH_SECONDS || 600) * 1000
@@ -347,26 +345,6 @@ function selectMainTarget(targets) {
}) ?? pages.find((target) => !/avatar-overlay|composition-surface/i.test(target.url || ""));
}
-async function fetchJson(url, timeout = 1200) {
- const response = await fetch(url, { signal: AbortSignal.timeout(timeout) });
- if (!response.ok) throw new Error(`${url} returned HTTP ${response.status}`);
- return response.json();
-}
-
-async function discoverDebugPort() {
- const { stdout } = await execFileAsync("/bin/ps", ["-axo", "command="], { timeout: 4000 });
- for (const line of stdout.split("\n")) {
- if (!line.includes("--remote-debugging-address=127.0.0.1")) continue;
- const port = Number(line.match(/--remote-debugging-port(?:=|\s+)(\d+)/)?.[1]);
- if (!Number.isInteger(port)) continue;
- try {
- await fetchJson(`http://127.0.0.1:${port}/json/version`, 500);
- return port;
- } catch {}
- }
- throw new Error("Codex is not running with the local debug bridge");
-}
-
export class CodexCdpClient {
socket = null;
nextId = 0;
diff --git a/src/bridge/platform.mjs b/src/bridge/platform.mjs
new file mode 100644
index 0000000..958f31a
--- /dev/null
+++ b/src/bridge/platform.mjs
@@ -0,0 +1,197 @@
+import { execFile, spawn } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+
+export const CDP_HOST = "127.0.0.1";
+export const DEFAULT_CDP_PORT = 9222;
+export const CDP_ARGUMENTS = Object.freeze([
+ `--remote-debugging-address=${CDP_HOST}`,
+ `--remote-debugging-port=${DEFAULT_CDP_PORT}`,
+ `--remote-allow-origins=http://${CDP_HOST}:${DEFAULT_CDP_PORT}`
+]);
+
+const WINDOWS_PROCESS_COMMAND = String.raw`
+$ErrorActionPreference = 'Stop'
+Get-CimInstance Win32_Process |
+ Where-Object { $_.CommandLine -like '*--remote-debugging-address=127.0.0.1*' } |
+ ForEach-Object { $_.CommandLine }
+`;
+
+const WINDOWS_PACKAGE_COMMAND = String.raw`
+$ErrorActionPreference = 'Stop'
+$rows = foreach ($name in @('OpenAI.Codex', 'OpenAI.CodexBeta')) {
+ $package = Get-AppxPackage -Name $name | Sort-Object Version -Descending | Select-Object -First 1
+ if (-not $package) { continue }
+ $app = Join-Path $package.InstallLocation 'app'
+ $names = if ($name -eq 'OpenAI.CodexBeta') {
+ @('ChatGPT (Beta).exe', 'Codex (Beta).exe', 'ChatGPT.exe')
+ } else {
+ @('ChatGPT.exe', 'Codex.exe')
+ }
+ foreach ($file in $names) {
+ $candidate = Join-Path $app $file
+ if (Test-Path -LiteralPath $candidate) {
+ [pscustomobject]@{
+ channel = if ($name -eq 'OpenAI.CodexBeta') { 'beta' } else { 'stable' }
+ packageName = $name
+ packageFullName = $package.PackageFullName
+ executable = $candidate
+ }
+ break
+ }
+ }
+}
+@($rows) | ConvertTo-Json -Compress
+`;
+
+const WINDOWS_FOCUS_COMMAND = String.raw`
+$ErrorActionPreference = 'Stop'
+Add-Type @'
+using System;
+using System.Runtime.InteropServices;
+public static class CodexWindow {
+ [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
+ [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
+}
+'@
+$debugPids = @(Get-CimInstance Win32_Process |
+ Where-Object { $_.CommandLine -like '*--remote-debugging-address=127.0.0.1*' -and $_.CommandLine -notlike '*--type=*' } |
+ Select-Object -ExpandProperty ProcessId)
+$candidate = Get-Process |
+ Where-Object { $_.MainWindowHandle -ne 0 -and ($_.ProcessName -like 'ChatGPT*' -or $_.ProcessName -like 'Codex*') } |
+ Sort-Object @{ Expression = { if ($debugPids -contains $_.Id) { 0 } else { 1 } } }, StartTime |
+ Select-Object -First 1
+if (-not $candidate) { throw 'A Codex Desktop window was not found.' }
+[CodexWindow]::ShowWindowAsync($candidate.MainWindowHandle, 9) | Out-Null
+if (-not [CodexWindow]::SetForegroundWindow($candidate.MainWindowHandle)) {
+ $shell = New-Object -ComObject WScript.Shell
+ if (-not $shell.AppActivate($candidate.Id)) {
+ throw 'Windows did not allow Codex Desktop to receive focus.'
+ }
+}
+`;
+
+const WINDOWS_STOP_EXECUTABLE_COMMAND = String.raw`
+$ErrorActionPreference = 'Stop'
+$target = $env:CODEX_BRIDGE_TARGET_EXECUTABLE
+$processes = @(Get-CimInstance Win32_Process |
+ Where-Object {
+ $_.ExecutablePath -eq $target -and
+ $_.CommandLine -notlike '*--type=*' -and
+ $_.CommandLine -notlike '*crashpad-handler*'
+ })
+foreach ($process in $processes) { Stop-Process -Id $process.ProcessId -ErrorAction Stop }
+foreach ($process in $processes) { Wait-Process -Id $process.ProcessId -Timeout 8 -ErrorAction SilentlyContinue }
+`;
+
+function powershellArgs(command, extra = []) {
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command, ...extra];
+}
+
+export function debugPortsFromCommandLines(text) {
+ const ports = [];
+ for (const line of String(text || "").split(/\r?\n/)) {
+ if (!line.includes("--remote-debugging-address=127.0.0.1")) continue;
+ const port = Number(line.match(/--remote-debugging-port(?:=|\s+)(\d+)/)?.[1]);
+ if (Number.isInteger(port) && port > 0 && port <= 65535) ports.push(port);
+ }
+ return [...new Set(ports)];
+}
+
+export async function fetchJson(url, timeout = 1200, fetchImpl = fetch) {
+ const response = await fetchImpl(url, { signal: AbortSignal.timeout(timeout) });
+ if (!response.ok) throw new Error(`${url} returned HTTP ${response.status}`);
+ return response.json();
+}
+
+async function processCommandLines(platform, execute) {
+ if (platform === "win32") {
+ return (await execute("powershell.exe", powershellArgs(WINDOWS_PROCESS_COMMAND), { timeout: 4000 })).stdout;
+ }
+ if (platform === "darwin") {
+ return (await execute("/bin/ps", ["-axo", "command="], { timeout: 4000 })).stdout;
+ }
+ return "";
+}
+
+export async function discoverDebugPort({
+ platform = process.platform,
+ execute = execFileAsync,
+ fetchImpl = fetch,
+ preferredPort = DEFAULT_CDP_PORT
+} = {}) {
+ try {
+ await fetchJson(`http://${CDP_HOST}:${preferredPort}/json/version`, 500, fetchImpl);
+ return preferredPort;
+ } catch {}
+ const candidates = [];
+ try {
+ candidates.push(...debugPortsFromCommandLines(await processCommandLines(platform, execute)));
+ } catch {
+ // Direct loopback probing remains available when process inspection is denied.
+ }
+ for (const port of [...new Set(candidates)].filter((port) => port !== preferredPort)) {
+ try {
+ await fetchJson(`http://${CDP_HOST}:${port}/json/version`, 500, fetchImpl);
+ return port;
+ } catch {}
+ }
+ throw new Error("Codex is not running with the local debug bridge");
+}
+
+export async function discoverWindowsCodexExecutables({ execute = execFileAsync } = {}) {
+ const { stdout = "" } = await execute(
+ "powershell.exe",
+ powershellArgs(WINDOWS_PACKAGE_COMMAND),
+ { timeout: 8000 }
+ );
+ if (!String(stdout).trim()) return [];
+ const parsed = JSON.parse(String(stdout));
+ const rows = Array.isArray(parsed) ? parsed : [parsed];
+ return rows.filter((row) =>
+ ["stable", "beta"].includes(row?.channel) &&
+ typeof row?.executable === "string" && row.executable.toLowerCase().endsWith(".exe")
+ );
+}
+
+export async function focusCodex({ platform = process.platform, execute = execFileAsync } = {}) {
+ if (platform === "win32") {
+ await execute("powershell.exe", powershellArgs(WINDOWS_FOCUS_COMMAND), { timeout: 5000 });
+ return;
+ }
+ if (platform === "darwin") {
+ await execute("/usr/bin/open", ["-b", "com.openai.codex"], { timeout: 3000 });
+ return;
+ }
+ throw new Error(`Codex Desktop focus is not supported on ${platform}`);
+}
+
+export async function launchWindowsCodex({
+ channel = "stable",
+ execute = execFileAsync,
+ spawnProcess = spawn
+} = {}) {
+ const installations = await discoverWindowsCodexExecutables({ execute });
+ const selected = installations.find((item) => item.channel === channel)
+ ?? installations.find((item) => item.channel === "stable")
+ ?? installations[0];
+ if (!selected) {
+ throw new Error("Codex Desktop Stable or Beta was not found in the current Windows account.");
+ }
+ await execute(
+ "powershell.exe",
+ powershellArgs(WINDOWS_STOP_EXECUTABLE_COMMAND),
+ {
+ timeout: 10000,
+ env: { ...process.env, CODEX_BRIDGE_TARGET_EXECUTABLE: selected.executable }
+ }
+ );
+ const child = spawnProcess(selected.executable, [...CDP_ARGUMENTS], {
+ detached: true,
+ stdio: "ignore",
+ windowsHide: false
+ });
+ child.unref?.();
+ return selected;
+}
diff --git a/src/bridge/server.mjs b/src/bridge/server.mjs
index a0b7056..ea8b005 100644
--- a/src/bridge/server.mjs
+++ b/src/bridge/server.mjs
@@ -1,10 +1,12 @@
import { createServer } from "node:http";
-import { execFile } from "node:child_process";
-import { promisify } from "node:util";
+import { readFileSync } from "node:fs";
+import { homedir } from "node:os";
+import { join } from "node:path";
import { CodexCdpClient } from "./codex-cdp.mjs";
+import { bridgeRequestAuthorized } from "./auth.mjs";
+import { focusCodex } from "./platform.mjs";
import { decodeThreadPathSegment } from "./thread-key.mjs";
-const execFileAsync = promisify(execFile);
const HOST = "127.0.0.1";
const PORT = Number(process.env.CODEX_KEYBOARD_PORT || 17373);
const configuredRefreshMs = Number(process.env.CODEX_KEYBOARD_REFRESH_MS || 500);
@@ -12,6 +14,13 @@ const REFRESH_MS = Number.isFinite(configuredRefreshMs)
? Math.max(250, configuredRefreshMs)
: 500;
const client = new CodexCdpClient();
+const dataRoot = process.env.CODEX_BRIDGE_DATA_ROOT || (process.platform === "win32"
+ ? join(process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local"), "OpenCodexMicro")
+ : join(homedir(), "Library", "Application Support", "OpenCodexMicro"));
+const bridgeToken = process.env.CODEX_BRIDGE_TOKEN || (() => {
+ try { return readFileSync(join(dataRoot, "bridge-token"), "utf8").trim(); }
+ catch { return ""; }
+})();
let cached = {
connected: false,
slots: Array.from({ length: 6 }, (_, id) => ({
@@ -23,10 +32,8 @@ let cached = {
let refreshPromise = null;
let nextReconnectAt = 0;
-async function focusCodex() {
- await execFileAsync("/usr/bin/open", ["-b", "com.openai.codex"], {
- timeout: 3000
- });
+function authorized(request) {
+ return bridgeRequestAuthorized(bridgeToken, request.headers.authorization);
}
async function refresh(force = false) {
@@ -60,6 +67,9 @@ function json(response, status, body) {
const server = createServer(async (request, response) => {
const url = new URL(request.url || "/", `http://${HOST}:${PORT}`);
+ if (request.method === "POST" && !authorized(request)) {
+ return json(response, 401, { ok: false, error: "Bridge authorization required" });
+ }
if (request.method === "GET" && url.pathname === "/health") {
await refresh(true);
return json(response, 200, { ok: true, codexConnected: cached.connected, updatedAt: cached.updatedAt });
@@ -69,7 +79,7 @@ const server = createServer(async (request, response) => {
}
if (request.method === "POST" && url.pathname === "/focus") {
try {
- await focusCodex();
+ await focusCodex();
return json(response, 200, { ok: true });
} catch (error) {
return json(response, 503, { ok: false, error: error.message });
diff --git a/test/bridge-installer_test.mjs b/test/bridge-installer_test.mjs
index e663abb..04b82f1 100644
--- a/test/bridge-installer_test.mjs
+++ b/test/bridge-installer_test.mjs
@@ -6,6 +6,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import {
+ bridgeDataRoot,
createBridgeInstaller,
selectBridgeNodeRuntime
} from "../integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js";
@@ -111,3 +112,89 @@ test("bundled installer falls back when the system Node is older than version 20
await rm(home, { recursive: true, force: true });
}
});
+
+test("Windows installer uses LocalAppData, a capability token, and user-level processes", async () => {
+ const home = await mkdtemp(join(tmpdir(), "codex-micro-windows-installer-"));
+ const localAppData = join(home, "LocalAppData");
+ const bin = join(home, "bin");
+ const systemNode = join(bin, "node.exe");
+ const codexExecutable = join(home, "WindowsApps", "OpenAI.Codex_1.2.3.4_x64", "app", "ChatGPT.exe");
+ await mkdir(bin, { recursive: true });
+ await writeFile(systemNode, "fake node");
+ const commands = [];
+ const children = [];
+ const execute = async (command, args, options) => {
+ commands.push([command, args, options]);
+ if (args[0] === "--version") return { stdout: "v22.14.0\n", stderr: "" };
+ if (command === "powershell.exe" && args.join(" ").includes("Get-AppxPackage")) {
+ return {
+ stdout: JSON.stringify({
+ channel: "stable",
+ packageName: "OpenAI.Codex",
+ packageFullName: "OpenAI.Codex_1.2.3.4_x64__test",
+ executable: codexExecutable
+ }),
+ stderr: ""
+ };
+ }
+ return { stdout: "", stderr: "" };
+ };
+ const spawnProcess = (command, args, options) => {
+ children.push({ command, args, options });
+ return { unref() {} };
+ };
+ const installer = createBridgeInstaller({
+ pluginRoot,
+ bridgeUrl: "http://127.0.0.1:59999",
+ version: "9.8.7",
+ home,
+ localAppData,
+ platform: "win32",
+ nodeExecutable: process.execPath,
+ environmentPath: bin,
+ execute,
+ spawnProcess,
+ fetchImpl: async () => { throw new Error("offline fixture"); }
+ });
+
+ try {
+ assert.equal(
+ bridgeDataRoot({ platform: "win32", home, localAppData }),
+ join(localAppData, "OpenCodexMicro")
+ );
+ const installed = await installer.install();
+ assert.equal(installed.supported, true);
+ assert.equal(installed.installed, true);
+ const dataRoot = join(localAppData, "OpenCodexMicro");
+ await access(join(dataRoot, "bridge.mjs"));
+ const token = (await readFile(join(dataRoot, "bridge-token"), "utf8")).trim();
+ assert.match(token, /^[A-Za-z0-9_-]{40,}$/);
+ assert.match((await installer.authorizationHeaders()).Authorization, /^Bearer /);
+ assert.equal(children[0].command, await realpath(systemNode));
+ assert.equal(children[0].options.windowsHide, true);
+ assert.equal(children[0].options.env.CODEX_BRIDGE_TOKEN, token);
+
+ await installer.launch();
+ assert.equal(children[1].command, codexExecutable);
+ assert.deepEqual(children[1].args, [
+ "--remote-debugging-address=127.0.0.1",
+ "--remote-debugging-port=9222",
+ "--remote-allow-origins=http://127.0.0.1:9222"
+ ]);
+ assert.equal(children[1].options.windowsHide, false);
+ assert.ok(commands.some(([command, args]) =>
+ command === "powershell.exe" && args.join(" ").includes("Stop-Process")
+ ));
+ assert.ok(commands.some(([command, args, options]) =>
+ command === "powershell.exe" &&
+ args.join(" ").includes("Wait-Process") &&
+ options?.env?.CODEX_BRIDGE_TARGET_EXECUTABLE === codexExecutable
+ ));
+
+ const uninstalled = await installer.uninstall();
+ assert.equal(uninstalled.installed, false);
+ await assert.rejects(access(dataRoot));
+ } finally {
+ await rm(home, { recursive: true, force: true });
+ }
+});
diff --git a/test/bridge_test.mjs b/test/bridge_test.mjs
index 78c8e72..9f0eb3f 100644
--- a/test/bridge_test.mjs
+++ b/test/bridge_test.mjs
@@ -12,9 +12,55 @@ import {
composerSteerExpression,
rendererActionExpression
} from "../src/bridge/codex-cdp.mjs";
+import {
+ debugPortsFromCommandLines,
+ discoverDebugPort,
+ discoverWindowsCodexExecutables
+} from "../src/bridge/platform.mjs";
+import { bridgeRequestAuthorized } from "../src/bridge/auth.mjs";
const UUID = "f6805b8a-332a-43a0-a118-52d3e59542f6";
+test("Bridge write authorization fails closed and accepts only the installed token", () => {
+ const token = "fixture-local-capability-token";
+ assert.equal(bridgeRequestAuthorized("", `Bearer ${token}`), false);
+ assert.equal(bridgeRequestAuthorized(token, ""), false);
+ assert.equal(bridgeRequestAuthorized(token, "Bearer wrong-token"), false);
+ assert.equal(bridgeRequestAuthorized(token, `Bearer ${token}`), true);
+});
+
+test("CDP discovery probes the fixed loopback port before platform process inspection", async () => {
+ const requests = [];
+ let processInspections = 0;
+ const port = await discoverDebugPort({
+ platform: "win32",
+ execute: async () => { processInspections += 1; throw new Error("process inspection denied"); },
+ fetchImpl: async (url) => {
+ requests.push(url);
+ return { ok: true, json: async () => ({ Browser: "Chrome" }) };
+ }
+ });
+ assert.equal(port, 9222);
+ assert.deepEqual(requests, ["http://127.0.0.1:9222/json/version"]);
+ assert.equal(processInspections, 0);
+});
+
+test("Windows CDP and Appx discovery accept Stable and Beta process shapes", async () => {
+ assert.deepEqual(debugPortsFromCommandLines([
+ '"ChatGPT.exe" --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222',
+ '"ChatGPT (Beta).exe" --remote-debugging-address=127.0.0.1 --remote-debugging-port 9333'
+ ].join("\n")), [9222, 9333]);
+ const installs = await discoverWindowsCodexExecutables({
+ execute: async () => ({
+ stdout: JSON.stringify([
+ { channel: "stable", packageName: "OpenAI.Codex", executable: "C:\\WindowsApps\\Stable\\app\\ChatGPT.exe" },
+ { channel: "beta", packageName: "OpenAI.CodexBeta", executable: "C:\\WindowsApps\\Beta\\app\\ChatGPT (Beta).exe" }
+ ])
+ })
+ });
+ assert.deepEqual(installs.map((item) => item.channel), ["stable", "beta"]);
+});
+
test("accepts formal and explicit temporary Codex thread ids", () => {
assert.equal(validateThreadId(UUID), UUID);
assert.equal(
From c617849b60af461c17e1f4d84dd712d46ddbcc04 Mon Sep 17 00:00:00 2001
From: yyf <2358929807@qq.com>
Date: Fri, 21 Aug 2026 12:57:46 +0800
Subject: [PATCH 2/8] fix: hide Windows PowerShell helper windows
---
.../plugin/bridge-installer.js | 1 +
src/bridge/platform.mjs | 22 +++++++---
test/bridge-installer_test.mjs | 3 ++
test/bridge_test.mjs | 43 ++++++++++++++++---
4 files changed, 57 insertions(+), 12 deletions(-)
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
index 55ecd07..d8d5117 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
@@ -387,6 +387,7 @@ if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then exit 1; fi
try {
await execute("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", stopScript], {
timeout: 5000,
+ windowsHide: true,
env: { ...process.env, CODEX_BRIDGE_TARGET_RUNTIME: bridgeRuntime }
});
} catch {}
diff --git a/src/bridge/platform.mjs b/src/bridge/platform.mjs
index 958f31a..ce14dc6 100644
--- a/src/bridge/platform.mjs
+++ b/src/bridge/platform.mjs
@@ -89,6 +89,10 @@ function powershellArgs(command, extra = []) {
return ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command, ...extra];
}
+function powershellOptions(options = {}) {
+ return { ...options, windowsHide: true };
+}
+
export function debugPortsFromCommandLines(text) {
const ports = [];
for (const line of String(text || "").split(/\r?\n/)) {
@@ -107,7 +111,11 @@ export async function fetchJson(url, timeout = 1200, fetchImpl = fetch) {
async function processCommandLines(platform, execute) {
if (platform === "win32") {
- return (await execute("powershell.exe", powershellArgs(WINDOWS_PROCESS_COMMAND), { timeout: 4000 })).stdout;
+ return (await execute(
+ "powershell.exe",
+ powershellArgs(WINDOWS_PROCESS_COMMAND),
+ powershellOptions({ timeout: 4000 })
+ )).stdout;
}
if (platform === "darwin") {
return (await execute("/bin/ps", ["-axo", "command="], { timeout: 4000 })).stdout;
@@ -144,7 +152,7 @@ export async function discoverWindowsCodexExecutables({ execute = execFileAsync
const { stdout = "" } = await execute(
"powershell.exe",
powershellArgs(WINDOWS_PACKAGE_COMMAND),
- { timeout: 8000 }
+ powershellOptions({ timeout: 8000 })
);
if (!String(stdout).trim()) return [];
const parsed = JSON.parse(String(stdout));
@@ -157,7 +165,11 @@ export async function discoverWindowsCodexExecutables({ execute = execFileAsync
export async function focusCodex({ platform = process.platform, execute = execFileAsync } = {}) {
if (platform === "win32") {
- await execute("powershell.exe", powershellArgs(WINDOWS_FOCUS_COMMAND), { timeout: 5000 });
+ await execute(
+ "powershell.exe",
+ powershellArgs(WINDOWS_FOCUS_COMMAND),
+ powershellOptions({ timeout: 5000 })
+ );
return;
}
if (platform === "darwin") {
@@ -182,10 +194,10 @@ export async function launchWindowsCodex({
await execute(
"powershell.exe",
powershellArgs(WINDOWS_STOP_EXECUTABLE_COMMAND),
- {
+ powershellOptions({
timeout: 10000,
env: { ...process.env, CODEX_BRIDGE_TARGET_EXECUTABLE: selected.executable }
- }
+ })
);
const child = spawnProcess(selected.executable, [...CDP_ARGUMENTS], {
detached: true,
diff --git a/test/bridge-installer_test.mjs b/test/bridge-installer_test.mjs
index 04b82f1..43e4de6 100644
--- a/test/bridge-installer_test.mjs
+++ b/test/bridge-installer_test.mjs
@@ -194,6 +194,9 @@ test("Windows installer uses LocalAppData, a capability token, and user-level pr
const uninstalled = await installer.uninstall();
assert.equal(uninstalled.installed, false);
await assert.rejects(access(dataRoot));
+ const powershellCommands = commands.filter(([command]) => command === "powershell.exe");
+ assert.ok(powershellCommands.length >= 4);
+ assert.ok(powershellCommands.every(([, , options]) => options?.windowsHide === true));
} finally {
await rm(home, { recursive: true, force: true });
}
diff --git a/test/bridge_test.mjs b/test/bridge_test.mjs
index 9f0eb3f..b44a3c7 100644
--- a/test/bridge_test.mjs
+++ b/test/bridge_test.mjs
@@ -15,7 +15,8 @@ import {
import {
debugPortsFromCommandLines,
discoverDebugPort,
- discoverWindowsCodexExecutables
+ discoverWindowsCodexExecutables,
+ focusCodex
} from "../src/bridge/platform.mjs";
import { bridgeRequestAuthorized } from "../src/bridge/auth.mjs";
@@ -50,15 +51,43 @@ test("Windows CDP and Appx discovery accept Stable and Beta process shapes", asy
'"ChatGPT.exe" --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222',
'"ChatGPT (Beta).exe" --remote-debugging-address=127.0.0.1 --remote-debugging-port 9333'
].join("\n")), [9222, 9333]);
+ let appxOptions;
const installs = await discoverWindowsCodexExecutables({
- execute: async () => ({
- stdout: JSON.stringify([
- { channel: "stable", packageName: "OpenAI.Codex", executable: "C:\\WindowsApps\\Stable\\app\\ChatGPT.exe" },
- { channel: "beta", packageName: "OpenAI.CodexBeta", executable: "C:\\WindowsApps\\Beta\\app\\ChatGPT (Beta).exe" }
- ])
- })
+ execute: async (_command, _args, options) => {
+ appxOptions = options;
+ return {
+ stdout: JSON.stringify([
+ { channel: "stable", packageName: "OpenAI.Codex", executable: "C:\\WindowsApps\\Stable\\app\\ChatGPT.exe" },
+ { channel: "beta", packageName: "OpenAI.CodexBeta", executable: "C:\\WindowsApps\\Beta\\app\\ChatGPT (Beta).exe" }
+ ])
+ };
+ }
});
assert.deepEqual(installs.map((item) => item.channel), ["stable", "beta"]);
+ assert.equal(appxOptions.windowsHide, true);
+});
+
+test("Windows background PowerShell calls hide their console windows", async () => {
+ const calls = [];
+ const execute = async (command, args, options) => {
+ calls.push({ command, args, options });
+ return {
+ stdout: '\"ChatGPT.exe\" --remote-debugging-address=127.0.0.1 --remote-debugging-port=9333\n'
+ };
+ };
+ await discoverDebugPort({
+ platform: "win32",
+ execute,
+ fetchImpl: async (url) => {
+ if (url.includes(":9333/")) return { ok: true, json: async () => ({ Browser: "Chrome" }) };
+ throw new Error("offline fixture");
+ }
+ });
+ await focusCodex({ platform: "win32", execute });
+ assert.equal(calls.length, 2);
+ assert.ok(calls.every(({ command, options }) =>
+ command === "powershell.exe" && options?.windowsHide === true
+ ));
});
test("accepts formal and explicit temporary Codex thread ids", () => {
From 57c84c1acea5656832b870d6202ac5d60b229ea5 Mon Sep 17 00:00:00 2001
From: yyf <2358929807@qq.com>
Date: Fri, 21 Aug 2026 12:57:46 +0800
Subject: [PATCH 3/8] perf: reduce Codex control action latency
---
.../plugin/bridge-installer.js | 32 +++++++-
src/bridge/codex-cdp.mjs | 77 +++++++++++++++----
src/bridge/platform.mjs | 70 ++++++++++++-----
src/bridge/server.mjs | 6 +-
test/bridge-installer_test.mjs | 12 ++-
test/bridge_test.mjs | 68 +++++++++++++++-
6 files changed, 226 insertions(+), 39 deletions(-)
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
index d8d5117..0770e9a 100644
--- a/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/plugin/bridge-installer.js
@@ -163,13 +163,35 @@ export function createBridgeInstaller({
const agentsRoot = join(home, "Library", "LaunchAgents");
const bridgeAgent = join(agentsRoot, "io.opencodexmicro.bridge.plist");
let lastWindowsServiceStart = 0;
+ let authorizationCache = null;
+ let authorizationPromise = null;
+ let authorizationGeneration = 0;
+
+ function resetAuthorizationCache() {
+ authorizationGeneration += 1;
+ authorizationCache = null;
+ authorizationPromise = null;
+ }
async function authorizationHeaders() {
+ if (authorizationCache) return authorizationCache;
+ if (authorizationPromise) return authorizationPromise;
+ const generation = authorizationGeneration;
+ const operation = (async () => {
+ try {
+ const token = (await readFile(tokenPath, "utf8")).trim();
+ const headers = token ? Object.freeze({ Authorization: `Bearer ${token}` }) : {};
+ if (token && generation === authorizationGeneration) authorizationCache = headers;
+ return headers;
+ } catch {
+ return {};
+ }
+ })();
+ authorizationPromise = operation;
try {
- const token = (await readFile(tokenPath, "utf8")).trim();
- return token ? { Authorization: `Bearer ${token}` } : {};
- } catch {
- return {};
+ return await operation;
+ } finally {
+ if (authorizationPromise === operation) authorizationPromise = null;
}
}
@@ -315,6 +337,7 @@ if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then exit 1; fi
if (!await exists(tokenPath)) {
await writeFile(tokenPath, `${randomBytes(32).toString("base64url")}\n`, { mode: 0o600 });
}
+ resetAuthorizationCache();
await writeFile(installMetadata, `${JSON.stringify({
version,
platform,
@@ -393,6 +416,7 @@ if /usr/bin/pgrep -x ChatGPT >/dev/null 2>&1; then exit 1; fi
} catch {}
}
await rm(appRoot, { recursive: true, force: true });
+ resetAuthorizationCache();
return status();
}
diff --git a/src/bridge/codex-cdp.mjs b/src/bridge/codex-cdp.mjs
index 32a0587..7467f48 100644
--- a/src/bridge/codex-cdp.mjs
+++ b/src/bridge/codex-cdp.mjs
@@ -347,26 +347,73 @@ function selectMainTarget(targets) {
export class CodexCdpClient {
socket = null;
+ connectPromise = null;
+ connectionGeneration = 0;
nextId = 0;
pending = new Map();
lastSnapshot = null;
+ constructor({
+ discoverPort = discoverDebugPort,
+ fetchTargets = fetchJson,
+ createSocket = url => new WebSocket(url)
+ } = {}) {
+ this.discoverPort = discoverPort;
+ this.fetchTargets = fetchTargets;
+ this.createSocket = createSocket;
+ }
+
async connect() {
if (this.socket?.readyState === WebSocket.OPEN) return;
- const port = await discoverDebugPort();
- const target = selectMainTarget(await fetchJson(`http://127.0.0.1:${port}/json/list`));
+ if (this.connectPromise) return this.connectPromise;
+ const generation = ++this.connectionGeneration;
+ const operation = this.openConnection(generation);
+ this.connectPromise = operation;
+ try {
+ return await operation;
+ } finally {
+ if (this.connectPromise === operation) this.connectPromise = null;
+ }
+ }
+
+ async openConnection(generation) {
+ const port = await this.discoverPort();
+ const target = selectMainTarget(await this.fetchTargets(`http://127.0.0.1:${port}/json/list`));
if (!target?.webSocketDebuggerUrl) throw new Error("Codex main renderer was not found");
- const socket = new WebSocket(target.webSocketDebuggerUrl);
- await new Promise((resolve, reject) => {
- const timer = setTimeout(() => reject(new Error("Timed out connecting to Codex")), 3000);
- socket.once("open", () => { clearTimeout(timer); resolve(); });
- socket.once("error", reject);
- });
- socket.on("message", (raw) => this.handleMessage(String(raw)));
- socket.on("close", () => this.disconnect());
- socket.on("error", () => this.disconnect());
+ if (generation !== this.connectionGeneration) throw new Error("Codex bridge connection was cancelled");
+ const socket = this.createSocket(target.webSocketDebuggerUrl);
this.socket = socket;
- await this.evaluate(ENABLE_EXPRESSION);
+ try {
+ await new Promise((resolve, reject) => {
+ const timer = setTimeout(() => {
+ cleanup();
+ reject(new Error("Timed out connecting to Codex"));
+ }, 3000);
+ const cleanup = () => {
+ clearTimeout(timer);
+ socket.off("open", onOpen);
+ socket.off("error", onError);
+ };
+ const onOpen = () => { cleanup(); resolve(); };
+ const onError = error => { cleanup(); reject(error); };
+ socket.once("open", onOpen);
+ socket.once("error", onError);
+ });
+ if (generation !== this.connectionGeneration || this.socket !== socket) {
+ throw new Error("Codex bridge connection was cancelled");
+ }
+ socket.on("message", (raw) => this.handleMessage(String(raw)));
+ socket.on("close", () => {
+ if (this.socket === socket) this.disconnect();
+ });
+ socket.on("error", () => {
+ if (this.socket === socket) this.disconnect();
+ });
+ await this.evaluate(ENABLE_EXPRESSION);
+ } catch (error) {
+ if (this.socket === socket) this.disconnect();
+ throw error;
+ }
}
async snapshot() {
@@ -555,9 +602,13 @@ export class CodexCdpClient {
}
disconnect() {
+ this.connectionGeneration += 1;
+ this.connectPromise = null;
const socket = this.socket;
this.socket = null;
- if (socket?.readyState === WebSocket.OPEN) socket.close();
+ if (socket && socket.readyState !== WebSocket.CLOSED) {
+ try { socket.close(); } catch { socket.terminate?.(); }
+ }
for (const { reject, timer } of this.pending.values()) {
clearTimeout(timer);
reject(new Error("Codex bridge disconnected"));
diff --git a/src/bridge/platform.mjs b/src/bridge/platform.mjs
index ce14dc6..06a2b43 100644
--- a/src/bridge/platform.mjs
+++ b/src/bridge/platform.mjs
@@ -2,6 +2,8 @@ import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
+let windowsFocusPid = null;
+let windowsFocusPromise = null;
export const CDP_HOST = "127.0.0.1";
export const DEFAULT_CDP_PORT = 9222;
@@ -47,7 +49,25 @@ $rows = foreach ($name in @('OpenAI.Codex', 'OpenAI.CodexBeta')) {
const WINDOWS_FOCUS_COMMAND = String.raw`
$ErrorActionPreference = 'Stop'
-Add-Type @'
+$preferredPid = 0
+$hasPreferredPid = [int]::TryParse($env:CODEX_BRIDGE_FOCUS_PID, [ref]$preferredPid)
+$candidate = if ($hasPreferredPid -and $preferredPid -gt 0) {
+ Get-Process -Id $preferredPid -ErrorAction SilentlyContinue |
+ Where-Object { $_.MainWindowHandle -ne 0 }
+} else { $null }
+if (-not $candidate) {
+ $debugPids = @(Get-CimInstance Win32_Process |
+ Where-Object { $_.CommandLine -like '*--remote-debugging-address=127.0.0.1*' -and $_.CommandLine -notlike '*--type=*' } |
+ Select-Object -ExpandProperty ProcessId)
+ $candidate = Get-Process |
+ Where-Object { $_.MainWindowHandle -ne 0 -and ($_.ProcessName -like 'ChatGPT*' -or $_.ProcessName -like 'Codex*') } |
+ Sort-Object @{ Expression = { if ($debugPids -contains $_.Id) { 0 } else { 1 } } }, StartTime |
+ Select-Object -First 1
+}
+if (-not $candidate) { throw 'A Codex Desktop window was not found.' }
+$shell = New-Object -ComObject WScript.Shell
+if (-not $shell.AppActivate($candidate.Id)) {
+ Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class CodexWindow {
@@ -55,21 +75,12 @@ public static class CodexWindow {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
}
'@
-$debugPids = @(Get-CimInstance Win32_Process |
- Where-Object { $_.CommandLine -like '*--remote-debugging-address=127.0.0.1*' -and $_.CommandLine -notlike '*--type=*' } |
- Select-Object -ExpandProperty ProcessId)
-$candidate = Get-Process |
- Where-Object { $_.MainWindowHandle -ne 0 -and ($_.ProcessName -like 'ChatGPT*' -or $_.ProcessName -like 'Codex*') } |
- Sort-Object @{ Expression = { if ($debugPids -contains $_.Id) { 0 } else { 1 } } }, StartTime |
- Select-Object -First 1
-if (-not $candidate) { throw 'A Codex Desktop window was not found.' }
-[CodexWindow]::ShowWindowAsync($candidate.MainWindowHandle, 9) | Out-Null
-if (-not [CodexWindow]::SetForegroundWindow($candidate.MainWindowHandle)) {
- $shell = New-Object -ComObject WScript.Shell
- if (-not $shell.AppActivate($candidate.Id)) {
+ [CodexWindow]::ShowWindowAsync($candidate.MainWindowHandle, 9) | Out-Null
+ if (-not [CodexWindow]::SetForegroundWindow($candidate.MainWindowHandle)) {
throw 'Windows did not allow Codex Desktop to receive focus.'
}
}
+$candidate.Id
`;
const WINDOWS_STOP_EXECUTABLE_COMMAND = String.raw`
@@ -165,12 +176,33 @@ export async function discoverWindowsCodexExecutables({ execute = execFileAsync
export async function focusCodex({ platform = process.platform, execute = execFileAsync } = {}) {
if (platform === "win32") {
- await execute(
- "powershell.exe",
- powershellArgs(WINDOWS_FOCUS_COMMAND),
- powershellOptions({ timeout: 5000 })
- );
- return;
+ if (windowsFocusPromise) return windowsFocusPromise;
+ const operation = (async () => {
+ try {
+ const { stdout = "" } = await execute(
+ "powershell.exe",
+ powershellArgs(WINDOWS_FOCUS_COMMAND),
+ powershellOptions({
+ timeout: 5000,
+ env: {
+ ...process.env,
+ ...(windowsFocusPid ? { CODEX_BRIDGE_FOCUS_PID: String(windowsFocusPid) } : {})
+ }
+ })
+ );
+ const focusedPid = Number(String(stdout).trim().split(/\r?\n/).at(-1));
+ windowsFocusPid = Number.isInteger(focusedPid) && focusedPid > 0 ? focusedPid : null;
+ } catch (error) {
+ windowsFocusPid = null;
+ throw error;
+ }
+ })();
+ windowsFocusPromise = operation;
+ try {
+ return await operation;
+ } finally {
+ if (windowsFocusPromise === operation) windowsFocusPromise = null;
+ }
}
if (platform === "darwin") {
await execute("/usr/bin/open", ["-b", "com.openai.codex"], { timeout: 3000 });
diff --git a/src/bridge/server.mjs b/src/bridge/server.mjs
index ea8b005..ddaf8e3 100644
--- a/src/bridge/server.mjs
+++ b/src/bridge/server.mjs
@@ -31,6 +31,7 @@ let cached = {
};
let refreshPromise = null;
let nextReconnectAt = 0;
+let hasRefreshed = false;
function authorized(request) {
return bridgeRequestAuthorized(bridgeToken, request.headers.authorization);
@@ -52,6 +53,7 @@ async function refresh(force = false) {
try {
await refreshPromise;
} finally {
+ hasRefreshed = true;
refreshPromise = null;
}
}
@@ -71,7 +73,9 @@ const server = createServer(async (request, response) => {
return json(response, 401, { ok: false, error: "Bridge authorization required" });
}
if (request.method === "GET" && url.pathname === "/health") {
- await refresh(true);
+ if (!hasRefreshed || Date.now() - cached.updatedAt >= REFRESH_MS * 2) {
+ await refresh(true);
+ }
return json(response, 200, { ok: true, codexConnected: cached.connected, updatedAt: cached.updatedAt });
}
if (request.method === "GET" && url.pathname === "/state") {
diff --git a/test/bridge-installer_test.mjs b/test/bridge-installer_test.mjs
index 43e4de6..7bdb011 100644
--- a/test/bridge-installer_test.mjs
+++ b/test/bridge-installer_test.mjs
@@ -158,6 +158,7 @@ test("Windows installer uses LocalAppData, a capability token, and user-level pr
});
try {
+ assert.deepEqual(await installer.authorizationHeaders(), {});
assert.equal(
bridgeDataRoot({ platform: "win32", home, localAppData }),
join(localAppData, "OpenCodexMicro")
@@ -169,7 +170,7 @@ test("Windows installer uses LocalAppData, a capability token, and user-level pr
await access(join(dataRoot, "bridge.mjs"));
const token = (await readFile(join(dataRoot, "bridge-token"), "utf8")).trim();
assert.match(token, /^[A-Za-z0-9_-]{40,}$/);
- assert.match((await installer.authorizationHeaders()).Authorization, /^Bearer /);
+ assert.deepEqual(await installer.authorizationHeaders(), { Authorization: `Bearer ${token}` });
assert.equal(children[0].command, await realpath(systemNode));
assert.equal(children[0].options.windowsHide, true);
assert.equal(children[0].options.env.CODEX_BRIDGE_TOKEN, token);
@@ -194,9 +195,18 @@ test("Windows installer uses LocalAppData, a capability token, and user-level pr
const uninstalled = await installer.uninstall();
assert.equal(uninstalled.installed, false);
await assert.rejects(access(dataRoot));
+ assert.deepEqual(await installer.authorizationHeaders(), {});
const powershellCommands = commands.filter(([command]) => command === "powershell.exe");
assert.ok(powershellCommands.length >= 4);
assert.ok(powershellCommands.every(([, , options]) => options?.windowsHide === true));
+
+ await installer.install();
+ const replacementToken = (await readFile(join(dataRoot, "bridge-token"), "utf8")).trim();
+ assert.notEqual(replacementToken, token);
+ assert.deepEqual(await installer.authorizationHeaders(), {
+ Authorization: `Bearer ${replacementToken}`
+ });
+ await installer.uninstall();
} finally {
await rm(home, { recursive: true, force: true });
}
diff --git a/test/bridge_test.mjs b/test/bridge_test.mjs
index b44a3c7..4b47b7b 100644
--- a/test/bridge_test.mjs
+++ b/test/bridge_test.mjs
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
+import { EventEmitter } from "node:events";
import test from "node:test";
import vm from "node:vm";
@@ -71,6 +72,9 @@ test("Windows background PowerShell calls hide their console windows", async ()
const calls = [];
const execute = async (command, args, options) => {
calls.push({ command, args, options });
+ if (args.join(" ").includes("CODEX_BRIDGE_FOCUS_PID")) {
+ return { stdout: "4242\n" };
+ }
return {
stdout: '\"ChatGPT.exe\" --remote-debugging-address=127.0.0.1 --remote-debugging-port=9333\n'
};
@@ -84,10 +88,72 @@ test("Windows background PowerShell calls hide their console windows", async ()
}
});
await focusCodex({ platform: "win32", execute });
- assert.equal(calls.length, 2);
+ await focusCodex({ platform: "win32", execute });
+ assert.equal(calls.length, 3);
assert.ok(calls.every(({ command, options }) =>
command === "powershell.exe" && options?.windowsHide === true
));
+ const focusCalls = calls.slice(1);
+ assert.ok(focusCalls.every(({ args, options }) =>
+ args.join(" ").includes("AppActivate") &&
+ args.join(" ").includes("SetForegroundWindow") &&
+ options.timeout === 5000
+ ));
+ assert.equal(focusCalls[1].options.env.CODEX_BRIDGE_FOCUS_PID, "4242");
+});
+
+test("concurrent CDP connects share one socket and ignore stale socket events", async () => {
+ class FakeSocket extends EventEmitter {
+ readyState = 0;
+
+ constructor() {
+ super();
+ queueMicrotask(() => {
+ this.readyState = 1;
+ this.emit("open");
+ });
+ }
+
+ send(raw) {
+ const { id } = JSON.parse(raw);
+ queueMicrotask(() => this.emit("message", JSON.stringify({
+ id,
+ result: { result: { value: true } }
+ })));
+ }
+
+ close() {
+ this.readyState = 3;
+ }
+ }
+
+ let discoveries = 0;
+ let targetFetches = 0;
+ const sockets = [];
+ const client = new CodexCdpClient({
+ discoverPort: async () => { discoveries += 1; return 9222; },
+ fetchTargets: async () => {
+ targetFetches += 1;
+ return [{ type: "page", url: "app://-/index.html", webSocketDebuggerUrl: "ws://fixture" }];
+ },
+ createSocket: () => {
+ const socket = new FakeSocket();
+ sockets.push(socket);
+ return socket;
+ }
+ });
+
+ await Promise.all([client.connect(), client.connect()]);
+ assert.equal(discoveries, 1);
+ assert.equal(targetFetches, 1);
+ assert.equal(sockets.length, 1);
+
+ const staleSocket = sockets[0];
+ client.disconnect();
+ await client.connect();
+ staleSocket.emit("close");
+ assert.equal(client.socket, sockets[1]);
+ client.disconnect();
});
test("accepts formal and explicit temporary Codex thread ids", () => {
From 3071dfde77c3bf9370f5c143a3485efe05c6c5e3 Mon Sep 17 00:00:00 2001
From: yyf <2358929807@qq.com>
Date: Fri, 21 Aug 2026 12:57:46 +0800
Subject: [PATCH 4/8] feat: expand Codex task controls
---
docs/architecture.md | 8 +-
docs/configuration.md | 8 +
.../assets/icons/model-luna-max.svg | 5 +
.../assets/icons/model-sol-high.svg | 5 +
.../assets/icons/model-sol-medium.svg | 5 +
.../assets/icons/task-complete.gif | Bin 0 -> 315522 bytes
.../de_DE.json | 7 +-
.../en.json | 7 +-
.../es_ES.json | 7 +-
.../ja_JP.json | 7 +-
.../ko_KR.json | 7 +-
.../manifest.json | 34 ++-
.../package-lock.json | 4 +-
.../package.json | 2 +-
.../plugin/app.js | 14 +-
.../pt_PT.json | 7 +-
.../test/smoke.mjs | 63 +++-
.../zh_CN.json | 7 +-
.../zh_HK.json | 7 +-
package-lock.json | 4 +-
package.json | 2 +-
src/bridge/codex-cdp.mjs | 276 +++++++++++++++++-
src/bridge/server.mjs | 2 +-
test/bridge_test.mjs | 18 +-
24 files changed, 451 insertions(+), 55 deletions(-)
create mode 100644 integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-luna-max.svg
create mode 100644 integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-high.svg
create mode 100644 integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-medium.svg
create mode 100644 integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/task-complete.gif
diff --git a/docs/architecture.md b/docs/architecture.md
index c5a5425..c8f1e55 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -31,9 +31,11 @@ context map, and usage query clients. Those references are cached for the
renderer lifecycle and rediscovered only when they become invalid.
Task activation and Fast, Fork, Submit, and Mic use Codex Micro events. Pin,
-New, and Steer invoke the matching semantic renderer controls. An uncertain
-HTTP failure is never replayed through another mechanism because the first
-request may already have executed.
+New, Steer, and the three model presets invoke semantic renderer controls. The
+model adapter requires the visible intelligence trigger, validates the exact
+model option and reasoning-effort ordering, and verifies the final selection.
+An uncertain HTTP failure is never replayed through another mechanism because
+the first request may already have executed.
## Ulanzi Studio plugin
diff --git a/docs/configuration.md b/docs/configuration.md
index 886b45c..ee9180f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -23,12 +23,20 @@ No repository or npm working directory is required for this flow.
| Steer | Send the visible composer text as steering input to a running task |
| Mic | Press and release the Codex Micro microphone action |
| Submit | Submit or queue the composer text |
+| Sol High | Switch the visible task to GPT-5.6 Sol with high reasoning effort |
+| Luna Max | Switch the visible task to GPT-5.6 Luna with maximum reasoning effort |
+| Sol Medium | Switch the visible task to GPT-5.6 Sol with medium reasoning effort |
Task actions dynamically use the plugin's idle, working, complete, attention,
error, and offline artwork. Usage is rendered at runtime from the allowance
returned by the Bridge. The Encoder action mirrors task 1's current title and
status artwork.
+Model presets operate the visible Codex Desktop intelligence picker. They
+validate the target model, the current reasoning-effort ordering, and the final
+selection. A running turn is not moved to another model; the preset applies to
+the next turn submitted from the visible task.
+
## Recommended layout
Keep Task 1–5 together in Most Recent order. Place frequently used controls on
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-luna-max.svg b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-luna-max.svg
new file mode 100644
index 0000000..8a1bce3
--- /dev/null
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-luna-max.svg
@@ -0,0 +1,5 @@
+
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-high.svg b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-high.svg
new file mode 100644
index 0000000..faeb66b
--- /dev/null
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-high.svg
@@ -0,0 +1,5 @@
+
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-medium.svg b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-medium.svg
new file mode 100644
index 0000000..eddd09d
--- /dev/null
+++ b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/model-sol-medium.svg
@@ -0,0 +1,5 @@
+
diff --git a/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/task-complete.gif b/integration/com.ulanzi.codexmicro.ulanziPlugin/assets/icons/task-complete.gif
new file mode 100644
index 0000000000000000000000000000000000000000..89c8bdb8095ad3454c6abc93ff917f6b13a75909
GIT binary patch
literal 315522
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