code-workspace-zhuiyi 0.1.0-beta.8 → 0.1.0-beta.9
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.
- package/README.md +7 -6
- package/README.zh-CN.md +7 -4
- package/artifacts/manifest.json +3 -21
- package/artifacts/templates/user-guide/en-US.md +5 -4
- package/artifacts/templates/user-guide/zh-CN.md +5 -4
- package/docs/extension-architecture.zh-CN.md +15 -4
- package/extensions/monitor/1.0.0/runtime.js +2 -1
- package/extensions/monitor/1.1.0/assets/i18n-icon.svg +1 -0
- package/extensions/monitor/1.1.0/assets/logo-vector.svg +30 -0
- package/extensions/monitor/1.1.0/assets/request_tip.mp3 +0 -0
- package/extensions/monitor/1.1.0/assets/session_finish.mp3 +0 -0
- package/extensions/monitor/1.1.0/init.js +37 -0
- package/extensions/monitor/1.1.0/manifest.json +39 -0
- package/{src/monitor/index.js → extensions/monitor/1.1.0/monitor.js} +38 -10
- package/extensions/monitor/1.1.0/runtime.js +105 -0
- package/package.json +1 -3
- package/schemas/extension-manifest-v3.json +6 -2
- package/spec/extension/v1/specification.en-US.md +7 -0
- package/spec/extension/v1/specification.zh-CN.md +6 -0
- package/src/cli/commands/extension.js +1 -3
- package/src/cli/commands/init.js +0 -9
- package/src/cli/commands/update.js +3 -6
- package/src/cli/commands/workspace.js +0 -1
- package/src/cli/registry.js +1 -4
- package/src/cli.js +1 -7
- package/src/core/config.js +1 -38
- package/src/core/doctor.js +2 -8
- package/src/core/extension-artifacts.js +13 -2
- package/src/core/extension-runtime.js +53 -2
- package/src/core/extensions.js +10 -7
- package/src/core/initializer.js +3 -20
- package/src/core/managed-files.js +5 -10
- package/src/index.js +0 -4
- package/src/init/plan.js +1 -2
- package/src/init/wizard.js +2 -9
- package/artifacts/templates/codex/hooks.json +0 -86
- package/src/cli/commands/monitor.js +0 -73
- /package/{src/monitor → extensions/monitor/1.1.0}/i18n/index.js +0 -0
- /package/{src/monitor → extensions/monitor/1.1.0}/i18n/locales/en-US.js +0 -0
- /package/{src/monitor → extensions/monitor/1.1.0}/i18n/locales/zh-CN.js +0 -0
- /package/{src/monitor → extensions/monitor/1.1.0}/page.js +0 -0
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ code-workspace init . \
|
|
|
33
33
|
--yes
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Use `--tools claude`, `--tools codex`, or `--tools none` to override the default tool selection.
|
|
36
|
+
Use `--tools claude`, `--tools codex`, or `--tools none` to override the default tool selection. Monitoring is not enabled implicitly; install the `monitor` extension with `--extensions monitor` or through the interactive extension picker.
|
|
37
37
|
|
|
38
38
|
Initialization writes only Workspace-owned state and integrations:
|
|
39
39
|
|
|
@@ -41,7 +41,7 @@ Initialization writes only Workspace-owned state and integrations:
|
|
|
41
41
|
- `USER_GUIDE.md`
|
|
42
42
|
- `CLAUDE.md` and/or `AGENTS.md`
|
|
43
43
|
- Workspace-specific commands and skills whose names start with `code-workspace-` or use the `/code-workspace` namespace
|
|
44
|
-
- `.codex/hooks.json` when
|
|
44
|
+
- `.codex/hooks.json` is written by the `monitor` extension when that extension is installed
|
|
45
45
|
|
|
46
46
|
It does not create `openspec/`, install native `/opsx` commands, or install native `openspec-*` skills.
|
|
47
47
|
|
|
@@ -185,13 +185,14 @@ code-workspace language --json
|
|
|
185
185
|
|
|
186
186
|
## Monitor
|
|
187
187
|
|
|
188
|
+
Monitor ships as the built-in `monitor` extension. Install it into a workspace and run its dashboard through the generic extension runtime:
|
|
189
|
+
|
|
188
190
|
```bash
|
|
189
|
-
code-workspace monitor --
|
|
191
|
+
code-workspace extension install monitor --yes
|
|
192
|
+
code-workspace ext monitor serve --port 3211
|
|
190
193
|
```
|
|
191
194
|
|
|
192
|
-
The monitor binds to loopback, combines events from multiple initialized workspaces, and keeps hook reporting failure-open. Review and trust project hooks in Codex before relying on reports.
|
|
193
|
-
|
|
194
|
-
## Completion
|
|
195
|
+
The monitor binds to loopback, combines events from multiple initialized workspaces, and keeps hook reporting (`codew ext monitor report`) failure-open. Review and trust project hooks in Codex before relying on reports.
|
|
195
196
|
|
|
196
197
|
```bash
|
|
197
198
|
code-workspace completion --shell zsh
|
package/README.zh-CN.md
CHANGED
|
@@ -33,7 +33,7 @@ code-workspace init . \
|
|
|
33
33
|
--yes
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
可用 `--tools claude`、`--tools codex` 或 `--tools none`
|
|
36
|
+
可用 `--tools claude`、`--tools codex` 或 `--tools none` 覆盖默认工具选择。监控不会隐式启用;请通过 `--extensions monitor` 或交互式扩展选择安装 `monitor` 扩展。
|
|
37
37
|
|
|
38
38
|
初始化只写入 Workspace 自有状态和集成:
|
|
39
39
|
|
|
@@ -41,7 +41,7 @@ code-workspace init . \
|
|
|
41
41
|
- `USER_GUIDE.md`
|
|
42
42
|
- `CLAUDE.md` 和/或 `AGENTS.md`
|
|
43
43
|
- 名称以 `code-workspace-` 开头或使用 `/code-workspace` 命名空间的 Workspace 专用命令与 Skill
|
|
44
|
-
-
|
|
44
|
+
- 安装 `monitor` 扩展时由扩展写入的 `.codex/hooks.json`
|
|
45
45
|
|
|
46
46
|
它不会创建 `openspec/`,不会安装原生 `/opsx` 命令,也不会安装原生 `openspec-*` Skill。
|
|
47
47
|
|
|
@@ -183,11 +183,14 @@ code-workspace language --json
|
|
|
183
183
|
|
|
184
184
|
## 监控
|
|
185
185
|
|
|
186
|
+
监控以内置 `monitor` 扩展的形式提供。先在 Workspace 中安装扩展,再通过通用扩展运行时启动面板:
|
|
187
|
+
|
|
186
188
|
```bash
|
|
187
|
-
code-workspace monitor --
|
|
189
|
+
code-workspace extension install monitor --yes
|
|
190
|
+
code-workspace ext monitor serve --port 3211
|
|
188
191
|
```
|
|
189
192
|
|
|
190
|
-
监控服务仅绑定 loopback,可汇总多个已初始化工作区的事件;hook
|
|
193
|
+
监控服务仅绑定 loopback,可汇总多个已初始化工作区的事件;hook 上报(`codew ext monitor report`)失败不会阻断 Agent。依赖监控前,请在 Codex 中检查并信任项目 hook。
|
|
191
194
|
|
|
192
195
|
## 命令补全
|
|
193
196
|
|
package/artifacts/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
|
-
"releaseVersion": "0.1.0-beta.
|
|
3
|
+
"releaseVersion": "0.1.0-beta.9",
|
|
4
4
|
"requirements": {
|
|
5
5
|
"node": ">=20.19.0"
|
|
6
6
|
},
|
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
"id": "workspace-user-guide-en-US",
|
|
14
14
|
"kind": "asset",
|
|
15
15
|
"path": "templates/user-guide/en-US.md",
|
|
16
|
-
"sha256": "
|
|
16
|
+
"sha256": "a91d45e4fc11b4f3eb3c651a4fcee6d981b0d076d93063e0480c5c3e377ea5d2"
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
"id": "workspace-user-guide-zh-CN",
|
|
20
20
|
"kind": "asset",
|
|
21
21
|
"path": "templates/user-guide/zh-CN.md",
|
|
22
|
-
"sha256": "
|
|
22
|
+
"sha256": "43bfd67f0725c4834cf33bb7ab96cbe090db6f4373f0b58d01276db293cb9598"
|
|
23
23
|
}
|
|
24
24
|
],
|
|
25
25
|
"managedFiles": [
|
|
@@ -86,24 +86,6 @@
|
|
|
86
86
|
"kind": "template"
|
|
87
87
|
}
|
|
88
88
|
},
|
|
89
|
-
{
|
|
90
|
-
"id": "workspace-codex-monitor-hooks",
|
|
91
|
-
"target": ".codex/hooks.json",
|
|
92
|
-
"tools": [
|
|
93
|
-
"codex"
|
|
94
|
-
],
|
|
95
|
-
"capabilities": [
|
|
96
|
-
"monitor"
|
|
97
|
-
],
|
|
98
|
-
"allowMissing": true,
|
|
99
|
-
"desired": {
|
|
100
|
-
"source": "templates/codex/hooks.json",
|
|
101
|
-
"sha256": "ecba346e0ce73d931802615dd798b952f1d0afabef3c67d7d1ee21f81b133b46"
|
|
102
|
-
},
|
|
103
|
-
"provenance": {
|
|
104
|
-
"kind": "template"
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
89
|
{
|
|
108
90
|
"id": "workspace-claude-add-projects-command",
|
|
109
91
|
"target": ".claude/commands/code-workspace/add-projects.md",
|
|
@@ -49,10 +49,11 @@ This also switches this managed guide. Existing project context is not translate
|
|
|
49
49
|
|
|
50
50
|
## Use the Agent monitor
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Monitoring ships as the built-in `monitor` extension. Install it in a workspace and start one global monitor for all workspaces:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
codew monitor
|
|
55
|
+
codew extension install monitor --yes
|
|
56
|
+
codew ext monitor
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
Open the printed local URL. The dashboard shows workspaces, execution state, pending approvals, completed turns, and live signals. Monitor language is selected on the page and is independent of `workspace.language`.
|
|
@@ -60,10 +61,10 @@ Open the printed local URL. The dashboard shows workspaces, execution state, pen
|
|
|
60
61
|
Use another port when necessary:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
|
-
codew monitor --port 8080
|
|
64
|
+
codew ext monitor serve --port 8080
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
The extension reports to the URL recorded in `.code-workspace/monitor-reporting.json`. After installation, review and trust the project hooks with `/hooks` in Codex.
|
|
67
68
|
|
|
68
69
|
## Practical commands
|
|
69
70
|
|
|
@@ -49,10 +49,11 @@ codew update --language zh-CN
|
|
|
49
49
|
|
|
50
50
|
## 使用 Agent Monitor
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
监控以内置 `monitor` 扩展的形式提供。先在 Workspace 中安装扩展,再为所有工作区启动一个全局 Monitor:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
codew monitor
|
|
55
|
+
codew extension install monitor --yes
|
|
56
|
+
codew ext monitor
|
|
56
57
|
```
|
|
57
58
|
|
|
58
59
|
打开命令输出的本地地址。面板会显示工作区、执行状态、待授权请求、已完成轮次和实时信号。Monitor 语言在页面中单独选择,与 `workspace.language` 相互独立。
|
|
@@ -60,10 +61,10 @@ codew monitor
|
|
|
60
61
|
必要时可使用其他端口:
|
|
61
62
|
|
|
62
63
|
```bash
|
|
63
|
-
codew monitor --port 8080
|
|
64
|
+
codew ext monitor serve --port 8080
|
|
64
65
|
```
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
扩展会向 `.code-workspace/monitor-reporting.json` 中记录的 URL 上报。安装后,请在 Codex 中使用 `/hooks` 检查并信任项目 Hook。
|
|
67
68
|
|
|
68
69
|
## 实用命令
|
|
69
70
|
|
|
@@ -181,13 +181,23 @@ installed manifest 是 Host 持久化的最终安装事实,保存在 Workspace
|
|
|
181
181
|
- 卸载前验证文件未发生未知修改;
|
|
182
182
|
- 验证成功后删除该文件。
|
|
183
183
|
|
|
184
|
-
### 6.2
|
|
184
|
+
### 6.2 种子文件(seeded)
|
|
185
|
+
|
|
186
|
+
`seeded` 是 `file` 的一种特殊所有权:Host 只在目标文件缺失时写入扩展提供的默认内容(种子),之后把该文件视为用户所有。Host 负责:
|
|
187
|
+
|
|
188
|
+
- 首次安装且目标缺失时写入默认内容;
|
|
189
|
+
- 升级时永不覆盖已存在的用户内容,也不做漂移检查;
|
|
190
|
+
- 卸载时直接删除该文件,即使内容已被用户修改。
|
|
191
|
+
|
|
192
|
+
该能力用于「随扩展安装、之后归用户拥有」的本地配置文件(例如 monitor 的上报地址与开关)。
|
|
193
|
+
|
|
194
|
+
### 6.3 独占目录
|
|
185
195
|
|
|
186
196
|
一个扩展独占一个 Workspace 相对目录目标。它用于安装包含多个文件的运行时,例如预构建的 Jira MCP 包。
|
|
187
197
|
|
|
188
198
|
Host 将目录视为一个制品,使用规范化目录摘要检测漂移,并通过 staging、备份和 rename 完成可恢复提交。目录摘要算法必须固定路径规则、排序、文件类型和文件内容计算方式。
|
|
189
199
|
|
|
190
|
-
### 6.
|
|
200
|
+
### 6.4 共享文本块
|
|
191
201
|
|
|
192
202
|
扩展提供一个文本片段,Host 使用稳定的扩展 id 和输出 id 标记,将其合成到声明的共享文本目标中。
|
|
193
203
|
|
|
@@ -200,7 +210,7 @@ Host 负责:
|
|
|
200
210
|
|
|
201
211
|
该能力覆盖当前 Codex TOML 配置需求,但协议不包含 Codex 或 MCP 业务字段。
|
|
202
212
|
|
|
203
|
-
### 6.
|
|
213
|
+
### 6.5 JSON 对象成员
|
|
204
214
|
|
|
205
215
|
扩展提供一个 JSON 值,并在静态 manifest 中声明目标文件和唯一成员位置。Host 只拥有该成员,不拥有整个 JSON 文件。
|
|
206
216
|
|
|
@@ -214,7 +224,7 @@ Host 负责:
|
|
|
214
224
|
|
|
215
225
|
该能力覆盖 `.mcp.json` 中单个 MCP server 的需求,但核心不验证 server 的 Jira 业务含义。
|
|
216
226
|
|
|
217
|
-
### 6.
|
|
227
|
+
### 6.6 暂不增加的输出
|
|
218
228
|
|
|
219
229
|
基础版不增加 `remote-archive` 之类的下载型制品。下载和解压是扩展准备候选目录的实现过程,不是 Host 的安装制品语义。
|
|
220
230
|
|
|
@@ -377,6 +387,7 @@ node init.js --context <context-file> --output <staging-directory> --result <res
|
|
|
377
387
|
|
|
378
388
|
- 独占目标同时只能由一个扩展拥有。
|
|
379
389
|
- 共享目标由 Host 管理多个局部 contribution 的所有权。
|
|
390
|
+
- 种子文件目标在首次安装后视为用户所有,升级不覆盖、卸载删除、不参与漂移检查。
|
|
380
391
|
- 静态 manifest 是安装意图,不是已安装事实。
|
|
381
392
|
- 初始化结果是扩展声明,不是可信事实。
|
|
382
393
|
- installed manifest 是 Host 验证后的事实来源。
|
|
@@ -43,7 +43,7 @@ async function main() {
|
|
|
43
43
|
// Rebind the callback to the concrete store and replay persisted facts.
|
|
44
44
|
monitor.store = monitor.store;
|
|
45
45
|
for (const event of persisted) {
|
|
46
|
-
try { monitor.store.accept(event
|
|
46
|
+
try { monitor.store.accept(event); } catch { /* ignore corrupt historical events */ }
|
|
47
47
|
}
|
|
48
48
|
loading = false;
|
|
49
49
|
save();
|
|
@@ -73,3 +73,4 @@ main().catch((error) => {
|
|
|
73
73
|
process.stderr.write(`${error.message}\n`);
|
|
74
74
|
process.exitCode = 1;
|
|
75
75
|
});
|
|
76
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" viewBox="0 0 24 24" class="vt-locales-btn-icon" data-v-6229553e=""><path d="M0 0h24v24H0z" fill="none"></path><path d=" M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z " class="css-c4d79v"></path></svg>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="1254" height="1254" viewBox="0 0 1254 1254"
|
|
4
|
+
role="img" aria-label="Geometric teal-blue logo">
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="logoGradient" x1="320" y1="175" x2="860" y2="1080" gradientUnits="userSpaceOnUse">
|
|
7
|
+
<stop offset="0" stop-color="#19AEB8"/>
|
|
8
|
+
<stop offset="0.48" stop-color="#087DA5"/>
|
|
9
|
+
<stop offset="1" stop-color="#07528C"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
|
|
13
|
+
<g fill="none"
|
|
14
|
+
stroke="url(#logoGradient)"
|
|
15
|
+
stroke-width="118"
|
|
16
|
+
stroke-linecap="butt"
|
|
17
|
+
stroke-linejoin="round">
|
|
18
|
+
<path d="M 794 289
|
|
19
|
+
L 630 184
|
|
20
|
+
L 291 394
|
|
21
|
+
L 291 786
|
|
22
|
+
L 630 1020
|
|
23
|
+
L 973 786
|
|
24
|
+
L 973 393
|
|
25
|
+
L 914 354"/>
|
|
26
|
+
<path d="M 850 386
|
|
27
|
+
L 617 526
|
|
28
|
+
L 617 850"/>
|
|
29
|
+
</g>
|
|
30
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
function option(name) {
|
|
7
|
+
const index = process.argv.indexOf(name);
|
|
8
|
+
return index >= 0 ? process.argv[index + 1] : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function main() {
|
|
12
|
+
const contextFile = option("--context");
|
|
13
|
+
const outputRoot = option("--output");
|
|
14
|
+
const resultFile = option("--result");
|
|
15
|
+
if (!contextFile || !outputRoot || !resultFile) throw new Error("Usage: init.js --context <file> --output <directory> --result <file>");
|
|
16
|
+
const context = JSON.parse(fs.readFileSync(contextFile, "utf8"));
|
|
17
|
+
if (context.schemaVersion !== 1 || context.extensionSpecVersion !== 1 || context.extension?.id !== "monitor" || !context.extension.version || !context.workspace || !Array.isArray(context.tools)) throw new Error("Invalid monitor extension context");
|
|
18
|
+
fs.mkdirSync(outputRoot, { recursive: true });
|
|
19
|
+
const config = {
|
|
20
|
+
schemaVersion: 1,
|
|
21
|
+
enable: true,
|
|
22
|
+
url: "http://127.0.0.1:3211",
|
|
23
|
+
workspace: context.workspace,
|
|
24
|
+
service: { id: "monitor", compatibilityGroup: "v1" },
|
|
25
|
+
};
|
|
26
|
+
const source = "monitor-reporting.json";
|
|
27
|
+
fs.writeFileSync(path.join(outputRoot, source), `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
|
28
|
+
const outputs = [{ id: "reporting-config", source }];
|
|
29
|
+
fs.writeFileSync(resultFile, `${JSON.stringify({
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
extensionSpecVersion: 1,
|
|
32
|
+
extension: { id: "monitor", version: context.extension.version },
|
|
33
|
+
outputs,
|
|
34
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 3,
|
|
3
|
+
"extensionSpecVersion": 1,
|
|
4
|
+
"experimental": true,
|
|
5
|
+
"id": "monitor",
|
|
6
|
+
"name": "Code Workspace Monitor",
|
|
7
|
+
"version": "1.1.0",
|
|
8
|
+
"entry": "init.js",
|
|
9
|
+
"entrySha256": "b80b9b88aa25b523433a63983e8c74883c18153a95e14302c4c0af3709770ab1",
|
|
10
|
+
"timeoutMs": 120000,
|
|
11
|
+
"capabilities": {},
|
|
12
|
+
"outputs": [
|
|
13
|
+
{
|
|
14
|
+
"id": "reporting-config",
|
|
15
|
+
"kind": "file",
|
|
16
|
+
"ownership": "seeded",
|
|
17
|
+
"target": ".code-workspace/monitor-reporting.json"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"hooks": [
|
|
21
|
+
{ "id": "monitor-task-started", "event": "task.started", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
22
|
+
{ "id": "monitor-task-activity", "event": "task.activity", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
23
|
+
{ "id": "monitor-write-before", "event": "write.before", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
24
|
+
{ "id": "monitor-write-after", "event": "write.after", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
25
|
+
{ "id": "monitor-turn-ended", "event": "task.turn-ended", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
26
|
+
{ "id": "monitor-task-ended", "event": "task.ended", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
27
|
+
{ "id": "monitor-subagent-started", "event": "task.subagent-started", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 },
|
|
28
|
+
{ "id": "monitor-subagent-ended", "event": "task.subagent-ended", "command": "codew ext monitor report", "tools": ["codex", "claude"], "timeoutMs": 2 }
|
|
29
|
+
],
|
|
30
|
+
"runtime": {
|
|
31
|
+
"runtimeProtocolVersion": 1,
|
|
32
|
+
"entry": "runtime.js",
|
|
33
|
+
"entrySha256": "df2c30b8541373b6b140c99a2e7ac22d864104e24cdf6a388860480c6f1fdd41",
|
|
34
|
+
"scope": "global",
|
|
35
|
+
"mode": "service",
|
|
36
|
+
"timeoutMs": 3000,
|
|
37
|
+
"service": { "id": "monitor", "compatibilityGroup": "v1", "singleton": "user" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -8,10 +8,10 @@ const DEFAULT_MONITOR_HOST = "127.0.0.1";
|
|
|
8
8
|
const DEFAULT_MONITOR_PORT = 3211;
|
|
9
9
|
const DEFAULT_SESSION_INACTIVITY_MS = 10 * 60 * 1000;
|
|
10
10
|
const MONITOR_ASSETS = new Map([
|
|
11
|
-
["/assets/request_tip.mp3", { file: path.join(__dirname, "
|
|
12
|
-
["/assets/session_finish.mp3", { file: path.join(__dirname, "
|
|
13
|
-
["/assets/logo-vector.svg", { file: path.join(__dirname, "
|
|
14
|
-
["/assets/i18n-icon.svg", { file: path.join(__dirname, "
|
|
11
|
+
["/assets/request_tip.mp3", { file: path.join(__dirname, "assets", "request_tip.mp3"), type: "audio/mpeg" }],
|
|
12
|
+
["/assets/session_finish.mp3", { file: path.join(__dirname, "assets", "session_finish.mp3"), type: "audio/mpeg" }],
|
|
13
|
+
["/assets/logo-vector.svg", { file: path.join(__dirname, "assets", "logo-vector.svg"), type: "image/svg+xml; charset=utf-8" }],
|
|
14
|
+
["/assets/i18n-icon.svg", { file: path.join(__dirname, "assets", "i18n-icon.svg"), type: "image/svg+xml; charset=utf-8" }],
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
17
|
const EVENT_DEFINITIONS = {
|
|
@@ -90,10 +90,10 @@ function createMonitorStore(options = {}) {
|
|
|
90
90
|
for (const response of subscribers) response.write(payload);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
function accept(event) {
|
|
93
|
+
function accept(event, replay = false) {
|
|
94
94
|
if (!validEvent(event)) throw new Error("invalid event payload");
|
|
95
95
|
if (events.some((current) => current.eventId === event.eventId)) return { duplicate: true };
|
|
96
|
-
const receivedAt = nowIso();
|
|
96
|
+
const receivedAt = replay && event.timestamp ? event.timestamp : nowIso();
|
|
97
97
|
const storedEvent = { ...event, timestamp: event.timestamp || receivedAt };
|
|
98
98
|
const previous = workspaces.get(event.workspace.uuid) || {
|
|
99
99
|
uuid: event.workspace.uuid,
|
|
@@ -130,6 +130,7 @@ function createMonitorStore(options = {}) {
|
|
|
130
130
|
events.unshift(storedEvent);
|
|
131
131
|
if (events.length > maxEvents) events.length = maxEvents;
|
|
132
132
|
publish({ event: storedEvent, workspace: previous });
|
|
133
|
+
options.onChange?.();
|
|
133
134
|
return { duplicate: false, workspaceUuid: previous.uuid };
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -139,6 +140,7 @@ function createMonitorStore(options = {}) {
|
|
|
139
140
|
if (events[index].workspace.uuid === uuid) events.splice(index, 1);
|
|
140
141
|
}
|
|
141
142
|
publish({ workspaceRemoved: uuid });
|
|
143
|
+
options.onChange?.();
|
|
142
144
|
return { removed: true, workspaceUuid: uuid };
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -163,6 +165,7 @@ function createMonitorStore(options = {}) {
|
|
|
163
165
|
workspace.firstSeenAt
|
|
164
166
|
);
|
|
165
167
|
publish({ sessionRemoved: { workspaceUuid, sessionId } });
|
|
168
|
+
options.onChange?.();
|
|
166
169
|
return { removed: true, workspaceUuid, sessionId, removedEvents };
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -308,14 +311,14 @@ function startMonitor(options = {}) {
|
|
|
308
311
|
});
|
|
309
312
|
}
|
|
310
313
|
|
|
311
|
-
async function reportHookEvent(input,
|
|
312
|
-
if (!
|
|
313
|
-
const event = normalizeHookEvent(input,
|
|
314
|
+
async function reportHookEvent(input, reporting, options = {}) {
|
|
315
|
+
if (!reporting?.enable || !reporting?.workspace) return { action: "skip", reason: "monitor disabled" };
|
|
316
|
+
const event = normalizeHookEvent(input, reporting);
|
|
314
317
|
if (!event) return { action: "skip", reason: "unsupported event" };
|
|
315
318
|
const controller = new AbortController();
|
|
316
319
|
const timer = setTimeout(() => controller.abort(), options.timeoutMs || 300);
|
|
317
320
|
try {
|
|
318
|
-
const response = await (options.fetch || fetch)(`${
|
|
321
|
+
const response = await (options.fetch || fetch)(`${reporting.url}/api/v1/events`, {
|
|
319
322
|
method: "POST",
|
|
320
323
|
headers: { "content-type": "application/json" },
|
|
321
324
|
body: JSON.stringify(event),
|
|
@@ -329,6 +332,29 @@ async function reportHookEvent(input, config, options = {}) {
|
|
|
329
332
|
}
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
function findReportingConfig(startDirectory) {
|
|
336
|
+
let directory = path.resolve(startDirectory);
|
|
337
|
+
for (;;) {
|
|
338
|
+
const candidate = path.join(directory, ".code-workspace", "monitor-reporting.json");
|
|
339
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
340
|
+
const parent = path.dirname(directory);
|
|
341
|
+
if (parent === directory) return null;
|
|
342
|
+
directory = parent;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function loadReportingConfig(startDirectory) {
|
|
347
|
+
const file = findReportingConfig(startDirectory);
|
|
348
|
+
if (!file) return null;
|
|
349
|
+
try {
|
|
350
|
+
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
351
|
+
if (value?.schemaVersion !== 1 || !value?.workspace?.uuid || typeof value.url !== "string") return null;
|
|
352
|
+
return value;
|
|
353
|
+
} catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
332
358
|
module.exports = {
|
|
333
359
|
DEFAULT_MONITOR_HOST,
|
|
334
360
|
DEFAULT_MONITOR_PORT,
|
|
@@ -336,6 +362,8 @@ module.exports = {
|
|
|
336
362
|
EVENT_DEFINITIONS,
|
|
337
363
|
createMonitorServer,
|
|
338
364
|
createMonitorStore,
|
|
365
|
+
findReportingConfig,
|
|
366
|
+
loadReportingConfig,
|
|
339
367
|
normalizeHookEvent,
|
|
340
368
|
reportHookEvent,
|
|
341
369
|
startMonitor,
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { createMonitorServer, DEFAULT_MONITOR_HOST, DEFAULT_MONITOR_PORT, loadReportingConfig, reportHookEvent } = require("./monitor");
|
|
6
|
+
|
|
7
|
+
function readContext() {
|
|
8
|
+
const file = process.env.CODE_WORKSPACE_RUNTIME_CONTEXT;
|
|
9
|
+
if (!file) throw new Error("CODE_WORKSPACE_RUNTIME_CONTEXT is required");
|
|
10
|
+
const context = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
11
|
+
if (context.schemaVersion !== 1 || context.mode !== "service" || context.extension?.id !== "monitor") throw new Error("Invalid monitor runtime context");
|
|
12
|
+
return context;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function option(name, fallback) {
|
|
16
|
+
const index = process.argv.indexOf(name);
|
|
17
|
+
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readStdinJson() {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Hook entry: always exit 0 so an unavailable monitor never blocks Agent tools.
|
|
31
|
+
async function report() {
|
|
32
|
+
try {
|
|
33
|
+
const input = await readStdinJson();
|
|
34
|
+
const reporting = loadReportingConfig(process.cwd());
|
|
35
|
+
await reportHookEvent(input, reporting || { enable: false });
|
|
36
|
+
} catch {
|
|
37
|
+
/* failure-open by contract */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function serve(context) {
|
|
42
|
+
const dataDirectory = context.dataDirectory;
|
|
43
|
+
fs.mkdirSync(dataDirectory, { recursive: true });
|
|
44
|
+
const dataFile = path.join(dataDirectory, "monitor-events.json");
|
|
45
|
+
let loading = true;
|
|
46
|
+
let persisted = [];
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(fs.readFileSync(dataFile, "utf8"));
|
|
49
|
+
if (Array.isArray(value)) persisted = value;
|
|
50
|
+
} catch { /* first run or an incomplete file */ }
|
|
51
|
+
const save = () => {
|
|
52
|
+
if (loading) return;
|
|
53
|
+
const temporary = `${dataFile}.tmp-${process.pid}`;
|
|
54
|
+
fs.writeFileSync(temporary, `${JSON.stringify(monitor.store.snapshot().events, null, 2)}\n`, { mode: 0o600 });
|
|
55
|
+
fs.renameSync(temporary, dataFile);
|
|
56
|
+
};
|
|
57
|
+
const monitor = createMonitorServer({
|
|
58
|
+
host: option("--host", DEFAULT_MONITOR_HOST),
|
|
59
|
+
port: Number(option("--port", DEFAULT_MONITOR_PORT)),
|
|
60
|
+
store: undefined,
|
|
61
|
+
onChange: save,
|
|
62
|
+
});
|
|
63
|
+
// Rebind the callback to the concrete store and replay persisted facts.
|
|
64
|
+
monitor.store = monitor.store;
|
|
65
|
+
for (const event of persisted) {
|
|
66
|
+
try { monitor.store.accept(event); } catch { /* ignore corrupt historical events */ }
|
|
67
|
+
}
|
|
68
|
+
loading = false;
|
|
69
|
+
save();
|
|
70
|
+
await new Promise((resolve, reject) => {
|
|
71
|
+
const onError = (error) => reject(error);
|
|
72
|
+
monitor.server.once("error", onError);
|
|
73
|
+
monitor.server.listen(monitor.port, monitor.host, () => {
|
|
74
|
+
monitor.server.off("error", onError);
|
|
75
|
+
if (context.readiness?.file) fs.writeFileSync(context.readiness.file, "ready\n", { mode: 0o600 });
|
|
76
|
+
resolve();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
process.stdout.write(`Code Workspace Monitor listening on http://${monitor.host}:${monitor.port}\n`);
|
|
80
|
+
await new Promise((resolve) => {
|
|
81
|
+
let closing = false;
|
|
82
|
+
const close = () => {
|
|
83
|
+
if (closing) return;
|
|
84
|
+
closing = true;
|
|
85
|
+
monitor.server.close(() => { save(); resolve(); });
|
|
86
|
+
};
|
|
87
|
+
process.once("SIGINT", close);
|
|
88
|
+
process.once("SIGTERM", close);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
const command = process.argv[2];
|
|
94
|
+
const context = readContext();
|
|
95
|
+
if (command === "report") {
|
|
96
|
+
await report();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await serve(context);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((error) => {
|
|
103
|
+
process.stderr.write(`${error.message}\n`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-workspace-zhuiyi",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.9",
|
|
4
4
|
"description": "Local multi-project registry and safety layer for AI coding workspaces",
|
|
5
5
|
"author": "icebearx-ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"src/hooks",
|
|
27
27
|
"src/i18n",
|
|
28
28
|
"src/init",
|
|
29
|
-
"src/monitor",
|
|
30
29
|
"artifacts",
|
|
31
30
|
"extensions",
|
|
32
31
|
"docs/extension-architecture.zh-CN.md",
|
|
@@ -44,7 +43,6 @@
|
|
|
44
43
|
},
|
|
45
44
|
"scripts": {
|
|
46
45
|
"test": "node --test src/__test__/*.test.js",
|
|
47
|
-
"dev:monitor": "node bin/code-workspace.js monitor",
|
|
48
46
|
"check": "npm test && npm run cli:architecture-check && npm run pack:check",
|
|
49
47
|
"cli:architecture-check": "node scripts/check-cli-architecture.js",
|
|
50
48
|
"pack:check": "npm pack --dry-run",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"properties": {
|
|
38
38
|
"id": { "$ref": "#/$defs/id" },
|
|
39
39
|
"kind": { "enum": ["file", "directory", "text-block", "json-member"] },
|
|
40
|
-
"ownership": { "enum": ["exclusive", "shared"] },
|
|
40
|
+
"ownership": { "enum": ["exclusive", "shared", "seeded"] },
|
|
41
41
|
"target": { "$ref": "#/$defs/relativePath" },
|
|
42
42
|
"selector": { "type": "string", "pattern": "^/(?:[^~/]|~[01])+(?:/(?:[^~/]|~[01])+)*$" },
|
|
43
43
|
"format": { "enum": ["text", "toml"] },
|
|
@@ -50,7 +50,11 @@
|
|
|
50
50
|
},
|
|
51
51
|
"allOf": [
|
|
52
52
|
{
|
|
53
|
-
"if": { "properties": { "kind": { "
|
|
53
|
+
"if": { "properties": { "kind": { "const": "file" } } },
|
|
54
|
+
"then": { "properties": { "ownership": { "enum": ["exclusive", "seeded"] } } }
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"if": { "properties": { "kind": { "const": "directory" } } },
|
|
54
58
|
"then": { "properties": { "ownership": { "const": "exclusive" } } }
|
|
55
59
|
},
|
|
56
60
|
{
|
|
@@ -106,6 +106,12 @@ block only that runtime and do not change installation compatibility. Runtime su
|
|
|
106
106
|
security sandbox. `global` means that a process may serve multiple Workspaces, not that it receives
|
|
107
107
|
arbitrary Workspace paths, files, or credentials.
|
|
108
108
|
|
|
109
|
+
For `service` runtimes invoked through `codew ext <id>`, the Host applies a fixed argv convention:
|
|
110
|
+
an empty argv or a leading `serve` starts or attaches to the singleton service; any other leading
|
|
111
|
+
argument runs the runtime entry as a short-lived command with inherited stdio, the caller's working
|
|
112
|
+
directory, and no service-registry or readiness handshake. Command invocations resolve with the
|
|
113
|
+
child's exit status and remain bounded by the declared `timeoutMs`.
|
|
114
|
+
|
|
109
115
|
Before confirmation, the Host MUST freeze the manifest, entry, and complete extension-version directory digest. The Host MUST verify them again before execution. An extension MUST NOT use a dynamic result to expand the declarations in its manifest.
|
|
110
116
|
|
|
111
117
|
## 4. Execution Entry
|
|
@@ -141,6 +147,7 @@ Spec v1 supports four output kinds:
|
|
|
141
147
|
| kind | ownership | Lifecycle semantics |
|
|
142
148
|
|---|---|---|
|
|
143
149
|
| `file` | `exclusive` | The Host exclusively writes, digests, drift-checks, and removes one regular file |
|
|
150
|
+
| `file` | `seeded` | The Host writes defaults only when the target is absent, never overwrites user edits, skips drift checks, and removes the file on uninstall |
|
|
144
151
|
| `directory` | `exclusive` | The Host exclusively replaces, canonically digests, drift-checks, and recursively removes a directory |
|
|
145
152
|
| `text-block` | `shared` | The Host manages a marked text contribution using the extension id and output id |
|
|
146
153
|
| `json-member` | `shared` | The Host manages one member selected by JSON Pointer while preserving other content |
|