sumulige-claude 1.0.5 → 1.0.7
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/.claude/.kickoff-hint.txt +51 -0
- package/.claude/ANCHORS.md +40 -0
- package/.claude/MEMORY.md +34 -0
- package/.claude/PROJECT_LOG.md +101 -0
- package/.claude/THINKING_CHAIN_GUIDE.md +287 -0
- package/.claude/commands/commit-push-pr.md +59 -0
- package/.claude/commands/commit.md +53 -0
- package/.claude/commands/pr.md +76 -0
- package/.claude/commands/review.md +61 -0
- package/.claude/commands/sessions.md +62 -0
- package/.claude/commands/skill-create.md +131 -0
- package/.claude/commands/test.md +56 -0
- package/.claude/commands/todos.md +99 -0
- package/.claude/commands/verify-work.md +63 -0
- package/.claude/hooks/code-formatter.cjs +187 -0
- package/.claude/hooks/code-tracer.cjs +331 -0
- package/.claude/hooks/conversation-recorder.cjs +340 -0
- package/.claude/hooks/decision-tracker.cjs +398 -0
- package/.claude/hooks/export.cjs +329 -0
- package/.claude/hooks/multi-session.cjs +181 -0
- package/.claude/hooks/privacy-filter.js +224 -0
- package/.claude/hooks/project-kickoff.cjs +114 -0
- package/.claude/hooks/rag-skill-loader.cjs +159 -0
- package/.claude/hooks/session-end.sh +61 -0
- package/.claude/hooks/sync-to-log.sh +83 -0
- package/.claude/hooks/thinking-silent.cjs +145 -0
- package/.claude/hooks/tl-summary.sh +54 -0
- package/.claude/hooks/todo-manager.cjs +248 -0
- package/.claude/hooks/verify-work.cjs +134 -0
- package/.claude/sessions/active-sessions.json +444 -0
- package/.claude/settings.local.json +36 -2
- package/.claude/thinking-routes/.last-sync +1 -0
- package/.claude/thinking-routes/QUICKREF.md +98 -0
- package/CHANGELOG.md +56 -0
- package/DEV_TOOLS_GUIDE.md +190 -0
- package/PROJECT_STRUCTURE.md +10 -1
- package/README.md +20 -6
- package/cli.js +85 -824
- package/config/defaults.json +34 -0
- package/development/todos/INDEX.md +14 -58
- package/lib/commands.js +698 -0
- package/lib/config.js +71 -0
- package/lib/utils.js +62 -0
- package/package.json +2 -2
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Code Tracer - 代码变更追踪
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* - 监听文件修改事件
|
|
7
|
+
* - 自动关联到最近的决策
|
|
8
|
+
* - 维护双向映射表 (文件 ↔ 决策)
|
|
9
|
+
* - 支持查询文件的历史决策
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
16
|
+
const CODE_TRACE_DIR = path.join(PROJECT_DIR, '.claude', 'code-trace');
|
|
17
|
+
const FILES_MAP = path.join(CODE_TRACE_DIR, 'files-map.json');
|
|
18
|
+
const DECISIONS_MAP = path.join(CODE_TRACE_DIR, 'decisions-map.json');
|
|
19
|
+
const DECISIONS_FILE = path.join(PROJECT_DIR, '.claude', 'decisions', 'DECISIONS.md');
|
|
20
|
+
|
|
21
|
+
// 确保目录存在
|
|
22
|
+
try { fs.mkdirSync(CODE_TRACE_DIR, { recursive: true }); } catch (e) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 初始化映射文件
|
|
26
|
+
*/
|
|
27
|
+
function initMaps() {
|
|
28
|
+
if (!fs.existsSync(FILES_MAP)) {
|
|
29
|
+
fs.writeFileSync(FILES_MAP, JSON.stringify({ files: {}, lastUpdated: null }, null, 2), 'utf-8');
|
|
30
|
+
}
|
|
31
|
+
if (!fs.existsSync(DECISIONS_MAP)) {
|
|
32
|
+
fs.writeFileSync(DECISIONS_MAP, JSON.stringify({ decisions: {}, lastUpdated: null }, null, 2), 'utf-8');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 读取映射文件
|
|
38
|
+
*/
|
|
39
|
+
function readMaps() {
|
|
40
|
+
initMaps();
|
|
41
|
+
return {
|
|
42
|
+
filesMap: JSON.parse(fs.readFileSync(FILES_MAP, 'utf-8')),
|
|
43
|
+
decisionsMap: JSON.parse(fs.readFileSync(DECISIONS_MAP, 'utf-8'))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 写入映射文件
|
|
49
|
+
*/
|
|
50
|
+
function writeMaps(filesMap, decisionsMap) {
|
|
51
|
+
const now = new Date().toISOString();
|
|
52
|
+
filesMap.lastUpdated = now;
|
|
53
|
+
decisionsMap.lastUpdated = now;
|
|
54
|
+
fs.writeFileSync(FILES_MAP, JSON.stringify(filesMap, null, 2), 'utf-8');
|
|
55
|
+
fs.writeFileSync(DECISIONS_MAP, JSON.stringify(decisionsMap, null, 2), 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 关联文件到决策
|
|
60
|
+
*/
|
|
61
|
+
function linkFileToDecision(filePath, decisionId, metadata = {}) {
|
|
62
|
+
const { filesMap, decisionsMap } = readMaps();
|
|
63
|
+
|
|
64
|
+
// 规范化路径
|
|
65
|
+
const normalizedPath = path.relative(PROJECT_DIR, filePath);
|
|
66
|
+
|
|
67
|
+
// 更新文件映射
|
|
68
|
+
if (!filesMap.files[normalizedPath]) {
|
|
69
|
+
filesMap.files[normalizedPath] = {
|
|
70
|
+
decisions: [],
|
|
71
|
+
firstSeen: new Date().toISOString(),
|
|
72
|
+
lastModified: new Date().toISOString()
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!filesMap.files[normalizedPath].decisions.includes(decisionId)) {
|
|
77
|
+
filesMap.files[normalizedPath].decisions.push(decisionId);
|
|
78
|
+
}
|
|
79
|
+
filesMap.files[normalizedPath].lastModified = new Date().toISOString();
|
|
80
|
+
|
|
81
|
+
// 更新决策映射
|
|
82
|
+
if (!decisionsMap.decisions[decisionId]) {
|
|
83
|
+
decisionsMap.decisions[decisionId] = {
|
|
84
|
+
description: '',
|
|
85
|
+
files: [],
|
|
86
|
+
firstLinked: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!decisionsMap.decisions[decisionId].files.includes(normalizedPath)) {
|
|
91
|
+
decisionsMap.decisions[decisionId].files.push(normalizedPath);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 添加元数据
|
|
95
|
+
if (metadata.description) {
|
|
96
|
+
decisionsMap.decisions[decisionId].description = metadata.description;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
writeMaps(filesMap, decisionsMap);
|
|
100
|
+
|
|
101
|
+
return { normalizedPath, decisionId };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 批量关联文件到决策
|
|
106
|
+
*/
|
|
107
|
+
function linkFilesToDecision(filePaths, decisionId, metadata = {}) {
|
|
108
|
+
const results = [];
|
|
109
|
+
filePaths.forEach(filePath => {
|
|
110
|
+
results.push(linkFileToDecision(filePath, decisionId, metadata));
|
|
111
|
+
});
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 查询文件的决策历史
|
|
117
|
+
*/
|
|
118
|
+
function traceFile(filePath) {
|
|
119
|
+
const { filesMap } = readMaps();
|
|
120
|
+
const normalizedPath = path.relative(PROJECT_DIR, filePath);
|
|
121
|
+
|
|
122
|
+
if (!filesMap.files[normalizedPath]) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return filesMap.files[normalizedPath];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 查询决策关联的文件
|
|
131
|
+
*/
|
|
132
|
+
function traceDecision(decisionId) {
|
|
133
|
+
const { decisionsMap } = readMaps();
|
|
134
|
+
|
|
135
|
+
if (!decisionsMap.decisions[decisionId]) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return decisionsMap.decisions[decisionId];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 获取所有文件-决策关系
|
|
144
|
+
*/
|
|
145
|
+
function getAllLinks() {
|
|
146
|
+
const { filesMap, decisionsMap } = readMaps();
|
|
147
|
+
return { filesMap, decisionsMap };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 格式化显示文件追踪信息
|
|
152
|
+
*/
|
|
153
|
+
function displayFileTrace(filePath) {
|
|
154
|
+
const trace = traceFile(filePath);
|
|
155
|
+
|
|
156
|
+
if (!trace) {
|
|
157
|
+
console.log(`\n📭 文件 "${filePath}" 暂无决策记录\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n📄 文件追踪: ${filePath}\n`);
|
|
162
|
+
console.log(`⏰ 首次记录: ${trace.firstSeen}`);
|
|
163
|
+
console.log(`🔄 最后修改: ${trace.lastModified}`);
|
|
164
|
+
console.log(`\n🔗 关联的决策 (${trace.decisions.length}):\n`);
|
|
165
|
+
|
|
166
|
+
if (trace.decisions.length > 0) {
|
|
167
|
+
const { decisionsMap } = readMaps();
|
|
168
|
+
trace.decisions.forEach(decisionId => {
|
|
169
|
+
const decision = decisionsMap.decisions[decisionId];
|
|
170
|
+
if (decision) {
|
|
171
|
+
console.log(` - [${decisionId}] ${decision.description || '无描述'}`);
|
|
172
|
+
} else {
|
|
173
|
+
console.log(` - [${decisionId}] (决策详情未找到)`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 格式化显示决策关联文件
|
|
181
|
+
*/
|
|
182
|
+
function displayDecisionTrace(decisionId) {
|
|
183
|
+
const trace = traceDecision(decisionId);
|
|
184
|
+
|
|
185
|
+
if (!trace) {
|
|
186
|
+
console.log(`\n📭 决策 "${decisionId}" 暂无文件记录\n`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(`\n🔗 决策追踪: ${decisionId}\n`);
|
|
191
|
+
console.log(`📝 ${trace.description || '无描述'}`);
|
|
192
|
+
console.log(`⏰ 首次关联: ${trace.firstLinked}`);
|
|
193
|
+
console.log(`\n📄 关联的文件 (${trace.files.length}):\n`);
|
|
194
|
+
|
|
195
|
+
trace.files.forEach(file => {
|
|
196
|
+
const fullPath = path.join(PROJECT_DIR, file);
|
|
197
|
+
const exists = fs.existsSync(fullPath) ? '✅' : '❌';
|
|
198
|
+
console.log(` ${exists} ${file}`);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 显示所有关联
|
|
204
|
+
*/
|
|
205
|
+
function displayAllLinks() {
|
|
206
|
+
const { filesMap, decisionsMap } = readMaps();
|
|
207
|
+
|
|
208
|
+
console.log('\n📊 代码-决策关联图谱\n');
|
|
209
|
+
console.log(`📄 文件数量: ${Object.keys(filesMap.files).length}`);
|
|
210
|
+
console.log(`🔗 决策数量: ${Object.keys(decisionsMap.decisions).length}\n`);
|
|
211
|
+
|
|
212
|
+
if (Object.keys(filesMap.files).length > 0) {
|
|
213
|
+
console.log('📄 文件列表:\n');
|
|
214
|
+
Object.entries(filesMap.files).forEach(([file, data]) => {
|
|
215
|
+
const decisionCount = data.decisions.length;
|
|
216
|
+
const status = decisionCount > 0 ? `🔗 ${decisionCount} 决策` : '⚪ 无关联';
|
|
217
|
+
console.log(` ${status} - ${file}`);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* 从 DECISIONS.md 同步决策描述
|
|
224
|
+
*/
|
|
225
|
+
function syncDecisionDescriptions() {
|
|
226
|
+
if (!fs.existsSync(DECISIONS_FILE)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const content = fs.readFileSync(DECISIONS_FILE, 'utf-8');
|
|
231
|
+
const { decisionsMap } = readMaps();
|
|
232
|
+
|
|
233
|
+
// 提取决策标题
|
|
234
|
+
const decisionRegex = /^## \[([A-Z]\d+)\]\s+\d{4}-\d{2}-\d{2}.*?-\s*(.+?)$/gm;
|
|
235
|
+
let match;
|
|
236
|
+
|
|
237
|
+
while ((match = decisionRegex.exec(content)) !== null) {
|
|
238
|
+
const [, decisionId, title] = match;
|
|
239
|
+
if (decisionsMap.decisions[decisionId]) {
|
|
240
|
+
if (!decisionsMap.decisions[decisionId].description) {
|
|
241
|
+
decisionsMap.decisions[decisionId].description = title;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const { filesMap } = readMaps();
|
|
247
|
+
writeMaps(filesMap, decisionsMap);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// CLI
|
|
251
|
+
function main() {
|
|
252
|
+
const args = process.argv.slice(2);
|
|
253
|
+
const command = args[0];
|
|
254
|
+
|
|
255
|
+
switch (command) {
|
|
256
|
+
case 'trace': {
|
|
257
|
+
// 追踪文件
|
|
258
|
+
const filePath = args[1];
|
|
259
|
+
if (!filePath) {
|
|
260
|
+
console.error('用法: node code-tracer.cjs trace <文件路径>');
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
displayFileTrace(filePath);
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
case 'decision': {
|
|
268
|
+
// 查看决策的文件
|
|
269
|
+
const decisionId = args[1];
|
|
270
|
+
if (!decisionId) {
|
|
271
|
+
console.error('用法: node code-tracer.cjs decision <决策ID>');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
displayDecisionTrace(decisionId);
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
case 'link': {
|
|
279
|
+
// 手动关联
|
|
280
|
+
const filePath = args[1];
|
|
281
|
+
const decisionId = args[2];
|
|
282
|
+
if (!filePath || !decisionId) {
|
|
283
|
+
console.error('用法: node code-tracer.cjs link <文件路径> <决策ID>');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
linkFileToDecision(filePath, decisionId);
|
|
287
|
+
console.log(`✅ 已关联 ${filePath} → ${decisionId}`);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case 'all': {
|
|
292
|
+
displayAllLinks();
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
case 'sync': {
|
|
297
|
+
syncDecisionDescriptions();
|
|
298
|
+
console.log('✅ 已同步决策描述');
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
default:
|
|
303
|
+
console.log(`
|
|
304
|
+
Code Tracer - 代码变更追踪工具
|
|
305
|
+
|
|
306
|
+
用法:
|
|
307
|
+
node code-tracer.cjs trace <文件> 查看文件的决策历史
|
|
308
|
+
node code-tracer.cjs decision <ID> 查看决策关联的文件
|
|
309
|
+
node code-tracer.cjs link <文件> <ID> 手动关联文件到决策
|
|
310
|
+
node code-tracer.cjs all 显示所有关联
|
|
311
|
+
node code-tracer.cjs sync 同步决策描述
|
|
312
|
+
|
|
313
|
+
快捷命令:
|
|
314
|
+
alias trace='node .claude/hooks/code-tracer.cjs trace'
|
|
315
|
+
alias dtrace='node .claude/hooks/code-tracer.cjs decision'
|
|
316
|
+
`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module.exports = {
|
|
321
|
+
linkFileToDecision,
|
|
322
|
+
linkFilesToDecision,
|
|
323
|
+
traceFile,
|
|
324
|
+
traceDecision,
|
|
325
|
+
getAllLinks,
|
|
326
|
+
syncDecisionDescriptions
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
if (require.main === module) {
|
|
330
|
+
main();
|
|
331
|
+
}
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Conversation Recorder - 完整对话记录器
|
|
4
|
+
*
|
|
5
|
+
* 功能:
|
|
6
|
+
* - 捕获完整对话(用户输入 + AI 输出)
|
|
7
|
+
* - 按日期组织存储
|
|
8
|
+
* - 自动过滤敏感信息
|
|
9
|
+
* - 记录使用的工具
|
|
10
|
+
* - 支持 Session 管理
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { filterSensitive } = require('./privacy-filter.js');
|
|
16
|
+
|
|
17
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
18
|
+
const TRANSCRIPTS_DIR = path.join(PROJECT_DIR, '.claude', 'transcripts');
|
|
19
|
+
const CURRENT_SESSION_FILE = path.join(PROJECT_DIR, '.claude', 'thinking-routes', '.current-session');
|
|
20
|
+
|
|
21
|
+
// 确保目录存在
|
|
22
|
+
try { fs.mkdirSync(TRANSCRIPTS_DIR, { recursive: true }); } catch (e) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取当前会话 ID
|
|
26
|
+
*/
|
|
27
|
+
function getSessionId() {
|
|
28
|
+
try {
|
|
29
|
+
const data = fs.readFileSync(CURRENT_SESSION_FILE, 'utf-8').trim();
|
|
30
|
+
if (data) return data;
|
|
31
|
+
} catch (e) {}
|
|
32
|
+
return 'session-' + Date.now();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 获取今日 transcript 文件路径
|
|
37
|
+
*/
|
|
38
|
+
function getTodayTranscriptPath() {
|
|
39
|
+
const now = new Date();
|
|
40
|
+
const year = now.getFullYear();
|
|
41
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
42
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
43
|
+
|
|
44
|
+
const dir = path.join(TRANSCRIPTS_DIR, String(year), month);
|
|
45
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch (e) {}
|
|
46
|
+
|
|
47
|
+
return path.join(dir, `${day}.md`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 获取当前 Session 编号
|
|
52
|
+
*/
|
|
53
|
+
function getSessionNumber() {
|
|
54
|
+
const transcriptPath = getTodayTranscriptPath();
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(transcriptPath, 'utf-8');
|
|
57
|
+
const matches = content.match(/^## Session \d+/gm);
|
|
58
|
+
if (matches) {
|
|
59
|
+
const lastNum = parseInt(matches[matches.length - 1].match(/\d+/)[0]);
|
|
60
|
+
return String(lastNum + 1).padStart(3, '0');
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {}
|
|
63
|
+
return '001';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 获取当前时间戳
|
|
68
|
+
*/
|
|
69
|
+
function getTimestamp() {
|
|
70
|
+
return new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 格式化消息内容
|
|
75
|
+
*/
|
|
76
|
+
function formatMessage(role, content, metadata = {}) {
|
|
77
|
+
const timestamp = getTimestamp();
|
|
78
|
+
const roleEmoji = role === 'user' ? '👤' : role === 'assistant' ? '🤖' : '🔧';
|
|
79
|
+
|
|
80
|
+
let section = `\n### ${roleEmoji} ${role.charAt(0).toUpperCase() + role.slice(1)} - ${timestamp}\n\n`;
|
|
81
|
+
|
|
82
|
+
// 过滤敏感信息
|
|
83
|
+
const filtered = filterSensitive(content || '');
|
|
84
|
+
section += filtered.filteredText;
|
|
85
|
+
|
|
86
|
+
// 添加元数据
|
|
87
|
+
if (Object.keys(metadata).length > 0) {
|
|
88
|
+
section += '\n\n**Metadata**:\n';
|
|
89
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
90
|
+
section += `- ${key}: ${value}\n`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return section + '\n';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 追加到 transcript
|
|
99
|
+
*/
|
|
100
|
+
function appendToTranscript(content) {
|
|
101
|
+
const transcriptPath = getTodayTranscriptPath();
|
|
102
|
+
|
|
103
|
+
// 如果文件不存在,创建头部
|
|
104
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
105
|
+
const now = new Date();
|
|
106
|
+
const header = `# ${now.toISOString().split('T')[0]} - Conversation Transcript\n\n`;
|
|
107
|
+
fs.writeFileSync(transcriptPath, header, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fs.appendFileSync(transcriptPath, content, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 记录用户消息
|
|
115
|
+
*/
|
|
116
|
+
function recordUserMessage(content) {
|
|
117
|
+
const sessionId = getSessionId();
|
|
118
|
+
const sessionNum = getSessionNumber();
|
|
119
|
+
|
|
120
|
+
const section = `\n${'='.repeat(60)}\n`;
|
|
121
|
+
const header = `## Session ${sessionNum} - ${getTimestamp()} | ID: ${sessionId}\n`;
|
|
122
|
+
|
|
123
|
+
appendToTranscript(section + header + formatMessage('user', content));
|
|
124
|
+
|
|
125
|
+
return { sessionId, sessionNum };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 记录 AI 回复
|
|
130
|
+
*/
|
|
131
|
+
function recordAssistantMessage(content, toolsUsed = []) {
|
|
132
|
+
let section = formatMessage('assistant', content);
|
|
133
|
+
|
|
134
|
+
if (toolsUsed.length > 0) {
|
|
135
|
+
section += `\n**Tools Used**:\n`;
|
|
136
|
+
toolsUsed.forEach(tool => {
|
|
137
|
+
section += `- \`${tool.name}\`${tool.args ? `: ${tool.args}` : ''}\n`;
|
|
138
|
+
});
|
|
139
|
+
section += '\n';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
appendToTranscript(section);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 记录工具使用
|
|
147
|
+
*/
|
|
148
|
+
function recordToolUse(toolName, args, result) {
|
|
149
|
+
const timestamp = getTimestamp();
|
|
150
|
+
let section = `\n### 🔧 Tool: ${toolName} - ${timestamp}\n\n`;
|
|
151
|
+
|
|
152
|
+
if (args) {
|
|
153
|
+
section += `**Args**:\n\`\`\`\n${JSON.stringify(args, null, 2)}\n\`\`\`\n\n`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result) {
|
|
157
|
+
const filtered = filterSensitive(String(result));
|
|
158
|
+
section += `**Result**:\n\`\`\`\n${filtered.filteredText.substring(0, 500)}${filtered.filteredText.length > 500 ? '...' : ''}\n\`\`\`\n\n`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
appendToTranscript(section);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 记录决策
|
|
166
|
+
*/
|
|
167
|
+
function recordDecision(title, details, links = {}) {
|
|
168
|
+
const timestamp = getTimestamp();
|
|
169
|
+
const decisionsPath = path.join(PROJECT_DIR, '.claude', 'decisions', 'DECISIONS.md');
|
|
170
|
+
|
|
171
|
+
// 确保文件存在
|
|
172
|
+
if (!fs.existsSync(decisionsPath)) {
|
|
173
|
+
fs.writeFileSync(decisionsPath, `# Decisions Log\n\n> 所有项目决策的完整记录\n\n`, 'utf-8');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 生成决策 ID
|
|
177
|
+
const content = fs.readFileSync(decisionsPath, 'utf-8');
|
|
178
|
+
const match = content.match(/\[D(\d+)\]/g);
|
|
179
|
+
const nextId = match ? Math.max(...match.map(m => parseInt(m.slice(2, -1)))) + 1 : 1;
|
|
180
|
+
const decisionId = `D${String(nextId).padStart(3, '0')}`;
|
|
181
|
+
|
|
182
|
+
let section = `\n## [${decisionId}] ${timestamp} - ${title}\n\n`;
|
|
183
|
+
|
|
184
|
+
if (details.reason) {
|
|
185
|
+
section += `### 💡 理由\n${details.reason}\n\n`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (details.content) {
|
|
189
|
+
section += `### 📌 决策内容\n${details.content}\n\n`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (Object.keys(links).length > 0) {
|
|
193
|
+
section += `### 🔗 关联\n`;
|
|
194
|
+
if (links.conversation) section += `- 对话: \`${links.conversation}\`\n`;
|
|
195
|
+
if (links.files && links.files.length > 0) {
|
|
196
|
+
section += `- 代码: ${links.files.map(f => `\`${f}\``).join(', ')}\n`;
|
|
197
|
+
}
|
|
198
|
+
if (links.commit) section += `- Commit: \`${links.commit}\`\n`;
|
|
199
|
+
section += '\n';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (details.tags && details.tags.length > 0) {
|
|
203
|
+
section += `### 🏷️ 主题\n${details.tags.map(t => `- ${t}`).join('\n')}\n\n`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 追加到文件
|
|
207
|
+
fs.appendFileSync(decisionsPath, section, 'utf-8');
|
|
208
|
+
|
|
209
|
+
// 如果有主题,也添加到主题文件
|
|
210
|
+
if (details.tags && details.tags.length > 0) {
|
|
211
|
+
const topicDir = path.join(PROJECT_DIR, '.claude', 'decisions', 'by-topic');
|
|
212
|
+
try { fs.mkdirSync(topicDir, { recursive: true }); } catch (e) {}
|
|
213
|
+
|
|
214
|
+
details.tags.forEach(tag => {
|
|
215
|
+
const topicPath = path.join(topicDir, `${tag}.md`);
|
|
216
|
+
const topicEntry = `\n- [${decisionId}](${getRelativePath(decisionsPath, topicPath)}#${timestamp}) - ${title}\n`;
|
|
217
|
+
fs.appendFileSync(topicPath, topicEntry, 'utf-8');
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return decisionId;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 获取相对路径
|
|
226
|
+
*/
|
|
227
|
+
function getRelativePath(from, to) {
|
|
228
|
+
return path.relative(path.dirname(from), to);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 从环境变量读取并记录
|
|
233
|
+
*/
|
|
234
|
+
function recordFromEnv() {
|
|
235
|
+
const eventType = process.env.CLAUDE_EVENT_TYPE || '';
|
|
236
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT || '';
|
|
237
|
+
|
|
238
|
+
if (eventType === 'UserPromptSubmit' && toolInput) {
|
|
239
|
+
recordUserMessage(toolInput);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 主函数
|
|
244
|
+
function main() {
|
|
245
|
+
const args = process.argv.slice(2);
|
|
246
|
+
const command = args[0];
|
|
247
|
+
|
|
248
|
+
switch (command) {
|
|
249
|
+
case 'user':
|
|
250
|
+
// 记录用户消息
|
|
251
|
+
const userContent = args.slice(1).join(' ') || '';
|
|
252
|
+
if (userContent) {
|
|
253
|
+
const { sessionId, sessionNum } = recordUserMessage(userContent);
|
|
254
|
+
console.log(`✅ 记录用户消息 - Session ${sessionNum} (${sessionId})`);
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
case 'assistant':
|
|
259
|
+
// 记录 AI 回复
|
|
260
|
+
const assistantContent = args.slice(1).join(' ') || '';
|
|
261
|
+
if (assistantContent) {
|
|
262
|
+
recordAssistantMessage(assistantContent);
|
|
263
|
+
console.log('✅ 记录 AI 回复');
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
|
|
267
|
+
case 'decision':
|
|
268
|
+
// 记录决策
|
|
269
|
+
// 用法: node conversation-recorder.cjs decision "标题" "理由" "内容"
|
|
270
|
+
const title = args[1] || '';
|
|
271
|
+
const reason = args[2] || '';
|
|
272
|
+
const content = args[3] || '';
|
|
273
|
+
if (title) {
|
|
274
|
+
const id = recordDecision(title, { reason, content });
|
|
275
|
+
console.log(`✅ 记录决策 - ${id}: ${title}`);
|
|
276
|
+
}
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case 'today':
|
|
280
|
+
// 查看今日对话
|
|
281
|
+
const todayPath = getTodayTranscriptPath();
|
|
282
|
+
if (fs.existsSync(todayPath)) {
|
|
283
|
+
console.log(fs.readFileSync(todayPath, 'utf-8'));
|
|
284
|
+
} else {
|
|
285
|
+
console.log('📭 今日暂无对话记录');
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
case 'list':
|
|
290
|
+
// 列出所有 transcript
|
|
291
|
+
listTranscripts();
|
|
292
|
+
break;
|
|
293
|
+
|
|
294
|
+
default:
|
|
295
|
+
// 从环境变量读取并记录
|
|
296
|
+
recordFromEnv();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 列出所有 transcript
|
|
302
|
+
*/
|
|
303
|
+
function listTranscripts() {
|
|
304
|
+
function listDir(dir, prefix = '') {
|
|
305
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
306
|
+
entries.forEach(entry => {
|
|
307
|
+
const fullPath = path.join(dir, entry.name);
|
|
308
|
+
if (entry.isDirectory()) {
|
|
309
|
+
console.log(`${prefix}📁 ${entry.name}/`);
|
|
310
|
+
listDir(fullPath, prefix + ' ');
|
|
311
|
+
} else if (entry.name.endsWith('.md')) {
|
|
312
|
+
const stat = fs.statSync(fullPath);
|
|
313
|
+
const size = (stat.size / 1024).toFixed(1);
|
|
314
|
+
console.log(`${prefix}📄 ${entry.name} (${size}KB)`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (fs.existsSync(TRANSCRIPTS_DIR)) {
|
|
320
|
+
console.log('\n📂 Conversation Transcripts:\n');
|
|
321
|
+
listDir(TRANSCRIPTS_DIR);
|
|
322
|
+
} else {
|
|
323
|
+
console.log('📭 暂无对话记录');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// 导出
|
|
328
|
+
module.exports = {
|
|
329
|
+
recordUserMessage,
|
|
330
|
+
recordAssistantMessage,
|
|
331
|
+
recordToolUse,
|
|
332
|
+
recordDecision,
|
|
333
|
+
getTodayTranscriptPath,
|
|
334
|
+
getSessionId
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// 直接运行
|
|
338
|
+
if (require.main === module) {
|
|
339
|
+
main();
|
|
340
|
+
}
|