ronds_ai 0.1.19 → 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 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 或 Cursor 的 hook payload,转换为统一事件后发送到服务端。
100
+ 从标准输入读取 Claude、Codex、CursorHermes 的 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` 与 `cursor`
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`);
@@ -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
  {
@@ -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.19",
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"
@@ -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
  }