claude-session-continuity-mcp 1.8.1 → 1.9.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.
|
@@ -6,34 +6,54 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as fs from 'fs';
|
|
8
8
|
import * as path from 'path';
|
|
9
|
-
import * as os from 'os';
|
|
10
9
|
import Database from 'better-sqlite3';
|
|
11
|
-
function
|
|
12
|
-
|
|
10
|
+
function detectWorkspaceRoot(cwd) {
|
|
11
|
+
let current = cwd;
|
|
12
|
+
const root = path.parse(current).root;
|
|
13
|
+
while (current !== root) {
|
|
14
|
+
if (fs.existsSync(path.join(current, 'apps')))
|
|
15
|
+
return current;
|
|
16
|
+
if (fs.existsSync(path.join(current, '.claude', 'sessions.db')))
|
|
17
|
+
return current;
|
|
18
|
+
current = path.dirname(current);
|
|
19
|
+
}
|
|
20
|
+
return cwd;
|
|
21
|
+
}
|
|
22
|
+
function getDbPath(cwd) {
|
|
23
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
24
|
+
const claudeDir = path.join(workspaceRoot, '.claude');
|
|
13
25
|
if (!fs.existsSync(claudeDir)) {
|
|
14
26
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
15
27
|
}
|
|
16
28
|
return path.join(claudeDir, 'sessions.db');
|
|
17
29
|
}
|
|
18
30
|
function detectProject(cwd) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
32
|
+
const appsDir = path.join(workspaceRoot, 'apps');
|
|
33
|
+
// apps/ 하위인지 확인
|
|
34
|
+
if (cwd.startsWith(appsDir + path.sep)) {
|
|
35
|
+
const relative = path.relative(appsDir, cwd);
|
|
36
|
+
return relative.split(path.sep)[0];
|
|
37
|
+
}
|
|
38
|
+
// apps/ 외부 하위 프로젝트 (hackathons/ 등)
|
|
39
|
+
if (cwd !== workspaceRoot) {
|
|
40
|
+
let current = cwd;
|
|
41
|
+
while (current !== workspaceRoot && current !== path.parse(current).root) {
|
|
42
|
+
const pkgPath = path.join(current, 'package.json');
|
|
43
|
+
if (fs.existsSync(pkgPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
46
|
+
return pkg.name || path.basename(current);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return path.basename(current);
|
|
50
|
+
}
|
|
32
51
|
}
|
|
52
|
+
current = path.dirname(current);
|
|
33
53
|
}
|
|
34
|
-
current = path.dirname(current);
|
|
35
54
|
}
|
|
36
|
-
|
|
55
|
+
// 워크스페이스 루트 → 폴더명 반환
|
|
56
|
+
return path.basename(workspaceRoot);
|
|
37
57
|
}
|
|
38
58
|
function getFileExtension(filePath) {
|
|
39
59
|
return path.extname(filePath).slice(1).toLowerCase();
|
|
@@ -111,7 +131,7 @@ async function main() {
|
|
|
111
131
|
}
|
|
112
132
|
const cwd = input.cwd || process.cwd();
|
|
113
133
|
const project = detectProject(cwd);
|
|
114
|
-
const dbPath = getDbPath();
|
|
134
|
+
const dbPath = getDbPath(cwd);
|
|
115
135
|
if (!fs.existsSync(dbPath)) {
|
|
116
136
|
process.exit(0);
|
|
117
137
|
}
|
|
@@ -28,26 +28,32 @@ function getDbPath(cwd) {
|
|
|
28
28
|
return path.join(claudeDir, 'sessions.db');
|
|
29
29
|
}
|
|
30
30
|
function detectProject(cwd) {
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
const workspaceRoot = detectWorkspaceRoot(cwd);
|
|
32
|
+
const appsDir = path.join(workspaceRoot, 'apps');
|
|
33
|
+
// apps/ 하위인지 확인
|
|
34
|
+
if (cwd.startsWith(appsDir + path.sep)) {
|
|
35
|
+
const relative = path.relative(appsDir, cwd);
|
|
36
|
+
return relative.split(path.sep)[0];
|
|
37
|
+
}
|
|
38
|
+
// apps/ 외부 하위 프로젝트 (hackathons/ 등)
|
|
39
|
+
if (cwd !== workspaceRoot) {
|
|
40
|
+
let current = cwd;
|
|
41
|
+
while (current !== workspaceRoot && current !== path.parse(current).root) {
|
|
42
|
+
const pkgPath = path.join(current, 'package.json');
|
|
43
|
+
if (fs.existsSync(pkgPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
46
|
+
return pkg.name || path.basename(current);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return path.basename(current);
|
|
50
|
+
}
|
|
46
51
|
}
|
|
52
|
+
current = path.dirname(current);
|
|
47
53
|
}
|
|
48
|
-
current = path.dirname(current);
|
|
49
54
|
}
|
|
50
|
-
|
|
55
|
+
// 워크스페이스 루트 → 폴더명 반환
|
|
56
|
+
return path.basename(workspaceRoot);
|
|
51
57
|
}
|
|
52
58
|
function extractKeyPoints(transcript) {
|
|
53
59
|
const keyPoints = [];
|
|
@@ -55,19 +55,62 @@ function detectProject(cwd) {
|
|
|
55
55
|
// 워크스페이스 루트 (모노레포 포함) → 폴더명 반환
|
|
56
56
|
return path.basename(workspaceRoot);
|
|
57
57
|
}
|
|
58
|
+
function extractLastWork(transcript) {
|
|
59
|
+
// 마지막 assistant 메시지들에서 의미있는 요약 추출 (역순)
|
|
60
|
+
const assistantMessages = transcript.filter(m => m.role === 'assistant');
|
|
61
|
+
for (let i = assistantMessages.length - 1; i >= Math.max(0, assistantMessages.length - 5); i--) {
|
|
62
|
+
const content = assistantMessages[i].content;
|
|
63
|
+
if (!content || content.length < 10)
|
|
64
|
+
continue;
|
|
65
|
+
// 전략 1: "✅ 작업 완료" 또는 "## Summary" 등 요약 섹션 추출
|
|
66
|
+
const summaryPatterns = [
|
|
67
|
+
/✅\s*(.+?)(?:\n\n|\n(?=[#*-]))/s,
|
|
68
|
+
/(?:##?\s*(?:Summary|변경\s*사항|작업\s*완료|결과))\s*\n([\s\S]+?)(?:\n\n|\n(?=##))/i,
|
|
69
|
+
];
|
|
70
|
+
for (const pattern of summaryPatterns) {
|
|
71
|
+
const match = content.match(pattern);
|
|
72
|
+
if (match?.[1]) {
|
|
73
|
+
const summary = match[1].replace(/\n/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
74
|
+
if (summary.length > 10)
|
|
75
|
+
return summary;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 전략 2: 볼드 텍스트(**...**) 추출 — 핵심 내용이 담겨 있는 경우 많음
|
|
79
|
+
const boldMatches = content.match(/\*\*(.+?)\*\*/g);
|
|
80
|
+
if (boldMatches && boldMatches.length > 0) {
|
|
81
|
+
const cleaned = boldMatches
|
|
82
|
+
.map(b => b.replace(/\*\*/g, ''))
|
|
83
|
+
.filter(b => b.length > 5 && b.length < 100)
|
|
84
|
+
.slice(0, 3);
|
|
85
|
+
if (cleaned.length > 0)
|
|
86
|
+
return cleaned.join('; ').slice(0, 200);
|
|
87
|
+
}
|
|
88
|
+
// 전략 3: 첫 줄이 의미있는 텍스트인 경우
|
|
89
|
+
const firstLine = content.split('\n').find(line => {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
return trimmed.length > 10 && !trimmed.startsWith('#') && !trimmed.startsWith('```');
|
|
92
|
+
});
|
|
93
|
+
if (firstLine) {
|
|
94
|
+
const trimmed = firstLine.trim().slice(0, 200);
|
|
95
|
+
if (trimmed.length > 10)
|
|
96
|
+
return trimmed;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return 'Session work completed';
|
|
100
|
+
}
|
|
58
101
|
function extractSessionSummary(transcript) {
|
|
59
|
-
const lastWork = [];
|
|
60
102
|
const nextTasks = [];
|
|
61
103
|
const modifiedFiles = new Set();
|
|
104
|
+
// last_work: 개선된 다중 전략 추출
|
|
105
|
+
const lastWork = extractLastWork(transcript);
|
|
62
106
|
// 최근 메시지 분석
|
|
63
107
|
const recentMessages = transcript.slice(-30);
|
|
64
108
|
for (const msg of recentMessages) {
|
|
65
109
|
const content = msg.content;
|
|
66
|
-
// 파일 수정 추출 (Edit, Write 도구 결과에서)
|
|
110
|
+
// 파일 수정 추출 (Edit, Write 도구 결과에서) — 폴백용
|
|
67
111
|
const filePatterns = [
|
|
68
112
|
/(?:edited|modified|updated|created|wrote|수정|생성|변경)\s+[`"]?([^\s`"]+\.[a-z]+)/gi,
|
|
69
113
|
/file[:\s]+[`"]?([^\s`"]+\.[a-z]+)/gi,
|
|
70
|
-
/[`"]([^\s`"]+\.[a-z]{1,4})[`"]/g,
|
|
71
114
|
];
|
|
72
115
|
for (const pattern of filePatterns) {
|
|
73
116
|
let match;
|
|
@@ -78,22 +121,6 @@ function extractSessionSummary(transcript) {
|
|
|
78
121
|
}
|
|
79
122
|
}
|
|
80
123
|
if (msg.role === 'assistant') {
|
|
81
|
-
// 완료된 작업 추출
|
|
82
|
-
const donePatterns = [
|
|
83
|
-
/(?:completed|finished|done|완료|수정|구현|추가|삭제|배포|적용|해결|변경|개선|리팩토링)[:\s]*([^.!?\n]+)/gi,
|
|
84
|
-
/(?:implemented|added|fixed|created|updated|deployed|resolved)[:\s]*([^.!?\n]+)/gi,
|
|
85
|
-
/✅\s*([^.!?\n]+)/g,
|
|
86
|
-
];
|
|
87
|
-
for (const pattern of donePatterns) {
|
|
88
|
-
let match;
|
|
89
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
90
|
-
if (match[1]) {
|
|
91
|
-
const work = match[1].trim().slice(0, 100);
|
|
92
|
-
if (work.length > 5)
|
|
93
|
-
lastWork.push(work);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
124
|
// 다음 할 일 추출
|
|
98
125
|
const nextPatterns = [
|
|
99
126
|
/(?:next|todo|remaining|다음|남은|해야|예정)[:\s]*([^.!?\n]+)/gi,
|
|
@@ -112,7 +139,7 @@ function extractSessionSummary(transcript) {
|
|
|
112
139
|
}
|
|
113
140
|
}
|
|
114
141
|
return {
|
|
115
|
-
lastWork
|
|
142
|
+
lastWork,
|
|
116
143
|
nextTasks: [...new Set(nextTasks)].slice(0, 5),
|
|
117
144
|
modifiedFiles: [...modifiedFiles].slice(0, 10)
|
|
118
145
|
};
|
|
@@ -145,6 +172,16 @@ async function main() {
|
|
|
145
172
|
const summary = input.transcript
|
|
146
173
|
? extractSessionSummary(input.transcript)
|
|
147
174
|
: { lastWork: 'Session ended', nextTasks: [], modifiedFiles: [] };
|
|
175
|
+
// active_context에서 PostToolUse가 실시간 저장한 파일 목록 병합
|
|
176
|
+
try {
|
|
177
|
+
const activeCtx = db.prepare('SELECT recent_files FROM active_context WHERE project = ?').get(project);
|
|
178
|
+
if (activeCtx?.recent_files) {
|
|
179
|
+
const realtimeFiles = JSON.parse(activeCtx.recent_files);
|
|
180
|
+
const mergedFiles = [...new Set([...realtimeFiles, ...summary.modifiedFiles])].slice(0, 15);
|
|
181
|
+
summary.modifiedFiles = mergedFiles;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* active_context may not exist */ }
|
|
148
185
|
// 빈 세션 저장 방지: 의미 있는 작업이 없으면 skip
|
|
149
186
|
const emptyWorkPatterns = ['Session ended', 'Session work completed', 'Session started', ''];
|
|
150
187
|
if (emptyWorkPatterns.includes(summary.lastWork) && summary.modifiedFiles.length === 0) {
|
|
@@ -13,9 +13,6 @@ function detectWorkspaceRoot(cwd) {
|
|
|
13
13
|
return current;
|
|
14
14
|
if (fs.existsSync(path.join(current, '.claude', 'sessions.db')))
|
|
15
15
|
return current;
|
|
16
|
-
if (fs.existsSync(path.join(current, 'package.json'))) {
|
|
17
|
-
return current;
|
|
18
|
-
}
|
|
19
16
|
current = path.dirname(current);
|
|
20
17
|
}
|
|
21
18
|
return cwd;
|