claude-session-continuity-mcp 1.1.1 → 1.3.0
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 +0 -1
- package/dist/db/database.js +62 -2
- package/dist/hooks/install.d.ts +9 -0
- package/dist/hooks/install.js +221 -0
- package/dist/hooks/session-start.d.ts +5 -0
- package/dist/hooks/session-start.js +140 -0
- package/dist/hooks/user-prompt-submit.d.ts +5 -0
- package/dist/hooks/user-prompt-submit.js +167 -0
- package/dist/index.js +152 -95
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -458,4 +458,3 @@ PRs welcome! Please:
|
|
|
458
458
|
|
|
459
459
|
- [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic
|
|
460
460
|
- [Xenova Transformers](https://github.com/xenova/transformers.js) for embeddings
|
|
461
|
-
- Inspired by [mcp-memory-service](https://github.com/doobidoo/mcp-memory-service)
|
package/dist/db/database.js
CHANGED
|
@@ -1,10 +1,70 @@
|
|
|
1
1
|
// SQLite 데이터베이스 초기화 및 관리
|
|
2
2
|
import Database from 'better-sqlite3';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
// ===== 경로 자동 감지 =====
|
|
7
|
+
/**
|
|
8
|
+
* 워크스페이스 루트 자동 감지
|
|
9
|
+
* 우선순위:
|
|
10
|
+
* 1. WORKSPACE_ROOT 환경변수
|
|
11
|
+
* 2. 현재 디렉토리에서 상위 탐색 (.claude/, apps/, turbo.json)
|
|
12
|
+
* 3. 현재 작업 디렉토리
|
|
13
|
+
* 4. 홈 디렉토리 (fallback)
|
|
14
|
+
*/
|
|
15
|
+
function detectWorkspaceRoot() {
|
|
16
|
+
// 1. 환경변수 우선
|
|
17
|
+
if (process.env.WORKSPACE_ROOT) {
|
|
18
|
+
return process.env.WORKSPACE_ROOT;
|
|
19
|
+
}
|
|
20
|
+
// 2. 현재 디렉토리에서 상위로 탐색
|
|
21
|
+
let current = process.cwd();
|
|
22
|
+
const root = path.parse(current).root;
|
|
23
|
+
while (current !== root) {
|
|
24
|
+
// 모노레포 root 감지
|
|
25
|
+
if (fs.existsSync(path.join(current, 'apps'))) {
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
// .claude 디렉토리가 있으면 여기가 프로젝트 루트
|
|
29
|
+
if (fs.existsSync(path.join(current, '.claude'))) {
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
32
|
+
// turbo.json + package.json = 모노레포
|
|
33
|
+
if (fs.existsSync(path.join(current, 'turbo.json')) && fs.existsSync(path.join(current, 'package.json'))) {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
current = path.dirname(current);
|
|
37
|
+
}
|
|
38
|
+
// 3. 현재 작업 디렉토리 사용 (단일 프로젝트로 간주)
|
|
39
|
+
return process.cwd();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* DB 경로 결정
|
|
43
|
+
* - 워크스페이스 루트의 .claude/sessions.db
|
|
44
|
+
* - 없으면 생성
|
|
45
|
+
*/
|
|
46
|
+
function getDbPath(workspaceRoot) {
|
|
47
|
+
const claudeDir = path.join(workspaceRoot, '.claude');
|
|
48
|
+
// .claude 디렉토리 생성
|
|
49
|
+
if (!fs.existsSync(claudeDir)) {
|
|
50
|
+
try {
|
|
51
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// 권한 없으면 홈 디렉토리에 생성
|
|
55
|
+
const homeClaudeDir = path.join(os.homedir(), '.claude');
|
|
56
|
+
if (!fs.existsSync(homeClaudeDir)) {
|
|
57
|
+
fs.mkdirSync(homeClaudeDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
return path.join(homeClaudeDir, 'sessions.db');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return path.join(claudeDir, 'sessions.db');
|
|
63
|
+
}
|
|
4
64
|
// 기본 경로 설정
|
|
5
|
-
export const WORKSPACE_ROOT =
|
|
65
|
+
export const WORKSPACE_ROOT = detectWorkspaceRoot();
|
|
6
66
|
export const APPS_DIR = path.join(WORKSPACE_ROOT, 'apps');
|
|
7
|
-
const DB_PATH =
|
|
67
|
+
const DB_PATH = getDbPath(WORKSPACE_ROOT);
|
|
8
68
|
// 데이터베이스 인스턴스
|
|
9
69
|
export const db = new Database(DB_PATH);
|
|
10
70
|
// Content Filtering 패턴 캐시
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code Hooks + MCP Server 자동 설치 스크립트
|
|
4
|
+
*
|
|
5
|
+
* npm install 시 자동으로:
|
|
6
|
+
* 1. ~/.claude/settings.local.json에 Hook 등록
|
|
7
|
+
* 2. ~/.claude.json에 MCP 서버 등록
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import { fileURLToPath } from 'url';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
16
|
+
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.local.json');
|
|
17
|
+
const MCP_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
|
|
18
|
+
// 설치된 패키지 경로 찾기
|
|
19
|
+
function getPackagePath() {
|
|
20
|
+
// 1. 글로벌 설치 확인
|
|
21
|
+
const globalPath = path.dirname(process.argv[1]);
|
|
22
|
+
if (fs.existsSync(path.join(globalPath, 'hooks'))) {
|
|
23
|
+
return globalPath;
|
|
24
|
+
}
|
|
25
|
+
// 2. 로컬 node_modules 확인
|
|
26
|
+
let current = process.cwd();
|
|
27
|
+
while (current !== path.parse(current).root) {
|
|
28
|
+
const candidate = path.join(current, 'node_modules', 'claude-session-continuity-mcp', 'dist', 'hooks');
|
|
29
|
+
if (fs.existsSync(candidate)) {
|
|
30
|
+
return path.join(current, 'node_modules', 'claude-session-continuity-mcp', 'dist');
|
|
31
|
+
}
|
|
32
|
+
current = path.dirname(current);
|
|
33
|
+
}
|
|
34
|
+
// 3. 현재 패키지 디렉토리 (ESM 호환)
|
|
35
|
+
return path.dirname(__dirname);
|
|
36
|
+
}
|
|
37
|
+
function loadSettings() {
|
|
38
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function saveSettings(settings) {
|
|
49
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
50
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
53
|
+
}
|
|
54
|
+
function loadMcpConfig() {
|
|
55
|
+
if (!fs.existsSync(MCP_CONFIG_FILE)) {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(MCP_CONFIG_FILE, 'utf-8'));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function saveMcpConfig(config) {
|
|
66
|
+
fs.writeFileSync(MCP_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
67
|
+
}
|
|
68
|
+
function installMcpServer() {
|
|
69
|
+
console.log('🔧 Registering MCP server...');
|
|
70
|
+
try {
|
|
71
|
+
const config = loadMcpConfig();
|
|
72
|
+
const mcpServers = config.mcpServers || {};
|
|
73
|
+
// 이미 등록되어 있으면 스킵
|
|
74
|
+
if (mcpServers['project-manager']) {
|
|
75
|
+
console.log(' MCP server already registered');
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// MCP 서버 등록
|
|
79
|
+
mcpServers['project-manager'] = {
|
|
80
|
+
command: 'npx',
|
|
81
|
+
args: ['claude-session-continuity-mcp']
|
|
82
|
+
};
|
|
83
|
+
config.mcpServers = mcpServers;
|
|
84
|
+
saveMcpConfig(config);
|
|
85
|
+
console.log('✅ MCP server registered in ~/.claude.json');
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('⚠️ Failed to register MCP server:', error);
|
|
90
|
+
console.log(' You can manually add to ~/.claude.json:');
|
|
91
|
+
console.log(' {');
|
|
92
|
+
console.log(' "mcpServers": {');
|
|
93
|
+
console.log(' "project-manager": {');
|
|
94
|
+
console.log(' "command": "npx",');
|
|
95
|
+
console.log(' "args": ["claude-session-continuity-mcp"]');
|
|
96
|
+
console.log(' }');
|
|
97
|
+
console.log(' }');
|
|
98
|
+
console.log(' }');
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function install() {
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
105
|
+
console.log('║ Claude Session Continuity MCP - Installation ║');
|
|
106
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
107
|
+
console.log('');
|
|
108
|
+
const packagePath = getPackagePath();
|
|
109
|
+
const hooksDir = path.join(packagePath, 'hooks');
|
|
110
|
+
// Hook 스크립트 경로
|
|
111
|
+
const sessionStartHook = path.join(hooksDir, 'session-start.js');
|
|
112
|
+
const userPromptHook = path.join(hooksDir, 'user-prompt-submit.js');
|
|
113
|
+
// ===== 1. Hooks 설치 =====
|
|
114
|
+
console.log('📌 Step 1: Installing Hooks...');
|
|
115
|
+
const settings = loadSettings();
|
|
116
|
+
// 기존 hooks 유지하면서 추가
|
|
117
|
+
const hooks = settings.hooks || {};
|
|
118
|
+
// SessionStart Hook
|
|
119
|
+
hooks.SessionStart = [
|
|
120
|
+
{
|
|
121
|
+
hooks: [
|
|
122
|
+
{
|
|
123
|
+
type: 'command',
|
|
124
|
+
command: `node "${sessionStartHook}"`
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|
|
128
|
+
];
|
|
129
|
+
// UserPromptSubmit Hook
|
|
130
|
+
hooks.UserPromptSubmit = [
|
|
131
|
+
{
|
|
132
|
+
hooks: [
|
|
133
|
+
{
|
|
134
|
+
type: 'command',
|
|
135
|
+
command: `node "${userPromptHook}"`
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
}
|
|
139
|
+
];
|
|
140
|
+
settings.hooks = hooks;
|
|
141
|
+
saveSettings(settings);
|
|
142
|
+
console.log('✅ Hooks installed');
|
|
143
|
+
console.log(` SessionStart: ${sessionStartHook}`);
|
|
144
|
+
console.log(` UserPromptSubmit: ${userPromptHook}`);
|
|
145
|
+
console.log('');
|
|
146
|
+
// ===== 2. MCP 서버 등록 =====
|
|
147
|
+
console.log('📌 Step 2: Registering MCP Server...');
|
|
148
|
+
installMcpServer();
|
|
149
|
+
console.log('');
|
|
150
|
+
// ===== 완료 메시지 =====
|
|
151
|
+
console.log('╔════════════════════════════════════════════════════════════╗');
|
|
152
|
+
console.log('║ ✅ Installation Complete! ║');
|
|
153
|
+
console.log('╠════════════════════════════════════════════════════════════╣');
|
|
154
|
+
console.log('║ ║');
|
|
155
|
+
console.log('║ 🚀 Restart Claude Code to activate: ║');
|
|
156
|
+
console.log('║ - 24 MCP tools (session_start, memory_store, etc.) ║');
|
|
157
|
+
console.log('║ - Auto context injection on session start ║');
|
|
158
|
+
console.log('║ ║');
|
|
159
|
+
console.log('║ 📖 Quick Start: ║');
|
|
160
|
+
console.log('║ 1. Start a new Claude Code session ║');
|
|
161
|
+
console.log('║ 2. Context will be auto-injected ║');
|
|
162
|
+
console.log('║ 3. Use session_end to save context ║');
|
|
163
|
+
console.log('║ ║');
|
|
164
|
+
console.log('╚════════════════════════════════════════════════════════════╝');
|
|
165
|
+
console.log('');
|
|
166
|
+
}
|
|
167
|
+
function uninstall() {
|
|
168
|
+
console.log('🔧 Removing Claude Code Hooks...');
|
|
169
|
+
const settings = loadSettings();
|
|
170
|
+
const hooks = settings.hooks || {};
|
|
171
|
+
// session-continuity 관련 Hook만 제거
|
|
172
|
+
delete hooks.SessionStart;
|
|
173
|
+
delete hooks.UserPromptSubmit;
|
|
174
|
+
if (Object.keys(hooks).length === 0) {
|
|
175
|
+
delete settings.hooks;
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
settings.hooks = hooks;
|
|
179
|
+
}
|
|
180
|
+
saveSettings(settings);
|
|
181
|
+
console.log('✅ Hooks removed successfully!');
|
|
182
|
+
}
|
|
183
|
+
function status() {
|
|
184
|
+
console.log('📊 Claude Code Hooks Status\n');
|
|
185
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
186
|
+
console.log('❌ No hooks configured');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const settings = loadSettings();
|
|
190
|
+
const hooks = settings.hooks;
|
|
191
|
+
if (!hooks) {
|
|
192
|
+
console.log('❌ No hooks configured');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
console.log('Configured hooks:');
|
|
196
|
+
for (const [event, hookList] of Object.entries(hooks)) {
|
|
197
|
+
console.log(` ${event}:`);
|
|
198
|
+
for (const hook of hookList) {
|
|
199
|
+
for (const h of hook.hooks || []) {
|
|
200
|
+
console.log(` → ${h.command}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// CLI
|
|
206
|
+
const args = process.argv.slice(2);
|
|
207
|
+
const command = args[0] || 'install';
|
|
208
|
+
switch (command) {
|
|
209
|
+
case 'install':
|
|
210
|
+
install();
|
|
211
|
+
break;
|
|
212
|
+
case 'uninstall':
|
|
213
|
+
case 'remove':
|
|
214
|
+
uninstall();
|
|
215
|
+
break;
|
|
216
|
+
case 'status':
|
|
217
|
+
status();
|
|
218
|
+
break;
|
|
219
|
+
default:
|
|
220
|
+
console.log('Usage: npx claude-session-continuity-hooks [install|uninstall|status]');
|
|
221
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionStart Hook - 세션 시작 시 컨텍스트 자동 주입
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
function detectWorkspaceRoot(cwd) {
|
|
9
|
+
let current = cwd;
|
|
10
|
+
const root = path.parse(current).root;
|
|
11
|
+
while (current !== root) {
|
|
12
|
+
if (fs.existsSync(path.join(current, 'apps')))
|
|
13
|
+
return current;
|
|
14
|
+
if (fs.existsSync(path.join(current, '.claude', 'sessions.db')))
|
|
15
|
+
return current;
|
|
16
|
+
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
17
|
+
// package.json이 있으면 여기가 프로젝트 루트일 가능성 높음
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
current = path.dirname(current);
|
|
21
|
+
}
|
|
22
|
+
return cwd;
|
|
23
|
+
}
|
|
24
|
+
function getProject(cwd, workspaceRoot) {
|
|
25
|
+
const appsDir = path.join(workspaceRoot, 'apps');
|
|
26
|
+
// apps/ 하위인지 확인
|
|
27
|
+
if (cwd.startsWith(appsDir + path.sep)) {
|
|
28
|
+
const relative = path.relative(appsDir, cwd);
|
|
29
|
+
return relative.split(path.sep)[0];
|
|
30
|
+
}
|
|
31
|
+
// 단일 프로젝트 모드
|
|
32
|
+
if (!fs.existsSync(appsDir)) {
|
|
33
|
+
return path.basename(workspaceRoot);
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function loadContext(dbPath, project) {
|
|
38
|
+
if (!fs.existsSync(dbPath))
|
|
39
|
+
return null;
|
|
40
|
+
try {
|
|
41
|
+
const db = new Database(dbPath, { readonly: true });
|
|
42
|
+
const lines = [`# ${project} - Session Resumed\n`];
|
|
43
|
+
// 기술 스택
|
|
44
|
+
const fixed = db.prepare('SELECT tech_stack FROM project_context WHERE project = ?').get(project);
|
|
45
|
+
if (fixed?.tech_stack) {
|
|
46
|
+
const stack = JSON.parse(fixed.tech_stack);
|
|
47
|
+
const stackStr = Object.entries(stack).map(([k, v]) => `**${k}**: ${v}`).join(', ');
|
|
48
|
+
lines.push(`## Tech Stack\n${stackStr}\n`);
|
|
49
|
+
}
|
|
50
|
+
// 현재 상태
|
|
51
|
+
const active = db.prepare('SELECT current_state, blockers FROM active_context WHERE project = ?').get(project);
|
|
52
|
+
if (active?.current_state) {
|
|
53
|
+
lines.push(`## Current State\n📍 ${active.current_state}`);
|
|
54
|
+
if (active.blockers)
|
|
55
|
+
lines.push(`🚧 **Blocker**: ${active.blockers}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
// 마지막 세션
|
|
59
|
+
const last = db.prepare('SELECT last_work, next_tasks, timestamp FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
60
|
+
if (last?.last_work) {
|
|
61
|
+
lines.push(`## Last Session (${last.timestamp?.slice(0, 10) || 'unknown'})`);
|
|
62
|
+
lines.push(`**Work**: ${last.last_work}`);
|
|
63
|
+
if (last.next_tasks) {
|
|
64
|
+
const next = JSON.parse(last.next_tasks);
|
|
65
|
+
if (next.length > 0)
|
|
66
|
+
lines.push(`**Next**: ${next.slice(0, 3).join(' → ')}`);
|
|
67
|
+
}
|
|
68
|
+
lines.push('');
|
|
69
|
+
}
|
|
70
|
+
// 미완료 태스크
|
|
71
|
+
const tasks = db.prepare(`
|
|
72
|
+
SELECT title, priority, status FROM tasks
|
|
73
|
+
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
74
|
+
ORDER BY priority DESC LIMIT 5
|
|
75
|
+
`).all(project);
|
|
76
|
+
if (tasks.length > 0) {
|
|
77
|
+
lines.push('## 📋 Pending Tasks');
|
|
78
|
+
for (const t of tasks) {
|
|
79
|
+
const icon = t.status === 'in_progress' ? '🔄' : '⏳';
|
|
80
|
+
lines.push(`- ${icon} [P${t.priority}] ${t.title}`);
|
|
81
|
+
}
|
|
82
|
+
lines.push('');
|
|
83
|
+
}
|
|
84
|
+
// 중요 메모리
|
|
85
|
+
const memories = db.prepare(`
|
|
86
|
+
SELECT content, memory_type FROM memories
|
|
87
|
+
WHERE project = ?
|
|
88
|
+
ORDER BY importance DESC, created_at DESC LIMIT 5
|
|
89
|
+
`).all(project);
|
|
90
|
+
if (memories.length > 0) {
|
|
91
|
+
const typeIcons = {
|
|
92
|
+
observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
|
|
93
|
+
};
|
|
94
|
+
lines.push('## 🧠 Key Memories');
|
|
95
|
+
for (const m of memories) {
|
|
96
|
+
const icon = typeIcons[m.memory_type] || '💭';
|
|
97
|
+
const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
|
|
98
|
+
lines.push(`- ${icon} [${m.memory_type}] ${content}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
db.close();
|
|
103
|
+
lines.push('---');
|
|
104
|
+
lines.push('_Auto-injected by session-continuity. Use `session_end` when done._');
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function main() {
|
|
112
|
+
try {
|
|
113
|
+
// stdin에서 입력 읽기
|
|
114
|
+
let inputData = '';
|
|
115
|
+
for await (const chunk of process.stdin) {
|
|
116
|
+
inputData += chunk;
|
|
117
|
+
}
|
|
118
|
+
const input = inputData ? JSON.parse(inputData) : {};
|
|
119
|
+
const cwd = input.cwd || process.cwd();
|
|
120
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
121
|
+
const project = getProject(cwd, workspaceRoot);
|
|
122
|
+
if (!project) {
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
const dbPath = path.join(workspaceRoot, '.claude', 'sessions.db');
|
|
126
|
+
const context = loadContext(dbPath, project);
|
|
127
|
+
if (context) {
|
|
128
|
+
console.log(`\n<session-context project="${project}">\n${context}\n</session-context>\n`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
console.log(`\n[Session] Project: ${project} (no context yet)\n`);
|
|
132
|
+
}
|
|
133
|
+
process.exit(0);
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
// 에러 시 조용히 종료
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
main();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* UserPromptSubmit Hook - 매 프롬프트마다 관련 컨텍스트 자동 주입
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import Database from 'better-sqlite3';
|
|
8
|
+
function detectWorkspaceRoot(cwd) {
|
|
9
|
+
let current = cwd;
|
|
10
|
+
const root = path.parse(current).root;
|
|
11
|
+
while (current !== root) {
|
|
12
|
+
if (fs.existsSync(path.join(current, 'apps')))
|
|
13
|
+
return current;
|
|
14
|
+
if (fs.existsSync(path.join(current, '.claude', 'sessions.db')))
|
|
15
|
+
return current;
|
|
16
|
+
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
current = path.dirname(current);
|
|
20
|
+
}
|
|
21
|
+
return cwd;
|
|
22
|
+
}
|
|
23
|
+
function getProject(cwd, workspaceRoot) {
|
|
24
|
+
const appsDir = path.join(workspaceRoot, 'apps');
|
|
25
|
+
if (cwd.startsWith(appsDir + path.sep)) {
|
|
26
|
+
const relative = path.relative(appsDir, cwd);
|
|
27
|
+
return relative.split(path.sep)[0];
|
|
28
|
+
}
|
|
29
|
+
if (!fs.existsSync(appsDir)) {
|
|
30
|
+
return path.basename(workspaceRoot);
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
function loadContext(dbPath, project) {
|
|
35
|
+
if (!fs.existsSync(dbPath))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const db = new Database(dbPath, { readonly: true });
|
|
39
|
+
const lines = [`# 🚀 ${project} Context\n`];
|
|
40
|
+
// 기술 스택
|
|
41
|
+
const fixed = db.prepare('SELECT tech_stack FROM project_context WHERE project = ?').get(project);
|
|
42
|
+
if (fixed?.tech_stack) {
|
|
43
|
+
const stack = JSON.parse(fixed.tech_stack);
|
|
44
|
+
const stackStr = Object.entries(stack).map(([k, v]) => `**${k}**: ${v}`).join(', ');
|
|
45
|
+
lines.push(`## Tech Stack\n${stackStr}\n`);
|
|
46
|
+
}
|
|
47
|
+
// 현재 상태
|
|
48
|
+
const active = db.prepare('SELECT current_state, blockers, last_verification FROM active_context WHERE project = ?').get(project);
|
|
49
|
+
if (active?.current_state) {
|
|
50
|
+
lines.push(`## Current State`);
|
|
51
|
+
lines.push(`📍 ${active.current_state}`);
|
|
52
|
+
if (active.blockers)
|
|
53
|
+
lines.push(`🚧 **Blocker**: ${active.blockers}`);
|
|
54
|
+
if (active.last_verification) {
|
|
55
|
+
const emoji = active.last_verification.includes('passed') ? '✅' : '❌';
|
|
56
|
+
lines.push(`${emoji} Last verify: ${active.last_verification}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push('');
|
|
59
|
+
}
|
|
60
|
+
// 마지막 세션
|
|
61
|
+
const last = db.prepare('SELECT last_work, next_tasks, timestamp FROM sessions WHERE project = ? ORDER BY timestamp DESC LIMIT 1').get(project);
|
|
62
|
+
if (last?.last_work) {
|
|
63
|
+
lines.push(`## Last Session (${last.timestamp?.slice(0, 10) || 'unknown'})`);
|
|
64
|
+
lines.push(`**Work**: ${last.last_work}`);
|
|
65
|
+
if (last.next_tasks) {
|
|
66
|
+
const next = JSON.parse(last.next_tasks);
|
|
67
|
+
if (next.length > 0)
|
|
68
|
+
lines.push(`**Next**: ${next.slice(0, 3).join(' → ')}`);
|
|
69
|
+
}
|
|
70
|
+
lines.push('');
|
|
71
|
+
}
|
|
72
|
+
// 미완료 태스크
|
|
73
|
+
const tasks = db.prepare(`
|
|
74
|
+
SELECT id, title, priority, status FROM tasks
|
|
75
|
+
WHERE project = ? AND status IN ('pending', 'in_progress')
|
|
76
|
+
ORDER BY priority DESC LIMIT 5
|
|
77
|
+
`).all(project);
|
|
78
|
+
if (tasks.length > 0) {
|
|
79
|
+
lines.push('## 📋 Pending Tasks');
|
|
80
|
+
for (const t of tasks) {
|
|
81
|
+
const icon = t.status === 'in_progress' ? '🔄' : '⏳';
|
|
82
|
+
lines.push(`- ${icon} [P${t.priority}] ${t.title} (#${t.id})`);
|
|
83
|
+
}
|
|
84
|
+
lines.push('');
|
|
85
|
+
}
|
|
86
|
+
// 중요 메모리
|
|
87
|
+
const memories = db.prepare(`
|
|
88
|
+
SELECT content, memory_type, importance FROM memories
|
|
89
|
+
WHERE project = ?
|
|
90
|
+
ORDER BY importance DESC, created_at DESC LIMIT 5
|
|
91
|
+
`).all(project);
|
|
92
|
+
if (memories.length > 0) {
|
|
93
|
+
const typeIcons = {
|
|
94
|
+
observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
|
|
95
|
+
};
|
|
96
|
+
lines.push('## 🧠 Key Memories');
|
|
97
|
+
for (const m of memories) {
|
|
98
|
+
const icon = typeIcons[m.memory_type] || '💭';
|
|
99
|
+
const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
|
|
100
|
+
lines.push(`- ${icon} [${m.memory_type}] ${content}`);
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
// 최근 에러 솔루션
|
|
105
|
+
const solutions = db.prepare(`
|
|
106
|
+
SELECT error_signature, solution FROM solutions
|
|
107
|
+
WHERE project = ?
|
|
108
|
+
ORDER BY created_at DESC LIMIT 3
|
|
109
|
+
`).all(project);
|
|
110
|
+
if (solutions.length > 0) {
|
|
111
|
+
lines.push('## 🔧 Recent Error Solutions');
|
|
112
|
+
for (const s of solutions) {
|
|
113
|
+
const sol = s.solution.length > 80 ? s.solution.slice(0, 80) + '...' : s.solution;
|
|
114
|
+
lines.push(`- **${s.error_signature}**: ${sol}`);
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
}
|
|
118
|
+
db.close();
|
|
119
|
+
lines.push('---');
|
|
120
|
+
lines.push('_Auto-injected by MCP v5. Use `session_end` when done._');
|
|
121
|
+
return lines.join('\n');
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function main() {
|
|
128
|
+
// 환경 변수로 비활성화 가능
|
|
129
|
+
if (process.env.MCP_HOOKS_DISABLED === 'true') {
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
// stdin에서 입력 읽기 (타임아웃 방지)
|
|
134
|
+
let inputData = '';
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
// 입력 없으면 그냥 진행
|
|
137
|
+
}, 100);
|
|
138
|
+
process.stdin.setEncoding('utf8');
|
|
139
|
+
process.stdin.on('data', (chunk) => {
|
|
140
|
+
inputData += chunk;
|
|
141
|
+
});
|
|
142
|
+
await new Promise((resolve) => {
|
|
143
|
+
process.stdin.on('end', () => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
resolve();
|
|
146
|
+
});
|
|
147
|
+
// 100ms 후 타임아웃
|
|
148
|
+
setTimeout(resolve, 100);
|
|
149
|
+
});
|
|
150
|
+
const cwd = process.cwd();
|
|
151
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
152
|
+
const project = getProject(cwd, workspaceRoot);
|
|
153
|
+
if (!project) {
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
const dbPath = path.join(workspaceRoot, '.claude', 'sessions.db');
|
|
157
|
+
const context = loadContext(dbPath, project);
|
|
158
|
+
if (context) {
|
|
159
|
+
console.log(`\n<project-context project="${project}">\n${context}\n</project-context>\n`);
|
|
160
|
+
}
|
|
161
|
+
process.exit(0);
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
main();
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
18
18
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
19
19
|
import { ListToolsRequestSchema, CallToolRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
20
20
|
import * as fs from 'fs/promises';
|
|
21
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
21
22
|
import * as path from 'path';
|
|
22
23
|
import { execSync } from 'child_process';
|
|
23
24
|
import Database from 'better-sqlite3';
|
|
@@ -26,15 +27,63 @@ import { pipeline, env } from '@xenova/transformers';
|
|
|
26
27
|
// 모델 캐시 설정
|
|
27
28
|
env.cacheDir = path.join(process.env.HOME || '/tmp', '.cache', 'transformers');
|
|
28
29
|
env.allowLocalModels = true;
|
|
29
|
-
// 기본 경로 설정
|
|
30
|
-
|
|
30
|
+
// 기본 경로 설정 (자동 감지)
|
|
31
|
+
function detectWorkspaceRoot() {
|
|
32
|
+
// 1. 환경변수가 설정되어 있으면 사용
|
|
33
|
+
if (process.env.WORKSPACE_ROOT) {
|
|
34
|
+
return process.env.WORKSPACE_ROOT;
|
|
35
|
+
}
|
|
36
|
+
// 2. 현재 디렉토리에서 상위로 탐색하며 apps/ 또는 .claude/ 디렉토리 찾기
|
|
37
|
+
let current = process.cwd();
|
|
38
|
+
const root = path.parse(current).root;
|
|
39
|
+
while (current !== root) {
|
|
40
|
+
// apps/ 디렉토리가 있으면 여기가 workspace root
|
|
41
|
+
if (existsSync(path.join(current, 'apps'))) {
|
|
42
|
+
return current;
|
|
43
|
+
}
|
|
44
|
+
// .claude/ 디렉토리가 있으면 여기가 workspace root
|
|
45
|
+
if (existsSync(path.join(current, '.claude'))) {
|
|
46
|
+
return current;
|
|
47
|
+
}
|
|
48
|
+
// package.json + turbo.json이 있으면 모노레포 루트
|
|
49
|
+
if (existsSync(path.join(current, 'package.json')) && existsSync(path.join(current, 'turbo.json'))) {
|
|
50
|
+
return current;
|
|
51
|
+
}
|
|
52
|
+
current = path.dirname(current);
|
|
53
|
+
}
|
|
54
|
+
// 3. 못 찾으면 현재 디렉토리 사용 (경고 출력)
|
|
55
|
+
console.error('Warning: WORKSPACE_ROOT not set and could not auto-detect. Using current directory.');
|
|
56
|
+
console.error('Set WORKSPACE_ROOT environment variable in your MCP config for best results.');
|
|
57
|
+
return process.cwd();
|
|
58
|
+
}
|
|
59
|
+
const WORKSPACE_ROOT = detectWorkspaceRoot();
|
|
31
60
|
const APPS_DIR = path.join(WORKSPACE_ROOT, 'apps');
|
|
32
|
-
const
|
|
61
|
+
const CLAUDE_DIR = path.join(WORKSPACE_ROOT, '.claude');
|
|
62
|
+
const DB_PATH = path.join(CLAUDE_DIR, 'sessions.db');
|
|
63
|
+
// 모노레포 vs 단일 프로젝트 모드 감지
|
|
64
|
+
const IS_MONOREPO = existsSync(APPS_DIR);
|
|
65
|
+
const DEFAULT_PROJECT = IS_MONOREPO ? null : path.basename(WORKSPACE_ROOT);
|
|
66
|
+
// ===== 디렉토리 생성 (동기) =====
|
|
67
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
68
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
69
|
+
}
|
|
33
70
|
// ===== SQLite 데이터베이스 초기화 =====
|
|
34
71
|
const db = new Database(DB_PATH);
|
|
35
|
-
//
|
|
72
|
+
// v5 스키마 - 세션 + 메모리 분류 체계 + 지식 그래프
|
|
36
73
|
db.exec(`
|
|
37
|
-
--
|
|
74
|
+
-- 세션 테이블 (핵심)
|
|
75
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
76
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
project TEXT NOT NULL,
|
|
78
|
+
last_work TEXT,
|
|
79
|
+
current_status TEXT,
|
|
80
|
+
next_tasks TEXT,
|
|
81
|
+
modified_files TEXT,
|
|
82
|
+
issues TEXT,
|
|
83
|
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
84
|
+
);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
38
87
|
|
|
39
88
|
-- 프로젝트 컨텍스트 (고정)
|
|
40
89
|
CREATE TABLE IF NOT EXISTS project_context (
|
|
@@ -249,6 +298,22 @@ function runCommand(cmd, cwd) {
|
|
|
249
298
|
return { success: false, output: e.stdout || e.stderr || e.message || 'Unknown error' };
|
|
250
299
|
}
|
|
251
300
|
}
|
|
301
|
+
// ===== 프로젝트 경로 헬퍼 (모노레포/단일 프로젝트 호환) =====
|
|
302
|
+
function getProjectPath(project) {
|
|
303
|
+
// 단일 프로젝트 모드: 프로젝트명이 workspace 이름과 같으면 루트 반환
|
|
304
|
+
if (!IS_MONOREPO && project === DEFAULT_PROJECT) {
|
|
305
|
+
return WORKSPACE_ROOT;
|
|
306
|
+
}
|
|
307
|
+
// 모노레포 모드: apps/ 하위 경로
|
|
308
|
+
return path.join(APPS_DIR, project);
|
|
309
|
+
}
|
|
310
|
+
function resolveProject(project) {
|
|
311
|
+
// 프로젝트명이 없으면 기본 프로젝트 사용 (단일 프로젝트 모드)
|
|
312
|
+
if (!project && DEFAULT_PROJECT) {
|
|
313
|
+
return DEFAULT_PROJECT;
|
|
314
|
+
}
|
|
315
|
+
return project || 'default';
|
|
316
|
+
}
|
|
252
317
|
// ===== MCP 서버 =====
|
|
253
318
|
const server = new Server({ name: 'project-manager-v5', version: '5.0.0' }, {
|
|
254
319
|
capabilities: {
|
|
@@ -619,9 +684,17 @@ async function handleTool(name, args) {
|
|
|
619
684
|
case 'session_start': {
|
|
620
685
|
const project = args.project;
|
|
621
686
|
const compact = args.compact !== false;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
687
|
+
// 모노레포 모드에서만 프로젝트 디렉토리 체크
|
|
688
|
+
// 단일 프로젝트 모드나 DB에 이미 데이터가 있으면 스킵
|
|
689
|
+
if (IS_MONOREPO) {
|
|
690
|
+
const projectPath = getProjectPath(project);
|
|
691
|
+
if (!await fileExists(projectPath)) {
|
|
692
|
+
// DB에 컨텍스트가 있는지 확인 (디렉토리 없어도 컨텍스트는 있을 수 있음)
|
|
693
|
+
const hasContext = db.prepare('SELECT 1 FROM project_context WHERE project = ?').get(project);
|
|
694
|
+
if (!hasContext) {
|
|
695
|
+
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
696
|
+
}
|
|
697
|
+
}
|
|
625
698
|
}
|
|
626
699
|
// 고정 컨텍스트
|
|
627
700
|
const fixedRow = db.prepare('SELECT * FROM project_context WHERE project = ?').get(project);
|
|
@@ -770,7 +843,7 @@ async function handleTool(name, args) {
|
|
|
770
843
|
// ===== 프로젝트 관리 =====
|
|
771
844
|
case 'project_status': {
|
|
772
845
|
const project = args.project;
|
|
773
|
-
const projectPath =
|
|
846
|
+
const projectPath = getProjectPath(project);
|
|
774
847
|
if (!await fileExists(projectPath)) {
|
|
775
848
|
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
776
849
|
}
|
|
@@ -815,7 +888,7 @@ async function handleTool(name, args) {
|
|
|
815
888
|
const project = args.project;
|
|
816
889
|
const techStack = args.techStack;
|
|
817
890
|
const description = args.description;
|
|
818
|
-
const projectPath =
|
|
891
|
+
const projectPath = getProjectPath(project);
|
|
819
892
|
// 기술 스택 자동 감지
|
|
820
893
|
const detectedStack = await detectTechStack(projectPath);
|
|
821
894
|
const finalStack = { ...detectedStack, ...techStack };
|
|
@@ -836,7 +909,7 @@ async function handleTool(name, args) {
|
|
|
836
909
|
}
|
|
837
910
|
case 'project_analyze': {
|
|
838
911
|
const project = args.project;
|
|
839
|
-
const projectPath =
|
|
912
|
+
const projectPath = getProjectPath(project);
|
|
840
913
|
if (!await fileExists(projectPath)) {
|
|
841
914
|
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
842
915
|
}
|
|
@@ -867,10 +940,18 @@ async function handleTool(name, args) {
|
|
|
867
940
|
}
|
|
868
941
|
case 'list_projects': {
|
|
869
942
|
try {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
943
|
+
let projects = [];
|
|
944
|
+
// 단일 프로젝트 모드
|
|
945
|
+
if (!IS_MONOREPO && DEFAULT_PROJECT) {
|
|
946
|
+
projects = [DEFAULT_PROJECT];
|
|
947
|
+
}
|
|
948
|
+
else if (IS_MONOREPO) {
|
|
949
|
+
// 모노레포 모드: apps/ 하위 디렉토리
|
|
950
|
+
const entries = await fs.readdir(APPS_DIR, { withFileTypes: true });
|
|
951
|
+
projects = entries
|
|
952
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
953
|
+
.map(e => e.name);
|
|
954
|
+
}
|
|
874
955
|
// 각 프로젝트 상태 조회
|
|
875
956
|
const projectsWithStatus = await Promise.all(projects.map(async (p) => {
|
|
876
957
|
const active = db.prepare('SELECT current_state FROM active_context WHERE project = ?').get(p);
|
|
@@ -878,7 +959,8 @@ async function handleTool(name, args) {
|
|
|
878
959
|
return {
|
|
879
960
|
name: p,
|
|
880
961
|
status: active?.current_state || 'No context',
|
|
881
|
-
pendingTasks: taskCount?.count || 0
|
|
962
|
+
pendingTasks: taskCount?.count || 0,
|
|
963
|
+
mode: IS_MONOREPO ? 'monorepo' : 'single'
|
|
882
964
|
};
|
|
883
965
|
}));
|
|
884
966
|
return { content: [{ type: 'text', text: JSON.stringify(projectsWithStatus, null, 2) }] };
|
|
@@ -931,12 +1013,12 @@ async function handleTool(name, args) {
|
|
|
931
1013
|
const tasks = status === 'all'
|
|
932
1014
|
? db.prepare(sql).all(project)
|
|
933
1015
|
: db.prepare(sql).all(project, status);
|
|
934
|
-
return { content: [{ type: 'text', text: JSON.stringify(tasks, null, 2) }] };
|
|
1016
|
+
return { content: [{ type: 'text', text: JSON.stringify({ project, status, count: tasks.length, tasks }, null, 2) }] };
|
|
935
1017
|
}
|
|
936
1018
|
case 'task_suggest': {
|
|
937
1019
|
const project = args.project;
|
|
938
1020
|
const searchPath = args.path;
|
|
939
|
-
const projectPath = path.join(
|
|
1021
|
+
const projectPath = path.join(getProjectPath(project), searchPath || '');
|
|
940
1022
|
// TODO, FIXME 등 검색
|
|
941
1023
|
try {
|
|
942
1024
|
const result = runCommand(`grep -rn "TODO\\|FIXME\\|HACK\\|XXX" --include="*.ts" --include="*.tsx" --include="*.dart" --include="*.kt" . | head -20`, projectPath);
|
|
@@ -984,12 +1066,13 @@ async function handleTool(name, args) {
|
|
|
984
1066
|
INSERT INTO solutions (project, error_signature, error_message, solution, related_files, keywords)
|
|
985
1067
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
986
1068
|
`).run(project || null, errorSignature, errorMessage || null, solution, relatedFiles ? JSON.stringify(relatedFiles) : null, keywords);
|
|
987
|
-
// 임베딩 저장 (시맨틱 검색용)
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1069
|
+
// 임베딩 저장 (시맨틱 검색용) - embeddings_v4 사용
|
|
1070
|
+
generateEmbedding(`${errorSignature} ${errorMessage || ''} ${solution}`).then(embedding => {
|
|
1071
|
+
if (embedding) {
|
|
1072
|
+
const buffer = Buffer.from(new Float32Array(embedding).buffer);
|
|
1073
|
+
db.prepare('INSERT OR REPLACE INTO embeddings_v4 (entity_type, entity_id, embedding) VALUES (?, ?, ?)').run('solution', result.lastInsertRowid, buffer);
|
|
1074
|
+
}
|
|
1075
|
+
}).catch(() => { });
|
|
993
1076
|
return {
|
|
994
1077
|
content: [{
|
|
995
1078
|
type: 'text',
|
|
@@ -1001,55 +1084,30 @@ async function handleTool(name, args) {
|
|
|
1001
1084
|
const query = args.query;
|
|
1002
1085
|
const project = args.project;
|
|
1003
1086
|
const limit = args.limit || 3;
|
|
1004
|
-
// 키워드 검색
|
|
1087
|
+
// 키워드 검색 (LIKE 기반, 안정적)
|
|
1005
1088
|
const keywordResults = db.prepare(`
|
|
1006
1089
|
SELECT * FROM solutions
|
|
1007
|
-
WHERE error_signature LIKE ? OR error_message LIKE ? OR keywords LIKE ?
|
|
1090
|
+
WHERE error_signature LIKE ? OR error_message LIKE ? OR solution LIKE ? OR keywords LIKE ?
|
|
1008
1091
|
${project ? 'AND project = ?' : ''}
|
|
1009
1092
|
ORDER BY created_at DESC LIMIT ?
|
|
1010
|
-
`).all(`%${query}%`, `%${query}%`, `%${query}%`, ...(project ? [project, limit] : [limit]));
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
created: r.created_at
|
|
1021
|
-
})), null, 2)
|
|
1022
|
-
}]
|
|
1023
|
-
};
|
|
1024
|
-
}
|
|
1025
|
-
// 시맨틱 검색 폴백
|
|
1026
|
-
const queryEmb = await generateEmbedding(query);
|
|
1027
|
-
if (!queryEmb) {
|
|
1028
|
-
return { content: [{ type: 'text', text: 'No solutions found' }] };
|
|
1029
|
-
}
|
|
1030
|
-
const allSolutions = db.prepare(`
|
|
1031
|
-
SELECT s.*, e.embedding FROM solutions s
|
|
1032
|
-
LEFT JOIN embeddings_v3 e ON e.type = 'solution' AND e.ref_id = s.id
|
|
1033
|
-
${project ? 'WHERE s.project = ?' : ''}
|
|
1034
|
-
LIMIT 50
|
|
1035
|
-
`).all(project ? [project] : []);
|
|
1036
|
-
const scored = allSolutions.map(s => {
|
|
1037
|
-
if (!s.embedding)
|
|
1038
|
-
return { ...s, similarity: 0 };
|
|
1039
|
-
const emb = Array.from(new Float32Array(s.embedding.buffer));
|
|
1040
|
-
return { ...s, similarity: cosineSimilarity(queryEmb, emb) };
|
|
1041
|
-
});
|
|
1042
|
-
scored.sort((a, b) => b.similarity - a.similarity);
|
|
1043
|
-
const topResults = scored.slice(0, limit);
|
|
1093
|
+
`).all(`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, ...(project ? [project, limit] : [limit]));
|
|
1094
|
+
const results = keywordResults.map(r => ({
|
|
1095
|
+
id: r.id,
|
|
1096
|
+
errorSignature: r.error_signature,
|
|
1097
|
+
errorMessage: r.error_message,
|
|
1098
|
+
solution: r.solution,
|
|
1099
|
+
project: r.project,
|
|
1100
|
+
relatedFiles: r.related_files,
|
|
1101
|
+
created: r.created_at
|
|
1102
|
+
}));
|
|
1044
1103
|
return {
|
|
1045
1104
|
content: [{
|
|
1046
1105
|
type: 'text',
|
|
1047
|
-
text: JSON.stringify(
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
})), null, 2)
|
|
1106
|
+
text: JSON.stringify({
|
|
1107
|
+
query,
|
|
1108
|
+
found: results.length,
|
|
1109
|
+
results
|
|
1110
|
+
}, null, 2)
|
|
1053
1111
|
}]
|
|
1054
1112
|
};
|
|
1055
1113
|
}
|
|
@@ -1081,7 +1139,7 @@ async function handleTool(name, args) {
|
|
|
1081
1139
|
// ===== 검증/품질 =====
|
|
1082
1140
|
case 'verify_build': {
|
|
1083
1141
|
const project = args.project;
|
|
1084
|
-
const projectPath =
|
|
1142
|
+
const projectPath = getProjectPath(project);
|
|
1085
1143
|
const platform = await detectPlatform(projectPath);
|
|
1086
1144
|
let cmd;
|
|
1087
1145
|
switch (platform) {
|
|
@@ -1113,7 +1171,7 @@ async function handleTool(name, args) {
|
|
|
1113
1171
|
case 'verify_test': {
|
|
1114
1172
|
const project = args.project;
|
|
1115
1173
|
const testPath = args.testPath;
|
|
1116
|
-
const projectPath =
|
|
1174
|
+
const projectPath = getProjectPath(project);
|
|
1117
1175
|
const platform = await detectPlatform(projectPath);
|
|
1118
1176
|
let cmd;
|
|
1119
1177
|
switch (platform) {
|
|
@@ -1137,7 +1195,7 @@ async function handleTool(name, args) {
|
|
|
1137
1195
|
case 'verify_all': {
|
|
1138
1196
|
const project = args.project;
|
|
1139
1197
|
const stopOnFail = args.stopOnFail === true;
|
|
1140
|
-
const projectPath =
|
|
1198
|
+
const projectPath = getProjectPath(project);
|
|
1141
1199
|
const platform = await detectPlatform(projectPath);
|
|
1142
1200
|
const results = [];
|
|
1143
1201
|
// Build
|
|
@@ -1187,6 +1245,13 @@ async function handleTool(name, args) {
|
|
|
1187
1245
|
const tags = args.tags;
|
|
1188
1246
|
const importance = args.importance || 5;
|
|
1189
1247
|
const relatedTo = args.relatedTo;
|
|
1248
|
+
// 필수 파라미터 검증
|
|
1249
|
+
if (!content || content.trim().length === 0) {
|
|
1250
|
+
return { content: [{ type: 'text', text: 'Error: content is required and cannot be empty' }] };
|
|
1251
|
+
}
|
|
1252
|
+
if (!memoryType) {
|
|
1253
|
+
return { content: [{ type: 'text', text: 'Error: type is required' }] };
|
|
1254
|
+
}
|
|
1190
1255
|
// 메모리 저장
|
|
1191
1256
|
const result = db.prepare(`
|
|
1192
1257
|
INSERT INTO memories (content, memory_type, tags, project, importance, metadata)
|
|
@@ -1250,43 +1315,35 @@ async function handleTool(name, args) {
|
|
|
1250
1315
|
}
|
|
1251
1316
|
}
|
|
1252
1317
|
else {
|
|
1253
|
-
//
|
|
1254
|
-
|
|
1318
|
+
// LIKE 기반 키워드 검색 (FTS5보다 안정적)
|
|
1319
|
+
// 검색어를 단어로 분리하여 OR 조건으로 검색
|
|
1320
|
+
const words = query.split(/\s+/).filter(w => w.length > 0);
|
|
1321
|
+
const likeConditions = words.map(() => '(content LIKE ? OR tags LIKE ?)').join(' OR ');
|
|
1322
|
+
const likeParams = [];
|
|
1323
|
+
words.forEach(w => {
|
|
1324
|
+
likeParams.push(`%${w}%`, `%${w}%`);
|
|
1325
|
+
});
|
|
1255
1326
|
let sql = `
|
|
1256
|
-
SELECT
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
AND m.importance >= ?
|
|
1327
|
+
SELECT * FROM memories
|
|
1328
|
+
WHERE (${likeConditions || 'content LIKE ?'})
|
|
1329
|
+
AND importance >= ?
|
|
1260
1330
|
`;
|
|
1261
|
-
const params = [
|
|
1331
|
+
const params = [...(likeConditions ? likeParams : [`%${query}%`]), minImportance];
|
|
1262
1332
|
if (memoryType && memoryType !== 'all') {
|
|
1263
|
-
sql += ` AND
|
|
1333
|
+
sql += ` AND memory_type = ?`;
|
|
1264
1334
|
params.push(memoryType);
|
|
1265
1335
|
}
|
|
1266
1336
|
if (project) {
|
|
1267
|
-
sql += ` AND
|
|
1337
|
+
sql += ` AND project = ?`;
|
|
1268
1338
|
params.push(project);
|
|
1269
1339
|
}
|
|
1270
1340
|
if (tags && tags.length > 0) {
|
|
1271
|
-
sql += ` AND (${tags.map(() => '
|
|
1341
|
+
sql += ` AND (${tags.map(() => 'tags LIKE ?').join(' OR ')})`;
|
|
1272
1342
|
params.push(...tags.map(t => `%"${t}"%`));
|
|
1273
1343
|
}
|
|
1274
|
-
sql += ` ORDER BY
|
|
1344
|
+
sql += ` ORDER BY importance DESC, accessed_at DESC LIMIT ?`;
|
|
1275
1345
|
params.push(limit);
|
|
1276
|
-
|
|
1277
|
-
results = db.prepare(sql).all(...params);
|
|
1278
|
-
}
|
|
1279
|
-
catch {
|
|
1280
|
-
// FTS 실패 시 LIKE 폴백
|
|
1281
|
-
results = db.prepare(`
|
|
1282
|
-
SELECT * FROM memories
|
|
1283
|
-
WHERE (content LIKE ? OR tags LIKE ?)
|
|
1284
|
-
AND importance >= ?
|
|
1285
|
-
${memoryType && memoryType !== 'all' ? 'AND memory_type = ?' : ''}
|
|
1286
|
-
${project ? 'AND project = ?' : ''}
|
|
1287
|
-
ORDER BY importance DESC LIMIT ?
|
|
1288
|
-
`).all(`%${query}%`, `%${query}%`, minImportance, ...(memoryType && memoryType !== 'all' ? [memoryType] : []), ...(project ? [project] : []), limit);
|
|
1289
|
-
}
|
|
1346
|
+
results = db.prepare(sql).all(...params);
|
|
1290
1347
|
}
|
|
1291
1348
|
// 접근 기록 업데이트
|
|
1292
1349
|
const ids = results.map(r => r.id);
|
|
@@ -1623,7 +1680,7 @@ const prompts = [
|
|
|
1623
1680
|
];
|
|
1624
1681
|
// ===== Prompt 내용 생성 함수 =====
|
|
1625
1682
|
async function generateProjectContext(project) {
|
|
1626
|
-
const projectPath =
|
|
1683
|
+
const projectPath = getProjectPath(project);
|
|
1627
1684
|
// 프로젝트 존재 확인
|
|
1628
1685
|
if (!await fileExists(projectPath)) {
|
|
1629
1686
|
return `⚠️ 프로젝트를 찾을 수 없습니다: ${project}\n\napps/ 디렉토리에서 사용 가능한 프로젝트를 확인하세요.`;
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-session-continuity-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Session Continuity for Claude Code - Never re-explain your project again",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
|
-
"claude-session-continuity-mcp": "./dist/index.js"
|
|
8
|
+
"claude-session-continuity-mcp": "./dist/index.js",
|
|
9
|
+
"claude-session-hooks": "./dist/hooks/install.js"
|
|
9
10
|
},
|
|
10
11
|
"scripts": {
|
|
11
12
|
"build": "tsc",
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
"test:coverage": "vitest run --coverage",
|
|
18
19
|
"dashboard": "node dist/dashboard.js",
|
|
19
20
|
"dashboard:v2": "node dist/dashboard-v2.js",
|
|
21
|
+
"postinstall": "node dist/hooks/install.js install 2>/dev/null || true",
|
|
20
22
|
"prepublishOnly": "npm run build && npm test"
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
"homepage": "https://github.com/leesgit/claude-session-continuity-mcp#readme",
|
|
44
46
|
"files": [
|
|
45
47
|
"dist",
|
|
48
|
+
"dist/hooks",
|
|
46
49
|
"README.md",
|
|
47
50
|
"LICENSE"
|
|
48
51
|
],
|