@covro/covro 0.0.0-stage → 0.1.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +173 -2
  3. package/README.zh-CN.md +174 -0
  4. package/bin/covro.mjs +8 -0
  5. package/dist/cli/control.js +66 -0
  6. package/dist/cli/control.js.map +7 -0
  7. package/dist/cli/daemon.js +118 -0
  8. package/dist/cli/daemon.js.map +7 -0
  9. package/dist/cli/main.js +152 -0
  10. package/dist/cli/main.js.map +7 -0
  11. package/dist/cli/paths.js +60 -0
  12. package/dist/cli/paths.js.map +7 -0
  13. package/dist/cli/service.js +87 -0
  14. package/dist/cli/service.js.map +7 -0
  15. package/dist/codex/adapter.js +1195 -0
  16. package/dist/codex/adapter.js.map +7 -0
  17. package/dist/codex/auto-review.js +102 -0
  18. package/dist/codex/auto-review.js.map +7 -0
  19. package/dist/codex/compatibility.js +7 -0
  20. package/dist/codex/compatibility.js.map +7 -0
  21. package/dist/codex/mapping.js +210 -0
  22. package/dist/codex/mapping.js.map +7 -0
  23. package/dist/codex/permissions.js +51 -0
  24. package/dist/codex/permissions.js.map +7 -0
  25. package/dist/codex/thread-title-policy.js +52 -0
  26. package/dist/codex/thread-title-policy.js.map +7 -0
  27. package/dist/codex/thread-titles.js +259 -0
  28. package/dist/codex/thread-titles.js.map +7 -0
  29. package/dist/codex/transport.js +236 -0
  30. package/dist/codex/transport.js.map +7 -0
  31. package/dist/codex/turn-token-usage.js +46 -0
  32. package/dist/codex/turn-token-usage.js.map +7 -0
  33. package/dist/codex/types.js +1 -0
  34. package/dist/codex/types.js.map +7 -0
  35. package/dist/drizzle/0000_past_wildside.sql +45 -0
  36. package/dist/drizzle/0001_shocking_ender_wiggin.sql +2 -0
  37. package/dist/drizzle/0002_allow_parallel_threads.sql +1 -0
  38. package/dist/drizzle/0003_project_registrations.sql +11 -0
  39. package/dist/drizzle/0004_turn_usage.sql +6 -0
  40. package/dist/drizzle/meta/0000_snapshot.json +309 -0
  41. package/dist/drizzle/meta/0001_snapshot.json +323 -0
  42. package/dist/drizzle/meta/0002_snapshot.json +315 -0
  43. package/dist/drizzle/meta/0003_snapshot.json +381 -0
  44. package/dist/drizzle/meta/0004_snapshot.json +420 -0
  45. package/dist/drizzle/meta/_journal.json +41 -0
  46. package/dist/server/auth.js +47 -0
  47. package/dist/server/auth.js.map +7 -0
  48. package/dist/server/compaction.js +40 -0
  49. package/dist/server/compaction.js.map +7 -0
  50. package/dist/server/config-store.js +147 -0
  51. package/dist/server/config-store.js.map +7 -0
  52. package/dist/server/config.js +124 -0
  53. package/dist/server/config.js.map +7 -0
  54. package/dist/server/db/index.js +21 -0
  55. package/dist/server/db/index.js.map +7 -0
  56. package/dist/server/db/migrate.js +26 -0
  57. package/dist/server/db/migrate.js.map +7 -0
  58. package/dist/server/db/operations.js +201 -0
  59. package/dist/server/db/operations.js.map +7 -0
  60. package/dist/server/db/schema.js +99 -0
  61. package/dist/server/db/schema.js.map +7 -0
  62. package/dist/server/db/turn-usage.js +31 -0
  63. package/dist/server/db/turn-usage.js.map +7 -0
  64. package/dist/server/events.js +123 -0
  65. package/dist/server/events.js.map +7 -0
  66. package/dist/server/http.js +305 -0
  67. package/dist/server/http.js.map +7 -0
  68. package/dist/server/images.js +49 -0
  69. package/dist/server/images.js.map +7 -0
  70. package/dist/server/instance-lock.js +25 -0
  71. package/dist/server/instance-lock.js.map +7 -0
  72. package/dist/server/local-files.js +100 -0
  73. package/dist/server/local-files.js.map +7 -0
  74. package/dist/server/main.js +125 -0
  75. package/dist/server/main.js.map +7 -0
  76. package/dist/server/markdown.js +28 -0
  77. package/dist/server/markdown.js.map +7 -0
  78. package/dist/server/projection.js +348 -0
  79. package/dist/server/projection.js.map +7 -0
  80. package/dist/server/projects.js +230 -0
  81. package/dist/server/projects.js.map +7 -0
  82. package/dist/server/service.js +1221 -0
  83. package/dist/server/service.js.map +7 -0
  84. package/dist/server/snapshot.js +98 -0
  85. package/dist/server/snapshot.js.map +7 -0
  86. package/dist/server/thread-export.js +97 -0
  87. package/dist/server/thread-export.js.map +7 -0
  88. package/dist/server/websocket.js +126 -0
  89. package/dist/server/websocket.js.map +7 -0
  90. package/dist/shared/async-questions.js +39 -0
  91. package/dist/shared/async-questions.js.map +7 -0
  92. package/dist/shared/codex-compatibility.js +56 -0
  93. package/dist/shared/codex-compatibility.js.map +7 -0
  94. package/dist/shared/elicitation.js +314 -0
  95. package/dist/shared/elicitation.js.map +7 -0
  96. package/dist/shared/errors.js +24 -0
  97. package/dist/shared/errors.js.map +7 -0
  98. package/dist/shared/gateway-config.js +1 -0
  99. package/dist/shared/gateway-config.js.map +7 -0
  100. package/dist/shared/images.js +21 -0
  101. package/dist/shared/images.js.map +7 -0
  102. package/dist/shared/package.js +5 -0
  103. package/dist/shared/package.js.map +7 -0
  104. package/dist/shared/permissions.js +63 -0
  105. package/dist/shared/permissions.js.map +7 -0
  106. package/dist/shared/thread-name.js +14 -0
  107. package/dist/shared/thread-name.js.map +7 -0
  108. package/dist/shared/thread-summary.js +24 -0
  109. package/dist/shared/thread-summary.js.map +7 -0
  110. package/dist/shared/types.js +1 -0
  111. package/dist/shared/types.js.map +7 -0
  112. package/dist/shared/updates.js +7 -0
  113. package/dist/shared/updates.js.map +7 -0
  114. package/dist/shared/validation.js +116 -0
  115. package/dist/shared/validation.js.map +7 -0
  116. package/dist/shared/versions.js +36 -0
  117. package/dist/shared/versions.js.map +7 -0
  118. package/dist/update-worker.cjs +21968 -0
  119. package/dist/updates/files.js +54 -0
  120. package/dist/updates/files.js.map +7 -0
  121. package/dist/updates/manager.js +315 -0
  122. package/dist/updates/manager.js.map +7 -0
  123. package/dist/updates/registry.js +71 -0
  124. package/dist/updates/registry.js.map +7 -0
  125. package/dist/updates/transaction.js +96 -0
  126. package/dist/updates/transaction.js.map +7 -0
  127. package/dist/updates/worker.js +345 -0
  128. package/dist/updates/worker.js.map +7 -0
  129. package/dist/web/assets/index-BW9afH_Y.js +83 -0
  130. package/dist/web/assets/index-L3S1ZiO0.css +1 -0
  131. package/dist/web/covro.svg +6 -0
  132. package/dist/web/index.html +15 -0
  133. package/package.json +88 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 郁蒸十四
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/covro-logo-dark.svg" />
4
+ <img src="docs/assets/covro-logo.svg" alt="Covro" width="266" height="64" />
5
+ </picture>
6
+ </p>
2
7
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
8
+ <h1 align="center">Codex CLI. Now in your browser.</h1>
9
+
10
+ <p align="center">
11
+ <strong>Covro is a self-hosted Web UI for Codex CLI.</strong><br />
12
+ Start a task at your desk. Review the changes, approve the next step, and keep it moving from your phone.
13
+ </p>
14
+
15
+ <p align="center"><strong>Self-hosted · Desktop & mobile · Your existing Codex login · MIT</strong></p>
16
+
17
+ <p align="center">
18
+ <a href="#get-started">🚀 Get started</a> ·
19
+ <a href="#what-you-can-do">✨ Features</a> ·
20
+ <a href="#access-from-another-device">🌐 Remote access</a> ·
21
+ <a href="README.zh-CN.md">简体中文</a>
22
+ </p>
23
+
24
+ ![Covro desktop workspace with a conversation and a Markdown document side by side](docs/assets/workspace-desktop.png)
25
+
26
+ <p align="center"><sub>Captured from the running app with sample projects and fixture-generated conversations.</sub></p>
27
+
28
+ <a id="what-you-can-do"></a>
29
+
30
+ ## ✨ What you can do
31
+
32
+ - 📱 **Step away from your desk. Stay with the task.** Follow tool activity, inspect file changes, handle permission requests, and answer Codex's questions from your computer or phone. Closing the page doesn't cancel the task.
33
+ - 🗂️ **See every project in one place.** Run independent tasks in parallel and spot which conversations are working, waiting for approval, or ready to read.
34
+ - 💬 **Pick up your Codex CLI conversations.** Browse saved history, resume existing work, or fork a new direction. Rename conversations to find them later and archive them when you're done.
35
+ - 📖 **Review the work beside the conversation.** Read Markdown documents in a desktop side panel while you keep talking to Codex. View code, tables, and images, with a full-screen document reader on mobile.
36
+ - ✍️ **Switch tasks without losing your draft.** Each conversation keeps its own draft, pasted images, and model settings in the current page. Copy or export results when you're done.
37
+
38
+ <details>
39
+ <summary><strong>📱 A workspace that fits your phone</strong></summary>
40
+ <p>Check a result, answer a question, or send the next instruction without returning to your desk.</p>
41
+ <img src="docs/assets/workspace-mobile.png" alt="Covro conversation on a phone-sized screen" width="320" />
42
+ </details>
43
+
44
+ <a id="get-started"></a>
45
+
46
+ ## 🚀 Get started
47
+
48
+ You need **Node.js 24.19 or later in the 24.x line** and an installed, signed-in **Codex CLI**. The interface currently uses Simplified Chinese.
49
+
50
+ <!-- codex-compatibility:start -->
51
+
52
+ Recommended Codex CLI: **0.163.0**. Verified versions: `0.163.0`.
53
+
54
+ <!-- codex-compatibility:end -->
55
+
56
+ <a id="npm-installation"></a>
57
+
58
+ ### 📦 npm installation
59
+
60
+ The first public npm release is being prepared. Until `@covro/covro` is published, [run from source](#run-from-source). After publication, install and start with:
61
+
62
+ ```sh
63
+ npm install -g @covro/covro
64
+ covro
65
+ ```
66
+
67
+ `covro` starts in the background and prints the local address. Open **<http://127.0.0.1:8787>** to get started. Close the terminal and keep working in your browser.
68
+
69
+ | Command | Purpose |
70
+ | ------------------------- | -------------------------------------------- |
71
+ | `covro status` | Show the running instance and its address |
72
+ | `covro logs` | Read recent service logs |
73
+ | `covro stop` | Stop the service |
74
+ | `covro run` | Run in the foreground |
75
+ | `covro service install` | Opt in to login auto-start on macOS or Linux |
76
+ | `covro service uninstall` | Remove login auto-start |
77
+
78
+ The npm package is `@covro/covro`; the command is `covro`. Install the development channel with `npm install -g @covro/covro@dev`.
79
+
80
+ The npm CLI uses `~/.covro/config.json` and a fixed data directory beside it. Reuse an existing configuration with `covro --config /absolute/path/config.json`.
81
+
82
+ <a id="run-from-source"></a>
83
+
84
+ ### 🛠️ Run from source
85
+
86
+ To try Covro before the npm release or work on the code, use **pnpm 12.11.2**:
87
+
88
+ ```sh
89
+ git clone https://github.com/yuzheng14/codex-web-ui.git covro
90
+ cd covro
91
+ pnpm install --frozen-lockfile
92
+ cp config.example.json config.local.json
93
+ pnpm build
94
+ pnpm start -- --config ./config.local.json
95
+ ```
96
+
97
+ Open **<http://127.0.0.1:8787>**, click **添加项目** (Add project), and enter a project directory on the computer running Covro. Open a conversation or send your first task.
98
+
99
+ For local development, run `pnpm dev` instead and open <http://127.0.0.1:5173>.
100
+
101
+ <a id="access-from-another-device"></a>
102
+
103
+ ## 🌐 Access from another device
104
+
105
+ Covro runs on your computer; the browser is your control surface. Tasks continue when you close the page, and reopening it reconnects to the service. The host computer must stay awake and online.
106
+
107
+ | Setup | How to connect |
108
+ | ------------------------------ | -------------------------------------------------------------------- |
109
+ | On the host computer | Open the local address |
110
+ | Your devices through Tailscale | Configure Tailscale Serve and add its HTTPS origin to `publicOrigin` |
111
+ | Through Cloudflare | Use a tunnel with Cloudflare Access and Covro's JWT verification |
112
+
113
+ Follow the [deployment guide](docs/deployment.md) for complete configuration. Remote access is explicit: a fresh installation listens on loopback only. When adding projects remotely, choose directories on the host computer.
114
+
115
+ ## Updates and release channels
116
+
117
+ Open **Covro** in the sidebar to check versions, choose a channel, enable idle-time automatic upgrades, or upgrade and restart. The CLI provides the same update engine:
118
+
119
+ ```sh
120
+ covro update status
121
+ covro update
122
+ covro update --channel dev
123
+ ```
124
+
125
+ | Channel | npm tag | Choose it for |
126
+ | ------- | -------- | ------------------------------------------- |
127
+ | Stable | `latest` | Regular use |
128
+ | Beta | `beta` | Preview releases selected by the maintainer |
129
+ | Dev | `dev` | New main-branch changes after CI passes |
130
+
131
+ Each release declares its recommended and verified Codex CLI versions. The update panel shows the current and candidate combinations. Automatic upgrades require a verified combination; an unverified combination needs explicit confirmation for that exact upgrade. Unknown versions, invalid/missing declarations, and explicitly unsupported combinations block managed upgrades. The CLI offers `covro update --allow-unverified-codex` for the same explicit attempt. Covro does not upgrade your Codex CLI.
132
+
133
+ Automatic installation is off by default. Managed upgrades support **macOS and Linux npm global installations**; source, linked, container, and Windows installations use their original update method. A failed candidate is rolled back before business writes resume; downgrades must pass database compatibility checks. Old backups are retained without automatic rotation.
134
+
135
+ See [distribution and recovery](docs/distribution.md) and [release operations](docs/releases.md) for details.
136
+
137
+ ## Your computer, your workspace
138
+
139
+ Covro is a single-user Web UI for your existing Codex CLI. It connects through `codex app-server`, which executes tasks on the host computer using your Codex login and configuration. Projects and Codex history stay there; Covro stores its own project registry and operation records in SQLite. It does not provide a separate hosted account or copy your Codex login into the browser. Codex itself still uses its configured model provider.
140
+
141
+ Use the browser to follow work, review requests, and continue conversations. An interactive terminal, file editor, Git GUI, and multi-user team management are outside the current scope.
142
+
143
+ ## Development and contribution
144
+
145
+ ```sh
146
+ pnpm typecheck
147
+ pnpm lint
148
+ pnpm test
149
+ pnpm build
150
+ pnpm test:e2e
151
+ ```
152
+
153
+ Browser tests require the Chromium version used by the existing Playwright dependency. Tests and CI use explicit fixtures; they do not send real model requests.
154
+
155
+ Found a problem or have an idea? [Open an issue](https://github.com/yuzheng14/codex-web-ui/issues). Include your OS, Node.js and Codex versions, installation method, and steps to reproduce. Small, focused PRs are welcome. Update both `README.md` and `README.zh-CN.md` when changing user-facing facts or setup instructions.
156
+
157
+ <a id="documentation"></a>
158
+
159
+ ## 📚 Documentation
160
+
161
+ | Guide | Contents |
162
+ | -------------------------------------------- | ----------------------------------------------------------------------- |
163
+ | [Detailed usage · 中文](docs/usage.zh-CN.md) | Projects, conversations, approvals, notifications, images, and recovery |
164
+ | [Deployment](docs/deployment.md) | Local access, Tailscale, and Cloudflare Access |
165
+ | [Distribution](docs/distribution.md) | CLI, services, data locations, upgrades, and repair |
166
+ | [Compatibility](docs/compatibility.md) | Verified Codex versions and behavior boundaries |
167
+ | [Slash commands](docs/slash-commands.md) | Supported commands and usage |
168
+ | [Architecture](docs/architecture.md) | How the gateway, Codex adapter, and browser fit together |
169
+ | [Verification](docs/verification.md) | Test coverage and recorded live checks |
170
+ | [Brand assets](docs/brand.md) | Editable SVG logo and wordmarks |
171
+
172
+ ## License
173
+
174
+ [MIT](LICENSE). Covro is an independent project and is not affiliated with OpenAI.
@@ -0,0 +1,174 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/covro-logo-dark.svg" />
4
+ <img src="docs/assets/covro-logo.svg" alt="Covro" width="266" height="64" />
5
+ </picture>
6
+ </p>
7
+
8
+ <h1 align="center">在浏览器里,用你的 Codex CLI。</h1>
9
+
10
+ <p align="center">
11
+ <strong>Covro 是 Codex CLI 的自部署 Web UI。</strong><br />
12
+ 在桌前发起任务,用手机查看改动、处理审批,接着推进下一步。
13
+ </p>
14
+
15
+ <p align="center"><strong>自行部署 · 电脑与手机 · 沿用 Codex 登录 · MIT 开源</strong></p>
16
+
17
+ <p align="center">
18
+ <a href="#开始使用">🚀 开始使用</a> ·
19
+ <a href="#你可以做什么">✨ 功能介绍</a> ·
20
+ <a href="#从其他设备访问">🌐 远程访问</a> ·
21
+ <a href="README.md">English</a>
22
+ </p>
23
+
24
+ ![Covro 桌面工作台:会话与 Markdown 文档并排阅读](docs/assets/workspace-desktop.png)
25
+
26
+ <p align="center"><sub>截图来自实际运行的应用,使用示例项目与测试夹具生成的会话。</sub></p>
27
+
28
+ <a id="你可以做什么"></a>
29
+
30
+ ## ✨ 你可以做什么
31
+
32
+ - 📱 **离开电脑,也能跟进任务。** 在电脑或手机上查看工具活动与文件变更、处理权限审批、回答 Codex 的问题。关闭页面,任务仍会继续。
33
+ - 🗂️ **多个项目,一眼掌握进展。** 让独立任务并行执行,随时看到哪些会话正在工作、等待审批,哪些已有结果可读。
34
+ - 💬 **接着你的 Codex CLI 会话。** 浏览已有历史、恢复工作,或 Fork 一个新方向;为会话命名以便查找,完成后归档。
35
+ - 📖 **一边看产出,一边继续讨论。** 电脑上将 Markdown 文档与会话并排打开,继续向 Codex 提问;支持代码、表格与图片,手机上也有全屏文档阅读器。
36
+ - ✍️ **切换任务,草稿还在。** 每个会话分别保留当前页面中的草稿、粘贴图片和模型设置;完成后可以复制或导出结果。
37
+
38
+ <details>
39
+ <summary><strong>📱 手机上也能舒服地使用</strong></summary>
40
+ <p>查看结果、回答问题、补充下一步要求,不必每次都回到电脑前。</p>
41
+ <img src="docs/assets/workspace-mobile.png" alt="手机屏幕上的 Covro 会话" width="320" />
42
+ </details>
43
+
44
+ <a id="开始使用"></a>
45
+
46
+ ## 🚀 开始使用
47
+
48
+ 需要 **Node.js 24.x,版本不低于 24.19**,以及已经安装、登录的 **Codex CLI**。界面目前使用简体中文。
49
+
50
+ <!-- codex-compatibility:start -->
51
+
52
+ 推荐 Codex CLI:**0.163.0**。已验证版本:`0.163.0`。
53
+
54
+ <!-- codex-compatibility:end -->
55
+
56
+ <a id="npm-安装"></a>
57
+
58
+ ### 📦 npm 安装
59
+
60
+ 首个公开 npm 版本正在准备中。`@covro/covro` 发布前,请先[从源码运行](#从源码运行);发布后,只需安装并启动:
61
+
62
+ ```sh
63
+ npm install -g @covro/covro
64
+ covro
65
+ ```
66
+
67
+ `covro` 默认在后台启动,并输出本机访问地址。打开 **<http://127.0.0.1:8787>** 即可开始使用。关掉终端后,仍可继续通过浏览器工作。
68
+
69
+ | 命令 | 用途 |
70
+ | ------------------------- | ------------------------------------ |
71
+ | `covro status` | 查看运行实例与访问地址 |
72
+ | `covro logs` | 查看最近的服务日志 |
73
+ | `covro stop` | 停止服务 |
74
+ | `covro run` | 在前台运行 |
75
+ | `covro service install` | 在 macOS 或 Linux 上主动启用登录自启 |
76
+ | `covro service uninstall` | 移除登录自启 |
77
+
78
+ npm 包名为 `@covro/covro`,命令为 `covro`。开发渠道使用 `npm install -g @covro/covro@dev` 安装。
79
+
80
+ npm CLI 默认使用 `~/.covro/config.json`,数据目录固定在配置文件旁。可以通过 `covro --config /absolute/path/config.json` 沿用已有配置。
81
+
82
+ <a id="从源码运行"></a>
83
+
84
+ ### 🛠️ 从源码运行
85
+
86
+ 想在 npm 发布前体验,或参与开发,可以使用 **pnpm 12.11.2** 从源码运行:
87
+
88
+ ```sh
89
+ git clone https://github.com/yuzheng14/codex-web-ui.git covro
90
+ cd covro
91
+ pnpm install --frozen-lockfile
92
+ cp config.example.json config.local.json
93
+ pnpm build
94
+ pnpm start -- --config ./config.local.json
95
+ ```
96
+
97
+ 打开 **<http://127.0.0.1:8787>**,点击 **添加项目**,填写运行 Covro 的电脑上的项目目录,然后打开会话或发送第一条任务。
98
+
99
+ 本地开发可以改用 `pnpm dev`,打开 <http://127.0.0.1:5173>。
100
+
101
+ <a id="从其他设备访问"></a>
102
+
103
+ ## 🌐 从其他设备访问
104
+
105
+ Covro 运行在你的电脑上,浏览器是操作入口。关闭页面不会取消任务;重新打开后会连接到服务。运行服务的电脑需要保持唤醒并在线。
106
+
107
+ | 使用方式 | 连接方法 |
108
+ | ----------------------------- | -------------------------------------------------------- |
109
+ | 在运行服务的电脑上 | 打开本机地址 |
110
+ | 通过 Tailscale 访问自己的设备 | 配置 Tailscale Serve,并将 HTTPS 地址加入 `publicOrigin` |
111
+ | 通过 Cloudflare 访问 | 使用 Tunnel、Cloudflare Access 及 Covro 的 JWT 校验 |
112
+
113
+ 完整配置见[部署指南](docs/deployment.md)。新安装默认只监听回环地址,远程访问需单独配置。在手机上添加项目时,填写的仍是运行服务的电脑上的目录。
114
+
115
+ ## 更新与版本渠道
116
+
117
+ 点击侧栏的 **Covro**,可以检查版本、选择渠道、启用空闲时自动升级,或一键升级并重启。CLI 使用同一个升级引擎:
118
+
119
+ ```sh
120
+ covro update status
121
+ covro update
122
+ covro update --channel dev
123
+ ```
124
+
125
+ | 渠道 | npm 标签 | 适合 |
126
+ | ------ | -------- | --------------------------- |
127
+ | 稳定版 | `latest` | 日常使用 |
128
+ | 预览版 | `beta` | 维护者选定的预览版本 |
129
+ | 开发版 | `dev` | CI 通过后的 main 分支新功能 |
130
+
131
+ 每个发行版声明推荐和已验证的 Codex CLI 版本,更新面板会同时显示当前与目标组合。自动升级只接受已验证组合;未验证组合需要对确切目标明确确认。版本未知、声明缺失/无效或明确不支持的组合会阻止受管升级。CLI 可通过 `covro update --allow-unverified-codex` 明确尝试同一组合。Covro 不会自动升级你的 Codex CLI。
132
+
133
+ 自动安装默认关闭。受管升级支持 **macOS 和 Linux 的 npm 全局安装**;源码、链接、容器和 Windows 安装沿用原更新方式。候选版本失败时,会在重新开放业务写入前回滚;降级需通过数据库兼容检查。旧备份会保留,暂不自动轮转。
134
+
135
+ 详细说明见[分发与恢复](docs/distribution.md)、[发版操作](docs/releases.md)。
136
+
137
+ ## 你的电脑,你的工作台
138
+
139
+ Covro 是面向单用户的 Codex CLI Web UI,通过 `codex app-server` 连接已有 CLI,沿用你的 Codex 登录与配置,在运行服务的电脑上执行任务。项目与 Codex 历史留在那里;Covro 使用 SQLite 保存自身的项目注册和操作记录。它不提供单独的托管账号,也不会把 Codex 登录信息复制到浏览器。Codex 本身仍会访问你配置的模型提供方。
140
+
141
+ 浏览器用于跟进工作、处理请求和继续对话。交互终端、文件编辑器、Git 图形界面与多人团队管理不在当前范围内。
142
+
143
+ ## 开发与参与
144
+
145
+ ```sh
146
+ pnpm typecheck
147
+ pnpm lint
148
+ pnpm test
149
+ pnpm build
150
+ pnpm test:e2e
151
+ ```
152
+
153
+ 浏览器测试需要现有 Playwright 依赖所对应的 Chromium。测试与 CI 使用明确的测试夹具,不发起真实模型请求。
154
+
155
+ 遇到问题或有想法,欢迎[提交 Issue](https://github.com/yuzheng14/codex-web-ui/issues),附上操作系统、Node.js 与 Codex 版本、安装方式和复现步骤。也欢迎范围明确的小 PR。修改面向使用者的事实或安装步骤时,请同步更新 `README.md` 和 `README.zh-CN.md`。
156
+
157
+ <a id="文档导航"></a>
158
+
159
+ ## 📚 文档导航
160
+
161
+ | 指南 | 内容 |
162
+ | ------------------------------------ | ------------------------------------ |
163
+ | [详细使用指南](docs/usage.zh-CN.md) | 项目、会话、审批、通知、图片和恢复 |
164
+ | [部署](docs/deployment.md) | 本机、Tailscale 与 Cloudflare Access |
165
+ | [分发](docs/distribution.md) | CLI、服务、数据位置、升级与修复 |
166
+ | [兼容性](docs/compatibility.md) | 已验证的 Codex 版本与行为边界 |
167
+ | [Slash 命令](docs/slash-commands.md) | 支持的命令和用法 |
168
+ | [架构](docs/architecture.md) | 网关、Codex 适配器与浏览器的协作方式 |
169
+ | [验证记录](docs/verification.md) | 测试范围与真实环境检查记录 |
170
+ | [品牌素材](docs/brand.md) | 可编辑的 SVG 图标与完整标识 |
171
+
172
+ ## 许可证
173
+
174
+ [MIT](LICENSE)。Covro 是独立项目,与 OpenAI 无隶属关系。
package/bin/covro.mjs ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import console from 'node:console';
3
+ import process from 'node:process';
4
+ import { main } from '../dist/cli/main.js';
5
+ await main().catch((error) => {
6
+ console.error(error instanceof Error ? error.message : 'Covro failed');
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1,66 @@
1
+ import { request } from "node:http";
2
+ import lockfile from "proper-lockfile";
3
+ import { setTimeout } from "node:timers/promises";
4
+ function control(location, action, input = {}) {
5
+ return new Promise((resolve, reject) => {
6
+ const requestBody = JSON.stringify(input);
7
+ const outgoing = request(
8
+ {
9
+ socketPath: location.socket,
10
+ path: `/${action}`,
11
+ method: "POST",
12
+ headers: {
13
+ "content-type": "application/json",
14
+ "content-length": Buffer.byteLength(requestBody)
15
+ }
16
+ },
17
+ (incoming) => {
18
+ let body = "";
19
+ incoming.on("data", (chunk) => {
20
+ body += chunk.toString();
21
+ if (body.length > 256 * 1024) incoming.destroy(new Error("Control response too large"));
22
+ });
23
+ incoming.on("error", reject);
24
+ incoming.on("end", () => {
25
+ try {
26
+ const result = JSON.parse(body);
27
+ if (incoming.statusCode !== 200)
28
+ reject(new Error(result.error ?? "Control request failed"));
29
+ else resolve(result);
30
+ } catch (error) {
31
+ reject(error);
32
+ }
33
+ });
34
+ }
35
+ );
36
+ outgoing.setTimeout(15e3, () => outgoing.destroy(new Error("Covro control timed out")));
37
+ outgoing.on("error", reject);
38
+ outgoing.end(requestBody);
39
+ });
40
+ }
41
+ function isNotRunning(error) {
42
+ return ["ENOENT", "ECONNREFUSED"].includes(error.code ?? "");
43
+ }
44
+ async function stopAndWait(location) {
45
+ try {
46
+ await control(location, "stop");
47
+ } catch (error) {
48
+ if (!isNotRunning(error)) throw error;
49
+ }
50
+ for (let attempt = 0; attempt < 120; attempt++) {
51
+ try {
52
+ if (!await lockfile.check(location.state, { stale: 1e4 })) return;
53
+ } catch (error) {
54
+ if (error.code === "ENOENT") return;
55
+ throw error;
56
+ }
57
+ await setTimeout(100);
58
+ }
59
+ throw new Error("Covro \u5C1A\u672A\u5B8C\u6210\u5173\u95ED\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
60
+ }
61
+ export {
62
+ control,
63
+ isNotRunning,
64
+ stopAndWait
65
+ };
66
+ //# sourceMappingURL=control.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cli/control.ts"],
4
+ "sourcesContent": ["import { request } from 'node:http';\nimport type { CovroPaths } from './paths.js';\nimport lockfile from 'proper-lockfile';\nimport { setTimeout } from 'node:timers/promises';\n\n/** Local owner control uses a private socket, never the public HTTP authentication boundary. */\nexport function control<T = unknown>(\n location: CovroPaths,\n action: string,\n input: object = {},\n): Promise<T> {\n return new Promise((resolve, reject) => {\n const requestBody = JSON.stringify(input);\n const outgoing = request(\n {\n socketPath: location.socket,\n path: `/${action}`,\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n 'content-length': Buffer.byteLength(requestBody),\n },\n },\n (incoming) => {\n let body = '';\n incoming.on('data', (chunk: Buffer) => {\n body += chunk.toString();\n if (body.length > 256 * 1024) incoming.destroy(new Error('Control response too large'));\n });\n incoming.on('error', reject);\n incoming.on('end', () => {\n try {\n const result = JSON.parse(body) as T & { error?: string };\n if (incoming.statusCode !== 200)\n reject(new Error(result.error ?? 'Control request failed'));\n else resolve(result);\n } catch (error) {\n reject(error);\n }\n });\n },\n );\n outgoing.setTimeout(15_000, () => outgoing.destroy(new Error('Covro control timed out')));\n outgoing.on('error', reject);\n outgoing.end(requestBody);\n });\n}\nexport function isNotRunning(error: unknown) {\n return ['ENOENT', 'ECONNREFUSED'].includes((error as NodeJS.ErrnoException).code ?? '');\n}\nexport async function stopAndWait(location: CovroPaths) {\n try {\n await control(location, 'stop');\n } catch (error) {\n if (!isNotRunning(error)) throw error;\n }\n for (let attempt = 0; attempt < 120; attempt++) {\n try {\n if (!(await lockfile.check(location.state, { stale: 10_000 }))) return;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;\n throw error;\n }\n await setTimeout(100);\n }\n throw new Error('Covro \u5C1A\u672A\u5B8C\u6210\u5173\u95ED\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5');\n}\n"],
5
+ "mappings": "AAAA,SAAS,eAAe;AAExB,OAAO,cAAc;AACrB,SAAS,kBAAkB;AAGpB,SAAS,QACd,UACA,QACA,QAAgB,CAAC,GACL;AACZ,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAM,cAAc,KAAK,UAAU,KAAK;AACxC,UAAM,WAAW;AAAA,MACf;AAAA,QACE,YAAY,SAAS;AAAA,QACrB,MAAM,IAAI,MAAM;AAAA,QAChB,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,kBAAkB,OAAO,WAAW,WAAW;AAAA,QACjD;AAAA,MACF;AAAA,MACA,CAAC,aAAa;AACZ,YAAI,OAAO;AACX,iBAAS,GAAG,QAAQ,CAAC,UAAkB;AACrC,kBAAQ,MAAM,SAAS;AACvB,cAAI,KAAK,SAAS,MAAM,KAAM,UAAS,QAAQ,IAAI,MAAM,4BAA4B,CAAC;AAAA,QACxF,CAAC;AACD,iBAAS,GAAG,SAAS,MAAM;AAC3B,iBAAS,GAAG,OAAO,MAAM;AACvB,cAAI;AACF,kBAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,gBAAI,SAAS,eAAe;AAC1B,qBAAO,IAAI,MAAM,OAAO,SAAS,wBAAwB,CAAC;AAAA,gBACvD,SAAQ,MAAM;AAAA,UACrB,SAAS,OAAO;AACd,mBAAO,KAAK;AAAA,UACd;AAAA,QACF,CAAC;AAAA,MACH;AAAA,IACF;AACA,aAAS,WAAW,MAAQ,MAAM,SAAS,QAAQ,IAAI,MAAM,yBAAyB,CAAC,CAAC;AACxF,aAAS,GAAG,SAAS,MAAM;AAC3B,aAAS,IAAI,WAAW;AAAA,EAC1B,CAAC;AACH;AACO,SAAS,aAAa,OAAgB;AAC3C,SAAO,CAAC,UAAU,cAAc,EAAE,SAAU,MAAgC,QAAQ,EAAE;AACxF;AACA,eAAsB,YAAY,UAAsB;AACtD,MAAI;AACF,UAAM,QAAQ,UAAU,MAAM;AAAA,EAChC,SAAS,OAAO;AACd,QAAI,CAAC,aAAa,KAAK,EAAG,OAAM;AAAA,EAClC;AACA,WAAS,UAAU,GAAG,UAAU,KAAK,WAAW;AAC9C,QAAI;AACF,UAAI,CAAE,MAAM,SAAS,MAAM,SAAS,OAAO,EAAE,OAAO,IAAO,CAAC,EAAI;AAAA,IAClE,SAAS,OAAO;AACd,UAAK,MAAgC,SAAS,SAAU;AACxD,YAAM;AAAA,IACR;AACA,UAAM,WAAW,GAAG;AAAA,EACtB;AACA,QAAM,IAAI,MAAM,gFAAoB;AACtC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,118 @@
1
+ import { createServer } from "node:http";
2
+ import { chmod, mkdir, rm } from "node:fs/promises";
3
+ import { acquireInstanceLock } from "../server/instance-lock.js";
4
+ import { loadConfig } from "../server/config.js";
5
+ import { startGateway } from "../server/main.js";
6
+ import { packageInfo, paths } from "./paths.js";
7
+ import { UpdateManager } from "../updates/manager.js";
8
+ import { saveJson } from "../updates/files.js";
9
+ async function runDaemon(configPath) {
10
+ const location = paths(configPath);
11
+ await mkdir(location.state, { recursive: true, mode: 448 });
12
+ const release = await acquireInstanceLock(location.state);
13
+ let config;
14
+ let runtime;
15
+ let updates;
16
+ const server = createServer((request, response) => {
17
+ void (async () => {
18
+ response.setHeader("content-type", "application/json");
19
+ if (request.method !== "POST") throw new Error("Expected POST");
20
+ let body = "";
21
+ for await (const chunk of request) {
22
+ body += String(chunk);
23
+ if (body.length > 8192) throw new Error("Control request too large");
24
+ }
25
+ const input = body ? JSON.parse(body) : {};
26
+ if (request.url === "/status") {
27
+ response.end(
28
+ JSON.stringify({
29
+ version: packageInfo.version,
30
+ pid: process.pid,
31
+ origin: config.publicOrigin[0],
32
+ configPath,
33
+ dataDir: config.dataDir,
34
+ maintenance: runtime?.service.isInMaintenance(),
35
+ codexVersion: runtime?.codex.diagnostics().codexVersion ?? null,
36
+ codexReady: runtime?.codex.ready ?? false
37
+ })
38
+ );
39
+ } else if (request.url === "/stop") {
40
+ await stop(() => response.end(JSON.stringify({ stopped: true })));
41
+ } else if (request.url === "/prepare-update") {
42
+ response.end(JSON.stringify(runtime.service.prepareUpdate()));
43
+ } else if (request.url === "/cancel-update") {
44
+ runtime.service.setMaintenance(false);
45
+ response.end(JSON.stringify({ cancelled: true }));
46
+ } else if (request.url === "/activate-update") {
47
+ const id = input.id;
48
+ if (typeof id !== "string") throw new Error("Invalid upgrade identity");
49
+ await updates.commitActivation(id);
50
+ runtime.service.setMaintenance(false);
51
+ response.end(JSON.stringify({ activated: true }));
52
+ } else if (request.url === "/update-status") {
53
+ response.end(JSON.stringify(await updates.status()));
54
+ } else if (request.url === "/update-check") {
55
+ const channel = input.channel;
56
+ if (channel !== void 0 && !["stable", "beta", "dev"].includes(String(channel)))
57
+ throw new Error("Invalid channel");
58
+ response.end(
59
+ JSON.stringify(await updates.check(channel))
60
+ );
61
+ } else if (request.url === "/update") {
62
+ response.end(
63
+ JSON.stringify(await updates.start(input))
64
+ );
65
+ } else {
66
+ response.statusCode = 404;
67
+ response.end(JSON.stringify({ error: "Unknown control action" }));
68
+ }
69
+ })().catch((error) => {
70
+ response.statusCode = 400;
71
+ response.end(
72
+ JSON.stringify({ error: error instanceof Error ? error.message : "Control failed" })
73
+ );
74
+ });
75
+ });
76
+ let stopping;
77
+ const stop = (respond) => stopping ??= (async () => {
78
+ await runtime?.close();
79
+ respond?.();
80
+ await new Promise((done) => server.close(() => done()));
81
+ if (process.platform !== "win32") await rm(location.socket, { force: true });
82
+ await release();
83
+ })();
84
+ try {
85
+ config = await loadConfig({ configPath, development: false });
86
+ updates = new UpdateManager(
87
+ location,
88
+ config.dataDir,
89
+ () => runtime?.codex.diagnostics().codexVersion ?? null
90
+ );
91
+ await updates.initialize();
92
+ const maintenance = await updates.activationPending();
93
+ runtime = await startGateway(config, { updates, maintenance });
94
+ if (process.platform !== "win32") await rm(location.socket, { force: true });
95
+ await new Promise((done, reject) => {
96
+ server.once("error", reject);
97
+ server.listen(location.socket, done);
98
+ });
99
+ if (process.platform !== "win32") await chmod(location.socket, 384);
100
+ await saveJson(`${location.state}/instance.json`, {
101
+ root: updates.context?.root,
102
+ config: location.config,
103
+ socket: location.socket
104
+ });
105
+ console.log(`Covro ${packageInfo.version} listening on ${config.publicOrigin[0]}`);
106
+ process.once("SIGINT", () => void stop());
107
+ process.once("SIGTERM", () => void stop());
108
+ } catch (error) {
109
+ updates?.close();
110
+ await runtime?.close();
111
+ await release();
112
+ throw error;
113
+ }
114
+ }
115
+ export {
116
+ runDaemon
117
+ };
118
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/cli/daemon.ts"],
4
+ "sourcesContent": ["import { createServer } from 'node:http';\nimport { chmod, mkdir, rm } from 'node:fs/promises';\nimport { acquireInstanceLock } from '../server/instance-lock.js';\nimport { loadConfig } from '../server/config.js';\nimport { startGateway } from '../server/main.js';\nimport { packageInfo, paths } from './paths.js';\nimport { UpdateManager } from '../updates/manager.js';\nimport { saveJson } from '../updates/files.js';\n\nexport async function runDaemon(configPath: string) {\n const location = paths(configPath);\n await mkdir(location.state, { recursive: true, mode: 0o700 });\n const release = await acquireInstanceLock(location.state);\n let config: Awaited<ReturnType<typeof loadConfig>>;\n let runtime: Awaited<ReturnType<typeof startGateway>> | undefined;\n let updates: UpdateManager | undefined;\n const server = createServer((request, response) => {\n void (async () => {\n response.setHeader('content-type', 'application/json');\n if (request.method !== 'POST') throw new Error('Expected POST');\n let body = '';\n for await (const chunk of request) {\n body += String(chunk);\n if (body.length > 8192) throw new Error('Control request too large');\n }\n const input: unknown = body ? JSON.parse(body) : {};\n if (request.url === '/status') {\n response.end(\n JSON.stringify({\n version: packageInfo.version,\n pid: process.pid,\n origin: config.publicOrigin[0],\n configPath,\n dataDir: config.dataDir,\n maintenance: runtime?.service.isInMaintenance(),\n codexVersion: runtime?.codex.diagnostics().codexVersion ?? null,\n codexReady: runtime?.codex.ready ?? false,\n }),\n );\n } else if (request.url === '/stop') {\n await stop(() => response.end(JSON.stringify({ stopped: true })));\n } else if (request.url === '/prepare-update') {\n response.end(JSON.stringify(runtime!.service.prepareUpdate()));\n } else if (request.url === '/cancel-update') {\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ cancelled: true }));\n } else if (request.url === '/activate-update') {\n const id = (input as { id?: unknown }).id;\n if (typeof id !== 'string') throw new Error('Invalid upgrade identity');\n await updates!.commitActivation(id);\n runtime!.service.setMaintenance(false);\n response.end(JSON.stringify({ activated: true }));\n } else if (request.url === '/update-status') {\n response.end(JSON.stringify(await updates!.status()));\n } else if (request.url === '/update-check') {\n const channel = (input as { channel?: unknown }).channel;\n if (channel !== undefined && !['stable', 'beta', 'dev'].includes(String(channel)))\n throw new Error('Invalid channel');\n response.end(\n JSON.stringify(await updates!.check(channel as 'stable' | 'beta' | 'dev' | undefined)),\n );\n } else if (request.url === '/update') {\n response.end(\n JSON.stringify(await updates!.start(input as Parameters<UpdateManager['start']>[0])),\n );\n } else {\n response.statusCode = 404;\n response.end(JSON.stringify({ error: 'Unknown control action' }));\n }\n })().catch((error: unknown) => {\n response.statusCode = 400;\n response.end(\n JSON.stringify({ error: error instanceof Error ? error.message : 'Control failed' }),\n );\n });\n });\n let stopping: Promise<void> | undefined;\n const stop = (respond?: () => void) =>\n (stopping ??= (async () => {\n await runtime?.close();\n respond?.();\n await new Promise<void>((done) => server.close(() => done()));\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await release();\n })());\n try {\n config = await loadConfig({ configPath, development: false });\n updates = new UpdateManager(\n location,\n config.dataDir,\n () => runtime?.codex.diagnostics().codexVersion ?? null,\n );\n await updates.initialize();\n const maintenance = await updates.activationPending();\n runtime = await startGateway(config, { updates, maintenance });\n if (process.platform !== 'win32') await rm(location.socket, { force: true });\n await new Promise<void>((done, reject) => {\n server.once('error', reject);\n server.listen(location.socket, done);\n });\n if (process.platform !== 'win32') await chmod(location.socket, 0o600);\n await saveJson(`${location.state}/instance.json`, {\n root: updates.context?.root,\n config: location.config,\n socket: location.socket,\n });\n console.log(`Covro ${packageInfo.version} listening on ${config.publicOrigin[0]}`);\n process.once('SIGINT', () => void stop());\n process.once('SIGTERM', () => void stop());\n } catch (error) {\n updates?.close();\n await runtime?.close();\n await release();\n throw error;\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }