ronds_ai 0.1.27 → 0.1.29

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/bin/ronds_ai.js CHANGED
@@ -82,8 +82,8 @@ function printUsage() {
82
82
  ' npx ronds_ai@latest analyze claude',
83
83
  ' npx ronds_ai@latest record cursor',
84
84
  ' npx ronds_ai@latest check record',
85
- ' npx ronds_ai@latest doctor claude',
86
- ' npx ronds_ai@latest doctor analyze',
85
+ ' npx ronds_ai@latest doctor claude',
86
+ ' npx ronds_ai@latest doctor analyze',
87
87
  ' npx ronds_ai@latest doctor cursor',
88
88
  ' npx ronds_ai@latest hooks deploy',
89
89
  ' npx ronds_ai@latest hooks deploy --scope user',
@@ -301,21 +301,21 @@ async function run() {
301
301
  throw new Error(`Unsupported analyze tool: ${source || ''}`);
302
302
  }
303
303
 
304
- // analyze 仅在 SessionEnd 生成会话级摘要,使用 Node 16 原生 API。
304
+ // analyze 主要挂在 Stop,每轮生成增量会话快照;使用 Node 16 原生 API。
305
305
  const { runClaudeAnalyze } = require('../lib/analyze_claude');
306
306
  await runClaudeAnalyze();
307
307
  return;
308
308
  }
309
309
 
310
- if (command === 'doctor') {
310
+ if (command === 'doctor') {
311
311
  const [tool] = args;
312
312
  const normalizedTool = String(tool || '').trim().toLowerCase();
313
313
 
314
- if (!SUPPORTED_SOURCES.has(normalizedTool) && normalizedTool !== 'analyze') {
314
+ if (!SUPPORTED_SOURCES.has(normalizedTool) && normalizedTool !== 'analyze') {
315
315
  throw new Error(`Unsupported doctor tool: ${tool || ''}`);
316
316
  }
317
317
 
318
- const result = await runDoctor(normalizedTool, process.cwd());
318
+ const result = await runDoctor(normalizedTool, process.cwd());
319
319
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
320
320
  return;
321
321
  }
@@ -344,8 +344,8 @@ async function run() {
344
344
  }
345
345
 
346
346
  run().catch((error) => {
347
- if (error && error.code === 'ANALYZE_PREFLIGHT') {
348
- // 旁路 analyze 错误时静默记录,避免干扰宿主 Hook。
347
+ if (error && error.code === 'ANALYZE_PREFLIGHT') {
348
+ // 旁路 analyze 错误时静默记录,避免干扰宿主 Hook。
349
349
  try {
350
350
  const fs = require('fs');
351
351
  const os = require('os');
@@ -353,7 +353,7 @@ run().catch((error) => {
353
353
  const analyzeDir = path.join(os.homedir(), '.ronds_ai', 'analyze');
354
354
  fs.mkdirSync(analyzeDir, { recursive: true });
355
355
  fs.appendFileSync(
356
- path.join(analyzeDir, 'session_hook.log'),
356
+ path.join(analyzeDir, 'session_hook.log'),
357
357
  `${new Date().toISOString()} [INFO] analyze preflight skipped: ${error.message}\n`,
358
358
  'utf8',
359
359
  );
@@ -14,11 +14,14 @@ function readStdin() {
14
14
  });
15
15
  }
16
16
 
17
+ /** Stop 为主要采集入口(每轮增量上报);SessionEnd 仅为存量部署的兼容兜底。 */
18
+ const SUPPORTED_HOOK_EVENTS = new Set(['Stop', 'SessionEnd']);
19
+
17
20
  /** 校验 Hook 事件并提取会话、transcript 与项目目录。 */
18
21
  function extractHookContextDetailed(payload) {
19
22
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return { context: null, reasonCode: 'PAYLOAD_NOT_OBJECT' };
20
23
  const event = payload.hook_event_name || payload.hookEventName || '';
21
- if (event !== 'SessionEnd') return { context: null, reasonCode: 'UNSUPPORTED_EVENT' };
24
+ if (!SUPPORTED_HOOK_EVENTS.has(event)) return { context: null, reasonCode: 'UNSUPPORTED_EVENT' };
22
25
  const sessionId = payload.sessionId || payload.session_id || payload.session?.id;
23
26
  const transcriptPath = payload.transcriptPath || payload.transcript_path || payload.transcript?.path;
24
27
  if (typeof sessionId !== 'string' || !sessionId.trim()) return { context: null, reasonCode: 'PAYLOAD_MISSING_SESSION_ID' };
@@ -29,10 +32,10 @@ function extractHookContextDetailed(payload) {
29
32
  return { context: { sessionId: sessionId.trim(), transcriptPath: resolvedPath, projectDir: projectDir ? path.resolve(projectDir) : undefined }, reasonCode: '' };
30
33
  }
31
34
 
32
- /** 兼容调用方,返回通过校验的 SessionEnd 上下文。 */
35
+ /** 兼容调用方,返回通过校验的会话上下文。 */
33
36
  function extractHookContext(payload) { return extractHookContextDetailed(payload).context; }
34
37
 
35
- /** 运行 SessionEnd 摘要入口;Stop、无效 payload 和其他事件均静默忽略。 */
38
+ /** 运行会话摘要入口;无效 payload 和不支持的事件均静默忽略。 */
36
39
  async function runClaudeAnalyze() {
37
40
  const raw = await readStdin();
38
41
  if (!raw.trim()) return null;
@@ -12,6 +12,37 @@ const PREVIEW_LIMIT = 150;
12
12
  const REQUEST_TIMEOUT_MS = 250;
13
13
  const STALE_QUEUE_LOCK_MS = 30_000;
14
14
 
15
+ // 用户直敲 slash 命令时,Claude Code 内置命令不计入 Skill 统计。
16
+ // 名单来自官方 commands 文档(code.claude.com/docs/en/commands),补充少量未文档化内置命令。
17
+ const BUILTIN_COMMANDS = new Set([
18
+ // 官方文档列出的内置命令
19
+ 'agents', 'artifacts', 'auto-mode-setup', 'chrome', 'cost', 'design-login', 'desktop', 'diff',
20
+ 'doctor', 'exit', 'fewer-permission-prompts', 'focus', 'heapdump', 'help', 'hooks', 'ide',
21
+ 'init', 'insights', 'install-github-app', 'install-slack-app', 'keybindings', 'list-agents',
22
+ 'login', 'logout', 'memory', 'mobile', 'passes', 'permissions', 'powerup', 'privacy-settings',
23
+ 'radio', 'rate-limit-options', 'recap', 'release-notes', 'reload-skills', 'remote-control',
24
+ 'remote-env', 'rewind', 'run-skill-generator', 'run', 'sandbox', 'scroll-speed', 'security-review',
25
+ 'setup-bedrock', 'setup-vertex', 'skill-doctor', 'skills', 'stats', 'status', 'statusline',
26
+ 'stickers', 'stop', 'tasks', 'team-onboarding', 'teleport', 'terminal-setup', 'theme', 'upgrade',
27
+ 'usage-credits', 'usage', 'verify', 'vim', 'web-setup', 'workflow-authoring', 'workflows',
28
+ // 未在文档列出但实际存在的内置命令
29
+ 'clear', 'model', 'compact', 'resume', 'continue', 'config', 'mcp', 'bug', 'context', 'todos',
30
+ 'export', 'quit', 'add-dir', 'output-style',
31
+ ]);
32
+
33
+ // 用户直敲的 slash 命令标记;字符集覆盖插件技能的 plugin:skill 命名形式。
34
+ const COMMAND_NAME_PATTERN = /<command-name>\/([a-zA-Z0-9_:-]+)\s*<\/command-name>/g;
35
+
36
+ /** 从用户直敲的 slash 命令消息中提取技能名,排除内置命令。 */
37
+ function extractSkillCommandNames(text) {
38
+ const names = new Set();
39
+ for (const match of String(text || '').matchAll(COMMAND_NAME_PATTERN)) {
40
+ const name = match[1].trim();
41
+ if (name && !BUILTIN_COMMANDS.has(name)) names.add(name);
42
+ }
43
+ return names;
44
+ }
45
+
15
46
  /** 流式扫描 Claude JSONL,只保留会话摘要字段,避免构造完整对话树。 */
16
47
  async function summarizeTranscript(filePath) {
17
48
  const summary = { startedAt: null, endedAt: null, turnCount: 0, firstPromptPreview: '', skills: {} };
@@ -33,6 +64,12 @@ async function summarizeTranscript(filePath) {
33
64
  const text = typeof content === 'string' ? content : blocks.filter((b) => b?.type === 'text').map((b) => b.text || '').join('\n');
34
65
  if (text.trim()) summary.firstPromptPreview = text.trim().slice(0, PREVIEW_LIMIT);
35
66
  }
67
+ if (role === 'user' && !row.isMeta && typeof content === 'string') {
68
+ // 用户直敲 /skill-name 的调用路径不产生 Skill 工具调用,从命令标记补计。
69
+ for (const name of extractSkillCommandNames(content)) {
70
+ summary.skills[name] = (summary.skills[name] || 0) + 1;
71
+ }
72
+ }
36
73
  if (role === 'user' && !row.isMeta && (typeof content === 'string' || blocks.some((b) => b?.type === 'text'))) summary.turnCount += 1;
37
74
  for (const block of blocks) {
38
75
  if (block?.type === 'tool_use' && block.name === 'Skill' && typeof block.input?.skill === 'string' && block.input.skill.trim()) {
@@ -100,7 +137,7 @@ async function flushSnapshotQueue(queueDir, url, limit = 1) {
100
137
  } finally { fs.rmSync(lock, { recursive: true, force: true }); }
101
138
  }
102
139
 
103
- /** 在 SessionEnd 生成并上报会话快照;Stop 路径不调用此方法。 */
140
+ /** 在 Stop 每轮生成并上报增量会话快照;后端按 (source, session_id) 与 snapshot_version 幂等合并。 */
104
141
  async function collectAndReportSession({ transcriptPath, sessionId, config, projectDir }) {
105
142
  const summary = await summarizeTranscript(transcriptPath);
106
143
  const git = {
@@ -127,4 +164,4 @@ async function collectAndReportSession({ transcriptPath, sessionId, config, proj
127
164
  return payload;
128
165
  }
129
166
 
130
- module.exports = { MAX_QUEUE_ITEMS, collectAndReportSession, enqueueSnapshot, flushSnapshotQueue, postSnapshot, summarizeTranscript };
167
+ module.exports = { MAX_QUEUE_ITEMS, collectAndReportSession, enqueueSnapshot, extractSkillCommandNames, flushSnapshotQueue, postSnapshot, summarizeTranscript };
package/lib/doctor.js CHANGED
@@ -1,14 +1,14 @@
1
- const fs = require('fs');
2
- const net = require('net');
1
+ const fs = require('fs');
2
+ const net = require('net');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
  const { runGit } = require('./git');
6
- const yaml = require('js-yaml');
7
- const { getAnalyzeConfig } = require('./analyze_config');
8
- const { inspectLocalDiagnostics } = require('./analyze_diagnostics');
9
-
10
- const SUPPORTED_TOOLS = new Set(['analyze', 'claude', 'codex', 'cursor', 'hermes']);
11
- const ANALYZE_COMMAND = 'npx ronds_ai@latest analyze claude';
6
+ const yaml = require('js-yaml');
7
+ const { getAnalyzeConfig } = require('./analyze_config');
8
+ const { inspectLocalDiagnostics } = require('./analyze_diagnostics');
9
+
10
+ const SUPPORTED_TOOLS = new Set(['analyze', 'claude', 'codex', 'cursor', 'hermes']);
11
+ const ANALYZE_COMMAND = 'npx ronds_ai@latest analyze claude';
12
12
 
13
13
  function getFailedEventDir() {
14
14
  return path.join(os.homedir(), '.ronds_ai', 'failed-events');
@@ -28,15 +28,15 @@ function readRecentLogs(tool) {
28
28
  .map((name) => path.join(dir, name));
29
29
  }
30
30
 
31
- function getConfigChecks(tool, baseDir) {
32
- if (tool === 'analyze') {
33
- return [
34
- {
35
- label: '~/.claude/settings.json',
36
- path: path.join(os.homedir(), '.claude', 'settings.json'),
37
- },
38
- ];
39
- }
31
+ function getConfigChecks(tool, baseDir) {
32
+ if (tool === 'analyze') {
33
+ return [
34
+ {
35
+ label: '~/.claude/settings.json',
36
+ path: path.join(os.homedir(), '.claude', 'settings.json'),
37
+ },
38
+ ];
39
+ }
40
40
  if (tool === 'hermes') {
41
41
  const hermesConfig = path.join(os.homedir(), '.hermes', 'config.yaml');
42
42
  let hookEntryFound = false;
@@ -106,54 +106,54 @@ function readGitRemoteUrl(baseDir) {
106
106
  return runGit(baseDir, ['remote', 'get-url', 'origin'], false);
107
107
  }
108
108
 
109
- function hasAnalyzeHook(settingsPath, eventName) {
110
- try {
111
- const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
112
- const entries = settings && settings.hooks && settings.hooks[eventName];
113
- return Array.isArray(entries) && entries.some((entry) => (
114
- entry && Array.isArray(entry.hooks) && entry.hooks.some((hook) => (
115
- hook && hook.type === 'command' && hook.command === ANALYZE_COMMAND
116
- ))
117
- ));
118
- } catch {
119
- return false;
120
- }
121
- }
122
-
123
- /**
124
- * 保留旧导出兼容调用方;会话采集不再做远端服务探测。
125
- */
126
- function probeTcpAddress(rawUrl, timeoutMs = 2000) {
127
- return new Promise((resolve) => {
128
- let target;
129
- try {
130
- target = new URL(rawUrl);
131
- } catch {
132
- resolve({ reachable: false, reason: 'invalid_url' });
133
- return;
134
- }
135
- const port = Number(target.port || (target.protocol === 'https:' ? 443 : 80));
136
- const startedAt = Date.now();
137
- const socket = net.createConnection({ host: target.hostname, port });
138
- const finish = (result) => {
139
- socket.destroy();
140
- resolve({ ...result, durationMs: Date.now() - startedAt });
141
- };
142
- socket.setTimeout(timeoutMs);
143
- socket.once('connect', () => finish({ reachable: true, host: target.host }));
144
- socket.once('timeout', () => finish({ reachable: false, reason: 'timeout', host: target.host }));
145
- socket.once('error', (error) => finish({
146
- reachable: false,
147
- reason: error.code || error.message,
148
- host: target.host,
149
- }));
150
- });
151
- }
152
-
153
- /**
154
- * 执行指定工具的环境诊断。
155
- */
156
- async function runDoctor(tool, targetDir = process.cwd()) {
109
+ function hasAnalyzeHook(settingsPath, eventName) {
110
+ try {
111
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
112
+ const entries = settings && settings.hooks && settings.hooks[eventName];
113
+ return Array.isArray(entries) && entries.some((entry) => (
114
+ entry && Array.isArray(entry.hooks) && entry.hooks.some((hook) => (
115
+ hook && hook.type === 'command' && hook.command === ANALYZE_COMMAND
116
+ ))
117
+ ));
118
+ } catch {
119
+ return false;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * 保留旧导出兼容调用方;会话采集不再做远端服务探测。
125
+ */
126
+ function probeTcpAddress(rawUrl, timeoutMs = 2000) {
127
+ return new Promise((resolve) => {
128
+ let target;
129
+ try {
130
+ target = new URL(rawUrl);
131
+ } catch {
132
+ resolve({ reachable: false, reason: 'invalid_url' });
133
+ return;
134
+ }
135
+ const port = Number(target.port || (target.protocol === 'https:' ? 443 : 80));
136
+ const startedAt = Date.now();
137
+ const socket = net.createConnection({ host: target.hostname, port });
138
+ const finish = (result) => {
139
+ socket.destroy();
140
+ resolve({ ...result, durationMs: Date.now() - startedAt });
141
+ };
142
+ socket.setTimeout(timeoutMs);
143
+ socket.once('connect', () => finish({ reachable: true, host: target.host }));
144
+ socket.once('timeout', () => finish({ reachable: false, reason: 'timeout', host: target.host }));
145
+ socket.once('error', (error) => finish({
146
+ reachable: false,
147
+ reason: error.code || error.message,
148
+ host: target.host,
149
+ }));
150
+ });
151
+ }
152
+
153
+ /**
154
+ * 执行指定工具的环境诊断。
155
+ */
156
+ async function runDoctor(tool, targetDir = process.cwd()) {
157
157
  const normalizedTool = String(tool || '').trim().toLowerCase();
158
158
  if (!SUPPORTED_TOOLS.has(normalizedTool)) {
159
159
  throw new Error(`Unsupported doctor tool: ${tool || ''}`);
@@ -165,30 +165,32 @@ async function runDoctor(tool, targetDir = process.cwd()) {
165
165
  exists: fs.existsSync(item.path),
166
166
  }));
167
167
 
168
- const result = {
168
+ const result = {
169
169
  tool: normalizedTool,
170
170
  targetDir: baseDir,
171
171
  gitUserEmail: readGitUserEmail(baseDir),
172
172
  gitRemoteUrl: readGitRemoteUrl(baseDir),
173
173
  configChecks,
174
- recentLogs: readRecentLogs(normalizedTool),
175
- };
176
- if (normalizedTool === 'analyze') {
177
- const analyzeConfig = getAnalyzeConfig(baseDir);
178
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
179
- result.analyze = {
180
- nodeSupported: Number(String(process.versions.node || '').split('.')[0]) >= 16,
181
- stopHookInstalled: hasAnalyzeHook(settingsPath, 'Stop'),
182
- sessionEndHookInstalled: hasAnalyzeHook(settingsPath, 'SessionEnd'),
183
- workerId: analyzeConfig.workerId,
184
- userId: analyzeConfig.userId,
185
- localDiagnostics: inspectLocalDiagnostics(),
186
- };
187
- }
188
- return result;
189
- }
174
+ recentLogs: readRecentLogs(normalizedTool),
175
+ };
176
+ if (normalizedTool === 'analyze') {
177
+ const analyzeConfig = getAnalyzeConfig(baseDir);
178
+ const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
179
+ result.analyze = {
180
+ nodeSupported: Number(String(process.versions.node || '').split('.')[0]) >= 16,
181
+ stopHookInstalled: hasAnalyzeHook(settingsPath, 'Stop'),
182
+ sessionEndHookInstalled: hasAnalyzeHook(settingsPath, 'SessionEnd'),
183
+ workerId: analyzeConfig.workerId,
184
+ userId: analyzeConfig.userId,
185
+ localDiagnostics: inspectLocalDiagnostics(),
186
+ };
187
+ // Stop 是当前主要采集事件;SessionEnd 仅反映历史遗留安装。
188
+ result.analyze.ready = result.analyze.stopHookInstalled;
189
+ }
190
+ return result;
191
+ }
190
192
 
191
193
  module.exports = {
192
- runDoctor,
193
- probeTcpAddress,
194
- };
194
+ runDoctor,
195
+ probeTcpAddress,
196
+ };
@@ -20,7 +20,8 @@ const { spawn } = require('child_process');
20
20
 
21
21
  /** 当前 hooks schema 版本。当用户级 hooks deploy 输出发生变化时 +1。 */
22
22
  // v4: SessionEnd 会话摘要 hook,不再需要额外运行时依赖。
23
- const HOOKS_SCHEMA_VERSION = 4;
23
+ // v5: analyze 从 SessionEnd 迁移到 Stop(SessionEnd 有 1.5s 硬超时且进程被强杀时不触发)。
24
+ const HOOKS_SCHEMA_VERSION = 5;
24
25
 
25
26
  /** sentinel 有效时长:24 小时 */
26
27
  const HOOKS_AUTO_SYNC_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
@@ -8,7 +8,11 @@ const yaml = require('js-yaml');
8
8
  const CURSOR_COMMAND = 'npx ronds_ai@latest record cursor';
9
9
  const CLAUDE_COMMAND = 'npx ronds_ai@latest record claude';
10
10
  const CLAUDE_ANALYZE_COMMAND = 'npx ronds_ai@latest analyze claude';
11
- const CLAUDE_ANALYZE_EVENTS = ['SessionEnd'];
11
+ // Stop 每轮触发增量快照,异常退出最多丢最后一轮;SessionEnd 有 1.5s 硬超时且强杀进程不触发。
12
+ const CLAUDE_ANALYZE_EVENTS = ['Stop'];
13
+ // SessionEnd 是历史版本(schema v4 及以前)的部署事件,ensure/remove 时都要清理残留。
14
+ const CLAUDE_ANALYZE_LEGACY_EVENTS = ['SessionEnd'];
15
+ const CLAUDE_ANALYZE_CLEANUP_EVENTS = [...CLAUDE_ANALYZE_EVENTS, ...CLAUDE_ANALYZE_LEGACY_EVENTS];
12
16
  const CODEX_COMMAND = 'npx ronds_ai@latest record codex';
13
17
  const CURSOR_OLD_COMMAND = 'node .cursor/hooks/cursor_hook_request.cjs';
14
18
  const CURSOR_OLD_JS_COMMAND = 'node .cursor/hooks/cursor_hook_request.js';
@@ -267,10 +271,43 @@ function splitClaudeAnalyzeEntries(entries) {
267
271
  }
268
272
 
269
273
  /**
270
- * 在 Claude settings 中确保 SessionEnd 的 analyze hook 条目。
274
+ * 从单个 Claude hook 事件数组中过滤掉本工具管理的 analyze 命令。
275
+ * 仅删除命令完全相等的 command hook;entry 中其余命令保留,空 entry 整体移除。
276
+ *
277
+ * @param {Array} entries - hooks[event] 数组
278
+ * @returns {Array} 清理后的 entries
279
+ */
280
+ function cleanClaudeAnalyzeEventEntries(entries) {
281
+ if (!Array.isArray(entries)) return [];
282
+ const cleanedEntries = [];
283
+
284
+ for (const entry of entries) {
285
+ if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
286
+ cleanedEntries.push(entry);
287
+ continue;
288
+ }
289
+
290
+ const filteredHooks = entry.hooks.filter(
291
+ (hook) => !(isPlainObject(hook)
292
+ && hook.type === 'command'
293
+ && hook.command === CLAUDE_ANALYZE_COMMAND),
294
+ );
295
+
296
+ if (filteredHooks.length === entry.hooks.length) {
297
+ cleanedEntries.push(entry);
298
+ } else if (filteredHooks.length > 0) {
299
+ cleanedEntries.push({ ...entry, hooks: filteredHooks });
300
+ }
301
+ }
302
+
303
+ return cleanedEntries;
304
+ }
305
+
306
+ /**
307
+ * 在 Claude settings 中确保 Stop 的 analyze hook 条目,并清理历史遗留的 SessionEnd 条目。
271
308
  * 只管理命令完全等于 CLAUDE_ANALYZE_COMMAND 的 hook,保留用户自定义条目。
272
309
  */
273
- function ensureClaudeAnalyzeHooks(config) {
310
+ function ensureClaudeAnalyzeHooks(config) {
274
311
  const next = isPlainObject(config) ? { ...config } : {};
275
312
  const hooks = isPlainObject(next.hooks) ? { ...next.hooks } : {};
276
313
 
@@ -286,60 +323,50 @@ function ensureClaudeAnalyzeHooks(config) {
286
323
  hooks[eventName] = keepEntries.concat([{ hooks: desiredHooks }]);
287
324
  }
288
325
 
326
+ for (const eventName of CLAUDE_ANALYZE_LEGACY_EVENTS) {
327
+ if (CLAUDE_ANALYZE_EVENTS.includes(eventName)) continue;
328
+ const cleanedEntries = cleanClaudeAnalyzeEventEntries(hooks[eventName]);
329
+ if (cleanedEntries.length > 0) {
330
+ hooks[eventName] = cleanedEntries;
331
+ } else {
332
+ delete hooks[eventName];
333
+ }
334
+ }
335
+
289
336
  next.hooks = hooks;
290
- return next;
291
- }
292
-
293
- /**
294
- * 从 Claude settings 的 SessionEnd 中移除本工具管理的 analyze hook。
295
- * 仅删除命令完全相等的 command hook,并保留用户自定义 entry 与命令。
296
- * @param {object} config - Claude settings 配置
297
- * @returns {object}
298
- */
299
- function removeClaudeAnalyzeHooks(config) {
300
- const next = isPlainObject(config) ? { ...config } : {};
301
- if (!isPlainObject(next.hooks)) {
302
- return next;
303
- }
304
-
305
- const hooks = { ...next.hooks };
306
- for (const eventName of CLAUDE_ANALYZE_EVENTS) {
307
- const entries = Array.isArray(hooks[eventName]) ? hooks[eventName] : [];
308
- const cleanedEntries = [];
309
-
310
- for (const entry of entries) {
311
- if (!isPlainObject(entry) || !Array.isArray(entry.hooks)) {
312
- cleanedEntries.push(entry);
313
- continue;
314
- }
315
-
316
- const filteredHooks = entry.hooks.filter(
317
- (hook) => !(isPlainObject(hook)
318
- && hook.type === 'command'
319
- && hook.command === CLAUDE_ANALYZE_COMMAND),
320
- );
321
-
322
- if (filteredHooks.length === entry.hooks.length) {
323
- cleanedEntries.push(entry);
324
- } else if (filteredHooks.length > 0) {
325
- cleanedEntries.push({ ...entry, hooks: filteredHooks });
326
- }
327
- }
328
-
329
- if (cleanedEntries.length > 0) {
330
- hooks[eventName] = cleanedEntries;
331
- } else {
332
- delete hooks[eventName];
333
- }
334
- }
335
-
336
- if (Object.keys(hooks).length > 0) {
337
- next.hooks = hooks;
338
- } else {
339
- delete next.hooks;
340
- }
341
- return next;
342
- }
337
+ return next;
338
+ }
339
+
340
+ /**
341
+ * 从 Claude settings 中移除本工具管理的 analyze hook(含历史遗留的 SessionEnd 事件)。
342
+ * 仅删除命令完全相等的 command hook,并保留用户自定义 entry 与命令。
343
+ * @param {object} config - Claude settings 配置
344
+ * @returns {object}
345
+ */
346
+ function removeClaudeAnalyzeHooks(config) {
347
+ const next = isPlainObject(config) ? { ...config } : {};
348
+ if (!isPlainObject(next.hooks)) {
349
+ return next;
350
+ }
351
+
352
+ const hooks = { ...next.hooks };
353
+ for (const eventName of CLAUDE_ANALYZE_CLEANUP_EVENTS) {
354
+ const cleanedEntries = cleanClaudeAnalyzeEventEntries(hooks[eventName]);
355
+
356
+ if (cleanedEntries.length > 0) {
357
+ hooks[eventName] = cleanedEntries;
358
+ } else {
359
+ delete hooks[eventName];
360
+ }
361
+ }
362
+
363
+ if (Object.keys(hooks).length > 0) {
364
+ next.hooks = hooks;
365
+ } else {
366
+ delete next.hooks;
367
+ }
368
+ return next;
369
+ }
343
370
 
344
371
  function ensureCodexHooksFeatureFlag(toml) {
345
372
  const newline = toml.includes('\r\n') ? '\r\n' : '\n';
@@ -640,20 +667,20 @@ function deployCursorHook(cursorPath, result) {
640
667
  * 判断本次部署是否写入 Claude analyze hook。
641
668
  * 不满足条件时把原因写入 result.analyze.reason 并返回 false。
642
669
  */
643
- function shouldDeployClaudeAnalyze(result, options = {}) {
644
- // 会话摘要使用 Node 16 原生 API,仅在 SessionEnd 事件运行。
645
- void options;
646
- return true;
647
- }
670
+ function shouldDeployClaudeAnalyze(result, options = {}) {
671
+ // 会话摘要使用 Node 16 原生 API,主要挂在 Stop 事件增量运行。
672
+ void options;
673
+ return true;
674
+ }
648
675
 
649
676
  function deployClaudeSettings(claudeSettingsPath, result, analyzeDeployed) {
650
677
  const claudeSettingsResult = readJsonFile(claudeSettingsPath, {});
651
678
  let nextClaudeSettings = ensureClaudeSettings(claudeSettingsResult.data);
652
679
 
653
- if (analyzeDeployed) {
654
- nextClaudeSettings = ensureClaudeAnalyzeHooks(nextClaudeSettings);
655
- } else {
656
- nextClaudeSettings = removeClaudeAnalyzeHooks(nextClaudeSettings);
680
+ if (analyzeDeployed) {
681
+ nextClaudeSettings = ensureClaudeAnalyzeHooks(nextClaudeSettings);
682
+ } else {
683
+ nextClaudeSettings = removeClaudeAnalyzeHooks(nextClaudeSettings);
657
684
  }
658
685
 
659
686
  const changed = writeJsonFile(claudeSettingsPath, nextClaudeSettings);
@@ -733,7 +760,7 @@ function deployHooks(targetDir = process.cwd(), options = {}) {
733
760
  }
734
761
 
735
762
  if (!options.tool || options.tool === 'claude') {
736
- const analyzeDeployed = shouldDeployClaudeAnalyze(result, options.analyzeRuntime);
763
+ const analyzeDeployed = shouldDeployClaudeAnalyze(result, options.analyzeRuntime);
737
764
  deployClaudeSettings(paths.claudeSettingsPath, result, analyzeDeployed);
738
765
  if (analyzeDeployed) {
739
766
  result.analyze.installed = true;
@@ -837,8 +864,8 @@ function deployHermesHook() {
837
864
  module.exports = {
838
865
  deployHooks,
839
866
  deployHermesHook,
840
- ensureClaudeAnalyzeHooks,
841
- getNodeMajorVersion,
842
- removeClaudeAnalyzeHooks,
867
+ ensureClaudeAnalyzeHooks,
868
+ getNodeMajorVersion,
869
+ removeClaudeAnalyzeHooks,
843
870
  shouldDeployClaudeAnalyze,
844
871
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds_ai",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "CLI for reporting AI code edit events.",
5
5
  "bin": {
6
6
  "ronds_ai": "bin/ronds_ai.js"