ronds_ai 0.1.21 → 0.1.23

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.
@@ -19,7 +19,8 @@ const { spawn } = require('child_process');
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
21
  /** 当前 hooks schema 版本。当用户级 hooks deploy 输出发生变化时 +1。 */
22
- const HOOKS_SCHEMA_VERSION = 1;
22
+ // v2: 用户级 Claude settings 新增 Stop/SessionEnd analyze hook。
23
+ const HOOKS_SCHEMA_VERSION = 2;
23
24
 
24
25
  /** sentinel 有效时长:24 小时 */
25
26
  const HOOKS_AUTO_SYNC_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
@@ -6,6 +6,8 @@ const yaml = require('js-yaml');
6
6
 
7
7
  const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
8
8
  const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
9
+ const CLAUDE_ANALYZE_COMMAND = 'npx ronds_ai@latest analyze claude';
10
+ const CLAUDE_ANALYZE_EVENTS = ['Stop', 'SessionEnd'];
9
11
  const CODEX_COMMAND = 'npx ronds_ai@latest record codex';
10
12
  const CURSOR_OLD_COMMAND = 'node .cursor/hooks/cursor_hook_request.cjs';
11
13
  const CURSOR_OLD_JS_COMMAND = 'node .cursor/hooks/cursor_hook_request.js';
@@ -216,6 +218,77 @@ function removeClaudeHookFromConfig(config) {
216
218
  return next;
217
219
  }
218
220
 
221
+ /**
222
+ * 解析 Node.js 主版本号,解析失败返回 0。
223
+ * @param {string} version - Node.js 版本字符串
224
+ * @returns {number}
225
+ */
226
+ function getNodeMajorVersion(version = process.versions.node) {
227
+ const major = Number(String(version || '').replace(/^v/i, '').split('.')[0]);
228
+ return Number.isInteger(major) ? major : 0;
229
+ }
230
+
231
+ /**
232
+ * 从单个 Claude hook 事件数组中抽离本工具管理的 analyze 命令。
233
+ * 保留用户在该事件下的其他 entry;analyze 所在 entry 的其余命令会合并到结果中。
234
+ *
235
+ * @param {Array} entries - hooks[event] 数组
236
+ * @returns {{ keepEntries: Array, desiredHooks: Array }}
237
+ */
238
+ function splitClaudeAnalyzeEntries(entries) {
239
+ const keepEntries = [];
240
+ const desiredHooks = [];
241
+
242
+ for (const entry of entries) {
243
+ if (!isPlainObject(entry)) {
244
+ keepEntries.push(entry);
245
+ continue;
246
+ }
247
+
248
+ const existingHooks = Array.isArray(entry.hooks) ? entry.hooks : [];
249
+ const containsAnalyze = existingHooks.some(
250
+ (hook) => isPlainObject(hook) && hook.type === 'command' && hook.command === CLAUDE_ANALYZE_COMMAND,
251
+ );
252
+
253
+ if (!containsAnalyze) {
254
+ keepEntries.push(entry);
255
+ continue;
256
+ }
257
+
258
+ for (const hook of existingHooks) {
259
+ if (!(isPlainObject(hook) && hook.type === 'command' && hook.command === CLAUDE_ANALYZE_COMMAND)) {
260
+ desiredHooks.push(hook);
261
+ }
262
+ }
263
+ }
264
+
265
+ return { keepEntries, desiredHooks };
266
+ }
267
+
268
+ /**
269
+ * 在 Claude settings 中确保 Stop/SessionEnd 的 analyze hook 条目。
270
+ * 只管理命令完全等于 CLAUDE_ANALYZE_COMMAND 的 hook,保留用户自定义条目。
271
+ */
272
+ function ensureClaudeAnalyzeHooks(config) {
273
+ const next = isPlainObject(config) ? { ...config } : {};
274
+ const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
275
+
276
+ for (const eventName of CLAUDE_ANALYZE_EVENTS) {
277
+ const entries = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
278
+ const { keepEntries, desiredHooks } = splitClaudeAnalyzeEntries(entries);
279
+
280
+ desiredHooks.push({
281
+ type: 'command',
282
+ command: CLAUDE_ANALYZE_COMMAND,
283
+ });
284
+
285
+ hooks[eventName] = keepEntries.concat([{ hooks: desiredHooks }]);
286
+ }
287
+
288
+ next.hooks = hooks;
289
+ return next;
290
+ }
291
+
219
292
  function ensureCodexHooksFeatureFlag(toml) {
220
293
  const newline = toml.includes('\r\n') ? '\r\n' : '\n';
221
294
  const desiredLine = `hooks = true${newline}`;
@@ -511,9 +584,28 @@ function deployCursorHook(cursorPath, result) {
511
584
  recordWriteResult(cursorPath, cursorResult.exists, changed, result);
512
585
  }
513
586
 
514
- function deployClaudeSettings(claudeSettingsPath, result) {
587
+ /**
588
+ * 判断本次部署是否写入 Claude analyze hook。
589
+ * 不满足条件时把原因写入 result.analyze.reason 并返回 false。
590
+ */
591
+ function shouldDeployClaudeAnalyze(result) {
592
+ const nodeMajor = getNodeMajorVersion();
593
+ if (nodeMajor < 20) {
594
+ result.analyze.reason = `analyze requires Node.js 20 or newer; current is ${process.versions.node || 'unknown'}`;
595
+ return false;
596
+ }
597
+
598
+ return true;
599
+ }
600
+
601
+ function deployClaudeSettings(claudeSettingsPath, result, analyzeDeployed) {
515
602
  const claudeSettingsResult = readJsonFile(claudeSettingsPath, {});
516
- const nextClaudeSettings = ensureClaudeSettings(claudeSettingsResult.data);
603
+ let nextClaudeSettings = ensureClaudeSettings(claudeSettingsResult.data);
604
+
605
+ if (analyzeDeployed) {
606
+ nextClaudeSettings = ensureClaudeAnalyzeHooks(nextClaudeSettings);
607
+ }
608
+
517
609
  const changed = writeJsonFile(claudeSettingsPath, nextClaudeSettings);
518
610
  recordWriteResult(claudeSettingsPath, claudeSettingsResult.exists, changed, result);
519
611
  }
@@ -574,6 +666,12 @@ function deployHooks(targetDir = process.cwd(), options = {}) {
574
666
  updatedFiles: [],
575
667
  createdFiles: [],
576
668
  skipped: [],
669
+ analyze: {
670
+ tool: 'claude',
671
+ installed: false,
672
+ events: [],
673
+ command: CLAUDE_ANALYZE_COMMAND,
674
+ },
577
675
  };
578
676
 
579
677
  cleanupLegacyHookScripts(paths.configRoot, result, options.tool);
@@ -585,9 +683,15 @@ function deployHooks(targetDir = process.cwd(), options = {}) {
585
683
  }
586
684
 
587
685
  if (!options.tool || options.tool === 'claude') {
588
- deployClaudeSettings(paths.claudeSettingsPath, result);
686
+ const analyzeDeployed = shouldDeployClaudeAnalyze(result);
687
+ deployClaudeSettings(paths.claudeSettingsPath, result, analyzeDeployed);
688
+ if (analyzeDeployed) {
689
+ result.analyze.installed = true;
690
+ result.analyze.events = [...CLAUDE_ANALYZE_EVENTS];
691
+ }
589
692
  } else {
590
693
  result.skipped.push({ tool: 'claude', reason: 'Skipped by --tool filter' });
694
+ result.analyze.reason = 'Skipped by --tool filter';
591
695
  }
592
696
 
593
697
  if (options.tool && options.tool !== 'codex') {
@@ -683,4 +787,6 @@ function deployHermesHook() {
683
787
  module.exports = {
684
788
  deployHooks,
685
789
  deployHermesHook,
790
+ ensureClaudeAnalyzeHooks,
791
+ getNodeMajorVersion,
686
792
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds_ai",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "CLI for reporting AI code edit events.",
5
5
  "bin": {
6
6
  "ronds_ai": "bin/ronds_ai.js"
@@ -12,9 +12,17 @@
12
12
  "engines": {
13
13
  "node": ">=16"
14
14
  },
15
+ "scripts": {
16
+ "test": "node --test"
17
+ },
15
18
  "dependencies": {
16
19
  "comment-json": "^4.2.5",
17
- "js-yaml": "^5.1.0"
20
+ "js-yaml": "^5.2.2"
18
21
  },
19
- "license": "MIT"
22
+ "license": "MIT",
23
+ "optionalDependencies": {
24
+ "@langfuse/otel": "^5.9.1",
25
+ "@langfuse/tracing": "^5.9.1",
26
+ "@opentelemetry/sdk-node": "^0.221.0"
27
+ }
20
28
  }