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 getDbPath() {
12
- const claudeDir = path.join(os.homedir(), '.claude');
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 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);
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
- return path.basename(cwd);
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
- // apps/ 하위 프로젝트 감지
32
- const appsMatch = cwd.match(/apps[\/\\]([^\/\\]+)/);
33
- if (appsMatch)
34
- return appsMatch[1];
35
- // package.json 기반
36
- let current = cwd;
37
- while (current !== path.parse(current).root) {
38
- const pkgPath = path.join(current, 'package.json');
39
- if (fs.existsSync(pkgPath)) {
40
- try {
41
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
42
- return pkg.name || path.basename(current);
43
- }
44
- catch {
45
- return path.basename(current);
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
- return path.basename(cwd);
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: lastWork.slice(0, 3).join('; ') || 'Session work completed',
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.8.1",
3
+ "version": "1.9.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",