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 +9 -9
- package/lib/analyze_claude.js +6 -3
- package/lib/analyze_session.js +39 -2
- package/lib/doctor.js +87 -85
- package/lib/hooks_auto_sync.js +2 -1
- package/lib/hooks_deploy.js +96 -69
- package/package.json +1 -1
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
|
|
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
|
);
|
package/lib/analyze_claude.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
35
|
+
/** 兼容调用方,返回通过校验的会话上下文。 */
|
|
33
36
|
function extractHookContext(payload) { return extractHookContextDetailed(payload).context; }
|
|
34
37
|
|
|
35
|
-
/**
|
|
38
|
+
/** 运行会话摘要入口;无效 payload 和不支持的事件均静默忽略。 */
|
|
36
39
|
async function runClaudeAnalyze() {
|
|
37
40
|
const raw = await readStdin();
|
|
38
41
|
if (!raw.trim()) return null;
|
package/lib/analyze_session.js
CHANGED
|
@@ -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
|
-
/** 在
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/hooks_auto_sync.js
CHANGED
|
@@ -20,7 +20,8 @@ const { spawn } = require('child_process');
|
|
|
20
20
|
|
|
21
21
|
/** 当前 hooks schema 版本。当用户级 hooks deploy 输出发生变化时 +1。 */
|
|
22
22
|
// v4: SessionEnd 会话摘要 hook,不再需要额外运行时依赖。
|
|
23
|
-
|
|
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;
|
package/lib/hooks_deploy.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
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
|
};
|