claude-session-continuity-mcp 1.1.0 → 1.2.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 -25
- package/dist/hooks/install.d.ts +7 -0
- package/dist/hooks/install.js +145 -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 +93 -19
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -115,30 +115,6 @@ This registers automatic context hooks in `~/.claude/settings.local.json`.
|
|
|
115
115
|
|
|
116
116
|
---
|
|
117
117
|
|
|
118
|
-
## Why Not Use...?
|
|
119
|
-
|
|
120
|
-
| Tool | What It Does | Why This Is Different |
|
|
121
|
-
|------|--------------|----------------------|
|
|
122
|
-
| **mcp-memory-service** | Generic AI memory | **Git integration**, task/solution unified, multilingual |
|
|
123
|
-
| **Official Memory** | Simple key-value store | **Auto-capture**, semantic search, knowledge graph |
|
|
124
|
-
| **SESSION.md files** | Manual markdown files | **Fully automatic**, Hook-based |
|
|
125
|
-
|
|
126
|
-
### vs mcp-memory-service (Detailed Comparison)
|
|
127
|
-
|
|
128
|
-
| Feature | This MCP | mcp-memory-service |
|
|
129
|
-
|---------|----------|-------------------|
|
|
130
|
-
| Auto-capture | ✅ Hook-based | ✅ Hook-based |
|
|
131
|
-
| Semantic search | ✅ MiniLM-L6-v2 | ✅ MiniLM-L6-v2 |
|
|
132
|
-
| **Git commit integration** | ✅ Commit → Memory | ❌ |
|
|
133
|
-
| **Task management** | ✅ Built-in | ❌ |
|
|
134
|
-
| **Solution archive** | ✅ Error solution DB | ❌ |
|
|
135
|
-
| **Build/Test** | ✅ verify_all | ❌ |
|
|
136
|
-
| **Multilingual patterns** | ✅ KO/EN/JA | ❌ English-centric |
|
|
137
|
-
| Cloud sync | ❌ | ✅ Cloudflare D1 |
|
|
138
|
-
| Web dashboard | ❌ | ✅ Port 8000 |
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
118
|
## Claude Hooks (v5) - Auto-Capture System
|
|
143
119
|
|
|
144
120
|
### Directory Structure
|
|
@@ -482,4 +458,3 @@ PRs welcome! Please:
|
|
|
482
458
|
|
|
483
459
|
- [Model Context Protocol](https://modelcontextprotocol.io/) by Anthropic
|
|
484
460
|
- [Xenova Transformers](https://github.com/xenova/transformers.js) for embeddings
|
|
485
|
-
- Inspired by [mcp-memory-service](https://github.com/doobidoo/mcp-memory-service)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code Hooks 자동 설치 스크립트
|
|
4
|
+
*
|
|
5
|
+
* npm install 시 자동으로 ~/.claude/settings.local.json에 Hook 등록
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
14
|
+
const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.local.json');
|
|
15
|
+
// 설치된 패키지 경로 찾기
|
|
16
|
+
function getPackagePath() {
|
|
17
|
+
// 1. 글로벌 설치 확인
|
|
18
|
+
const globalPath = path.dirname(process.argv[1]);
|
|
19
|
+
if (fs.existsSync(path.join(globalPath, 'hooks'))) {
|
|
20
|
+
return globalPath;
|
|
21
|
+
}
|
|
22
|
+
// 2. 로컬 node_modules 확인
|
|
23
|
+
let current = process.cwd();
|
|
24
|
+
while (current !== path.parse(current).root) {
|
|
25
|
+
const candidate = path.join(current, 'node_modules', 'claude-session-continuity-mcp', 'dist', 'hooks');
|
|
26
|
+
if (fs.existsSync(candidate)) {
|
|
27
|
+
return path.join(current, 'node_modules', 'claude-session-continuity-mcp', 'dist');
|
|
28
|
+
}
|
|
29
|
+
current = path.dirname(current);
|
|
30
|
+
}
|
|
31
|
+
// 3. 현재 패키지 디렉토리 (ESM 호환)
|
|
32
|
+
return path.dirname(__dirname);
|
|
33
|
+
}
|
|
34
|
+
function loadSettings() {
|
|
35
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(SETTINGS_FILE, 'utf-8'));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function saveSettings(settings) {
|
|
46
|
+
if (!fs.existsSync(CLAUDE_DIR)) {
|
|
47
|
+
fs.mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
50
|
+
}
|
|
51
|
+
function install() {
|
|
52
|
+
console.log('🔧 Installing Claude Code Hooks for session-continuity...');
|
|
53
|
+
const packagePath = getPackagePath();
|
|
54
|
+
const hooksDir = path.join(packagePath, 'hooks');
|
|
55
|
+
// Hook 스크립트 경로
|
|
56
|
+
const sessionStartHook = path.join(hooksDir, 'session-start.js');
|
|
57
|
+
const userPromptHook = path.join(hooksDir, 'user-prompt-submit.js');
|
|
58
|
+
const settings = loadSettings();
|
|
59
|
+
// 기존 hooks 유지하면서 추가
|
|
60
|
+
const hooks = settings.hooks || {};
|
|
61
|
+
// SessionStart Hook
|
|
62
|
+
hooks.SessionStart = [
|
|
63
|
+
{
|
|
64
|
+
hooks: [
|
|
65
|
+
{
|
|
66
|
+
type: 'command',
|
|
67
|
+
command: `node "${sessionStartHook}"`
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
];
|
|
72
|
+
// UserPromptSubmit Hook
|
|
73
|
+
hooks.UserPromptSubmit = [
|
|
74
|
+
{
|
|
75
|
+
hooks: [
|
|
76
|
+
{
|
|
77
|
+
type: 'command',
|
|
78
|
+
command: `node "${userPromptHook}"`
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
];
|
|
83
|
+
settings.hooks = hooks;
|
|
84
|
+
saveSettings(settings);
|
|
85
|
+
console.log('✅ Hooks installed successfully!');
|
|
86
|
+
console.log(` SessionStart: ${sessionStartHook}`);
|
|
87
|
+
console.log(` UserPromptSubmit: ${userPromptHook}`);
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log('🚀 Restart Claude Code to activate hooks.');
|
|
90
|
+
}
|
|
91
|
+
function uninstall() {
|
|
92
|
+
console.log('🔧 Removing Claude Code Hooks...');
|
|
93
|
+
const settings = loadSettings();
|
|
94
|
+
const hooks = settings.hooks || {};
|
|
95
|
+
// session-continuity 관련 Hook만 제거
|
|
96
|
+
delete hooks.SessionStart;
|
|
97
|
+
delete hooks.UserPromptSubmit;
|
|
98
|
+
if (Object.keys(hooks).length === 0) {
|
|
99
|
+
delete settings.hooks;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
settings.hooks = hooks;
|
|
103
|
+
}
|
|
104
|
+
saveSettings(settings);
|
|
105
|
+
console.log('✅ Hooks removed successfully!');
|
|
106
|
+
}
|
|
107
|
+
function status() {
|
|
108
|
+
console.log('📊 Claude Code Hooks Status\n');
|
|
109
|
+
if (!fs.existsSync(SETTINGS_FILE)) {
|
|
110
|
+
console.log('❌ No hooks configured');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const settings = loadSettings();
|
|
114
|
+
const hooks = settings.hooks;
|
|
115
|
+
if (!hooks) {
|
|
116
|
+
console.log('❌ No hooks configured');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log('Configured hooks:');
|
|
120
|
+
for (const [event, hookList] of Object.entries(hooks)) {
|
|
121
|
+
console.log(` ${event}:`);
|
|
122
|
+
for (const hook of hookList) {
|
|
123
|
+
for (const h of hook.hooks || []) {
|
|
124
|
+
console.log(` → ${h.command}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// CLI
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const command = args[0] || 'install';
|
|
132
|
+
switch (command) {
|
|
133
|
+
case 'install':
|
|
134
|
+
install();
|
|
135
|
+
break;
|
|
136
|
+
case 'uninstall':
|
|
137
|
+
case 'remove':
|
|
138
|
+
uninstall();
|
|
139
|
+
break;
|
|
140
|
+
case 'status':
|
|
141
|
+
status();
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
console.log('Usage: npx claude-session-continuity-hooks [install|uninstall|status]');
|
|
145
|
+
}
|
|
@@ -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 getProjectPath(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,7 +684,7 @@ async function handleTool(name, args) {
|
|
|
619
684
|
case 'session_start': {
|
|
620
685
|
const project = args.project;
|
|
621
686
|
const compact = args.compact !== false;
|
|
622
|
-
const projectPath =
|
|
687
|
+
const projectPath = getProjectPath(project);
|
|
623
688
|
if (!await fileExists(projectPath)) {
|
|
624
689
|
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
625
690
|
}
|
|
@@ -770,7 +835,7 @@ async function handleTool(name, args) {
|
|
|
770
835
|
// ===== 프로젝트 관리 =====
|
|
771
836
|
case 'project_status': {
|
|
772
837
|
const project = args.project;
|
|
773
|
-
const projectPath =
|
|
838
|
+
const projectPath = getProjectPath(project);
|
|
774
839
|
if (!await fileExists(projectPath)) {
|
|
775
840
|
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
776
841
|
}
|
|
@@ -815,7 +880,7 @@ async function handleTool(name, args) {
|
|
|
815
880
|
const project = args.project;
|
|
816
881
|
const techStack = args.techStack;
|
|
817
882
|
const description = args.description;
|
|
818
|
-
const projectPath =
|
|
883
|
+
const projectPath = getProjectPath(project);
|
|
819
884
|
// 기술 스택 자동 감지
|
|
820
885
|
const detectedStack = await detectTechStack(projectPath);
|
|
821
886
|
const finalStack = { ...detectedStack, ...techStack };
|
|
@@ -836,7 +901,7 @@ async function handleTool(name, args) {
|
|
|
836
901
|
}
|
|
837
902
|
case 'project_analyze': {
|
|
838
903
|
const project = args.project;
|
|
839
|
-
const projectPath =
|
|
904
|
+
const projectPath = getProjectPath(project);
|
|
840
905
|
if (!await fileExists(projectPath)) {
|
|
841
906
|
return { content: [{ type: 'text', text: `Project not found: ${project}` }] };
|
|
842
907
|
}
|
|
@@ -867,10 +932,18 @@ async function handleTool(name, args) {
|
|
|
867
932
|
}
|
|
868
933
|
case 'list_projects': {
|
|
869
934
|
try {
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
935
|
+
let projects = [];
|
|
936
|
+
// 단일 프로젝트 모드
|
|
937
|
+
if (!IS_MONOREPO && DEFAULT_PROJECT) {
|
|
938
|
+
projects = [DEFAULT_PROJECT];
|
|
939
|
+
}
|
|
940
|
+
else if (IS_MONOREPO) {
|
|
941
|
+
// 모노레포 모드: apps/ 하위 디렉토리
|
|
942
|
+
const entries = await fs.readdir(APPS_DIR, { withFileTypes: true });
|
|
943
|
+
projects = entries
|
|
944
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
945
|
+
.map(e => e.name);
|
|
946
|
+
}
|
|
874
947
|
// 각 프로젝트 상태 조회
|
|
875
948
|
const projectsWithStatus = await Promise.all(projects.map(async (p) => {
|
|
876
949
|
const active = db.prepare('SELECT current_state FROM active_context WHERE project = ?').get(p);
|
|
@@ -878,7 +951,8 @@ async function handleTool(name, args) {
|
|
|
878
951
|
return {
|
|
879
952
|
name: p,
|
|
880
953
|
status: active?.current_state || 'No context',
|
|
881
|
-
pendingTasks: taskCount?.count || 0
|
|
954
|
+
pendingTasks: taskCount?.count || 0,
|
|
955
|
+
mode: IS_MONOREPO ? 'monorepo' : 'single'
|
|
882
956
|
};
|
|
883
957
|
}));
|
|
884
958
|
return { content: [{ type: 'text', text: JSON.stringify(projectsWithStatus, null, 2) }] };
|
|
@@ -936,7 +1010,7 @@ async function handleTool(name, args) {
|
|
|
936
1010
|
case 'task_suggest': {
|
|
937
1011
|
const project = args.project;
|
|
938
1012
|
const searchPath = args.path;
|
|
939
|
-
const projectPath = path.join(
|
|
1013
|
+
const projectPath = path.join(getProjectPath(project), searchPath || '');
|
|
940
1014
|
// TODO, FIXME 등 검색
|
|
941
1015
|
try {
|
|
942
1016
|
const result = runCommand(`grep -rn "TODO\\|FIXME\\|HACK\\|XXX" --include="*.ts" --include="*.tsx" --include="*.dart" --include="*.kt" . | head -20`, projectPath);
|
|
@@ -1081,7 +1155,7 @@ async function handleTool(name, args) {
|
|
|
1081
1155
|
// ===== 검증/품질 =====
|
|
1082
1156
|
case 'verify_build': {
|
|
1083
1157
|
const project = args.project;
|
|
1084
|
-
const projectPath =
|
|
1158
|
+
const projectPath = getProjectPath(project);
|
|
1085
1159
|
const platform = await detectPlatform(projectPath);
|
|
1086
1160
|
let cmd;
|
|
1087
1161
|
switch (platform) {
|
|
@@ -1113,7 +1187,7 @@ async function handleTool(name, args) {
|
|
|
1113
1187
|
case 'verify_test': {
|
|
1114
1188
|
const project = args.project;
|
|
1115
1189
|
const testPath = args.testPath;
|
|
1116
|
-
const projectPath =
|
|
1190
|
+
const projectPath = getProjectPath(project);
|
|
1117
1191
|
const platform = await detectPlatform(projectPath);
|
|
1118
1192
|
let cmd;
|
|
1119
1193
|
switch (platform) {
|
|
@@ -1137,7 +1211,7 @@ async function handleTool(name, args) {
|
|
|
1137
1211
|
case 'verify_all': {
|
|
1138
1212
|
const project = args.project;
|
|
1139
1213
|
const stopOnFail = args.stopOnFail === true;
|
|
1140
|
-
const projectPath =
|
|
1214
|
+
const projectPath = getProjectPath(project);
|
|
1141
1215
|
const platform = await detectPlatform(projectPath);
|
|
1142
1216
|
const results = [];
|
|
1143
1217
|
// Build
|
|
@@ -1623,7 +1697,7 @@ const prompts = [
|
|
|
1623
1697
|
];
|
|
1624
1698
|
// ===== Prompt 내용 생성 함수 =====
|
|
1625
1699
|
async function generateProjectContext(project) {
|
|
1626
|
-
const projectPath =
|
|
1700
|
+
const projectPath = getProjectPath(project);
|
|
1627
1701
|
// 프로젝트 존재 확인
|
|
1628
1702
|
if (!await fileExists(projectPath)) {
|
|
1629
1703
|
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.2.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
|
],
|