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 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,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code Hooks 자동 설치 스크립트
4
+ *
5
+ * npm install 시 자동으로 ~/.claude/settings.local.json에 Hook 등록
6
+ */
7
+ export {};
@@ -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,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart Hook - 세션 시작 시 컨텍스트 자동 주입
4
+ */
5
+ export {};
@@ -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,5 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * UserPromptSubmit Hook - 매 프롬프트마다 관련 컨텍스트 자동 주입
4
+ */
5
+ export {};
@@ -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
- const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || '/Users/ibyeongchang/Documents/dev/ai-service-generator';
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 DB_PATH = path.join(WORKSPACE_ROOT, '.claude', 'sessions.db');
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
- // v4 스키마 - 메모리 분류 체계 + 지식 그래프 강화
72
+ // v5 스키마 - 세션 + 메모리 분류 체계 + 지식 그래프
36
73
  db.exec(`
37
- -- 기존 sessions 테이블 사용 (스키마 변경 없음)
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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
- const entries = await fs.readdir(APPS_DIR, { withFileTypes: true });
871
- const projects = entries
872
- .filter(e => e.isDirectory() && !e.name.startsWith('.'))
873
- .map(e => e.name);
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(APPS_DIR, project, searchPath || '');
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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 = path.join(APPS_DIR, project);
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.1.0",
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
  ],