@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,567 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* SessionAnalyzer.hook.ts
|
|
4
|
+
* 在会话结束时分析和分类会话
|
|
5
|
+
*
|
|
6
|
+
* Hook 类型: Stop
|
|
7
|
+
* 触发时机: Claude Code 会话结束时
|
|
8
|
+
* 职责: 读取会话事件流、分类、提取关键信息、创建索引
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { createHookLogger } from '../lib/logger.ts';
|
|
14
|
+
import {
|
|
15
|
+
hookErrorHandler,
|
|
16
|
+
safeJSONParse,
|
|
17
|
+
validateRequired,
|
|
18
|
+
FileSystemError
|
|
19
|
+
} from '../lib/errors.ts';
|
|
20
|
+
import { config } from '../lib/config.ts';
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// 类型定义
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
type SessionType =
|
|
27
|
+
| 'coding' // 编码:主要是代码编辑
|
|
28
|
+
| 'debugging' // 调试:测试和错误修复
|
|
29
|
+
| 'research' // 研究:代码搜索和阅读
|
|
30
|
+
| 'writing' // 写作:文档编写
|
|
31
|
+
| 'git' // Git:版本控制操作
|
|
32
|
+
| 'refactoring' // 重构:代码结构调整
|
|
33
|
+
| 'mixed'; // 混合:无明显模式
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// 初始化
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
const logger = createHookLogger('SessionAnalyzer');
|
|
40
|
+
const cfg = config.get();
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// 1. 读取 Hook 输入
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
const input = await Bun.stdin.text();
|
|
47
|
+
const event = JSON.parse(input);
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// 2. 核心逻辑
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const startTime = Date.now();
|
|
55
|
+
|
|
56
|
+
// 2.1 验证必需字段
|
|
57
|
+
const sessionId = validateRequired(event.session_id, 'session_id');
|
|
58
|
+
const timestamp = new Date().toISOString();
|
|
59
|
+
const yearMonth = config.getYearMonth();
|
|
60
|
+
|
|
61
|
+
logger.info('Analyzing session', { sessionId });
|
|
62
|
+
|
|
63
|
+
// 2.2 定位会话文件
|
|
64
|
+
const sessionFile = config.getSessionFilePath(sessionId, yearMonth);
|
|
65
|
+
|
|
66
|
+
if (!existsSync(sessionFile)) {
|
|
67
|
+
logger.warn('Session file not found', { sessionFile, sessionId });
|
|
68
|
+
console.log(JSON.stringify({ continue: true }));
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2.3 读取所有事件
|
|
73
|
+
const events = readSessionEvents(sessionFile);
|
|
74
|
+
|
|
75
|
+
if (events.length === 0) {
|
|
76
|
+
logger.warn('No events found in session', { sessionId });
|
|
77
|
+
console.log(JSON.stringify({ continue: true }));
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2.3 提取会话元数据
|
|
82
|
+
const sessionStart = events.find(e => e.event_type === 'session_start');
|
|
83
|
+
const toolEvents = events.filter(e => e.event_type === 'tool_use');
|
|
84
|
+
|
|
85
|
+
// 2.4 分析会话类型
|
|
86
|
+
const sessionType = classifySessionType(toolEvents);
|
|
87
|
+
|
|
88
|
+
// 2.5 提取关键信息
|
|
89
|
+
const filesModified = extractModifiedFiles(toolEvents);
|
|
90
|
+
const toolUsage = analyzeToolUsage(toolEvents);
|
|
91
|
+
const successRate = calculateSuccessRate(toolEvents);
|
|
92
|
+
|
|
93
|
+
// 2.5.1 提取对话内容(从transcript)
|
|
94
|
+
const conversation = extractConversation(event.transcript_path, sessionId);
|
|
95
|
+
|
|
96
|
+
// 2.6 生成摘要
|
|
97
|
+
const summary = {
|
|
98
|
+
session_id: sessionId,
|
|
99
|
+
timestamp: timestamp,
|
|
100
|
+
|
|
101
|
+
// 会话上下文
|
|
102
|
+
working_directory: sessionStart?.working_directory || 'unknown',
|
|
103
|
+
git_repo: sessionStart?.git_repo || null,
|
|
104
|
+
git_branch: sessionStart?.git_branch || null,
|
|
105
|
+
|
|
106
|
+
// 主机和用户信息
|
|
107
|
+
hostname: sessionStart?.hostname || 'unknown',
|
|
108
|
+
user: sessionStart?.user || 'unknown',
|
|
109
|
+
platform: sessionStart?.platform || 'unknown',
|
|
110
|
+
|
|
111
|
+
// 会话分类
|
|
112
|
+
session_type: sessionType,
|
|
113
|
+
|
|
114
|
+
// 统计信息
|
|
115
|
+
duration_seconds: calculateDuration(events),
|
|
116
|
+
total_tools: toolEvents.length,
|
|
117
|
+
success_rate: successRate,
|
|
118
|
+
|
|
119
|
+
// 关键信息
|
|
120
|
+
files_modified: filesModified,
|
|
121
|
+
tool_usage: toolUsage,
|
|
122
|
+
|
|
123
|
+
// 对话内容
|
|
124
|
+
conversation: conversation,
|
|
125
|
+
|
|
126
|
+
// 生成的摘要文本
|
|
127
|
+
summary_text: generateSummaryText(sessionType, toolEvents, filesModified),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// 2.7 保存摘要
|
|
131
|
+
const summaryDir = join(cfg.summariesDir, yearMonth);
|
|
132
|
+
try {
|
|
133
|
+
mkdirSync(summaryDir, { recursive: true });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
throw new FileSystemError(
|
|
136
|
+
`Failed to create summary directory: ${summaryDir}`,
|
|
137
|
+
summaryDir,
|
|
138
|
+
'mkdir'
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const summaryFile = config.getSummaryFilePath(sessionId, yearMonth);
|
|
143
|
+
try {
|
|
144
|
+
writeFileSync(summaryFile, JSON.stringify(summary, null, 2), { mode: 0o600 });
|
|
145
|
+
logger.debug('Summary file created', { summaryFile });
|
|
146
|
+
} catch (error) {
|
|
147
|
+
throw new FileSystemError(
|
|
148
|
+
`Failed to write summary file: ${summaryFile}`,
|
|
149
|
+
summaryFile,
|
|
150
|
+
'write'
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2.8 创建索引
|
|
155
|
+
createTypeIndex(sessionType, sessionId, summary);
|
|
156
|
+
createDirectoryIndex(sessionStart?.working_directory, sessionId, summary);
|
|
157
|
+
|
|
158
|
+
// 2.9 更新全局元数据
|
|
159
|
+
updateGlobalMetadata(summary);
|
|
160
|
+
|
|
161
|
+
logger.perf('SessionAnalyzer', startTime);
|
|
162
|
+
logger.info('Session analyzed successfully', {
|
|
163
|
+
sessionId,
|
|
164
|
+
sessionType,
|
|
165
|
+
toolCount: toolEvents.length,
|
|
166
|
+
successRate,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
} catch (error) {
|
|
170
|
+
hookErrorHandler('SessionAnalyzer')(error);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// 3. 输出决策
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
console.log(JSON.stringify({ continue: true }));
|
|
178
|
+
process.exit(0);
|
|
179
|
+
|
|
180
|
+
// ============================================================================
|
|
181
|
+
// 辅助函数
|
|
182
|
+
// ============================================================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 读取会话文件中的所有事件
|
|
186
|
+
*/
|
|
187
|
+
function readSessionEvents(filePath: string): any[] {
|
|
188
|
+
try {
|
|
189
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
190
|
+
const lines = content.trim().split('\n');
|
|
191
|
+
return lines.map(line => safeJSONParse(line, {}, 'session event')).filter(e => Object.keys(e).length > 0);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger.error('Error reading session file', {
|
|
194
|
+
error: error instanceof Error ? error.message : String(error),
|
|
195
|
+
filePath,
|
|
196
|
+
});
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 从 transcript 提取对话内容
|
|
203
|
+
*/
|
|
204
|
+
function extractConversation(transcriptPath: string | undefined, sessionId: string): any {
|
|
205
|
+
if (!transcriptPath || !existsSync(transcriptPath)) {
|
|
206
|
+
logger.debug('Transcript not found', { transcriptPath, sessionId });
|
|
207
|
+
return {
|
|
208
|
+
user_messages: [],
|
|
209
|
+
assistant_responses: [],
|
|
210
|
+
message_count: 0
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const content = readFileSync(transcriptPath, 'utf-8');
|
|
216
|
+
const lines = content.trim().split('\n');
|
|
217
|
+
|
|
218
|
+
const userMessages: string[] = [];
|
|
219
|
+
const assistantResponses: string[] = [];
|
|
220
|
+
|
|
221
|
+
for (const line of lines) {
|
|
222
|
+
const entry = safeJSONParse<any>(line, null, 'transcript line');
|
|
223
|
+
if (!entry) continue;
|
|
224
|
+
|
|
225
|
+
// 提取用户消息
|
|
226
|
+
if (entry.type === 'user' && entry.message?.content) {
|
|
227
|
+
userMessages.push(entry.message.content);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 提取助手回复
|
|
231
|
+
if (entry.type === 'assistant' && entry.message?.content) {
|
|
232
|
+
const content = entry.message.content;
|
|
233
|
+
if (Array.isArray(content)) {
|
|
234
|
+
// 提取 text 类型的内容
|
|
235
|
+
const textContent = content
|
|
236
|
+
.filter(c => c.type === 'text')
|
|
237
|
+
.map(c => c.text)
|
|
238
|
+
.join('\n');
|
|
239
|
+
if (textContent) {
|
|
240
|
+
assistantResponses.push(textContent);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
logger.debug('Conversation extracted', {
|
|
247
|
+
userMessages: userMessages.length,
|
|
248
|
+
assistantResponses: assistantResponses.length,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
user_messages: userMessages,
|
|
253
|
+
assistant_responses: assistantResponses,
|
|
254
|
+
message_count: userMessages.length + assistantResponses.length
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
logger.error('Error reading transcript', {
|
|
258
|
+
error: error instanceof Error ? error.message : String(error),
|
|
259
|
+
transcriptPath,
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
user_messages: [],
|
|
263
|
+
assistant_responses: [],
|
|
264
|
+
message_count: 0
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 分类会话类型
|
|
271
|
+
*/
|
|
272
|
+
function classifySessionType(toolEvents: any[]): SessionType {
|
|
273
|
+
if (toolEvents.length === 0) return 'mixed';
|
|
274
|
+
|
|
275
|
+
// 统计工具使用次数
|
|
276
|
+
const toolCounts: Record<string, number> = {};
|
|
277
|
+
const bashCommands: string[] = [];
|
|
278
|
+
|
|
279
|
+
for (const event of toolEvents) {
|
|
280
|
+
const toolName = event.tool_name;
|
|
281
|
+
toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
|
|
282
|
+
|
|
283
|
+
// 收集 Bash 命令
|
|
284
|
+
if (toolName === 'Bash' && event.tool_input?.command) {
|
|
285
|
+
bashCommands.push(event.tool_input.command);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const editCount = (toolCounts['Edit'] || 0) + (toolCounts['Write'] || 0);
|
|
290
|
+
const readCount = toolCounts['Read'] || 0;
|
|
291
|
+
const searchCount = (toolCounts['Grep'] || 0) + (toolCounts['Glob'] || 0);
|
|
292
|
+
const bashCount = toolCounts['Bash'] || 0;
|
|
293
|
+
|
|
294
|
+
// Git 操作检测
|
|
295
|
+
const gitCommandCount = bashCommands.filter(cmd =>
|
|
296
|
+
cmd.includes('git ') || cmd.startsWith('git')
|
|
297
|
+
).length;
|
|
298
|
+
|
|
299
|
+
if (gitCommandCount > bashCount * 0.5) {
|
|
300
|
+
return 'git';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 测试/调试检测
|
|
304
|
+
const testCommandCount = bashCommands.filter(cmd =>
|
|
305
|
+
cmd.includes('test') || cmd.includes('npm test') ||
|
|
306
|
+
cmd.includes('pytest') || cmd.includes('jest')
|
|
307
|
+
).length;
|
|
308
|
+
|
|
309
|
+
if (testCommandCount > 0 && readCount > editCount) {
|
|
310
|
+
return 'debugging';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// 编码检测
|
|
314
|
+
if (editCount > toolEvents.length * 0.4) {
|
|
315
|
+
return 'coding';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 研究检测
|
|
319
|
+
if (searchCount > toolEvents.length * 0.3 && readCount > editCount) {
|
|
320
|
+
return 'research';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// 文档编写检测
|
|
324
|
+
const mdWriteCount = toolEvents.filter(e =>
|
|
325
|
+
(e.tool_name === 'Write' || e.tool_name === 'Edit') &&
|
|
326
|
+
e.tool_input?.file_path?.endsWith('.md')
|
|
327
|
+
).length;
|
|
328
|
+
|
|
329
|
+
if (mdWriteCount > editCount * 0.5) {
|
|
330
|
+
return 'writing';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 默认为混合类型
|
|
334
|
+
return 'mixed';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 提取修改的文件列表
|
|
339
|
+
*/
|
|
340
|
+
function extractModifiedFiles(toolEvents: any[]): string[] {
|
|
341
|
+
const files = new Set<string>();
|
|
342
|
+
|
|
343
|
+
for (const event of toolEvents) {
|
|
344
|
+
if (event.tool_name === 'Edit' || event.tool_name === 'Write') {
|
|
345
|
+
const filePath = event.tool_input?.file_path;
|
|
346
|
+
if (filePath) {
|
|
347
|
+
files.add(filePath);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return Array.from(files);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 分析工具使用情况
|
|
357
|
+
*/
|
|
358
|
+
function analyzeToolUsage(toolEvents: any[]): Record<string, number> {
|
|
359
|
+
const usage: Record<string, number> = {};
|
|
360
|
+
|
|
361
|
+
for (const event of toolEvents) {
|
|
362
|
+
const toolName = event.tool_name;
|
|
363
|
+
usage[toolName] = (usage[toolName] || 0) + 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return usage;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* 计算成功率
|
|
371
|
+
*/
|
|
372
|
+
function calculateSuccessRate(toolEvents: any[]): number {
|
|
373
|
+
if (toolEvents.length === 0) return 0;
|
|
374
|
+
|
|
375
|
+
const successCount = toolEvents.filter(e => e.success === true).length;
|
|
376
|
+
return Math.round((successCount / toolEvents.length) * 100);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 计算会话持续时间(秒)
|
|
381
|
+
*/
|
|
382
|
+
function calculateDuration(events: any[]): number {
|
|
383
|
+
if (events.length < 2) return 0;
|
|
384
|
+
|
|
385
|
+
const startTime = new Date(events[0].timestamp).getTime();
|
|
386
|
+
const endTime = new Date(events[events.length - 1].timestamp).getTime();
|
|
387
|
+
|
|
388
|
+
return Math.round((endTime - startTime) / 1000);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* 生成摘要文本
|
|
393
|
+
*/
|
|
394
|
+
function generateSummaryText(
|
|
395
|
+
sessionType: SessionType,
|
|
396
|
+
toolEvents: any[],
|
|
397
|
+
filesModified: string[]
|
|
398
|
+
): string {
|
|
399
|
+
const toolCount = toolEvents.length;
|
|
400
|
+
const fileCount = filesModified.length;
|
|
401
|
+
|
|
402
|
+
let summary = `${sessionType} session with ${toolCount} tool operations`;
|
|
403
|
+
|
|
404
|
+
if (fileCount > 0) {
|
|
405
|
+
summary += `, modified ${fileCount} file(s)`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return summary;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 创建类型索引
|
|
413
|
+
*/
|
|
414
|
+
function createTypeIndex(
|
|
415
|
+
sessionType: SessionType,
|
|
416
|
+
sessionId: string,
|
|
417
|
+
summary: any
|
|
418
|
+
): void {
|
|
419
|
+
try {
|
|
420
|
+
const typeIndexPath = config.getTypeIndexPath(sessionType);
|
|
421
|
+
const typeDir = join(cfg.analysisDir, 'by-type', sessionType);
|
|
422
|
+
mkdirSync(typeDir, { recursive: true });
|
|
423
|
+
|
|
424
|
+
// 读取现有索引
|
|
425
|
+
let sessions: any[] = [];
|
|
426
|
+
if (existsSync(typeIndexPath)) {
|
|
427
|
+
const content = readFileSync(typeIndexPath, 'utf-8');
|
|
428
|
+
sessions = safeJSONParse<any[]>(content, [], 'type index');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 添加新会话
|
|
432
|
+
sessions.push({
|
|
433
|
+
session_id: sessionId,
|
|
434
|
+
timestamp: summary.timestamp,
|
|
435
|
+
working_directory: summary.working_directory,
|
|
436
|
+
duration_seconds: summary.duration_seconds,
|
|
437
|
+
total_tools: summary.total_tools,
|
|
438
|
+
success_rate: summary.success_rate,
|
|
439
|
+
summary_text: summary.summary_text,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// 按时间倒序排序
|
|
443
|
+
sessions.sort((a, b) =>
|
|
444
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// 写回文件
|
|
448
|
+
writeFileSync(typeIndexPath, JSON.stringify(sessions, null, 2), { mode: 0o600 });
|
|
449
|
+
|
|
450
|
+
logger.debug('Type index updated', { sessionType, sessionCount: sessions.length });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
logger.error('Error creating type index', {
|
|
453
|
+
error: error instanceof Error ? error.message : String(error),
|
|
454
|
+
sessionType,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* 创建目录索引
|
|
461
|
+
*/
|
|
462
|
+
function createDirectoryIndex(
|
|
463
|
+
workingDirectory: string | undefined,
|
|
464
|
+
sessionId: string,
|
|
465
|
+
summary: any
|
|
466
|
+
): void {
|
|
467
|
+
if (!workingDirectory) return;
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
// Base64 编码目录路径
|
|
471
|
+
const dirHash = Buffer.from(workingDirectory).toString('base64')
|
|
472
|
+
.replace(/\//g, '_')
|
|
473
|
+
.replace(/\+/g, '-')
|
|
474
|
+
.replace(/=/g, '');
|
|
475
|
+
|
|
476
|
+
const dirIndexDir = join(cfg.analysisDir, 'by-directory', dirHash);
|
|
477
|
+
mkdirSync(dirIndexDir, { recursive: true });
|
|
478
|
+
|
|
479
|
+
// 保存原始路径
|
|
480
|
+
const pathFile = join(dirIndexDir, 'path.txt');
|
|
481
|
+
if (!existsSync(pathFile)) {
|
|
482
|
+
writeFileSync(pathFile, workingDirectory, { mode: 0o600 });
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 读取现有索引
|
|
486
|
+
const indexFile = join(dirIndexDir, 'sessions.json');
|
|
487
|
+
let sessions: any[] = [];
|
|
488
|
+
if (existsSync(indexFile)) {
|
|
489
|
+
const content = readFileSync(indexFile, 'utf-8');
|
|
490
|
+
sessions = safeJSONParse<any[]>(content, [], 'directory index');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 添加新会话
|
|
494
|
+
sessions.push({
|
|
495
|
+
session_id: sessionId,
|
|
496
|
+
timestamp: summary.timestamp,
|
|
497
|
+
session_type: summary.session_type,
|
|
498
|
+
duration_seconds: summary.duration_seconds,
|
|
499
|
+
total_tools: summary.total_tools,
|
|
500
|
+
files_modified: summary.files_modified,
|
|
501
|
+
summary_text: summary.summary_text,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// 按时间倒序排序
|
|
505
|
+
sessions.sort((a, b) =>
|
|
506
|
+
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
// 写回文件
|
|
510
|
+
writeFileSync(indexFile, JSON.stringify(sessions, null, 2), { mode: 0o600 });
|
|
511
|
+
|
|
512
|
+
logger.debug('Directory index updated', { workingDirectory, sessionCount: sessions.length });
|
|
513
|
+
} catch (error) {
|
|
514
|
+
logger.error('Error creating directory index', {
|
|
515
|
+
error: error instanceof Error ? error.message : String(error),
|
|
516
|
+
workingDirectory,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* 更新全局元数据索引
|
|
523
|
+
*/
|
|
524
|
+
function updateGlobalMetadata(summary: any): void {
|
|
525
|
+
try {
|
|
526
|
+
mkdirSync(cfg.indexDir, { recursive: true });
|
|
527
|
+
|
|
528
|
+
const metadataFile = config.getMetadataPath();
|
|
529
|
+
|
|
530
|
+
// 读取现有元数据
|
|
531
|
+
let metadata: any = {
|
|
532
|
+
total_sessions: 0,
|
|
533
|
+
sessions_by_type: {},
|
|
534
|
+
sessions_by_directory: {},
|
|
535
|
+
last_updated: null,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (existsSync(metadataFile)) {
|
|
539
|
+
const content = readFileSync(metadataFile, 'utf-8');
|
|
540
|
+
metadata = safeJSONParse(content, metadata, 'global metadata');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 更新统计
|
|
544
|
+
metadata.total_sessions += 1;
|
|
545
|
+
metadata.sessions_by_type[summary.session_type] =
|
|
546
|
+
(metadata.sessions_by_type[summary.session_type] || 0) + 1;
|
|
547
|
+
|
|
548
|
+
if (summary.working_directory) {
|
|
549
|
+
metadata.sessions_by_directory[summary.working_directory] =
|
|
550
|
+
(metadata.sessions_by_directory[summary.working_directory] || 0) + 1;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
metadata.last_updated = new Date().toISOString();
|
|
554
|
+
|
|
555
|
+
// 写回文件
|
|
556
|
+
writeFileSync(metadataFile, JSON.stringify(metadata, null, 2), { mode: 0o600 });
|
|
557
|
+
|
|
558
|
+
logger.debug('Global metadata updated', {
|
|
559
|
+
totalSessions: metadata.total_sessions,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
} catch (error) {
|
|
563
|
+
logger.error('Error updating global metadata', {
|
|
564
|
+
error: error instanceof Error ? error.message : String(error),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|