claude-session-continuity-mcp 1.16.2 → 1.17.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/dist/index-old.js DELETED
@@ -1,3426 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
- import * as fs from 'fs/promises';
6
- import * as path from 'path';
7
- import { execSync } from 'child_process';
8
- import Database from 'better-sqlite3';
9
- // @ts-ignore - transformers.js
10
- import { pipeline, env } from '@xenova/transformers';
11
- // 모델 캐시 설정
12
- env.cacheDir = path.join(process.env.HOME || '/tmp', '.cache', 'transformers');
13
- env.allowLocalModels = true;
14
- // 기본 경로 설정
15
- const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/Users/ibyeongchang/Documents/dev/ai-service-generator';
16
- const APPS_DIR = path.join(WORKSPACE_ROOT, 'apps');
17
- const DB_PATH = path.join(WORKSPACE_ROOT, '.claude', 'sessions.db');
18
- // ===== SQLite 데이터베이스 초기화 =====
19
- const db = new Database(DB_PATH);
20
- // 테이블 생성
21
- db.exec(`
22
- CREATE TABLE IF NOT EXISTS sessions (
23
- id INTEGER PRIMARY KEY AUTOINCREMENT,
24
- project TEXT NOT NULL,
25
- timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
26
- last_work TEXT NOT NULL,
27
- current_status TEXT,
28
- next_tasks TEXT,
29
- modified_files TEXT,
30
- issues TEXT,
31
- verification_result TEXT,
32
- duration_minutes INTEGER
33
- );
34
-
35
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
36
- CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp);
37
-
38
- CREATE TABLE IF NOT EXISTS work_patterns (
39
- id INTEGER PRIMARY KEY AUTOINCREMENT,
40
- project TEXT NOT NULL,
41
- work_type TEXT NOT NULL,
42
- description TEXT,
43
- files_pattern TEXT,
44
- success_rate REAL DEFAULT 0,
45
- avg_duration_minutes REAL DEFAULT 0,
46
- count INTEGER DEFAULT 1,
47
- last_used DATETIME DEFAULT CURRENT_TIMESTAMP
48
- );
49
-
50
- CREATE INDEX IF NOT EXISTS idx_patterns_project ON work_patterns(project);
51
- CREATE INDEX IF NOT EXISTS idx_patterns_type ON work_patterns(work_type);
52
-
53
- -- ===== 메모리 시스템 테이블 (mcp-memory-service 영감) =====
54
-
55
- CREATE TABLE IF NOT EXISTS memories (
56
- id INTEGER PRIMARY KEY AUTOINCREMENT,
57
- content TEXT NOT NULL,
58
- memory_type TEXT NOT NULL DEFAULT 'observation',
59
- tags TEXT,
60
- project TEXT,
61
- importance INTEGER DEFAULT 5,
62
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
63
- accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
64
- access_count INTEGER DEFAULT 0,
65
- metadata TEXT
66
- );
67
-
68
- CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(memory_type);
69
- CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
70
- CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
71
- CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at);
72
-
73
- -- FTS5 전체 텍스트 검색 (시맨틱 검색 대용)
74
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
75
- content,
76
- tags,
77
- content='memories',
78
- content_rowid='id'
79
- );
80
-
81
- -- FTS 트리거
82
- CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
83
- INSERT INTO memories_fts(rowid, content, tags) VALUES (new.id, new.content, new.tags);
84
- END;
85
-
86
- CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
87
- INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES('delete', old.id, old.content, old.tags);
88
- END;
89
-
90
- CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
91
- INSERT INTO memories_fts(memories_fts, rowid, content, tags) VALUES('delete', old.id, old.content, old.tags);
92
- INSERT INTO memories_fts(rowid, content, tags) VALUES (new.id, new.content, new.tags);
93
- END;
94
-
95
- -- 지식 그래프 관계 테이블
96
- CREATE TABLE IF NOT EXISTS memory_relations (
97
- id INTEGER PRIMARY KEY AUTOINCREMENT,
98
- source_id INTEGER NOT NULL,
99
- target_id INTEGER NOT NULL,
100
- relation_type TEXT NOT NULL,
101
- strength REAL DEFAULT 1.0,
102
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
103
- FOREIGN KEY (source_id) REFERENCES memories(id) ON DELETE CASCADE,
104
- FOREIGN KEY (target_id) REFERENCES memories(id) ON DELETE CASCADE,
105
- UNIQUE(source_id, target_id, relation_type)
106
- );
107
-
108
- CREATE INDEX IF NOT EXISTS idx_relations_source ON memory_relations(source_id);
109
- CREATE INDEX IF NOT EXISTS idx_relations_target ON memory_relations(target_id);
110
- CREATE INDEX IF NOT EXISTS idx_relations_type ON memory_relations(relation_type);
111
-
112
- -- ===== 시맨틱 검색용 임베딩 테이블 =====
113
- CREATE TABLE IF NOT EXISTS embeddings (
114
- memory_id INTEGER PRIMARY KEY,
115
- embedding BLOB NOT NULL,
116
- model TEXT DEFAULT 'all-MiniLM-L6-v2',
117
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
118
- FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
119
- );
120
-
121
- -- ===== Content Filtering 학습 테이블 =====
122
- CREATE TABLE IF NOT EXISTS content_filter_patterns (
123
- id INTEGER PRIMARY KEY AUTOINCREMENT,
124
- pattern_type TEXT NOT NULL,
125
- pattern_description TEXT NOT NULL,
126
- file_extension TEXT,
127
- example_context TEXT,
128
- mitigation_strategy TEXT,
129
- occurrence_count INTEGER DEFAULT 1,
130
- last_occurred DATETIME DEFAULT CURRENT_TIMESTAMP,
131
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
132
- );
133
-
134
- CREATE INDEX IF NOT EXISTS idx_filter_patterns_type ON content_filter_patterns(pattern_type);
135
-
136
- -- ===== 프로젝트 연속성 시스템 (v2) =====
137
-
138
- -- Layer 1: 프로젝트 고정 컨텍스트 (거의 안 바뀜)
139
- CREATE TABLE IF NOT EXISTS project_context (
140
- project TEXT PRIMARY KEY,
141
- tech_stack TEXT, -- JSON: {framework, language, database, ...}
142
- architecture_decisions TEXT, -- JSON array: 핵심 아키텍처 결정 (최대 5개)
143
- code_patterns TEXT, -- JSON array: 코드 컨벤션/패턴 (최대 5개)
144
- special_notes TEXT, -- 프로젝트 특이사항
145
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
146
- );
147
-
148
- -- Layer 2: 활성 작업 컨텍스트 (자주 바뀜)
149
- CREATE TABLE IF NOT EXISTS active_context (
150
- project TEXT PRIMARY KEY,
151
- current_state TEXT, -- 현재 상태 (1줄 요약)
152
- active_tasks TEXT, -- JSON array: [{id, title, status}] (최대 3개)
153
- recent_files TEXT, -- JSON array: 최근 수정 파일 (최대 10개)
154
- blockers TEXT, -- 블로커/이슈
155
- last_verification TEXT, -- passed/failed
156
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
157
- );
158
-
159
- -- Layer 3: 태스크 백로그 (영구 보존)
160
- CREATE TABLE IF NOT EXISTS tasks (
161
- id INTEGER PRIMARY KEY AUTOINCREMENT,
162
- project TEXT NOT NULL,
163
- title TEXT NOT NULL,
164
- description TEXT,
165
- status TEXT DEFAULT 'pending', -- pending, in_progress, done, blocked
166
- priority INTEGER DEFAULT 5, -- 1-10 (10이 가장 높음)
167
- related_files TEXT, -- JSON array
168
- acceptance_criteria TEXT,
169
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
170
- completed_at DATETIME
171
- );
172
-
173
- CREATE INDEX IF NOT EXISTS idx_tasks_project_status ON tasks(project, status);
174
- CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority DESC);
175
-
176
- -- Layer 3: 해결된 이슈 아카이브 (검색용)
177
- CREATE TABLE IF NOT EXISTS resolved_issues (
178
- id INTEGER PRIMARY KEY AUTOINCREMENT,
179
- project TEXT,
180
- error_signature TEXT NOT NULL, -- 에러 패턴 (검색 키)
181
- error_message TEXT,
182
- solution TEXT NOT NULL,
183
- related_files TEXT, -- JSON array
184
- keywords TEXT, -- 자동 추출 키워드
185
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
186
- );
187
-
188
- CREATE INDEX IF NOT EXISTS idx_issues_signature ON resolved_issues(error_signature);
189
- CREATE INDEX IF NOT EXISTS idx_issues_project ON resolved_issues(project);
190
- `);
191
- // Content Filtering 패턴 캐시 (메모리에 로드)
192
- let contentFilterPatterns = [];
193
- function loadContentFilterPatterns() {
194
- try {
195
- const stmt = db.prepare(`
196
- SELECT id, pattern_type, pattern_description, file_extension, mitigation_strategy
197
- FROM content_filter_patterns
198
- ORDER BY occurrence_count DESC
199
- `);
200
- const rows = stmt.all();
201
- contentFilterPatterns = rows.map(r => ({
202
- id: r.id,
203
- patternType: r.pattern_type,
204
- patternDescription: r.pattern_description,
205
- fileExtension: r.file_extension,
206
- mitigationStrategy: r.mitigation_strategy
207
- }));
208
- }
209
- catch {
210
- contentFilterPatterns = [];
211
- }
212
- }
213
- // 초기 로드
214
- loadContentFilterPatterns();
215
- // ===== 시맨틱 검색 엔진 =====
216
- let embeddingPipeline = null;
217
- let embeddingReady = false;
218
- async function initEmbedding() {
219
- if (embeddingPipeline)
220
- return;
221
- try {
222
- console.error('Loading embedding model (first time may take a while)...');
223
- embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
224
- embeddingReady = true;
225
- console.error('Embedding model loaded successfully!');
226
- }
227
- catch (error) {
228
- console.error('Failed to load embedding model:', error);
229
- }
230
- }
231
- // 백그라운드에서 모델 로드 시작
232
- initEmbedding();
233
- async function generateEmbedding(text) {
234
- if (!embeddingPipeline) {
235
- await initEmbedding();
236
- }
237
- if (!embeddingPipeline)
238
- return null;
239
- try {
240
- const output = await embeddingPipeline(text, { pooling: 'mean', normalize: true });
241
- return Array.from(output.data);
242
- }
243
- catch (error) {
244
- console.error('Embedding generation error:', error);
245
- return null;
246
- }
247
- }
248
- function cosineSimilarity(a, b) {
249
- if (a.length !== b.length)
250
- return 0;
251
- let dotProduct = 0;
252
- let normA = 0;
253
- let normB = 0;
254
- for (let i = 0; i < a.length; i++) {
255
- dotProduct += a[i] * b[i];
256
- normA += a[i] * a[i];
257
- normB += b[i] * b[i];
258
- }
259
- return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
260
- }
261
- function embeddingToBuffer(embedding) {
262
- const float32Array = new Float32Array(embedding);
263
- return Buffer.from(float32Array.buffer);
264
- }
265
- function bufferToEmbedding(buffer) {
266
- const float32Array = new Float32Array(buffer.buffer, buffer.byteOffset, buffer.length / 4);
267
- return Array.from(float32Array);
268
- }
269
- // MCP 서버 생성
270
- const server = new Server({ name: 'project-manager', version: '1.0.0' }, { capabilities: { tools: { listChanged: true } } });
271
- // ===== 유틸리티 함수 =====
272
- async function fileExists(filePath) {
273
- try {
274
- await fs.access(filePath);
275
- return true;
276
- }
277
- catch {
278
- return false;
279
- }
280
- }
281
- async function readFileContent(filePath) {
282
- try {
283
- return await fs.readFile(filePath, 'utf-8');
284
- }
285
- catch {
286
- return null;
287
- }
288
- }
289
- async function writeFileContent(filePath, content) {
290
- await fs.mkdir(path.dirname(filePath), { recursive: true });
291
- await fs.writeFile(filePath, content, 'utf-8');
292
- }
293
- function parseMarkdownTable(content, tableName) {
294
- const result = {};
295
- const lines = content.split('\n');
296
- let inTable = false;
297
- for (const line of lines) {
298
- if (line.includes(tableName)) {
299
- inTable = true;
300
- continue;
301
- }
302
- if (inTable && line.startsWith('|') && !line.includes('---')) {
303
- const cells = line.split('|').map(c => c.trim()).filter(Boolean);
304
- if (cells.length >= 2 && cells[0] !== '항목' && cells[0] !== '작업') {
305
- result[cells[0]] = cells[1];
306
- }
307
- }
308
- if (inTable && line.trim() === '') {
309
- inTable = false;
310
- }
311
- }
312
- return result;
313
- }
314
- const tools = [
315
- {
316
- name: 'list_projects',
317
- description: 'apps/ 디렉토리의 모든 프로젝트 목록과 상태를 반환합니다.',
318
- inputSchema: {
319
- type: 'object',
320
- properties: {},
321
- }
322
- },
323
- {
324
- name: 'get_session',
325
- description: '프로젝트의 SESSION.md를 파싱하여 구조화된 데이터로 반환합니다.',
326
- inputSchema: {
327
- type: 'object',
328
- properties: {
329
- project: { type: 'string', description: '프로젝트 이름' },
330
- includeRaw: { type: 'boolean', description: 'raw 전체 내용 포함 여부 (기본: false, 응답 크기 줄이기)' },
331
- maxContentLength: { type: 'number', description: '최대 내용 길이 (기본: 2000, 0이면 무제한)' }
332
- },
333
- required: ['project']
334
- }
335
- },
336
- {
337
- name: 'update_session',
338
- description: '프로젝트의 SESSION.md를 업데이트하고 DB에 이력을 저장합니다.',
339
- inputSchema: {
340
- type: 'object',
341
- properties: {
342
- project: { type: 'string', description: '프로젝트 이름' },
343
- lastWork: { type: 'string', description: '마지막 작업 내용' },
344
- currentStatus: { type: 'string', description: '현재 상태' },
345
- nextTasks: { type: 'array', items: { type: 'string' }, description: '다음 작업 목록' },
346
- modifiedFiles: { type: 'array', items: { type: 'string' }, description: '수정된 파일 목록' },
347
- issues: { type: 'array', items: { type: 'string' }, description: '알려진 이슈' },
348
- verificationResult: { type: 'string', description: '검증 결과 (passed/failed)' }
349
- },
350
- required: ['project', 'lastWork']
351
- }
352
- },
353
- {
354
- name: 'get_tech_stack',
355
- description: '프로젝트의 plan.md에서 기술 스택 정보를 추출합니다.',
356
- inputSchema: {
357
- type: 'object',
358
- properties: {
359
- project: { type: 'string', description: '프로젝트 이름' }
360
- },
361
- required: ['project']
362
- }
363
- },
364
- {
365
- name: 'run_verification',
366
- description: '프로젝트의 빌드/테스트/린트를 한 번에 실행하고 결과를 반환합니다.',
367
- inputSchema: {
368
- type: 'object',
369
- properties: {
370
- project: { type: 'string', description: '프로젝트 이름' },
371
- gates: {
372
- type: 'array',
373
- items: { type: 'string', enum: ['build', 'test', 'lint'] },
374
- description: '실행할 게이트 목록 (기본: 전체)'
375
- }
376
- },
377
- required: ['project']
378
- }
379
- },
380
- {
381
- name: 'detect_platform',
382
- description: '프로젝트의 플랫폼을 자동 감지합니다 (Web, Android, Flutter, Server).',
383
- inputSchema: {
384
- type: 'object',
385
- properties: {
386
- project: { type: 'string', description: '프로젝트 이름' }
387
- },
388
- required: ['project']
389
- }
390
- },
391
- // ===== 새로운 SQLite 기반 도구들 =====
392
- {
393
- name: 'save_session_history',
394
- description: '세션 기록을 DB에 저장합니다. update_session 호출 시 자동으로 호출됩니다.',
395
- inputSchema: {
396
- type: 'object',
397
- properties: {
398
- project: { type: 'string', description: '프로젝트 이름' },
399
- lastWork: { type: 'string', description: '작업 내용' },
400
- currentStatus: { type: 'string', description: '현재 상태' },
401
- nextTasks: { type: 'array', items: { type: 'string' }, description: '다음 작업' },
402
- modifiedFiles: { type: 'array', items: { type: 'string' }, description: '수정된 파일' },
403
- issues: { type: 'array', items: { type: 'string' }, description: '이슈' },
404
- verificationResult: { type: 'string', description: '검증 결과 (passed/failed)' },
405
- durationMinutes: { type: 'number', description: '작업 소요 시간 (분)' }
406
- },
407
- required: ['project', 'lastWork']
408
- }
409
- },
410
- {
411
- name: 'get_session_history',
412
- description: '프로젝트의 세션 이력을 조회합니다.',
413
- inputSchema: {
414
- type: 'object',
415
- properties: {
416
- project: { type: 'string', description: '프로젝트 이름 (없으면 전체)' },
417
- limit: { type: 'number', description: '조회 개수 (기본: 10)' },
418
- keyword: { type: 'string', description: '검색 키워드 (작업 내용에서 검색)' }
419
- }
420
- }
421
- },
422
- {
423
- name: 'search_similar_work',
424
- description: '과거에 비슷한 작업을 했는지 검색합니다. 이전 작업 패턴을 참고할 때 유용합니다.',
425
- inputSchema: {
426
- type: 'object',
427
- properties: {
428
- keyword: { type: 'string', description: '검색할 작업 키워드 (예: 로그인, API 연동)' },
429
- project: { type: 'string', description: '특정 프로젝트만 검색 (선택)' }
430
- },
431
- required: ['keyword']
432
- }
433
- },
434
- {
435
- name: 'get_project_stats',
436
- description: '프로젝트별 작업 통계를 조회합니다.',
437
- inputSchema: {
438
- type: 'object',
439
- properties: {
440
- project: { type: 'string', description: '프로젝트 이름 (없으면 전체)' }
441
- }
442
- }
443
- },
444
- {
445
- name: 'record_work_pattern',
446
- description: '자주 사용하는 작업 패턴을 기록합니다.',
447
- inputSchema: {
448
- type: 'object',
449
- properties: {
450
- project: { type: 'string', description: '프로젝트 이름' },
451
- workType: { type: 'string', description: '작업 유형 (예: feature, bugfix, refactor)' },
452
- description: { type: 'string', description: '작업 설명' },
453
- filesPattern: { type: 'string', description: '관련 파일 패턴' },
454
- success: { type: 'boolean', description: '성공 여부' },
455
- durationMinutes: { type: 'number', description: '소요 시간' }
456
- },
457
- required: ['project', 'workType', 'description']
458
- }
459
- },
460
- {
461
- name: 'get_work_patterns',
462
- description: '프로젝트의 작업 패턴을 조회합니다. 자주 하는 작업과 성공률을 확인할 수 있습니다.',
463
- inputSchema: {
464
- type: 'object',
465
- properties: {
466
- project: { type: 'string', description: '프로젝트 이름' },
467
- workType: { type: 'string', description: '작업 유형 필터' }
468
- }
469
- }
470
- },
471
- // ===== 메모리 시스템 도구 (mcp-memory-service 영감) =====
472
- {
473
- name: 'store_memory',
474
- description: '새로운 지식/학습/결정 사항을 메모리에 저장합니다. Claude가 배운 것들을 기억합니다.',
475
- inputSchema: {
476
- type: 'object',
477
- properties: {
478
- content: { type: 'string', description: '저장할 내용' },
479
- memoryType: {
480
- type: 'string',
481
- enum: ['observation', 'decision', 'learning', 'error', 'pattern', 'preference'],
482
- description: '메모리 유형 (observation: 관찰, decision: 결정, learning: 학습, error: 에러, pattern: 패턴, preference: 선호)'
483
- },
484
- tags: { type: 'array', items: { type: 'string' }, description: '태그 목록 (검색용)' },
485
- project: { type: 'string', description: '관련 프로젝트 (선택)' },
486
- importance: { type: 'number', description: '중요도 1-10 (기본: 5)' },
487
- metadata: { type: 'object', description: '추가 메타데이터 (선택)' }
488
- },
489
- required: ['content', 'memoryType']
490
- }
491
- },
492
- {
493
- name: 'recall_memory',
494
- description: '키워드로 관련 메모리를 검색합니다. FTS5 전체 텍스트 검색을 사용합니다.',
495
- inputSchema: {
496
- type: 'object',
497
- properties: {
498
- query: { type: 'string', description: '검색 쿼리 (자연어)' },
499
- memoryType: { type: 'string', description: '메모리 유형 필터 (선택)' },
500
- project: { type: 'string', description: '프로젝트 필터 (선택)' },
501
- limit: { type: 'number', description: '최대 결과 수 (기본: 10)' },
502
- minImportance: { type: 'number', description: '최소 중요도 (기본: 1)' },
503
- maxContentLength: { type: 'number', description: '각 메모리 내용 최대 길이 (기본: 500, 0이면 무제한)' }
504
- },
505
- required: ['query']
506
- }
507
- },
508
- {
509
- name: 'recall_by_timeframe',
510
- description: '특정 기간의 메모리를 조회합니다. (예: 오늘, 이번주, 지난달)',
511
- inputSchema: {
512
- type: 'object',
513
- properties: {
514
- timeframe: {
515
- type: 'string',
516
- enum: ['today', 'yesterday', 'this_week', 'last_week', 'this_month', 'last_month'],
517
- description: '조회 기간'
518
- },
519
- memoryType: { type: 'string', description: '메모리 유형 필터 (선택)' },
520
- project: { type: 'string', description: '프로젝트 필터 (선택)' },
521
- limit: { type: 'number', description: '최대 결과 수 (기본: 20)' }
522
- },
523
- required: ['timeframe']
524
- }
525
- },
526
- {
527
- name: 'search_by_tag',
528
- description: '태그로 메모리를 검색합니다.',
529
- inputSchema: {
530
- type: 'object',
531
- properties: {
532
- tags: { type: 'array', items: { type: 'string' }, description: '검색할 태그들' },
533
- matchAll: { type: 'boolean', description: '모든 태그 일치 필요 여부 (기본: false)' },
534
- limit: { type: 'number', description: '최대 결과 수 (기본: 20)' }
535
- },
536
- required: ['tags']
537
- }
538
- },
539
- {
540
- name: 'create_relation',
541
- description: '두 메모리 간의 관계를 생성합니다. (지식 그래프)',
542
- inputSchema: {
543
- type: 'object',
544
- properties: {
545
- sourceId: { type: 'number', description: '출발 메모리 ID' },
546
- targetId: { type: 'number', description: '도착 메모리 ID' },
547
- relationType: {
548
- type: 'string',
549
- enum: ['related_to', 'causes', 'solves', 'depends_on', 'contradicts', 'extends', 'example_of'],
550
- description: '관계 유형'
551
- },
552
- strength: { type: 'number', description: '관계 강도 0-1 (기본: 1.0)' }
553
- },
554
- required: ['sourceId', 'targetId', 'relationType']
555
- }
556
- },
557
- {
558
- name: 'find_connected_memories',
559
- description: '특정 메모리와 연결된 모든 메모리를 찾습니다. (지식 그래프 탐색)',
560
- inputSchema: {
561
- type: 'object',
562
- properties: {
563
- memoryId: { type: 'number', description: '기준 메모리 ID' },
564
- depth: { type: 'number', description: '탐색 깊이 (기본: 1, 최대: 3)' },
565
- relationType: { type: 'string', description: '관계 유형 필터 (선택)' }
566
- },
567
- required: ['memoryId']
568
- }
569
- },
570
- {
571
- name: 'get_memory_stats',
572
- description: '메모리 시스템 통계를 조회합니다.',
573
- inputSchema: {
574
- type: 'object',
575
- properties: {}
576
- }
577
- },
578
- {
579
- name: 'delete_memory',
580
- description: '메모리를 삭제합니다.',
581
- inputSchema: {
582
- type: 'object',
583
- properties: {
584
- memoryId: { type: 'number', description: '삭제할 메모리 ID' }
585
- },
586
- required: ['memoryId']
587
- }
588
- },
589
- // ===== 시맨틱 검색 도구 =====
590
- {
591
- name: 'semantic_search',
592
- description: '시맨틱 검색으로 의미적으로 유사한 메모리를 찾습니다. AI 임베딩(all-MiniLM-L6-v2)을 사용합니다.',
593
- inputSchema: {
594
- type: 'object',
595
- properties: {
596
- query: { type: 'string', description: '검색 쿼리 (자연어, 의미 기반 검색)' },
597
- limit: { type: 'number', description: '최대 결과 수 (기본: 10)' },
598
- minSimilarity: { type: 'number', description: '최소 유사도 0-1 (기본: 0.3)' },
599
- memoryType: { type: 'string', description: '메모리 유형 필터 (선택)' },
600
- project: { type: 'string', description: '프로젝트 필터 (선택)' }
601
- },
602
- required: ['query']
603
- }
604
- },
605
- {
606
- name: 'rebuild_embeddings',
607
- description: '모든 메모리의 임베딩을 다시 생성합니다. 모델 업데이트 후 사용합니다.',
608
- inputSchema: {
609
- type: 'object',
610
- properties: {
611
- force: { type: 'boolean', description: '기존 임베딩도 다시 생성 (기본: false, 누락된 것만)' }
612
- }
613
- }
614
- },
615
- {
616
- name: 'get_embedding_status',
617
- description: '임베딩 시스템 상태를 확인합니다.',
618
- inputSchema: {
619
- type: 'object',
620
- properties: {}
621
- }
622
- },
623
- // ===== 자동 피드백 수집 도구 =====
624
- {
625
- name: 'collect_work_feedback',
626
- description: '/work 작업 완료 시 자동으로 피드백을 수집합니다. 에러, 타임아웃, 불편사항 등을 기록합니다.',
627
- inputSchema: {
628
- type: 'object',
629
- properties: {
630
- project: { type: 'string', description: '작업한 프로젝트 이름' },
631
- workSummary: { type: 'string', description: '수행한 작업 요약' },
632
- feedbackType: {
633
- type: 'string',
634
- enum: ['bug', 'timeout', 'feature-request', 'ux', 'performance', 'none'],
635
- description: '피드백 유형 (none: 피드백 없음)'
636
- },
637
- feedbackContent: { type: 'string', description: '피드백 내용 (feedbackType이 none이 아닐 때)' },
638
- affectedTool: { type: 'string', description: '문제가 발생한 MCP 도구명 (선택)' },
639
- verificationPassed: { type: 'boolean', description: '검증 통과 여부' },
640
- duration: { type: 'number', description: '작업 소요 시간 (분)' }
641
- },
642
- required: ['project', 'workSummary', 'feedbackType', 'verificationPassed']
643
- }
644
- },
645
- {
646
- name: 'get_pending_feedbacks',
647
- description: '해결되지 않은 피드백 목록을 중요도순으로 조회합니다.',
648
- inputSchema: {
649
- type: 'object',
650
- properties: {
651
- feedbackType: { type: 'string', description: '피드백 유형 필터 (선택)' },
652
- limit: { type: 'number', description: '최대 결과 수 (기본: 20)' }
653
- }
654
- }
655
- },
656
- {
657
- name: 'resolve_feedback',
658
- description: '피드백을 해결 완료 처리합니다.',
659
- inputSchema: {
660
- type: 'object',
661
- properties: {
662
- feedbackId: { type: 'number', description: '해결된 피드백 ID' },
663
- resolution: { type: 'string', description: '해결 방법 설명' }
664
- },
665
- required: ['feedbackId']
666
- }
667
- },
668
- // ===== Content Filtering 학습/회피 도구 =====
669
- {
670
- name: 'record_filter_pattern',
671
- description: 'API content filtering에 걸린 패턴을 기록합니다. 비슷한 상황 회피에 사용됩니다.',
672
- inputSchema: {
673
- type: 'object',
674
- properties: {
675
- patternType: {
676
- type: 'string',
677
- enum: ['code_block', 'file_content', 'long_output', 'sensitive_keyword', 'binary_like', 'other'],
678
- description: '패턴 유형'
679
- },
680
- patternDescription: { type: 'string', description: '어떤 상황에서 발생했는지 설명' },
681
- fileExtension: { type: 'string', description: '관련 파일 확장자 (선택, 예: .kt, .tsx)' },
682
- exampleContext: { type: 'string', description: '발생 컨텍스트 예시 (민감 정보 제외)' },
683
- mitigationStrategy: { type: 'string', description: '회피 전략 (예: 청크 분할, 요약만 출력)' }
684
- },
685
- required: ['patternType', 'patternDescription']
686
- }
687
- },
688
- {
689
- name: 'get_filter_patterns',
690
- description: '기록된 content filtering 패턴 목록을 조회합니다. 응답 생성 시 참고용.',
691
- inputSchema: {
692
- type: 'object',
693
- properties: {
694
- patternType: { type: 'string', description: '패턴 유형 필터 (선택)' },
695
- fileExtension: { type: 'string', description: '파일 확장자 필터 (선택)' }
696
- }
697
- }
698
- },
699
- {
700
- name: 'get_safe_output_guidelines',
701
- description: '현재 학습된 패턴 기반으로 안전한 출력 가이드라인을 반환합니다.',
702
- inputSchema: {
703
- type: 'object',
704
- properties: {
705
- context: { type: 'string', description: '현재 작업 컨텍스트 (예: kotlin 파일 분석, 긴 코드 출력)' }
706
- }
707
- }
708
- },
709
- // ===== 자동 학습 시스템 도구 =====
710
- {
711
- name: 'auto_learn_decision',
712
- description: '아키텍처/기술 결정 사항을 자동 기록합니다. 왜 이 선택을 했는지 기록하여 나중에 참조할 수 있습니다.',
713
- inputSchema: {
714
- type: 'object',
715
- properties: {
716
- project: { type: 'string', description: '프로젝트명' },
717
- decision: { type: 'string', description: '결정 내용 (예: Socket.IO 대신 WebSocket 사용)' },
718
- reason: { type: 'string', description: '결정 이유' },
719
- context: { type: 'string', description: '결정 배경/맥락' },
720
- alternatives: { type: 'array', items: { type: 'string' }, description: '고려했던 대안들' },
721
- files: { type: 'array', items: { type: 'string' }, description: '관련 파일들' }
722
- },
723
- required: ['project', 'decision', 'reason']
724
- }
725
- },
726
- {
727
- name: 'auto_learn_fix',
728
- description: '에러/버그 해결 방법을 자동 기록합니다. 비슷한 에러 발생 시 참조할 수 있습니다.',
729
- inputSchema: {
730
- type: 'object',
731
- properties: {
732
- project: { type: 'string', description: '프로젝트명' },
733
- error: { type: 'string', description: '에러 메시지 또는 증상' },
734
- cause: { type: 'string', description: '원인 (선택)' },
735
- solution: { type: 'string', description: '해결 방법' },
736
- files: { type: 'array', items: { type: 'string' }, description: '수정한 파일들' },
737
- preventionTip: { type: 'string', description: '재발 방지 팁 (선택)' }
738
- },
739
- required: ['project', 'error', 'solution']
740
- }
741
- },
742
- {
743
- name: 'auto_learn_pattern',
744
- description: '프로젝트의 코드 패턴/컨벤션을 자동 기록합니다. 일관성 유지에 활용됩니다.',
745
- inputSchema: {
746
- type: 'object',
747
- properties: {
748
- project: { type: 'string', description: '프로젝트명' },
749
- patternName: { type: 'string', description: '패턴 이름 (예: Repository 패턴, State hoisting)' },
750
- description: { type: 'string', description: '패턴 설명' },
751
- example: { type: 'string', description: '예시 코드나 파일 경로' },
752
- appliesTo: { type: 'string', description: '적용 대상 (예: 모든 Repository, Compose UI)' }
753
- },
754
- required: ['project', 'patternName', 'description']
755
- }
756
- },
757
- {
758
- name: 'auto_learn_dependency',
759
- description: '의존성 변경 사항을 자동 기록합니다. 버전 충돌이나 업그레이드 시 참조합니다.',
760
- inputSchema: {
761
- type: 'object',
762
- properties: {
763
- project: { type: 'string', description: '프로젝트명' },
764
- dependency: { type: 'string', description: '의존성 이름' },
765
- action: { type: 'string', enum: ['add', 'remove', 'upgrade', 'downgrade'], description: '작업 유형' },
766
- fromVersion: { type: 'string', description: '이전 버전 (선택)' },
767
- toVersion: { type: 'string', description: '새 버전 (선택)' },
768
- reason: { type: 'string', description: '변경 이유' },
769
- breakingChanges: { type: 'string', description: 'Breaking changes 내용 (선택)' }
770
- },
771
- required: ['project', 'dependency', 'action', 'reason']
772
- }
773
- },
774
- {
775
- name: 'get_project_knowledge',
776
- description: '프로젝트에서 학습된 모든 지식을 조회합니다. 결정, 해결, 패턴, 의존성 변경 등.',
777
- inputSchema: {
778
- type: 'object',
779
- properties: {
780
- project: { type: 'string', description: '프로젝트명' },
781
- knowledgeType: {
782
- type: 'string',
783
- enum: ['all', 'decision', 'fix', 'pattern', 'dependency'],
784
- description: '지식 유형 필터 (기본: all)'
785
- },
786
- limit: { type: 'number', description: '최대 결과 수 (기본: 20)' }
787
- },
788
- required: ['project']
789
- }
790
- },
791
- {
792
- name: 'get_similar_issues',
793
- description: '비슷한 에러/이슈의 해결 방법을 검색합니다. 시맨틱 검색으로 유사한 문제를 찾습니다.',
794
- inputSchema: {
795
- type: 'object',
796
- properties: {
797
- errorOrIssue: { type: 'string', description: '에러 메시지 또는 이슈 설명' },
798
- project: { type: 'string', description: '특정 프로젝트에서만 검색 (선택)' },
799
- limit: { type: 'number', description: '최대 결과 수 (기본: 5)' }
800
- },
801
- required: ['errorOrIssue']
802
- }
803
- },
804
- // ===== 프로젝트 연속성 시스템 v2 =====
805
- {
806
- name: 'get_project_context',
807
- description: '프로젝트의 전체 컨텍스트를 한번에 조회합니다. /work 시작 시 필수 호출. 고정 컨텍스트(기술스택, 아키텍처)와 활성 컨텍스트(현재 상태, 태스크)를 ~650토큰으로 반환합니다.',
808
- inputSchema: {
809
- type: 'object',
810
- properties: {
811
- project: { type: 'string', description: '프로젝트명' }
812
- },
813
- required: ['project']
814
- }
815
- },
816
- {
817
- name: 'update_active_context',
818
- description: '프로젝트의 활성 컨텍스트를 업데이트합니다. 작업 종료 시 호출.',
819
- inputSchema: {
820
- type: 'object',
821
- properties: {
822
- project: { type: 'string', description: '프로젝트명' },
823
- currentState: { type: 'string', description: '현재 상태 (1줄 요약)' },
824
- recentFiles: { type: 'array', items: { type: 'string' }, description: '최근 수정 파일' },
825
- blockers: { type: 'string', description: '블로커/이슈 (없으면 null)' },
826
- lastVerification: { type: 'string', enum: ['passed', 'failed'], description: '마지막 검증 결과' }
827
- },
828
- required: ['project', 'currentState']
829
- }
830
- },
831
- {
832
- name: 'init_project_context',
833
- description: '새 프로젝트의 고정 컨텍스트를 초기화합니다. plan.md 기반으로 자동 추출하거나 직접 입력.',
834
- inputSchema: {
835
- type: 'object',
836
- properties: {
837
- project: { type: 'string', description: '프로젝트명' },
838
- techStack: { type: 'object', description: '기술 스택 {framework, language, database, ...}' },
839
- architectureDecisions: { type: 'array', items: { type: 'string' }, description: '핵심 아키텍처 결정 (최대 5개)' },
840
- codePatterns: { type: 'array', items: { type: 'string' }, description: '코드 컨벤션/패턴 (최대 5개)' },
841
- specialNotes: { type: 'string', description: '프로젝트 특이사항' }
842
- },
843
- required: ['project']
844
- }
845
- },
846
- {
847
- name: 'update_architecture_decision',
848
- description: '프로젝트에 아키텍처 결정을 추가합니다. 중요한 기술 결정 시 호출.',
849
- inputSchema: {
850
- type: 'object',
851
- properties: {
852
- project: { type: 'string', description: '프로젝트명' },
853
- decision: { type: 'string', description: '결정 내용 (예: "Socket.IO 대신 WebSocket 사용 - 번들 사이즈 절약")' }
854
- },
855
- required: ['project', 'decision']
856
- }
857
- },
858
- // ===== 태스크 관리 =====
859
- {
860
- name: 'add_task',
861
- description: '프로젝트에 태스크를 추가합니다.',
862
- inputSchema: {
863
- type: 'object',
864
- properties: {
865
- project: { type: 'string', description: '프로젝트명' },
866
- title: { type: 'string', description: '태스크 제목' },
867
- description: { type: 'string', description: '태스크 설명 (선택)' },
868
- priority: { type: 'number', description: '우선순위 1-10 (기본: 5, 10이 가장 높음)' },
869
- relatedFiles: { type: 'array', items: { type: 'string' }, description: '관련 파일' },
870
- acceptanceCriteria: { type: 'string', description: '완료 조건' }
871
- },
872
- required: ['project', 'title']
873
- }
874
- },
875
- {
876
- name: 'complete_task',
877
- description: '태스크를 완료 처리합니다.',
878
- inputSchema: {
879
- type: 'object',
880
- properties: {
881
- taskId: { type: 'number', description: '태스크 ID' }
882
- },
883
- required: ['taskId']
884
- }
885
- },
886
- {
887
- name: 'update_task_status',
888
- description: '태스크 상태를 변경합니다.',
889
- inputSchema: {
890
- type: 'object',
891
- properties: {
892
- taskId: { type: 'number', description: '태스크 ID' },
893
- status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: '새 상태' }
894
- },
895
- required: ['taskId', 'status']
896
- }
897
- },
898
- {
899
- name: 'get_pending_tasks',
900
- description: '프로젝트의 미완료 태스크 목록을 조회합니다. /work 시작 시 호출 권장.',
901
- inputSchema: {
902
- type: 'object',
903
- properties: {
904
- project: { type: 'string', description: '프로젝트명' },
905
- includeBlocked: { type: 'boolean', description: 'blocked 상태 포함 여부 (기본: true)' }
906
- },
907
- required: ['project']
908
- }
909
- },
910
- // ===== 에러 솔루션 아카이브 =====
911
- {
912
- name: 'record_solution',
913
- description: '에러 해결 방법을 기록합니다. 에러 해결 후 호출하면 나중에 같은 에러 시 참조 가능.',
914
- inputSchema: {
915
- type: 'object',
916
- properties: {
917
- project: { type: 'string', description: '프로젝트명 (선택, 범용 솔루션이면 생략)' },
918
- errorSignature: { type: 'string', description: '에러 패턴/시그니처 (검색 키, 예: "WorkManager not initialized")' },
919
- errorMessage: { type: 'string', description: '전체 에러 메시지 (선택)' },
920
- solution: { type: 'string', description: '해결 방법' },
921
- relatedFiles: { type: 'array', items: { type: 'string' }, description: '수정한 파일' }
922
- },
923
- required: ['errorSignature', 'solution']
924
- }
925
- },
926
- {
927
- name: 'find_solution',
928
- description: '비슷한 에러의 해결 방법을 검색합니다. 에러 발생 시 먼저 호출하여 기존 솔루션 확인.',
929
- inputSchema: {
930
- type: 'object',
931
- properties: {
932
- errorText: { type: 'string', description: '에러 메시지 또는 키워드' },
933
- project: { type: 'string', description: '특정 프로젝트에서만 검색 (선택)' }
934
- },
935
- required: ['errorText']
936
- }
937
- },
938
- // ===== 시스템 평가/모니터링 =====
939
- {
940
- name: 'get_continuity_stats',
941
- description: '연속성 시스템 사용 통계를 조회합니다. 시스템이 제대로 활용되고 있는지 평가용.',
942
- inputSchema: {
943
- type: 'object',
944
- properties: {
945
- project: { type: 'string', description: '특정 프로젝트 (선택, 없으면 전체)' }
946
- }
947
- }
948
- }
949
- ];
950
- // ===== 도구 핸들러 =====
951
- async function listProjects() {
952
- try {
953
- const entries = await fs.readdir(APPS_DIR, { withFileTypes: true });
954
- const projects = [];
955
- for (const entry of entries) {
956
- if (!entry.isDirectory())
957
- continue;
958
- const projectPath = path.join(APPS_DIR, entry.name);
959
- const sessionPath = path.join(projectPath, 'docs', 'SESSION.md');
960
- const planPath = path.join(projectPath, 'plan.md');
961
- const hasSession = await fileExists(sessionPath);
962
- const hasPlan = await fileExists(planPath);
963
- // 플랫폼 감지
964
- let platform = 'Unknown';
965
- if (await fileExists(path.join(projectPath, 'package.json'))) {
966
- platform = 'Web';
967
- }
968
- else if (await fileExists(path.join(projectPath, 'build.gradle.kts')) ||
969
- await fileExists(path.join(projectPath, 'build.gradle'))) {
970
- platform = 'Android';
971
- }
972
- else if (await fileExists(path.join(projectPath, 'pubspec.yaml'))) {
973
- platform = 'Flutter';
974
- }
975
- projects.push({
976
- name: entry.name,
977
- path: projectPath,
978
- platform,
979
- hasSession,
980
- hasPlan
981
- });
982
- }
983
- return {
984
- content: [{
985
- type: 'text',
986
- text: JSON.stringify({ projects, count: projects.length }, null, 2)
987
- }]
988
- };
989
- }
990
- catch (error) {
991
- return {
992
- content: [{ type: 'text', text: `Error: ${error}` }],
993
- isError: true
994
- };
995
- }
996
- }
997
- async function getSession(project, includeRaw = false, maxContentLength = 2000) {
998
- const sessionPath = path.join(APPS_DIR, project, 'docs', 'SESSION.md');
999
- const content = await readFileContent(sessionPath);
1000
- if (!content) {
1001
- return {
1002
- content: [{ type: 'text', text: JSON.stringify({ exists: false, project }) }]
1003
- };
1004
- }
1005
- // SESSION.md 파싱
1006
- const session = {
1007
- exists: true,
1008
- project
1009
- };
1010
- // 마지막 업데이트 추출
1011
- const updateMatch = content.match(/마지막 업데이트[:\s]*(.+)/);
1012
- if (updateMatch) {
1013
- session.lastUpdate = updateMatch[1].trim();
1014
- }
1015
- // 현재 상태 추출
1016
- const statusMatch = content.match(/현재 상태[:\s]*(.+)/);
1017
- if (statusMatch) {
1018
- session.currentStatus = statusMatch[1].trim();
1019
- }
1020
- // 다음 작업 추출
1021
- const nextTasksMatch = content.match(/다음 작업[^:]*:([\s\S]*?)(?=##|$)/);
1022
- if (nextTasksMatch) {
1023
- const tasks = nextTasksMatch[1].match(/[-*]\s*(.+)/g);
1024
- session.nextTasks = tasks?.map(t => t.replace(/^[-*]\s*/, '').trim()) || [];
1025
- }
1026
- // 수정된 파일 추출
1027
- const filesMatch = content.match(/수정된 파일[^:]*:([\s\S]*?)(?=##|$)/);
1028
- if (filesMatch) {
1029
- const files = filesMatch[1].match(/[-*]\s*(.+)/g);
1030
- session.modifiedFiles = files?.map(f => f.replace(/^[-*]\s*/, '').trim()) || [];
1031
- }
1032
- // 이슈 추출
1033
- const issuesMatch = content.match(/(?:알려진 )?이슈[^:]*:([\s\S]*?)(?=##|$)/);
1034
- if (issuesMatch) {
1035
- const issues = issuesMatch[1].match(/[-*]\s*(.+)/g);
1036
- session.issues = issues?.map(i => i.replace(/^[-*]\s*/, '').trim()) || [];
1037
- }
1038
- // raw 내용 (선택적, 응답 크기 조절용)
1039
- if (includeRaw) {
1040
- if (maxContentLength > 0 && content.length > maxContentLength) {
1041
- session.raw = content.slice(0, maxContentLength);
1042
- session.truncated = true;
1043
- session.totalLength = content.length;
1044
- }
1045
- else {
1046
- session.raw = content;
1047
- session.truncated = false;
1048
- }
1049
- }
1050
- // 응답 크기 정보 추가
1051
- session.contentLength = content.length;
1052
- return {
1053
- content: [{ type: 'text', text: JSON.stringify(session, null, 2) }]
1054
- };
1055
- }
1056
- async function updateSession(project, lastWork, currentStatus, nextTasks, modifiedFiles, issues, verificationResult) {
1057
- const sessionPath = path.join(APPS_DIR, project, 'docs', 'SESSION.md');
1058
- const now = new Date().toISOString().split('T')[0];
1059
- let content = `# SESSION - ${project}
1060
-
1061
- ## 마지막 업데이트
1062
- - **날짜**: ${now}
1063
- - **작업**: ${lastWork}
1064
-
1065
- ## 현재 상태
1066
- ${currentStatus || '진행 중'}
1067
-
1068
- `;
1069
- if (nextTasks && nextTasks.length > 0) {
1070
- content += `## 다음 작업
1071
- ${nextTasks.map(t => `- ${t}`).join('\n')}
1072
-
1073
- `;
1074
- }
1075
- if (modifiedFiles && modifiedFiles.length > 0) {
1076
- content += `## 수정된 파일
1077
- ${modifiedFiles.map(f => `- ${f}`).join('\n')}
1078
-
1079
- `;
1080
- }
1081
- if (issues && issues.length > 0) {
1082
- content += `## 알려진 이슈
1083
- ${issues.map(i => `- ${i}`).join('\n')}
1084
- `;
1085
- }
1086
- await writeFileContent(sessionPath, content);
1087
- // DB에도 자동 저장
1088
- try {
1089
- const stmt = db.prepare(`
1090
- INSERT INTO sessions (project, last_work, current_status, next_tasks, modified_files, issues, verification_result)
1091
- VALUES (?, ?, ?, ?, ?, ?, ?)
1092
- `);
1093
- stmt.run(project, lastWork, currentStatus || null, nextTasks ? JSON.stringify(nextTasks) : null, modifiedFiles ? JSON.stringify(modifiedFiles) : null, issues ? JSON.stringify(issues) : null, verificationResult || null);
1094
- }
1095
- catch (dbError) {
1096
- console.error('DB save error:', dbError);
1097
- }
1098
- return {
1099
- content: [{ type: 'text', text: JSON.stringify({ success: true, path: sessionPath, savedToDb: true }) }]
1100
- };
1101
- }
1102
- async function getTechStack(project) {
1103
- const planPath = path.join(APPS_DIR, project, 'plan.md');
1104
- const content = await readFileContent(planPath);
1105
- if (!content) {
1106
- return {
1107
- content: [{ type: 'text', text: JSON.stringify({ exists: false, project }) }]
1108
- };
1109
- }
1110
- const stack = parseMarkdownTable(content, '기술 스택');
1111
- const commands = parseMarkdownTable(content, '명령어');
1112
- return {
1113
- content: [{
1114
- type: 'text',
1115
- text: JSON.stringify({
1116
- exists: true,
1117
- project,
1118
- techStack: stack,
1119
- commands: commands
1120
- }, null, 2)
1121
- }]
1122
- };
1123
- }
1124
- async function runVerification(project, gates) {
1125
- const projectPath = path.join(APPS_DIR, project);
1126
- const planPath = path.join(projectPath, 'plan.md');
1127
- // plan.md에서 명령어 추출
1128
- const planContent = await readFileContent(planPath);
1129
- let commands = {};
1130
- if (planContent) {
1131
- commands = parseMarkdownTable(planContent, '명령어');
1132
- }
1133
- // 플랫폼별 기본 명령어
1134
- const defaultCommands = {
1135
- Web: { build: 'pnpm build', test: 'pnpm test:run', lint: 'pnpm lint' },
1136
- Android: { build: './gradlew assembleDebug', test: './gradlew test', lint: './gradlew lint' },
1137
- Flutter: { build: 'flutter build', test: 'flutter test', lint: 'flutter analyze' }
1138
- };
1139
- // 플랫폼 감지
1140
- let platform = 'Web';
1141
- if (await fileExists(path.join(projectPath, 'build.gradle.kts')) ||
1142
- await fileExists(path.join(projectPath, 'build.gradle'))) {
1143
- platform = 'Android';
1144
- }
1145
- else if (await fileExists(path.join(projectPath, 'pubspec.yaml'))) {
1146
- platform = 'Flutter';
1147
- }
1148
- const finalCommands = { ...defaultCommands[platform], ...commands };
1149
- const gatesToRun = gates || ['build', 'test', 'lint'];
1150
- const results = {};
1151
- for (const gate of gatesToRun) {
1152
- const cmd = finalCommands[gate === 'build' ? '빌드' : gate === 'test' ? '테스트' : '린트']
1153
- || finalCommands[gate];
1154
- if (!cmd) {
1155
- results[gate] = { success: false, output: `No command found for ${gate}` };
1156
- continue;
1157
- }
1158
- try {
1159
- const output = execSync(cmd, {
1160
- cwd: projectPath,
1161
- encoding: 'utf-8',
1162
- timeout: 300000, // 5분 타임아웃
1163
- stdio: ['pipe', 'pipe', 'pipe']
1164
- });
1165
- results[gate] = { success: true, output: output.slice(-1000) }; // 마지막 1000자만
1166
- }
1167
- catch (error) {
1168
- const execError = error;
1169
- results[gate] = {
1170
- success: false,
1171
- output: (execError.stdout || execError.stderr || execError.message || 'Unknown error').slice(-1000)
1172
- };
1173
- }
1174
- }
1175
- const allPassed = Object.values(results).every(r => r.success);
1176
- return {
1177
- content: [{
1178
- type: 'text',
1179
- text: JSON.stringify({
1180
- project,
1181
- platform,
1182
- allPassed,
1183
- results
1184
- }, null, 2)
1185
- }]
1186
- };
1187
- }
1188
- async function detectPlatform(project) {
1189
- const projectPath = path.join(APPS_DIR, project);
1190
- const checks = {
1191
- 'package.json': 'Web',
1192
- 'build.gradle.kts': 'Android',
1193
- 'build.gradle': 'Android',
1194
- 'pubspec.yaml': 'Flutter',
1195
- 'go.mod': 'Server (Go)',
1196
- 'Cargo.toml': 'Server (Rust)',
1197
- 'pom.xml': 'Server (Java)',
1198
- 'requirements.txt': 'Server (Python)'
1199
- };
1200
- for (const [file, platform] of Object.entries(checks)) {
1201
- if (await fileExists(path.join(projectPath, file))) {
1202
- // 추가 정보 수집
1203
- const info = { platform };
1204
- if (file === 'package.json') {
1205
- const pkg = JSON.parse(await readFileContent(path.join(projectPath, file)) || '{}');
1206
- info.framework = pkg.dependencies?.next ? 'Next.js' :
1207
- pkg.dependencies?.react ? 'React' :
1208
- pkg.dependencies?.vue ? 'Vue' : 'Unknown';
1209
- }
1210
- if (file === 'pubspec.yaml') {
1211
- const content = await readFileContent(path.join(projectPath, file)) || '';
1212
- info.hasFlame = content.includes('flame:');
1213
- info.hasRiverpod = content.includes('riverpod');
1214
- }
1215
- return {
1216
- content: [{ type: 'text', text: JSON.stringify({ project, ...info }) }]
1217
- };
1218
- }
1219
- }
1220
- return {
1221
- content: [{ type: 'text', text: JSON.stringify({ project, platform: 'Unknown' }) }]
1222
- };
1223
- }
1224
- // ===== SQLite 기반 새 도구 핸들러 =====
1225
- function saveSessionHistory(project, lastWork, currentStatus, nextTasks, modifiedFiles, issues, verificationResult, durationMinutes) {
1226
- try {
1227
- const stmt = db.prepare(`
1228
- INSERT INTO sessions (project, last_work, current_status, next_tasks, modified_files, issues, verification_result, duration_minutes)
1229
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1230
- `);
1231
- const result = stmt.run(project, lastWork, currentStatus || null, nextTasks ? JSON.stringify(nextTasks) : null, modifiedFiles ? JSON.stringify(modifiedFiles) : null, issues ? JSON.stringify(issues) : null, verificationResult || null, durationMinutes || null);
1232
- return {
1233
- content: [{
1234
- type: 'text',
1235
- text: JSON.stringify({ success: true, id: result.lastInsertRowid })
1236
- }]
1237
- };
1238
- }
1239
- catch (error) {
1240
- return {
1241
- content: [{ type: 'text', text: `Error: ${error}` }],
1242
- isError: true
1243
- };
1244
- }
1245
- }
1246
- function getSessionHistory(project, limit = 10, keyword) {
1247
- try {
1248
- let query = 'SELECT * FROM sessions WHERE 1=1';
1249
- const params = [];
1250
- if (project) {
1251
- query += ' AND project = ?';
1252
- params.push(project);
1253
- }
1254
- if (keyword) {
1255
- query += ' AND (last_work LIKE ? OR current_status LIKE ?)';
1256
- params.push(`%${keyword}%`, `%${keyword}%`);
1257
- }
1258
- query += ' ORDER BY timestamp DESC LIMIT ?';
1259
- params.push(limit);
1260
- const stmt = db.prepare(query);
1261
- const rows = stmt.all(...params);
1262
- const sessions = rows.map(row => ({
1263
- id: row.id,
1264
- project: row.project,
1265
- timestamp: row.timestamp,
1266
- lastWork: row.last_work,
1267
- currentStatus: row.current_status,
1268
- nextTasks: row.next_tasks ? JSON.parse(row.next_tasks) : [],
1269
- modifiedFiles: row.modified_files ? JSON.parse(row.modified_files) : [],
1270
- issues: row.issues ? JSON.parse(row.issues) : [],
1271
- verificationResult: row.verification_result,
1272
- durationMinutes: row.duration_minutes
1273
- }));
1274
- return {
1275
- content: [{
1276
- type: 'text',
1277
- text: JSON.stringify({ sessions, count: sessions.length }, null, 2)
1278
- }]
1279
- };
1280
- }
1281
- catch (error) {
1282
- return {
1283
- content: [{ type: 'text', text: `Error: ${error}` }],
1284
- isError: true
1285
- };
1286
- }
1287
- }
1288
- function searchSimilarWork(keyword, project) {
1289
- try {
1290
- let query = `
1291
- SELECT project, last_work, current_status, modified_files, verification_result, timestamp
1292
- FROM sessions
1293
- WHERE last_work LIKE ?
1294
- `;
1295
- const params = [`%${keyword}%`];
1296
- if (project) {
1297
- query += ' AND project = ?';
1298
- params.push(project);
1299
- }
1300
- query += ' ORDER BY timestamp DESC LIMIT 20';
1301
- const stmt = db.prepare(query);
1302
- const rows = stmt.all(...params);
1303
- const results = rows.map(row => ({
1304
- project: row.project,
1305
- work: row.last_work,
1306
- status: row.current_status,
1307
- files: row.modified_files ? JSON.parse(row.modified_files) : [],
1308
- result: row.verification_result,
1309
- date: row.timestamp
1310
- }));
1311
- return {
1312
- content: [{
1313
- type: 'text',
1314
- text: JSON.stringify({
1315
- keyword,
1316
- found: results.length,
1317
- results
1318
- }, null, 2)
1319
- }]
1320
- };
1321
- }
1322
- catch (error) {
1323
- return {
1324
- content: [{ type: 'text', text: `Error: ${error}` }],
1325
- isError: true
1326
- };
1327
- }
1328
- }
1329
- function getProjectStats(project) {
1330
- try {
1331
- let query;
1332
- let params = [];
1333
- if (project) {
1334
- query = `
1335
- SELECT
1336
- project,
1337
- COUNT(*) as total_sessions,
1338
- SUM(CASE WHEN verification_result = 'passed' THEN 1 ELSE 0 END) as passed,
1339
- SUM(CASE WHEN verification_result = 'failed' THEN 1 ELSE 0 END) as failed,
1340
- AVG(duration_minutes) as avg_duration,
1341
- MAX(timestamp) as last_session
1342
- FROM sessions
1343
- WHERE project = ?
1344
- GROUP BY project
1345
- `;
1346
- params = [project];
1347
- }
1348
- else {
1349
- query = `
1350
- SELECT
1351
- project,
1352
- COUNT(*) as total_sessions,
1353
- SUM(CASE WHEN verification_result = 'passed' THEN 1 ELSE 0 END) as passed,
1354
- SUM(CASE WHEN verification_result = 'failed' THEN 1 ELSE 0 END) as failed,
1355
- AVG(duration_minutes) as avg_duration,
1356
- MAX(timestamp) as last_session
1357
- FROM sessions
1358
- GROUP BY project
1359
- ORDER BY last_session DESC
1360
- `;
1361
- }
1362
- const stmt = db.prepare(query);
1363
- const rows = stmt.all(...params);
1364
- const stats = rows.map(row => ({
1365
- project: row.project,
1366
- totalSessions: row.total_sessions,
1367
- passed: row.passed || 0,
1368
- failed: row.failed || 0,
1369
- successRate: row.total_sessions > 0
1370
- ? Math.round(((row.passed || 0) / row.total_sessions) * 100)
1371
- : 0,
1372
- avgDurationMinutes: row.avg_duration ? Math.round(row.avg_duration) : null,
1373
- lastSession: row.last_session
1374
- }));
1375
- return {
1376
- content: [{
1377
- type: 'text',
1378
- text: JSON.stringify({ stats }, null, 2)
1379
- }]
1380
- };
1381
- }
1382
- catch (error) {
1383
- return {
1384
- content: [{ type: 'text', text: `Error: ${error}` }],
1385
- isError: true
1386
- };
1387
- }
1388
- }
1389
- function recordWorkPattern(project, workType, description, filesPattern, success, durationMinutes) {
1390
- try {
1391
- // 기존 패턴 확인
1392
- const existingStmt = db.prepare(`
1393
- SELECT id, success_rate, avg_duration_minutes, count
1394
- FROM work_patterns
1395
- WHERE project = ? AND work_type = ? AND description = ?
1396
- `);
1397
- const existing = existingStmt.get(project, workType, description);
1398
- if (existing) {
1399
- // 업데이트
1400
- const newCount = existing.count + 1;
1401
- const newSuccessRate = success !== undefined
1402
- ? ((existing.success_rate * existing.count) + (success ? 1 : 0)) / newCount
1403
- : existing.success_rate;
1404
- const newAvgDuration = durationMinutes !== undefined
1405
- ? ((existing.avg_duration_minutes * existing.count) + durationMinutes) / newCount
1406
- : existing.avg_duration_minutes;
1407
- const updateStmt = db.prepare(`
1408
- UPDATE work_patterns
1409
- SET success_rate = ?, avg_duration_minutes = ?, count = ?, last_used = CURRENT_TIMESTAMP
1410
- WHERE id = ?
1411
- `);
1412
- updateStmt.run(newSuccessRate, newAvgDuration, newCount, existing.id);
1413
- return {
1414
- content: [{
1415
- type: 'text',
1416
- text: JSON.stringify({ success: true, action: 'updated', count: newCount })
1417
- }]
1418
- };
1419
- }
1420
- else {
1421
- // 새로 삽입
1422
- const insertStmt = db.prepare(`
1423
- INSERT INTO work_patterns (project, work_type, description, files_pattern, success_rate, avg_duration_minutes)
1424
- VALUES (?, ?, ?, ?, ?, ?)
1425
- `);
1426
- insertStmt.run(project, workType, description, filesPattern || null, success ? 1 : 0, durationMinutes || 0);
1427
- return {
1428
- content: [{
1429
- type: 'text',
1430
- text: JSON.stringify({ success: true, action: 'created' })
1431
- }]
1432
- };
1433
- }
1434
- }
1435
- catch (error) {
1436
- return {
1437
- content: [{ type: 'text', text: `Error: ${error}` }],
1438
- isError: true
1439
- };
1440
- }
1441
- }
1442
- function getWorkPatterns(project, workType) {
1443
- try {
1444
- let query = 'SELECT * FROM work_patterns WHERE 1=1';
1445
- const params = [];
1446
- if (project) {
1447
- query += ' AND project = ?';
1448
- params.push(project);
1449
- }
1450
- if (workType) {
1451
- query += ' AND work_type = ?';
1452
- params.push(workType);
1453
- }
1454
- query += ' ORDER BY count DESC, last_used DESC';
1455
- const stmt = db.prepare(query);
1456
- const rows = stmt.all(...params);
1457
- const patterns = rows.map(row => ({
1458
- project: row.project,
1459
- workType: row.work_type,
1460
- description: row.description,
1461
- filesPattern: row.files_pattern,
1462
- successRate: Math.round(row.success_rate * 100),
1463
- avgDurationMinutes: Math.round(row.avg_duration_minutes),
1464
- usageCount: row.count,
1465
- lastUsed: row.last_used
1466
- }));
1467
- return {
1468
- content: [{
1469
- type: 'text',
1470
- text: JSON.stringify({ patterns, count: patterns.length }, null, 2)
1471
- }]
1472
- };
1473
- }
1474
- catch (error) {
1475
- return {
1476
- content: [{ type: 'text', text: `Error: ${error}` }],
1477
- isError: true
1478
- };
1479
- }
1480
- }
1481
- async function storeMemory(content, memoryType, tags, project, importance, metadata) {
1482
- try {
1483
- const stmt = db.prepare(`
1484
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
1485
- VALUES (?, ?, ?, ?, ?, ?)
1486
- `);
1487
- const result = stmt.run(content, memoryType, tags ? JSON.stringify(tags) : null, project || null, importance || 5, metadata ? JSON.stringify(metadata) : null);
1488
- const memoryId = result.lastInsertRowid;
1489
- // 백그라운드에서 임베딩 생성 (비동기, 실패해도 메모리는 저장됨)
1490
- generateEmbedding(content).then(embedding => {
1491
- if (embedding) {
1492
- try {
1493
- const embStmt = db.prepare(`
1494
- INSERT OR REPLACE INTO embeddings (memory_id, embedding)
1495
- VALUES (?, ?)
1496
- `);
1497
- embStmt.run(memoryId, embeddingToBuffer(embedding));
1498
- }
1499
- catch (e) {
1500
- console.error('Failed to save embedding:', e);
1501
- }
1502
- }
1503
- });
1504
- return {
1505
- content: [{
1506
- type: 'text',
1507
- text: JSON.stringify({
1508
- success: true,
1509
- id: memoryId,
1510
- message: `메모리 저장 완료: ${memoryType}`,
1511
- embeddingStatus: embeddingReady ? 'generating' : 'model_loading'
1512
- })
1513
- }]
1514
- };
1515
- }
1516
- catch (error) {
1517
- return {
1518
- content: [{ type: 'text', text: `Error: ${error}` }],
1519
- isError: true
1520
- };
1521
- }
1522
- }
1523
- function recallMemory(query, memoryType, project, limit = 10, minImportance = 1, maxContentLength = 500) {
1524
- try {
1525
- // FTS5 검색 쿼리 구성
1526
- const searchTerms = query.split(/\s+/).filter(t => t.length > 0);
1527
- const ftsQuery = searchTerms.map(t => `"${t}"*`).join(' OR ');
1528
- let sql = `
1529
- SELECT m.*, bm25(memories_fts) as rank
1530
- FROM memories m
1531
- JOIN memories_fts ON m.id = memories_fts.rowid
1532
- WHERE memories_fts MATCH ? AND m.importance >= ?
1533
- `;
1534
- const params = [ftsQuery, minImportance];
1535
- if (memoryType) {
1536
- sql += ' AND m.memory_type = ?';
1537
- params.push(memoryType);
1538
- }
1539
- if (project) {
1540
- sql += ' AND m.project = ?';
1541
- params.push(project);
1542
- }
1543
- sql += ' ORDER BY rank LIMIT ?';
1544
- params.push(limit);
1545
- const stmt = db.prepare(sql);
1546
- const rows = stmt.all(...params);
1547
- // 접근 횟수 업데이트
1548
- const updateStmt = db.prepare(`
1549
- UPDATE memories SET accessed_at = CURRENT_TIMESTAMP, access_count = access_count + 1
1550
- WHERE id = ?
1551
- `);
1552
- rows.forEach(row => updateStmt.run(row.id));
1553
- const memories = rows.map(row => {
1554
- const contentTruncated = maxContentLength > 0 && row.content.length > maxContentLength;
1555
- return {
1556
- id: row.id,
1557
- content: contentTruncated ? row.content.slice(0, maxContentLength) + '...' : row.content,
1558
- contentTruncated,
1559
- type: row.memory_type,
1560
- tags: row.tags ? JSON.parse(row.tags) : [],
1561
- project: row.project,
1562
- importance: row.importance,
1563
- createdAt: row.created_at,
1564
- accessCount: row.access_count + 1,
1565
- relevance: Math.abs(row.rank)
1566
- };
1567
- });
1568
- return {
1569
- content: [{
1570
- type: 'text',
1571
- text: JSON.stringify({
1572
- query,
1573
- found: memories.length,
1574
- memories
1575
- }, null, 2)
1576
- }]
1577
- };
1578
- }
1579
- catch (error) {
1580
- // FTS 검색 실패 시 LIKE 폴백
1581
- try {
1582
- let sql = `
1583
- SELECT * FROM memories
1584
- WHERE (content LIKE ? OR tags LIKE ?) AND importance >= ?
1585
- `;
1586
- const likePattern = `%${query}%`;
1587
- const params = [likePattern, likePattern, minImportance];
1588
- if (memoryType) {
1589
- sql += ' AND memory_type = ?';
1590
- params.push(memoryType);
1591
- }
1592
- if (project) {
1593
- sql += ' AND project = ?';
1594
- params.push(project);
1595
- }
1596
- sql += ' ORDER BY importance DESC, created_at DESC LIMIT ?';
1597
- params.push(limit);
1598
- const stmt = db.prepare(sql);
1599
- const rows = stmt.all(...params);
1600
- const memories = rows.map(row => ({
1601
- id: row.id,
1602
- content: row.content,
1603
- type: row.memory_type,
1604
- tags: row.tags ? JSON.parse(row.tags) : [],
1605
- project: row.project,
1606
- importance: row.importance,
1607
- createdAt: row.created_at
1608
- }));
1609
- return {
1610
- content: [{
1611
- type: 'text',
1612
- text: JSON.stringify({
1613
- query,
1614
- found: memories.length,
1615
- memories,
1616
- note: 'Used LIKE fallback'
1617
- }, null, 2)
1618
- }]
1619
- };
1620
- }
1621
- catch (fallbackError) {
1622
- return {
1623
- content: [{ type: 'text', text: `Error: ${fallbackError}` }],
1624
- isError: true
1625
- };
1626
- }
1627
- }
1628
- }
1629
- function recallByTimeframe(timeframe, memoryType, project, limit = 20) {
1630
- try {
1631
- const timeConditions = {
1632
- 'today': "date(created_at) = date('now')",
1633
- 'yesterday': "date(created_at) = date('now', '-1 day')",
1634
- 'this_week': "created_at >= date('now', '-7 days')",
1635
- 'last_week': "created_at >= date('now', '-14 days') AND created_at < date('now', '-7 days')",
1636
- 'this_month': "created_at >= date('now', '-30 days')",
1637
- 'last_month': "created_at >= date('now', '-60 days') AND created_at < date('now', '-30 days')"
1638
- };
1639
- const timeCondition = timeConditions[timeframe];
1640
- if (!timeCondition) {
1641
- return {
1642
- content: [{ type: 'text', text: `Invalid timeframe: ${timeframe}` }],
1643
- isError: true
1644
- };
1645
- }
1646
- let sql = `SELECT * FROM memories WHERE ${timeCondition}`;
1647
- const params = [];
1648
- if (memoryType) {
1649
- sql += ' AND memory_type = ?';
1650
- params.push(memoryType);
1651
- }
1652
- if (project) {
1653
- sql += ' AND project = ?';
1654
- params.push(project);
1655
- }
1656
- sql += ' ORDER BY created_at DESC LIMIT ?';
1657
- params.push(limit);
1658
- const stmt = db.prepare(sql);
1659
- const rows = stmt.all(...params);
1660
- const memories = rows.map(row => ({
1661
- id: row.id,
1662
- content: row.content,
1663
- type: row.memory_type,
1664
- tags: row.tags ? JSON.parse(row.tags) : [],
1665
- project: row.project,
1666
- importance: row.importance,
1667
- createdAt: row.created_at
1668
- }));
1669
- return {
1670
- content: [{
1671
- type: 'text',
1672
- text: JSON.stringify({
1673
- timeframe,
1674
- found: memories.length,
1675
- memories
1676
- }, null, 2)
1677
- }]
1678
- };
1679
- }
1680
- catch (error) {
1681
- return {
1682
- content: [{ type: 'text', text: `Error: ${error}` }],
1683
- isError: true
1684
- };
1685
- }
1686
- }
1687
- function searchByTag(tags, matchAll = false, limit = 20) {
1688
- try {
1689
- const stmt = db.prepare(`SELECT * FROM memories ORDER BY importance DESC, created_at DESC`);
1690
- const allRows = stmt.all();
1691
- const filteredRows = allRows.filter(row => {
1692
- if (!row.tags)
1693
- return false;
1694
- const memoryTags = JSON.parse(row.tags);
1695
- if (matchAll) {
1696
- return tags.every(tag => memoryTags.includes(tag));
1697
- }
1698
- else {
1699
- return tags.some(tag => memoryTags.includes(tag));
1700
- }
1701
- }).slice(0, limit);
1702
- const memories = filteredRows.map(row => ({
1703
- id: row.id,
1704
- content: row.content,
1705
- type: row.memory_type,
1706
- tags: JSON.parse(row.tags),
1707
- project: row.project,
1708
- importance: row.importance,
1709
- createdAt: row.created_at
1710
- }));
1711
- return {
1712
- content: [{
1713
- type: 'text',
1714
- text: JSON.stringify({
1715
- searchTags: tags,
1716
- matchAll,
1717
- found: memories.length,
1718
- memories
1719
- }, null, 2)
1720
- }]
1721
- };
1722
- }
1723
- catch (error) {
1724
- return {
1725
- content: [{ type: 'text', text: `Error: ${error}` }],
1726
- isError: true
1727
- };
1728
- }
1729
- }
1730
- function createRelation(sourceId, targetId, relationType, strength = 1.0) {
1731
- try {
1732
- const stmt = db.prepare(`
1733
- INSERT OR REPLACE INTO memory_relations (source_id, target_id, relation_type, strength)
1734
- VALUES (?, ?, ?, ?)
1735
- `);
1736
- stmt.run(sourceId, targetId, relationType, strength);
1737
- return {
1738
- content: [{
1739
- type: 'text',
1740
- text: JSON.stringify({
1741
- success: true,
1742
- relation: { sourceId, targetId, relationType, strength }
1743
- })
1744
- }]
1745
- };
1746
- }
1747
- catch (error) {
1748
- return {
1749
- content: [{ type: 'text', text: `Error: ${error}` }],
1750
- isError: true
1751
- };
1752
- }
1753
- }
1754
- function findConnectedMemories(memoryId, depth = 1, relationType) {
1755
- try {
1756
- const maxDepth = Math.min(depth, 3);
1757
- const visited = new Set([memoryId]);
1758
- const results = [];
1759
- const findAtDepth = (currentIds, currentDepth) => {
1760
- if (currentDepth > maxDepth || currentIds.length === 0)
1761
- return;
1762
- let sql = `
1763
- SELECT m.*, mr.relation_type, mr.source_id, mr.target_id
1764
- FROM memory_relations mr
1765
- JOIN memories m ON (
1766
- (mr.source_id IN (${currentIds.join(',')}) AND m.id = mr.target_id) OR
1767
- (mr.target_id IN (${currentIds.join(',')}) AND m.id = mr.source_id)
1768
- )
1769
- `;
1770
- if (relationType) {
1771
- sql += ` WHERE mr.relation_type = '${relationType}'`;
1772
- }
1773
- const stmt = db.prepare(sql);
1774
- const rows = stmt.all();
1775
- const nextIds = [];
1776
- for (const row of rows) {
1777
- const connectedId = currentIds.includes(row.source_id) ? row.target_id : row.source_id;
1778
- if (!visited.has(connectedId)) {
1779
- visited.add(connectedId);
1780
- nextIds.push(connectedId);
1781
- results.push({
1782
- memory: row,
1783
- relation: row.relation_type,
1784
- direction: row.source_id === memoryId ? 'outgoing' : 'incoming',
1785
- depth: currentDepth
1786
- });
1787
- }
1788
- }
1789
- findAtDepth(nextIds, currentDepth + 1);
1790
- };
1791
- findAtDepth([memoryId], 1);
1792
- const connected = results.map(r => ({
1793
- id: r.memory.id,
1794
- content: r.memory.content,
1795
- type: r.memory.memory_type,
1796
- relation: r.relation,
1797
- direction: r.direction,
1798
- depth: r.depth
1799
- }));
1800
- return {
1801
- content: [{
1802
- type: 'text',
1803
- text: JSON.stringify({
1804
- sourceMemoryId: memoryId,
1805
- depth: maxDepth,
1806
- found: connected.length,
1807
- connected
1808
- }, null, 2)
1809
- }]
1810
- };
1811
- }
1812
- catch (error) {
1813
- return {
1814
- content: [{ type: 'text', text: `Error: ${error}` }],
1815
- isError: true
1816
- };
1817
- }
1818
- }
1819
- function getMemoryStats() {
1820
- try {
1821
- const totalStmt = db.prepare('SELECT COUNT(*) as count FROM memories');
1822
- const total = totalStmt.get().count;
1823
- const byTypeStmt = db.prepare(`
1824
- SELECT memory_type, COUNT(*) as count
1825
- FROM memories
1826
- GROUP BY memory_type
1827
- ORDER BY count DESC
1828
- `);
1829
- const byType = byTypeStmt.all();
1830
- const byProjectStmt = db.prepare(`
1831
- SELECT project, COUNT(*) as count
1832
- FROM memories
1833
- WHERE project IS NOT NULL
1834
- GROUP BY project
1835
- ORDER BY count DESC
1836
- `);
1837
- const byProject = byProjectStmt.all();
1838
- const relationsStmt = db.prepare('SELECT COUNT(*) as count FROM memory_relations');
1839
- const relationsCount = relationsStmt.get().count;
1840
- const recentStmt = db.prepare(`
1841
- SELECT * FROM memories
1842
- ORDER BY created_at DESC
1843
- LIMIT 5
1844
- `);
1845
- const recentRows = recentStmt.all();
1846
- const recent = recentRows.map(row => ({
1847
- id: row.id,
1848
- content: row.content.slice(0, 100) + (row.content.length > 100 ? '...' : ''),
1849
- type: row.memory_type,
1850
- createdAt: row.created_at
1851
- }));
1852
- const mostAccessedStmt = db.prepare(`
1853
- SELECT * FROM memories
1854
- ORDER BY access_count DESC
1855
- LIMIT 5
1856
- `);
1857
- const mostAccessedRows = mostAccessedStmt.all();
1858
- const mostAccessed = mostAccessedRows.map(row => ({
1859
- id: row.id,
1860
- content: row.content.slice(0, 100) + (row.content.length > 100 ? '...' : ''),
1861
- type: row.memory_type,
1862
- accessCount: row.access_count
1863
- }));
1864
- return {
1865
- content: [{
1866
- type: 'text',
1867
- text: JSON.stringify({
1868
- totalMemories: total,
1869
- totalRelations: relationsCount,
1870
- byType: Object.fromEntries(byType.map(t => [t.memory_type, t.count])),
1871
- byProject: Object.fromEntries(byProject.map(p => [p.project, p.count])),
1872
- recentMemories: recent,
1873
- mostAccessedMemories: mostAccessed
1874
- }, null, 2)
1875
- }]
1876
- };
1877
- }
1878
- catch (error) {
1879
- return {
1880
- content: [{ type: 'text', text: `Error: ${error}` }],
1881
- isError: true
1882
- };
1883
- }
1884
- }
1885
- function deleteMemory(memoryId) {
1886
- try {
1887
- // 관계도 함께 삭제 (CASCADE)
1888
- const stmt = db.prepare('DELETE FROM memories WHERE id = ?');
1889
- const result = stmt.run(memoryId);
1890
- return {
1891
- content: [{
1892
- type: 'text',
1893
- text: JSON.stringify({
1894
- success: result.changes > 0,
1895
- deleted: result.changes > 0 ? memoryId : null
1896
- })
1897
- }]
1898
- };
1899
- }
1900
- catch (error) {
1901
- return {
1902
- content: [{ type: 'text', text: `Error: ${error}` }],
1903
- isError: true
1904
- };
1905
- }
1906
- }
1907
- // ===== 시맨틱 검색 핸들러 =====
1908
- async function semanticSearch(query, limit = 10, minSimilarity = 0.3, memoryType, project) {
1909
- try {
1910
- // 쿼리 임베딩 생성
1911
- const queryEmbedding = await generateEmbedding(query);
1912
- if (!queryEmbedding) {
1913
- return {
1914
- content: [{
1915
- type: 'text',
1916
- text: JSON.stringify({
1917
- error: 'Embedding model not ready',
1918
- fallback: 'Using FTS search instead'
1919
- })
1920
- }]
1921
- };
1922
- }
1923
- // 모든 임베딩 가져오기
1924
- let sql = `
1925
- SELECT e.memory_id, e.embedding, m.*
1926
- FROM embeddings e
1927
- JOIN memories m ON e.memory_id = m.id
1928
- WHERE 1=1
1929
- `;
1930
- const params = [];
1931
- if (memoryType) {
1932
- sql += ' AND m.memory_type = ?';
1933
- params.push(memoryType);
1934
- }
1935
- if (project) {
1936
- sql += ' AND m.project = ?';
1937
- params.push(project);
1938
- }
1939
- const stmt = db.prepare(sql);
1940
- const rows = stmt.all(...params);
1941
- // 유사도 계산
1942
- const results = rows.map(row => {
1943
- const embedding = bufferToEmbedding(row.embedding);
1944
- const similarity = cosineSimilarity(queryEmbedding, embedding);
1945
- return { ...row, similarity };
1946
- })
1947
- .filter(r => r.similarity >= minSimilarity)
1948
- .sort((a, b) => b.similarity - a.similarity)
1949
- .slice(0, limit);
1950
- // 접근 횟수 업데이트
1951
- const updateStmt = db.prepare(`
1952
- UPDATE memories SET accessed_at = CURRENT_TIMESTAMP, access_count = access_count + 1
1953
- WHERE id = ?
1954
- `);
1955
- results.forEach(r => updateStmt.run(r.id));
1956
- const memories = results.map(r => ({
1957
- id: r.id,
1958
- content: r.content,
1959
- type: r.memory_type,
1960
- tags: r.tags ? JSON.parse(r.tags) : [],
1961
- project: r.project,
1962
- importance: r.importance,
1963
- createdAt: r.created_at,
1964
- similarity: Math.round(r.similarity * 1000) / 1000
1965
- }));
1966
- return {
1967
- content: [{
1968
- type: 'text',
1969
- text: JSON.stringify({
1970
- query,
1971
- searchType: 'semantic',
1972
- found: memories.length,
1973
- memories
1974
- }, null, 2)
1975
- }]
1976
- };
1977
- }
1978
- catch (error) {
1979
- return {
1980
- content: [{ type: 'text', text: `Error: ${error}` }],
1981
- isError: true
1982
- };
1983
- }
1984
- }
1985
- async function rebuildEmbeddings(force = false) {
1986
- try {
1987
- let sql = 'SELECT id, content FROM memories';
1988
- if (!force) {
1989
- sql += ' WHERE id NOT IN (SELECT memory_id FROM embeddings)';
1990
- }
1991
- const stmt = db.prepare(sql);
1992
- const rows = stmt.all();
1993
- if (rows.length === 0) {
1994
- return {
1995
- content: [{
1996
- type: 'text',
1997
- text: JSON.stringify({ message: 'No memories need embedding', processed: 0 })
1998
- }]
1999
- };
2000
- }
2001
- let processed = 0;
2002
- let failed = 0;
2003
- for (const row of rows) {
2004
- const embedding = await generateEmbedding(row.content);
2005
- if (embedding) {
2006
- try {
2007
- const embStmt = db.prepare(`
2008
- INSERT OR REPLACE INTO embeddings (memory_id, embedding)
2009
- VALUES (?, ?)
2010
- `);
2011
- embStmt.run(row.id, embeddingToBuffer(embedding));
2012
- processed++;
2013
- }
2014
- catch {
2015
- failed++;
2016
- }
2017
- }
2018
- else {
2019
- failed++;
2020
- }
2021
- }
2022
- return {
2023
- content: [{
2024
- type: 'text',
2025
- text: JSON.stringify({
2026
- message: 'Embedding rebuild complete',
2027
- total: rows.length,
2028
- processed,
2029
- failed
2030
- })
2031
- }]
2032
- };
2033
- }
2034
- catch (error) {
2035
- return {
2036
- content: [{ type: 'text', text: `Error: ${error}` }],
2037
- isError: true
2038
- };
2039
- }
2040
- }
2041
- function getEmbeddingStatus() {
2042
- try {
2043
- const totalMemories = db.prepare('SELECT COUNT(*) as count FROM memories').get().count;
2044
- const totalEmbeddings = db.prepare('SELECT COUNT(*) as count FROM embeddings').get().count;
2045
- const missingEmbeddings = totalMemories - totalEmbeddings;
2046
- return {
2047
- content: [{
2048
- type: 'text',
2049
- text: JSON.stringify({
2050
- modelReady: embeddingReady,
2051
- model: 'all-MiniLM-L6-v2',
2052
- dimensions: 384,
2053
- totalMemories,
2054
- totalEmbeddings,
2055
- missingEmbeddings,
2056
- coverage: totalMemories > 0 ? Math.round((totalEmbeddings / totalMemories) * 100) : 100
2057
- })
2058
- }]
2059
- };
2060
- }
2061
- catch (error) {
2062
- return {
2063
- content: [{ type: 'text', text: `Error: ${error}` }],
2064
- isError: true
2065
- };
2066
- }
2067
- }
2068
- // ===== 자동 피드백 수집 핸들러 =====
2069
- const FEEDBACK_IMPORTANCE = {
2070
- 'bug': 8,
2071
- 'timeout': 7,
2072
- 'performance': 6,
2073
- 'feature-request': 5,
2074
- 'ux': 4,
2075
- 'none': 0
2076
- };
2077
- async function collectWorkFeedback(project, workSummary, feedbackType, verificationPassed, feedbackContent, affectedTool, duration) {
2078
- try {
2079
- const result = {
2080
- workRecorded: false,
2081
- feedbackRecorded: false,
2082
- message: ''
2083
- };
2084
- // 1. 작업 기록 저장 (항상)
2085
- try {
2086
- const sessionStmt = db.prepare(`
2087
- INSERT INTO sessions (project, last_work, verification_result, duration_minutes)
2088
- VALUES (?, ?, ?, ?)
2089
- `);
2090
- sessionStmt.run(project, workSummary, verificationPassed ? 'passed' : 'failed', duration || null);
2091
- result.workRecorded = true;
2092
- }
2093
- catch (e) {
2094
- console.error('Failed to record work session:', e);
2095
- }
2096
- // 2. 피드백이 있으면 메모리에 저장
2097
- if (feedbackType !== 'none' && feedbackContent) {
2098
- const importance = FEEDBACK_IMPORTANCE[feedbackType] || 5;
2099
- const tags = ['mcp-feedback', feedbackType];
2100
- if (affectedTool) {
2101
- tags.push(affectedTool);
2102
- }
2103
- const content = `[MCP 피드백] ${feedbackContent}${affectedTool ? ` (도구: ${affectedTool})` : ''}`;
2104
- const memoryStmt = db.prepare(`
2105
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
2106
- VALUES (?, ?, ?, ?, ?, ?)
2107
- `);
2108
- const memoryResult = memoryStmt.run(content, 'feedback', JSON.stringify(tags), 'project-manager-mcp', importance, JSON.stringify({
2109
- sourceProject: project,
2110
- workSummary,
2111
- feedbackType,
2112
- affectedTool,
2113
- verificationPassed,
2114
- collectedAt: new Date().toISOString()
2115
- }));
2116
- result.feedbackRecorded = true;
2117
- result.feedbackId = memoryResult.lastInsertRowid;
2118
- // 임베딩 생성 (백그라운드)
2119
- generateEmbedding(content).then(embedding => {
2120
- if (embedding) {
2121
- try {
2122
- const embStmt = db.prepare(`
2123
- INSERT OR REPLACE INTO embeddings (memory_id, embedding)
2124
- VALUES (?, ?)
2125
- `);
2126
- embStmt.run(result.feedbackId, embeddingToBuffer(embedding));
2127
- }
2128
- catch (e) {
2129
- console.error('Failed to save feedback embedding:', e);
2130
- }
2131
- }
2132
- });
2133
- }
2134
- // 결과 메시지 생성
2135
- const messages = [];
2136
- if (result.workRecorded) {
2137
- messages.push(`작업 기록 저장됨: ${project}`);
2138
- }
2139
- if (result.feedbackRecorded) {
2140
- messages.push(`피드백 저장됨 (ID: ${result.feedbackId}, 유형: ${feedbackType})`);
2141
- }
2142
- if (!result.workRecorded && !result.feedbackRecorded) {
2143
- messages.push('저장된 내용 없음');
2144
- }
2145
- result.message = messages.join(', ');
2146
- return {
2147
- content: [{
2148
- type: 'text',
2149
- text: JSON.stringify(result, null, 2)
2150
- }]
2151
- };
2152
- }
2153
- catch (error) {
2154
- return {
2155
- content: [{ type: 'text', text: `Error: ${error}` }],
2156
- isError: true
2157
- };
2158
- }
2159
- }
2160
- function getPendingFeedbacks(feedbackType, limit = 20) {
2161
- try {
2162
- let sql = `
2163
- SELECT * FROM memories
2164
- WHERE memory_type = 'feedback'
2165
- AND tags LIKE '%mcp-feedback%'
2166
- `;
2167
- const params = [];
2168
- if (feedbackType) {
2169
- sql += ` AND tags LIKE ?`;
2170
- params.push(`%${feedbackType}%`);
2171
- }
2172
- sql += ` ORDER BY importance DESC, created_at DESC LIMIT ?`;
2173
- params.push(limit);
2174
- const stmt = db.prepare(sql);
2175
- const rows = stmt.all(...params);
2176
- const feedbacks = rows.map(row => {
2177
- const metadata = row.metadata ? JSON.parse(row.metadata) : {};
2178
- const tags = row.tags ? JSON.parse(row.tags) : [];
2179
- return {
2180
- id: row.id,
2181
- content: row.content.replace('[MCP 피드백] ', ''),
2182
- type: tags.find((t) => ['bug', 'timeout', 'feature-request', 'ux', 'performance'].includes(t)) || 'unknown',
2183
- affectedTool: metadata.affectedTool || null,
2184
- sourceProject: metadata.sourceProject || null,
2185
- importance: row.importance,
2186
- createdAt: row.created_at
2187
- };
2188
- });
2189
- return {
2190
- content: [{
2191
- type: 'text',
2192
- text: JSON.stringify({
2193
- found: feedbacks.length,
2194
- feedbacks
2195
- }, null, 2)
2196
- }]
2197
- };
2198
- }
2199
- catch (error) {
2200
- return {
2201
- content: [{ type: 'text', text: `Error: ${error}` }],
2202
- isError: true
2203
- };
2204
- }
2205
- }
2206
- function resolveFeedback(feedbackId, resolution) {
2207
- try {
2208
- // 피드백 존재 확인
2209
- const checkStmt = db.prepare('SELECT * FROM memories WHERE id = ? AND memory_type = ?');
2210
- const existing = checkStmt.get(feedbackId, 'feedback');
2211
- if (!existing) {
2212
- return {
2213
- content: [{
2214
- type: 'text',
2215
- text: JSON.stringify({ success: false, error: 'Feedback not found' })
2216
- }]
2217
- };
2218
- }
2219
- // 해결 기록 저장 (선택사항)
2220
- if (resolution) {
2221
- const metadata = existing.metadata ? JSON.parse(existing.metadata) : {};
2222
- metadata.resolved = true;
2223
- metadata.resolution = resolution;
2224
- metadata.resolvedAt = new Date().toISOString();
2225
- const updateStmt = db.prepare('UPDATE memories SET metadata = ? WHERE id = ?');
2226
- updateStmt.run(JSON.stringify(metadata), feedbackId);
2227
- }
2228
- // 피드백 삭제
2229
- const deleteStmt = db.prepare('DELETE FROM memories WHERE id = ?');
2230
- deleteStmt.run(feedbackId);
2231
- return {
2232
- content: [{
2233
- type: 'text',
2234
- text: JSON.stringify({
2235
- success: true,
2236
- resolvedId: feedbackId,
2237
- resolution: resolution || 'No resolution provided'
2238
- })
2239
- }]
2240
- };
2241
- }
2242
- catch (error) {
2243
- return {
2244
- content: [{ type: 'text', text: `Error: ${error}` }],
2245
- isError: true
2246
- };
2247
- }
2248
- }
2249
- // ===== Content Filtering 학습/회피 핸들러 =====
2250
- function recordFilterPattern(patternType, patternDescription, fileExtension, exampleContext, mitigationStrategy) {
2251
- try {
2252
- // 기존 유사 패턴 확인
2253
- const existingStmt = db.prepare(`
2254
- SELECT id, occurrence_count FROM content_filter_patterns
2255
- WHERE pattern_type = ? AND pattern_description = ?
2256
- `);
2257
- const existing = existingStmt.get(patternType, patternDescription);
2258
- if (existing) {
2259
- // 기존 패턴 업데이트
2260
- const updateStmt = db.prepare(`
2261
- UPDATE content_filter_patterns
2262
- SET occurrence_count = occurrence_count + 1,
2263
- last_occurred = CURRENT_TIMESTAMP,
2264
- mitigation_strategy = COALESCE(?, mitigation_strategy)
2265
- WHERE id = ?
2266
- `);
2267
- updateStmt.run(mitigationStrategy || null, existing.id);
2268
- // 캐시 갱신
2269
- loadContentFilterPatterns();
2270
- return {
2271
- content: [{
2272
- type: 'text',
2273
- text: JSON.stringify({
2274
- success: true,
2275
- action: 'updated',
2276
- id: existing.id,
2277
- occurrenceCount: existing.occurrence_count + 1
2278
- })
2279
- }]
2280
- };
2281
- }
2282
- else {
2283
- // 새 패턴 추가
2284
- const insertStmt = db.prepare(`
2285
- INSERT INTO content_filter_patterns
2286
- (pattern_type, pattern_description, file_extension, example_context, mitigation_strategy)
2287
- VALUES (?, ?, ?, ?, ?)
2288
- `);
2289
- const result = insertStmt.run(patternType, patternDescription, fileExtension || null, exampleContext || null, mitigationStrategy || null);
2290
- // 캐시 갱신
2291
- loadContentFilterPatterns();
2292
- return {
2293
- content: [{
2294
- type: 'text',
2295
- text: JSON.stringify({
2296
- success: true,
2297
- action: 'created',
2298
- id: result.lastInsertRowid
2299
- })
2300
- }]
2301
- };
2302
- }
2303
- }
2304
- catch (error) {
2305
- return {
2306
- content: [{ type: 'text', text: `Error: ${error}` }],
2307
- isError: true
2308
- };
2309
- }
2310
- }
2311
- function getFilterPatterns(patternType, fileExtension) {
2312
- try {
2313
- let sql = 'SELECT * FROM content_filter_patterns WHERE 1=1';
2314
- const params = [];
2315
- if (patternType) {
2316
- sql += ' AND pattern_type = ?';
2317
- params.push(patternType);
2318
- }
2319
- if (fileExtension) {
2320
- sql += ' AND file_extension = ?';
2321
- params.push(fileExtension);
2322
- }
2323
- sql += ' ORDER BY occurrence_count DESC, last_occurred DESC';
2324
- const stmt = db.prepare(sql);
2325
- const rows = stmt.all(...params);
2326
- const patterns = rows.map(row => ({
2327
- id: row.id,
2328
- patternType: row.pattern_type,
2329
- patternDescription: row.pattern_description,
2330
- fileExtension: row.file_extension,
2331
- exampleContext: row.example_context,
2332
- mitigationStrategy: row.mitigation_strategy,
2333
- occurrenceCount: row.occurrence_count,
2334
- lastOccurred: row.last_occurred
2335
- }));
2336
- return {
2337
- content: [{
2338
- type: 'text',
2339
- text: JSON.stringify({
2340
- found: patterns.length,
2341
- patterns
2342
- }, null, 2)
2343
- }]
2344
- };
2345
- }
2346
- catch (error) {
2347
- return {
2348
- content: [{ type: 'text', text: `Error: ${error}` }],
2349
- isError: true
2350
- };
2351
- }
2352
- }
2353
- function getProjectContext(project) {
2354
- try {
2355
- // Layer 1: 고정 컨텍스트
2356
- const fixedStmt = db.prepare(`SELECT * FROM project_context WHERE project = ?`);
2357
- const fixed = fixedStmt.get(project);
2358
- // Layer 2: 활성 컨텍스트
2359
- const activeStmt = db.prepare(`SELECT * FROM active_context WHERE project = ?`);
2360
- const active = activeStmt.get(project);
2361
- // Layer 3: 미완료 태스크 (상위 3개만)
2362
- const tasksStmt = db.prepare(`
2363
- SELECT id, title, status FROM tasks
2364
- WHERE project = ? AND status IN ('pending', 'in_progress', 'blocked')
2365
- ORDER BY
2366
- CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
2367
- priority DESC
2368
- LIMIT 3
2369
- `);
2370
- const topTasks = tasksStmt.all(project);
2371
- // 전체 미완료 태스크 수
2372
- const countStmt = db.prepare(`
2373
- SELECT COUNT(*) as count FROM tasks
2374
- WHERE project = ? AND status IN ('pending', 'in_progress', 'blocked')
2375
- `);
2376
- const countResult = countStmt.get(project);
2377
- const result = {
2378
- project,
2379
- tokenEstimate: 0,
2380
- fixed: {
2381
- techStack: fixed?.tech_stack ? JSON.parse(fixed.tech_stack) : null,
2382
- architecture: fixed?.architecture_decisions ? JSON.parse(fixed.architecture_decisions) : [],
2383
- patterns: fixed?.code_patterns ? JSON.parse(fixed.code_patterns) : [],
2384
- specialNotes: fixed?.special_notes || null
2385
- },
2386
- active: {
2387
- state: active?.current_state || null,
2388
- tasks: topTasks,
2389
- recentFiles: active?.recent_files ? JSON.parse(active.recent_files) : [],
2390
- blockers: active?.blockers || null,
2391
- lastVerification: active?.last_verification || null
2392
- },
2393
- pendingTaskCount: countResult.count
2394
- };
2395
- // 토큰 추정 (대략 4자 = 1토큰)
2396
- const jsonStr = JSON.stringify(result);
2397
- result.tokenEstimate = Math.ceil(jsonStr.length / 4);
2398
- // 사용 통계 기록
2399
- recordContextAccess(project, 'get');
2400
- return {
2401
- content: [{
2402
- type: 'text',
2403
- text: JSON.stringify(result, null, 2)
2404
- }]
2405
- };
2406
- }
2407
- catch (error) {
2408
- return {
2409
- content: [{ type: 'text', text: `Error: ${error}` }],
2410
- isError: true
2411
- };
2412
- }
2413
- }
2414
- function updateActiveContext(project, currentState, recentFiles, blockers, lastVerification) {
2415
- try {
2416
- // 활성 태스크 자동 조회
2417
- const tasksStmt = db.prepare(`
2418
- SELECT id, title, status FROM tasks
2419
- WHERE project = ? AND status IN ('in_progress', 'blocked')
2420
- ORDER BY priority DESC LIMIT 3
2421
- `);
2422
- const activeTasks = tasksStmt.all(project);
2423
- const stmt = db.prepare(`
2424
- INSERT INTO active_context (project, current_state, active_tasks, recent_files, blockers, last_verification, updated_at)
2425
- VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
2426
- ON CONFLICT(project) DO UPDATE SET
2427
- current_state = excluded.current_state,
2428
- active_tasks = excluded.active_tasks,
2429
- recent_files = excluded.recent_files,
2430
- blockers = excluded.blockers,
2431
- last_verification = excluded.last_verification,
2432
- updated_at = CURRENT_TIMESTAMP
2433
- `);
2434
- stmt.run(project, currentState, JSON.stringify(activeTasks), recentFiles ? JSON.stringify(recentFiles.slice(0, 10)) : null, blockers || null, lastVerification || null);
2435
- // 사용 통계 기록
2436
- recordContextAccess(project, 'update');
2437
- return {
2438
- content: [{
2439
- type: 'text',
2440
- text: JSON.stringify({
2441
- success: true,
2442
- project,
2443
- updated: {
2444
- currentState,
2445
- activeTasks: activeTasks.length,
2446
- recentFiles: recentFiles?.length || 0,
2447
- blockers: !!blockers,
2448
- lastVerification
2449
- }
2450
- }, null, 2)
2451
- }]
2452
- };
2453
- }
2454
- catch (error) {
2455
- return {
2456
- content: [{ type: 'text', text: `Error: ${error}` }],
2457
- isError: true
2458
- };
2459
- }
2460
- }
2461
- function initProjectContext(project, techStack, architectureDecisions, codePatterns, specialNotes) {
2462
- try {
2463
- const stmt = db.prepare(`
2464
- INSERT INTO project_context (project, tech_stack, architecture_decisions, code_patterns, special_notes, updated_at)
2465
- VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
2466
- ON CONFLICT(project) DO UPDATE SET
2467
- tech_stack = COALESCE(excluded.tech_stack, project_context.tech_stack),
2468
- architecture_decisions = COALESCE(excluded.architecture_decisions, project_context.architecture_decisions),
2469
- code_patterns = COALESCE(excluded.code_patterns, project_context.code_patterns),
2470
- special_notes = COALESCE(excluded.special_notes, project_context.special_notes),
2471
- updated_at = CURRENT_TIMESTAMP
2472
- `);
2473
- stmt.run(project, techStack ? JSON.stringify(techStack) : null, architectureDecisions ? JSON.stringify(architectureDecisions.slice(0, 5)) : null, codePatterns ? JSON.stringify(codePatterns.slice(0, 5)) : null, specialNotes || null);
2474
- return {
2475
- content: [{
2476
- type: 'text',
2477
- text: JSON.stringify({
2478
- success: true,
2479
- project,
2480
- initialized: {
2481
- techStack: !!techStack,
2482
- architectureDecisions: architectureDecisions?.length || 0,
2483
- codePatterns: codePatterns?.length || 0,
2484
- specialNotes: !!specialNotes
2485
- }
2486
- }, null, 2)
2487
- }]
2488
- };
2489
- }
2490
- catch (error) {
2491
- return {
2492
- content: [{ type: 'text', text: `Error: ${error}` }],
2493
- isError: true
2494
- };
2495
- }
2496
- }
2497
- function updateArchitectureDecision(project, decision) {
2498
- try {
2499
- // 기존 결정들 조회
2500
- const selectStmt = db.prepare(`SELECT architecture_decisions FROM project_context WHERE project = ?`);
2501
- const row = selectStmt.get(project);
2502
- let decisions = [];
2503
- if (row?.architecture_decisions) {
2504
- decisions = JSON.parse(row.architecture_decisions);
2505
- }
2506
- // 중복 체크 및 추가 (최대 5개, 최신이 앞으로)
2507
- if (!decisions.includes(decision)) {
2508
- decisions.unshift(decision);
2509
- decisions = decisions.slice(0, 5);
2510
- }
2511
- const stmt = db.prepare(`
2512
- INSERT INTO project_context (project, architecture_decisions, updated_at)
2513
- VALUES (?, ?, CURRENT_TIMESTAMP)
2514
- ON CONFLICT(project) DO UPDATE SET
2515
- architecture_decisions = excluded.architecture_decisions,
2516
- updated_at = CURRENT_TIMESTAMP
2517
- `);
2518
- stmt.run(project, JSON.stringify(decisions));
2519
- return {
2520
- content: [{
2521
- type: 'text',
2522
- text: JSON.stringify({
2523
- success: true,
2524
- project,
2525
- decision,
2526
- totalDecisions: decisions.length
2527
- }, null, 2)
2528
- }]
2529
- };
2530
- }
2531
- catch (error) {
2532
- return {
2533
- content: [{ type: 'text', text: `Error: ${error}` }],
2534
- isError: true
2535
- };
2536
- }
2537
- }
2538
- // ===== 태스크 관리 핸들러 =====
2539
- function addTask(project, title, description, priority = 5, relatedFiles, acceptanceCriteria) {
2540
- try {
2541
- const stmt = db.prepare(`
2542
- INSERT INTO tasks (project, title, description, priority, related_files, acceptance_criteria)
2543
- VALUES (?, ?, ?, ?, ?, ?)
2544
- `);
2545
- const result = stmt.run(project, title, description || null, Math.min(10, Math.max(1, priority)), relatedFiles ? JSON.stringify(relatedFiles) : null, acceptanceCriteria || null);
2546
- // 활성 컨텍스트의 태스크 목록 자동 업데이트
2547
- syncActiveTasksToContext(project);
2548
- return {
2549
- content: [{
2550
- type: 'text',
2551
- text: JSON.stringify({
2552
- success: true,
2553
- taskId: result.lastInsertRowid,
2554
- project,
2555
- title,
2556
- priority
2557
- }, null, 2)
2558
- }]
2559
- };
2560
- }
2561
- catch (error) {
2562
- return {
2563
- content: [{ type: 'text', text: `Error: ${error}` }],
2564
- isError: true
2565
- };
2566
- }
2567
- }
2568
- function completeTask(taskId) {
2569
- try {
2570
- const stmt = db.prepare(`
2571
- UPDATE tasks SET status = 'done', completed_at = CURRENT_TIMESTAMP
2572
- WHERE id = ?
2573
- `);
2574
- const result = stmt.run(taskId);
2575
- if (result.changes === 0) {
2576
- return {
2577
- content: [{ type: 'text', text: `Task ${taskId} not found` }],
2578
- isError: true
2579
- };
2580
- }
2581
- // 프로젝트 조회해서 활성 컨텍스트 업데이트
2582
- const taskStmt = db.prepare(`SELECT project, title FROM tasks WHERE id = ?`);
2583
- const task = taskStmt.get(taskId);
2584
- if (task) {
2585
- syncActiveTasksToContext(task.project);
2586
- }
2587
- return {
2588
- content: [{
2589
- type: 'text',
2590
- text: JSON.stringify({
2591
- success: true,
2592
- taskId,
2593
- title: task?.title,
2594
- status: 'done'
2595
- }, null, 2)
2596
- }]
2597
- };
2598
- }
2599
- catch (error) {
2600
- return {
2601
- content: [{ type: 'text', text: `Error: ${error}` }],
2602
- isError: true
2603
- };
2604
- }
2605
- }
2606
- function updateTaskStatus(taskId, status) {
2607
- try {
2608
- const validStatuses = ['pending', 'in_progress', 'done', 'blocked'];
2609
- if (!validStatuses.includes(status)) {
2610
- return {
2611
- content: [{ type: 'text', text: `Invalid status. Use: ${validStatuses.join(', ')}` }],
2612
- isError: true
2613
- };
2614
- }
2615
- const stmt = db.prepare(`
2616
- UPDATE tasks SET status = ?,
2617
- completed_at = CASE WHEN ? = 'done' THEN CURRENT_TIMESTAMP ELSE completed_at END
2618
- WHERE id = ?
2619
- `);
2620
- const result = stmt.run(status, status, taskId);
2621
- if (result.changes === 0) {
2622
- return {
2623
- content: [{ type: 'text', text: `Task ${taskId} not found` }],
2624
- isError: true
2625
- };
2626
- }
2627
- // 프로젝트 조회해서 활성 컨텍스트 업데이트
2628
- const taskStmt = db.prepare(`SELECT project, title FROM tasks WHERE id = ?`);
2629
- const task = taskStmt.get(taskId);
2630
- if (task) {
2631
- syncActiveTasksToContext(task.project);
2632
- }
2633
- return {
2634
- content: [{
2635
- type: 'text',
2636
- text: JSON.stringify({
2637
- success: true,
2638
- taskId,
2639
- title: task?.title,
2640
- newStatus: status
2641
- }, null, 2)
2642
- }]
2643
- };
2644
- }
2645
- catch (error) {
2646
- return {
2647
- content: [{ type: 'text', text: `Error: ${error}` }],
2648
- isError: true
2649
- };
2650
- }
2651
- }
2652
- function getPendingTasks(project, includeBlocked = true) {
2653
- try {
2654
- const statusFilter = includeBlocked
2655
- ? `('pending', 'in_progress', 'blocked')`
2656
- : `('pending', 'in_progress')`;
2657
- const stmt = db.prepare(`
2658
- SELECT id, title, description, status, priority, related_files, acceptance_criteria, created_at
2659
- FROM tasks
2660
- WHERE project = ? AND status IN ${statusFilter}
2661
- ORDER BY
2662
- CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
2663
- priority DESC,
2664
- created_at ASC
2665
- `);
2666
- const tasks = stmt.all(project);
2667
- const formattedTasks = tasks.map(t => ({
2668
- id: t.id,
2669
- title: t.title,
2670
- description: t.description,
2671
- status: t.status,
2672
- priority: t.priority,
2673
- relatedFiles: t.related_files ? JSON.parse(t.related_files) : [],
2674
- acceptanceCriteria: t.acceptance_criteria,
2675
- createdAt: t.created_at
2676
- }));
2677
- return {
2678
- content: [{
2679
- type: 'text',
2680
- text: JSON.stringify({
2681
- project,
2682
- totalPending: formattedTasks.length,
2683
- tasks: formattedTasks
2684
- }, null, 2)
2685
- }]
2686
- };
2687
- }
2688
- catch (error) {
2689
- return {
2690
- content: [{ type: 'text', text: `Error: ${error}` }],
2691
- isError: true
2692
- };
2693
- }
2694
- }
2695
- // 헬퍼: 활성 태스크를 active_context에 동기화
2696
- function syncActiveTasksToContext(project) {
2697
- try {
2698
- const tasksStmt = db.prepare(`
2699
- SELECT id, title, status FROM tasks
2700
- WHERE project = ? AND status IN ('in_progress', 'blocked')
2701
- ORDER BY priority DESC LIMIT 3
2702
- `);
2703
- const activeTasks = tasksStmt.all(project);
2704
- const updateStmt = db.prepare(`
2705
- UPDATE active_context SET active_tasks = ?, updated_at = CURRENT_TIMESTAMP
2706
- WHERE project = ?
2707
- `);
2708
- updateStmt.run(JSON.stringify(activeTasks), project);
2709
- }
2710
- catch {
2711
- // 실패해도 무시 (active_context가 없을 수 있음)
2712
- }
2713
- }
2714
- // ===== 에러 솔루션 아카이브 핸들러 =====
2715
- function recordSolution(errorSignature, solution, project, errorMessage, relatedFiles) {
2716
- try {
2717
- // 키워드 자동 추출 (에러 시그니처에서)
2718
- const keywords = errorSignature
2719
- .toLowerCase()
2720
- .split(/[\s:.\-_]+/)
2721
- .filter(w => w.length > 2)
2722
- .slice(0, 10)
2723
- .join(',');
2724
- const stmt = db.prepare(`
2725
- INSERT INTO resolved_issues (project, error_signature, error_message, solution, related_files, keywords)
2726
- VALUES (?, ?, ?, ?, ?, ?)
2727
- `);
2728
- const result = stmt.run(project || null, errorSignature, errorMessage || null, solution, relatedFiles ? JSON.stringify(relatedFiles) : null, keywords);
2729
- return {
2730
- content: [{
2731
- type: 'text',
2732
- text: JSON.stringify({
2733
- success: true,
2734
- solutionId: result.lastInsertRowid,
2735
- errorSignature,
2736
- keywords: keywords.split(',')
2737
- }, null, 2)
2738
- }]
2739
- };
2740
- }
2741
- catch (error) {
2742
- return {
2743
- content: [{ type: 'text', text: `Error: ${error}` }],
2744
- isError: true
2745
- };
2746
- }
2747
- }
2748
- function findSolution(errorText, project) {
2749
- try {
2750
- // 1. 정확한 시그니처 매칭
2751
- let stmt = db.prepare(`
2752
- SELECT id, project, error_signature, solution, related_files, created_at
2753
- FROM resolved_issues
2754
- WHERE error_signature LIKE ?
2755
- ${project ? 'AND (project = ? OR project IS NULL)' : ''}
2756
- ORDER BY created_at DESC
2757
- LIMIT 5
2758
- `);
2759
- let results = project
2760
- ? stmt.all(`%${errorText}%`, project)
2761
- : stmt.all(`%${errorText}%`);
2762
- // 2. 시그니처 매칭 없으면 키워드 검색
2763
- if (results.length === 0) {
2764
- const keywords = errorText
2765
- .toLowerCase()
2766
- .split(/[\s:.\-_]+/)
2767
- .filter(w => w.length > 2)
2768
- .slice(0, 5);
2769
- if (keywords.length > 0) {
2770
- const keywordPattern = keywords.map(k => `keywords LIKE '%${k}%'`).join(' OR ');
2771
- stmt = db.prepare(`
2772
- SELECT id, project, error_signature, solution, related_files, created_at
2773
- FROM resolved_issues
2774
- WHERE (${keywordPattern})
2775
- ${project ? 'AND (project = ? OR project IS NULL)' : ''}
2776
- ORDER BY created_at DESC
2777
- LIMIT 5
2778
- `);
2779
- results = project ? stmt.all(project) : stmt.all();
2780
- }
2781
- }
2782
- const solutions = results.map(r => ({
2783
- id: r.id,
2784
- project: r.project,
2785
- errorSignature: r.error_signature,
2786
- solution: r.solution,
2787
- relatedFiles: r.related_files ? JSON.parse(r.related_files) : [],
2788
- createdAt: r.created_at
2789
- }));
2790
- return {
2791
- content: [{
2792
- type: 'text',
2793
- text: JSON.stringify({
2794
- query: errorText.substring(0, 100),
2795
- found: solutions.length,
2796
- solutions
2797
- }, null, 2)
2798
- }]
2799
- };
2800
- }
2801
- catch (error) {
2802
- return {
2803
- content: [{ type: 'text', text: `Error: ${error}` }],
2804
- isError: true
2805
- };
2806
- }
2807
- }
2808
- // ===== 시스템 평가/모니터링 =====
2809
- // 컨텍스트 접근 기록용 (평가에 사용)
2810
- function recordContextAccess(project, accessType) {
2811
- try {
2812
- // memories 테이블에 접근 로그 저장 (기존 시스템 활용)
2813
- const stmt = db.prepare(`
2814
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
2815
- VALUES (?, 'observation', ?, ?, 1, ?)
2816
- `);
2817
- stmt.run(`Context ${accessType}: ${project}`, JSON.stringify(['system', 'context-access', accessType]), project, JSON.stringify({ accessType, timestamp: new Date().toISOString() }));
2818
- }
2819
- catch {
2820
- // 실패해도 무시
2821
- }
2822
- }
2823
- function getContinuityStats(project) {
2824
- try {
2825
- const stats = {};
2826
- // 1. 프로젝트 컨텍스트 현황
2827
- if (project) {
2828
- const contextStmt = db.prepare(`SELECT * FROM project_context WHERE project = ?`);
2829
- const context = contextStmt.get(project);
2830
- stats.hasProjectContext = !!context;
2831
- const activeStmt = db.prepare(`SELECT * FROM active_context WHERE project = ?`);
2832
- const active = activeStmt.get(project);
2833
- stats.hasActiveContext = !!active;
2834
- }
2835
- // 2. 태스크 통계
2836
- const taskStatsStmt = db.prepare(`
2837
- SELECT
2838
- ${project ? '' : 'project,'}
2839
- status,
2840
- COUNT(*) as count
2841
- FROM tasks
2842
- ${project ? 'WHERE project = ?' : ''}
2843
- GROUP BY ${project ? 'status' : 'project, status'}
2844
- `);
2845
- const taskStats = project ? taskStatsStmt.all(project) : taskStatsStmt.all();
2846
- stats.taskStats = taskStats;
2847
- // 3. 솔루션 아카이브 통계
2848
- const solutionStmt = db.prepare(`
2849
- SELECT COUNT(*) as count FROM resolved_issues
2850
- ${project ? 'WHERE project = ? OR project IS NULL' : ''}
2851
- `);
2852
- const solutionCount = (project ? solutionStmt.get(project) : solutionStmt.get());
2853
- stats.solutionCount = solutionCount.count;
2854
- // 4. 컨텍스트 접근 통계 (최근 7일)
2855
- const accessStmt = db.prepare(`
2856
- SELECT
2857
- json_extract(metadata, '$.accessType') as accessType,
2858
- COUNT(*) as count
2859
- FROM memories
2860
- WHERE memory_type = 'observation'
2861
- AND tags LIKE '%context-access%'
2862
- AND created_at > datetime('now', '-7 days')
2863
- ${project ? 'AND project = ?' : ''}
2864
- GROUP BY json_extract(metadata, '$.accessType')
2865
- `);
2866
- const accessStats = project ? accessStmt.all(project) : accessStmt.all();
2867
- stats.contextAccessLast7Days = accessStats;
2868
- // 5. 전체 프로젝트 수
2869
- if (!project) {
2870
- const projectCountStmt = db.prepare(`SELECT COUNT(DISTINCT project) as count FROM project_context`);
2871
- const projectCount = projectCountStmt.get();
2872
- stats.totalProjectsWithContext = projectCount.count;
2873
- }
2874
- // 6. 평가 메트릭
2875
- stats.evaluation = {
2876
- contextSystemUsed: stats.contextAccessLast7Days?.length > 0,
2877
- hasTasks: taskStats.length > 0,
2878
- hasSolutions: solutionCount.count > 0,
2879
- recommendation: generateRecommendation(stats)
2880
- };
2881
- return {
2882
- content: [{
2883
- type: 'text',
2884
- text: JSON.stringify(stats, null, 2)
2885
- }]
2886
- };
2887
- }
2888
- catch (error) {
2889
- return {
2890
- content: [{ type: 'text', text: `Error: ${error}` }],
2891
- isError: true
2892
- };
2893
- }
2894
- }
2895
- function generateRecommendation(stats) {
2896
- const issues = [];
2897
- if (!stats.hasProjectContext && stats.hasProjectContext !== undefined) {
2898
- issues.push('init_project_context로 프로젝트 컨텍스트 초기화 필요');
2899
- }
2900
- if (stats.contextAccessLast7Days?.length === 0) {
2901
- issues.push('/work 시작 시 get_project_context 호출 권장');
2902
- }
2903
- if (stats.solutionCount === 0) {
2904
- issues.push('에러 해결 시 record_solution으로 기록하면 재활용 가능');
2905
- }
2906
- return issues.length > 0 ? issues.join('; ') : '시스템 정상 활용 중';
2907
- }
2908
- function getSafeOutputGuidelines(context) {
2909
- try {
2910
- // 캐시된 패턴에서 가이드라인 생성
2911
- const guidelines = [];
2912
- const relevantPatterns = [];
2913
- // 컨텍스트 기반 필터링
2914
- if (context) {
2915
- const contextLower = context.toLowerCase();
2916
- for (const pattern of contentFilterPatterns) {
2917
- // 파일 확장자 매칭
2918
- if (pattern.fileExtension && contextLower.includes(pattern.fileExtension.replace('.', ''))) {
2919
- relevantPatterns.push(pattern);
2920
- continue;
2921
- }
2922
- // 패턴 설명 매칭
2923
- if (pattern.patternDescription.toLowerCase().split(' ').some(word => word.length > 3 && contextLower.includes(word))) {
2924
- relevantPatterns.push(pattern);
2925
- }
2926
- }
2927
- }
2928
- else {
2929
- // 모든 패턴 사용
2930
- relevantPatterns.push(...contentFilterPatterns);
2931
- }
2932
- // 기본 가이드라인
2933
- guidelines.push('1. 긴 코드 블록은 500자 이내로 요약하거나 청크로 분할');
2934
- guidelines.push('2. 파일 전체 내용 대신 핵심 부분만 인용');
2935
- guidelines.push('3. 바이너리/인코딩된 데이터는 메타정보만 출력');
2936
- // 학습된 패턴 기반 가이드라인
2937
- for (const pattern of relevantPatterns.slice(0, 5)) {
2938
- if (pattern.mitigationStrategy) {
2939
- guidelines.push(`- [${pattern.patternType}] ${pattern.mitigationStrategy}`);
2940
- }
2941
- }
2942
- // 파일 확장자별 특별 주의사항
2943
- const extensionWarnings = {};
2944
- for (const pattern of contentFilterPatterns) {
2945
- if (pattern.fileExtension && pattern.mitigationStrategy) {
2946
- extensionWarnings[pattern.fileExtension] = pattern.mitigationStrategy;
2947
- }
2948
- }
2949
- return {
2950
- content: [{
2951
- type: 'text',
2952
- text: JSON.stringify({
2953
- context: context || 'general',
2954
- guidelines,
2955
- extensionSpecificWarnings: extensionWarnings,
2956
- relevantPatternsCount: relevantPatterns.length,
2957
- totalPatternsLearned: contentFilterPatterns.length
2958
- }, null, 2)
2959
- }]
2960
- };
2961
- }
2962
- catch (error) {
2963
- return {
2964
- content: [{ type: 'text', text: `Error: ${error}` }],
2965
- isError: true
2966
- };
2967
- }
2968
- }
2969
- async function autoLearnDecision(args) {
2970
- try {
2971
- const { project, decision, reason, context, alternatives, files } = args;
2972
- const content = `[결정] ${decision}\n이유: ${reason}${context ? `\n맥락: ${context}` : ''}${alternatives?.length ? `\n대안: ${alternatives.join(', ')}` : ''}`;
2973
- const metadata = {
2974
- type: 'decision',
2975
- alternatives,
2976
- files,
2977
- context
2978
- };
2979
- const stmt = db.prepare(`
2980
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
2981
- VALUES (?, 'decision', ?, ?, 7, ?)
2982
- `);
2983
- const tags = JSON.stringify(['decision', project, ...(files?.map(f => path.basename(f)) || [])]);
2984
- const result = stmt.run(content, tags, project, JSON.stringify(metadata));
2985
- const memoryId = result.lastInsertRowid;
2986
- // 백그라운드 임베딩 생성
2987
- generateEmbedding(content).then(embedding => {
2988
- if (embedding) {
2989
- try {
2990
- const embStmt = db.prepare(`INSERT OR REPLACE INTO embeddings (memory_id, embedding) VALUES (?, ?)`);
2991
- embStmt.run(memoryId, embeddingToBuffer(embedding));
2992
- }
2993
- catch { }
2994
- }
2995
- });
2996
- return {
2997
- content: [{
2998
- type: 'text',
2999
- text: JSON.stringify({
3000
- success: true,
3001
- id: result.lastInsertRowid,
3002
- message: `결정 기록 저장됨: ${decision}`,
3003
- project
3004
- }, null, 2)
3005
- }]
3006
- };
3007
- }
3008
- catch (error) {
3009
- return {
3010
- content: [{ type: 'text', text: `Error: ${error}` }],
3011
- isError: true
3012
- };
3013
- }
3014
- }
3015
- async function autoLearnFix(args) {
3016
- try {
3017
- const { project, error, cause, solution, files, preventionTip } = args;
3018
- const content = `[에러] ${error}\n${cause ? `원인: ${cause}\n` : ''}해결: ${solution}${preventionTip ? `\n예방: ${preventionTip}` : ''}`;
3019
- const metadata = {
3020
- type: 'fix',
3021
- error,
3022
- cause,
3023
- solution,
3024
- files,
3025
- preventionTip
3026
- };
3027
- const stmt = db.prepare(`
3028
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
3029
- VALUES (?, 'error', ?, ?, 8, ?)
3030
- `);
3031
- const tags = JSON.stringify(['fix', 'error', project, ...(files?.map(f => path.basename(f)) || [])]);
3032
- const result = stmt.run(content, tags, project, JSON.stringify(metadata));
3033
- const memoryId = result.lastInsertRowid;
3034
- // 백그라운드 임베딩 생성
3035
- generateEmbedding(content).then(embedding => {
3036
- if (embedding) {
3037
- try {
3038
- const embStmt = db.prepare(`INSERT OR REPLACE INTO embeddings (memory_id, embedding) VALUES (?, ?)`);
3039
- embStmt.run(memoryId, embeddingToBuffer(embedding));
3040
- }
3041
- catch { }
3042
- }
3043
- });
3044
- return {
3045
- content: [{
3046
- type: 'text',
3047
- text: JSON.stringify({
3048
- success: true,
3049
- id: result.lastInsertRowid,
3050
- message: `에러 해결 기록 저장됨`,
3051
- error: error.substring(0, 100),
3052
- project
3053
- }, null, 2)
3054
- }]
3055
- };
3056
- }
3057
- catch (error) {
3058
- return {
3059
- content: [{ type: 'text', text: `Error: ${error}` }],
3060
- isError: true
3061
- };
3062
- }
3063
- }
3064
- async function autoLearnPattern(args) {
3065
- try {
3066
- const { project, patternName, description, example, appliesTo } = args;
3067
- const content = `[패턴] ${patternName}\n${description}${appliesTo ? `\n적용 대상: ${appliesTo}` : ''}${example ? `\n예시: ${example}` : ''}`;
3068
- const metadata = {
3069
- type: 'pattern',
3070
- patternName,
3071
- example,
3072
- appliesTo
3073
- };
3074
- const stmt = db.prepare(`
3075
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
3076
- VALUES (?, 'pattern', ?, ?, 6, ?)
3077
- `);
3078
- const tags = JSON.stringify(['pattern', project, patternName.toLowerCase().replace(/\s+/g, '-')]);
3079
- const result = stmt.run(content, tags, project, JSON.stringify(metadata));
3080
- const memoryId = result.lastInsertRowid;
3081
- generateEmbedding(content).then(embedding => {
3082
- if (embedding) {
3083
- try {
3084
- const embStmt = db.prepare(`INSERT OR REPLACE INTO embeddings (memory_id, embedding) VALUES (?, ?)`);
3085
- embStmt.run(memoryId, embeddingToBuffer(embedding));
3086
- }
3087
- catch { }
3088
- }
3089
- });
3090
- return {
3091
- content: [{
3092
- type: 'text',
3093
- text: JSON.stringify({
3094
- success: true,
3095
- id: result.lastInsertRowid,
3096
- message: `패턴 기록 저장됨: ${patternName}`,
3097
- project
3098
- }, null, 2)
3099
- }]
3100
- };
3101
- }
3102
- catch (error) {
3103
- return {
3104
- content: [{ type: 'text', text: `Error: ${error}` }],
3105
- isError: true
3106
- };
3107
- }
3108
- }
3109
- async function autoLearnDependency(args) {
3110
- try {
3111
- const { project, dependency, action, fromVersion, toVersion, reason, breakingChanges } = args;
3112
- const actionText = {
3113
- add: '추가',
3114
- remove: '제거',
3115
- upgrade: '업그레이드',
3116
- downgrade: '다운그레이드'
3117
- }[action];
3118
- const versionInfo = fromVersion && toVersion
3119
- ? `${fromVersion} → ${toVersion}`
3120
- : toVersion || fromVersion || '';
3121
- const content = `[의존성] ${dependency} ${actionText}${versionInfo ? ` (${versionInfo})` : ''}\n이유: ${reason}${breakingChanges ? `\nBreaking changes: ${breakingChanges}` : ''}`;
3122
- const metadata = {
3123
- type: 'dependency',
3124
- dependency,
3125
- action,
3126
- fromVersion,
3127
- toVersion,
3128
- breakingChanges
3129
- };
3130
- const stmt = db.prepare(`
3131
- INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
3132
- VALUES (?, 'learning', ?, ?, 6, ?)
3133
- `);
3134
- const tags = JSON.stringify(['dependency', action, project, dependency.toLowerCase()]);
3135
- const result = stmt.run(content, tags, project, JSON.stringify(metadata));
3136
- const memoryId = result.lastInsertRowid;
3137
- generateEmbedding(content).then(embedding => {
3138
- if (embedding) {
3139
- try {
3140
- const embStmt = db.prepare(`INSERT OR REPLACE INTO embeddings (memory_id, embedding) VALUES (?, ?)`);
3141
- embStmt.run(memoryId, embeddingToBuffer(embedding));
3142
- }
3143
- catch { }
3144
- }
3145
- });
3146
- return {
3147
- content: [{
3148
- type: 'text',
3149
- text: JSON.stringify({
3150
- success: true,
3151
- id: result.lastInsertRowid,
3152
- message: `의존성 변경 기록됨: ${dependency} ${actionText}`,
3153
- project
3154
- }, null, 2)
3155
- }]
3156
- };
3157
- }
3158
- catch (error) {
3159
- return {
3160
- content: [{ type: 'text', text: `Error: ${error}` }],
3161
- isError: true
3162
- };
3163
- }
3164
- }
3165
- function getProjectKnowledge(project, knowledgeType = 'all', limit = 20) {
3166
- try {
3167
- let query = `
3168
- SELECT id, content, memory_type, tags, importance, created_at, metadata
3169
- FROM memories
3170
- WHERE project = ?
3171
- `;
3172
- const params = [project];
3173
- if (knowledgeType !== 'all') {
3174
- const typeMap = {
3175
- 'decision': 'decision',
3176
- 'fix': 'error',
3177
- 'pattern': 'pattern',
3178
- 'dependency': 'learning'
3179
- };
3180
- query += ` AND memory_type = ?`;
3181
- params.push(typeMap[knowledgeType] || knowledgeType);
3182
- }
3183
- query += ` ORDER BY created_at DESC LIMIT ?`;
3184
- params.push(limit);
3185
- const stmt = db.prepare(query);
3186
- const rows = stmt.all(...params);
3187
- const knowledge = rows.map(row => {
3188
- let metadata = {};
3189
- try {
3190
- metadata = JSON.parse(row.metadata || '{}');
3191
- }
3192
- catch { }
3193
- const metaObj = metadata;
3194
- const typeValue = typeof metaObj.type === 'string' ? metaObj.type : row.memory_type;
3195
- return {
3196
- id: row.id,
3197
- type: typeValue,
3198
- content: row.content,
3199
- importance: row.importance,
3200
- createdAt: row.created_at
3201
- };
3202
- });
3203
- // 유형별 통계
3204
- const stats = {};
3205
- for (const k of knowledge) {
3206
- const typeKey = k.type;
3207
- stats[typeKey] = (stats[typeKey] || 0) + 1;
3208
- }
3209
- return {
3210
- content: [{
3211
- type: 'text',
3212
- text: JSON.stringify({
3213
- project,
3214
- totalKnowledge: knowledge.length,
3215
- stats,
3216
- knowledge: knowledge.slice(0, limit)
3217
- }, null, 2)
3218
- }]
3219
- };
3220
- }
3221
- catch (error) {
3222
- return {
3223
- content: [{ type: 'text', text: `Error: ${error}` }],
3224
- isError: true
3225
- };
3226
- }
3227
- }
3228
- async function getSimilarIssues(errorOrIssue, project, limit = 5) {
3229
- try {
3230
- // 먼저 시맨틱 검색 시도
3231
- if (embeddingPipeline) {
3232
- const result = await semanticSearch(errorOrIssue, limit, 0.3, 'error', project);
3233
- const resultText = JSON.parse(result.content[0].text);
3234
- if (resultText.found > 0) {
3235
- return {
3236
- content: [{
3237
- type: 'text',
3238
- text: JSON.stringify({
3239
- searchType: 'semantic',
3240
- query: errorOrIssue.substring(0, 100),
3241
- found: resultText.found,
3242
- solutions: resultText.results.map((r) => ({
3243
- id: r.id,
3244
- similarity: r.similarity,
3245
- content: r.content,
3246
- project: r.project
3247
- }))
3248
- }, null, 2)
3249
- }]
3250
- };
3251
- }
3252
- }
3253
- // 시맨틱 검색 결과 없으면 FTS 검색
3254
- const keywords = errorOrIssue.split(/\s+/).filter(w => w.length > 3).slice(0, 5);
3255
- const ftsQuery = keywords.join(' OR ');
3256
- let query = `
3257
- SELECT m.id, m.content, m.project, m.metadata
3258
- FROM memories_fts fts
3259
- JOIN memories m ON m.id = fts.rowid
3260
- WHERE memories_fts MATCH ?
3261
- AND m.memory_type = 'error'
3262
- `;
3263
- const params = [ftsQuery];
3264
- if (project) {
3265
- query += ` AND m.project = ?`;
3266
- params.push(project);
3267
- }
3268
- query += ` LIMIT ?`;
3269
- params.push(limit);
3270
- const stmt = db.prepare(query);
3271
- const rows = stmt.all(...params);
3272
- const solutions = rows.map(row => {
3273
- let metadata = {};
3274
- try {
3275
- metadata = JSON.parse(row.metadata || '{}');
3276
- }
3277
- catch { }
3278
- return {
3279
- id: row.id,
3280
- content: row.content,
3281
- project: row.project,
3282
- solution: metadata.solution
3283
- };
3284
- });
3285
- return {
3286
- content: [{
3287
- type: 'text',
3288
- text: JSON.stringify({
3289
- searchType: 'fts',
3290
- query: errorOrIssue.substring(0, 100),
3291
- found: solutions.length,
3292
- solutions
3293
- }, null, 2)
3294
- }]
3295
- };
3296
- }
3297
- catch (error) {
3298
- return {
3299
- content: [{ type: 'text', text: `Error: ${error}` }],
3300
- isError: true
3301
- };
3302
- }
3303
- }
3304
- // ===== 요청 핸들러 등록 =====
3305
- server.setRequestHandler(ListToolsRequestSchema, async () => {
3306
- return { tools };
3307
- });
3308
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
3309
- const { name, arguments: args } = request.params;
3310
- switch (name) {
3311
- case 'list_projects':
3312
- return listProjects();
3313
- case 'get_session':
3314
- return getSession(args?.project, args?.includeRaw || false, args?.maxContentLength ?? 2000);
3315
- case 'update_session':
3316
- return updateSession(args?.project, args?.lastWork, args?.currentStatus, args?.nextTasks, args?.modifiedFiles, args?.issues, args?.verificationResult);
3317
- case 'get_tech_stack':
3318
- return getTechStack(args?.project);
3319
- case 'run_verification':
3320
- return runVerification(args?.project, args?.gates);
3321
- case 'detect_platform':
3322
- return detectPlatform(args?.project);
3323
- // ===== SQLite 기반 새 도구들 =====
3324
- case 'save_session_history':
3325
- return saveSessionHistory(args?.project, args?.lastWork, args?.currentStatus, args?.nextTasks, args?.modifiedFiles, args?.issues, args?.verificationResult, args?.durationMinutes);
3326
- case 'get_session_history':
3327
- return getSessionHistory(args?.project, args?.limit || 10, args?.keyword);
3328
- case 'search_similar_work':
3329
- return searchSimilarWork(args?.keyword, args?.project);
3330
- case 'get_project_stats':
3331
- return getProjectStats(args?.project);
3332
- case 'record_work_pattern':
3333
- return recordWorkPattern(args?.project, args?.workType, args?.description, args?.filesPattern, args?.success, args?.durationMinutes);
3334
- case 'get_work_patterns':
3335
- return getWorkPatterns(args?.project, args?.workType);
3336
- // ===== 메모리 시스템 도구 =====
3337
- case 'store_memory':
3338
- return storeMemory(args?.content, args?.memoryType, args?.tags, args?.project, args?.importance, args?.metadata);
3339
- case 'recall_memory':
3340
- return recallMemory(args?.query, args?.memoryType, args?.project, args?.limit || 10, args?.minImportance || 1, args?.maxContentLength ?? 500);
3341
- case 'recall_by_timeframe':
3342
- return recallByTimeframe(args?.timeframe, args?.memoryType, args?.project, args?.limit || 20);
3343
- case 'search_by_tag':
3344
- return searchByTag(args?.tags, args?.matchAll || false, args?.limit || 20);
3345
- case 'create_relation':
3346
- return createRelation(args?.sourceId, args?.targetId, args?.relationType, args?.strength || 1.0);
3347
- case 'find_connected_memories':
3348
- return findConnectedMemories(args?.memoryId, args?.depth || 1, args?.relationType);
3349
- case 'get_memory_stats':
3350
- return getMemoryStats();
3351
- case 'delete_memory':
3352
- return deleteMemory(args?.memoryId);
3353
- // ===== 시맨틱 검색 도구 =====
3354
- case 'semantic_search':
3355
- return semanticSearch(args?.query, args?.limit || 10, args?.minSimilarity || 0.3, args?.memoryType, args?.project);
3356
- case 'rebuild_embeddings':
3357
- return rebuildEmbeddings(args?.force || false);
3358
- case 'get_embedding_status':
3359
- return getEmbeddingStatus();
3360
- // ===== 자동 피드백 수집 도구 =====
3361
- case 'collect_work_feedback':
3362
- return collectWorkFeedback(args?.project, args?.workSummary, args?.feedbackType, args?.verificationPassed, args?.feedbackContent, args?.affectedTool, args?.duration);
3363
- case 'get_pending_feedbacks':
3364
- return getPendingFeedbacks(args?.feedbackType, args?.limit || 20);
3365
- case 'resolve_feedback':
3366
- return resolveFeedback(args?.feedbackId, args?.resolution);
3367
- // ===== Content Filtering 학습/회피 도구 =====
3368
- case 'record_filter_pattern':
3369
- return recordFilterPattern(args?.patternType, args?.patternDescription, args?.fileExtension, args?.exampleContext, args?.mitigationStrategy);
3370
- case 'get_filter_patterns':
3371
- return getFilterPatterns(args?.patternType, args?.fileExtension);
3372
- case 'get_safe_output_guidelines':
3373
- return getSafeOutputGuidelines(args?.context);
3374
- // ===== 자동 학습 시스템 =====
3375
- case 'auto_learn_decision':
3376
- return await autoLearnDecision(args);
3377
- case 'auto_learn_fix':
3378
- return await autoLearnFix(args);
3379
- case 'auto_learn_pattern':
3380
- return await autoLearnPattern(args);
3381
- case 'auto_learn_dependency':
3382
- return await autoLearnDependency(args);
3383
- case 'get_project_knowledge':
3384
- return getProjectKnowledge(args?.project, args?.knowledgeType, args?.limit);
3385
- case 'get_similar_issues':
3386
- return await getSimilarIssues(args?.errorOrIssue, args?.project, args?.limit);
3387
- // ===== 프로젝트 연속성 시스템 v2 =====
3388
- case 'get_project_context':
3389
- return getProjectContext(args?.project);
3390
- case 'update_active_context':
3391
- return updateActiveContext(args?.project, args?.currentState, args?.recentFiles, args?.blockers, args?.lastVerification);
3392
- case 'init_project_context':
3393
- return initProjectContext(args?.project, args?.techStack, args?.architectureDecisions, args?.codePatterns, args?.specialNotes);
3394
- case 'update_architecture_decision':
3395
- return updateArchitectureDecision(args?.project, args?.decision);
3396
- // 태스크 관리
3397
- case 'add_task':
3398
- return addTask(args?.project, args?.title, args?.description, args?.priority, args?.relatedFiles, args?.acceptanceCriteria);
3399
- case 'complete_task':
3400
- return completeTask(args?.taskId);
3401
- case 'update_task_status':
3402
- return updateTaskStatus(args?.taskId, args?.status);
3403
- case 'get_pending_tasks':
3404
- return getPendingTasks(args?.project, args?.includeBlocked);
3405
- // 에러 솔루션 아카이브
3406
- case 'record_solution':
3407
- return recordSolution(args?.errorSignature, args?.solution, args?.project, args?.errorMessage, args?.relatedFiles);
3408
- case 'find_solution':
3409
- return findSolution(args?.errorText, args?.project);
3410
- // 시스템 평가
3411
- case 'get_continuity_stats':
3412
- return getContinuityStats(args?.project);
3413
- default:
3414
- return {
3415
- content: [{ type: 'text', text: `Unknown tool: ${name}` }],
3416
- isError: true
3417
- };
3418
- }
3419
- });
3420
- // ===== 서버 시작 =====
3421
- async function main() {
3422
- const transport = new StdioServerTransport();
3423
- await server.connect(transport);
3424
- console.error('Project Manager MCP Server running on stdio');
3425
- }
3426
- main().catch(console.error);