claude-session-continuity-mcp 1.4.3 → 1.5.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 +17 -7
- package/dist/db/database.d.ts +19 -0
- package/dist/db/database.js +97 -0
- package/dist/hooks/install.js +46 -5
- package/dist/hooks/post-tool-use.d.ts +7 -0
- package/dist/hooks/post-tool-use.js +168 -0
- package/dist/hooks/pre-compact.d.ts +7 -0
- package/dist/hooks/pre-compact.js +122 -0
- package/dist/hooks/session-end.d.ts +7 -0
- package/dist/hooks/session-end.js +134 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# claude-session-continuity-mcp (v1.
|
|
1
|
+
# claude-session-continuity-mcp (v1.5.0)
|
|
2
2
|
|
|
3
3
|
> **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search
|
|
4
4
|
|
|
@@ -64,7 +64,7 @@ npm install claude-session-continuity-mcp
|
|
|
64
64
|
1. Registers MCP server in `~/.claude.json`
|
|
65
65
|
2. Installs Claude Hooks in `~/.claude/settings.local.json`
|
|
66
66
|
|
|
67
|
-
> **v1.
|
|
67
|
+
> **v1.5.0+:** Full lifecycle hooks! SessionStart, PostToolUse (file tracking), PreCompact (save before compression), and Stop (auto-save on exit). Works with both local and global installation.
|
|
68
68
|
|
|
69
69
|
### What Gets Installed
|
|
70
70
|
|
|
@@ -85,19 +85,25 @@ npm install claude-session-continuity-mcp
|
|
|
85
85
|
{
|
|
86
86
|
"hooks": {
|
|
87
87
|
"SessionStart": [{ "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-session-start" }] }],
|
|
88
|
-
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-user-prompt" }] }]
|
|
88
|
+
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-user-prompt" }] }],
|
|
89
|
+
"PostToolUse": [{ "matcher": { "tool_name": "Edit|Write" }, "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-post-tool" }] }],
|
|
90
|
+
"PreCompact": [{ "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-pre-compact" }] }],
|
|
91
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "npm exec -- claude-hook-session-end" }] }]
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
**Note (v1.
|
|
96
|
+
**Note (v1.5.0+):** Full lifecycle coverage with 5 hooks. Uses `npm exec --` which finds local `node_modules/.bin` first.
|
|
94
97
|
|
|
95
|
-
### Installed Hooks
|
|
98
|
+
### Installed Hooks (v1.5.0+)
|
|
96
99
|
|
|
97
100
|
| Hook | Command | Function |
|
|
98
101
|
|------|---------|----------|
|
|
99
|
-
| `SessionStart` | `
|
|
100
|
-
| `UserPromptSubmit` | `
|
|
102
|
+
| `SessionStart` | `claude-hook-session-start` | Auto-loads project context on session start |
|
|
103
|
+
| `UserPromptSubmit` | `claude-hook-user-prompt` | Auto-injects relevant memories per prompt |
|
|
104
|
+
| `PostToolUse` | `claude-hook-post-tool` | Tracks file changes (Edit, Write) automatically |
|
|
105
|
+
| `PreCompact` | `claude-hook-pre-compact` | Saves important context before compression |
|
|
106
|
+
| `Stop` | `claude-hook-session-end` | Auto-saves session on exit (no manual call needed) |
|
|
101
107
|
|
|
102
108
|
### Manual Hook Management
|
|
103
109
|
|
|
@@ -132,6 +138,10 @@ After installation, restart Claude Code to activate the hooks.
|
|
|
132
138
|
| ✅ **Integrated Verification** | One-click build/test/lint execution |
|
|
133
139
|
| 📋 **Task Management** | Priority-based task management |
|
|
134
140
|
| 🔧 **Solution Archive** | Auto-search error solutions |
|
|
141
|
+
| 📁 **File Change Tracking** | **(v1.5.0)** Auto-track Edit/Write tool usage |
|
|
142
|
+
| 💾 **Auto Backup** | **(v1.5.0)** Daily SQLite backup (max 5) |
|
|
143
|
+
| 🛡️ **PreCompact Save** | **(v1.5.0)** Save context before compression |
|
|
144
|
+
| 🚪 **Auto Session End** | **(v1.5.0)** No manual session_end needed |
|
|
135
145
|
|
|
136
146
|
---
|
|
137
147
|
|
package/dist/db/database.d.ts
CHANGED
|
@@ -6,3 +6,22 @@ export declare const db: DatabaseType;
|
|
|
6
6
|
export declare let contentFilterPatterns: ContentFilterPattern[];
|
|
7
7
|
export declare function initDatabase(): void;
|
|
8
8
|
export declare function loadContentFilterPatterns(): void;
|
|
9
|
+
/**
|
|
10
|
+
* DB 백업 생성
|
|
11
|
+
* - 최대 5개 유지
|
|
12
|
+
* - 하루에 한 번만 백업
|
|
13
|
+
*/
|
|
14
|
+
export declare function createBackup(): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* 백업에서 복원
|
|
17
|
+
*/
|
|
18
|
+
export declare function restoreFromBackup(backupPath: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 백업 목록 조회
|
|
21
|
+
*/
|
|
22
|
+
export declare function listBackups(): Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
path: string;
|
|
25
|
+
date: string;
|
|
26
|
+
size: number;
|
|
27
|
+
}>;
|
package/dist/db/database.js
CHANGED
|
@@ -263,6 +263,103 @@ export function loadContentFilterPatterns() {
|
|
|
263
263
|
contentFilterPatterns = [];
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
// ===== 백업 시스템 =====
|
|
267
|
+
const BACKUP_DIR = path.join(os.homedir(), '.claude', 'backups');
|
|
268
|
+
const MAX_BACKUPS = 5;
|
|
269
|
+
/**
|
|
270
|
+
* DB 백업 생성
|
|
271
|
+
* - 최대 5개 유지
|
|
272
|
+
* - 하루에 한 번만 백업
|
|
273
|
+
*/
|
|
274
|
+
export function createBackup() {
|
|
275
|
+
try {
|
|
276
|
+
if (!fs.existsSync(BACKUP_DIR)) {
|
|
277
|
+
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
280
|
+
const backupPath = path.join(BACKUP_DIR, `sessions-${today}.db`);
|
|
281
|
+
// 오늘 이미 백업했으면 스킵
|
|
282
|
+
if (fs.existsSync(backupPath)) {
|
|
283
|
+
return backupPath;
|
|
284
|
+
}
|
|
285
|
+
// SQLite 백업 (VACUUM INTO)
|
|
286
|
+
db.exec(`VACUUM INTO '${backupPath}'`);
|
|
287
|
+
// 오래된 백업 삭제
|
|
288
|
+
cleanupOldBackups();
|
|
289
|
+
return backupPath;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 오래된 백업 삭제 (MAX_BACKUPS 개수 유지)
|
|
297
|
+
*/
|
|
298
|
+
function cleanupOldBackups() {
|
|
299
|
+
try {
|
|
300
|
+
const files = fs.readdirSync(BACKUP_DIR)
|
|
301
|
+
.filter(f => f.startsWith('sessions-') && f.endsWith('.db'))
|
|
302
|
+
.map(f => ({
|
|
303
|
+
name: f,
|
|
304
|
+
path: path.join(BACKUP_DIR, f),
|
|
305
|
+
time: fs.statSync(path.join(BACKUP_DIR, f)).mtime.getTime()
|
|
306
|
+
}))
|
|
307
|
+
.sort((a, b) => b.time - a.time);
|
|
308
|
+
// MAX_BACKUPS 초과분 삭제
|
|
309
|
+
for (const file of files.slice(MAX_BACKUPS)) {
|
|
310
|
+
fs.unlinkSync(file.path);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// 무시
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 백업에서 복원
|
|
319
|
+
*/
|
|
320
|
+
export function restoreFromBackup(backupPath) {
|
|
321
|
+
try {
|
|
322
|
+
if (!fs.existsSync(backupPath)) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
// 현재 DB 닫기
|
|
326
|
+
db.close();
|
|
327
|
+
// 백업으로 교체
|
|
328
|
+
fs.copyFileSync(backupPath, DB_PATH);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* 백업 목록 조회
|
|
337
|
+
*/
|
|
338
|
+
export function listBackups() {
|
|
339
|
+
try {
|
|
340
|
+
if (!fs.existsSync(BACKUP_DIR)) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
return fs.readdirSync(BACKUP_DIR)
|
|
344
|
+
.filter(f => f.startsWith('sessions-') && f.endsWith('.db'))
|
|
345
|
+
.map(f => {
|
|
346
|
+
const filePath = path.join(BACKUP_DIR, f);
|
|
347
|
+
const stat = fs.statSync(filePath);
|
|
348
|
+
return {
|
|
349
|
+
name: f,
|
|
350
|
+
path: filePath,
|
|
351
|
+
date: f.replace('sessions-', '').replace('.db', ''),
|
|
352
|
+
size: stat.size
|
|
353
|
+
};
|
|
354
|
+
})
|
|
355
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
266
361
|
// 초기화
|
|
267
362
|
initDatabase();
|
|
268
363
|
loadContentFilterPatterns();
|
|
364
|
+
// 시작 시 백업 생성 (하루 한 번)
|
|
365
|
+
createBackup();
|
package/dist/hooks/install.js
CHANGED
|
@@ -110,8 +110,7 @@ function install() {
|
|
|
110
110
|
const settings = loadSettings();
|
|
111
111
|
// 기존 hooks 유지하면서 추가
|
|
112
112
|
const hooks = settings.hooks || {};
|
|
113
|
-
// SessionStart Hook -
|
|
114
|
-
// npm exec는 로컬 node_modules/.bin을 먼저 찾고, 없으면 글로벌에서 찾음
|
|
113
|
+
// SessionStart Hook - 세션 시작 시 컨텍스트 로드
|
|
115
114
|
hooks.SessionStart = [
|
|
116
115
|
{
|
|
117
116
|
hooks: [
|
|
@@ -122,7 +121,7 @@ function install() {
|
|
|
122
121
|
]
|
|
123
122
|
}
|
|
124
123
|
];
|
|
125
|
-
// UserPromptSubmit Hook -
|
|
124
|
+
// UserPromptSubmit Hook - 프롬프트 제출 시 관련 컨텍스트 주입
|
|
126
125
|
hooks.UserPromptSubmit = [
|
|
127
126
|
{
|
|
128
127
|
hooks: [
|
|
@@ -133,11 +132,50 @@ function install() {
|
|
|
133
132
|
]
|
|
134
133
|
}
|
|
135
134
|
];
|
|
135
|
+
// PostToolUse Hook - 파일 변경 시 자동 기록 (Edit, Write)
|
|
136
|
+
hooks.PostToolUse = [
|
|
137
|
+
{
|
|
138
|
+
matcher: {
|
|
139
|
+
tool_name: 'Edit|Write'
|
|
140
|
+
},
|
|
141
|
+
hooks: [
|
|
142
|
+
{
|
|
143
|
+
type: 'command',
|
|
144
|
+
command: 'npm exec -- claude-hook-post-tool'
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
// PreCompact Hook - 컨텍스트 압축 전 중요 정보 저장
|
|
150
|
+
hooks.PreCompact = [
|
|
151
|
+
{
|
|
152
|
+
hooks: [
|
|
153
|
+
{
|
|
154
|
+
type: 'command',
|
|
155
|
+
command: 'npm exec -- claude-hook-pre-compact'
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
];
|
|
160
|
+
// Stop Hook - 세션 종료 시 자동 저장
|
|
161
|
+
hooks.Stop = [
|
|
162
|
+
{
|
|
163
|
+
hooks: [
|
|
164
|
+
{
|
|
165
|
+
type: 'command',
|
|
166
|
+
command: 'npm exec -- claude-hook-session-end'
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
];
|
|
136
171
|
settings.hooks = hooks;
|
|
137
172
|
saveSettings(settings);
|
|
138
173
|
console.log('✅ Hooks installed (npm exec mode - works with local or global install!)');
|
|
139
|
-
console.log(' SessionStart:
|
|
140
|
-
console.log(' UserPromptSubmit:
|
|
174
|
+
console.log(' SessionStart: context auto-load');
|
|
175
|
+
console.log(' UserPromptSubmit: relevant memory injection');
|
|
176
|
+
console.log(' PostToolUse: file change tracking (Edit, Write)');
|
|
177
|
+
console.log(' PreCompact: save before context compression');
|
|
178
|
+
console.log(' Stop: auto-save session on exit');
|
|
141
179
|
console.log('');
|
|
142
180
|
// ===== 2. MCP 서버 등록 =====
|
|
143
181
|
console.log('📌 Step 2: Registering MCP Server...');
|
|
@@ -167,6 +205,9 @@ function uninstall() {
|
|
|
167
205
|
// session-continuity 관련 Hook만 제거
|
|
168
206
|
delete hooks.SessionStart;
|
|
169
207
|
delete hooks.UserPromptSubmit;
|
|
208
|
+
delete hooks.PostToolUse;
|
|
209
|
+
delete hooks.PreCompact;
|
|
210
|
+
delete hooks.Stop;
|
|
170
211
|
if (Object.keys(hooks).length === 0) {
|
|
171
212
|
delete settings.hooks;
|
|
172
213
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostToolUse Hook - 파일 변경 시 자동 기록
|
|
4
|
+
*
|
|
5
|
+
* Edit, Write 도구 사용 후 변경 사항을 메모리에 기록합니다.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
function getDbPath() {
|
|
12
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
13
|
+
if (!fs.existsSync(claudeDir)) {
|
|
14
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
return path.join(claudeDir, 'sessions.db');
|
|
17
|
+
}
|
|
18
|
+
function detectProject(cwd) {
|
|
19
|
+
const appsMatch = cwd.match(/apps[\/\\]([^\/\\]+)/);
|
|
20
|
+
if (appsMatch)
|
|
21
|
+
return appsMatch[1];
|
|
22
|
+
let current = cwd;
|
|
23
|
+
while (current !== path.parse(current).root) {
|
|
24
|
+
const pkgPath = path.join(current, 'package.json');
|
|
25
|
+
if (fs.existsSync(pkgPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
28
|
+
return pkg.name || path.basename(current);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return path.basename(current);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
current = path.dirname(current);
|
|
35
|
+
}
|
|
36
|
+
return path.basename(cwd);
|
|
37
|
+
}
|
|
38
|
+
function getFileExtension(filePath) {
|
|
39
|
+
return path.extname(filePath).slice(1).toLowerCase();
|
|
40
|
+
}
|
|
41
|
+
function categorizeChange(toolName, filePath, oldString, newString) {
|
|
42
|
+
const ext = getFileExtension(filePath);
|
|
43
|
+
const fileName = path.basename(filePath);
|
|
44
|
+
// 파일 타입별 분류
|
|
45
|
+
const isConfig = ['json', 'yaml', 'yml', 'toml', 'env'].includes(ext) || fileName.includes('config');
|
|
46
|
+
const isTest = filePath.includes('test') || filePath.includes('spec');
|
|
47
|
+
const isStyle = ['css', 'scss', 'less', 'styled'].some(s => filePath.includes(s));
|
|
48
|
+
const isComponent = ['tsx', 'jsx', 'vue', 'svelte'].includes(ext);
|
|
49
|
+
let changeType = 'code';
|
|
50
|
+
if (isConfig)
|
|
51
|
+
changeType = 'config';
|
|
52
|
+
else if (isTest)
|
|
53
|
+
changeType = 'test';
|
|
54
|
+
else if (isStyle)
|
|
55
|
+
changeType = 'style';
|
|
56
|
+
else if (isComponent)
|
|
57
|
+
changeType = 'component';
|
|
58
|
+
// 변경 요약 생성
|
|
59
|
+
let summary = '';
|
|
60
|
+
if (toolName === 'Write') {
|
|
61
|
+
summary = `Created ${fileName}`;
|
|
62
|
+
}
|
|
63
|
+
else if (toolName === 'Edit') {
|
|
64
|
+
if (oldString && newString) {
|
|
65
|
+
const added = newString.split('\n').length;
|
|
66
|
+
const removed = oldString.split('\n').length;
|
|
67
|
+
if (added > removed) {
|
|
68
|
+
summary = `Added ${added - removed} lines to ${fileName}`;
|
|
69
|
+
}
|
|
70
|
+
else if (removed > added) {
|
|
71
|
+
summary = `Removed ${removed - added} lines from ${fileName}`;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
summary = `Modified ${fileName}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
summary = `Modified ${fileName}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return { changeType, summary };
|
|
82
|
+
}
|
|
83
|
+
async function main() {
|
|
84
|
+
try {
|
|
85
|
+
let inputData = '';
|
|
86
|
+
for await (const chunk of process.stdin) {
|
|
87
|
+
inputData += chunk;
|
|
88
|
+
}
|
|
89
|
+
const input = inputData ? JSON.parse(inputData) : {};
|
|
90
|
+
// Edit, Write 도구만 처리
|
|
91
|
+
const toolName = input.tool_name;
|
|
92
|
+
if (!toolName || !['Edit', 'Write'].includes(toolName)) {
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
const filePath = input.tool_input?.file_path;
|
|
96
|
+
if (!filePath) {
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
99
|
+
const cwd = input.cwd || process.cwd();
|
|
100
|
+
const project = detectProject(cwd);
|
|
101
|
+
const dbPath = getDbPath();
|
|
102
|
+
if (!fs.existsSync(dbPath)) {
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
const db = new Database(dbPath);
|
|
106
|
+
const { changeType, summary } = categorizeChange(toolName, filePath, input.tool_input?.old_string, input.tool_input?.new_string);
|
|
107
|
+
// 최근 파일 목록 업데이트
|
|
108
|
+
const activeStmt = db.prepare(`
|
|
109
|
+
INSERT OR REPLACE INTO active_context (project, recent_files, updated_at)
|
|
110
|
+
VALUES (
|
|
111
|
+
?,
|
|
112
|
+
COALESCE(
|
|
113
|
+
(SELECT json_insert(
|
|
114
|
+
COALESCE(recent_files, '[]'),
|
|
115
|
+
'$[#]',
|
|
116
|
+
?
|
|
117
|
+
) FROM active_context WHERE project = ?),
|
|
118
|
+
json_array(?)
|
|
119
|
+
),
|
|
120
|
+
datetime('now')
|
|
121
|
+
)
|
|
122
|
+
`);
|
|
123
|
+
try {
|
|
124
|
+
// 간단한 방식으로 recent_files 업데이트
|
|
125
|
+
const existing = db.prepare('SELECT recent_files FROM active_context WHERE project = ?').get(project);
|
|
126
|
+
let recentFiles = [];
|
|
127
|
+
if (existing?.recent_files) {
|
|
128
|
+
try {
|
|
129
|
+
recentFiles = JSON.parse(existing.recent_files);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
recentFiles = [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 중복 제거하고 최신 파일 추가 (최대 10개)
|
|
136
|
+
recentFiles = recentFiles.filter(f => f !== filePath);
|
|
137
|
+
recentFiles.unshift(filePath);
|
|
138
|
+
recentFiles = recentFiles.slice(0, 10);
|
|
139
|
+
db.prepare(`
|
|
140
|
+
INSERT OR REPLACE INTO active_context (project, recent_files, updated_at)
|
|
141
|
+
VALUES (?, ?, datetime('now'))
|
|
142
|
+
`).run(project, JSON.stringify(recentFiles));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// 오류 시 무시
|
|
146
|
+
}
|
|
147
|
+
// 중요 변경사항은 메모리에 기록 (하루에 같은 파일 중복 방지)
|
|
148
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
149
|
+
const existingMemory = db.prepare(`
|
|
150
|
+
SELECT id FROM memories
|
|
151
|
+
WHERE project = ?
|
|
152
|
+
AND content LIKE ?
|
|
153
|
+
AND date(created_at) = ?
|
|
154
|
+
`).get(project, `%${path.basename(filePath)}%`, today);
|
|
155
|
+
if (!existingMemory) {
|
|
156
|
+
db.prepare(`
|
|
157
|
+
INSERT INTO memories (content, memory_type, project, importance, tags)
|
|
158
|
+
VALUES (?, 'observation', ?, 3, ?)
|
|
159
|
+
`).run(`[File Change] ${summary}`, project, `auto-tracked,${changeType},${getFileExtension(filePath)}`);
|
|
160
|
+
}
|
|
161
|
+
db.close();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
main();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PreCompact Hook - 컨텍스트 압축 전 중요 메모리 저장
|
|
4
|
+
*
|
|
5
|
+
* 컨텍스트가 압축되기 전에 현재 세션의 중요 정보를 메모리에 저장합니다.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
function getDbPath() {
|
|
12
|
+
// 글로벌 DB 경로
|
|
13
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
14
|
+
if (!fs.existsSync(claudeDir)) {
|
|
15
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
return path.join(claudeDir, 'sessions.db');
|
|
18
|
+
}
|
|
19
|
+
function detectProject(cwd) {
|
|
20
|
+
// apps/ 하위 프로젝트 감지
|
|
21
|
+
const appsMatch = cwd.match(/apps[\/\\]([^\/\\]+)/);
|
|
22
|
+
if (appsMatch)
|
|
23
|
+
return appsMatch[1];
|
|
24
|
+
// package.json 기반
|
|
25
|
+
let current = cwd;
|
|
26
|
+
while (current !== path.parse(current).root) {
|
|
27
|
+
const pkgPath = path.join(current, 'package.json');
|
|
28
|
+
if (fs.existsSync(pkgPath)) {
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
31
|
+
return pkg.name || path.basename(current);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return path.basename(current);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
current = path.dirname(current);
|
|
38
|
+
}
|
|
39
|
+
return path.basename(cwd);
|
|
40
|
+
}
|
|
41
|
+
function extractKeyPoints(transcript) {
|
|
42
|
+
const keyPoints = [];
|
|
43
|
+
// 최근 메시지에서 중요 패턴 추출
|
|
44
|
+
const recentMessages = transcript.slice(-20);
|
|
45
|
+
for (const msg of recentMessages) {
|
|
46
|
+
if (msg.role !== 'assistant')
|
|
47
|
+
continue;
|
|
48
|
+
const content = msg.content;
|
|
49
|
+
// 결정 사항 패턴
|
|
50
|
+
const decisionPatterns = [
|
|
51
|
+
/(?:decided|결정|선택)[^.]*\./gi,
|
|
52
|
+
/(?:will use|사용할)[^.]*\./gi,
|
|
53
|
+
/(?:approach|방식)[^.]*\./gi,
|
|
54
|
+
];
|
|
55
|
+
for (const pattern of decisionPatterns) {
|
|
56
|
+
const matches = content.match(pattern);
|
|
57
|
+
if (matches) {
|
|
58
|
+
keyPoints.push(...matches.slice(0, 2));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// 에러 해결 패턴
|
|
62
|
+
const errorPatterns = [
|
|
63
|
+
/(?:fixed|수정|해결)[^.]*(?:error|bug|issue|오류|버그)[^.]*\./gi,
|
|
64
|
+
/(?:error|bug|issue|오류|버그)[^.]*(?:fixed|수정|해결)[^.]*\./gi,
|
|
65
|
+
];
|
|
66
|
+
for (const pattern of errorPatterns) {
|
|
67
|
+
const matches = content.match(pattern);
|
|
68
|
+
if (matches) {
|
|
69
|
+
keyPoints.push(...matches.slice(0, 2));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 중복 제거 및 길이 제한
|
|
74
|
+
const unique = [...new Set(keyPoints)].slice(0, 5);
|
|
75
|
+
return unique.map(p => p.slice(0, 200));
|
|
76
|
+
}
|
|
77
|
+
async function main() {
|
|
78
|
+
try {
|
|
79
|
+
let inputData = '';
|
|
80
|
+
for await (const chunk of process.stdin) {
|
|
81
|
+
inputData += chunk;
|
|
82
|
+
}
|
|
83
|
+
const input = inputData ? JSON.parse(inputData) : {};
|
|
84
|
+
const cwd = input.cwd || process.cwd();
|
|
85
|
+
const project = detectProject(cwd);
|
|
86
|
+
const dbPath = getDbPath();
|
|
87
|
+
if (!fs.existsSync(dbPath)) {
|
|
88
|
+
console.log('[PreCompact] No DB found, skipping');
|
|
89
|
+
process.exit(0);
|
|
90
|
+
}
|
|
91
|
+
const db = new Database(dbPath);
|
|
92
|
+
// 현재 활성 컨텍스트 저장
|
|
93
|
+
const activeStmt = db.prepare(`
|
|
94
|
+
INSERT OR REPLACE INTO active_context (project, current_state, updated_at)
|
|
95
|
+
VALUES (?, ?, datetime('now'))
|
|
96
|
+
`);
|
|
97
|
+
// transcript에서 핵심 포인트 추출
|
|
98
|
+
const keyPoints = input.transcript ? extractKeyPoints(input.transcript) : [];
|
|
99
|
+
if (keyPoints.length > 0) {
|
|
100
|
+
// 중요 메모리로 저장
|
|
101
|
+
const memoryStmt = db.prepare(`
|
|
102
|
+
INSERT INTO memories (content, memory_type, project, importance, tags)
|
|
103
|
+
VALUES (?, 'pattern', ?, 8, 'auto-compact,session-summary')
|
|
104
|
+
`);
|
|
105
|
+
const summary = `[Pre-Compact Summary] ${keyPoints.join(' | ')}`;
|
|
106
|
+
memoryStmt.run(summary, project);
|
|
107
|
+
// 활성 컨텍스트 업데이트
|
|
108
|
+
activeStmt.run(project, `Compacted: ${keyPoints[0]?.slice(0, 50) || 'Session context saved'}`);
|
|
109
|
+
console.log(`[PreCompact] Saved ${keyPoints.length} key points for ${project}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(`[PreCompact] No key points extracted for ${project}`);
|
|
113
|
+
}
|
|
114
|
+
db.close();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
// 에러 시 조용히 종료
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
main();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SessionEnd Hook (Stop 이벤트) - 세션 종료 시 자동 저장
|
|
4
|
+
*
|
|
5
|
+
* Claude Code 세션 종료 시 자동으로 컨텍스트를 저장합니다.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import Database from 'better-sqlite3';
|
|
11
|
+
function getDbPath() {
|
|
12
|
+
const claudeDir = path.join(os.homedir(), '.claude');
|
|
13
|
+
if (!fs.existsSync(claudeDir)) {
|
|
14
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
return path.join(claudeDir, 'sessions.db');
|
|
17
|
+
}
|
|
18
|
+
function detectProject(cwd) {
|
|
19
|
+
const appsMatch = cwd.match(/apps[\/\\]([^\/\\]+)/);
|
|
20
|
+
if (appsMatch)
|
|
21
|
+
return appsMatch[1];
|
|
22
|
+
let current = cwd;
|
|
23
|
+
while (current !== path.parse(current).root) {
|
|
24
|
+
const pkgPath = path.join(current, 'package.json');
|
|
25
|
+
if (fs.existsSync(pkgPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
28
|
+
return pkg.name || path.basename(current);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return path.basename(current);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
current = path.dirname(current);
|
|
35
|
+
}
|
|
36
|
+
return path.basename(cwd);
|
|
37
|
+
}
|
|
38
|
+
function extractSessionSummary(transcript) {
|
|
39
|
+
const lastWork = [];
|
|
40
|
+
const nextTasks = [];
|
|
41
|
+
const modifiedFiles = new Set();
|
|
42
|
+
// 최근 메시지 분석
|
|
43
|
+
const recentMessages = transcript.slice(-30);
|
|
44
|
+
for (const msg of recentMessages) {
|
|
45
|
+
const content = msg.content;
|
|
46
|
+
// 파일 수정 추출 (Edit, Write 도구 결과에서)
|
|
47
|
+
const filePatterns = [
|
|
48
|
+
/(?:edited|modified|updated|created|wrote)\s+[`"]?([^\s`"]+\.[a-z]+)/gi,
|
|
49
|
+
/file[:\s]+[`"]?([^\s`"]+\.[a-z]+)/gi,
|
|
50
|
+
];
|
|
51
|
+
for (const pattern of filePatterns) {
|
|
52
|
+
let match;
|
|
53
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
54
|
+
if (match[1] && !match[1].includes('...')) {
|
|
55
|
+
modifiedFiles.add(match[1]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (msg.role === 'assistant') {
|
|
60
|
+
// 완료된 작업 추출
|
|
61
|
+
const donePatterns = [
|
|
62
|
+
/(?:completed|finished|done|완료|수정)[:\s]*([^.!?\n]+)/gi,
|
|
63
|
+
/(?:implemented|added|fixed|created)[:\s]*([^.!?\n]+)/gi,
|
|
64
|
+
];
|
|
65
|
+
for (const pattern of donePatterns) {
|
|
66
|
+
const match = pattern.exec(content);
|
|
67
|
+
if (match && match[1]) {
|
|
68
|
+
const work = match[1].trim().slice(0, 100);
|
|
69
|
+
if (work.length > 10)
|
|
70
|
+
lastWork.push(work);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 다음 할 일 추출
|
|
74
|
+
const nextPatterns = [
|
|
75
|
+
/(?:next|todo|remaining|다음)[:\s]*([^.!?\n]+)/gi,
|
|
76
|
+
/(?:should|need to|필요)[:\s]*([^.!?\n]+)/gi,
|
|
77
|
+
];
|
|
78
|
+
for (const pattern of nextPatterns) {
|
|
79
|
+
const match = pattern.exec(content);
|
|
80
|
+
if (match && match[1]) {
|
|
81
|
+
const task = match[1].trim().slice(0, 100);
|
|
82
|
+
if (task.length > 10)
|
|
83
|
+
nextTasks.push(task);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
lastWork: lastWork.slice(0, 3).join('; ') || 'Session work completed',
|
|
90
|
+
nextTasks: [...new Set(nextTasks)].slice(0, 5),
|
|
91
|
+
modifiedFiles: [...modifiedFiles].slice(0, 10)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function main() {
|
|
95
|
+
try {
|
|
96
|
+
let inputData = '';
|
|
97
|
+
for await (const chunk of process.stdin) {
|
|
98
|
+
inputData += chunk;
|
|
99
|
+
}
|
|
100
|
+
const input = inputData ? JSON.parse(inputData) : {};
|
|
101
|
+
const cwd = input.cwd || process.cwd();
|
|
102
|
+
const project = detectProject(cwd);
|
|
103
|
+
const dbPath = getDbPath();
|
|
104
|
+
if (!fs.existsSync(dbPath)) {
|
|
105
|
+
console.log('[SessionEnd] No DB found, skipping');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
const db = new Database(dbPath);
|
|
109
|
+
// transcript에서 세션 요약 추출
|
|
110
|
+
const summary = input.transcript
|
|
111
|
+
? extractSessionSummary(input.transcript)
|
|
112
|
+
: { lastWork: 'Session ended', nextTasks: [], modifiedFiles: [] };
|
|
113
|
+
// 세션 기록 저장
|
|
114
|
+
db.prepare(`
|
|
115
|
+
INSERT INTO sessions (project, last_work, next_tasks, modified_files)
|
|
116
|
+
VALUES (?, ?, ?, ?)
|
|
117
|
+
`).run(project, summary.lastWork, JSON.stringify(summary.nextTasks), JSON.stringify(summary.modifiedFiles));
|
|
118
|
+
// 활성 컨텍스트 업데이트
|
|
119
|
+
db.prepare(`
|
|
120
|
+
INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
|
|
121
|
+
VALUES (?, ?, ?, datetime('now'))
|
|
122
|
+
`).run(project, summary.lastWork, JSON.stringify(summary.modifiedFiles));
|
|
123
|
+
db.close();
|
|
124
|
+
console.log(`[SessionEnd] Saved session for ${project}`);
|
|
125
|
+
console.log(` Last work: ${summary.lastWork.slice(0, 50)}...`);
|
|
126
|
+
console.log(` Modified files: ${summary.modifiedFiles.length}`);
|
|
127
|
+
console.log(` Next tasks: ${summary.nextTasks.length}`);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
process.exit(0);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-session-continuity-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
"claude-session-continuity-mcp": "dist/index.js",
|
|
9
9
|
"claude-session-hooks": "dist/hooks/install.js",
|
|
10
10
|
"claude-hook-session-start": "dist/hooks/session-start.js",
|
|
11
|
-
"claude-hook-user-prompt": "dist/hooks/user-prompt-submit.js"
|
|
11
|
+
"claude-hook-user-prompt": "dist/hooks/user-prompt-submit.js",
|
|
12
|
+
"claude-hook-post-tool": "dist/hooks/post-tool-use.js",
|
|
13
|
+
"claude-hook-pre-compact": "dist/hooks/pre-compact.js",
|
|
14
|
+
"claude-hook-session-end": "dist/hooks/session-end.js"
|
|
12
15
|
},
|
|
13
16
|
"scripts": {
|
|
14
17
|
"build": "tsc",
|