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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Privacy Filter - 过滤对话中的敏感信息
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - 检测并过滤 API Key、Token 等敏感信息
|
|
6
|
+
* - 支持自定义过滤规则
|
|
7
|
+
* - 保留敏感信息的标记而非完全删除
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
// 敏感信息模式匹配
|
|
14
|
+
const SENSITIVE_PATTERNS = [
|
|
15
|
+
// API Keys (sk-开头,通常很长)
|
|
16
|
+
{
|
|
17
|
+
pattern: /sk-[a-zA-Z0-9_-]{20,}/g,
|
|
18
|
+
replace: 'sk-[REDACTED]',
|
|
19
|
+
name: 'OpenAI API Key'
|
|
20
|
+
},
|
|
21
|
+
// Bearer Tokens (JWT格式)
|
|
22
|
+
{
|
|
23
|
+
pattern: /Bearer\s+[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/gi,
|
|
24
|
+
replace: 'Bearer [REDACTED]',
|
|
25
|
+
name: 'Bearer Token'
|
|
26
|
+
},
|
|
27
|
+
// API Key/Token/Secret 配置格式
|
|
28
|
+
{
|
|
29
|
+
pattern: /(?:api[_-]?key|token|secret|password|passwd)["'']?\s*[:=]\s*["'']([a-zA-Z0-9_\-+=]{8,})["'']?/gi,
|
|
30
|
+
replace: (match) => {
|
|
31
|
+
const key = match.split(/[:=]/)[0].trim();
|
|
32
|
+
return `${key}: [REDACTED]`;
|
|
33
|
+
},
|
|
34
|
+
name: 'Config Key'
|
|
35
|
+
},
|
|
36
|
+
// URL 中的敏感参数
|
|
37
|
+
{
|
|
38
|
+
pattern: /([?&](key|token|api[_-]?key|secret|password|passwd)=)[a-zA-Z0-9_\-+=%]{8,}/gi,
|
|
39
|
+
replace: '$1[REDACTED]',
|
|
40
|
+
name: 'URL Param'
|
|
41
|
+
},
|
|
42
|
+
// JSON 中的敏感字段
|
|
43
|
+
{
|
|
44
|
+
pattern: /(["'](?:api[_-]?key|token|secret|password|passwd)["']\s*:\s*["'])([a-zA-Z0-9_\-+=]{8,})(["'])/gi,
|
|
45
|
+
replace: '$1[REDACTED]$3',
|
|
46
|
+
name: 'JSON Field'
|
|
47
|
+
},
|
|
48
|
+
// AWS Access Key
|
|
49
|
+
{
|
|
50
|
+
pattern: /AKIA[0-9A-Z]{16}/g,
|
|
51
|
+
replace: 'AKIA[REDACTED]',
|
|
52
|
+
name: 'AWS Access Key'
|
|
53
|
+
},
|
|
54
|
+
// GitHub Token
|
|
55
|
+
{
|
|
56
|
+
pattern: /ghp_[a-zA-Z0-9]{36,}/g,
|
|
57
|
+
replace: 'ghp_[REDACTED]',
|
|
58
|
+
name: 'GitHub Token'
|
|
59
|
+
},
|
|
60
|
+
// 信用卡号 (简单模式)
|
|
61
|
+
{
|
|
62
|
+
pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g,
|
|
63
|
+
replace: '[CARD-REDACTED]',
|
|
64
|
+
name: 'Credit Card'
|
|
65
|
+
},
|
|
66
|
+
// IP 地址 (可选)
|
|
67
|
+
{
|
|
68
|
+
pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
|
|
69
|
+
replace: '[IP-REDACTED]',
|
|
70
|
+
name: 'IP Address'
|
|
71
|
+
},
|
|
72
|
+
// 邮箱 (可选)
|
|
73
|
+
{
|
|
74
|
+
pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
75
|
+
replace: '[EMAIL-REDACTED]',
|
|
76
|
+
name: 'Email'
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 过滤文本中的敏感信息
|
|
82
|
+
* @param {string} text - 原始文本
|
|
83
|
+
* @param {Object} options - 选项
|
|
84
|
+
* @returns {Object} { filteredText, detectedItems }
|
|
85
|
+
*/
|
|
86
|
+
function filterSensitive(text, options = {}) {
|
|
87
|
+
const {
|
|
88
|
+
excludePatterns = [], // 排除的模式名称
|
|
89
|
+
includePatterns = [], // 只包含的模式名称
|
|
90
|
+
markOnly = false, // 只标记不过滤
|
|
91
|
+
} = options;
|
|
92
|
+
|
|
93
|
+
let filteredText = text;
|
|
94
|
+
const detectedItems = [];
|
|
95
|
+
|
|
96
|
+
// 确定要使用的模式
|
|
97
|
+
let patterns = SENSITIVE_PATTERNS;
|
|
98
|
+
if (includePatterns.length > 0) {
|
|
99
|
+
patterns = patterns.filter(p => includePatterns.includes(p.name));
|
|
100
|
+
}
|
|
101
|
+
if (excludePatterns.length > 0) {
|
|
102
|
+
patterns = patterns.filter(p => !excludePatterns.includes(p.name));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 应用每个模式
|
|
106
|
+
patterns.forEach(pattern => {
|
|
107
|
+
const matches = text.matchAll(pattern.pattern);
|
|
108
|
+
for (const match of matches) {
|
|
109
|
+
detectedItems.push({
|
|
110
|
+
type: pattern.name,
|
|
111
|
+
original: match[0],
|
|
112
|
+
position: match.index,
|
|
113
|
+
length: match[0].length
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (markOnly) {
|
|
118
|
+
// 只标记,不过滤
|
|
119
|
+
filteredText = filteredText.replace(pattern.pattern, `🚨${match[0]}🚨`);
|
|
120
|
+
} else {
|
|
121
|
+
// 过滤替换
|
|
122
|
+
if (typeof pattern.replace === 'string') {
|
|
123
|
+
filteredText = filteredText.replace(pattern.pattern, pattern.replace);
|
|
124
|
+
} else if (typeof pattern.replace === 'function') {
|
|
125
|
+
filteredText = filteredText.replace(pattern.pattern, pattern.replace);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
filteredText,
|
|
132
|
+
detectedItems,
|
|
133
|
+
hasSensitive: detectedItems.length > 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 检查文本是否包含敏感信息
|
|
139
|
+
*/
|
|
140
|
+
function hasSensitive(text) {
|
|
141
|
+
for (const pattern of SENSITIVE_PATTERNS) {
|
|
142
|
+
if (pattern.pattern.test(text)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 获取所有可用的模式名称
|
|
151
|
+
*/
|
|
152
|
+
function getPatternNames() {
|
|
153
|
+
return SENSITIVE_PATTERNS.map(p => p.name);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// CLI 使用
|
|
157
|
+
if (require.main === module) {
|
|
158
|
+
const args = process.argv.slice(2);
|
|
159
|
+
|
|
160
|
+
if (args[0] === '--check') {
|
|
161
|
+
// 检查文件
|
|
162
|
+
const filePath = args[1];
|
|
163
|
+
if (!filePath) {
|
|
164
|
+
console.error('Usage: node privacy-filter.js --check <file>');
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
168
|
+
const result = filterSensitive(content);
|
|
169
|
+
|
|
170
|
+
if (result.hasSensitive) {
|
|
171
|
+
console.log(`🚨 检测到 ${result.detectedItems.length} 处敏感信息:`);
|
|
172
|
+
result.detectedItems.forEach(item => {
|
|
173
|
+
console.log(` - [${item.type}] ${item.original.substring(0, 50)}...`);
|
|
174
|
+
});
|
|
175
|
+
process.exit(1);
|
|
176
|
+
} else {
|
|
177
|
+
console.log('✅ 未检测到敏感信息');
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
} else if (args[0] === '--filter') {
|
|
181
|
+
// 过滤文件
|
|
182
|
+
const filePath = args[1];
|
|
183
|
+
const outputPath = args[2] || filePath;
|
|
184
|
+
|
|
185
|
+
if (!filePath) {
|
|
186
|
+
console.error('Usage: node privacy-filter.js --filter <input> [output]');
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
191
|
+
const result = filterSensitive(content);
|
|
192
|
+
|
|
193
|
+
fs.writeFileSync(outputPath, result.filteredText, 'utf-8');
|
|
194
|
+
|
|
195
|
+
if (result.hasSensitive) {
|
|
196
|
+
console.log(`⚠️ 已过滤 ${result.detectedItems.length} 处敏感信息 → ${outputPath}`);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(`✅ 无敏感信息,已复制 → ${outputPath}`);
|
|
199
|
+
}
|
|
200
|
+
} else if (args[0] === '--patterns') {
|
|
201
|
+
// 列出所有模式
|
|
202
|
+
console.log('可用的过滤模式:');
|
|
203
|
+
getPatternNames().forEach(name => console.log(` - ${name}`));
|
|
204
|
+
} else {
|
|
205
|
+
console.log(`
|
|
206
|
+
Privacy Filter - 敏感信息过滤工具
|
|
207
|
+
|
|
208
|
+
用法:
|
|
209
|
+
node privacy-filter.js --check <file> 检查文件是否包含敏感信息
|
|
210
|
+
node privacy-filter.js --filter <input> [out] 过滤敏感信息
|
|
211
|
+
node privacy-filter.js --patterns 列出所有过滤模式
|
|
212
|
+
|
|
213
|
+
导出使用:
|
|
214
|
+
const { filterSensitive, hasSensitive } = require('./privacy-filter.js');
|
|
215
|
+
`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
filterSensitive,
|
|
221
|
+
hasSensitive,
|
|
222
|
+
getPatternNames,
|
|
223
|
+
SENSITIVE_PATTERNS
|
|
224
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Project Kickoff Hook - Manus-style project initialization
|
|
4
|
+
*
|
|
5
|
+
* 触发条件:
|
|
6
|
+
* - 检测到新项目 (缺少 PROJECT_KICKOFF.md)
|
|
7
|
+
* - 用户明确请求项目启动
|
|
8
|
+
*
|
|
9
|
+
* 功能:
|
|
10
|
+
* - 检查项目是否已启动
|
|
11
|
+
* - 如果未启动,提示 AI 进行项目规划
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
18
|
+
const TEMPLATES_DIR = path.join(PROJECT_DIR, '.claude/templates');
|
|
19
|
+
const KICKOFF_FILE = path.join(PROJECT_DIR, 'PROJECT_KICKOFF.md');
|
|
20
|
+
const PLAN_FILE = path.join(PROJECT_DIR, 'TASK_PLAN.md');
|
|
21
|
+
const PROPOSAL_FILE = path.join(PROJECT_DIR, 'PROJECT_PROPOSAL.md');
|
|
22
|
+
const HINT_FILE = path.join(PROJECT_DIR, '.claude/.kickoff-hint.txt');
|
|
23
|
+
|
|
24
|
+
// 检查项目是否已启动
|
|
25
|
+
function isProjectStarted() {
|
|
26
|
+
return fs.existsSync(KICKOFF_FILE);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 检查是否为强制启动模式
|
|
30
|
+
function isForceKickoff() {
|
|
31
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT || '';
|
|
32
|
+
return toolInput.includes('kickoff') ||
|
|
33
|
+
toolInput.includes('项目启动') ||
|
|
34
|
+
toolInput.includes('project plan') ||
|
|
35
|
+
toolInput.includes('重新规划');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 生成启动提示
|
|
39
|
+
function generateKickoffHint() {
|
|
40
|
+
const now = new Date().toISOString().split('T')[0];
|
|
41
|
+
|
|
42
|
+
return `
|
|
43
|
+
╔══════════════════════════════════════════════════════════════════════╗
|
|
44
|
+
║ 🚀 项目启动检测 (Project Kickoff) ║
|
|
45
|
+
╚══════════════════════════════════════════════════════════════════════╝
|
|
46
|
+
|
|
47
|
+
检测到此项目尚未完成启动流程。
|
|
48
|
+
|
|
49
|
+
根据 Manus 风格的 AI 2.0 开发范式,在开始编码前,我们需要:
|
|
50
|
+
|
|
51
|
+
═══════════════════════════════════════════════════════════════════════
|
|
52
|
+
|
|
53
|
+
📋 第一步: 项目启动清单 (PROJECT_KICKOFF.md)
|
|
54
|
+
├── 定义项目目标和成功标准
|
|
55
|
+
├── 明确技术约束和非技术约束
|
|
56
|
+
└── 划定 AI/Human 责任边界
|
|
57
|
+
|
|
58
|
+
📋 第二步: 任务执行计划 (TASK_PLAN.md)
|
|
59
|
+
├── 任务分解 (WBS)
|
|
60
|
+
├── 依赖关系分析
|
|
61
|
+
├── Agent 分配策略
|
|
62
|
+
└── 检查点设置
|
|
63
|
+
|
|
64
|
+
📋 第三步: 项目计划书 (PROJECT_PROPOSAL.md)
|
|
65
|
+
├── 技术架构设计
|
|
66
|
+
├── 功能需求分析
|
|
67
|
+
├── 开发迭代规划
|
|
68
|
+
└── 风险评估
|
|
69
|
+
|
|
70
|
+
═══════════════════════════════════════════════════════════════════════
|
|
71
|
+
|
|
72
|
+
🎯 下一步行动:
|
|
73
|
+
|
|
74
|
+
请回答以下问题,我将为你生成完整的项目规划:
|
|
75
|
+
|
|
76
|
+
1. 项目名称是什么?
|
|
77
|
+
2. 用一句话描述这个项目要解决什么问题?
|
|
78
|
+
3. 核心目标是什么?(成功标准是什么?)
|
|
79
|
+
4. 有什么技术约束或偏好?(语言/框架/部署等)
|
|
80
|
+
5. 预期的时间框架是怎样的?
|
|
81
|
+
|
|
82
|
+
回答这些问题后,我将:
|
|
83
|
+
✅ 生成 PROJECT_KICKOFF.md
|
|
84
|
+
✅ 生成 TASK_PLAN.md
|
|
85
|
+
✅ 生成 PROJECT_PROPOSAL.md
|
|
86
|
+
✅ 等待你的确认后开始执行
|
|
87
|
+
|
|
88
|
+
═══════════════════════════════════════════════════════════════════════
|
|
89
|
+
|
|
90
|
+
💡 提示: 你也可以直接说 "跳过启动" 使用传统开发模式
|
|
91
|
+
|
|
92
|
+
生成日期: ${now}
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 主函数
|
|
97
|
+
function main() {
|
|
98
|
+
// 如果项目已启动且不是强制模式,静默退出
|
|
99
|
+
if (isProjectStarted() && !isForceKickoff()) {
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 生成提示文件
|
|
104
|
+
const hint = generateKickoffHint();
|
|
105
|
+
fs.mkdirSync(path.dirname(HINT_FILE), { recursive: true });
|
|
106
|
+
fs.writeFileSync(HINT_FILE, hint);
|
|
107
|
+
|
|
108
|
+
// 同时输出到 stdout (供 AI 直接读取)
|
|
109
|
+
console.log(hint);
|
|
110
|
+
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main();
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* RAG Skill Loader - 动态技能感知加载器
|
|
4
|
+
*
|
|
5
|
+
* 基于用户任务内容,自动检测并加载相关技能
|
|
6
|
+
* 无感知运行,零打扰
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
13
|
+
const RAG_DIR = path.join(PROJECT_DIR, '.claude/rag');
|
|
14
|
+
const SKILL_INDEX_FILE = path.join(RAG_DIR, 'skill-index.json');
|
|
15
|
+
const SKILLS_DIR = path.join(PROJECT_DIR, '.claude/skills');
|
|
16
|
+
|
|
17
|
+
// 技能关键词匹配权重
|
|
18
|
+
const KEYWORD_WEIGHTS = {
|
|
19
|
+
exact: 1.0, // 完全匹配
|
|
20
|
+
partial: 0.8, // 部分匹配
|
|
21
|
+
related: 0.5 // 相关匹配
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// 加载技能索引
|
|
25
|
+
function loadSkillIndex() {
|
|
26
|
+
if (!fs.existsSync(SKILL_INDEX_FILE)) {
|
|
27
|
+
return { skills: [], auto_load: { enabled: false } };
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(SKILL_INDEX_FILE, 'utf-8'));
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { skills: [], auto_load: { enabled: false } };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 分析用户输入,提取关键词
|
|
37
|
+
function analyzeInput(input) {
|
|
38
|
+
const toolName = process.env.CLAUDE_TOOL_NAME || '';
|
|
39
|
+
const lowerInput = input.toLowerCase();
|
|
40
|
+
|
|
41
|
+
// 关键词提取
|
|
42
|
+
const keywords = [
|
|
43
|
+
// 前端相关
|
|
44
|
+
'frontend', 'ui', 'react', 'vue', 'component', 'design', 'styling', 'css', 'tailwind',
|
|
45
|
+
// 文档相关
|
|
46
|
+
'document', 'docx', 'word', 'pdf', 'ppt', 'slide', 'spreadsheet', 'excel', 'xlsx',
|
|
47
|
+
// 艺术相关
|
|
48
|
+
'art', 'design', 'creative', 'poster', 'generative', 'algorithmic', 'canvas',
|
|
49
|
+
// 测试相关
|
|
50
|
+
'test', 'testing', 'e2e', 'automation', 'browser',
|
|
51
|
+
// Agent 相关
|
|
52
|
+
'agent', 'orchestration', 'workflow', 'multi-agent',
|
|
53
|
+
// 工具相关
|
|
54
|
+
'mcp', 'api', 'server', 'integration', 'cli'
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const found = keywords.filter(k => lowerInput.includes(k));
|
|
58
|
+
return {
|
|
59
|
+
keywords: found,
|
|
60
|
+
toolName,
|
|
61
|
+
inputLength: input.length
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 匹配技能
|
|
66
|
+
function matchSkills(analysis, skillIndex) {
|
|
67
|
+
const { keywords, toolName } = analysis;
|
|
68
|
+
const matches = [];
|
|
69
|
+
|
|
70
|
+
for (const skill of skillIndex.skills) {
|
|
71
|
+
let score = 0;
|
|
72
|
+
let matchedKeywords = [];
|
|
73
|
+
|
|
74
|
+
for (const keyword of keywords) {
|
|
75
|
+
for (const skillKeyword of skill.keywords) {
|
|
76
|
+
if (skillKeyword === keyword) {
|
|
77
|
+
score += KEYWORD_WEIGHTS.exact;
|
|
78
|
+
matchedKeywords.push(keyword);
|
|
79
|
+
} else if (skillKeyword.includes(keyword) || keyword.includes(skillKeyword)) {
|
|
80
|
+
score += KEYWORD_WEIGHTS.partial;
|
|
81
|
+
matchedKeywords.push(keyword);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 工具名称匹配
|
|
87
|
+
if (toolName && skill.keywords.some(k => toolName.toLowerCase().includes(k))) {
|
|
88
|
+
score += KEYWORD_WEIGHTS.related;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (score > 0) {
|
|
92
|
+
matches.push({
|
|
93
|
+
name: skill.name,
|
|
94
|
+
score: score,
|
|
95
|
+
keywords: matchedKeywords,
|
|
96
|
+
description: skill.description
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 按分数排序
|
|
102
|
+
return matches.sort((a, b) => b.score - a.score);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 生成技能加载提示
|
|
106
|
+
function generateSkillLoadHint(matches) {
|
|
107
|
+
if (matches.length === 0) {
|
|
108
|
+
return '';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const topMatches = matches.slice(0, 3);
|
|
112
|
+
const skillNames = topMatches.map(m => m.name).join(', ');
|
|
113
|
+
|
|
114
|
+
return `
|
|
115
|
+
🧠 [RAG] 检测到相关技能: ${skillNames}
|
|
116
|
+
|
|
117
|
+
使用方式: openskills read <skill-name>
|
|
118
|
+
|
|
119
|
+
匹配详情:
|
|
120
|
+
${topMatches.map(m => ` - ${m.name}: ${(m.score).toFixed(1)} (${m.keywords.join(', ')})`).join('\n')}
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 主函数 - 完全静默,输出到文件
|
|
125
|
+
function main() {
|
|
126
|
+
try {
|
|
127
|
+
const skillIndex = loadSkillIndex();
|
|
128
|
+
if (!skillIndex.auto_load?.enabled) {
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const toolInput = process.env.CLAUDE_TOOL_INPUT || '';
|
|
133
|
+
if (!toolInput || toolInput.length < 10) {
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const analysis = analyzeInput(toolInput);
|
|
138
|
+
if (analysis.keywords.length === 0) {
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const matches = matchSkills(analysis, skillIndex);
|
|
143
|
+
if (matches.length === 0) {
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 将提示写入文件供 AI 阅读
|
|
148
|
+
const hint = generateSkillLoadHint(matches);
|
|
149
|
+
const hintFile = path.join(RAG_DIR, '.skill-hint.txt');
|
|
150
|
+
fs.writeFileSync(hintFile, hint + '\n');
|
|
151
|
+
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// 完全静默
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 会话结束 Hook - 自动生成对话摘要并同步到 PROJECT_LOG.md
|
|
3
|
+
#
|
|
4
|
+
# 用法:在 shell 中 source 此文件
|
|
5
|
+
# source .claude/hooks/session-end.sh
|
|
6
|
+
#
|
|
7
|
+
# 或者添加到 ~/.zshrc 或 ~/.bashrc:
|
|
8
|
+
# source /path/to/project/.claude/hooks/session-end.sh
|
|
9
|
+
|
|
10
|
+
# 检测是否在项目目录中
|
|
11
|
+
CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
12
|
+
|
|
13
|
+
# 会话数据文件
|
|
14
|
+
THINKING_DIR="$CLAUDE_PROJECT_DIR/.claude/thinking-routes"
|
|
15
|
+
ACTIVITY_FILE="$THINKING_DIR/.conversation-flow.json"
|
|
16
|
+
PROJECT_LOG="$CLAUDE_PROJECT_DIR/.claude/PROJECT_LOG.md"
|
|
17
|
+
|
|
18
|
+
# 生成会话摘要并追加到 PROJECT_LOG.md
|
|
19
|
+
save_session_summary() {
|
|
20
|
+
# 只在项目目录内执行
|
|
21
|
+
if [[ ! -f "$ACTIVITY_FILE" ]]; then
|
|
22
|
+
return
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
local today=$(date +%Y-%m-%d)
|
|
26
|
+
local time=$(date +%H:%M:%S)
|
|
27
|
+
local turn_count=0
|
|
28
|
+
|
|
29
|
+
# 读取对话流数据
|
|
30
|
+
if [[ -f "$ACTIVITY_FILE" ]]; then
|
|
31
|
+
turn_count=$(node -e "
|
|
32
|
+
const fs = require('fs');
|
|
33
|
+
try {
|
|
34
|
+
const flow = JSON.parse(fs.readFileSync('$ACTIVITY_FILE', 'utf-8'));
|
|
35
|
+
console.log(flow.turns ? flow.turns.length : 0);
|
|
36
|
+
} catch(e) {
|
|
37
|
+
console.log(0);
|
|
38
|
+
}
|
|
39
|
+
" 2>/dev/null || echo "0")
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# 只在有对话记录时保存
|
|
43
|
+
if [[ "$turn_count" -gt 0 ]]; then
|
|
44
|
+
# 追加到 PROJECT_LOG.md
|
|
45
|
+
cat >> "$PROJECT_LOG" << EOF
|
|
46
|
+
|
|
47
|
+
## $today $time - 会话结束
|
|
48
|
+
- 对话轮次: $turn_count
|
|
49
|
+
- 会话文件: .claude/thinking-routes/.conversation-flow.json
|
|
50
|
+
- 查看摘要: 运行 \`.claude/hooks/tl-summary.sh\`
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
EOF
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# 注册退出时的处理
|
|
58
|
+
trap 'save_session_summary' EXIT INT TERM
|
|
59
|
+
|
|
60
|
+
# 导出函数供手动调用
|
|
61
|
+
export -f save_session_summary
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 对话摘要同步脚本 - 将对话摘要追加到 PROJECT_LOG.md
|
|
3
|
+
# 用法: .claude/hooks/sync-to-log.sh
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# 自动检测项目目录
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
10
|
+
|
|
11
|
+
THINKING_DIR="$CLAUDE_PROJECT_DIR/.claude/thinking-routes"
|
|
12
|
+
ACTIVITY_FILE="$THINKING_DIR/.conversation-flow.json"
|
|
13
|
+
PROJECT_LOG="$CLAUDE_PROJECT_DIR/.claude/PROJECT_LOG.md"
|
|
14
|
+
SESSION_FILE="$THINKING_DIR/.current-session"
|
|
15
|
+
|
|
16
|
+
# 确保目录存在
|
|
17
|
+
mkdir -p "$THINKING_DIR"
|
|
18
|
+
|
|
19
|
+
# 获取对话数据
|
|
20
|
+
if [[ ! -f "$ACTIVITY_FILE" ]]; then
|
|
21
|
+
echo "❌ 没有对话记录"
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# 生成摘要
|
|
26
|
+
SUMMARY=$(node -e "
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const flow = JSON.parse(fs.readFileSync('$ACTIVITY_FILE', 'utf-8'));
|
|
29
|
+
|
|
30
|
+
const turns = flow.turns || [];
|
|
31
|
+
const today = new Date().toISOString().split('T')[0];
|
|
32
|
+
|
|
33
|
+
// 统计今天的对话
|
|
34
|
+
const todayTurns = turns.filter(t => t.time && t.time.startsWith(today));
|
|
35
|
+
|
|
36
|
+
// 统计工具使用
|
|
37
|
+
const toolCounts = {};
|
|
38
|
+
todayTurns.forEach(t => {
|
|
39
|
+
const tool = t.toolName || 'unknown';
|
|
40
|
+
toolCounts[tool] = (toolCounts[tool] || 0) + 1;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// 生成工具使用统计
|
|
44
|
+
const toolStats = Object.entries(toolCounts)
|
|
45
|
+
.sort((a, b) => b[1] - a[1])
|
|
46
|
+
.slice(0, 5)
|
|
47
|
+
.map(([tool, count]) => \` - \${tool}: \${count}\`)
|
|
48
|
+
.join('\\n');
|
|
49
|
+
|
|
50
|
+
// 获取最近活动
|
|
51
|
+
const recent = todayTurns.slice(-3).map(t => {
|
|
52
|
+
const time = t.time ? t.time.split('T')[1].split(':')[0] + ':' + t.time.split('T')[1].split(':')[1] : '??:??';
|
|
53
|
+
return \` - \${time} \${t.toolName || 'unknown'}\`;
|
|
54
|
+
}).join('\\n');
|
|
55
|
+
|
|
56
|
+
console.log('## ' + today + ' - 对话摘要');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('### 统计');
|
|
59
|
+
console.log('- 对话轮次: ' + todayTurns.length);
|
|
60
|
+
console.log('- 会话 ID: ' + (flow.sessionId || 'unknown'));
|
|
61
|
+
console.log('');
|
|
62
|
+
console.log('### 工具使用');
|
|
63
|
+
console.log(toolStats || ' (无)');
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log('### 最近活动');
|
|
66
|
+
console.log(recent || ' (无)');
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log('---');
|
|
69
|
+
" 2>/dev/null || echo "生成摘要失败")
|
|
70
|
+
|
|
71
|
+
# 追加到 PROJECT_LOG.md
|
|
72
|
+
if [[ -n "$SUMMARY" ]]; then
|
|
73
|
+
# 检查今天是否已有记录
|
|
74
|
+
today=$(date +%Y-%m-%d)
|
|
75
|
+
if grep -q "^## $today" "$PROJECT_LOG" 2>/dev/null; then
|
|
76
|
+
# 更新今天的记录(删除旧记录,追加新的)
|
|
77
|
+
# 简单处理:直接追加
|
|
78
|
+
echo "$SUMMARY" >> "$PROJECT_LOG"
|
|
79
|
+
else
|
|
80
|
+
echo "$SUMMARY" >> "$PROJECT_LOG"
|
|
81
|
+
fi
|
|
82
|
+
echo "✅ 已同步到 PROJECT_LOG.md"
|
|
83
|
+
fi
|