ronds_ai 0.1.22 → 0.1.24
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 +69 -67
- package/lib/analyze_claude.js +19 -8
- package/lib/analyze_config.js +22 -2
- package/lib/hooks_auto_sync.js +2 -1
- package/lib/hooks_deploy.js +109 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
## Requirements
|
|
9
9
|
|
|
10
|
-
- Node.js `>=16`
|
|
11
|
-
- `git`
|
|
12
|
-
- `unzip`
|
|
13
|
-
|
|
14
|
-
`git` 主要用于读取仓库信息和用户邮箱;`unzip` 用于 `skills install` 解压技能包。
|
|
15
|
-
可选的 `analyze` 命令单独要求 Node.js `>=20`,不会改变 `record` 等现有命令的 Node 版本要求。
|
|
10
|
+
- Node.js `>=16`
|
|
11
|
+
- `git`
|
|
12
|
+
- `unzip`
|
|
13
|
+
|
|
14
|
+
`git` 主要用于读取仓库信息和用户邮箱;`unzip` 用于 `skills install` 解压技能包。
|
|
15
|
+
可选的 `analyze` 命令单独要求 Node.js `>=20`,不会改变 `record` 等现有命令的 Node 版本要求。
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -170,65 +170,65 @@ Codex 输出示例:
|
|
|
170
170
|
{"continue":true,"hookSpecificOutput":{"hookEventName":"PostToolUse"}}
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
**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 结果行。
|
|
174
|
-
|
|
175
|
-
### `analyze`
|
|
176
|
-
|
|
177
|
-
从 Claude Code 的会话 transcript 增量生成 Langfuse 轨迹:
|
|
178
|
-
|
|
179
|
-
```bash
|
|
180
|
-
ronds_ai analyze claude
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
该命令用于 `Stop` 和 `SessionEnd` Hook
|
|
184
|
-
|
|
185
|
-
- 每个对话 Turn 的根 Span
|
|
186
|
-
- 每次 Assistant 消息对应的 LLM Generation 和 Token usage
|
|
187
|
-
- Tool 调用的输入、输出和时间
|
|
188
|
-
- Skill 标签及可选的 Skill 注入内容
|
|
189
|
-
- 一层 Subagent 的 Generation 和 Tool 轨迹
|
|
190
|
-
|
|
191
|
-
行为与 Langfuse Claude Observability Plugin 一致,只采集 transcript 中的 `text`、`tool_use` 和 `tool_result`,不上传 `thinking` block。状态和日志保存在 `~/.ronds_ai/analyze/`。
|
|
192
|
-
|
|
193
|
-
Langfuse 连接信息已内置,Trace 的 `userId`
|
|
194
|
-
|
|
195
|
-
- `CC_LANGFUSE_DEBUG=true`:启用详细日志
|
|
196
|
-
- `CC_LANGFUSE_MAX_CHARS=<正整数>`:单个文本字段的最大字符数,默认 `20000`
|
|
197
|
-
- `CC_LANGFUSE_SKILL_TAGS=false`:关闭 `skill:<name>` 标签
|
|
198
|
-
- `CC_LANGFUSE_CAPTURE_SKILL_CONTENT=true`:把 Skill 注入内容写入 Tool 输出
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
```json
|
|
203
|
-
{
|
|
204
|
-
"hooks": {
|
|
205
|
-
"Stop": [
|
|
206
|
-
{
|
|
207
|
-
"hooks": [
|
|
208
|
-
{
|
|
209
|
-
"type": "command",
|
|
210
|
-
"command": "npx ronds_ai@latest analyze claude"
|
|
211
|
-
}
|
|
212
|
-
]
|
|
213
|
-
}
|
|
214
|
-
],
|
|
215
|
-
"SessionEnd": [
|
|
216
|
-
{
|
|
217
|
-
"hooks": [
|
|
218
|
-
{
|
|
219
|
-
"type": "command",
|
|
220
|
-
"command": "npx ronds_ai@latest analyze claude"
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
}
|
|
224
|
-
]
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
Node.js 低于 20 或安装时使用了 `--omit=optional` 时,只有 `analyze` 会返回明确错误;`record` 等其他命令仍可运行。Hook payload 或上传处理失败时,`analyze` 保持 fail-open 并把诊断信息写入本地日志。
|
|
230
|
-
|
|
231
|
-
### `doctor`
|
|
173
|
+
**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 结果行。
|
|
174
|
+
|
|
175
|
+
### `analyze`
|
|
176
|
+
|
|
177
|
+
从 Claude Code 的会话 transcript 增量生成 Langfuse 轨迹:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
ronds_ai analyze claude
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
该命令用于 `Stop` 和 `SessionEnd` Hook,会由 `hooks deploy` 自动安装(project 或 user scope,未用 `--tool` 排除 claude 时)。它会在 Langfuse 中记录:
|
|
184
|
+
|
|
185
|
+
- 每个对话 Turn 的根 Span
|
|
186
|
+
- 每次 Assistant 消息对应的 LLM Generation 和 Token usage
|
|
187
|
+
- Tool 调用的输入、输出和时间
|
|
188
|
+
- Skill 标签及可选的 Skill 注入内容
|
|
189
|
+
- 一层 Subagent 的 Generation 和 Tool 轨迹
|
|
190
|
+
|
|
191
|
+
行为与 Langfuse Claude Observability Plugin 一致,只采集 transcript 中的 `text`、`tool_use` 和 `tool_result`,不上传 `thinking` block。状态和日志保存在 `~/.ronds_ai/analyze/`。
|
|
192
|
+
|
|
193
|
+
Langfuse 连接信息已内置,Trace 的 `userId` 优先使用当前项目的 `git user.email`,读取不到时回退到当前操作系统用户名。可选配置:
|
|
194
|
+
|
|
195
|
+
- `CC_LANGFUSE_DEBUG=true`:启用详细日志
|
|
196
|
+
- `CC_LANGFUSE_MAX_CHARS=<正整数>`:单个文本字段的最大字符数,默认 `20000`
|
|
197
|
+
- `CC_LANGFUSE_SKILL_TAGS=false`:关闭 `skill:<name>` 标签
|
|
198
|
+
- `CC_LANGFUSE_CAPTURE_SKILL_CONTENT=true`:把 Skill 注入内容写入 Tool 输出
|
|
199
|
+
|
|
200
|
+
`hooks deploy` 会自动把以下条目写入 Claude Code 的 hooks 配置;如需手动配置可参考。注意停用其他 Langfuse Claude 插件以避免重复上传:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"hooks": {
|
|
205
|
+
"Stop": [
|
|
206
|
+
{
|
|
207
|
+
"hooks": [
|
|
208
|
+
{
|
|
209
|
+
"type": "command",
|
|
210
|
+
"command": "npx ronds_ai@latest analyze claude"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
],
|
|
215
|
+
"SessionEnd": [
|
|
216
|
+
{
|
|
217
|
+
"hooks": [
|
|
218
|
+
{
|
|
219
|
+
"type": "command",
|
|
220
|
+
"command": "npx ronds_ai@latest analyze claude"
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Node.js 低于 20 或安装时使用了 `--omit=optional` 时,只有 `analyze` 会返回明确错误;`record` 等其他命令仍可运行。Hook payload 或上传处理失败时,`analyze` 保持 fail-open 并把诊断信息写入本地日志。
|
|
230
|
+
|
|
231
|
+
### `doctor`
|
|
232
232
|
|
|
233
233
|
检查当前目录下的 hook 配置和最近错误日志,方便排查接入问题。
|
|
234
234
|
|
|
@@ -297,14 +297,14 @@ npx ronds_ai@latest hooks deploy --scope user
|
|
|
297
297
|
默认 scope 为 `project`(项目级部署),行为:
|
|
298
298
|
|
|
299
299
|
- 写入或更新 `.cursor/hooks.json`
|
|
300
|
-
- 写入或更新 `.claude/settings.json`
|
|
300
|
+
- 写入或更新 `.claude/settings.json`(`PostToolUse` 的 `record claude` 采集 hook,以及 `Stop`、`SessionEnd` 的 `analyze claude` Langfuse 轨迹 hook;Node 低于 20 时跳过 analyze 并在输出 `analyze` 字段说明原因)
|
|
301
301
|
- 清理项目内旧版 Cursor、Claude、Codex hook 脚本
|
|
302
302
|
- 跳过 Codex 项目级配置部署,并在输出中说明原因
|
|
303
303
|
|
|
304
304
|
`--scope user`(用户级部署),行为:
|
|
305
305
|
|
|
306
306
|
- 写入或更新用户级 Cursor hook 配置
|
|
307
|
-
- 写入或更新用户级 Claude hook
|
|
307
|
+
- 写入或更新用户级 Claude hook 配置(同上:`record claude` 采集 hook + `analyze claude` 轨迹 hook)
|
|
308
308
|
- 写入或更新系统级 `requirements.toml`(Windows: `C:\ProgramData\OpenAI\Codex\requirements.toml`,Linux / macOS: `/etc/codex/requirements.toml`),包含 `[features] hooks = true`、`[hooks]` 托管目录配置以及 `PostToolUse` 的 `apply_patch` hook
|
|
309
309
|
- 清理用户目录中的旧版 hook 脚本文件
|
|
310
310
|
|
|
@@ -342,6 +342,7 @@ hooks_auto_accept: true
|
|
|
342
342
|
- `updatedFiles`
|
|
343
343
|
- `removedFiles`
|
|
344
344
|
- `skipped`: 被跳过的工具及原因(仅在 project scope 下出现)
|
|
345
|
+
- `analyze`: Claude Langfuse 轨迹 hook 的安装状态(`installed`、`events`、`command`;未安装时包含 `reason`)
|
|
345
346
|
|
|
346
347
|
### Hooks Auto Sync(自动后台同步)
|
|
347
348
|
|
|
@@ -364,6 +365,7 @@ RONDS_AI_DISABLE_HOOKS_AUTO_SYNC=1
|
|
|
364
365
|
注意:
|
|
365
366
|
|
|
366
367
|
- auto-sync 只维护**用户级** hooks,不影响项目级配置
|
|
368
|
+
- auto-sync 会同步维护用户级 Claude 的 `analyze claude` Langfuse 轨迹 hook
|
|
367
369
|
- 后台同步失败时,错误记录到 `~/.ronds_ai/hooks_auto_sync_error.log`,不污染当前命令输出
|
|
368
370
|
- 首次安装 hooks 仍建议执行 `ronds_ai hooks deploy --scope user`
|
|
369
371
|
|
package/lib/analyze_claude.js
CHANGED
|
@@ -102,11 +102,20 @@ function extractHookContext(payload) {
|
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
const hookEventName = payload.hook_event_name || payload.hookEventName || '';
|
|
105
|
-
|
|
105
|
+
const context = {
|
|
106
106
|
sessionId,
|
|
107
107
|
transcriptPath: resolvedPath,
|
|
108
108
|
flushDeferredAgentTurns: hookEventName === 'SessionEnd',
|
|
109
109
|
};
|
|
110
|
+
const projectDir = [
|
|
111
|
+
payload.cwd,
|
|
112
|
+
payload.workspace && payload.workspace.current_dir,
|
|
113
|
+
payload.workspace && payload.workspace.project_dir,
|
|
114
|
+
].find((value) => typeof value === 'string' && value.trim());
|
|
115
|
+
if (projectDir) {
|
|
116
|
+
context.projectDir = path.resolve(projectDir);
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
110
119
|
}
|
|
111
120
|
|
|
112
121
|
/**
|
|
@@ -426,10 +435,7 @@ async function processHookContext(context, config, sdkModules) {
|
|
|
426
435
|
async function runClaudeAnalyze() {
|
|
427
436
|
assertAnalyzeNodeVersion();
|
|
428
437
|
const sdkModules = loadLangfuseSdk();
|
|
429
|
-
const config = getAnalyzeConfig();
|
|
430
|
-
writeAnalyzeDebug('analyze hook started', config);
|
|
431
438
|
const raw = await readStdin();
|
|
432
|
-
writeAnalyzeDebug(`stdin received ${raw.length} chars`, config);
|
|
433
439
|
if (!raw.trim()) {
|
|
434
440
|
return;
|
|
435
441
|
}
|
|
@@ -438,18 +444,23 @@ async function runClaudeAnalyze() {
|
|
|
438
444
|
try {
|
|
439
445
|
payload = JSON.parse(raw);
|
|
440
446
|
} catch (error) {
|
|
447
|
+
const config = getAnalyzeConfig();
|
|
441
448
|
writeAnalyzeLog('INFO', `invalid hook payload: ${error.message}`, config);
|
|
442
449
|
return;
|
|
443
450
|
}
|
|
444
|
-
writeAnalyzeDebug(
|
|
445
|
-
`payload top-level keys: ${Object.keys(payload).sort().join(', ')}`,
|
|
446
|
-
config,
|
|
447
|
-
);
|
|
448
451
|
const context = extractHookContext(payload);
|
|
449
452
|
if (!context) {
|
|
453
|
+
const config = getAnalyzeConfig();
|
|
450
454
|
writeAnalyzeLog('INFO', 'hook payload has no usable session or transcript', config);
|
|
451
455
|
return;
|
|
452
456
|
}
|
|
457
|
+
const config = getAnalyzeConfig(context.projectDir);
|
|
458
|
+
writeAnalyzeDebug('analyze hook started', config);
|
|
459
|
+
writeAnalyzeDebug(`stdin received ${raw.length} chars`, config);
|
|
460
|
+
writeAnalyzeDebug(
|
|
461
|
+
`payload top-level keys: ${Object.keys(payload).sort().join(', ')}`,
|
|
462
|
+
config,
|
|
463
|
+
);
|
|
453
464
|
|
|
454
465
|
try {
|
|
455
466
|
await processHookContext(context, config, sdkModules);
|
package/lib/analyze_config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
|
+
const { runGit } = require('./git');
|
|
2
3
|
|
|
3
4
|
// Langfuse 连接信息按产品要求从当前 Claude 配置写入源码。
|
|
4
5
|
const LANGFUSE_PUBLIC_KEY = 'pk-lf-default-001';
|
|
@@ -46,15 +47,32 @@ function resolveSystemUsername() {
|
|
|
46
47
|
return process.env.USERNAME || process.env.USER || 'unknown';
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 读取当前项目的 Git 邮箱,与 record 上报的 git.user.email 保持一致。
|
|
52
|
+
*/
|
|
53
|
+
function resolveGitUserEmail(projectDir) {
|
|
54
|
+
const workingDirectory = typeof projectDir === 'string' && projectDir.trim()
|
|
55
|
+
? projectDir.trim()
|
|
56
|
+
: process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
57
|
+
return runGit(workingDirectory, ['config', 'user.email'], false).toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 解析 Langfuse 用户标识,优先使用 Git 邮箱并保留系统用户名兜底。
|
|
62
|
+
*/
|
|
63
|
+
function resolveAnalyzeUserId(projectDir) {
|
|
64
|
+
return resolveGitUserEmail(projectDir) || resolveSystemUsername();
|
|
65
|
+
}
|
|
66
|
+
|
|
49
67
|
/**
|
|
50
68
|
* 返回 analyze 命令使用的固定 Langfuse 配置。
|
|
51
69
|
*/
|
|
52
|
-
function getAnalyzeConfig() {
|
|
70
|
+
function getAnalyzeConfig(projectDir) {
|
|
53
71
|
return {
|
|
54
72
|
publicKey: LANGFUSE_PUBLIC_KEY,
|
|
55
73
|
secretKey: LANGFUSE_SECRET_KEY,
|
|
56
74
|
baseUrl: LANGFUSE_BASE_URL,
|
|
57
|
-
userId:
|
|
75
|
+
userId: resolveAnalyzeUserId(projectDir),
|
|
58
76
|
debug: readBooleanOption('CC_LANGFUSE_DEBUG', false),
|
|
59
77
|
maxChars: readPositiveIntegerOption('CC_LANGFUSE_MAX_CHARS', 20000),
|
|
60
78
|
skillTags: readBooleanOption('CC_LANGFUSE_SKILL_TAGS', true),
|
|
@@ -64,5 +82,7 @@ function getAnalyzeConfig() {
|
|
|
64
82
|
|
|
65
83
|
module.exports = {
|
|
66
84
|
getAnalyzeConfig,
|
|
85
|
+
resolveAnalyzeUserId,
|
|
86
|
+
resolveGitUserEmail,
|
|
67
87
|
resolveSystemUsername,
|
|
68
88
|
};
|
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
|
};
|