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/README.md +43 -2
- package/dist/hooks/install.js +62 -0
- package/dist/hooks/post-tool-use.js +0 -0
- package/dist/hooks/pre-compact.js +0 -0
- package/dist/hooks/session-end.js +104 -60
- package/dist/hooks/session-start.js +4 -3
- package/dist/hooks/user-prompt-submit.js +0 -0
- package/dist/index.js +0 -0
- package/dist/schemas.d.ts +54 -54
- package/dist/utils/logger.d.ts +10 -2
- package/dist/utils/logger.js +20 -3
- package/package.json +6 -5
- package/dist/dashboard-v2.d.ts +0 -11
- package/dist/dashboard-v2.js +0 -1321
- package/dist/dashboard.d.ts +0 -2
- package/dist/dashboard.js +0 -1196
- package/dist/index-old.d.ts +0 -2
- package/dist/index-old.js +0 -3426
- package/dist/index-v3.d.ts +0 -16
- package/dist/index-v3.js +0 -1024
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);
|