claude-session-continuity-mcp 1.12.1 → 1.13.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/README.md +2 -1
- package/dist/hooks/post-tool-use.js +47 -0
- package/dist/hooks/pre-compact.js +51 -0
- package/dist/hooks/session-end.js +31 -0
- package/dist/hooks/session-start.js +97 -59
- package/dist/index.js +167 -129
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# claude-session-continuity-mcp (v1.
|
|
1
|
+
# claude-session-continuity-mcp (v1.13.0)
|
|
2
2
|
|
|
3
3
|
> **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search + auto error→solution pipeline
|
|
4
4
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[]()
|
|
8
8
|
[]()
|
|
9
|
+
[](https://glama.ai/mcp/servers/leesgit/claude-session-continuity-mcp)
|
|
9
10
|
|
|
10
11
|
## The Problem
|
|
11
12
|
|
|
@@ -7,6 +7,47 @@
|
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
9
|
import Database from 'better-sqlite3';
|
|
10
|
+
// ===== Playwright 캐시 정리 (20MB 컨텍스트 초과 방지) =====
|
|
11
|
+
function cleanPlaywrightCache(cwd) {
|
|
12
|
+
const MAX_TOTAL = 3 * 1024 * 1024; // 3MB 초과 시 정리
|
|
13
|
+
const MAX_FILES = 20; // 최대 파일 수
|
|
14
|
+
// .playwright-mcp/ 정리
|
|
15
|
+
const playwrightDir = path.join(cwd, '.playwright-mcp');
|
|
16
|
+
if (fs.existsSync(playwrightDir)) {
|
|
17
|
+
try {
|
|
18
|
+
const files = fs.readdirSync(playwrightDir)
|
|
19
|
+
.map(f => ({ name: f, path: path.join(playwrightDir, f), stat: fs.statSync(path.join(playwrightDir, f)) }))
|
|
20
|
+
.sort((a, b) => a.stat.mtimeMs - b.stat.mtimeMs);
|
|
21
|
+
let totalSize = files.reduce((sum, f) => sum + f.stat.size, 0);
|
|
22
|
+
// 오래된 파일부터 삭제 (최근 5개만 유지)
|
|
23
|
+
while (files.length > 5 || totalSize > MAX_TOTAL) {
|
|
24
|
+
const oldest = files.shift();
|
|
25
|
+
if (!oldest)
|
|
26
|
+
break;
|
|
27
|
+
try {
|
|
28
|
+
fs.unlinkSync(oldest.path);
|
|
29
|
+
totalSize -= oldest.stat.size;
|
|
30
|
+
}
|
|
31
|
+
catch { /* ignore */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { /* ignore */ }
|
|
35
|
+
}
|
|
36
|
+
// 루트의 스크린샷 파일 정리 (최근 3개만 유지)
|
|
37
|
+
try {
|
|
38
|
+
const screenshots = fs.readdirSync(cwd)
|
|
39
|
+
.filter(f => /\.(png|jpeg|jpg)$/i.test(f))
|
|
40
|
+
.map(f => ({ name: f, path: path.join(cwd, f), stat: fs.statSync(path.join(cwd, f)) }))
|
|
41
|
+
.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs); // 최신순
|
|
42
|
+
for (let i = 3; i < screenshots.length; i++) {
|
|
43
|
+
try {
|
|
44
|
+
fs.unlinkSync(screenshots[i].path);
|
|
45
|
+
}
|
|
46
|
+
catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch { /* ignore */ }
|
|
50
|
+
}
|
|
10
51
|
// ===== 에러 감지 → 솔루션 자동 주입 =====
|
|
11
52
|
const ERROR_PATTERNS = [
|
|
12
53
|
/(?:error|Error|ERROR)\s*[:\[]/,
|
|
@@ -190,6 +231,12 @@ async function main() {
|
|
|
190
231
|
}
|
|
191
232
|
process.exit(0);
|
|
192
233
|
}
|
|
234
|
+
// Playwright 도구 사용 후 캐시 정리 (20MB 컨텍스트 초과 방지)
|
|
235
|
+
if (toolName.startsWith('mcp__playwright__')) {
|
|
236
|
+
const cwd = input.cwd || process.cwd();
|
|
237
|
+
cleanPlaywrightCache(cwd);
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
193
240
|
const TRACKED_TOOLS = ['Edit', 'Write', 'Read', 'Glob', 'Grep'];
|
|
194
241
|
const IGNORED_PATTERNS = ['node_modules', '.git/', 'dist/', 'build/', '.next/', 'coverage/', '.DS_Store'];
|
|
195
242
|
if (!TRACKED_TOOLS.includes(toolName)) {
|
|
@@ -143,6 +143,55 @@ function buildHandoverContext(transcript) {
|
|
|
143
143
|
context.recentErrors = context.recentErrors.slice(0, 3);
|
|
144
144
|
return context;
|
|
145
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Playwright 캐시 정리 - 오래된 스냅샷/로그 제거 (20MB 컨텍스트 초과 방지)
|
|
148
|
+
*/
|
|
149
|
+
function cleanPlaywrightCache(cwd) {
|
|
150
|
+
const playwrightDir = path.join(cwd, '.playwright-mcp');
|
|
151
|
+
if (!fs.existsSync(playwrightDir))
|
|
152
|
+
return;
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const MAX_AGE = 30 * 60 * 1000; // 30분 이상 된 파일 정리
|
|
155
|
+
const MAX_DIR_SIZE = 5 * 1024 * 1024; // 5MB 초과 시 정리
|
|
156
|
+
try {
|
|
157
|
+
const files = fs.readdirSync(playwrightDir);
|
|
158
|
+
let totalSize = 0;
|
|
159
|
+
const fileInfos = [];
|
|
160
|
+
for (const file of files) {
|
|
161
|
+
const filePath = path.join(playwrightDir, file);
|
|
162
|
+
const stat = fs.statSync(filePath);
|
|
163
|
+
totalSize += stat.size;
|
|
164
|
+
fileInfos.push({ name: file, mtime: stat.mtimeMs, size: stat.size });
|
|
165
|
+
}
|
|
166
|
+
if (totalSize < MAX_DIR_SIZE)
|
|
167
|
+
return; // 5MB 미만이면 정리 불필요
|
|
168
|
+
// 오래된 파일부터 삭제
|
|
169
|
+
fileInfos.sort((a, b) => a.mtime - b.mtime);
|
|
170
|
+
for (const fi of fileInfos) {
|
|
171
|
+
if (now - fi.mtime > MAX_AGE) {
|
|
172
|
+
fs.unlinkSync(path.join(playwrightDir, fi.name));
|
|
173
|
+
totalSize -= fi.size;
|
|
174
|
+
}
|
|
175
|
+
if (totalSize < MAX_DIR_SIZE)
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { /* ignore */ }
|
|
180
|
+
// 루트의 오래된 스크린샷 PNG/JPEG도 정리
|
|
181
|
+
try {
|
|
182
|
+
const rootFiles = fs.readdirSync(cwd);
|
|
183
|
+
for (const file of rootFiles) {
|
|
184
|
+
if (!/\.(png|jpeg|jpg)$/i.test(file))
|
|
185
|
+
continue;
|
|
186
|
+
const filePath = path.join(cwd, file);
|
|
187
|
+
const stat = fs.statSync(filePath);
|
|
188
|
+
if (now - stat.mtimeMs > MAX_AGE) {
|
|
189
|
+
fs.unlinkSync(filePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch { /* ignore */ }
|
|
194
|
+
}
|
|
146
195
|
async function main() {
|
|
147
196
|
try {
|
|
148
197
|
let inputData = '';
|
|
@@ -151,6 +200,8 @@ async function main() {
|
|
|
151
200
|
}
|
|
152
201
|
const input = inputData ? JSON.parse(inputData) : {};
|
|
153
202
|
const cwd = input.cwd || process.cwd();
|
|
203
|
+
// Playwright 캐시 정리 (20MB 컨텍스트 초과 방지)
|
|
204
|
+
cleanPlaywrightCache(cwd);
|
|
154
205
|
const project = detectProject(cwd);
|
|
155
206
|
const dbPath = getDbPath(cwd);
|
|
156
207
|
if (!fs.existsSync(dbPath)) {
|
|
@@ -567,6 +567,37 @@ async function main() {
|
|
|
567
567
|
}
|
|
568
568
|
catch { /* solutions table may not exist */ }
|
|
569
569
|
}
|
|
570
|
+
// architecture_decisions 자동 누적 (세션에서 추출된 결정사항 병합)
|
|
571
|
+
if (decisions.length > 0) {
|
|
572
|
+
try {
|
|
573
|
+
const existing = db.prepare('SELECT architecture_decisions FROM project_context WHERE project = ?').get(project);
|
|
574
|
+
let existingDecisions = [];
|
|
575
|
+
if (existing?.architecture_decisions) {
|
|
576
|
+
try {
|
|
577
|
+
existingDecisions = JSON.parse(existing.architecture_decisions);
|
|
578
|
+
}
|
|
579
|
+
catch { /* ignore */ }
|
|
580
|
+
}
|
|
581
|
+
// 중복 제거 후 병합 (최대 20개 유지)
|
|
582
|
+
const merged = [...new Set([...existingDecisions, ...decisions])].slice(-20);
|
|
583
|
+
db.prepare(`
|
|
584
|
+
INSERT INTO project_context (project, architecture_decisions)
|
|
585
|
+
VALUES (?, ?)
|
|
586
|
+
ON CONFLICT(project) DO UPDATE SET architecture_decisions = ?
|
|
587
|
+
`).run(project, JSON.stringify(merged), JSON.stringify(merged));
|
|
588
|
+
}
|
|
589
|
+
catch { /* project_context table may not exist */ }
|
|
590
|
+
}
|
|
591
|
+
// 세션 임베딩 사전 생성 (search_sessions 성능 최적화)
|
|
592
|
+
try {
|
|
593
|
+
const lastSession = db.prepare('SELECT id FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
594
|
+
if (lastSession && lastWork) {
|
|
595
|
+
// 간단한 임베딩은 동기적으로 시도하지 않고 DB에 표시만 남김
|
|
596
|
+
// MCP 서버의 generateEmbedding이 search 시 캐시 miss에서 lazy 생성
|
|
597
|
+
// session-end 훅은 transformers 모델 로드 오버헤드가 크므로 skip
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch { /* ignore */ }
|
|
570
601
|
db.close();
|
|
571
602
|
console.log(`[SessionEnd] Saved session for ${project}`);
|
|
572
603
|
console.log(` Last work: ${lastWork.slice(0, 80)}`);
|
|
@@ -62,6 +62,11 @@ function cleanupNoiseMemories(db) {
|
|
|
62
62
|
}
|
|
63
63
|
catch { /* ignore */ }
|
|
64
64
|
}
|
|
65
|
+
// 토큰 예산 시스템 (컨텍스트 무한 증가 방지)
|
|
66
|
+
const MAX_CONTEXT_TOKENS = parseInt(process.env.MCP_CONTEXT_BUDGET || '2000', 10);
|
|
67
|
+
function estimateTokens(text) {
|
|
68
|
+
return Math.ceil(text.length / 4); // 대략적 추정 (한글은 1.5~2배)
|
|
69
|
+
}
|
|
65
70
|
function loadContext(dbPath, project) {
|
|
66
71
|
if (!fs.existsSync(dbPath))
|
|
67
72
|
return null;
|
|
@@ -70,15 +75,19 @@ function loadContext(dbPath, project) {
|
|
|
70
75
|
// 노이즈 메모리 자동 정리
|
|
71
76
|
cleanupNoiseMemories(db);
|
|
72
77
|
const lines = [`# ${project} - Session Resumed\n`];
|
|
73
|
-
|
|
78
|
+
let tokenBudget = MAX_CONTEXT_TOKENS;
|
|
79
|
+
// [Priority 1] 현재 상태
|
|
74
80
|
const active = db.prepare('SELECT current_state, blockers FROM active_context WHERE project = ?').get(project);
|
|
75
81
|
if (active?.current_state) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const stateBlock = `📍 **State**: ${active.current_state}` + (active.blockers ? `\n🚧 **Blocker**: ${active.blockers}` : '');
|
|
83
|
+
const cost = estimateTokens(stateBlock);
|
|
84
|
+
if (tokenBudget > cost) {
|
|
85
|
+
lines.push(stateBlock);
|
|
86
|
+
lines.push('');
|
|
87
|
+
tokenBudget -= cost;
|
|
88
|
+
}
|
|
80
89
|
}
|
|
81
|
-
// 최근 3개 세션 (빈 세션 skip)
|
|
90
|
+
// [Priority 2] 최근 3개 세션 (빈 세션 skip)
|
|
82
91
|
const recentSessions = db.prepare(`
|
|
83
92
|
SELECT last_work, next_tasks, issues, timestamp FROM sessions
|
|
84
93
|
WHERE project = ?
|
|
@@ -89,61 +98,83 @@ function loadContext(dbPath, project) {
|
|
|
89
98
|
AND length(last_work) > 15
|
|
90
99
|
ORDER BY timestamp DESC LIMIT 3
|
|
91
100
|
`).all(project);
|
|
92
|
-
if (recentSessions.length > 0) {
|
|
93
|
-
|
|
101
|
+
if (recentSessions.length > 0 && tokenBudget > 100) {
|
|
102
|
+
const sessionLines = ['## Recent Sessions'];
|
|
94
103
|
for (const session of recentSessions) {
|
|
95
|
-
// last_work 60자 제한 (토큰 예산)
|
|
96
104
|
const work = session.last_work.length > 60 ? session.last_work.slice(0, 60) + '...' : session.last_work;
|
|
97
|
-
|
|
98
|
-
// 커밋 정보 (간결하게)
|
|
105
|
+
sessionLines.push(`- [${session.timestamp?.slice(0, 10) || '?'}] ${work}`);
|
|
99
106
|
if (session.issues) {
|
|
100
107
|
try {
|
|
101
108
|
const meta = JSON.parse(session.issues);
|
|
102
109
|
if (meta.commits?.length > 0) {
|
|
103
|
-
|
|
110
|
+
sessionLines.push(` commits: ${meta.commits.slice(0, 2).join('; ').slice(0, 80)}`);
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
catch { /* skip */ }
|
|
107
114
|
}
|
|
108
115
|
}
|
|
109
|
-
|
|
116
|
+
const cost = estimateTokens(sessionLines.join('\n'));
|
|
117
|
+
if (tokenBudget > cost) {
|
|
118
|
+
lines.push(...sessionLines, '');
|
|
119
|
+
tokenBudget -= cost;
|
|
120
|
+
}
|
|
110
121
|
}
|
|
111
|
-
// 사용자 지시사항
|
|
122
|
+
// [Priority 3] 사용자 지시사항 (가장 중요 - 예산 부족해도 high priority는 포함)
|
|
112
123
|
try {
|
|
113
124
|
const directives = db.prepare(`
|
|
114
125
|
SELECT directive, priority FROM user_directives
|
|
115
126
|
WHERE project = ? ORDER BY priority DESC, created_at DESC LIMIT 5
|
|
116
127
|
`).all(project);
|
|
117
128
|
if (directives.length > 0) {
|
|
118
|
-
|
|
129
|
+
const directiveLines = ['## Directives'];
|
|
119
130
|
for (const d of directives) {
|
|
120
131
|
const icon = d.priority === 'high' ? '🔴' : '📎';
|
|
121
|
-
|
|
132
|
+
directiveLines.push(`- ${icon} ${d.directive}`);
|
|
133
|
+
}
|
|
134
|
+
const cost = estimateTokens(directiveLines.join('\n'));
|
|
135
|
+
// 지시사항은 예산 초과해도 high priority는 포함
|
|
136
|
+
const highOnly = directives.filter(d => d.priority === 'high');
|
|
137
|
+
if (tokenBudget > cost) {
|
|
138
|
+
lines.push(...directiveLines, '');
|
|
139
|
+
tokenBudget -= cost;
|
|
140
|
+
}
|
|
141
|
+
else if (highOnly.length > 0) {
|
|
142
|
+
const criticalLines = ['## Directives'];
|
|
143
|
+
for (const d of highOnly)
|
|
144
|
+
criticalLines.push(`- 🔴 ${d.directive}`);
|
|
145
|
+
lines.push(...criticalLines, '');
|
|
146
|
+
tokenBudget -= estimateTokens(criticalLines.join('\n'));
|
|
122
147
|
}
|
|
123
|
-
lines.push('');
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
catch { /* table may not exist yet */ }
|
|
127
|
-
// 미완료 태스크
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
151
|
+
// [Priority 4] 미완료 태스크
|
|
152
|
+
if (tokenBudget > 50) {
|
|
153
|
+
try {
|
|
154
|
+
const tasks = db.prepare(`
|
|
155
|
+
SELECT title, priority, status FROM tasks
|
|
156
|
+
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
157
|
+
ORDER BY priority DESC LIMIT 5
|
|
158
|
+
`).all(project);
|
|
159
|
+
if (tasks.length > 0) {
|
|
160
|
+
const taskLines = ['## Pending Tasks'];
|
|
161
|
+
for (const t of tasks) {
|
|
162
|
+
const icon = t.status === 'in_progress' ? '🔄' : '⏳';
|
|
163
|
+
taskLines.push(`- ${icon} [P${t.priority}] ${t.title}`);
|
|
164
|
+
}
|
|
165
|
+
const cost = estimateTokens(taskLines.join('\n'));
|
|
166
|
+
if (tokenBudget > cost) {
|
|
167
|
+
lines.push(...taskLines, '');
|
|
168
|
+
tokenBudget -= cost;
|
|
169
|
+
}
|
|
139
170
|
}
|
|
140
|
-
lines.push('');
|
|
141
171
|
}
|
|
172
|
+
catch { /* table may not exist */ }
|
|
142
173
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
174
|
+
// [Priority 5] 중요 메모리 (temporal decay 적용, 예산 내에서)
|
|
175
|
+
if (tokenBudget > 80)
|
|
176
|
+
try {
|
|
177
|
+
const memories = db.prepare(`
|
|
147
178
|
SELECT content, memory_type, importance, created_at, access_count FROM memories
|
|
148
179
|
WHERE project = ?
|
|
149
180
|
AND memory_type IN ('decision', 'learning', 'error', 'preference')
|
|
@@ -152,36 +183,43 @@ function loadContext(dbPath, project) {
|
|
|
152
183
|
AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
|
|
153
184
|
ORDER BY importance DESC, accessed_at DESC LIMIT 20
|
|
154
185
|
`).all(project);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
if (memories.length > 0) {
|
|
187
|
+
// Decay 적용 후 top 5 선택
|
|
188
|
+
const DECAY_RATES = {
|
|
189
|
+
decision: 0.001, learning: 0.003, error: 0.01, preference: 0.002
|
|
190
|
+
};
|
|
191
|
+
const scored = memories.map(m => {
|
|
192
|
+
const ageDays = (Date.now() - new Date(m.created_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
193
|
+
const decayRate = DECAY_RATES[m.memory_type] ?? 0.005;
|
|
194
|
+
const score = m.importance * Math.exp(-decayRate * ageDays) * Math.log2(m.access_count + 2);
|
|
195
|
+
return { ...m, score };
|
|
196
|
+
}).sort((a, b) => b.score - a.score).slice(0, 5);
|
|
197
|
+
const typeIcons = {
|
|
198
|
+
decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
|
|
199
|
+
};
|
|
200
|
+
const memoryLines = ['## Key Memories'];
|
|
201
|
+
for (const m of scored) {
|
|
202
|
+
const icon = typeIcons[m.memory_type] || '💭';
|
|
203
|
+
const content = m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content;
|
|
204
|
+
memoryLines.push(`- ${icon} ${content}`);
|
|
205
|
+
}
|
|
206
|
+
const cost = estimateTokens(memoryLines.join('\n'));
|
|
207
|
+
if (tokenBudget > cost) {
|
|
208
|
+
lines.push(...memoryLines, '');
|
|
209
|
+
tokenBudget -= cost;
|
|
210
|
+
}
|
|
174
211
|
}
|
|
175
|
-
lines.push('');
|
|
176
212
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// 솔루션 통계 (1줄)
|
|
213
|
+
catch { /* ignore */ }
|
|
214
|
+
// [Priority 6] 솔루션 통계 (1줄, 저비용)
|
|
180
215
|
try {
|
|
181
216
|
const solCount = db.prepare('SELECT COUNT(*) as cnt FROM solutions WHERE project = ?').get(project)?.cnt || 0;
|
|
182
217
|
if (solCount > 0) {
|
|
183
|
-
|
|
184
|
-
|
|
218
|
+
const solLine = `\nSolutions: ${solCount} recorded (auto-injected on error)\n`;
|
|
219
|
+
if (tokenBudget > 10) {
|
|
220
|
+
lines.push(solLine);
|
|
221
|
+
tokenBudget -= estimateTokens(solLine);
|
|
222
|
+
}
|
|
185
223
|
}
|
|
186
224
|
}
|
|
187
225
|
catch { /* solutions table may not exist */ }
|
package/dist/index.js
CHANGED
|
@@ -22,11 +22,23 @@ import { mkdirSync, existsSync } from 'fs';
|
|
|
22
22
|
import * as path from 'path';
|
|
23
23
|
import { execSync } from 'child_process';
|
|
24
24
|
import Database from 'better-sqlite3';
|
|
25
|
-
// @
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// @xenova/transformers - 동적 import (sharp 의존성 문제 방지)
|
|
26
|
+
let transformersModule = null;
|
|
27
|
+
async function loadTransformers() {
|
|
28
|
+
if (transformersModule)
|
|
29
|
+
return transformersModule;
|
|
30
|
+
try {
|
|
31
|
+
// @ts-ignore - transformers.js
|
|
32
|
+
const mod = await import('@xenova/transformers');
|
|
33
|
+
mod.env.cacheDir = path.join(process.env.HOME || '/tmp', '.cache', 'transformers');
|
|
34
|
+
mod.env.allowLocalModels = true;
|
|
35
|
+
transformersModule = mod;
|
|
36
|
+
return mod;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
30
42
|
// 기본 경로 설정 (자동 감지)
|
|
31
43
|
function detectWorkspaceRoot() {
|
|
32
44
|
// 1. 환경변수가 설정되어 있으면 사용
|
|
@@ -234,7 +246,10 @@ async function initEmbedding() {
|
|
|
234
246
|
if (embeddingPipeline)
|
|
235
247
|
return;
|
|
236
248
|
try {
|
|
237
|
-
|
|
249
|
+
const mod = await loadTransformers();
|
|
250
|
+
if (!mod)
|
|
251
|
+
return;
|
|
252
|
+
embeddingPipeline = await mod.pipeline('feature-extraction', 'Xenova/multilingual-e5-small');
|
|
238
253
|
}
|
|
239
254
|
catch (error) {
|
|
240
255
|
console.error('Failed to load embedding model:', error);
|
|
@@ -278,6 +293,23 @@ function cosineSimilarity(a, b) {
|
|
|
278
293
|
}
|
|
279
294
|
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
280
295
|
}
|
|
296
|
+
// ===== 임베딩 저장 (재시도 포함) =====
|
|
297
|
+
async function storeEmbeddingWithRetry(db, entityType, entityId, text, retries = 2) {
|
|
298
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
299
|
+
try {
|
|
300
|
+
const embedding = await generateEmbedding(text, 'passage');
|
|
301
|
+
if (embedding) {
|
|
302
|
+
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
303
|
+
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)')
|
|
304
|
+
.run(entityType, entityId, buffer);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch { /* retry */ }
|
|
309
|
+
if (attempt < retries)
|
|
310
|
+
await new Promise(r => setTimeout(r, 1000 * (attempt + 1)));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
281
313
|
// ===== 유틸리티 함수 =====
|
|
282
314
|
async function fileExists(filePath) {
|
|
283
315
|
try {
|
|
@@ -379,54 +411,54 @@ const tools = [
|
|
|
379
411
|
// ===== 1. 세션/컨텍스트 (4개) =====
|
|
380
412
|
{
|
|
381
413
|
name: 'session_start',
|
|
382
|
-
description: '
|
|
414
|
+
description: 'Load project context at the beginning of a session. Typically auto-invoked by the SessionStart hook, but can be called manually. Returns the project\'s tech stack, recent activity, pending tasks, and active blockers as a compressed context payload (~650 tokens). Read-only — does not modify any state. Use this instead of project_status when you need the full session bootstrap context.',
|
|
383
415
|
inputSchema: {
|
|
384
416
|
type: 'object',
|
|
385
417
|
properties: {
|
|
386
|
-
project: { type: 'string', description: '
|
|
387
|
-
compact: { type: 'boolean', description: '
|
|
418
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
419
|
+
compact: { type: 'boolean', description: 'Return compressed format (default: true). Set false for verbose output.' }
|
|
388
420
|
},
|
|
389
421
|
required: ['project']
|
|
390
422
|
}
|
|
391
423
|
},
|
|
392
424
|
{
|
|
393
425
|
name: 'session_end',
|
|
394
|
-
description: '
|
|
426
|
+
description: 'Save the current session state before ending a conversation. Persists a summary, completed work, next steps, modified files, and blockers to SQLite. The saved state is automatically restored by session_start in the next session. Side effects: writes to the sessions table and updates the active_context record for the project. Idempotent — calling multiple times overwrites the previous session record.',
|
|
395
427
|
inputSchema: {
|
|
396
428
|
type: 'object',
|
|
397
429
|
properties: {
|
|
398
|
-
project: { type: 'string', description: '
|
|
399
|
-
summary: { type: 'string', description: '
|
|
400
|
-
workDone: { type: 'string', description: '
|
|
401
|
-
nextSteps: { type: 'array', items: { type: 'string' }, description: '
|
|
402
|
-
modifiedFiles: { type: 'array', items: { type: 'string' }, description: '
|
|
403
|
-
blockers: { type: 'string', description: '
|
|
430
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
431
|
+
summary: { type: 'string', description: 'One-line summary of this session' },
|
|
432
|
+
workDone: { type: 'string', description: 'Description of completed work' },
|
|
433
|
+
nextSteps: { type: 'array', items: { type: 'string' }, description: 'Ordered list of next tasks to pick up' },
|
|
434
|
+
modifiedFiles: { type: 'array', items: { type: 'string' }, description: 'Files modified during this session' },
|
|
435
|
+
blockers: { type: 'string', description: 'Current blockers or issues (null if none)' }
|
|
404
436
|
},
|
|
405
437
|
required: ['project', 'summary']
|
|
406
438
|
}
|
|
407
439
|
},
|
|
408
440
|
{
|
|
409
441
|
name: 'session_history',
|
|
410
|
-
description: '
|
|
442
|
+
description: 'Retrieve past session records for a project. Returns an array of session objects ordered by most recent first, each containing summary, work done, modified files, and verification results. Read-only. Use search_sessions instead when you need semantic/keyword matching rather than a chronological list.',
|
|
411
443
|
inputSchema: {
|
|
412
444
|
type: 'object',
|
|
413
445
|
properties: {
|
|
414
|
-
project: { type: 'string', description: '
|
|
415
|
-
limit: { type: 'number', description: '
|
|
416
|
-
days: { type: 'number', description: '
|
|
446
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
447
|
+
limit: { type: 'number', description: 'Max records to return (default: 5)' },
|
|
448
|
+
days: { type: 'number', description: 'Only return sessions from the last N days (default: 7)' }
|
|
417
449
|
},
|
|
418
450
|
required: ['project']
|
|
419
451
|
}
|
|
420
452
|
},
|
|
421
453
|
{
|
|
422
454
|
name: 'search_sessions',
|
|
423
|
-
description: '
|
|
455
|
+
description: 'Semantic search across session history using multilingual embeddings (94+ languages). Finds past sessions by meaning, not just keywords — e.g. "when I worked on authentication" matches sessions about login, OAuth, JWT. Falls back to FTS5 keyword search when embeddings are unavailable. Read-only. Use session_history instead when you just need the N most recent sessions.',
|
|
424
456
|
inputSchema: {
|
|
425
457
|
type: 'object',
|
|
426
458
|
properties: {
|
|
427
|
-
query: { type: 'string', description: '
|
|
428
|
-
project: { type: 'string', description: '
|
|
429
|
-
limit: { type: 'number', description: '
|
|
459
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
460
|
+
project: { type: 'string', description: 'Filter by project (optional)' },
|
|
461
|
+
limit: { type: 'number', description: 'Max results to return (default: 5)' }
|
|
430
462
|
},
|
|
431
463
|
required: ['query']
|
|
432
464
|
}
|
|
@@ -434,42 +466,42 @@ const tools = [
|
|
|
434
466
|
// ===== 2. 프로젝트 관리 (4개) =====
|
|
435
467
|
{
|
|
436
468
|
name: 'project_status',
|
|
437
|
-
description: '
|
|
469
|
+
description: 'Get a project\'s current status including completion percentage, task breakdown (pending/in-progress/done/blocked), recent session activity, and active blockers. Read-only. Returns a structured JSON object. Use session_start instead when bootstrapping a new conversation; use this for mid-session status checks.',
|
|
438
470
|
inputSchema: {
|
|
439
471
|
type: 'object',
|
|
440
472
|
properties: {
|
|
441
|
-
project: { type: 'string', description: '
|
|
473
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
442
474
|
},
|
|
443
475
|
required: ['project']
|
|
444
476
|
}
|
|
445
477
|
},
|
|
446
478
|
{
|
|
447
479
|
name: 'project_init',
|
|
448
|
-
description: '
|
|
480
|
+
description: 'Initialize a new project in the continuity system. Creates records in the project_context and active_context tables. Auto-detects tech stack from package.json/pubspec.yaml/build.gradle if present. Side effects: writes to SQLite. Idempotent — safe to call on an already-initialized project (updates existing record). Call this once when adding a new project, then use session_start for subsequent sessions.',
|
|
449
481
|
inputSchema: {
|
|
450
482
|
type: 'object',
|
|
451
483
|
properties: {
|
|
452
|
-
project: { type: 'string', description: '
|
|
453
|
-
techStack: { type: 'object', description: '
|
|
454
|
-
description: { type: 'string', description: '
|
|
484
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
485
|
+
techStack: { type: 'object', description: 'Tech stack override {framework, language, database, ...}. Omit for auto-detection.' },
|
|
486
|
+
description: { type: 'string', description: 'Human-readable project description' }
|
|
455
487
|
},
|
|
456
488
|
required: ['project']
|
|
457
489
|
}
|
|
458
490
|
},
|
|
459
491
|
{
|
|
460
492
|
name: 'project_analyze',
|
|
461
|
-
description: '
|
|
493
|
+
description: 'Auto-detect a project\'s tech stack, framework, platform (Web/Android/Flutter/Server), directory structure, and dependency count by scanning its files. Read-only — does not persist results. Returns a structured analysis object. Use project_init to persist the detected configuration.',
|
|
462
494
|
inputSchema: {
|
|
463
495
|
type: 'object',
|
|
464
496
|
properties: {
|
|
465
|
-
project: { type: 'string', description: '
|
|
497
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
466
498
|
},
|
|
467
499
|
required: ['project']
|
|
468
500
|
}
|
|
469
501
|
},
|
|
470
502
|
{
|
|
471
503
|
name: 'list_projects',
|
|
472
|
-
description: 'apps/
|
|
504
|
+
description: 'List all projects under the apps/ directory with their platform type (Web/Android/Flutter), initialization status, and whether session context exists. Read-only. Returns an array of project summary objects. No parameters required.',
|
|
473
505
|
inputSchema: {
|
|
474
506
|
type: 'object',
|
|
475
507
|
properties: {}
|
|
@@ -478,52 +510,52 @@ const tools = [
|
|
|
478
510
|
// ===== 3. 태스크/백로그 (4개) =====
|
|
479
511
|
{
|
|
480
512
|
name: 'task_add',
|
|
481
|
-
description: '
|
|
513
|
+
description: 'Add a new task to a project\'s backlog. Tasks are persisted in SQLite with priority ranking and optional file associations. Side effects: inserts into the tasks table. Returns the created task ID. Use task_list to view existing tasks before adding duplicates. Use task_suggest to auto-generate tasks from code comments (TODO/FIXME).',
|
|
482
514
|
inputSchema: {
|
|
483
515
|
type: 'object',
|
|
484
516
|
properties: {
|
|
485
|
-
project: { type: 'string', description: '
|
|
486
|
-
title: { type: 'string', description: '
|
|
487
|
-
description: { type: 'string', description: '
|
|
488
|
-
priority: { type: 'number', description: '
|
|
489
|
-
relatedFiles: { type: 'array', items: { type: 'string' }, description: '
|
|
517
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
518
|
+
title: { type: 'string', description: 'Task title (concise, actionable)' },
|
|
519
|
+
description: { type: 'string', description: 'Detailed description (optional)' },
|
|
520
|
+
priority: { type: 'number', description: 'Priority 1-10 where 10 is highest (default: 5)' },
|
|
521
|
+
relatedFiles: { type: 'array', items: { type: 'string' }, description: 'Associated file paths (optional)' }
|
|
490
522
|
},
|
|
491
523
|
required: ['project', 'title']
|
|
492
524
|
}
|
|
493
525
|
},
|
|
494
526
|
{
|
|
495
527
|
name: 'task_update',
|
|
496
|
-
description: '
|
|
528
|
+
description: 'Update a task\'s status. Valid transitions: pending → in_progress → done, or any state → blocked. Setting status to "done" automatically records a completion timestamp. Side effects: updates the tasks table. Idempotent. Returns success/failure and whether the row was actually modified.',
|
|
497
529
|
inputSchema: {
|
|
498
530
|
type: 'object',
|
|
499
531
|
properties: {
|
|
500
|
-
taskId: { type: 'number', description: '
|
|
501
|
-
status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: '
|
|
502
|
-
note: { type: 'string', description: '
|
|
532
|
+
taskId: { type: 'number', description: 'Task ID (from task_add or task_list)' },
|
|
533
|
+
status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: 'New status' },
|
|
534
|
+
note: { type: 'string', description: 'Optional note (e.g. completion summary or block reason)' }
|
|
503
535
|
},
|
|
504
536
|
required: ['taskId', 'status']
|
|
505
537
|
}
|
|
506
538
|
},
|
|
507
539
|
{
|
|
508
540
|
name: 'task_list',
|
|
509
|
-
description: '
|
|
541
|
+
description: 'List tasks for a project, filtered by status. Returns an array of task objects with id, title, description, status, priority, related files, and timestamps, plus a summary count by status. Read-only. Default filter is "pending" — pass status="all" to see everything.',
|
|
510
542
|
inputSchema: {
|
|
511
543
|
type: 'object',
|
|
512
544
|
properties: {
|
|
513
|
-
project: { type: 'string', description: '
|
|
514
|
-
status: { type: 'string', enum: ['all', 'pending', 'in_progress', 'done', 'blocked'], description: '
|
|
545
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
546
|
+
status: { type: 'string', enum: ['all', 'pending', 'in_progress', 'done', 'blocked'], description: 'Status filter (default: "pending")' }
|
|
515
547
|
},
|
|
516
548
|
required: ['project']
|
|
517
549
|
}
|
|
518
550
|
},
|
|
519
551
|
{
|
|
520
552
|
name: 'task_suggest',
|
|
521
|
-
description: '
|
|
553
|
+
description: 'Scan project source files for TODO, FIXME, HACK, and XXX comments and return them as suggested tasks. Read-only — does not create tasks automatically. Review the suggestions and use task_add to persist the ones you want. Optionally scope the scan to a specific subdirectory.',
|
|
522
554
|
inputSchema: {
|
|
523
555
|
type: 'object',
|
|
524
556
|
properties: {
|
|
525
|
-
project: { type: 'string', description: '
|
|
526
|
-
path: { type: 'string', description: '
|
|
557
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
558
|
+
path: { type: 'string', description: 'Subdirectory path to limit the scan (optional, e.g. "src/components")' }
|
|
527
559
|
},
|
|
528
560
|
required: ['project']
|
|
529
561
|
}
|
|
@@ -531,41 +563,41 @@ const tools = [
|
|
|
531
563
|
// ===== 4. 솔루션 아카이브 (3개) =====
|
|
532
564
|
{
|
|
533
565
|
name: 'solution_record',
|
|
534
|
-
description: '
|
|
566
|
+
description: 'Record an error-solution pair in the solution archive. Associates an error signature (the searchable key), optional full error message, the fix, and related files. Automatically extracts keywords for FTS5 indexing. Side effects: inserts into the solutions table. Use solution_find to check for existing solutions before recording a duplicate.',
|
|
535
567
|
inputSchema: {
|
|
536
568
|
type: 'object',
|
|
537
569
|
properties: {
|
|
538
|
-
project: { type: 'string', description: '
|
|
539
|
-
errorSignature: { type: 'string', description: '
|
|
540
|
-
errorMessage: { type: 'string', description: '
|
|
541
|
-
solution: { type: 'string', description: '
|
|
542
|
-
relatedFiles: { type: 'array', items: { type: 'string' }, description: '
|
|
570
|
+
project: { type: 'string', description: 'Project name (optional — omit for cross-project solutions)' },
|
|
571
|
+
errorSignature: { type: 'string', description: 'Error pattern/signature used as the search key (e.g. "ENOENT: no such file", "WorkManager not initialized")' },
|
|
572
|
+
errorMessage: { type: 'string', description: 'Full error message or stack trace (optional)' },
|
|
573
|
+
solution: { type: 'string', description: 'Step-by-step fix description' },
|
|
574
|
+
relatedFiles: { type: 'array', items: { type: 'string' }, description: 'Files that were modified to fix the error' }
|
|
543
575
|
},
|
|
544
576
|
required: ['errorSignature', 'solution']
|
|
545
577
|
}
|
|
546
578
|
},
|
|
547
579
|
{
|
|
548
580
|
name: 'solution_find',
|
|
549
|
-
description: '
|
|
581
|
+
description: 'Search the solution archive for previously resolved errors. Matches against error signatures, messages, and keywords using FTS5. Set semantic=true to enable embedding-based similarity search for better recall across different error phrasings. Read-only. Returns matched solutions with their fix descriptions and related files. Use solution_suggest instead if you want AI-powered fix recommendations.',
|
|
550
582
|
inputSchema: {
|
|
551
583
|
type: 'object',
|
|
552
584
|
properties: {
|
|
553
|
-
query: { type: 'string', description: '
|
|
554
|
-
project: { type: 'string', description: '
|
|
555
|
-
limit: { type: 'number', description: '
|
|
556
|
-
semantic: { type: 'boolean', description: '
|
|
585
|
+
query: { type: 'string', description: 'Error message, signature, or natural language description of the problem' },
|
|
586
|
+
project: { type: 'string', description: 'Filter by project (optional — also includes cross-project solutions)' },
|
|
587
|
+
limit: { type: 'number', description: 'Max results to return (default: 3)' },
|
|
588
|
+
semantic: { type: 'boolean', description: 'Enable semantic/embedding search for fuzzy matching (default: false)' }
|
|
557
589
|
},
|
|
558
590
|
required: ['query']
|
|
559
591
|
}
|
|
560
592
|
},
|
|
561
593
|
{
|
|
562
594
|
name: 'solution_suggest',
|
|
563
|
-
description: '
|
|
595
|
+
description: 'Get AI-powered fix suggestions for a current error based on the solution archive. Retrieves the most relevant past solutions and generates a contextual recommendation. Read-only. Use solution_find for direct archive lookup without AI synthesis; use solution_record after fixing an error to grow the archive.',
|
|
564
596
|
inputSchema: {
|
|
565
597
|
type: 'object',
|
|
566
598
|
properties: {
|
|
567
|
-
errorMessage: { type: 'string', description: '
|
|
568
|
-
project: { type: 'string', description: '
|
|
599
|
+
errorMessage: { type: 'string', description: 'The current error message or stack trace' },
|
|
600
|
+
project: { type: 'string', description: 'Project name for context (optional)' }
|
|
569
601
|
},
|
|
570
602
|
required: ['errorMessage']
|
|
571
603
|
}
|
|
@@ -573,35 +605,35 @@ const tools = [
|
|
|
573
605
|
// ===== 5. 검증/품질 (3개) =====
|
|
574
606
|
{
|
|
575
607
|
name: 'verify_build',
|
|
576
|
-
description: '
|
|
608
|
+
description: 'Run the project\'s build command (auto-detected per platform: "pnpm build" for Web, "flutter build" for Flutter, "./gradlew assembleDebug" for Android). Side effects: executes a shell command in the project directory with a 5-minute timeout. Returns {success, output} with the last 1000 chars of stdout/stderr. Use verify_all to run build + test + lint together.',
|
|
577
609
|
inputSchema: {
|
|
578
610
|
type: 'object',
|
|
579
611
|
properties: {
|
|
580
|
-
project: { type: 'string', description: '
|
|
612
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
581
613
|
},
|
|
582
614
|
required: ['project']
|
|
583
615
|
}
|
|
584
616
|
},
|
|
585
617
|
{
|
|
586
618
|
name: 'verify_test',
|
|
587
|
-
description: '
|
|
619
|
+
description: 'Run the project\'s test suite (auto-detected per platform: "pnpm test:run" for Web, "flutter test" for Flutter, "./gradlew test" for Android). Optionally scope to a specific test file or directory. Side effects: executes a shell command with a 5-minute timeout. Returns {success, output}. Use verify_all to run build + test + lint together.',
|
|
588
620
|
inputSchema: {
|
|
589
621
|
type: 'object',
|
|
590
622
|
properties: {
|
|
591
|
-
project: { type: 'string', description: '
|
|
592
|
-
testPath: { type: 'string', description: '
|
|
623
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
624
|
+
testPath: { type: 'string', description: 'Specific test file or directory to run (optional — runs all tests if omitted)' }
|
|
593
625
|
},
|
|
594
626
|
required: ['project']
|
|
595
627
|
}
|
|
596
628
|
},
|
|
597
629
|
{
|
|
598
630
|
name: 'verify_all',
|
|
599
|
-
description: '
|
|
631
|
+
description: 'Run build, test, and lint sequentially for a project. Auto-detects platform-specific commands. Side effects: executes up to 3 shell commands with 5-minute timeouts each. Returns per-gate results and an overall pass/fail status. Use this as a quality gate before committing or ending a session. Use verify_build or verify_test individually when you only need one check.',
|
|
600
632
|
inputSchema: {
|
|
601
633
|
type: 'object',
|
|
602
634
|
properties: {
|
|
603
|
-
project: { type: 'string', description: '
|
|
604
|
-
stopOnFail: { type: 'boolean', description: '
|
|
635
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' },
|
|
636
|
+
stopOnFail: { type: 'boolean', description: 'Abort remaining gates on first failure (default: false — runs all gates)' }
|
|
605
637
|
},
|
|
606
638
|
required: ['project']
|
|
607
639
|
}
|
|
@@ -609,117 +641,117 @@ const tools = [
|
|
|
609
641
|
// ===== 6. 메모리 시스템 (4개) - v4 신규 =====
|
|
610
642
|
{
|
|
611
643
|
name: 'memory_store',
|
|
612
|
-
description: '
|
|
644
|
+
description: 'Store a piece of knowledge in the memory system. Memories are typed (observation, decision, learning, error, pattern), tagged, and automatically embedded for semantic retrieval. Side effects: inserts into the memories table and asynchronously generates a vector embedding. If relatedTo is provided, also creates a knowledge graph edge. Returns the new memory ID. Use memory_search to verify no duplicate exists before storing.',
|
|
613
645
|
inputSchema: {
|
|
614
646
|
type: 'object',
|
|
615
647
|
properties: {
|
|
616
|
-
content: { type: 'string', description: '
|
|
648
|
+
content: { type: 'string', description: 'The knowledge content to store' },
|
|
617
649
|
type: {
|
|
618
650
|
type: 'string',
|
|
619
651
|
enum: ['observation', 'decision', 'learning', 'error', 'pattern'],
|
|
620
|
-
description: '
|
|
652
|
+
description: 'Memory type: observation (discovery/finding), decision (architecture/tech choice), learning (new knowledge), error (error encountered), pattern (code convention)'
|
|
621
653
|
},
|
|
622
|
-
project: { type: 'string', description: '
|
|
623
|
-
tags: { type: 'array', items: { type: 'string' }, description: '
|
|
624
|
-
importance: { type: 'number', description: '
|
|
625
|
-
relatedTo: { type: 'number', description: '
|
|
654
|
+
project: { type: 'string', description: 'Associated project name (optional — omit for cross-project knowledge)' },
|
|
655
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Tags for filtering (e.g. ["auth", "performance"])' },
|
|
656
|
+
importance: { type: 'number', description: 'Importance score 1-10 where 10 is critical (default: 5)' },
|
|
657
|
+
relatedTo: { type: 'number', description: 'ID of an existing memory to link via knowledge graph (optional)' }
|
|
626
658
|
},
|
|
627
659
|
required: ['content', 'type']
|
|
628
660
|
}
|
|
629
661
|
},
|
|
630
662
|
{
|
|
631
663
|
name: 'memory_search',
|
|
632
|
-
description: '
|
|
664
|
+
description: 'Search stored memories using FTS5 full-text search or semantic/embedding similarity. Default mode returns compact index entries (id, type, truncated content) to save tokens — set detail=true for full content. Supports filtering by type, project, tags, and minimum importance. Read-only. Use memory_get to fetch full content for specific IDs found in search results. Use memory_related to explore graph connections from a known memory.',
|
|
633
665
|
inputSchema: {
|
|
634
666
|
type: 'object',
|
|
635
667
|
properties: {
|
|
636
|
-
query: { type: 'string', description: '
|
|
668
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
637
669
|
type: {
|
|
638
670
|
type: 'string',
|
|
639
671
|
enum: ['observation', 'decision', 'learning', 'error', 'pattern', 'all'],
|
|
640
|
-
description: '
|
|
672
|
+
description: 'Filter by memory type (default: "all")'
|
|
641
673
|
},
|
|
642
|
-
project: { type: 'string', description: '
|
|
643
|
-
tags: { type: 'array', items: { type: 'string' }, description: '
|
|
644
|
-
semantic: { type: 'boolean', description: '
|
|
645
|
-
minImportance: { type: 'number', description: '
|
|
646
|
-
limit: { type: 'number', description: '
|
|
647
|
-
detail: { type: 'boolean', description: '
|
|
674
|
+
project: { type: 'string', description: 'Filter by project (optional)' },
|
|
675
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags — matches if any tag is present (optional)' },
|
|
676
|
+
semantic: { type: 'boolean', description: 'Use embedding-based semantic search instead of keyword FTS5 (default: false)' },
|
|
677
|
+
minImportance: { type: 'number', description: 'Minimum importance threshold 1-10 (default: 1)' },
|
|
678
|
+
limit: { type: 'number', description: 'Max results to return (default: 10)' },
|
|
679
|
+
detail: { type: 'boolean', description: 'Return full content per memory (default: false — returns compact index only)' }
|
|
648
680
|
},
|
|
649
681
|
required: ['query']
|
|
650
682
|
}
|
|
651
683
|
},
|
|
652
684
|
{
|
|
653
685
|
name: 'memory_get',
|
|
654
|
-
description: '
|
|
686
|
+
description: 'Retrieve full content for one or more memories by ID. Designed as a follow-up to memory_search: first search to find relevant IDs, then use memory_get to load full details. Read-only. Accepts up to 20 IDs per call. Returns an array of complete memory objects including content, type, tags, importance, timestamps, and access count.',
|
|
655
687
|
inputSchema: {
|
|
656
688
|
type: 'object',
|
|
657
689
|
properties: {
|
|
658
|
-
ids: { type: 'array', items: { type: 'number' }, description: '
|
|
690
|
+
ids: { type: 'array', items: { type: 'number' }, description: 'Array of memory IDs to retrieve (max 20)' }
|
|
659
691
|
},
|
|
660
692
|
required: ['ids']
|
|
661
693
|
}
|
|
662
694
|
},
|
|
663
695
|
{
|
|
664
696
|
name: 'memory_related',
|
|
665
|
-
description: '
|
|
697
|
+
description: 'Find memories related to a given memory using knowledge graph traversal and/or semantic similarity. Combines two strategies: (1) graph edges created via graph_connect or memory_store\'s relatedTo, and (2) cosine similarity between embeddings. Read-only. Use graph_explore for pure graph traversal with depth control; use memory_search for text-based search.',
|
|
666
698
|
inputSchema: {
|
|
667
699
|
type: 'object',
|
|
668
700
|
properties: {
|
|
669
|
-
memoryId: { type: 'number', description: '
|
|
670
|
-
includeGraph: { type: 'boolean', description: '
|
|
671
|
-
includeSemantic: { type: 'boolean', description: '
|
|
672
|
-
limit: { type: 'number', description: '
|
|
701
|
+
memoryId: { type: 'number', description: 'The anchor memory ID to find relations for' },
|
|
702
|
+
includeGraph: { type: 'boolean', description: 'Include knowledge graph connections (default: true)' },
|
|
703
|
+
includeSemantic: { type: 'boolean', description: 'Include semantically similar memories via embeddings (default: true)' },
|
|
704
|
+
limit: { type: 'number', description: 'Max results to return (default: 10)' }
|
|
673
705
|
},
|
|
674
706
|
required: ['memoryId']
|
|
675
707
|
}
|
|
676
708
|
},
|
|
677
709
|
{
|
|
678
710
|
name: 'memory_stats',
|
|
679
|
-
description: '
|
|
711
|
+
description: 'Get aggregate statistics about the memory system: total count, breakdown by type (observation/decision/learning/error/pattern), breakdown by project, top 5 most accessed memories, and 5 most recent entries. Read-only. Useful for understanding memory distribution and system health. Optionally scope to a single project.',
|
|
680
712
|
inputSchema: {
|
|
681
713
|
type: 'object',
|
|
682
714
|
properties: {
|
|
683
|
-
project: { type: 'string', description: '
|
|
715
|
+
project: { type: 'string', description: 'Scope statistics to a single project (optional — omit for global stats)' }
|
|
684
716
|
}
|
|
685
717
|
}
|
|
686
718
|
},
|
|
687
719
|
// ===== 7. 지식 그래프 (2개) - v4 신규 =====
|
|
688
720
|
{
|
|
689
721
|
name: 'graph_connect',
|
|
690
|
-
description: '
|
|
722
|
+
description: 'Create a directed edge between two memories in the knowledge graph. Supports 7 relation types for structured knowledge organization. Side effects: inserts or replaces a row in memory_relations (upsert on sourceId+targetId+relation). Use memory_related to discover existing connections; use graph_explore to traverse the graph from a starting node.',
|
|
691
723
|
inputSchema: {
|
|
692
724
|
type: 'object',
|
|
693
725
|
properties: {
|
|
694
|
-
sourceId: { type: 'number', description: '
|
|
695
|
-
targetId: { type: 'number', description: '
|
|
726
|
+
sourceId: { type: 'number', description: 'Source memory ID (the "from" node)' },
|
|
727
|
+
targetId: { type: 'number', description: 'Target memory ID (the "to" node)' },
|
|
696
728
|
relation: {
|
|
697
729
|
type: 'string',
|
|
698
730
|
enum: ['related_to', 'causes', 'solves', 'depends_on', 'contradicts', 'extends', 'example_of'],
|
|
699
|
-
description: '
|
|
731
|
+
description: 'Edge type: related_to (general association), causes (A causes B), solves (A fixes B), depends_on (A requires B), contradicts (A conflicts with B), extends (A builds on B), example_of (A demonstrates B)'
|
|
700
732
|
},
|
|
701
|
-
strength: { type: 'number', description: '
|
|
733
|
+
strength: { type: 'number', description: 'Connection strength 0.0-1.0 (default: 1.0). Lower values indicate weaker associations.' }
|
|
702
734
|
},
|
|
703
735
|
required: ['sourceId', 'targetId', 'relation']
|
|
704
736
|
}
|
|
705
737
|
},
|
|
706
738
|
{
|
|
707
739
|
name: 'graph_explore',
|
|
708
|
-
description: '
|
|
740
|
+
description: 'Traverse the knowledge graph from a starting memory using depth-first search. Returns all connected memories up to the specified depth, with their relation types, strengths, and directions. Read-only. Supports filtering by relation type and traversal direction. Use memory_related instead for a combined graph+semantic approach; use graph_connect to add new edges.',
|
|
709
741
|
inputSchema: {
|
|
710
742
|
type: 'object',
|
|
711
743
|
properties: {
|
|
712
|
-
memoryId: { type: 'number', description: '
|
|
713
|
-
depth: { type: 'number', description: '
|
|
744
|
+
memoryId: { type: 'number', description: 'Starting memory ID for graph traversal' },
|
|
745
|
+
depth: { type: 'number', description: 'Maximum traversal depth 1-4 (default: 2). Higher values return more results but may be slower.' },
|
|
714
746
|
relation: {
|
|
715
747
|
type: 'string',
|
|
716
748
|
enum: ['related_to', 'causes', 'solves', 'depends_on', 'contradicts', 'extends', 'example_of', 'all'],
|
|
717
|
-
description: '
|
|
749
|
+
description: 'Filter by relation type (default: "all")'
|
|
718
750
|
},
|
|
719
751
|
direction: {
|
|
720
752
|
type: 'string',
|
|
721
753
|
enum: ['outgoing', 'incoming', 'both'],
|
|
722
|
-
description: '
|
|
754
|
+
description: 'Traversal direction — outgoing (A→B), incoming (B→A), or both (default: "both")'
|
|
723
755
|
}
|
|
724
756
|
},
|
|
725
757
|
required: ['memoryId']
|
|
@@ -892,9 +924,9 @@ async function handleTool(name, args) {
|
|
|
892
924
|
const days = args.days || 7;
|
|
893
925
|
const sessions = db.prepare(`
|
|
894
926
|
SELECT * FROM sessions
|
|
895
|
-
WHERE project = ? AND timestamp > datetime('now', '
|
|
927
|
+
WHERE project = ? AND timestamp > datetime('now', '-' || ? || ' days')
|
|
896
928
|
ORDER BY timestamp DESC LIMIT ?
|
|
897
|
-
`).all(project, limit);
|
|
929
|
+
`).all(project, days, limit);
|
|
898
930
|
return {
|
|
899
931
|
content: [{
|
|
900
932
|
type: 'text',
|
|
@@ -928,10 +960,26 @@ async function handleTool(name, args) {
|
|
|
928
960
|
// 모든 세션 가져와서 유사도 계산
|
|
929
961
|
const allSessions = db.prepare(project
|
|
930
962
|
? 'SELECT * FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 100'
|
|
931
|
-
: 'SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 100').all(project ? [project] : []);
|
|
963
|
+
: 'SELECT * FROM sessions ORDER BY timestamp DESC LIMIT 100').all(...(project ? [project] : []));
|
|
964
|
+
// 사전 캐시된 임베딩 활용 (없으면 on-the-fly 생성 후 캐시)
|
|
932
965
|
const scored = await Promise.all(allSessions.map(async (s) => {
|
|
933
|
-
const
|
|
934
|
-
|
|
966
|
+
const sessionId = s.id;
|
|
967
|
+
// 캐시된 임베딩 확인
|
|
968
|
+
const cached = db.prepare('SELECT embedding FROM embeddings_v4 WHERE entity_type = ? AND entity_id = ?').get('session', sessionId);
|
|
969
|
+
let emb = null;
|
|
970
|
+
if (cached?.embedding) {
|
|
971
|
+
emb = Array.from(new Float32Array(cached.embedding.buffer, cached.embedding.byteOffset, cached.embedding.byteLength / 4));
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
const text = `${s.last_work} ${s.current_status || ''}`;
|
|
975
|
+
emb = await generateEmbedding(text, 'passage');
|
|
976
|
+
// 생성 성공 시 캐시 저장
|
|
977
|
+
if (emb) {
|
|
978
|
+
const buffer = Buffer.from(new Float32Array(emb).buffer);
|
|
979
|
+
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)')
|
|
980
|
+
.run('session', sessionId, buffer);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
935
983
|
const similarity = emb ? cosineSimilarity(queryEmbedding, emb) : 0;
|
|
936
984
|
return { ...s, similarity };
|
|
937
985
|
}));
|
|
@@ -1180,13 +1228,8 @@ async function handleTool(name, args) {
|
|
|
1180
1228
|
INSERT INTO solutions (project, error_signature, error_message, solution, related_files, keywords)
|
|
1181
1229
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
1182
1230
|
`).run(project || null, errorSignature, errorMessage || null, solution, relatedFiles ? JSON.stringify(relatedFiles) : null, keywords);
|
|
1183
|
-
// 임베딩 저장 (시맨틱
|
|
1184
|
-
|
|
1185
|
-
if (embedding) {
|
|
1186
|
-
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
1187
|
-
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)').run('solution', result.lastInsertRowid, buffer);
|
|
1188
|
-
}
|
|
1189
|
-
}).catch(() => { });
|
|
1231
|
+
// 임베딩 저장 (시맨틱 검색용, 재시도 포함)
|
|
1232
|
+
storeEmbeddingWithRetry(db, 'solution', result.lastInsertRowid, `${errorSignature} ${errorMessage || ''} ${solution}`);
|
|
1190
1233
|
return {
|
|
1191
1234
|
content: [{
|
|
1192
1235
|
type: 'text',
|
|
@@ -1408,13 +1451,8 @@ async function handleTool(name, args) {
|
|
|
1408
1451
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
1409
1452
|
`).run(content, memoryType, tags ? JSON.stringify(tags) : null, project || null, importance, null);
|
|
1410
1453
|
const memoryId = result.lastInsertRowid;
|
|
1411
|
-
// 임베딩 생성 (
|
|
1412
|
-
|
|
1413
|
-
if (embedding) {
|
|
1414
|
-
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
1415
|
-
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)').run('memory', memoryId, buffer);
|
|
1416
|
-
}
|
|
1417
|
-
}).catch(() => { });
|
|
1454
|
+
// 임베딩 생성 (재시도 포함)
|
|
1455
|
+
storeEmbeddingWithRetry(db, 'memory', memoryId, content);
|
|
1418
1456
|
// 관계 자동 생성
|
|
1419
1457
|
if (relatedTo) {
|
|
1420
1458
|
db.prepare(`
|
|
@@ -1645,10 +1683,10 @@ async function handleTool(name, args) {
|
|
|
1645
1683
|
const project = args.project;
|
|
1646
1684
|
const totalMemories = db.prepare(project
|
|
1647
1685
|
? 'SELECT COUNT(*) as count FROM memories WHERE project = ?'
|
|
1648
|
-
: 'SELECT COUNT(*) as count FROM memories').get(project ? [project] : []).count;
|
|
1686
|
+
: 'SELECT COUNT(*) as count FROM memories').get(...(project ? [project] : [])).count;
|
|
1649
1687
|
const byType = db.prepare(project
|
|
1650
1688
|
? 'SELECT memory_type as type, COUNT(*) as count FROM memories WHERE project = ? GROUP BY memory_type'
|
|
1651
|
-
: 'SELECT memory_type as type, COUNT(*) as count FROM memories GROUP BY memory_type').all(project ? [project] : []);
|
|
1689
|
+
: 'SELECT memory_type as type, COUNT(*) as count FROM memories GROUP BY memory_type').all(...(project ? [project] : []));
|
|
1652
1690
|
const byProject = db.prepare(`
|
|
1653
1691
|
SELECT COALESCE(project, 'global') as project, COUNT(*) as count
|
|
1654
1692
|
FROM memories GROUP BY project ORDER BY count DESC LIMIT 10
|
|
@@ -1663,13 +1701,13 @@ async function handleTool(name, args) {
|
|
|
1663
1701
|
FROM memories
|
|
1664
1702
|
${project ? 'WHERE project = ?' : ''}
|
|
1665
1703
|
ORDER BY created_at DESC LIMIT 5
|
|
1666
|
-
`).all(project ? [project] : []);
|
|
1704
|
+
`).all(...(project ? [project] : []));
|
|
1667
1705
|
const topAccessedMemories = db.prepare(`
|
|
1668
1706
|
SELECT id, memory_type, content, access_count
|
|
1669
1707
|
FROM memories
|
|
1670
1708
|
${project ? 'WHERE project = ?' : ''}
|
|
1671
1709
|
ORDER BY access_count DESC LIMIT 5
|
|
1672
|
-
`).all(project ? [project] : []);
|
|
1710
|
+
`).all(...(project ? [project] : []));
|
|
1673
1711
|
return {
|
|
1674
1712
|
content: [{
|
|
1675
1713
|
type: 'text',
|