sumulige-claude 1.0.11 → 1.1.1
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/commands/todos.md +41 -6
- package/.claude/hooks/pre-commit.cjs +86 -0
- package/.claude/hooks/pre-push.cjs +103 -0
- package/.claude/hooks/session-restore.cjs +102 -0
- package/.claude/hooks/session-save.cjs +164 -0
- package/.claude/hooks/todo-manager.cjs +262 -141
- package/.claude/quality-gate.json +61 -0
- package/.claude/settings.local.json +12 -1
- package/.claude/skills/api-tester/SKILL.md +52 -23
- package/.claude/skills/test-workflow/SKILL.md +191 -0
- package/.claude/templates/tasks/develop.md +69 -0
- package/.claude/templates/tasks/research.md +64 -0
- package/.claude/templates/tasks/test.md +96 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/.versionrc +25 -0
- package/AGENTS.md +7 -1
- package/CHANGELOG.md +83 -4
- package/PROJECT_STRUCTURE.md +40 -3
- package/Q&A.md +184 -0
- package/README.md +52 -2
- package/cli.js +102 -5
- package/config/official-skills.json +183 -0
- package/config/quality-gate.json +61 -0
- package/development/todos/.state.json +4 -0
- package/development/todos/INDEX.md +64 -38
- package/docs/RELEASE.md +93 -0
- package/lib/commands.js +1865 -39
- package/lib/config-manager.js +441 -0
- package/lib/config-schema.js +408 -0
- package/lib/config-validator.js +330 -0
- package/lib/config.js +52 -1
- package/lib/errors.js +305 -0
- package/lib/quality-gate.js +431 -0
- package/lib/quality-rules.js +373 -0
- package/lib/utils.js +102 -14
- package/lib/version-check.js +169 -0
- package/package.json +11 -2
- package/template/.claude/hooks/project-kickoff.cjs +190 -1
- package/template/.claude/hooks/session-restore.cjs +102 -0
- package/template/.claude/hooks/session-save.cjs +164 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sumulige-claude",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "The Best Agent Harness for Claude Code",
|
|
5
5
|
"main": "cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
"postinstall": "node cli.js init",
|
|
15
15
|
"sync": "node scripts/sync-external.mjs",
|
|
16
16
|
"update-registry": "node scripts/update-registry.mjs",
|
|
17
|
-
"sync:all": "npm run sync && npm run update-registry"
|
|
17
|
+
"sync:all": "npm run sync && npm run update-registry",
|
|
18
|
+
"release": "standard-version",
|
|
19
|
+
"release:patch": "standard-version --release-as patch",
|
|
20
|
+
"release:minor": "standard-version --release-as minor",
|
|
21
|
+
"release:major": "standard-version --release-as major"
|
|
18
22
|
},
|
|
19
23
|
"keywords": [
|
|
20
24
|
"claude",
|
|
@@ -36,11 +40,16 @@
|
|
|
36
40
|
"engines": {
|
|
37
41
|
"node": ">=16.0.0"
|
|
38
42
|
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ajv": "^8.17.1",
|
|
45
|
+
"ajv-formats": "^3.0.1"
|
|
46
|
+
},
|
|
39
47
|
"devDependencies": {
|
|
40
48
|
"jest": "^30.2.0",
|
|
41
49
|
"mock-fs": "^5.5.0",
|
|
42
50
|
"prettier": "^3.7.4",
|
|
43
51
|
"sinon": "^21.0.1",
|
|
52
|
+
"standard-version": "^9.5.0",
|
|
44
53
|
"yaml": "^2.8.2"
|
|
45
54
|
}
|
|
46
55
|
}
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* 功能:
|
|
10
10
|
* - 检查项目是否已启动
|
|
11
11
|
* - 如果未启动,提示 AI 进行项目规划
|
|
12
|
+
* - 生成规划文档后,自动创建任务到 backlog/
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
const fs = require('fs');
|
|
15
16
|
const path = require('path');
|
|
17
|
+
const { execSync } = require('child_process');
|
|
16
18
|
|
|
17
19
|
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
18
20
|
const TEMPLATES_DIR = path.join(PROJECT_DIR, '.claude/templates');
|
|
@@ -20,6 +22,15 @@ const KICKOFF_FILE = path.join(PROJECT_DIR, 'PROJECT_KICKOFF.md');
|
|
|
20
22
|
const PLAN_FILE = path.join(PROJECT_DIR, 'TASK_PLAN.md');
|
|
21
23
|
const PROPOSAL_FILE = path.join(PROJECT_DIR, 'PROJECT_PROPOSAL.md');
|
|
22
24
|
const HINT_FILE = path.join(PROJECT_DIR, '.claude/.kickoff-hint.txt');
|
|
25
|
+
const TODOS_DIR = path.join(PROJECT_DIR, 'development/todos');
|
|
26
|
+
const BACKLOG_DIR = path.join(TODOS_DIR, 'backlog');
|
|
27
|
+
|
|
28
|
+
// 任务类型图标
|
|
29
|
+
const TASK_TYPES = {
|
|
30
|
+
research: { icon: '📊', dir: 'research', name: 'Research' },
|
|
31
|
+
develop: { icon: '💻', dir: 'develop', name: 'Develop' },
|
|
32
|
+
test: { icon: '🧪', dir: 'test', name: 'Test' }
|
|
33
|
+
};
|
|
23
34
|
|
|
24
35
|
// 检查项目是否已启动
|
|
25
36
|
function isProjectStarted() {
|
|
@@ -35,6 +46,169 @@ function isForceKickoff() {
|
|
|
35
46
|
toolInput.includes('重新规划');
|
|
36
47
|
}
|
|
37
48
|
|
|
49
|
+
// 检查任务规划文档是否存在
|
|
50
|
+
function hasTaskPlan() {
|
|
51
|
+
return fs.existsSync(PLAN_FILE);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 解析 TASK_PLAN.md 中的任务
|
|
55
|
+
function parseTasksFromPlan() {
|
|
56
|
+
if (!fs.existsSync(PLAN_FILE)) {
|
|
57
|
+
return { research: [], develop: [], test: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const content = fs.readFileSync(PLAN_FILE, 'utf-8');
|
|
61
|
+
const tasks = { research: [], develop: [], test: [] };
|
|
62
|
+
|
|
63
|
+
// 匹配任务项 - 支持 - [ ] 和 - [x] 格式
|
|
64
|
+
const taskRegex = /-\s*\[[ x]?\]\s*(?:\[([P0-3])\])?\s*(.+)/g;
|
|
65
|
+
let match;
|
|
66
|
+
|
|
67
|
+
// 当前任务类型上下文
|
|
68
|
+
let currentType = 'develop'; // 默认为开发任务
|
|
69
|
+
|
|
70
|
+
// 检测章节标题来确定任务类型
|
|
71
|
+
const lines = content.split('\n');
|
|
72
|
+
lines.forEach(line => {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
|
|
75
|
+
// 检测章节标题
|
|
76
|
+
if (trimmed.includes('研究') || trimmed.includes('Research') || trimmed.includes('调研')) {
|
|
77
|
+
currentType = 'research';
|
|
78
|
+
} else if (trimmed.includes('开发') || trimmed.includes('Develop') || trimmed.includes('实现')) {
|
|
79
|
+
currentType = 'develop';
|
|
80
|
+
} else if (trimmed.includes('测试') || trimmed.includes('Test') || trimmed.includes('验证')) {
|
|
81
|
+
currentType = 'test';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 匹配任务项
|
|
85
|
+
const taskMatch = trimmed.match(/^-\s*\[([ x])\]\s*(?:\[([P0-3])\])?\s*(.+)/);
|
|
86
|
+
if (taskMatch) {
|
|
87
|
+
const [, checked, priority, title] = taskMatch;
|
|
88
|
+
tasks[currentType].push({
|
|
89
|
+
title: title.trim(),
|
|
90
|
+
priority: priority || 'P2',
|
|
91
|
+
checked: checked === 'x'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return tasks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 创建任务文件
|
|
100
|
+
function createTaskFile(task, type, index) {
|
|
101
|
+
const typeConfig = TASK_TYPES[type];
|
|
102
|
+
const slug = titleToSlug(task.title);
|
|
103
|
+
const fileName = `${index}-${slug}.md`;
|
|
104
|
+
const filePath = path.join(BACKLOG_DIR, typeConfig.dir, fileName);
|
|
105
|
+
const now = new Date().toISOString().split('T')[0];
|
|
106
|
+
|
|
107
|
+
// 读取对应模板
|
|
108
|
+
const templatePath = path.join(__dirname, '../../../development/todos/_templates', `${typeConfig.dir}.md`);
|
|
109
|
+
let template = '';
|
|
110
|
+
|
|
111
|
+
if (fs.existsSync(templatePath)) {
|
|
112
|
+
template = fs.readFileSync(templatePath, 'utf-8');
|
|
113
|
+
} else {
|
|
114
|
+
// 简化模板
|
|
115
|
+
template = `# ${task.title}
|
|
116
|
+
|
|
117
|
+
> **类型**: ${typeConfig.icon} ${typeConfig.name} | ${getTypeDescription(type)}
|
|
118
|
+
> **状态**: 待规划
|
|
119
|
+
> **优先级**: ${task.priority}
|
|
120
|
+
> **创建时间**: ${now}
|
|
121
|
+
> **来源**: PROJECT_KICKOFF
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 📋 任务描述
|
|
126
|
+
|
|
127
|
+
${task.title}
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🎯 验收标准
|
|
132
|
+
|
|
133
|
+
- [ ] 验收标准 1
|
|
134
|
+
- [ ] 验收标准 2
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 📝 备注
|
|
139
|
+
|
|
140
|
+
来自项目启动规划。
|
|
141
|
+
`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 创建目录
|
|
145
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
146
|
+
|
|
147
|
+
// 写入任务文件
|
|
148
|
+
fs.writeFileSync(filePath, template);
|
|
149
|
+
|
|
150
|
+
return fileName;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 将标题转换为文件名友好的 slug
|
|
154
|
+
function titleToSlug(title) {
|
|
155
|
+
return title
|
|
156
|
+
.toLowerCase()
|
|
157
|
+
.replace(/[^\u4e00-\u9fa5a-z0-9]+/g, '-')
|
|
158
|
+
.replace(/^-+|-+$/g, '')
|
|
159
|
+
.substring(0, 50);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 获取任务类型描述
|
|
163
|
+
function getTypeDescription(type) {
|
|
164
|
+
const descriptions = {
|
|
165
|
+
research: '调研/设计/探索',
|
|
166
|
+
develop: '实现/编码/重构',
|
|
167
|
+
test: '测试/验证/QA'
|
|
168
|
+
};
|
|
169
|
+
return descriptions[type] || '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 从规划创建任务到 backlog
|
|
173
|
+
function createTasksFromPlan() {
|
|
174
|
+
const tasks = parseTasksFromPlan();
|
|
175
|
+
let createdCount = 0;
|
|
176
|
+
|
|
177
|
+
// 确保目录存在
|
|
178
|
+
Object.values(TASK_TYPES).forEach(type => {
|
|
179
|
+
fs.mkdirSync(path.join(BACKLOG_DIR, type.dir), { recursive: true });
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 创建各类任务
|
|
183
|
+
Object.entries(tasks).forEach(([type, taskList]) => {
|
|
184
|
+
taskList.forEach((task, index) => {
|
|
185
|
+
if (!task.checked) { // 跳过已完成任务
|
|
186
|
+
const fileName = createTaskFile(task, type, index + 1);
|
|
187
|
+
createdCount++;
|
|
188
|
+
console.log(` ✅ 创建 ${type} 任务: ${fileName}`);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return createdCount;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 刷新任务索引
|
|
197
|
+
function refreshTaskIndex() {
|
|
198
|
+
const todoManagerPath = path.join(PROJECT_DIR, '.claude/hooks/todo-manager.cjs');
|
|
199
|
+
if (fs.existsSync(todoManagerPath)) {
|
|
200
|
+
try {
|
|
201
|
+
execSync(`node "${todoManagerPath}" --force`, {
|
|
202
|
+
cwd: PROJECT_DIR,
|
|
203
|
+
stdio: 'pipe'
|
|
204
|
+
});
|
|
205
|
+
console.log(' ✅ 任务索引已更新');
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// 忽略错误
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
38
212
|
// 生成启动提示
|
|
39
213
|
function generateKickoffHint() {
|
|
40
214
|
const now = new Date().toISOString().split('T')[0];
|
|
@@ -83,7 +257,8 @@ function generateKickoffHint() {
|
|
|
83
257
|
✅ 生成 PROJECT_KICKOFF.md
|
|
84
258
|
✅ 生成 TASK_PLAN.md
|
|
85
259
|
✅ 生成 PROJECT_PROPOSAL.md
|
|
86
|
-
✅
|
|
260
|
+
✅ 🆕 自动创建任务到 development/todos/backlog/
|
|
261
|
+
✅ 🆕 刷新任务索引
|
|
87
262
|
|
|
88
263
|
═══════════════════════════════════════════════════════════════════════
|
|
89
264
|
|
|
@@ -100,6 +275,20 @@ function main() {
|
|
|
100
275
|
process.exit(0);
|
|
101
276
|
}
|
|
102
277
|
|
|
278
|
+
// 如果已经有任务规划,自动创建任务
|
|
279
|
+
if (hasTaskPlan()) {
|
|
280
|
+
console.log('📋 检测到 TASK_PLAN.md,正在创建任务...');
|
|
281
|
+
|
|
282
|
+
const createdCount = createTasksFromPlan();
|
|
283
|
+
|
|
284
|
+
if (createdCount > 0) {
|
|
285
|
+
console.log(`✅ 已创建 ${createdCount} 个任务到 backlog/`);
|
|
286
|
+
refreshTaskIndex();
|
|
287
|
+
} else {
|
|
288
|
+
console.log('ℹ️ 所有任务已完成或为空');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
103
292
|
// 生成提示文件
|
|
104
293
|
const hint = generateKickoffHint();
|
|
105
294
|
fs.mkdirSync(path.dirname(HINT_FILE), { recursive: true });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session Restore Hook - Restore conversation context from file
|
|
4
|
+
*
|
|
5
|
+
* Provides context continuity across sessions
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const PROJECT_DIR = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
12
|
+
const SESSIONS_DIR = path.join(PROJECT_DIR, '.claude', 'sessions');
|
|
13
|
+
const MEMORY_FILE = path.join(PROJECT_DIR, '.claude', 'MEMORY.md');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get latest session
|
|
17
|
+
*/
|
|
18
|
+
function getLatestSession() {
|
|
19
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const files = fs.readdirSync(SESSIONS_DIR)
|
|
24
|
+
.filter(f => f.startsWith('session_') && f.endsWith('.md'))
|
|
25
|
+
.sort()
|
|
26
|
+
.reverse();
|
|
27
|
+
|
|
28
|
+
if (files.length === 0) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const latestFile = files[0];
|
|
33
|
+
const filepath = path.join(SESSIONS_DIR, latestFile);
|
|
34
|
+
const content = fs.readFileSync(filepath, 'utf-8');
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
file: latestFile,
|
|
38
|
+
path: filepath,
|
|
39
|
+
content: content
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get recent memory entries
|
|
45
|
+
*/
|
|
46
|
+
function getRecentMemory(days = 7) {
|
|
47
|
+
if (!fs.existsSync(MEMORY_FILE)) {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const content = fs.readFileSync(MEMORY_FILE, 'utf-8');
|
|
52
|
+
const entries = content.split('## ').slice(1, days + 1);
|
|
53
|
+
|
|
54
|
+
return entries.join('## ');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format context summary for display
|
|
59
|
+
*/
|
|
60
|
+
function formatContextSummary(latestSession, recentMemory) {
|
|
61
|
+
let summary = '';
|
|
62
|
+
|
|
63
|
+
if (latestSession) {
|
|
64
|
+
summary += `\n📁 Last Session: ${latestSession.file}\n`;
|
|
65
|
+
summary += ` Date: ${fs.statSync(latestSession.path).mtime.toLocaleString()}\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const memoryEntries = recentMemory.split('\n').filter(l => l.trim()).length;
|
|
69
|
+
if (memoryEntries > 0) {
|
|
70
|
+
summary += `\n💾 Memory Entries: ${memoryEntries}\n`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return summary;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Main execution
|
|
77
|
+
if (require.main === module) {
|
|
78
|
+
const args = process.argv.slice(2);
|
|
79
|
+
|
|
80
|
+
if (args[0] === '--latest') {
|
|
81
|
+
const session = getLatestSession();
|
|
82
|
+
if (session) {
|
|
83
|
+
console.log(session.content);
|
|
84
|
+
} else {
|
|
85
|
+
console.log('No sessions found.');
|
|
86
|
+
}
|
|
87
|
+
} else if (args[0] === '--memory') {
|
|
88
|
+
const memory = getRecentMemory(parseInt(args[1]) || 7);
|
|
89
|
+
console.log('# Recent Memory\n');
|
|
90
|
+
console.log(memory);
|
|
91
|
+
} else if (args[0] === '--summary') {
|
|
92
|
+
const session = getLatestSession();
|
|
93
|
+
const memory = getRecentMemory();
|
|
94
|
+
console.log(formatContextSummary(session, memory));
|
|
95
|
+
} else {
|
|
96
|
+
console.log('Usage: node session-restore.cjs [--latest|--memory|--summary]');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
exports.getLatestSession = getLatestSession;
|
|
101
|
+
exports.getRecentMemory = getRecentMemory;
|
|
102
|
+
exports.formatContextSummary = formatContextSummary;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Session Save Hook - Save conversation context to file
|
|
4
|
+
*
|
|
5
|
+
* Triggered after each conversation turn
|
|
6
|
+
* Saves conversation history, context, and state
|
|
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 SESSIONS_DIR = path.join(PROJECT_DIR, '.claude', 'sessions');
|
|
14
|
+
const MEMORY_FILE = path.join(PROJECT_DIR, '.claude', 'MEMORY.md');
|
|
15
|
+
|
|
16
|
+
// Ensure sessions directory exists
|
|
17
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
18
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate session filename
|
|
23
|
+
*/
|
|
24
|
+
function getSessionFilename() {
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const date = now.toISOString().split('T')[0];
|
|
27
|
+
const time = now.toTimeString().split(' ')[0].replace(/:/g, '-');
|
|
28
|
+
return `session_${date}_${time}.md`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save session context
|
|
33
|
+
*/
|
|
34
|
+
function saveSession(context) {
|
|
35
|
+
const filename = getSessionFilename();
|
|
36
|
+
const filepath = path.join(SESSIONS_DIR, filename);
|
|
37
|
+
|
|
38
|
+
const content = `# Session - ${new Date().toISOString()}
|
|
39
|
+
|
|
40
|
+
> Type: ${context.type || 'chat'}
|
|
41
|
+
> Model: ${context.model || 'unknown'}
|
|
42
|
+
> Duration: ${context.duration || 'unknown'}
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Summary
|
|
47
|
+
|
|
48
|
+
${context.summary || 'No summary provided'}
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Context
|
|
53
|
+
|
|
54
|
+
\`\`\`json
|
|
55
|
+
${JSON.stringify(context.metadata || {}, null, 2)}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Key Points
|
|
61
|
+
|
|
62
|
+
${(context.keyPoints || []).map((p, i) => `${i + 1}. ${p}`).join('\n')}
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Artifacts
|
|
67
|
+
|
|
68
|
+
${(context.artifacts || []).map(a => `- ${a}`).join('\n') || 'None'}
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Next Steps
|
|
73
|
+
|
|
74
|
+
${context.nextSteps || 'None'}
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
*Session saved at: ${new Date().toISOString()}*
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(filepath, content, 'utf-8');
|
|
82
|
+
|
|
83
|
+
// Update sessions index
|
|
84
|
+
updateSessionsIndex();
|
|
85
|
+
|
|
86
|
+
return filename;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Update sessions index
|
|
91
|
+
*/
|
|
92
|
+
function updateSessionsIndex() {
|
|
93
|
+
const files = fs.readdirSync(SESSIONS_DIR)
|
|
94
|
+
.filter(f => f.endsWith('.md'))
|
|
95
|
+
.sort()
|
|
96
|
+
.reverse();
|
|
97
|
+
|
|
98
|
+
const indexPath = path.join(SESSIONS_DIR, 'INDEX.md');
|
|
99
|
+
|
|
100
|
+
let content = `# Sessions Index
|
|
101
|
+
|
|
102
|
+
> Total sessions: ${files.length}
|
|
103
|
+
> Last updated: ${new Date().toISOString()}
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Recent Sessions
|
|
108
|
+
|
|
109
|
+
${files.slice(0, 20).map(f => {
|
|
110
|
+
const filepath = path.join(SESSIONS_DIR, f);
|
|
111
|
+
const stat = fs.statSync(filepath);
|
|
112
|
+
return `- [${f}](${f}) - ${stat.mtime.toISOString()}`;
|
|
113
|
+
}).join('\n')}
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(indexPath, content, 'utf-8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Update MEMORY.md with latest context
|
|
123
|
+
*/
|
|
124
|
+
function updateMemory(context) {
|
|
125
|
+
const timestamp = new Date().toISOString();
|
|
126
|
+
|
|
127
|
+
let content = '';
|
|
128
|
+
|
|
129
|
+
if (fs.existsSync(MEMORY_FILE)) {
|
|
130
|
+
content = fs.readFileSync(MEMORY_FILE, 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check if we need to add a new entry
|
|
134
|
+
const newEntry = `\n## ${timestamp.split('T')[0]}\n\n${
|
|
135
|
+
context.summary || context.keyPoints?.join('\n') || 'No details'
|
|
136
|
+
}\n`;
|
|
137
|
+
|
|
138
|
+
// Keep only last 7 days
|
|
139
|
+
const entries = content.split('## ').slice(1, 8);
|
|
140
|
+
content = '# Memory\n\n<!-- Project memory updated by AI -->\n' +
|
|
141
|
+
'## ' + entries.join('## ') + newEntry;
|
|
142
|
+
|
|
143
|
+
fs.writeFileSync(MEMORY_FILE, content, 'utf-8');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Main execution
|
|
147
|
+
if (require.main === module) {
|
|
148
|
+
const args = process.argv.slice(2);
|
|
149
|
+
|
|
150
|
+
if (args[0] === '--save' && args[1]) {
|
|
151
|
+
const contextData = JSON.parse(fs.readFileSync(args[1], 'utf-8'));
|
|
152
|
+
const filename = saveSession(contextData);
|
|
153
|
+
console.log(`✅ Session saved: ${filename}`);
|
|
154
|
+
|
|
155
|
+
if (contextData.addToMemory !== false) {
|
|
156
|
+
updateMemory(contextData);
|
|
157
|
+
console.log(`✅ Memory updated`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
exports.saveSession = saveSession;
|
|
163
|
+
exports.updateMemory = updateMemory;
|
|
164
|
+
exports.updateSessionsIndex = updateSessionsIndex;
|