claude-session-continuity-mcp 1.13.0 → 1.13.2
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 +48 -0
- package/dist/hooks/pre-compact.js +54 -1
- package/dist/hooks/session-end.js +185 -15
- package/dist/hooks/session-start.js +1 -0
- package/dist/hooks/user-prompt-submit.js +8 -2
- package/dist/index.js +121 -106
- 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)) {
|
|
@@ -218,6 +265,7 @@ async function main() {
|
|
|
218
265
|
process.exit(0);
|
|
219
266
|
}
|
|
220
267
|
const db = new Database(dbPath);
|
|
268
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
221
269
|
// hot_paths 추적 (모든 추적 도구)
|
|
222
270
|
try {
|
|
223
271
|
const pathType = filePath.includes('.') ? 'file' : 'directory';
|
|
@@ -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)) {
|
|
@@ -158,6 +209,7 @@ async function main() {
|
|
|
158
209
|
process.exit(0);
|
|
159
210
|
}
|
|
160
211
|
const db = new Database(dbPath);
|
|
212
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
161
213
|
// 핸드오버 컨텍스트 빌드
|
|
162
214
|
const handover = input.transcript ? buildHandoverContext(input.transcript) : null;
|
|
163
215
|
// active_context 업데이트 (memories에는 저장하지 않음)
|
|
@@ -233,7 +285,8 @@ async function main() {
|
|
|
233
285
|
process.exit(0);
|
|
234
286
|
}
|
|
235
287
|
catch (e) {
|
|
236
|
-
//
|
|
288
|
+
// fail-soft: 컴팩션이 멈추지 않도록 continue:true 반드시 반환
|
|
289
|
+
process.stdout.write(JSON.stringify({ continue: true }));
|
|
237
290
|
process.exit(0);
|
|
238
291
|
}
|
|
239
292
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as readline from 'readline';
|
|
15
|
+
import * as crypto from 'crypto';
|
|
15
16
|
import Database from 'better-sqlite3';
|
|
16
17
|
function detectWorkspaceRoot(cwd) {
|
|
17
18
|
let current = cwd;
|
|
@@ -238,6 +239,8 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
238
239
|
userRequests: { firstRequest: '', allRequests: [] },
|
|
239
240
|
recentAssistantMessages: [],
|
|
240
241
|
errorFixPairs: [],
|
|
242
|
+
firstTimestamp: null,
|
|
243
|
+
lastTimestamp: null,
|
|
241
244
|
};
|
|
242
245
|
if (!transcriptPath || !fs.existsSync(transcriptPath))
|
|
243
246
|
return result;
|
|
@@ -267,6 +270,12 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
267
270
|
const entry = JSON.parse(line);
|
|
268
271
|
const role = entry.type || entry.role || '';
|
|
269
272
|
const content = entry.message?.content;
|
|
273
|
+
// === 0. Timestamp 추출 (세션 duration 계산용) ===
|
|
274
|
+
if (entry.timestamp) {
|
|
275
|
+
if (!result.firstTimestamp)
|
|
276
|
+
result.firstTimestamp = entry.timestamp;
|
|
277
|
+
result.lastTimestamp = entry.timestamp;
|
|
278
|
+
}
|
|
270
279
|
// === 1. Commit 추출 (tool_use 블록에서) ===
|
|
271
280
|
if (line.includes('git commit') && Array.isArray(content)) {
|
|
272
281
|
for (const block of content) {
|
|
@@ -343,9 +352,9 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
343
352
|
}
|
|
344
353
|
}
|
|
345
354
|
result.decisions = [...decisionSet].slice(0, 3);
|
|
346
|
-
// Error-Fix pairs
|
|
347
|
-
const errorRe = /(?:error|Error|ERROR
|
|
348
|
-
const fixRe = /(?:fixed|resolved
|
|
355
|
+
// Error-Fix pairs (한국어 패턴 보강)
|
|
356
|
+
const errorRe = /(?:error|Error|ERROR|오류|에러|버그|예외|실패|FAILED|Exception|TypeError|ReferenceError|SyntaxError|crash|crashed|충돌|문제)[:\s](.{5,80})/;
|
|
357
|
+
const fixRe = /(?:fixed|resolved|patched|수정|해결|고침|처리|완료|변경|적용|반영|커밋|Added|수정 완료|문제 해결|해결됨|되돌림)/i;
|
|
349
358
|
const pairSet = new Set();
|
|
350
359
|
for (let i = 0; i < recentEntries.length - 1; i++) {
|
|
351
360
|
const errorMatch = recentEntries[i].text.match(errorRe);
|
|
@@ -370,6 +379,45 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
370
379
|
result.recentAssistantMessages = result.recentAssistantMessages.slice(-5);
|
|
371
380
|
return result;
|
|
372
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* 슬래시 커맨드 prefix 제거 — "/mcp-dev 측정해줘" → "측정해줘"
|
|
384
|
+
* 첫 토큰이 `/`로 시작하면 다음 의미 토큰까지 스킵
|
|
385
|
+
* 슬래시뿐이면 빈 문자열 반환 (호출자가 폴백 처리)
|
|
386
|
+
*/
|
|
387
|
+
function stripSlashPrefix(text) {
|
|
388
|
+
const trimmed = text.trim();
|
|
389
|
+
if (!trimmed.startsWith('/'))
|
|
390
|
+
return trimmed;
|
|
391
|
+
// 첫 줄에서 슬래시 토큰을 제거
|
|
392
|
+
const firstLine = trimmed.split('\n')[0];
|
|
393
|
+
const tokens = firstLine.split(/\s+/);
|
|
394
|
+
let i = 0;
|
|
395
|
+
while (i < tokens.length && tokens[i].startsWith('/'))
|
|
396
|
+
i++;
|
|
397
|
+
const rest = tokens.slice(i).join(' ').trim();
|
|
398
|
+
if (rest.length >= 3)
|
|
399
|
+
return rest;
|
|
400
|
+
// 다음 줄에 의미 있는 본문이 있으면 사용
|
|
401
|
+
const lines = trimmed.split('\n').slice(1).map(l => l.trim()).filter(l => l.length >= 3 && !l.startsWith('/'));
|
|
402
|
+
return lines[0] || '';
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Jaccard 유사도 (토큰 단위) — 0~1 사이
|
|
406
|
+
* 동일하면 1, 완전 다르면 0
|
|
407
|
+
*/
|
|
408
|
+
function jaccardSimilarity(a, b) {
|
|
409
|
+
const tokenize = (s) => new Set(s.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter(t => t.length >= 2));
|
|
410
|
+
const setA = tokenize(a);
|
|
411
|
+
const setB = tokenize(b);
|
|
412
|
+
if (setA.size === 0 || setB.size === 0)
|
|
413
|
+
return 0;
|
|
414
|
+
let intersect = 0;
|
|
415
|
+
for (const t of setA)
|
|
416
|
+
if (setB.has(t))
|
|
417
|
+
intersect++;
|
|
418
|
+
const union = setA.size + setB.size - intersect;
|
|
419
|
+
return union === 0 ? 0 : intersect / union;
|
|
420
|
+
}
|
|
373
421
|
/**
|
|
374
422
|
* 사용자 메시지들을 세션 요약으로 압축
|
|
375
423
|
* 예: ["MCP 테스트해줘", "개선해줘", "npm 배포하고 커밋해줘"] → "MCP 테스트 + 개선 + npm 배포/커밋"
|
|
@@ -377,12 +425,24 @@ async function parseTranscriptSinglePass(transcriptPath) {
|
|
|
377
425
|
function summarizeUserRequests(requests) {
|
|
378
426
|
if (requests.length === 0)
|
|
379
427
|
return '';
|
|
380
|
-
|
|
381
|
-
|
|
428
|
+
// 슬래시 커맨드 도움말 본문(/work, /clone-pro 등이 첫 줄에 박히는 케이스) 제외
|
|
429
|
+
// → 36건 동일 last_work 누적 문제 해결
|
|
430
|
+
const meaningful = requests
|
|
431
|
+
.map(r => stripSlashPrefix(r))
|
|
432
|
+
.filter(r => {
|
|
433
|
+
if (!r)
|
|
434
|
+
return false;
|
|
435
|
+
if (/^[A-Z][a-z]+\s+(skill|command):/i.test(r))
|
|
436
|
+
return false;
|
|
437
|
+
return r.length > 0;
|
|
438
|
+
});
|
|
439
|
+
const source = meaningful.length > 0 ? meaningful : requests;
|
|
440
|
+
if (source.length === 1)
|
|
441
|
+
return source[0];
|
|
382
442
|
// 중복/유사 요청 제거 (앞 20글자 기준)
|
|
383
443
|
const unique = [];
|
|
384
444
|
const seen = new Set();
|
|
385
|
-
for (const req of
|
|
445
|
+
for (const req of source) {
|
|
386
446
|
const key = req.slice(0, 20).toLowerCase();
|
|
387
447
|
if (!seen.has(key)) {
|
|
388
448
|
seen.add(key);
|
|
@@ -407,20 +467,59 @@ async function main() {
|
|
|
407
467
|
inputData += chunk;
|
|
408
468
|
}
|
|
409
469
|
const input = inputData ? JSON.parse(inputData) : {};
|
|
470
|
+
// 중복 호출 가드 1: stop_hook_active 플래그 (Claude Code 공식 플래그)
|
|
471
|
+
if (input.stop_hook_active === true) {
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
410
474
|
const cwd = input.cwd || process.cwd();
|
|
411
475
|
const project = detectProject(cwd);
|
|
412
476
|
const dbPath = getDbPath(cwd);
|
|
477
|
+
// 중복 호출 가드 2: transcript_path 해시 기반 5초 윈도우 파일락
|
|
478
|
+
// Phase 3: session_id 5초 락 도입 (sid 있는 호출만 차단됨)
|
|
479
|
+
// Phase 5: 실측에서 모든 stop이 [sid 있는 호출 + sid 없는 호출] 페어로 들어옴
|
|
480
|
+
// → transcript_path 해시를 우선 키로 사용해야 같은 페어가 같은 락을 공유
|
|
481
|
+
// → transcript_path 없을 때만 session_id 폴백
|
|
482
|
+
const lockKey = input.transcript_path
|
|
483
|
+
? crypto.createHash('md5').update(input.transcript_path).digest('hex').slice(0, 16)
|
|
484
|
+
: (input.session_id || null);
|
|
485
|
+
if (lockKey) {
|
|
486
|
+
const lockPath = path.join(path.dirname(dbPath), `.session-end-${lockKey}.lock`);
|
|
487
|
+
const now = Date.now();
|
|
488
|
+
try {
|
|
489
|
+
// Phase 5: atomic `wx` (존재 시 EEXIST throw) → 두 hook 인스턴스가 거의 동시에 진입하는 race 차단
|
|
490
|
+
// 베이스라인: id=1606/1607이 ~500ms 차이로 동시 INSERT 됨 (debug.log 13:26:51.205 + 13:26:51.702)
|
|
491
|
+
fs.writeFileSync(lockPath, String(now), { flag: 'wx' });
|
|
492
|
+
}
|
|
493
|
+
catch (e) {
|
|
494
|
+
// 파일이 이미 존재 (다른 hook 인스턴스가 처리 중) → mtime 확인 후 5초 내면 차단
|
|
495
|
+
if (e.code === 'EEXIST') {
|
|
496
|
+
try {
|
|
497
|
+
const lockMtime = fs.statSync(lockPath).mtimeMs;
|
|
498
|
+
if (now - lockMtime < 5000) {
|
|
499
|
+
process.exit(0); // 5초 내 재발화 차단
|
|
500
|
+
}
|
|
501
|
+
// 5초 지난 stale 락 → 덮어쓰기 (이 호출이 새 작업)
|
|
502
|
+
fs.writeFileSync(lockPath, String(now));
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// stat/write 실패는 fail-soft
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// 그 외 락 파일 에러는 무시 (fail-soft)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
413
511
|
// 디버그 로그
|
|
414
512
|
const debugLogPath = path.join(path.dirname(dbPath), 'session-end-debug.log');
|
|
415
513
|
const inputKeys = Object.keys(input);
|
|
416
514
|
const lastMsgLen = input.last_assistant_message?.length || 0;
|
|
417
|
-
const debugLine = `[${new Date().toISOString()}] project=${project} keys=[${inputKeys.join(',')}] transcript_path=${input.transcript_path || 'none'} last_msg_len=${lastMsgLen}\n`;
|
|
515
|
+
const debugLine = `[${new Date().toISOString()}] project=${project} sid=${input.session_id?.slice(0, 8) || 'none'} keys=[${inputKeys.join(',')}] transcript_path=${input.transcript_path || 'none'} last_msg_len=${lastMsgLen}\n`;
|
|
418
516
|
fs.appendFileSync(debugLogPath, debugLine);
|
|
419
517
|
if (!fs.existsSync(dbPath)) {
|
|
420
518
|
console.log('[SessionEnd] No DB found, skipping');
|
|
421
519
|
process.exit(0);
|
|
422
520
|
}
|
|
423
521
|
const db = new Database(dbPath);
|
|
522
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
424
523
|
// === 추출 시작 ===
|
|
425
524
|
let lastWork = '';
|
|
426
525
|
let nextTasks = [];
|
|
@@ -433,6 +532,8 @@ async function main() {
|
|
|
433
532
|
userRequests: { firstRequest: '', allRequests: [] },
|
|
434
533
|
recentAssistantMessages: [],
|
|
435
534
|
errorFixPairs: [],
|
|
535
|
+
firstTimestamp: null,
|
|
536
|
+
lastTimestamp: null,
|
|
436
537
|
};
|
|
437
538
|
if (input.transcript_path) {
|
|
438
539
|
transcript = await parseTranscriptSinglePass(input.transcript_path);
|
|
@@ -441,7 +542,9 @@ async function main() {
|
|
|
441
542
|
decisions = transcript.decisions;
|
|
442
543
|
}
|
|
443
544
|
// lastWork 결정 (우선순위 폴백)
|
|
444
|
-
const { firstRequest, allRequests } = transcript.userRequests;
|
|
545
|
+
const { firstRequest: rawFirstRequest, allRequests } = transcript.userRequests;
|
|
546
|
+
// firstRequest 슬래시 prefix 제거 (예: "/mcp-dev 측정" → "측정")
|
|
547
|
+
const firstRequest = rawFirstRequest ? (stripSlashPrefix(rawFirstRequest) || rawFirstRequest) : '';
|
|
445
548
|
// 2a: 사용자 요청 + 커밋 메시지 조합 (가장 이상적)
|
|
446
549
|
if (firstRequest && commitMessages.length > 0) {
|
|
447
550
|
lastWork = `${firstRequest} → ${commitMessages.slice(0, 2).join('; ')}`;
|
|
@@ -519,23 +622,52 @@ async function main() {
|
|
|
519
622
|
const fileNames = modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
|
|
520
623
|
lastWork = `Modified files: ${fileNames}`;
|
|
521
624
|
}
|
|
625
|
+
// Phase 5: 모든 last_work 결정 경로에 stripSlashPrefix 강제 적용
|
|
626
|
+
// 베이스라인: 2a 경로(firstRequest)만 stripSlashPrefix 적용되고 2c~2e 폴백은 미적용
|
|
627
|
+
// → 같은 transcript에서 두 hook 인스턴스가 서로 다른 경로로 들어가
|
|
628
|
+
// 한쪽은 "측정", 한쪽은 "/mcp-dev 측정"으로 분기 → Jaccard 0.85 미달로 둘 다 통과
|
|
629
|
+
// stripSlashPrefix가 빈 문자열을 반환하면 원본 유지 (의미 토큰이 없는 경우)
|
|
630
|
+
if (lastWork) {
|
|
631
|
+
const stripped = stripSlashPrefix(lastWork);
|
|
632
|
+
if (stripped)
|
|
633
|
+
lastWork = stripped;
|
|
634
|
+
}
|
|
522
635
|
// 빈 세션 skip
|
|
523
636
|
if (!lastWork) {
|
|
524
637
|
console.log(`[SessionEnd] Skipping empty session for ${project} (no meaningful last_work)`);
|
|
525
638
|
db.close();
|
|
526
639
|
process.exit(0);
|
|
527
640
|
}
|
|
528
|
-
// 중복 저장 방지
|
|
529
|
-
|
|
641
|
+
// 중복 저장 방지 — Jaccard 유사도 기반 (1시간 이내, 동일 프로젝트)
|
|
642
|
+
// 베이스라인: 동일 last_work가 4~5분 간격으로 반복 INSERT되는 케이스 5건/24h 발견
|
|
643
|
+
// 정확 일치 + Jaccard >= 0.85 둘 다 차단
|
|
644
|
+
const recentExact = db.prepare(`
|
|
530
645
|
SELECT id FROM sessions
|
|
531
646
|
WHERE project = ? AND last_work = ? AND timestamp > datetime('now', '-1 hour')
|
|
532
647
|
LIMIT 1
|
|
533
648
|
`).get(project, lastWork);
|
|
534
|
-
if (
|
|
535
|
-
console.log(`[SessionEnd] Skipping duplicate
|
|
649
|
+
if (recentExact) {
|
|
650
|
+
console.log(`[SessionEnd] Skipping duplicate (exact) for ${project}`);
|
|
536
651
|
db.close();
|
|
537
652
|
process.exit(0);
|
|
538
653
|
}
|
|
654
|
+
const recentRows = db.prepare(`
|
|
655
|
+
SELECT last_work FROM sessions
|
|
656
|
+
WHERE project = ? AND timestamp > datetime('now', '-1 hour')
|
|
657
|
+
ORDER BY timestamp DESC LIMIT 10
|
|
658
|
+
`).all(project);
|
|
659
|
+
for (const row of recentRows) {
|
|
660
|
+
if (!row.last_work)
|
|
661
|
+
continue;
|
|
662
|
+
// Phase 5: 비교 전 양쪽 모두 stripSlashPrefix 정규화 → 표현 차이 흡수
|
|
663
|
+
const normalizedCurrent = stripSlashPrefix(lastWork) || lastWork;
|
|
664
|
+
const normalizedRow = stripSlashPrefix(row.last_work) || row.last_work;
|
|
665
|
+
if (jaccardSimilarity(normalizedCurrent, normalizedRow) >= 0.85) {
|
|
666
|
+
console.log(`[SessionEnd] Skipping near-duplicate (jaccard >= 0.85) for ${project}`);
|
|
667
|
+
db.close();
|
|
668
|
+
process.exit(0);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
539
671
|
// 구조화 메타데이터 (issues 컬럼 활용)
|
|
540
672
|
const metadata = {
|
|
541
673
|
commits: commitMessages,
|
|
@@ -543,11 +675,20 @@ async function main() {
|
|
|
543
675
|
errorsSolved
|
|
544
676
|
};
|
|
545
677
|
const hasMetadata = commitMessages.length > 0 || decisions.length > 0 || errorsSolved.length > 0;
|
|
678
|
+
// 세션 duration 계산 (transcript first ↔ last timestamp)
|
|
679
|
+
let durationMinutes = null;
|
|
680
|
+
if (transcript.firstTimestamp && transcript.lastTimestamp) {
|
|
681
|
+
const start = new Date(transcript.firstTimestamp).getTime();
|
|
682
|
+
const end = new Date(transcript.lastTimestamp).getTime();
|
|
683
|
+
if (!isNaN(start) && !isNaN(end) && end >= start) {
|
|
684
|
+
durationMinutes = Math.round((end - start) / 60000);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
546
687
|
// 세션 기록 저장
|
|
547
688
|
db.prepare(`
|
|
548
|
-
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues)
|
|
549
|
-
VALUES (?, ?, ?, ?, ?)
|
|
550
|
-
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null);
|
|
689
|
+
INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues, duration_minutes)
|
|
690
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
691
|
+
`).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null, durationMinutes);
|
|
551
692
|
// 활성 컨텍스트 업데이트
|
|
552
693
|
db.prepare(`
|
|
553
694
|
INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
|
|
@@ -588,6 +729,35 @@ async function main() {
|
|
|
588
729
|
}
|
|
589
730
|
catch { /* project_context table may not exist */ }
|
|
590
731
|
}
|
|
732
|
+
// 고품질 자동 메모리 추출 (v1.10 노이즈 제거 정책 유지하면서 가치 있는 것만)
|
|
733
|
+
// - decisions: 의미있는 의사결정 (importance=7)
|
|
734
|
+
// - commits: feat/fix만 (importance=6)
|
|
735
|
+
// - 동일 content 중복 방지 (검색해서 없을 때만 INSERT)
|
|
736
|
+
try {
|
|
737
|
+
const memoryDupCheck = db.prepare('SELECT id FROM memories WHERE project = ? AND content = ? LIMIT 1');
|
|
738
|
+
const memoryInsert = db.prepare(`
|
|
739
|
+
INSERT INTO memories (content, memory_type, tags, project, importance)
|
|
740
|
+
VALUES (?, ?, ?, ?, ?)
|
|
741
|
+
`);
|
|
742
|
+
for (const decision of decisions) {
|
|
743
|
+
if (decision.length < 15)
|
|
744
|
+
continue;
|
|
745
|
+
if (!memoryDupCheck.get(project, decision)) {
|
|
746
|
+
memoryInsert.run(decision, 'decision', JSON.stringify(['auto-extracted']), project, 7);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
// feat/fix 커밋만 (chore, docs, style은 학습 가치 낮음)
|
|
750
|
+
for (const commit of commitMessages) {
|
|
751
|
+
if (!/^(feat|fix)(\(.+\))?:/i.test(commit))
|
|
752
|
+
continue;
|
|
753
|
+
if (commit.length < 20)
|
|
754
|
+
continue;
|
|
755
|
+
if (!memoryDupCheck.get(project, commit)) {
|
|
756
|
+
memoryInsert.run(commit, 'learning', JSON.stringify(['auto-extracted', 'commit']), project, 6);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch { /* memories table issue, skip */ }
|
|
591
761
|
// 세션 임베딩 사전 생성 (search_sessions 성능 최적화)
|
|
592
762
|
try {
|
|
593
763
|
const lastSession = db.prepare('SELECT id FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
@@ -72,6 +72,7 @@ function loadContext(dbPath, project) {
|
|
|
72
72
|
return null;
|
|
73
73
|
try {
|
|
74
74
|
const db = new Database(dbPath);
|
|
75
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
75
76
|
// 노이즈 메모리 자동 정리
|
|
76
77
|
cleanupNoiseMemories(db);
|
|
77
78
|
const lines = [`# ${project} - Session Resumed\n`];
|
|
@@ -46,14 +46,19 @@ function getProject(cwd, workspaceRoot) {
|
|
|
46
46
|
}
|
|
47
47
|
// ===== 과거 참조 자동 감지 =====
|
|
48
48
|
const PAST_REFERENCE_PATTERNS = [
|
|
49
|
-
// 한국어
|
|
49
|
+
// 한국어 - 시간 참조
|
|
50
50
|
/(?:저번에|전에|이전에|그때|지난번에|예전에|아까)\s+(.+?)(?:\s*(?:어떻게|뭐|무엇|왜|어디|언제))/,
|
|
51
51
|
/(?:했던|했었던|만들었던|수정했던|구현했던|해결했던)\s*(.+)/,
|
|
52
52
|
/(?:지난|이전|전)\s*(?:세션|작업|시간|번).*?(?:에서|때)\s*(.+)/,
|
|
53
|
+
// 한국어 - 보유/기억 질문 ("내꺼 GCP 코인 정보 가지고 있나?")
|
|
54
|
+
/(?:내|내꺼|우리)\s+(.+?)\s+(?:가지고\s*있|있어|있나|있지|있냐|남아|남았|저장)/,
|
|
55
|
+
/(.+?)\s+(?:기억해|기억하고|기억나|알고\s*있|아는)/,
|
|
56
|
+
/(?:저장한|기록한|적어둔|메모한|남긴)\s+(.+)/,
|
|
53
57
|
// 영어
|
|
54
58
|
/(?:last time|before|previously|earlier)\s+(?:.*?)\s*((?:how|what|why|where|when).*)/i,
|
|
55
59
|
/(?:did we|did I|have we|have I)\s+(.+)\s+(?:before|last time|earlier)/i,
|
|
56
|
-
/(?:remember when|recall when)\s+(.+)/i,
|
|
60
|
+
/(?:remember when|recall when|do you remember|do you recall)\s+(.+)/i,
|
|
61
|
+
/(?:do you have|do you know|got)\s+(.+?)\s+(?:info|information|saved|stored|record)/i,
|
|
57
62
|
];
|
|
58
63
|
function extractPastKeywords(prompt) {
|
|
59
64
|
for (const pattern of PAST_REFERENCE_PATTERNS) {
|
|
@@ -176,6 +181,7 @@ const MAX_DIRECTIVES = 20;
|
|
|
176
181
|
function extractAndSaveDirectives(dbPath, project, prompt) {
|
|
177
182
|
try {
|
|
178
183
|
const db = new Database(dbPath);
|
|
184
|
+
db.pragma('journal_mode = WAL'); // 다중 hook 프로세스 동시성 보장
|
|
179
185
|
for (const { pattern, priority } of DIRECTIVE_PATTERNS) {
|
|
180
186
|
const match = prompt.match(pattern);
|
|
181
187
|
if (match && match[1]) {
|
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);
|
|
@@ -396,54 +411,54 @@ const tools = [
|
|
|
396
411
|
// ===== 1. 세션/컨텍스트 (4개) =====
|
|
397
412
|
{
|
|
398
413
|
name: 'session_start',
|
|
399
|
-
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.',
|
|
400
415
|
inputSchema: {
|
|
401
416
|
type: 'object',
|
|
402
417
|
properties: {
|
|
403
|
-
project: { type: 'string', description: '
|
|
404
|
-
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.' }
|
|
405
420
|
},
|
|
406
421
|
required: ['project']
|
|
407
422
|
}
|
|
408
423
|
},
|
|
409
424
|
{
|
|
410
425
|
name: 'session_end',
|
|
411
|
-
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.',
|
|
412
427
|
inputSchema: {
|
|
413
428
|
type: 'object',
|
|
414
429
|
properties: {
|
|
415
|
-
project: { type: 'string', description: '
|
|
416
|
-
summary: { type: 'string', description: '
|
|
417
|
-
workDone: { type: 'string', description: '
|
|
418
|
-
nextSteps: { type: 'array', items: { type: 'string' }, description: '
|
|
419
|
-
modifiedFiles: { type: 'array', items: { type: 'string' }, description: '
|
|
420
|
-
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)' }
|
|
421
436
|
},
|
|
422
437
|
required: ['project', 'summary']
|
|
423
438
|
}
|
|
424
439
|
},
|
|
425
440
|
{
|
|
426
441
|
name: 'session_history',
|
|
427
|
-
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.',
|
|
428
443
|
inputSchema: {
|
|
429
444
|
type: 'object',
|
|
430
445
|
properties: {
|
|
431
|
-
project: { type: 'string', description: '
|
|
432
|
-
limit: { type: 'number', description: '
|
|
433
|
-
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)' }
|
|
434
449
|
},
|
|
435
450
|
required: ['project']
|
|
436
451
|
}
|
|
437
452
|
},
|
|
438
453
|
{
|
|
439
454
|
name: 'search_sessions',
|
|
440
|
-
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.',
|
|
441
456
|
inputSchema: {
|
|
442
457
|
type: 'object',
|
|
443
458
|
properties: {
|
|
444
|
-
query: { type: 'string', description: '
|
|
445
|
-
project: { type: 'string', description: '
|
|
446
|
-
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)' }
|
|
447
462
|
},
|
|
448
463
|
required: ['query']
|
|
449
464
|
}
|
|
@@ -451,42 +466,42 @@ const tools = [
|
|
|
451
466
|
// ===== 2. 프로젝트 관리 (4개) =====
|
|
452
467
|
{
|
|
453
468
|
name: 'project_status',
|
|
454
|
-
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.',
|
|
455
470
|
inputSchema: {
|
|
456
471
|
type: 'object',
|
|
457
472
|
properties: {
|
|
458
|
-
project: { type: 'string', description: '
|
|
473
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
459
474
|
},
|
|
460
475
|
required: ['project']
|
|
461
476
|
}
|
|
462
477
|
},
|
|
463
478
|
{
|
|
464
479
|
name: 'project_init',
|
|
465
|
-
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.',
|
|
466
481
|
inputSchema: {
|
|
467
482
|
type: 'object',
|
|
468
483
|
properties: {
|
|
469
|
-
project: { type: 'string', description: '
|
|
470
|
-
techStack: { type: 'object', description: '
|
|
471
|
-
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' }
|
|
472
487
|
},
|
|
473
488
|
required: ['project']
|
|
474
489
|
}
|
|
475
490
|
},
|
|
476
491
|
{
|
|
477
492
|
name: 'project_analyze',
|
|
478
|
-
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.',
|
|
479
494
|
inputSchema: {
|
|
480
495
|
type: 'object',
|
|
481
496
|
properties: {
|
|
482
|
-
project: { type: 'string', description: '
|
|
497
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
483
498
|
},
|
|
484
499
|
required: ['project']
|
|
485
500
|
}
|
|
486
501
|
},
|
|
487
502
|
{
|
|
488
503
|
name: 'list_projects',
|
|
489
|
-
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.',
|
|
490
505
|
inputSchema: {
|
|
491
506
|
type: 'object',
|
|
492
507
|
properties: {}
|
|
@@ -495,52 +510,52 @@ const tools = [
|
|
|
495
510
|
// ===== 3. 태스크/백로그 (4개) =====
|
|
496
511
|
{
|
|
497
512
|
name: 'task_add',
|
|
498
|
-
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).',
|
|
499
514
|
inputSchema: {
|
|
500
515
|
type: 'object',
|
|
501
516
|
properties: {
|
|
502
|
-
project: { type: 'string', description: '
|
|
503
|
-
title: { type: 'string', description: '
|
|
504
|
-
description: { type: 'string', description: '
|
|
505
|
-
priority: { type: 'number', description: '
|
|
506
|
-
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)' }
|
|
507
522
|
},
|
|
508
523
|
required: ['project', 'title']
|
|
509
524
|
}
|
|
510
525
|
},
|
|
511
526
|
{
|
|
512
527
|
name: 'task_update',
|
|
513
|
-
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.',
|
|
514
529
|
inputSchema: {
|
|
515
530
|
type: 'object',
|
|
516
531
|
properties: {
|
|
517
|
-
taskId: { type: 'number', description: '
|
|
518
|
-
status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: '
|
|
519
|
-
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)' }
|
|
520
535
|
},
|
|
521
536
|
required: ['taskId', 'status']
|
|
522
537
|
}
|
|
523
538
|
},
|
|
524
539
|
{
|
|
525
540
|
name: 'task_list',
|
|
526
|
-
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.',
|
|
527
542
|
inputSchema: {
|
|
528
543
|
type: 'object',
|
|
529
544
|
properties: {
|
|
530
|
-
project: { type: 'string', description: '
|
|
531
|
-
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")' }
|
|
532
547
|
},
|
|
533
548
|
required: ['project']
|
|
534
549
|
}
|
|
535
550
|
},
|
|
536
551
|
{
|
|
537
552
|
name: 'task_suggest',
|
|
538
|
-
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.',
|
|
539
554
|
inputSchema: {
|
|
540
555
|
type: 'object',
|
|
541
556
|
properties: {
|
|
542
|
-
project: { type: 'string', description: '
|
|
543
|
-
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")' }
|
|
544
559
|
},
|
|
545
560
|
required: ['project']
|
|
546
561
|
}
|
|
@@ -548,41 +563,41 @@ const tools = [
|
|
|
548
563
|
// ===== 4. 솔루션 아카이브 (3개) =====
|
|
549
564
|
{
|
|
550
565
|
name: 'solution_record',
|
|
551
|
-
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.',
|
|
552
567
|
inputSchema: {
|
|
553
568
|
type: 'object',
|
|
554
569
|
properties: {
|
|
555
|
-
project: { type: 'string', description: '
|
|
556
|
-
errorSignature: { type: 'string', description: '
|
|
557
|
-
errorMessage: { type: 'string', description: '
|
|
558
|
-
solution: { type: 'string', description: '
|
|
559
|
-
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' }
|
|
560
575
|
},
|
|
561
576
|
required: ['errorSignature', 'solution']
|
|
562
577
|
}
|
|
563
578
|
},
|
|
564
579
|
{
|
|
565
580
|
name: 'solution_find',
|
|
566
|
-
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.',
|
|
567
582
|
inputSchema: {
|
|
568
583
|
type: 'object',
|
|
569
584
|
properties: {
|
|
570
|
-
query: { type: 'string', description: '
|
|
571
|
-
project: { type: 'string', description: '
|
|
572
|
-
limit: { type: 'number', description: '
|
|
573
|
-
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)' }
|
|
574
589
|
},
|
|
575
590
|
required: ['query']
|
|
576
591
|
}
|
|
577
592
|
},
|
|
578
593
|
{
|
|
579
594
|
name: 'solution_suggest',
|
|
580
|
-
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.',
|
|
581
596
|
inputSchema: {
|
|
582
597
|
type: 'object',
|
|
583
598
|
properties: {
|
|
584
|
-
errorMessage: { type: 'string', description: '
|
|
585
|
-
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)' }
|
|
586
601
|
},
|
|
587
602
|
required: ['errorMessage']
|
|
588
603
|
}
|
|
@@ -590,35 +605,35 @@ const tools = [
|
|
|
590
605
|
// ===== 5. 검증/품질 (3개) =====
|
|
591
606
|
{
|
|
592
607
|
name: 'verify_build',
|
|
593
|
-
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.',
|
|
594
609
|
inputSchema: {
|
|
595
610
|
type: 'object',
|
|
596
611
|
properties: {
|
|
597
|
-
project: { type: 'string', description: '
|
|
612
|
+
project: { type: 'string', description: 'Project name (must match a directory under apps/)' }
|
|
598
613
|
},
|
|
599
614
|
required: ['project']
|
|
600
615
|
}
|
|
601
616
|
},
|
|
602
617
|
{
|
|
603
618
|
name: 'verify_test',
|
|
604
|
-
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.',
|
|
605
620
|
inputSchema: {
|
|
606
621
|
type: 'object',
|
|
607
622
|
properties: {
|
|
608
|
-
project: { type: 'string', description: '
|
|
609
|
-
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)' }
|
|
610
625
|
},
|
|
611
626
|
required: ['project']
|
|
612
627
|
}
|
|
613
628
|
},
|
|
614
629
|
{
|
|
615
630
|
name: 'verify_all',
|
|
616
|
-
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.',
|
|
617
632
|
inputSchema: {
|
|
618
633
|
type: 'object',
|
|
619
634
|
properties: {
|
|
620
|
-
project: { type: 'string', description: '
|
|
621
|
-
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)' }
|
|
622
637
|
},
|
|
623
638
|
required: ['project']
|
|
624
639
|
}
|
|
@@ -626,117 +641,117 @@ const tools = [
|
|
|
626
641
|
// ===== 6. 메모리 시스템 (4개) - v4 신규 =====
|
|
627
642
|
{
|
|
628
643
|
name: 'memory_store',
|
|
629
|
-
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.',
|
|
630
645
|
inputSchema: {
|
|
631
646
|
type: 'object',
|
|
632
647
|
properties: {
|
|
633
|
-
content: { type: 'string', description: '
|
|
648
|
+
content: { type: 'string', description: 'The knowledge content to store' },
|
|
634
649
|
type: {
|
|
635
650
|
type: 'string',
|
|
636
651
|
enum: ['observation', 'decision', 'learning', 'error', 'pattern'],
|
|
637
|
-
description: '
|
|
652
|
+
description: 'Memory type: observation (discovery/finding), decision (architecture/tech choice), learning (new knowledge), error (error encountered), pattern (code convention)'
|
|
638
653
|
},
|
|
639
|
-
project: { type: 'string', description: '
|
|
640
|
-
tags: { type: 'array', items: { type: 'string' }, description: '
|
|
641
|
-
importance: { type: 'number', description: '
|
|
642
|
-
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)' }
|
|
643
658
|
},
|
|
644
659
|
required: ['content', 'type']
|
|
645
660
|
}
|
|
646
661
|
},
|
|
647
662
|
{
|
|
648
663
|
name: 'memory_search',
|
|
649
|
-
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.',
|
|
650
665
|
inputSchema: {
|
|
651
666
|
type: 'object',
|
|
652
667
|
properties: {
|
|
653
|
-
query: { type: 'string', description: '
|
|
668
|
+
query: { type: 'string', description: 'Natural language search query' },
|
|
654
669
|
type: {
|
|
655
670
|
type: 'string',
|
|
656
671
|
enum: ['observation', 'decision', 'learning', 'error', 'pattern', 'all'],
|
|
657
|
-
description: '
|
|
672
|
+
description: 'Filter by memory type (default: "all")'
|
|
658
673
|
},
|
|
659
|
-
project: { type: 'string', description: '
|
|
660
|
-
tags: { type: 'array', items: { type: 'string' }, description: '
|
|
661
|
-
semantic: { type: 'boolean', description: '
|
|
662
|
-
minImportance: { type: 'number', description: '
|
|
663
|
-
limit: { type: 'number', description: '
|
|
664
|
-
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)' }
|
|
665
680
|
},
|
|
666
681
|
required: ['query']
|
|
667
682
|
}
|
|
668
683
|
},
|
|
669
684
|
{
|
|
670
685
|
name: 'memory_get',
|
|
671
|
-
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.',
|
|
672
687
|
inputSchema: {
|
|
673
688
|
type: 'object',
|
|
674
689
|
properties: {
|
|
675
|
-
ids: { type: 'array', items: { type: 'number' }, description: '
|
|
690
|
+
ids: { type: 'array', items: { type: 'number' }, description: 'Array of memory IDs to retrieve (max 20)' }
|
|
676
691
|
},
|
|
677
692
|
required: ['ids']
|
|
678
693
|
}
|
|
679
694
|
},
|
|
680
695
|
{
|
|
681
696
|
name: 'memory_related',
|
|
682
|
-
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.',
|
|
683
698
|
inputSchema: {
|
|
684
699
|
type: 'object',
|
|
685
700
|
properties: {
|
|
686
|
-
memoryId: { type: 'number', description: '
|
|
687
|
-
includeGraph: { type: 'boolean', description: '
|
|
688
|
-
includeSemantic: { type: 'boolean', description: '
|
|
689
|
-
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)' }
|
|
690
705
|
},
|
|
691
706
|
required: ['memoryId']
|
|
692
707
|
}
|
|
693
708
|
},
|
|
694
709
|
{
|
|
695
710
|
name: 'memory_stats',
|
|
696
|
-
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.',
|
|
697
712
|
inputSchema: {
|
|
698
713
|
type: 'object',
|
|
699
714
|
properties: {
|
|
700
|
-
project: { type: 'string', description: '
|
|
715
|
+
project: { type: 'string', description: 'Scope statistics to a single project (optional — omit for global stats)' }
|
|
701
716
|
}
|
|
702
717
|
}
|
|
703
718
|
},
|
|
704
719
|
// ===== 7. 지식 그래프 (2개) - v4 신규 =====
|
|
705
720
|
{
|
|
706
721
|
name: 'graph_connect',
|
|
707
|
-
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.',
|
|
708
723
|
inputSchema: {
|
|
709
724
|
type: 'object',
|
|
710
725
|
properties: {
|
|
711
|
-
sourceId: { type: 'number', description: '
|
|
712
|
-
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)' },
|
|
713
728
|
relation: {
|
|
714
729
|
type: 'string',
|
|
715
730
|
enum: ['related_to', 'causes', 'solves', 'depends_on', 'contradicts', 'extends', 'example_of'],
|
|
716
|
-
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)'
|
|
717
732
|
},
|
|
718
|
-
strength: { type: 'number', description: '
|
|
733
|
+
strength: { type: 'number', description: 'Connection strength 0.0-1.0 (default: 1.0). Lower values indicate weaker associations.' }
|
|
719
734
|
},
|
|
720
735
|
required: ['sourceId', 'targetId', 'relation']
|
|
721
736
|
}
|
|
722
737
|
},
|
|
723
738
|
{
|
|
724
739
|
name: 'graph_explore',
|
|
725
|
-
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.',
|
|
726
741
|
inputSchema: {
|
|
727
742
|
type: 'object',
|
|
728
743
|
properties: {
|
|
729
|
-
memoryId: { type: 'number', description: '
|
|
730
|
-
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.' },
|
|
731
746
|
relation: {
|
|
732
747
|
type: 'string',
|
|
733
748
|
enum: ['related_to', 'causes', 'solves', 'depends_on', 'contradicts', 'extends', 'example_of', 'all'],
|
|
734
|
-
description: '
|
|
749
|
+
description: 'Filter by relation type (default: "all")'
|
|
735
750
|
},
|
|
736
751
|
direction: {
|
|
737
752
|
type: 'string',
|
|
738
753
|
enum: ['outgoing', 'incoming', 'both'],
|
|
739
|
-
description: '
|
|
754
|
+
description: 'Traversal direction — outgoing (A→B), incoming (B→A), or both (default: "both")'
|
|
740
755
|
}
|
|
741
756
|
},
|
|
742
757
|
required: ['memoryId']
|