ronds_ai 0.1.18 → 0.1.20
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 +41 -4
- package/bin/ronds_ai.js +11 -2
- package/lib/code_record.js +98 -0
- package/lib/doctor.js +31 -1
- package/lib/hooks_deploy.js +97 -16
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`ronds_ai` 是一个命令行工具,主要用于两类事情:
|
|
4
4
|
|
|
5
|
-
- 接收 Claude / Cursor 的 hook 事件,整理成统一的代码变更事件并上报
|
|
5
|
+
- 接收 Claude / Cursor / Hermes / Codex 的 hook 事件,整理成统一的代码变更事件并上报
|
|
6
6
|
- 帮助项目写入对应的 hook 配置,以及安装 Skills 到 Claude / Codex / Cursor
|
|
7
7
|
|
|
8
8
|
## Requirements
|
|
@@ -97,7 +97,7 @@ npx ronds_ai@latest check record --no-prompt
|
|
|
97
97
|
|
|
98
98
|
### `record`
|
|
99
99
|
|
|
100
|
-
从标准输入读取 Claude、Codex 或
|
|
100
|
+
从标准输入读取 Claude、Codex、Cursor 或 Hermes 的 hook payload,转换为统一事件后发送到服务端。
|
|
101
101
|
|
|
102
102
|
```bash
|
|
103
103
|
ronds_ai record <tool>
|
|
@@ -108,6 +108,7 @@ ronds_ai record <tool>
|
|
|
108
108
|
- `claude`
|
|
109
109
|
- `codex`
|
|
110
110
|
- `cursor`
|
|
111
|
+
- `hermes`
|
|
111
112
|
|
|
112
113
|
示例:
|
|
113
114
|
|
|
@@ -115,9 +116,10 @@ ronds_ai record <tool>
|
|
|
115
116
|
npx ronds_ai@latest record claude
|
|
116
117
|
npx ronds_ai@latest record codex
|
|
117
118
|
npx ronds_ai@latest record cursor
|
|
119
|
+
npx ronds_ai@latest record hermes
|
|
118
120
|
```
|
|
119
121
|
|
|
120
|
-
这个命令通常不是手工执行,而是被 Claude / Codex / Cursor 的 hook 配置调用。
|
|
122
|
+
这个命令通常不是手工执行,而是被 Claude / Codex / Cursor 或 Hermes 的 hook 配置调用。
|
|
121
123
|
|
|
122
124
|
行为说明:
|
|
123
125
|
|
|
@@ -151,6 +153,7 @@ npx ronds_ai@latest record cursor
|
|
|
151
153
|
- Claude: `PostToolUse`
|
|
152
154
|
- Codex: `PostToolUse`(`apply_patch` 编辑后采集)
|
|
153
155
|
- Cursor: `afterFileEdit`
|
|
156
|
+
- **Hermes**: `post_tool_call`
|
|
154
157
|
|
|
155
158
|
其中 Claude 仅处理这些工具产生的事件:
|
|
156
159
|
|
|
@@ -166,6 +169,8 @@ Codex 输出示例:
|
|
|
166
169
|
{"continue":true,"hookSpecificOutput":{"hookEventName":"PostToolUse"}}
|
|
167
170
|
```
|
|
168
171
|
|
|
172
|
+
**Hermes** 使用 [Shell hooks](https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks#shell-hooks),通过 `~/.hermes/config.yaml` 中的 `hooks.post_tool_call` 配置监听 `write_file` 和 `patch` 工具的调用。Hermes Shell hook 不要求特定 stdout 格式,成功/失败信息直接输出 JSON 结果行。
|
|
173
|
+
|
|
169
174
|
### `doctor`
|
|
170
175
|
|
|
171
176
|
检查当前目录下的 hook 配置和最近错误日志,方便排查接入问题。
|
|
@@ -179,6 +184,7 @@ ronds_ai doctor <tool>
|
|
|
179
184
|
- `claude`
|
|
180
185
|
- `codex`
|
|
181
186
|
- `cursor`
|
|
187
|
+
- `hermes`
|
|
182
188
|
|
|
183
189
|
示例:
|
|
184
190
|
|
|
@@ -186,6 +192,7 @@ ronds_ai doctor <tool>
|
|
|
186
192
|
npx ronds_ai@latest doctor claude
|
|
187
193
|
npx ronds_ai@latest doctor codex
|
|
188
194
|
npx ronds_ai@latest doctor cursor
|
|
195
|
+
npx ronds_ai@latest doctor hermes
|
|
189
196
|
```
|
|
190
197
|
|
|
191
198
|
输出内容包括:
|
|
@@ -210,6 +217,11 @@ Cursor 会检查:
|
|
|
210
217
|
|
|
211
218
|
- `.cursor/hooks.json`
|
|
212
219
|
|
|
220
|
+
Hermes 会检查:
|
|
221
|
+
|
|
222
|
+
- `~/.hermes/config.yaml` 是否存在
|
|
223
|
+
- 该文件中是否有 `hooks.post_tool_call` 下 `matcher: "write_file|patch"` 的 hook 条目
|
|
224
|
+
|
|
213
225
|
### `hooks deploy`
|
|
214
226
|
|
|
215
227
|
在当前项目目录或用户目录生成/更新 Claude / Codex / Cursor 的 hook 配置。
|
|
@@ -241,6 +253,29 @@ npx ronds_ai@latest hooks deploy --scope user
|
|
|
241
253
|
|
|
242
254
|
Codex 推荐使用 `--scope user`,因为项目级 `.codex` 配置在未被用户显式信任的项目中会被 Codex 忽略。
|
|
243
255
|
|
|
256
|
+
#### Hermes Shell Hook 部署
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
ronds_ai hooks deploy hermes
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
生成或更新 `~/.hermes/config.yaml`,添加 `hooks.post_tool_call` 条目(监听 `write_file` 和 `patch` 工具的调用)并设置 `hooks_auto_accept: true`。
|
|
263
|
+
|
|
264
|
+
因为 Hermes 的 Shell hook 配置独立于 Claude / Codex / Cursor(YAML 格式、用户级目录),所以走单独的子命令路径,不与其他工具混用。
|
|
265
|
+
|
|
266
|
+
执行后 `~/.hermes/config.yaml` 的效果:
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
hooks:
|
|
270
|
+
post_tool_call:
|
|
271
|
+
- matcher: write_file|patch
|
|
272
|
+
command: npx ronds_ai@latest record hermes
|
|
273
|
+
timeout: 30
|
|
274
|
+
hooks_auto_accept: true
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
也可手动复制以上内容到 `~/.hermes/config.yaml`。
|
|
278
|
+
|
|
244
279
|
输出是一个 JSON,包含:
|
|
245
280
|
|
|
246
281
|
- `scope`: 本次部署的 scope
|
|
@@ -334,11 +369,13 @@ https://aihub.ronds.com/api/api/v1/ai-code-events
|
|
|
334
369
|
- `YYYYMMDD-claude-error.jsonl`
|
|
335
370
|
- `YYYYMMDD-codex-error.jsonl`
|
|
336
371
|
- `YYYYMMDD-cursor-error.jsonl`
|
|
372
|
+
- `YYYYMMDD-hermes-error.jsonl`
|
|
337
373
|
- `YYYYMMDD-cli-error.jsonl`
|
|
338
374
|
|
|
339
375
|
## Notes
|
|
340
376
|
|
|
341
|
-
- `record` 和 `doctor` 目前支持 `claude`、`codex
|
|
377
|
+
- `record` 和 `doctor` 目前支持 `claude`、`codex`、`cursor`、`hermes`
|
|
378
|
+
- `hooks deploy` 支持 `claude`、`codex`、`cursor`、`hermes`(hermes 为独立子命令)
|
|
342
379
|
- `skills install` 支持 `claude`、`codex`、`cursor`
|
|
343
380
|
- 不支持的命令或参数会直接报错,并把错误写入 CLI 错误日志
|
|
344
381
|
|
package/bin/ronds_ai.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
|
10
10
|
savePersistentWorkerId,
|
|
11
11
|
} = require('../lib/check_record');
|
|
12
12
|
const { runDoctor } = require('../lib/doctor');
|
|
13
|
-
const { deployHooks } = require('../lib/hooks_deploy');
|
|
13
|
+
const { deployHooks, deployHermesHook } = require('../lib/hooks_deploy');
|
|
14
14
|
const { promptForText, promptYesNo } = require('../lib/skills_prompt');
|
|
15
15
|
|
|
16
16
|
function writeWorkerSummary(result) {
|
|
@@ -57,7 +57,7 @@ function stripCheckFlags(args) {
|
|
|
57
57
|
return args.filter((value) => value !== '--no-prompt');
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
const SUPPORTED_SOURCES = new Set(['claude', 'cursor', 'codex']);
|
|
60
|
+
const SUPPORTED_SOURCES = new Set(['claude', 'cursor', 'codex', 'hermes']);
|
|
61
61
|
|
|
62
62
|
function printUsage() {
|
|
63
63
|
process.stderr.write([
|
|
@@ -66,12 +66,14 @@ function printUsage() {
|
|
|
66
66
|
' ronds_ai check record',
|
|
67
67
|
' ronds_ai doctor <tool>',
|
|
68
68
|
' ronds_ai hooks deploy [--scope project|user] [--tool cursor|claude|codex]',
|
|
69
|
+
' ronds_ai hooks deploy hermes',
|
|
69
70
|
' ronds_ai skills install <name> [--tool claude,codex,cursor] [--scope project|global] [--project-dir <path>] [--force]',
|
|
70
71
|
'',
|
|
71
72
|
'Supported tools:',
|
|
72
73
|
' claude',
|
|
73
74
|
' cursor',
|
|
74
75
|
' codex',
|
|
76
|
+
' hermes',
|
|
75
77
|
'',
|
|
76
78
|
'Examples:',
|
|
77
79
|
' npx ronds_ai@latest record claude',
|
|
@@ -91,6 +93,13 @@ function printUsage() {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
async function runHooksCommand(args) {
|
|
96
|
+
// Hermes deploy 走独立路径(YAML 格式、用户级配置,与现有 JSON 部署不同)
|
|
97
|
+
if (args[0] === 'deploy' && args[1] === 'hermes') {
|
|
98
|
+
const result = deployHermesHook();
|
|
99
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
const options = parseHooksDeployArgs(args);
|
|
95
104
|
const result = deployHooks(process.cwd(), options);
|
|
96
105
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
package/lib/code_record.js
CHANGED
|
@@ -14,6 +14,8 @@ const CLAUDE_SUPPORTED_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
|
|
|
14
14
|
const CURSOR_SUPPORTED_HOOK_EVENT = 'afterFileEdit';
|
|
15
15
|
const CODEX_SUPPORTED_HOOK_EVENT = 'PostToolUse';
|
|
16
16
|
const CODEX_SUPPORTED_TOOL = 'apply_patch';
|
|
17
|
+
const HERMES_SUPPORTED_HOOK_EVENT = 'post_tool_call';
|
|
18
|
+
const HERMES_SUPPORTED_TOOLS = new Set(['write_file', 'patch']);
|
|
17
19
|
|
|
18
20
|
// 构造 Codex PostToolUse hook 要求的标准输出,避免 stdout 出现 schema 之外的字段。
|
|
19
21
|
function buildCodexPostToolUseOutput() {
|
|
@@ -207,6 +209,31 @@ function resolveClaudeAbsoluteFilePath(filePath, payload) {
|
|
|
207
209
|
return path.resolve(process.cwd(), rawPath);
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
function resolveHermesAbsoluteFilePath(filePath, payload) {
|
|
213
|
+
const rawPath = String(filePath || '').trim();
|
|
214
|
+
if (!rawPath) {
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (path.isAbsolute(rawPath)) {
|
|
219
|
+
return path.normalize(rawPath);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const candidateBases = [
|
|
223
|
+
payload.cwd,
|
|
224
|
+
process.cwd(),
|
|
225
|
+
].filter(Boolean);
|
|
226
|
+
|
|
227
|
+
for (const base of candidateBases) {
|
|
228
|
+
const candidate = path.resolve(base, rawPath);
|
|
229
|
+
if (fs.existsSync(candidate) || fs.existsSync(path.dirname(candidate))) {
|
|
230
|
+
return candidate;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return path.resolve(process.cwd(), rawPath);
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
function resolveCursorAbsoluteFilePath(filePath, payload) {
|
|
211
238
|
const rawPath = String(filePath || '').trim();
|
|
212
239
|
if (!rawPath) {
|
|
@@ -562,6 +589,73 @@ function buildCodexEvents(payload, source) {
|
|
|
562
589
|
return buildCodexApplyPatchEvents(payload, source, parseApplyPatchFiles(patchText));
|
|
563
590
|
}
|
|
564
591
|
|
|
592
|
+
function buildHermesEvents(payload, source) {
|
|
593
|
+
const hookEvent = payload.hook_event_name || payload.hookEvent;
|
|
594
|
+
const toolName = payload.tool_name || payload.toolName;
|
|
595
|
+
|
|
596
|
+
if (hookEvent !== HERMES_SUPPORTED_HOOK_EVENT || !HERMES_SUPPORTED_TOOLS.has(toolName)) {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const toolInput = payload.tool_input || payload.toolInput || {};
|
|
601
|
+
|
|
602
|
+
// write_file — 全量写入,等价于 Claude 的 Write
|
|
603
|
+
if (toolName === 'write_file') {
|
|
604
|
+
const absoluteFilePath = resolveHermesAbsoluteFilePath(toolInput.path, payload);
|
|
605
|
+
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
606
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
607
|
+
const changeInfo = buildWriteChange(toolInput);
|
|
608
|
+
|
|
609
|
+
return [{
|
|
610
|
+
event_id: randomUUID(),
|
|
611
|
+
timestamp: new Date().toISOString(),
|
|
612
|
+
source,
|
|
613
|
+
worker_id: workerId,
|
|
614
|
+
git,
|
|
615
|
+
file: {
|
|
616
|
+
path: repoRelativePath || String(toolInput.path || ''),
|
|
617
|
+
operation: changeInfo.operation,
|
|
618
|
+
},
|
|
619
|
+
changes: changeInfo.changes,
|
|
620
|
+
}];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// patch — 分两种子模式
|
|
624
|
+
if (toolName === 'patch') {
|
|
625
|
+
// replace 模式 — 锚定替换,等价于 Claude 的 Edit
|
|
626
|
+
if (toolInput.mode !== 'patch') {
|
|
627
|
+
const absoluteFilePath = resolveHermesAbsoluteFilePath(toolInput.path, payload);
|
|
628
|
+
const { repoRoot, git, repoRelativePath } = getGitInfo(absoluteFilePath);
|
|
629
|
+
const workerId = resolveWorkerId(repoRoot);
|
|
630
|
+
const changeInfo = buildEditChange(toolInput);
|
|
631
|
+
|
|
632
|
+
return [{
|
|
633
|
+
event_id: randomUUID(),
|
|
634
|
+
timestamp: new Date().toISOString(),
|
|
635
|
+
source,
|
|
636
|
+
worker_id: workerId,
|
|
637
|
+
git,
|
|
638
|
+
file: {
|
|
639
|
+
path: repoRelativePath || String(toolInput.path || ''),
|
|
640
|
+
operation: changeInfo.operation,
|
|
641
|
+
},
|
|
642
|
+
changes: changeInfo.changes,
|
|
643
|
+
}];
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// patch 模式 — V4A 多文件补丁,等价于 Codex 的 apply_patch
|
|
647
|
+
const patchText = toolInput.patch || '';
|
|
648
|
+
if (!patchText) {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const patchFiles = parseApplyPatchFiles(patchText);
|
|
653
|
+
return buildCodexApplyPatchEvents(payload, source, patchFiles);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
return [];
|
|
657
|
+
}
|
|
658
|
+
|
|
565
659
|
function loadRequestConfig() {
|
|
566
660
|
const url = process.env.HOOK_REPORT_URL || process.env.CHANGE_REPORT_URL || DEFAULT_HOOK_REPORT_URL;
|
|
567
661
|
const timeoutMs = Number(process.env.HOOK_REPORT_TIMEOUT_MS || DEFAULT_TIMEOUT_MS);
|
|
@@ -714,6 +808,10 @@ function buildEvents(payload, source) {
|
|
|
714
808
|
return buildCodexEvents(payload, source);
|
|
715
809
|
}
|
|
716
810
|
|
|
811
|
+
if (source === 'hermes') {
|
|
812
|
+
return buildHermesEvents(payload, source);
|
|
813
|
+
}
|
|
814
|
+
|
|
717
815
|
const singleEvent = buildEvent(payload, source);
|
|
718
816
|
return singleEvent ? [singleEvent] : [];
|
|
719
817
|
}
|
package/lib/doctor.js
CHANGED
|
@@ -2,8 +2,9 @@ const fs = require('fs');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const { runGit } = require('./git');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
5
6
|
|
|
6
|
-
const SUPPORTED_TOOLS = new Set(['claude', 'codex', 'cursor']);
|
|
7
|
+
const SUPPORTED_TOOLS = new Set(['claude', 'codex', 'cursor', 'hermes']);
|
|
7
8
|
|
|
8
9
|
function getFailedEventDir() {
|
|
9
10
|
return path.join(os.homedir(), '.ronds_ai', 'failed-events');
|
|
@@ -24,6 +25,35 @@ function readRecentLogs(tool) {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function getConfigChecks(tool, baseDir) {
|
|
28
|
+
if (tool === 'hermes') {
|
|
29
|
+
const hermesConfig = path.join(os.homedir(), '.hermes', 'config.yaml');
|
|
30
|
+
let hookEntryFound = false;
|
|
31
|
+
|
|
32
|
+
if (fs.existsSync(hermesConfig)) {
|
|
33
|
+
try {
|
|
34
|
+
const raw = fs.readFileSync(hermesConfig, 'utf8');
|
|
35
|
+
const parsed = yaml.load(raw) || {};
|
|
36
|
+
const postToolCall = parsed.hooks && parsed.hooks.post_tool_call;
|
|
37
|
+
if (Array.isArray(postToolCall)) {
|
|
38
|
+
hookEntryFound = postToolCall.some((entry) =>
|
|
39
|
+
entry && entry.matcher === 'write_file|patch'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// 解析失败,不做深度校验
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
label: '~/.hermes/config.yaml',
|
|
50
|
+
path: hermesConfig,
|
|
51
|
+
exists: fs.existsSync(hermesConfig),
|
|
52
|
+
hookEntryFound,
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
if (tool === 'cursor') {
|
|
28
58
|
return [
|
|
29
59
|
{
|
package/lib/hooks_deploy.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const commentJson = require('comment-json');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
4
6
|
|
|
5
7
|
const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
|
|
6
8
|
const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
|
|
@@ -42,7 +44,10 @@ function readJsonFile(filePath, fallback) {
|
|
|
42
44
|
|
|
43
45
|
let data;
|
|
44
46
|
try {
|
|
45
|
-
|
|
47
|
+
// comment-json 容忍 JSONC 注释和尾随逗号。~/.claude/settings.json 等
|
|
48
|
+
// Claude/Cursor 配置文件官方支持 JSONC,必须容忍用户加的注释。
|
|
49
|
+
// 注意:写回时仍用 JSON.stringify,原文件中的注释会被丢弃。
|
|
50
|
+
data = commentJson.parse(raw);
|
|
46
51
|
} catch (error) {
|
|
47
52
|
const detail = error instanceof Error ? error.message : String(error);
|
|
48
53
|
throw new Error(`Invalid JSON in ${filePath}: ${detail}`);
|
|
@@ -424,19 +429,19 @@ function generateRondsHookBlocks(newline) {
|
|
|
424
429
|
].join(newline);
|
|
425
430
|
}
|
|
426
431
|
|
|
427
|
-
function ensureCodexRequirementsToml(reqPath) {
|
|
428
|
-
const exists = fs.existsSync(reqPath);
|
|
429
|
-
let content = exists ? fs.readFileSync(reqPath, 'utf8') : '';
|
|
430
|
-
if (content.charCodeAt(0) === 0xFEFF) {
|
|
431
|
-
content = content.slice(1);
|
|
432
|
-
}
|
|
433
|
-
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
432
|
+
function ensureCodexRequirementsToml(reqPath) {
|
|
433
|
+
const exists = fs.existsSync(reqPath);
|
|
434
|
+
let content = exists ? fs.readFileSync(reqPath, 'utf8') : '';
|
|
435
|
+
if (content.charCodeAt(0) === 0xFEFF) {
|
|
436
|
+
content = content.slice(1);
|
|
437
|
+
}
|
|
438
|
+
const newline = content.includes('\r\n') ? '\r\n' : '\n';
|
|
434
439
|
|
|
435
440
|
content = ensureCodexHooksFeatureFlag(content);
|
|
436
441
|
content = ensureCodexManagedDir(content, reqPath);
|
|
437
442
|
content = removeRondsBlocksFromToml(content);
|
|
438
|
-
content = content.replace(/[\t ]*\r?\n*$/, newline);
|
|
439
|
-
content += generateRondsHookBlocks(newline);
|
|
443
|
+
content = content.replace(/[\t ]*\r?\n*$/, newline);
|
|
444
|
+
content += generateRondsHookBlocks(newline);
|
|
440
445
|
|
|
441
446
|
const changed = writeTextFileIfChanged(reqPath, content);
|
|
442
447
|
return { exists, changed };
|
|
@@ -527,16 +532,16 @@ function cleanupClaudeLocalSettings(localCandidates, result) {
|
|
|
527
532
|
}
|
|
528
533
|
}
|
|
529
534
|
|
|
530
|
-
// 将系统级 Codex requirements.toml 的权限错误转换成面向用户的部署提示。
|
|
531
|
-
function wrapCodexPermissionError(error, codexRequirementsPath) {
|
|
532
|
-
const code = isPlainObject(error) ? error.code : '';
|
|
535
|
+
// 将系统级 Codex requirements.toml 的权限错误转换成面向用户的部署提示。
|
|
536
|
+
function wrapCodexPermissionError(error, codexRequirementsPath) {
|
|
537
|
+
const code = isPlainObject(error) ? error.code : '';
|
|
533
538
|
if (code !== 'EACCES' && code !== 'EPERM') {
|
|
534
539
|
return error;
|
|
535
540
|
}
|
|
536
541
|
|
|
537
|
-
const wrapped = new Error(
|
|
538
|
-
`Codex 部署需要管理员/root 权限,无法写入 ${codexRequirementsPath}。请以管理员身份重新运行 hooks deploy --scope user。`,
|
|
539
|
-
);
|
|
542
|
+
const wrapped = new Error(
|
|
543
|
+
`Codex 部署需要管理员/root 权限,无法写入 ${codexRequirementsPath}。请以管理员身份重新运行 hooks deploy --scope user。`,
|
|
544
|
+
);
|
|
540
545
|
wrapped.cause = error;
|
|
541
546
|
wrapped.code = code;
|
|
542
547
|
return wrapped;
|
|
@@ -600,6 +605,82 @@ function deployHooks(targetDir = process.cwd(), options = {}) {
|
|
|
600
605
|
return result;
|
|
601
606
|
}
|
|
602
607
|
|
|
608
|
+
const HERMES_CONFIG_PATH = path.join(os.homedir(), '.hermes', 'config.yaml');
|
|
609
|
+
const HERMES_MATCHER = 'write_file|patch';
|
|
610
|
+
const HERMES_COMMAND = 'npx ronds_ai@latest record hermes';
|
|
611
|
+
|
|
612
|
+
function isHermesHookEntry(entry) {
|
|
613
|
+
return isPlainObject(entry)
|
|
614
|
+
&& entry.matcher === HERMES_MATCHER
|
|
615
|
+
&& entry.command === HERMES_COMMAND;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function deployHermesHook() {
|
|
619
|
+
const exists = fs.existsSync(HERMES_CONFIG_PATH);
|
|
620
|
+
let config = {};
|
|
621
|
+
|
|
622
|
+
if (exists) {
|
|
623
|
+
const raw = fs.readFileSync(HERMES_CONFIG_PATH, 'utf8');
|
|
624
|
+
if (raw.trim()) {
|
|
625
|
+
config = yaml.load(raw) || {};
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!isPlainObject(config)) {
|
|
630
|
+
config = {};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const result = {
|
|
634
|
+
tool: 'hermes',
|
|
635
|
+
configPath: HERMES_CONFIG_PATH,
|
|
636
|
+
created: !exists,
|
|
637
|
+
updated: false,
|
|
638
|
+
details: [],
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// 确保 hooks.post_tool_call 存在
|
|
642
|
+
const hooks = isPlainObject(config.hooks) ? { ...config.hooks } : {};
|
|
643
|
+
const postToolCall = Array.isArray(hooks.post_tool_call) ? hooks.post_tool_call.slice() : [];
|
|
644
|
+
|
|
645
|
+
// 检查是否已有我们的条目
|
|
646
|
+
const existingEntry = postToolCall.find(isHermesHookEntry);
|
|
647
|
+
|
|
648
|
+
if (!existingEntry) {
|
|
649
|
+
// 移除旧的 ronds 条目(如有),再插入新条目
|
|
650
|
+
const filtered = postToolCall.filter((entry) => !isHermesHookEntry(entry));
|
|
651
|
+
filtered.push({
|
|
652
|
+
matcher: HERMES_MATCHER,
|
|
653
|
+
command: HERMES_COMMAND,
|
|
654
|
+
timeout: 30,
|
|
655
|
+
});
|
|
656
|
+
hooks.post_tool_call = filtered;
|
|
657
|
+
result.details.push('Added Hermes shell hook entry for write_file|patch');
|
|
658
|
+
result.updated = true;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
config.hooks = hooks;
|
|
662
|
+
|
|
663
|
+
// 如果 hooks_auto_accept 未设置,添加它(首次运行跳过交互确认)
|
|
664
|
+
if (config.hooks_auto_accept === undefined) {
|
|
665
|
+
config.hooks_auto_accept = true;
|
|
666
|
+
result.details.push('Set hooks_auto_accept: true');
|
|
667
|
+
result.updated = true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (result.updated || result.created) {
|
|
671
|
+
const yamlContent = yaml.dump(config, {
|
|
672
|
+
lineWidth: -1,
|
|
673
|
+
noRefs: true,
|
|
674
|
+
sortKeys: false,
|
|
675
|
+
});
|
|
676
|
+
ensureDir(path.dirname(HERMES_CONFIG_PATH));
|
|
677
|
+
fs.writeFileSync(HERMES_CONFIG_PATH, yamlContent, 'utf8');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
|
|
603
683
|
module.exports = {
|
|
604
684
|
deployHooks,
|
|
685
|
+
deployHermesHook,
|
|
605
686
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ronds_ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "CLI for reporting AI code edit events.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ronds_ai": "bin/ronds_ai.js"
|
|
@@ -12,5 +12,9 @@
|
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=16"
|
|
14
14
|
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"comment-json": "^4.2.5",
|
|
17
|
+
"js-yaml": "^5.1.0"
|
|
18
|
+
},
|
|
15
19
|
"license": "MIT"
|
|
16
20
|
}
|