ronds_ai 0.1.19 → 0.1.21
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 +65 -4
- package/bin/ronds_ai.js +33 -2
- package/lib/code_record.js +98 -0
- package/lib/doctor.js +31 -1
- package/lib/hooks_auto_sync.js +397 -0
- package/lib/hooks_deploy.js +77 -0
- package/package.json +3 -2
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
|
|
@@ -251,6 +286,30 @@ Codex 推荐使用 `--scope user`,因为项目级 `.codex` 配置在未被用
|
|
|
251
286
|
- `removedFiles`
|
|
252
287
|
- `skipped`: 被跳过的工具及原因(仅在 project scope 下出现)
|
|
253
288
|
|
|
289
|
+
### Hooks Auto Sync(自动后台同步)
|
|
290
|
+
|
|
291
|
+
CLI 会**自动在后台检查并更新用户级 hooks 配置**,无需用户每次手动执行 `hooks deploy --scope user`。
|
|
292
|
+
|
|
293
|
+
工作方式:
|
|
294
|
+
|
|
295
|
+
1. 运行任意 CLI 命令时,进行轻量检查(多数情况下只读取一次 sentinel 文件的修改时间)
|
|
296
|
+
2. 如果距离上次检查超过 24 小时,判断 hooks schema 版本是否落后
|
|
297
|
+
3. 版本落后时,后台启动子进程自动执行 `hooks deploy --scope user` 和 `deploy hermes`
|
|
298
|
+
4. 当前命令不等待后台同步完成,也不受后台同步失败的影响
|
|
299
|
+
5. `record` 高频路径使用 sentinel fast path,额外成本约等于一次文件 stat
|
|
300
|
+
|
|
301
|
+
可通过环境变量禁用:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
RONDS_AI_DISABLE_HOOKS_AUTO_SYNC=1
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
注意:
|
|
308
|
+
|
|
309
|
+
- auto-sync 只维护**用户级** hooks,不影响项目级配置
|
|
310
|
+
- 后台同步失败时,错误记录到 `~/.ronds_ai/hooks_auto_sync_error.log`,不污染当前命令输出
|
|
311
|
+
- 首次安装 hooks 仍建议执行 `ronds_ai hooks deploy --scope user`
|
|
312
|
+
|
|
254
313
|
### `skills install`
|
|
255
314
|
|
|
256
315
|
下载一个技能包并安装到 Claude / Codex / Cursor 对应的技能目录。
|
|
@@ -334,11 +393,13 @@ https://aihub.ronds.com/api/api/v1/ai-code-events
|
|
|
334
393
|
- `YYYYMMDD-claude-error.jsonl`
|
|
335
394
|
- `YYYYMMDD-codex-error.jsonl`
|
|
336
395
|
- `YYYYMMDD-cursor-error.jsonl`
|
|
396
|
+
- `YYYYMMDD-hermes-error.jsonl`
|
|
337
397
|
- `YYYYMMDD-cli-error.jsonl`
|
|
338
398
|
|
|
339
399
|
## Notes
|
|
340
400
|
|
|
341
|
-
- `record` 和 `doctor` 目前支持 `claude`、`codex
|
|
401
|
+
- `record` 和 `doctor` 目前支持 `claude`、`codex`、`cursor`、`hermes`
|
|
402
|
+
- `hooks deploy` 支持 `claude`、`codex`、`cursor`、`hermes`(hermes 为独立子命令)
|
|
342
403
|
- `skills install` 支持 `claude`、`codex`、`cursor`
|
|
343
404
|
- 不支持的命令或参数会直接报错,并把错误写入 CLI 错误日志
|
|
344
405
|
|
package/bin/ronds_ai.js
CHANGED
|
@@ -10,7 +10,8 @@ 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
|
+
const { maybeStartHooksAutoSync, runHooksAutoSync } = require('../lib/hooks_auto_sync');
|
|
14
15
|
const { promptForText, promptYesNo } = require('../lib/skills_prompt');
|
|
15
16
|
|
|
16
17
|
function writeWorkerSummary(result) {
|
|
@@ -57,7 +58,7 @@ function stripCheckFlags(args) {
|
|
|
57
58
|
return args.filter((value) => value !== '--no-prompt');
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const SUPPORTED_SOURCES = new Set(['claude', 'cursor', 'codex']);
|
|
61
|
+
const SUPPORTED_SOURCES = new Set(['claude', 'cursor', 'codex', 'hermes']);
|
|
61
62
|
|
|
62
63
|
function printUsage() {
|
|
63
64
|
process.stderr.write([
|
|
@@ -66,12 +67,14 @@ function printUsage() {
|
|
|
66
67
|
' ronds_ai check record',
|
|
67
68
|
' ronds_ai doctor <tool>',
|
|
68
69
|
' ronds_ai hooks deploy [--scope project|user] [--tool cursor|claude|codex]',
|
|
70
|
+
' ronds_ai hooks deploy hermes',
|
|
69
71
|
' ronds_ai skills install <name> [--tool claude,codex,cursor] [--scope project|global] [--project-dir <path>] [--force]',
|
|
70
72
|
'',
|
|
71
73
|
'Supported tools:',
|
|
72
74
|
' claude',
|
|
73
75
|
' cursor',
|
|
74
76
|
' codex',
|
|
77
|
+
' hermes',
|
|
75
78
|
'',
|
|
76
79
|
'Examples:',
|
|
77
80
|
' npx ronds_ai@latest record claude',
|
|
@@ -91,6 +94,13 @@ function printUsage() {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
async function runHooksCommand(args) {
|
|
97
|
+
// Hermes deploy 走独立路径(YAML 格式、用户级配置,与现有 JSON 部署不同)
|
|
98
|
+
if (args[0] === 'deploy' && args[1] === 'hermes') {
|
|
99
|
+
const result = deployHermesHook();
|
|
100
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
const options = parseHooksDeployArgs(args);
|
|
95
105
|
const result = deployHooks(process.cwd(), options);
|
|
96
106
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
@@ -251,9 +261,25 @@ async function runSkillsCommand(args) {
|
|
|
251
261
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
252
262
|
}
|
|
253
263
|
|
|
264
|
+
/**
|
|
265
|
+
* 内部命令分发。当前只支持 hooks auto-sync。
|
|
266
|
+
*/
|
|
267
|
+
async function runInternalCommand(args) {
|
|
268
|
+
const [subcommand, ...subargs] = args;
|
|
269
|
+
|
|
270
|
+
if (subcommand === 'hooks' && subargs[0] === 'auto-sync') {
|
|
271
|
+
runHooksAutoSync();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw new Error('Unsupported internal command');
|
|
276
|
+
}
|
|
277
|
+
|
|
254
278
|
async function run() {
|
|
255
279
|
const [, , command, ...args] = process.argv;
|
|
256
280
|
|
|
281
|
+
maybeStartHooksAutoSync({ command, args });
|
|
282
|
+
|
|
257
283
|
if (command === 'record') {
|
|
258
284
|
const [source] = args;
|
|
259
285
|
const normalizedSource = String(source || '').trim().toLowerCase();
|
|
@@ -294,6 +320,11 @@ async function run() {
|
|
|
294
320
|
return;
|
|
295
321
|
}
|
|
296
322
|
|
|
323
|
+
if (command === 'internal') {
|
|
324
|
+
await runInternalCommand(args);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
297
328
|
throw new Error(`Unsupported command: ${command || ''}`);
|
|
298
329
|
}
|
|
299
330
|
|
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
|
{
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks_auto_sync.js
|
|
3
|
+
*
|
|
4
|
+
* 用户级 Hooks 自动同步模块。
|
|
5
|
+
*
|
|
6
|
+
* 提供轻量的前台检查(maybeStartHooksAutoSync)和后端子进程同步(runHooksAutoSync),
|
|
7
|
+
* 让 CLI 命令能无感自动维护用户级 hooks 配置(Claude/Cursor/Hermes/Codex)。
|
|
8
|
+
*
|
|
9
|
+
* 快速路径:sentinel 文件 mtime + TTL,避免高频命令(如 record)反复读取状态 JSON。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const { spawn } = require('child_process');
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// 常量
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** 当前 hooks schema 版本。当用户级 hooks deploy 输出发生变化时 +1。 */
|
|
22
|
+
const HOOKS_SCHEMA_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
/** sentinel 有效时长:24 小时 */
|
|
25
|
+
const HOOKS_AUTO_SYNC_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/** lock 文件视为 stale 的超时时长:10 分钟 */
|
|
28
|
+
const STALE_LOCK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// 路径解析
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function getAutoSyncDir() {
|
|
35
|
+
return path.join(os.homedir(), '.ronds_ai');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getStateFilePath() {
|
|
39
|
+
return path.join(getAutoSyncDir(), 'hooks_state.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getSentinelFilePath() {
|
|
43
|
+
return path.join(getAutoSyncDir(), 'hooks_auto_sync_sentinel');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getLockFilePath() {
|
|
47
|
+
return path.join(getAutoSyncDir(), 'hooks_auto_sync.lock');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getErrorLogPath() {
|
|
51
|
+
return path.join(getAutoSyncDir(), 'hooks_auto_sync_error.log');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureAutoSyncDir() {
|
|
55
|
+
const dir = getAutoSyncDir();
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
return dir;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Sentinel fast path(步骤 3)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 快速跳过检查——sentinel 文件存在且 mtime 在 TTL 内。
|
|
66
|
+
* @param {number} now - Date.now()
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function shouldFastSkipAutoSync(now) {
|
|
70
|
+
try {
|
|
71
|
+
const stat = fs.statSync(getSentinelFilePath());
|
|
72
|
+
return now - stat.mtimeMs < HOOKS_AUTO_SYNC_CHECK_TTL_MS;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 更新 sentinel 文件的 mtime。
|
|
80
|
+
* 文件不存在则创建空文件。
|
|
81
|
+
* @param {number} now - Date.now()
|
|
82
|
+
*/
|
|
83
|
+
function touchAutoSyncSentinel(now) {
|
|
84
|
+
ensureAutoSyncDir();
|
|
85
|
+
const sentinelPath = getSentinelFilePath();
|
|
86
|
+
const atime = Math.floor(now / 1000);
|
|
87
|
+
const mtime = Math.floor(now / 1000);
|
|
88
|
+
try {
|
|
89
|
+
fs.utimesSync(sentinelPath, atime, mtime);
|
|
90
|
+
} catch {
|
|
91
|
+
// 文件不存在,创建空文件
|
|
92
|
+
fs.writeFileSync(sentinelPath, '');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 状态文件读写(步骤 4)
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 读取 hooks_state.json。
|
|
102
|
+
* 文件不存在、内容为空、JSON 解析失败或顶层非对象时返回空对象。
|
|
103
|
+
* @returns {object}
|
|
104
|
+
*/
|
|
105
|
+
function readHooksAutoSyncState() {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(getStateFilePath(), 'utf-8');
|
|
108
|
+
if (!content.trim()) return {};
|
|
109
|
+
const parsed = JSON.parse(content);
|
|
110
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
111
|
+
return {};
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 写入 hooks_state.json。
|
|
121
|
+
* @param {{ schemaVersion?: number, lastCheckedAt?: number, lastSyncedAt?: number }} state
|
|
122
|
+
*/
|
|
123
|
+
function writeHooksAutoSyncState(state) {
|
|
124
|
+
ensureAutoSyncDir();
|
|
125
|
+
const data = {
|
|
126
|
+
schemaVersion: Number(state.schemaVersion) || 0,
|
|
127
|
+
lastCheckedAt: Number(state.lastCheckedAt) || 0,
|
|
128
|
+
lastSyncedAt: Number(state.lastSyncedAt) || 0,
|
|
129
|
+
};
|
|
130
|
+
fs.writeFileSync(getStateFilePath(), JSON.stringify(data, null, 2) + '\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// 命令过滤逻辑(步骤 5)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 判断当前命令是否应完全跳过 auto-sync。
|
|
139
|
+
* @param {string} command - 命令名称
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
function shouldSkipHooksAutoSync(command) {
|
|
143
|
+
if (process.env.RONDS_AI_DISABLE_HOOKS_AUTO_SYNC === '1') return true;
|
|
144
|
+
if (command === 'hooks' || command === 'internal') return true;
|
|
145
|
+
if (!command) return true;
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// TTL 与版本判断(步骤 6)
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 根据状态和当前时间判断是否需要调度后台同步。
|
|
155
|
+
*
|
|
156
|
+
* @param {object} state - 从状态文件读取的状态
|
|
157
|
+
* @param {number} now - Date.now()
|
|
158
|
+
* @returns {'skip' | 'update-timestamp' | 'schedule'}
|
|
159
|
+
*/
|
|
160
|
+
function shouldScheduleHooksAutoSync(state, now) {
|
|
161
|
+
const lastCheckedAt = Number(state.lastCheckedAt || 0);
|
|
162
|
+
|
|
163
|
+
// 仍在 TTL 窗口内,跳过
|
|
164
|
+
if (now - lastCheckedAt < HOOKS_AUTO_SYNC_CHECK_TTL_MS) {
|
|
165
|
+
return 'skip';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const schemaVersion = Number(state.schemaVersion || 0);
|
|
169
|
+
|
|
170
|
+
// 版本已是最新,仅更新时间戳
|
|
171
|
+
if (schemaVersion >= HOOKS_SCHEMA_VERSION) {
|
|
172
|
+
return 'update-timestamp';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 版本落后,需要调度同步
|
|
176
|
+
return 'schedule';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// 后台进程启动(步骤 7)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 启动后台子进程执行用户级 hooks deploy。
|
|
185
|
+
* 使用 process.execPath + require.resolve 确保:
|
|
186
|
+
* - Windows 上不需 shell,detach 可靠
|
|
187
|
+
* - 与当前进程使用相同的 Node 版本
|
|
188
|
+
* - 指向本地已安装的代码(版本一致)
|
|
189
|
+
*/
|
|
190
|
+
function spawnHooksAutoSyncProcess() {
|
|
191
|
+
try {
|
|
192
|
+
const cliEntry = require.resolve('../bin/ronds_ai.js');
|
|
193
|
+
const child = spawn(process.execPath, [cliEntry, 'internal', 'hooks', 'auto-sync'], {
|
|
194
|
+
detached: true,
|
|
195
|
+
stdio: 'ignore',
|
|
196
|
+
windowsHide: true,
|
|
197
|
+
});
|
|
198
|
+
child.on('error', saveHooksAutoSyncError);
|
|
199
|
+
child.unref();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
saveHooksAutoSyncError(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// maybeStartHooksAutoSync(步骤 8)
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 轻量前台检查函数。
|
|
211
|
+
*
|
|
212
|
+
* 所有 CLI 命令在分发前均可调用此函数,它会在绝大多数调用中快速返回。
|
|
213
|
+
*
|
|
214
|
+
* @param {{ command: string, args?: string[] }} params
|
|
215
|
+
*/
|
|
216
|
+
function maybeStartHooksAutoSync({ command, args } = {}) {
|
|
217
|
+
try {
|
|
218
|
+
if (shouldSkipHooksAutoSync(command)) return;
|
|
219
|
+
|
|
220
|
+
const now = Date.now();
|
|
221
|
+
if (shouldFastSkipAutoSync(now)) return;
|
|
222
|
+
|
|
223
|
+
const state = readHooksAutoSyncState();
|
|
224
|
+
const decision = shouldScheduleHooksAutoSync(state, now);
|
|
225
|
+
|
|
226
|
+
if (decision === 'skip') {
|
|
227
|
+
touchAutoSyncSentinel(now);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (decision === 'update-timestamp') {
|
|
232
|
+
writeHooksAutoSyncState({
|
|
233
|
+
schemaVersion: Number(state.schemaVersion || 0),
|
|
234
|
+
lastCheckedAt: now,
|
|
235
|
+
lastSyncedAt: Number(state.lastSyncedAt || 0),
|
|
236
|
+
});
|
|
237
|
+
touchAutoSyncSentinel(now);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// decision === 'schedule'
|
|
242
|
+
touchAutoSyncSentinel(now);
|
|
243
|
+
spawnHooksAutoSyncProcess();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
saveHooksAutoSyncError(error);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Lock 文件管理(步骤 9)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 获取 auto-sync 全局锁。
|
|
255
|
+
* 使用原子创建('wx')防止并发。
|
|
256
|
+
*
|
|
257
|
+
* @returns {number | null} lock 文件描述符,获取失败返回 null
|
|
258
|
+
*/
|
|
259
|
+
function acquireHooksAutoSyncLock() {
|
|
260
|
+
const lockPath = getLockFilePath();
|
|
261
|
+
ensureAutoSyncDir();
|
|
262
|
+
|
|
263
|
+
// 首次尝试创建
|
|
264
|
+
try {
|
|
265
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
266
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, time: Date.now() }) + '\n');
|
|
267
|
+
return fd;
|
|
268
|
+
} catch (err) {
|
|
269
|
+
if (err.code !== 'EEXIST') return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// 文件已存在,检查是否 stale
|
|
273
|
+
try {
|
|
274
|
+
const stat = fs.statSync(lockPath);
|
|
275
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
276
|
+
fs.unlinkSync(lockPath);
|
|
277
|
+
// 重试一次
|
|
278
|
+
try {
|
|
279
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
280
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, time: Date.now() }) + '\n');
|
|
281
|
+
return fd;
|
|
282
|
+
} catch {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// stat 失败,尝试直接创建
|
|
288
|
+
try {
|
|
289
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
290
|
+
return fd;
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* 释放 auto-sync 全局锁。
|
|
301
|
+
* @param {number} fd - 文件描述符
|
|
302
|
+
*/
|
|
303
|
+
function releaseHooksAutoSyncLock(fd) {
|
|
304
|
+
try {
|
|
305
|
+
fs.closeSync(fd);
|
|
306
|
+
} catch {
|
|
307
|
+
// 忽略关闭失败
|
|
308
|
+
}
|
|
309
|
+
try {
|
|
310
|
+
fs.unlinkSync(getLockFilePath());
|
|
311
|
+
} catch {
|
|
312
|
+
// 忽略删除失败
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// 后台同步函数 runHooksAutoSync(步骤 10)
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 执行用户级 hooks auto-sync。
|
|
322
|
+
*
|
|
323
|
+
* 此函数由后台子进程(ronds_ai internal hooks auto-sync)调用。
|
|
324
|
+
* 不向 stdout 输出,错误写入本地日志文件。
|
|
325
|
+
*/
|
|
326
|
+
function runHooksAutoSync() {
|
|
327
|
+
const lock = acquireHooksAutoSyncLock();
|
|
328
|
+
if (!lock) return; // 已有进程在同步
|
|
329
|
+
|
|
330
|
+
const now = Date.now();
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const { deployHooks, deployHermesHook } = require('./hooks_deploy');
|
|
334
|
+
|
|
335
|
+
deployHooks(os.homedir(), { scope: 'user' });
|
|
336
|
+
deployHermesHook();
|
|
337
|
+
|
|
338
|
+
writeHooksAutoSyncState({
|
|
339
|
+
schemaVersion: HOOKS_SCHEMA_VERSION,
|
|
340
|
+
lastCheckedAt: now,
|
|
341
|
+
lastSyncedAt: now,
|
|
342
|
+
});
|
|
343
|
+
touchAutoSyncSentinel(now);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
saveHooksAutoSyncError(error);
|
|
346
|
+
} finally {
|
|
347
|
+
releaseHooksAutoSyncLock(lock);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
// 错误记录
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 将 auto-sync 错误写入本地日志文件。
|
|
357
|
+
* @param {Error | string} error
|
|
358
|
+
*/
|
|
359
|
+
function saveHooksAutoSyncError(error) {
|
|
360
|
+
try {
|
|
361
|
+
ensureAutoSyncDir();
|
|
362
|
+
const timestamp = new Date().toISOString();
|
|
363
|
+
const message = error instanceof Error ? error.stack || error.message : String(error);
|
|
364
|
+
const entry = [
|
|
365
|
+
`--- ${timestamp} ---`,
|
|
366
|
+
`HOOKS_SCHEMA_VERSION=${HOOKS_SCHEMA_VERSION}`,
|
|
367
|
+
`node=${process.version}`,
|
|
368
|
+
`platform=${process.platform}`,
|
|
369
|
+
`error=${message}`,
|
|
370
|
+
'',
|
|
371
|
+
].join('\n');
|
|
372
|
+
|
|
373
|
+
fs.appendFileSync(getErrorLogPath(), entry, 'utf-8');
|
|
374
|
+
} catch {
|
|
375
|
+
// 错误记录失败时不做任何事,避免连锁异常
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// 导出
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
module.exports = {
|
|
384
|
+
HOOKS_SCHEMA_VERSION,
|
|
385
|
+
maybeStartHooksAutoSync,
|
|
386
|
+
runHooksAutoSync,
|
|
387
|
+
|
|
388
|
+
// 以下为测试和内部使用导出的函数
|
|
389
|
+
shouldSkipHooksAutoSync,
|
|
390
|
+
shouldFastSkipAutoSync,
|
|
391
|
+
shouldScheduleHooksAutoSync,
|
|
392
|
+
touchAutoSyncSentinel,
|
|
393
|
+
readHooksAutoSyncState,
|
|
394
|
+
writeHooksAutoSyncState,
|
|
395
|
+
acquireHooksAutoSyncLock,
|
|
396
|
+
releaseHooksAutoSyncLock,
|
|
397
|
+
};
|
package/lib/hooks_deploy.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const commentJson = require('comment-json');
|
|
5
|
+
const yaml = require('js-yaml');
|
|
5
6
|
|
|
6
7
|
const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
|
|
7
8
|
const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
|
|
@@ -604,6 +605,82 @@ function deployHooks(targetDir = process.cwd(), options = {}) {
|
|
|
604
605
|
return result;
|
|
605
606
|
}
|
|
606
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
|
+
|
|
607
683
|
module.exports = {
|
|
608
684
|
deployHooks,
|
|
685
|
+
deployHermesHook,
|
|
609
686
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ronds_ai",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"description": "CLI for reporting AI code edit events.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ronds_ai": "bin/ronds_ai.js"
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"node": ">=16"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"comment-json": "^4.2.5"
|
|
16
|
+
"comment-json": "^4.2.5",
|
|
17
|
+
"js-yaml": "^5.1.0"
|
|
17
18
|
},
|
|
18
19
|
"license": "MIT"
|
|
19
20
|
}
|