From 39d1d8de6c574c53e6ee8120017ac809c536d3de Mon Sep 17 00:00:00 2001 From: Sirius Date: Wed, 5 Aug 2026 15:36:36 +0800 Subject: [PATCH] init --- .commandcode/taste/taste.md | 0 .gitignore | 4 + AGENTS.md | 90 +++ README.md | 220 ++++++ config.example.yaml | 17 + config.yaml | 17 + examples/node_client.mjs | 17 + examples/python_client.py | 14 + package-lock.json | 1252 +++++++++++++++++++++++++++++++++++ package.json | 27 + scripts/install.sh | 27 + scripts/start.sh | 6 + src/command-code.ts | 283 ++++++++ src/config.ts | 43 ++ src/index.ts | 53 ++ src/openai.ts | 133 ++++ src/renderer.ts | 87 +++ src/server.ts | 188 ++++++ tsconfig.json | 17 + 19 files changed, 2495 insertions(+) create mode 100644 .commandcode/taste/taste.md create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 config.example.yaml create mode 100644 config.yaml create mode 100644 examples/node_client.mjs create mode 100644 examples/python_client.py create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 scripts/install.sh create mode 100755 scripts/start.sh create mode 100644 src/command-code.ts create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/openai.ts create mode 100644 src/renderer.ts create mode 100644 src/server.ts create mode 100644 tsconfig.json diff --git a/.commandcode/taste/taste.md b/.commandcode/taste/taste.md new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd6e803 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..91af78d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# Command Code OpenAI Bridge + +Command Code OpenAI Bridge 是一个仅在本机运行的 OpenAI Chat Completions 兼容服务。它把 OpenAI 客户端发送的文本对话转交给 Command Code CLI 执行,并将最终 assistant 回复以标准 Chat Completions 格式返回。 + +这个项目没有git,所以也不需要使用GitNexus相关的skill + +默认 API 地址:`http://127.0.0.1:18000/v1` + +## 工作方式 + +每个请求都会启动一个独立的 Command Code headless agent: + +1. 接收并校验 OpenAI Chat Completions 请求。 +2. 保留消息的角色、顺序和完整文本,将全部会话历史序列化后通过 stdin 传给 Command Code。 +3. 在启动服务的终端中显示模型状态、turn、工具调用、工具结果、stderr、最终回答和耗时。 +4. 从 Command Code 的 NDJSON 结构化结果中提取 `finalText`。 +5. 只把最终回复和可用的 token usage 返回给 API 客户端。 + +服务不保存跨请求会话。每次调用都使用 `--no-session`,对话历史由客户端维护并在请求中完整提供。 + +## 主要能力 + +- 提供 `GET /health`、`GET /v1/models` 和 `POST /v1/chat/completions`。 +- 支持字符串形式的 `content` 和 OpenAI 文本 part 数组。 +- 接受 `stream: true`,但 Command Code 调用仍为非流式;完整结果生成后再一次性封装为 SSE 事件返回。 +- 支持中文、Unicode、Markdown、代码块和较长文本。 +- 模型名称通过 YAML 配置映射到实际 Command Code 模型。 +- 请求内容通过 stdin 传输,不受命令行参数长度限制。 +- 同时只运行一个 Command Code 请求;忙碌时返回 HTTP 429。 +- 客户端断开、请求取消、总超时和 Ctrl+C 会终止当前子进程组。 +- 单次 Command Code 失败后,HTTP 服务可继续处理后续请求。 +- Authorization 请求头会被忽略,服务强制绑定 `127.0.0.1`。 + +当前只支持文本 Chat Completions。`stream: true` 是客户端兼容层,不会实时输出 Command Code 的生成过程。图片、音频、function calling、Responses API 和服务端会话均未实现。 + +## 技术实现 + +- Node.js 22+ +- TypeScript +- Fastify +- Zod +- YAML +- Command Code CLI 官方 headless 模式:`-p --output-format json` + +项目使用普通子进程读取 Command Code 的 NDJSON 事件。终端输出由本项目渲染,可显示主要运行事件,但不包含 Command Code 原始 Ink TUI、动画和键盘交互。启用 `dangerously_skip_permissions` 时会向 CLI 传入 `--yolo`,只应在可信工作目录中使用。 + +## 项目结构 + +```text +src/ +├── index.ts # 程序入口、服务启动和信号处理 +├── server.ts # HTTP 路由、单请求状态和错误处理 +├── openai.ts # 请求校验、消息转换和响应生成 +├── command-code.ts # CLI 检查、子进程管理和最终结果提取 +├── renderer.ts # Command Code 事件的终端显示 +└── config.ts # YAML 配置读取与校验 + +scripts/ +├── install.sh # 安装依赖并构建项目 +└── start.sh # 启动服务 + +examples/ +├── python_client.py # OpenAI Python SDK 示例 +└── node_client.mjs # OpenAI Node.js SDK 示例 +``` + +## 快速开始 + +```bash +./scripts/install.sh +command-code login +command-code status --json +./scripts/start.sh +``` + +默认配置位于 `config.yaml`,配置示例位于 `config.example.yaml`。常用配置包括监听端口、Command Code 可执行文件、工作目录、总超时、请求体上限、最大 turn 数、权限模式和模型映射。 + +调用示例: + +```bash +curl http://127.0.0.1:18000/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{ + "model": "command-default", + "messages": [{"role": "user", "content": "用一句话解释递归。"}], + "stream": false + }' +``` + +更完整的安装、配置、接口说明、运行行为、检查结果和已知限制见 `README.md`。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..0371f86 --- /dev/null +++ b/README.md @@ -0,0 +1,220 @@ +# Command Code OpenAI Bridge + +这是一个只绑定本机地址的 OpenAI Chat Completions 兼容转发服务。它为每个 API 请求启动一次独立的 Command Code headless agent,将完整消息历史通过 stdin 发送给 CLI,在当前终端显示运行过程,只把官方结构化结果中的 `finalText` 返回给客户端。 + +默认地址:`http://127.0.0.1:18000/v1` + +## 当前机器上的调查结论 + +- 系统:macOS 26.4.1 arm64。 +- Node.js:v22.22.2,符合 Command Code 的 Node.js 22+ 要求。 +- 安装方式:`npm install --global command-code@latest`。 +- 已安装 Command Code:v1.10.0,npm `latest` 也是 v1.10.0。 +- 可执行文件:`command-code`;macOS/Linux 短名为 `cmd`,Windows 短名为 `cmdc`。 +- 登录:`command-code login`;检查:`command-code status --json`;本机已登录。 +- npm 包没有 `exports` 字段,`main` 指向会直接启动 CLI 的 `dist/cli.mjs`,包尾直接解析命令行,没有公开、稳定的可嵌入 API。 +- 官方 headless 调用:`command-code -p --output-format json`。不传查询参数时会从 stdin 读取。 +- JSON 输出是 NDJSON:运行中输出 `AgentEvent`,正常情况下最后输出唯一的 `result` 行;最终回答位于 `finalText`,usage 和耗时也在该行。 +- `--no-session` 让每次请求只使用内存会话,不写入或恢复跨请求 session。 +- 官方 headless 模式明确不提供键盘、问题回答或权限批准等交互。原始 TTY/TUI 与可靠的独立 `finalText` 目前不能同时获得。 +- 官方退出码:0 成功;1 常规错误;3 未登录;4 权限拒绝;5 限流;6 网络错误;7 服务端错误;8 达到 turn 上限;9 无回复;10 余额不足;130 被信号中断。 + +官方资料: + +- [Quickstart](https://commandcode.ai/docs/quickstart) +- [CLI Reference](https://commandcode.ai/docs/reference/cli) +- [Headless Mode](https://commandcode.ai/docs/headless) +- [Permissions](https://commandcode.ai/docs/core-concepts/permissions) + +## 为什么使用普通子进程与结构化事件 + +本项目使用官方 headless 子进程和 NDJSON,不使用 PTY,也不解析 TUI 文本。 + +原因是 v1.10.0 没有公开嵌入 API,交互式 TUI 没有独立的结构化最终结果通道。headless 的最后一行提供稳定 `finalText`,能保证工具参数、工具结果、状态、stderr、思考事件和 ANSI 内容不会混入 API 回复。 + +终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。 + +本机 v1.10.0 实测表明,headless 即使设为 `auto-accept`,shell 仍会被拒绝。默认 `dangerously_skip_permissions: true`,因此实际传入 `--yolo`,让文件写入和命令工具能继续执行。它会绕过所有权限确认,只应对可信工作目录使用。若要只读,把 `dangerously_skip_permissions` 改为 `false`,同时把 `permission_mode` 改成 `plan`。 + +## 项目结构 + +```text +command-code-openai-bridge/ +├── config.yaml +├── config.example.yaml +├── package.json +├── tsconfig.json +├── src/ +│ ├── command-code.ts +│ ├── config.ts +│ ├── index.ts +│ ├── openai.ts +│ ├── renderer.ts +│ └── server.ts +├── scripts/ +│ ├── install.sh +│ └── start.sh +└── examples/ + ├── node_client.mjs + └── python_client.py +``` + +## 安装 + +```bash +cd /Users/zen/Documents/Codex/2026-08-04/files-mentioned-by-the-user-command/outputs/command-code-openai-bridge +./scripts/install.sh +``` + +脚本会检查 Node.js 版本、按需安装 Command Code、安装项目依赖并构建。 + +如未登录: + +```bash +command-code login +command-code status --json +``` + +`command-code login` 会打开浏览器,也可以粘贴从 Command Code Studio 创建的 API key。 + +## 配置 + +编辑 `config.yaml`: + +```yaml +host: 127.0.0.1 +port: 18000 +command_code_executable: command-code +command_code_working_directory: . +timeout_seconds: 1800 +max_request_bytes: 20971520 +max_turns: 100 +permission_mode: auto-accept +dangerously_skip_permissions: true + +models: + command-default: + cli_model: deepseek/deepseek-v4-flash + effort: max +``` + +`command_code_working_directory` 决定 Command Code 能看到和操作的项目目录。相对路径以 `config.yaml` 所在目录为基准。`models..effort` 会传给 Command Code 的 `--effort`,可用值取决于对应模型。服务强制只监听 `127.0.0.1`。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。 + +## 启动、停止与重启 + +```bash +./scripts/start.sh +``` + +`scripts/start.sh` 只运行已经生成的 `dist/index.js`,不会自动编译 TypeScript。修改 `src/` 中的源码后,必须先重新编译,再重启服务,新代码才会生效: + +```bash +npm run build +./scripts/start.sh +``` + +首次执行 `scripts/install.sh` 时会自动安装依赖并完成一次编译。 + +空闲时按 Ctrl+C 停止。请求运行时第一次 Ctrl+C 取消当前 Command Code 子进程组并保留服务;请求结束后再按 Ctrl+C 停止服务。重启就是再次执行启动脚本。 + +客户端断开连接也会先给整个 Command Code 子进程组发送 SIGINT,随后按需升级为 SIGTERM 和 SIGKILL。总超时由 `timeout_seconds` 控制。 + +## API + +- `GET /health` +- `GET /v1/models` +- `POST /v1/chat/completions` + +Bearer Token 会被忽略。只接受文本消息。支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。图片、音频和其他 part 会返回 400。 + +`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 时,服务仍会等待 Command Code 完整执行结束,再将最终文本一次性封装为 Chat Completions SSE 事件返回;它不提供实时生成过程。传入 `stream_options.include_usage: true` 时,结束前还会返回 usage 块。 + +### curl + +```bash +curl -sS http://127.0.0.1:18000/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ignored' \ + -d '{ + "model": "command-default", + "messages": [ + {"role": "user", "content": "用一句话解释递归。"} + ], + "stream": false + }' +``` + +### OpenAI Python SDK + +```bash +python3 -m venv .venv +.venv/bin/pip install openai +.venv/bin/python examples/python_client.py +``` + +### OpenAI Node.js SDK + +项目依赖已包含 `openai`: + +```bash +node examples/node_client.mjs +``` + +## 实际 Command Code 命令 + +每次请求实际执行以下固定参数,完整提示词不进入命令行: + +```bash +command-code \ + -p \ + --output-format json \ + --no-session \ + --skip-onboarding \ + --no-auto-update \ + --trust \ + --max-turns 100 \ + --model deepseek/deepseek-v4-flash \ + --effort max \ + --yolo +``` + +随后通过子进程 stdin 写入 UTF-8 的完整请求历史。消息序列被放进一个 JSON 对象,角色、顺序、空消息、Unicode、Markdown、代码块和自定义分隔符均不会被简单文本分隔符破坏。服务不总结、不删除、不截断消息,也不保存会话。 + +## 单并发和错误 + +同一时间只允许一个 Command Code 实例。第二个请求直接返回 HTTP 429 和 `code: busy`。请求体超过 `max_request_bytes` 返回 413。未知模型和非文本内容返回 4xx。CLI 的详细错误留在服务终端,客户端只收到简洁的 OpenAI 格式错误。 + +一次 CLI 异常不会结束 HTTP 服务,后续请求仍可继续。 + +## 本机实际检查结果 + +检查日期:2026-08-04。没有编写测试用例,以下均为构建后运行真实 CLI 和真实 HTTP 客户端得到的端到端结果。 + +- TypeScript 严格类型检查和生产构建通过。 +- npm 生产依赖审计:0 个已知漏洞。 +- `/health` 返回 Command Code v1.10.0、已登录、空闲。 +- `/v1/models` 返回两个本地映射。 +- curl 中文请求返回 HTTP 200,客户端只收到 `中文接口成功` 和标准 completion 字段。 +- 字符串 content 与 text part 数组均通过;非文本 part 返回 HTTP 400。 +- system、user、assistant、user 完整历史检查返回了历史 assistant 中的 `蓝鲸-42`。 +- 约 96 KiB 的中文消息通过 stdin 完整传入并返回 `长文本回退成功`,没有经过命令行参数。 +- read_file 工具调用、参数和文件结果显示在服务终端,API 只返回 package name。 +- `--yolo` 下 shell_command 实际执行并返回 `shell-tool-ok`;`auto-accept` 下该工具确实被 headless 权限引擎拒绝。 +- OpenAI Node.js SDK 示例通过。 +- OpenAI Python SDK 示例通过。 +- `stream: false` 返回普通 JSON;`stream: true` 在完整结果生成后一次性返回兼容 SSE。 +- 并发检查中第二个请求返回 HTTP 429 和 `code: busy`,没有启动第二个 CLI。 +- 超过 20 MiB 的请求体返回 HTTP 413,服务保持可用。 +- 客户端 1 秒超时断开后,当前子进程组被清理,`busy` 恢复为 false,没有发现残留 headless CLI。 +- 运行中按 Ctrl+C 后客户端收到 HTTP 499,服务保持运行;紧接着的真实请求返回 `中断后恢复成功`。 +- 原始交互模式的本机 TTY 探测确认 Ink TUI、ANSI、输入框和双 Ctrl+C 退出行为存在;该模式没有独立结构化最终结果通道。 + +## 已知限制 + +- 没有原始 Command Code TUI、颜色布局、动画和键盘交互。 +- headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;`ask_user_question` 不能由等待中的 HTTP 客户端处理。 +- 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。 +- API 只实现 Chat Completions 文本范围;`stream: true` 是最终结果的 SSE 兼容封装,不是实时生成。不实现 Responses API、图片、音频、function calling 或服务端会话。 +- `usage` 使用 Command Code 最终结果提供的真实 input/output token;CLI 未提供时返回 0。 +- 长文本受 HTTP 请求体上限和 Command Code 模型上下文上限共同限制,不会由桥接服务自行截断。 +- v1.10.0 的大输入会让 `run_end.nextState` 重复完整提示词。实测约 96 KiB 中文输入时,CLI 退出前可能截断该大事件并丢掉紧随其后的 compact `result` 行。桥接服务会忽略冗余 `run_end`,优先使用 `result.finalText`;若退出码为 0 且 result 缺失,只使用最后一个已完整结束、无工具调用的结构化 turn 文本和真实 turn usage,不从 TUI 文本解析。 diff --git a/config.example.yaml b/config.example.yaml new file mode 100644 index 0000000..4bc368c --- /dev/null +++ b/config.example.yaml @@ -0,0 +1,17 @@ +host: 127.0.0.1 +port: 18000 +command_code_executable: command-code +command_code_working_directory: . +timeout_seconds: 1800 +max_request_bytes: 20971520 +max_turns: 100 +permission_mode: auto-accept +dangerously_skip_permissions: true + +models: + command-default: + cli_model: deepseek/deepseek-v4-flash + effort: max + command-fast: + cli_model: deepseek/deepseek-v4-flash + effort: low diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..5cd7856 --- /dev/null +++ b/config.yaml @@ -0,0 +1,17 @@ +host: 127.0.0.1 +port: 18000 +command_code_executable: command-code +command_code_working_directory: . +timeout_seconds: 1800 +max_request_bytes: 20971520 +max_turns: 100 +permission_mode: auto-accept +dangerously_skip_permissions: true + +models: + command-default: + cli_model: deepseek/deepseek-v4-flash + effort: high + command-pro: + cli_model: deepseek/deepseek-v4-pro + effort: max diff --git a/examples/node_client.mjs b/examples/node_client.mjs new file mode 100644 index 0000000..6242c57 --- /dev/null +++ b/examples/node_client.mjs @@ -0,0 +1,17 @@ +import OpenAI from "openai"; + +const client = new OpenAI({ + baseURL: "http://127.0.0.1:18000/v1", + apiKey: "ignored", +}); + +const response = await client.chat.completions.create({ + model: "command-default", + messages: [ + { role: "system", content: "回答要简洁。" }, + { role: "user", content: "用一句话解释递归。" }, + ], + stream: false, +}); + +console.log(response.choices[0].message.content); diff --git a/examples/python_client.py b/examples/python_client.py new file mode 100644 index 0000000..201a36d --- /dev/null +++ b/examples/python_client.py @@ -0,0 +1,14 @@ +from openai import OpenAI + +client = OpenAI(base_url="http://127.0.0.1:18000/v1", api_key="ignored") + +response = client.chat.completions.create( + model="command-default", + messages=[ + {"role": "system", "content": "回答要简洁。"}, + {"role": "user", "content": "用一句话解释递归。"}, + ], + stream=False, +) + +print(response.choices[0].message.content) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..966a073 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1252 @@ +{ + "name": "command-code-openai-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "command-code-openai-bridge", + "version": "1.0.0", + "dependencies": { + "fastify": "^5.5.0", + "openai": "^5.19.1", + "yaml": "^2.8.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", + "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.11.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.11.2.tgz", + "integrity": "sha512-i/eJG7nXR9OkbFgoX4jFiPOHoRq0rXqDACVAXELh5Fdg6BFBErIVZ8MyibGCwup9Go1pYrxZJ0ogIub8vVdEcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", + "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/openai": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", + "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/tsx": { + "version": "4.23.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.5.tgz", + "integrity": "sha512-rw55FUaqOoI7RvlQwLbhO4nSDApnQ4/CykPuiQ/EPvtrX3WA9Ig55jIt9VvbBJbzJuj12ueRu4PMZ2SxPVbihg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6f76d98 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "command-code-openai-bridge", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Local OpenAI Chat Completions compatible bridge for Command Code CLI", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "check": "tsc -p tsconfig.json --noEmit", + "dev": "tsx src/index.ts --config config.yaml", + "start": "node dist/index.js --config config.yaml" + }, + "dependencies": { + "fastify": "^5.5.0", + "openai": "^5.19.1", + "yaml": "^2.8.1", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "tsx": "^4.20.5", + "typescript": "^5.9.2" + } +} diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..019b3c7 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_dir=$(cd "$(dirname "$0")/.." && pwd) +cd "$project_dir" + +if ! command -v node >/dev/null 2>&1; then + echo "需要 Node.js 22 或更高版本。" >&2 + exit 1 +fi + +node_major=$(node -p 'Number(process.versions.node.split(".")[0])') +if [ "$node_major" -lt 22 ]; then + echo "当前 Node.js 版本过低:$(node --version),需要 22 或更高版本。" >&2 + exit 1 +fi + +if ! command -v command-code >/dev/null 2>&1; then + npm install --global command-code@latest +fi + +npm install +npm run build + +echo "安装完成。" +echo "如未登录,请运行:command-code login" +echo "登录状态:command-code status --json" diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..21a7e38 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_dir=$(cd "$(dirname "$0")/.." && pwd) +cd "$project_dir" +exec node dist/index.js --config config.yaml diff --git a/src/command-code.ts b/src/command-code.ts new file mode 100644 index 0000000..d1dd430 --- /dev/null +++ b/src/command-code.ts @@ -0,0 +1,283 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { createInterface } from "node:readline"; +import type { BridgeConfig } from "./config.js"; +import type { CommandUsage } from "./openai.js"; +import { TerminalRenderer } from "./renderer.js"; + +interface ResultFrame { + type: "result"; + subtype: "success" | "error" | "max_turns" | string; + finalText: string; + durationMs: number; + usage?: CommandUsage; + error?: unknown; +} + +export interface CommandResult { + finalText: string; + durationMs: number; + usage?: CommandUsage; +} + +export interface InstallationStatus { + installed: boolean; + authenticated: boolean; + version?: string; +} + +export class CommandCodeError extends Error { + constructor( + message: string, + readonly exitCode?: number | null, + ) { + super(message); + this.name = "CommandCodeError"; + } +} + +function signalProcessGroup(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void { + if (child.pid === undefined) return; + try { + if (process.platform === "win32") child.kill(signal); + else process.kill(-child.pid, signal); + } catch { + try { child.kill(signal); } catch { /* process already exited */ } + } +} + +function terminateChild(child: ChildProcessWithoutNullStreams): () => void { + signalProcessGroup(child, "SIGINT"); + const termTimer = setTimeout(() => signalProcessGroup(child, "SIGTERM"), 2_000); + const killTimer = setTimeout(() => signalProcessGroup(child, "SIGKILL"), 7_000); + termTimer.unref(); + killTimer.unref(); + return () => { + clearTimeout(termTimer); + clearTimeout(killTimer); + }; +} + +function parseFrame(line: string): Record | undefined { + if (line.trim() === "") return undefined; + const value: unknown = JSON.parse(line); + if (typeof value !== "object" || value === null) throw new Error("NDJSON line is not an object"); + return value as Record; +} + +export async function runCommandCode( + config: BridgeConfig, + cliModel: string, + effort: string, + prompt: string, + signal: AbortSignal, +): Promise { + const args = [ + "-p", + "--output-format", "json", + "--no-session", + "--skip-onboarding", + "--no-auto-update", + "--trust", + "--max-turns", String(config.max_turns), + "--model", cliModel, + "--effort", effort, + ]; + + if (config.dangerously_skip_permissions) args.push("--yolo"); + else args.push("--permission-mode", config.permission_mode); + + const renderer = new TerminalRenderer(); + renderer.begin(cliModel, effort); + + return new Promise((resolve, reject) => { + const startedAt = Date.now(); + let child: ChildProcessWithoutNullStreams; + try { + child = spawn(config.command_code_executable, args, { + cwd: config.resolvedWorkingDirectory, + env: process.env, + detached: process.platform !== "win32", + stdio: ["pipe", "pipe", "pipe"], + }); + } catch (error) { + reject(new CommandCodeError(`CLI 启动失败:${String(error)}`)); + return; + } + + let settled = false; + let result: ResultFrame | undefined; + let parseError: Error | undefined; + let stderr = ""; + let cancelEscalation: (() => void) | undefined; + let lastMessageText: string | undefined; + let finalTurnText: string | undefined; + const eventUsage: Required = { inputTokens: 0, outputTokens: 0 }; + + const finish = (error?: Error, value?: CommandResult) => { + if (settled) return; + settled = true; + clearTimeout(timeout); + signal.removeEventListener("abort", onAbort); + cancelEscalation?.(); + if (error) reject(error); + else resolve(value!); + }; + + const onAbort = () => { + cancelEscalation ??= terminateChild(child); + }; + + const timeout = setTimeout(() => { + renderer.fail(`超过 ${config.timeout_seconds} 秒总超时`); + cancelEscalation ??= terminateChild(child); + }, config.timeout_seconds * 1000); + timeout.unref(); + + signal.addEventListener("abort", onAbort, { once: true }); + if (signal.aborted) onAbort(); + + child.once("error", (error) => { + renderer.fail(error.message); + finish(new CommandCodeError(`CLI 启动失败:${error.message}`)); + }); + + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (chunk: string) => { + stderr += chunk; + if (stderr.length > 64 * 1024) stderr = stderr.slice(-64 * 1024); + renderer.stderr(chunk); + }); + + child.stdout.setEncoding("utf8"); + const lines = createInterface({ input: child.stdout, crlfDelay: Infinity }); + lines.on("line", (line) => { + try { + // run_end carries nextState, which repeats the complete prompt and can be very large. + // The following result line is the authoritative compact result. + if (line.startsWith('{"type":"event","event":{"type":"run_end"')) return; + const frame = parseFrame(line); + if (!frame) return; + if (frame.type === "event" && typeof frame.event === "object" && frame.event !== null) { + const event = frame.event as Record; + if (event.type === "message_end" && Array.isArray(event.content)) { + const textParts = event.content + .filter((part): part is { type: string; text?: unknown } => ( + typeof part === "object" && part !== null && "type" in part + )) + .filter((part) => part.type === "text" && typeof part.text === "string") + .map((part) => String(part.text)); + lastMessageText = textParts.length > 0 ? textParts.join("") : undefined; + } + if (event.type === "turn_end") { + if (event.hadToolCalls === false && lastMessageText !== undefined) { + finalTurnText = lastMessageText; + } + if (typeof event.usage === "object" && event.usage !== null) { + const usage = event.usage as CommandUsage; + eventUsage.inputTokens += usage.inputTokens ?? 0; + eventUsage.outputTokens += usage.outputTokens ?? 0; + } + lastMessageText = undefined; + } + renderer.event(event); + } else if (frame.type === "result") { + result = frame as unknown as ResultFrame; + } + } catch (error) { + parseError = error instanceof Error ? error : new Error(String(error)); + process.stderr.write(`\n无法解析 Command Code NDJSON:${line.slice(0, 500)}\n`); + } + }); + + child.once("close", (code, closeSignal) => { + lines.close(); + + if (signal.aborted) { + renderer.fail("请求已取消"); + finish(new CommandCodeError("Command Code request cancelled", code)); + return; + } + + if (code !== 0) { + const detail = stderr.trim().split("\n").slice(-3).join(" | "); + renderer.fail(`退出码 ${String(code)}${closeSignal ? `,信号 ${closeSignal}` : ""}${detail ? `:${detail}` : ""}`); + finish(new CommandCodeError("Command Code request failed", code)); + return; + } + + if (!result) { + if (finalTurnText !== undefined) { + const durationMs = Date.now() - startedAt; + process.stderr.write("\n警告:Command Code 未输出最终 result 行,已使用最后一个完整结构化 turn 的文本。\n"); + renderer.finish(durationMs); + finish(undefined, { + finalText: finalTurnText, + durationMs, + usage: eventUsage, + }); + return; + } + renderer.fail(parseError ? `结构化事件解析失败:${parseError.message}` : "没有收到最终 result 行"); + finish(new CommandCodeError("Unable to identify final Command Code response", code)); + return; + } + + if (result.subtype !== "success") { + renderer.fail(`结果状态 ${result.subtype}`); + finish(new CommandCodeError("Command Code request failed", code)); + return; + } + + renderer.finish(result.durationMs); + finish(undefined, { + finalText: result.finalText, + durationMs: result.durationMs, + ...(result.usage ? { usage: result.usage } : {}), + }); + }); + + child.stdin.on("error", (error: NodeJS.ErrnoException) => { + if (error.code !== "EPIPE") renderer.fail(`stdin 错误:${error.message}`); + }); + child.stdin.end(prompt, "utf8"); + }); +} + +function capture(executable: string, args: string[], timeoutMs = 10_000): Promise<{ code: number | null; stdout: string }> { + return new Promise((resolve) => { + const child = spawn(executable, args, { stdio: ["ignore", "pipe", "ignore"] }); + let stdout = ""; + const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs); + timer.unref(); + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk: string) => { stdout += chunk; }); + child.on("error", () => { + clearTimeout(timer); + resolve({ code: null, stdout: "" }); + }); + child.on("close", (code) => { + clearTimeout(timer); + resolve({ code, stdout }); + }); + }); +} + +export async function inspectInstallation(executable: string): Promise { + const versionResult = await capture(executable, ["--version"]); + if (versionResult.code !== 0) return { installed: false, authenticated: false }; + + const statusResult = await capture(executable, ["status", "--json"]); + let authenticated = false; + try { + const status = JSON.parse(statusResult.stdout) as { authenticated?: boolean }; + authenticated = statusResult.code === 0 && status.authenticated === true; + } catch { + authenticated = false; + } + + return { + installed: true, + authenticated, + version: versionResult.stdout.trim(), + }; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..201d889 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,43 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import YAML from "yaml"; +import { z } from "zod"; + +const modelSchema = z.object({ + cli_model: z.string().min(1), + effort: z.string().min(1), +}); + +const configSchema = z.object({ + host: z.literal("127.0.0.1").default("127.0.0.1"), + port: z.number().int().min(1).max(65535).default(18000), + command_code_executable: z.string().min(1).default("command-code"), + command_code_working_directory: z.string().min(1).default("."), + timeout_seconds: z.number().int().positive().default(1800), + max_request_bytes: z.number().int().positive().default(20 * 1024 * 1024), + max_turns: z.number().int().positive().default(100), + permission_mode: z.enum(["default", "standard", "plan", "auto-accept", "dont-ask"]).default("auto-accept"), + dangerously_skip_permissions: z.boolean().default(false), + models: z.record(z.string().min(1), modelSchema).refine( + (models) => Object.keys(models).length > 0, + "At least one model mapping is required", + ), +}); + +export type BridgeConfig = z.infer & { + configDirectory: string; + resolvedWorkingDirectory: string; +}; + +export async function loadConfig(configPath: string): Promise { + const absolutePath = path.resolve(configPath); + const source = await readFile(absolutePath, "utf8"); + const parsed = configSchema.parse(YAML.parse(source)); + const configDirectory = path.dirname(absolutePath); + + return { + ...parsed, + configDirectory, + resolvedWorkingDirectory: path.resolve(configDirectory, parsed.command_code_working_directory), + }; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e90a9ae --- /dev/null +++ b/src/index.ts @@ -0,0 +1,53 @@ +#!/usr/bin/env node +import path from "node:path"; +import { loadConfig } from "./config.js"; +import { createServer } from "./server.js"; + +function configPathFromArgs(args: string[]): string { + const index = args.indexOf("--config"); + if (index === -1) return "config.yaml"; + const value = args[index + 1]; + if (!value) throw new Error("--config requires a file path"); + return value; +} + +async function main(): Promise { + const configPath = path.resolve(configPathFromArgs(process.argv.slice(2))); + const config = await loadConfig(configPath); + const bridge = await createServer(config); + + if (!bridge.installation.installed) { + process.stderr.write(`警告:找不到 ${config.command_code_executable},API 将返回 503。\n`); + } else if (!bridge.installation.authenticated) { + process.stderr.write("警告:Command Code 未登录。运行 command-code login 后重启服务。\n"); + } + + await bridge.app.listen({ host: config.host, port: config.port }); + process.stdout.write(`Command Code OpenAI Bridge 已启动\n`); + process.stdout.write(`API: http://${config.host}:${config.port}/v1\n`); + process.stdout.write(`工作目录: ${config.resolvedWorkingDirectory}\n`); + process.stdout.write(`Command Code: ${bridge.installation.version ?? "unavailable"}\n`); + process.stdout.write(`权限模式: ${config.dangerously_skip_permissions ? "yolo" : config.permission_mode}\n`); + process.stdout.write("空闲时按 Ctrl+C 停止;请求运行中第一次 Ctrl+C 只取消当前请求。\n"); + + let shuttingDown = false; + const shutdown = async (signal: NodeJS.Signals) => { + if (bridge.abortActive()) { + process.stderr.write(`\n收到 ${signal},正在取消当前 Command Code 请求。\n`); + return; + } + if (shuttingDown) return; + shuttingDown = true; + process.stdout.write(`\n收到 ${signal},正在停止服务。\n`); + await bridge.app.close(); + process.exitCode = 0; + }; + + process.on("SIGINT", () => { void shutdown("SIGINT"); }); + process.on("SIGTERM", () => { void shutdown("SIGTERM"); }); +} + +main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`); + process.exitCode = 1; +}); diff --git a/src/openai.ts b/src/openai.ts new file mode 100644 index 0000000..2fdbab6 --- /dev/null +++ b/src/openai.ts @@ -0,0 +1,133 @@ +import { randomUUID } from "node:crypto"; +import { z } from "zod"; + +const textPartSchema = z.object({ + type: z.literal("text"), + text: z.string(), +}).strict(); + +const messageSchema = z.object({ + role: z.enum(["system", "user", "assistant"]), + content: z.union([z.string(), z.array(textPartSchema)]), +}).strict(); + +export const chatCompletionRequestSchema = z.object({ + model: z.string().min(1), + messages: z.array(messageSchema).min(1), + stream: z.boolean().optional().default(false), + stream_options: z.object({ + include_usage: z.boolean().optional().default(false), + }).passthrough().optional(), +}).passthrough(); + +export type ChatCompletionRequest = z.infer; + +export interface CommandUsage { + inputTokens?: number; + outputTokens?: number; +} + +export function normalizeMessages(request: ChatCompletionRequest) { + return request.messages.map((message) => ({ + role: message.role, + content: typeof message.content === "string" + ? message.content + : message.content.map((part) => part.text).join(""), + })); +} + +export function buildCommandPrompt(request: ChatCompletionRequest): string { + const envelope = { + protocol: "openai-chat-completions-history-v1", + messages: normalizeMessages(request), + }; + + return [ + "下面 JSON 对象的 messages 数组是外部客户端提交的完整任务。", + "严格执行 system 消息和最后一条 user 消息中的具体指令,并结合此前消息理解上下文。", + "调用方可能要求改写问题、生成检索词、提取数据、分类或输出特定格式。这些属于内部处理任务,也必须严格执行。", + "如果最后一条 user 消息要求改写、压缩、提取或输出指定格式,只返回要求的结果,不回答消息中包含的问题。", + "不要复述 JSON,不要输出角色标签,不要添加指令未要求的解释或格式。", + "JSON 数据开始:", + JSON.stringify(envelope), + ].join("\n\n"); +} + +export function completionResponse(model: string, content: string, usage?: CommandUsage) { + const promptTokens = usage?.inputTokens ?? 0; + const completionTokens = usage?.outputTokens ?? 0; + + return { + id: `chatcmpl-local-${randomUUID().replaceAll("-", "")}`, + object: "chat.completion", + created: Math.floor(Date.now() / 1000), + model, + choices: [{ + index: 0, + message: { role: "assistant", content }, + finish_reason: "stop", + }], + usage: { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + total_tokens: promptTokens + completionTokens, + }, + }; +} + +export function completionStreamResponse( + model: string, + content: string, + usage?: CommandUsage, + includeUsage = false, +): string { + const id = `chatcmpl-local-${randomUUID().replaceAll("-", "")}`; + const created = Math.floor(Date.now() / 1000); + const base = { id, object: "chat.completion.chunk", created, model }; + const chunks: object[] = [ + { + ...base, + choices: [{ + index: 0, + delta: { role: "assistant", content: "" }, + finish_reason: null, + }], + }, + { + ...base, + choices: [{ + index: 0, + delta: { content }, + finish_reason: null, + }], + }, + { + ...base, + choices: [{ + index: 0, + delta: {}, + finish_reason: "stop", + }], + }, + ]; + + if (includeUsage) { + const promptTokens = usage?.inputTokens ?? 0; + const completionTokens = usage?.outputTokens ?? 0; + chunks.push({ + ...base, + choices: [], + usage: { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + total_tokens: promptTokens + completionTokens, + }, + }); + } + + return `${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`; +} + +export function openAIError(message: string, code: string, type = "invalid_request_error") { + return { error: { message, type, code } }; +} diff --git a/src/renderer.ts b/src/renderer.ts new file mode 100644 index 0000000..db16738 --- /dev/null +++ b/src/renderer.ts @@ -0,0 +1,87 @@ +import { inspect } from "node:util"; + +const tty = process.stdout.isTTY; +const color = (code: number, text: string) => tty ? `\u001b[${code}m${text}\u001b[0m` : text; +const cyan = (text: string) => color(36, text); +const green = (text: string) => color(32, text); +const yellow = (text: string) => color(33, text); +const dim = (text: string) => color(2, text); + +function details(event: Record): string { + const copy = { ...event }; + delete copy.type; + return inspect(copy, { colors: tty, depth: 8, compact: false, breakLength: 120 }); +} + +export class TerminalRenderer { + private answerStarted = false; + private thinkingShown = false; + + begin(model: string, effort: string): void { + this.answerStarted = false; + this.thinkingShown = false; + process.stdout.write(`\n${cyan("━━━━━━━━ Command Code 请求 ━━━━━━━━")}\n`); + process.stdout.write(`${dim("模型")} ${model}\n`); + process.stdout.write(`${dim("思考深度")} ${effort}\n`); + } + + event(event: Record): void { + const type = typeof event.type === "string" ? event.type : "unknown"; + + switch (type) { + case "run_start": + process.stdout.write(`${dim("会话")} ${String(event.sessionId ?? "-")}\n`); + return; + case "turn_start": + process.stdout.write(`${cyan(`\n[Turn ${String(event.turnNumber ?? "?")}]`)}\n`); + return; + case "model_request_start": + process.stdout.write(`${dim("模型请求开始")} ${String(event.model ?? "")}\n`); + return; + case "thinking_start": + if (!this.thinkingShown) { + process.stdout.write(`${dim("思考中…")}\n`); + this.thinkingShown = true; + } + return; + case "thinking_delta": + case "thinking_end": + case "message_update": + case "model_trace": + case "run_end": + return; + case "text_delta": { + if (!this.answerStarted) { + process.stdout.write(`${green("\n最终回答:")}\n`); + this.answerStarted = true; + } + process.stdout.write(String(event.delta ?? "")); + return; + } + case "model_request_end": + process.stdout.write(`\n${dim(`模型请求结束 · ${String(event.stopReason ?? "unknown")}`)}\n`); + return; + case "turn_end": + process.stdout.write(`${dim(`Turn ${String(event.turnNumber ?? "?")} 结束`)}\n`); + return; + default: + if (type.includes("tool") || type.includes("permission") || type.includes("question")) { + process.stdout.write(`${yellow(`\n[${type}]`)}\n${details(event)}\n`); + } + } + } + + stderr(chunk: string): void { + process.stderr.write(chunk); + } + + finish(durationMs: number): void { + if (this.answerStarted) process.stdout.write("\n"); + process.stdout.write(`${dim(`完成 · ${(durationMs / 1000).toFixed(2)}s`)}\n`); + process.stdout.write(`${cyan("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")}\n\n`); + } + + fail(message: string): void { + process.stderr.write(`${color(31, `\nCommand Code 失败:${message}`)}\n`); + } +} diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..efc79b9 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,188 @@ +import Fastify, { type FastifyInstance } from "fastify"; +import { ZodError } from "zod"; +import type { BridgeConfig } from "./config.js"; +import { + CommandCodeError, + inspectInstallation, + runCommandCode, + type InstallationStatus, +} from "./command-code.js"; +import { + buildCommandPrompt, + chatCompletionRequestSchema, + completionResponse, + completionStreamResponse, + openAIError, +} from "./openai.js"; + +interface ActiveRequest { + abortController: AbortController; +} + +export interface BridgeServer { + app: FastifyInstance; + installation: InstallationStatus; + abortActive: () => boolean; +} + +export async function createServer(config: BridgeConfig): Promise { + const installation = await inspectInstallation(config.command_code_executable); + const app = Fastify({ + logger: false, + bodyLimit: config.max_request_bytes, + requestTimeout: 0, + }); + let active: ActiveRequest | undefined; + + app.setErrorHandler((error, _request, reply) => { + const fastifyError = error as Error & { code?: string; statusCode?: number }; + if (fastifyError.code === "FST_ERR_CTP_BODY_TOO_LARGE") { + reply.status(413).send(openAIError( + `Request body exceeds max_request_bytes (${config.max_request_bytes})`, + "request_too_large", + )); + return; + } + + const errorText = error instanceof Error ? (error.stack ?? error.message) : String(error); + process.stderr.write(`${errorText}\n`); + reply.status(fastifyError.statusCode ?? 500).send(openAIError( + "Internal bridge error", + "bridge_error", + "server_error", + )); + }); + + app.get("/health", async () => ({ + status: installation.installed && installation.authenticated ? "ok" : "degraded", + command_code: installation, + busy: active !== undefined, + })); + + app.get("/v1/models", async () => ({ + object: "list", + data: Object.keys(config.models).map((id) => ({ + id, + object: "model", + created: 0, + owned_by: "command-code-local", + })), + })); + + app.post("/v1/chat/completions", async (request, reply) => { + if (active) { + return reply.status(429).send(openAIError( + "Another Command Code request is already running", + "busy", + "server_error", + )); + } + + if (!installation.installed) { + return reply.status(503).send(openAIError( + "Command Code CLI is not installed", + "command_code_not_installed", + "server_error", + )); + } + + if (!installation.authenticated) { + return reply.status(503).send(openAIError( + "Command Code is not authenticated; run command-code login", + "command_code_not_authenticated", + "server_error", + )); + } + + let parsed; + try { + parsed = chatCompletionRequestSchema.parse(request.body); + } catch (error) { + const message = error instanceof ZodError + ? error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ") + : "Invalid request body"; + return reply.status(400).send(openAIError(message, "invalid_request")); + } + + const model = config.models[parsed.model]; + if (!model) { + return reply.status(404).send(openAIError( + `Model '${parsed.model}' is not configured`, + "model_not_found", + )); + } + + const abortController = new AbortController(); + active = { abortController }; + let responseCompleted = false; + + const cancelOnDisconnect = () => { + if (!responseCompleted) abortController.abort("client disconnected"); + }; + request.raw.once("aborted", cancelOnDisconnect); + reply.raw.once("close", cancelOnDisconnect); + + try { + const result = await runCommandCode( + config, + model.cli_model, + model.effort, + buildCommandPrompt(parsed), + abortController.signal, + ); + responseCompleted = true; + if (parsed.stream) { + return reply + .type("text/event-stream; charset=utf-8") + .header("Cache-Control", "no-cache") + .header("Connection", "keep-alive") + .send(completionStreamResponse( + parsed.model, + result.finalText, + result.usage, + parsed.stream_options?.include_usage, + )); + } + return reply.send(completionResponse(parsed.model, result.finalText, result.usage)); + } catch (error) { + responseCompleted = true; + if (abortController.signal.aborted) { + if (!reply.raw.destroyed) { + return reply.status(499).send(openAIError( + "Command Code request cancelled", + "cancelled", + "server_error", + )); + } + return; + } + + const code = error instanceof CommandCodeError ? error.exitCode : undefined; + const status = code === 5 ? 429 : code === 10 ? 402 : 502; + const errorCode = code === 5 + ? "rate_limit_exceeded" + : code === 10 + ? "insufficient_credits" + : "command_code_error"; + return reply.status(status).send(openAIError( + "Command Code request failed", + errorCode, + "server_error", + )); + } finally { + request.raw.off("aborted", cancelOnDisconnect); + reply.raw.off("close", cancelOnDisconnect); + active = undefined; + } + }); + + return { + app, + installation, + abortActive: () => { + if (!active) return false; + active.abortController.abort("bridge interrupted"); + return true; + }, + }; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9eaba00 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "sourceMap": true + }, + "include": ["src/**/*.ts"] +}