@jhihjian/claude-daemon 1.1.0
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/QUICKSTART.md +214 -0
- package/README.md +173 -0
- package/bin/cli.js +118 -0
- package/hooks/SessionAnalyzer.hook.ts +567 -0
- package/hooks/SessionRecorder.hook.ts +202 -0
- package/hooks/SessionToolCapture-v2.hook.ts +231 -0
- package/hooks/SessionToolCapture.hook.ts +119 -0
- package/install.sh +257 -0
- package/lib/config.ts +223 -0
- package/lib/errors.ts +213 -0
- package/lib/logger.ts +140 -0
- package/package.json +45 -0
- package/tools/SessionQuery.ts +262 -0
- package/tools/SessionStats.ts +139 -0
- package/tools/show-conversation.sh +80 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionRecorder.hook.ts
|
|
4
|
+
* 在会话开始时记录上下文信息
|
|
5
|
+
*
|
|
6
|
+
* Hook 类型: SessionStart
|
|
7
|
+
* 触发时机: Claude Code 会话开始时
|
|
8
|
+
* 职责: 捕获启动目录、Git 信息、初始化会话文件
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { hostname } from 'os';
|
|
14
|
+
import { createHookLogger } from '../lib/logger.ts';
|
|
15
|
+
import {
|
|
16
|
+
hookErrorHandler,
|
|
17
|
+
withTimeout,
|
|
18
|
+
safeExecute,
|
|
19
|
+
validateRequired,
|
|
20
|
+
FileSystemError
|
|
21
|
+
} from '../lib/errors.ts';
|
|
22
|
+
import { config } from '../lib/config.ts';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// 初始化
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
const logger = createHookLogger('SessionRecorder');
|
|
29
|
+
const cfg = config.get();
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// 1. 读取 Hook 输入
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const input = await Bun.stdin.text();
|
|
36
|
+
const event = JSON.parse(input);
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// 2. 核心逻辑
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const startTime = Date.now();
|
|
44
|
+
|
|
45
|
+
// 2.1 验证必需字段
|
|
46
|
+
const sessionId = validateRequired(event.session_id, 'session_id');
|
|
47
|
+
const timestamp = new Date().toISOString();
|
|
48
|
+
|
|
49
|
+
// 2.2 获取启动目录
|
|
50
|
+
const workingDir = process.cwd();
|
|
51
|
+
logger.info('Session started', { sessionId, workingDir });
|
|
52
|
+
|
|
53
|
+
// 2.3 获取 Git 信息(带超时)
|
|
54
|
+
const gitInfo = await safeExecute(
|
|
55
|
+
() => getGitInfo(workingDir, cfg.gitTimeout),
|
|
56
|
+
{ repo: null, branch: null, commit: null, remote: null },
|
|
57
|
+
'getGitInfo'
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// 2.4 创建存储目录
|
|
61
|
+
const yearMonth = config.getYearMonth();
|
|
62
|
+
const rawDir = join(cfg.rawDir, yearMonth);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
mkdirSync(rawDir, { recursive: true });
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new FileSystemError(
|
|
68
|
+
`Failed to create directory: ${rawDir}`,
|
|
69
|
+
rawDir,
|
|
70
|
+
'mkdir'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2.5 构建会话元数据
|
|
75
|
+
const sessionMeta = {
|
|
76
|
+
event_type: 'session_start',
|
|
77
|
+
session_id: sessionId,
|
|
78
|
+
timestamp: timestamp,
|
|
79
|
+
|
|
80
|
+
// 工作上下文
|
|
81
|
+
working_directory: workingDir,
|
|
82
|
+
git_repo: gitInfo.repo,
|
|
83
|
+
git_branch: gitInfo.branch,
|
|
84
|
+
git_commit: gitInfo.commit,
|
|
85
|
+
git_remote: gitInfo.remote,
|
|
86
|
+
|
|
87
|
+
// 环境信息
|
|
88
|
+
platform: process.platform,
|
|
89
|
+
cwd_at_start: workingDir,
|
|
90
|
+
user: process.env.USER || process.env.USERNAME || 'unknown',
|
|
91
|
+
hostname: hostname(),
|
|
92
|
+
|
|
93
|
+
// 运行时信息
|
|
94
|
+
bun_version: Bun.version,
|
|
95
|
+
node_version: process.version,
|
|
96
|
+
shell: process.env.SHELL || 'unknown',
|
|
97
|
+
terminal: process.env.TERM || 'unknown',
|
|
98
|
+
|
|
99
|
+
// 元数据
|
|
100
|
+
claude_version: event.claude_version || 'unknown',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 2.6 写入会话文件(JSONL 格式)
|
|
104
|
+
const sessionFile = config.getSessionFilePath(sessionId, yearMonth);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
writeFileSync(sessionFile, JSON.stringify(sessionMeta) + '\n', { mode: 0o600 });
|
|
108
|
+
logger.debug('Session file created', { sessionFile });
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw new FileSystemError(
|
|
111
|
+
`Failed to write session file: ${sessionFile}`,
|
|
112
|
+
sessionFile,
|
|
113
|
+
'write'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 2.7 记录性能
|
|
118
|
+
logger.perf('SessionRecorder', startTime);
|
|
119
|
+
logger.info('Session recorded successfully', {
|
|
120
|
+
sessionId,
|
|
121
|
+
hostname: hostname(),
|
|
122
|
+
user: sessionMeta.user,
|
|
123
|
+
gitRepo: gitInfo.repo || 'none',
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// 错误处理:永远不阻塞 Claude Code
|
|
128
|
+
hookErrorHandler('SessionRecorder')(error);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// 3. 输出决策(必须)
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
console.log(JSON.stringify({ continue: true }));
|
|
136
|
+
process.exit(0);
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// 辅助函数
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
interface GitInfo {
|
|
143
|
+
repo: string | null;
|
|
144
|
+
branch: string | null;
|
|
145
|
+
commit: string | null;
|
|
146
|
+
remote: string | null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取所有 Git 信息(带超时)
|
|
151
|
+
*/
|
|
152
|
+
async function getGitInfo(dir: string, timeout: number): Promise<GitInfo> {
|
|
153
|
+
const commands = {
|
|
154
|
+
repo: ['git', 'rev-parse', '--show-toplevel'],
|
|
155
|
+
branch: ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
156
|
+
commit: ['git', 'rev-parse', '--short', 'HEAD'],
|
|
157
|
+
remote: ['git', 'remote', 'get-url', 'origin'],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const results = await Promise.all(
|
|
161
|
+
Object.entries(commands).map(async ([key, cmd]) => {
|
|
162
|
+
try {
|
|
163
|
+
const result = await withTimeout(
|
|
164
|
+
execGitCommand(cmd, dir),
|
|
165
|
+
timeout,
|
|
166
|
+
`git ${cmd[1]}`
|
|
167
|
+
);
|
|
168
|
+
return [key, result];
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.debug(`Git command failed: ${cmd.join(' ')}`, {
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
});
|
|
173
|
+
return [key, null];
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return Object.fromEntries(results) as GitInfo;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 执行 Git 命令
|
|
183
|
+
*/
|
|
184
|
+
async function execGitCommand(cmd: string[], dir: string): Promise<string | null> {
|
|
185
|
+
try {
|
|
186
|
+
const proc = Bun.spawn(cmd, {
|
|
187
|
+
cwd: dir,
|
|
188
|
+
stdout: 'pipe',
|
|
189
|
+
stderr: 'pipe',
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const output = await new Response(proc.stdout).text();
|
|
193
|
+
await proc.exited;
|
|
194
|
+
|
|
195
|
+
if (proc.exitCode === 0) {
|
|
196
|
+
return output.trim();
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionToolCapture-v2.hook.ts
|
|
4
|
+
* 从 transcript 读取完整的工具输出
|
|
5
|
+
*
|
|
6
|
+
* Hook 类型: PostToolUse
|
|
7
|
+
* 触发时机: 工具调用完成后
|
|
8
|
+
* 职责: 记录工具调用的输入、输出和状态
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, existsSync, readFileSync } from 'fs';
|
|
12
|
+
import { createHookLogger } from '../lib/logger.ts';
|
|
13
|
+
import {
|
|
14
|
+
hookErrorHandler,
|
|
15
|
+
withTimeout,
|
|
16
|
+
safeJSONParse,
|
|
17
|
+
validateRequired,
|
|
18
|
+
FileSystemError
|
|
19
|
+
} from '../lib/errors.ts';
|
|
20
|
+
import { config } from '../lib/config.ts';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// 初始化
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const logger = createHookLogger('SessionToolCapture');
|
|
27
|
+
const cfg = config.get();
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// 1. 读取 Hook 输入
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
const input = await Bun.stdin.text();
|
|
34
|
+
const event = JSON.parse(input);
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// 2. 核心逻辑
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const startTime = Date.now();
|
|
42
|
+
|
|
43
|
+
// 2.1 验证必需字段
|
|
44
|
+
const sessionId = validateRequired(event.session_id, 'session_id');
|
|
45
|
+
const toolName = validateRequired(event.tool_name, 'tool_name');
|
|
46
|
+
const timestamp = new Date().toISOString();
|
|
47
|
+
const yearMonth = config.getYearMonth();
|
|
48
|
+
|
|
49
|
+
// 2.2 定位会话文件
|
|
50
|
+
const sessionFile = config.getSessionFilePath(sessionId, yearMonth);
|
|
51
|
+
|
|
52
|
+
if (!existsSync(sessionFile)) {
|
|
53
|
+
logger.warn('Session file not found', { sessionFile, sessionId });
|
|
54
|
+
console.log(JSON.stringify({ continue: true }));
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2.3 提取工具输出和成功状态
|
|
59
|
+
let toolOutput = '';
|
|
60
|
+
let toolSuccess = false;
|
|
61
|
+
|
|
62
|
+
// 优先从 tool_response 字段获取(直接可用)
|
|
63
|
+
if (event.tool_response) {
|
|
64
|
+
const response = event.tool_response;
|
|
65
|
+
|
|
66
|
+
// 合并 stdout 和 stderr
|
|
67
|
+
const stdout = response.stdout || '';
|
|
68
|
+
const stderr = response.stderr || '';
|
|
69
|
+
toolOutput = stdout + (stderr ? '\n[stderr]\n' + stderr : '');
|
|
70
|
+
|
|
71
|
+
// 判断成功:没有中断且没有错误
|
|
72
|
+
toolSuccess = !response.interrupted && !response.is_error;
|
|
73
|
+
|
|
74
|
+
logger.debug('Tool output from tool_response', {
|
|
75
|
+
toolName,
|
|
76
|
+
outputLength: toolOutput.length,
|
|
77
|
+
success: toolSuccess,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// 备用方案:从 transcript 读取
|
|
81
|
+
else if (event.transcript_path && existsSync(event.transcript_path)) {
|
|
82
|
+
try {
|
|
83
|
+
const result = await withTimeout(
|
|
84
|
+
readToolResultFromTranscript(event.transcript_path, event.tool_use_id),
|
|
85
|
+
cfg.hookTimeout,
|
|
86
|
+
'readToolResultFromTranscript'
|
|
87
|
+
);
|
|
88
|
+
toolOutput = result.output;
|
|
89
|
+
toolSuccess = result.success;
|
|
90
|
+
|
|
91
|
+
logger.debug('Tool output from transcript', {
|
|
92
|
+
toolName,
|
|
93
|
+
outputLength: toolOutput.length,
|
|
94
|
+
success: toolSuccess,
|
|
95
|
+
});
|
|
96
|
+
} catch (error) {
|
|
97
|
+
logger.warn('Failed to read from transcript', {
|
|
98
|
+
error: error instanceof Error ? error.message : String(error),
|
|
99
|
+
transcriptPath: event.transcript_path,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2.4 构建工具事件
|
|
105
|
+
const toolEvent = {
|
|
106
|
+
event_type: 'tool_use',
|
|
107
|
+
session_id: sessionId,
|
|
108
|
+
timestamp: timestamp,
|
|
109
|
+
tool_name: toolName,
|
|
110
|
+
tool_use_id: event.tool_use_id,
|
|
111
|
+
|
|
112
|
+
// 工具输入
|
|
113
|
+
tool_input: event.tool_input || {},
|
|
114
|
+
|
|
115
|
+
// 工具输出(截断到配置的最大长度)
|
|
116
|
+
tool_output: truncateOutput(toolOutput, cfg.maxOutputLength),
|
|
117
|
+
|
|
118
|
+
// 状态
|
|
119
|
+
success: toolSuccess,
|
|
120
|
+
|
|
121
|
+
// 额外信息
|
|
122
|
+
duration_ms: event.duration_ms || null,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 2.5 追加到 JSONL 文件
|
|
126
|
+
try {
|
|
127
|
+
appendFileSync(sessionFile, JSON.stringify(toolEvent) + '\n');
|
|
128
|
+
} catch (error) {
|
|
129
|
+
throw new FileSystemError(
|
|
130
|
+
`Failed to append to session file: ${sessionFile}`,
|
|
131
|
+
sessionFile,
|
|
132
|
+
'append'
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2.6 提取文件修改信息
|
|
137
|
+
if (toolName === 'Edit' || toolName === 'Write') {
|
|
138
|
+
const filePath = event.tool_input?.file_path;
|
|
139
|
+
if (filePath) {
|
|
140
|
+
logger.info('File modified', { filePath, toolName });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2.7 记录性能
|
|
145
|
+
logger.perf('SessionToolCapture', startTime);
|
|
146
|
+
|
|
147
|
+
} catch (error) {
|
|
148
|
+
hookErrorHandler('SessionToolCapture')(error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// 3. 输出决策
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
console.log(JSON.stringify({ continue: true }));
|
|
156
|
+
process.exit(0);
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// 辅助函数
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 从 transcript 读取工具结果
|
|
164
|
+
*/
|
|
165
|
+
async function readToolResultFromTranscript(
|
|
166
|
+
transcriptPath: string,
|
|
167
|
+
toolUseId: string
|
|
168
|
+
): Promise<{ output: string; success: boolean }> {
|
|
169
|
+
try {
|
|
170
|
+
const content = readFileSync(transcriptPath, 'utf-8');
|
|
171
|
+
const lines = content.trim().split('\n');
|
|
172
|
+
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
const entry = safeJSONParse<any>(line, null, 'transcript line');
|
|
175
|
+
if (!entry) continue;
|
|
176
|
+
|
|
177
|
+
// 查找 tool_result 消息
|
|
178
|
+
if (entry.type === 'message' && entry.role === 'user') {
|
|
179
|
+
for (const block of entry.content || []) {
|
|
180
|
+
if (block.type === 'tool_result' && block.tool_use_id === toolUseId) {
|
|
181
|
+
// 提取输出
|
|
182
|
+
let output = '';
|
|
183
|
+
if (typeof block.content === 'string') {
|
|
184
|
+
output = block.content;
|
|
185
|
+
} else if (Array.isArray(block.content)) {
|
|
186
|
+
output = block.content
|
|
187
|
+
.filter(c => c.type === 'text')
|
|
188
|
+
.map(c => c.text)
|
|
189
|
+
.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
output,
|
|
194
|
+
success: !block.is_error,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { output: '', success: false };
|
|
202
|
+
} catch (error) {
|
|
203
|
+
logger.error('Error reading transcript', {
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
transcriptPath,
|
|
206
|
+
});
|
|
207
|
+
return { output: '', success: false };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 截断输出到指定长度
|
|
213
|
+
*/
|
|
214
|
+
function truncateOutput(output: any, maxLength: number): any {
|
|
215
|
+
if (typeof output === 'string') {
|
|
216
|
+
if (output.length > maxLength) {
|
|
217
|
+
return output.slice(0, maxLength) + '\n... (truncated)';
|
|
218
|
+
}
|
|
219
|
+
return output;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (typeof output === 'object' && output !== null) {
|
|
223
|
+
const json = JSON.stringify(output);
|
|
224
|
+
if (json.length > maxLength) {
|
|
225
|
+
return json.slice(0, maxLength) + '... (truncated)';
|
|
226
|
+
}
|
|
227
|
+
return output;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionToolCapture.hook.ts
|
|
4
|
+
* 在每次工具调用后记录操作
|
|
5
|
+
*
|
|
6
|
+
* Hook 类型: PostToolUse
|
|
7
|
+
* 触发时机: 每次工具调用后
|
|
8
|
+
* 职责: 捕获工具名称、输入、输出、成功状态
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { homedir } from 'os';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 1. 读取 Hook 输入
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
const input = await Bun.stdin.text();
|
|
20
|
+
const event = JSON.parse(input);
|
|
21
|
+
|
|
22
|
+
// 调试:记录实际接收到的事件结构
|
|
23
|
+
console.error('[SessionToolCapture] Received event:', JSON.stringify(event, null, 2));
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// 2. 核心逻辑
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const paiDir = process.env.PAI_DIR || join(homedir(), '.claude');
|
|
31
|
+
const sessionId = event.session_id;
|
|
32
|
+
const timestamp = new Date().toISOString();
|
|
33
|
+
const yearMonth = timestamp.slice(0, 7);
|
|
34
|
+
|
|
35
|
+
// 2.1 定位会话文件
|
|
36
|
+
const sessionFile = join(paiDir, 'SESSIONS/raw', yearMonth, `session-${sessionId}.jsonl`);
|
|
37
|
+
|
|
38
|
+
// 2.2 检查文件是否存在
|
|
39
|
+
if (!existsSync(sessionFile)) {
|
|
40
|
+
console.error(`[SessionToolCapture] Session file not found: ${sessionFile}`);
|
|
41
|
+
console.log(JSON.stringify({ continue: true }));
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2.3 构建工具事件
|
|
46
|
+
const toolEvent = {
|
|
47
|
+
event_type: 'tool_use',
|
|
48
|
+
session_id: sessionId,
|
|
49
|
+
timestamp: timestamp,
|
|
50
|
+
tool_name: event.tool_name,
|
|
51
|
+
|
|
52
|
+
// 工具输入(完整保留)
|
|
53
|
+
tool_input: event.tool_input || {},
|
|
54
|
+
|
|
55
|
+
// 工具输出(截断到 5000 字符)
|
|
56
|
+
// PostToolUse hook 可能不包含 tool_output,使用 result 或其他字段
|
|
57
|
+
tool_output: truncateOutput(event.tool_output || event.result || event.output || '', 5000),
|
|
58
|
+
|
|
59
|
+
// 状态
|
|
60
|
+
// PostToolUse hook 可能不包含 tool_use_status,尝试其他可能的字段
|
|
61
|
+
success: event.tool_use_status === 'success' || event.success === true || event.status === 'success',
|
|
62
|
+
status: event.tool_use_status || event.status || 'unknown',
|
|
63
|
+
|
|
64
|
+
// 额外信息
|
|
65
|
+
duration_ms: event.duration_ms || null,
|
|
66
|
+
|
|
67
|
+
// 调试:保留原始事件的所有字段(可选)
|
|
68
|
+
_raw_event_keys: Object.keys(event),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// 2.4 追加到 JSONL 文件
|
|
72
|
+
// 使用 appendFileSync 保证原子性
|
|
73
|
+
appendFileSync(sessionFile, JSON.stringify(toolEvent) + '\n');
|
|
74
|
+
|
|
75
|
+
// 2.5 可选:提取文件修改信息
|
|
76
|
+
if (event.tool_name === 'Edit' || event.tool_name === 'Write') {
|
|
77
|
+
const filePath = event.tool_input?.file_path;
|
|
78
|
+
if (filePath) {
|
|
79
|
+
console.error(`[SessionToolCapture] File modified: ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('[SessionToolCapture] Error:', error);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// 3. 输出决策
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
console.log(JSON.stringify({ continue: true }));
|
|
92
|
+
process.exit(0);
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// 辅助函数
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 截断输出到指定长度
|
|
100
|
+
*/
|
|
101
|
+
function truncateOutput(output: any, maxLength: number): any {
|
|
102
|
+
if (typeof output === 'string') {
|
|
103
|
+
if (output.length > maxLength) {
|
|
104
|
+
return output.slice(0, maxLength) + '\n... (truncated)';
|
|
105
|
+
}
|
|
106
|
+
return output;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 如果是对象,转为 JSON 后截断
|
|
110
|
+
if (typeof output === 'object' && output !== null) {
|
|
111
|
+
const json = JSON.stringify(output);
|
|
112
|
+
if (json.length > maxLength) {
|
|
113
|
+
return json.slice(0, maxLength) + '... (truncated)';
|
|
114
|
+
}
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return output;
|
|
119
|
+
}
|