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.
- package/README.md +61 -2
- package/bin/ronds_ai.js +30 -10
- package/lib/analyze_claude.js +471 -0
- package/lib/analyze_config.js +68 -0
- package/lib/analyze_langfuse.js +736 -0
- package/lib/analyze_transcript.js +803 -0
- package/lib/hooks_auto_sync.js +2 -1
- package/lib/hooks_deploy.js +109 -3
- package/package.json +11 -3
package/lib/hooks_auto_sync.js
CHANGED
|
@@ -19,7 +19,8 @@ const { spawn } = require('child_process');
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
/** 当前 hooks schema 版本。当用户级 hooks deploy 输出发生变化时 +1。 */
|
|
22
|
-
|
|
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;
|
package/lib/hooks_deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|