claude-session-continuity-mcp 1.9.6 → 1.10.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
@@ -1,4 +1,4 @@
1
- # claude-session-continuity-mcp (v1.9.0)
1
+ # claude-session-continuity-mcp (v1.10.0)
2
2
 
3
3
  > **Zero Re-explanation Session Continuity for Claude Code** — Automatic context capture + semantic search
4
4
 
@@ -25,27 +25,36 @@ Every new Claude Code session:
25
25
  **Fully automatic.** Claude Hooks handle everything without manual calls:
26
26
 
27
27
  ```bash
28
- # Session start → Auto-loads relevant context (Git-based semantic search)
29
- # When asking → Auto-injects memories/solutions related to your query
30
- # During conversation → Auto-captures important decisions/errors/learnings
31
- # On commitCommit messages automatically become memories
28
+ # Session start → Auto-loads relevant context + recent session history
29
+ # When asking → Auto-injects relevant memories/solutions
30
+ # During conversation → Tracks active files (no noise memories)
31
+ # On compactStructured handover context for continuity
32
+ # On exit → Extracts commits, decisions, error-fix pairs from transcript
32
33
  ```
33
34
 
34
35
  ```
35
36
  ← Auto-output on session start:
36
- # 🚀 my-app - Session Resumed
37
+ # my-app - Session Resumed
37
38
 
38
- ## Tech Stack
39
- **framework**: Next.js, **language**: TypeScript
39
+ 📍 **State**: Implementing signup form
40
40
 
41
- ## Current State
42
- 📍 Implementing signup form
43
- 🚧 **Blocker**: OAuth callback URL issue
41
+ ## Recent Sessions
42
+ ### 2026-02-28
43
+ **Work**: Completed OAuth integration with Google provider
44
+ **Commits**: feat: add OAuth callback handler; fix: redirect URI config
45
+ **Decisions**: Use Server Actions instead of API routes
46
+
47
+ ### 2026-02-27
48
+ **Work**: Set up authentication foundation
49
+ **Next**: Implement signup form validation
50
+
51
+ ## Directives
52
+ - 🔴 Always use Zod for form validation
53
+ - 📎 Prefer Server Components by default
44
54
 
45
- ## 🧠 Relevant Memories (semantic: 0.89)
46
- - 🎯 [decision] Decided on App Router, using Server Actions
47
- - ⚠️ [error] OAuth redirect_uri mismatch → check env file
48
- - 📚 [learning] Zod form validation gives automatic type inference
55
+ ## Key Memories
56
+ - 🎯 Decided on App Router, using Server Actions
57
+ - ⚠️ OAuth redirect_uri mismatch → check env file
49
58
  ```
50
59
 
51
60
  **Zero manual work. Context follows you.**
@@ -64,8 +73,6 @@ npm install claude-session-continuity-mcp
64
73
  1. Registers MCP server in `~/.claude.json`
65
74
  2. Installs Claude Hooks in `~/.claude/settings.json`
66
75
 
67
- > **v1.6.1:** Fixed critical bug where hooks were installed to wrong settings file. Now correctly installs to `~/.claude/settings.json`. Auto-migrates existing users from `settings.local.json`.
68
-
69
76
  ### What Gets Installed
70
77
 
71
78
  **MCP Server** (in `~/.claude.json`):
@@ -101,9 +108,9 @@ npm install claude-session-continuity-mcp
101
108
  |------|---------|----------|
102
109
  | `SessionStart` | `claude-hook-session-start` | Auto-loads project context on session start |
103
110
  | `UserPromptSubmit` | `claude-hook-user-prompt` | Auto-injects relevant memories + past reference search |
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) |
111
+ | `PostToolUse` | `claude-hook-post-tool` | Tracks active files (Edit, Write) no noise memories |
112
+ | `PreCompact` | `claude-hook-pre-compact` | Structured handover context before compression |
113
+ | `Stop` | `claude-hook-session-end` | Extracts commits, decisions, error-fix pairs from transcript |
107
114
 
108
115
  ### Manual Hook Management
109
116
 
@@ -129,19 +136,18 @@ After installation, restart Claude Code to activate the hooks.
129
136
  | Feature | Description |
130
137
  |---------|-------------|
131
138
  | 🤖 **Zero Manual Work** | Claude Hooks automate all context capture/load |
132
- | 🎯 **Query-Based Injection** | Selectively inject only relevant memories/solutions |
139
+ | 🎯 **Quality Memory Only** | **(v1.10.0)** Only decisions, learnings, errors — no file-change noise |
133
140
  | 🧠 **Semantic Search** | multilingual-e5-small embedding (94+ languages, 384d) |
134
141
  | 🌍 **Multilingual** | Korean/English/Japanese + cross-language search (EN→KR, KR→EN) |
135
- | 🔗 **Git Integration** | Commit messages auto-memorized |
142
+ | 🔗 **Git Integration** | Commit messages auto-extracted from transcripts |
136
143
  | 🕸️ **Knowledge Graph** | Memory relations (solves, causes, extends...) |
137
- | 📊 **Memory Classification** | 6 types: observation, decision, learning, error, pattern, code |
144
+ | 📊 **Memory Classification** | 5 types: observation, decision, learning, error, pattern |
138
145
  | ✅ **Integrated Verification** | One-click build/test/lint execution |
139
146
  | 📋 **Task Management** | Priority-based task management |
140
147
  | 🔧 **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 |
148
+ | 📝 **Structured Handover** | **(v1.10.0)** PreCompact saves work summary, active files, pending actions |
149
+ | 🚪 **Smart Session End** | **(v1.10.0)** Extracts commits, decisions, error-fix pairs from transcript |
150
+ | 🗑️ **Auto Noise Cleanup** | **(v1.10.0)** Auto-deletes stale observation memories (3d+) |
145
151
  | 🔍 **Past Reference Detection** | **(v1.8.0)** "저번에 X 어떻게 했어?" auto-searches DB |
146
152
  | 📝 **User Directive Extraction** | **(v1.8.0)** Auto-extracts "always/never" rules from prompts |
147
153
 
@@ -153,32 +159,53 @@ After installation, restart Claude Code to activate the hooks.
153
159
 
154
160
  **SessionStart Hook** (`npx claude-hook-session-start`):
155
161
  - Auto-detects project: monorepo (`apps/project-name/`) or single project (`package.json` root folder name)
156
- - Loads context from `~/.claude/sessions.db`
157
- - Injects: Tech stack, current state, pending tasks, recent memories
162
+ - Loads context from `.claude/sessions.db`
163
+ - Injects: Current state, **3 recent sessions** with commits/decisions, directives, pending tasks, filtered key memories
164
+ - Auto-cleans stale noise memories (3d+ auto-tracked, 14d+ auto-compact)
158
165
 
159
166
  **UserPromptSubmit Hook** (`npx claude-hook-user-prompt`):
160
167
  - Runs on every prompt submission
161
- - Injects relevant context based on current project
168
+ - Injects relevant context (filtered: decisions, learnings, errors only)
169
+
170
+ **PostToolUse Hook** (`npx claude-hook-post-tool`):
171
+ - Tracks hot file paths and updates `active_context.recent_files`
172
+ - **No longer creates observation memories** (v1.10.0 — eliminates `[File Change]` noise)
173
+
174
+ **PreCompact Hook** (`npx claude-hook-pre-compact`):
175
+ - Builds structured handover context: work summary, active file, pending action, key facts, recent errors
176
+ - **No longer stores auto-compact memories** (v1.10.0)
177
+
178
+ **Stop Hook** (`npx claude-hook-session-end`):
179
+ - Extracts commit messages from JSONL transcript (`git commit -m` patterns)
180
+ - Extracts error-fix pairs (error → resolution within 3 messages)
181
+ - Extracts decisions ("because", "instead of", "chose" patterns)
182
+ - Stores structured metadata in `sessions.issues` column as JSON
162
183
 
163
184
  ### Example Output (Session Start)
164
185
 
165
186
  ```markdown
166
- # 🚀 my-app - Session Resumed
167
-
168
- ## Tech Stack
169
- **framework**: Next.js, **language**: TypeScript
187
+ # my-app - Session Resumed
170
188
 
171
- ## Current State
172
- 📍 Implementing signup form
189
+ 📍 **State**: Implementing signup form
173
190
  🚧 **Blocker**: OAuth callback URL issue
174
191
 
175
- ## 📋 Pending Tasks
192
+ ## Recent Sessions
193
+ ### 2026-02-28
194
+ **Work**: Completed OAuth integration
195
+ **Commits**: feat: add OAuth handler; fix: redirect config
196
+ **Decisions**: Use Server Actions over API routes
197
+ **Next**: Implement form validation
198
+
199
+ ## Directives
200
+ - 🔴 Always use Zod for validation
201
+
202
+ ## Pending Tasks
176
203
  - 🔄 [P8] Implement form validation
177
204
  - ⏳ [P5] Add error handling
178
205
 
179
- ## 🧠 Key Memories
180
- - 🎯 [decision] Decided on App Router, using Server Actions
181
- - ⚠️ [error] OAuth redirect_uri mismatch → check env file
206
+ ## Key Memories
207
+ - 🎯 Decided on App Router, using Server Actions
208
+ - ⚠️ OAuth redirect_uri mismatch → check env file
182
209
  ```
183
210
 
184
211
  ### Hook Management
@@ -496,6 +523,11 @@ npm run test:coverage
496
523
  - [x] Empty session skip and techStack save improvements (v1.7.1)
497
524
  - [x] Past reference auto-detection in UserPromptSubmit hook (v1.8.0)
498
525
  - [x] User directive extraction ("always/never" rules) (v1.8.0)
526
+ - [x] Memory quality overhaul — no more `[File Change]` noise (v1.10.0)
527
+ - [x] Structured handover context in PreCompact (v1.10.0)
528
+ - [x] Smart session-end: commit/decision/error-fix extraction from transcript (v1.10.0)
529
+ - [x] Auto noise cleanup (3d+ observations, 14d+ auto-compact) (v1.10.0)
530
+ - [x] 3 recent sessions display with structured metadata (v1.10.0)
499
531
  - [ ] sqlite-vec native vector search (v2 - when data > 1000 records)
500
532
  - [ ] Web dashboard
501
533
  - [ ] Cloud sync option
@@ -183,20 +183,8 @@ async function main() {
183
183
  catch {
184
184
  // 오류 시 무시
185
185
  }
186
- // 중요 변경사항은 메모리에 기록 (하루에 같은 파일 중복 방지)
187
- const today = new Date().toISOString().slice(0, 10);
188
- const existingMemory = db.prepare(`
189
- SELECT id FROM memories
190
- WHERE project = ?
191
- AND content LIKE ?
192
- AND date(created_at) = ?
193
- `).get(project, `%${path.basename(filePath)}%`, today);
194
- if (!existingMemory) {
195
- db.prepare(`
196
- INSERT INTO memories (content, memory_type, project, importance, tags)
197
- VALUES (?, 'observation', ?, 3, ?)
198
- `).run(`[File Change] ${summary}`, project, `auto-tracked,${changeType},${getFileExtension(filePath)}`);
199
- }
186
+ // auto-tracked 메모리 기록 제거 (v1.10.0)
187
+ // git이 파일 변경을 잘 추적함. hot_paths + recent_files만 유지.
200
188
  }
201
189
  db.close();
202
190
  process.exit(0);
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PreCompact Hook - 컨텍스트 압축 전 중요 메모리 저장
3
+ * PreCompact Hook v2 - 컨텍스트 압축 전 구조화된 HANDOVER 생성
4
4
  *
5
- * 컨텍스트가 압축되기 전에 현재 세션의 중요 정보를 메모리에 저장합니다.
5
+ * 컴팩션 전에 대화 내용을 분석해 구조화된 컨텍스트를 systemMessage로 반환합니다.
6
+ * v1과 달리 memories 테이블에 저장하지 않습니다 (노이즈 방지).
6
7
  */
7
8
  export {};
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * PreCompact Hook - 컨텍스트 압축 전 중요 메모리 저장
3
+ * PreCompact Hook v2 - 컨텍스트 압축 전 구조화된 HANDOVER 생성
4
4
  *
5
- * 컨텍스트가 압축되기 전에 현재 세션의 중요 정보를 메모리에 저장합니다.
5
+ * 컴팩션 전에 대화 내용을 분석해 구조화된 컨텍스트를 systemMessage로 반환합니다.
6
+ * v1과 달리 memories 테이블에 저장하지 않습니다 (노이즈 방지).
6
7
  */
7
8
  import * as fs from 'fs';
8
9
  import * as path from 'path';
@@ -30,12 +31,10 @@ function getDbPath(cwd) {
30
31
  function detectProject(cwd) {
31
32
  const workspaceRoot = detectWorkspaceRoot(cwd);
32
33
  const appsDir = path.join(workspaceRoot, 'apps');
33
- // apps/ 하위인지 확인
34
34
  if (cwd.startsWith(appsDir + path.sep)) {
35
35
  const relative = path.relative(appsDir, cwd);
36
36
  return relative.split(path.sep)[0];
37
37
  }
38
- // apps/ 외부 하위 프로젝트 (hackathons/ 등)
39
38
  if (cwd !== workspaceRoot) {
40
39
  let current = cwd;
41
40
  while (current !== workspaceRoot && current !== path.parse(current).root) {
@@ -52,44 +51,97 @@ function detectProject(cwd) {
52
51
  current = path.dirname(current);
53
52
  }
54
53
  }
55
- // 워크스페이스 루트 → 폴더명 반환
56
54
  return path.basename(workspaceRoot);
57
55
  }
58
- function extractKeyPoints(transcript) {
59
- const keyPoints = [];
60
- // 최근 메시지에서 중요 패턴 추출
61
- const recentMessages = transcript.slice(-20);
62
- for (const msg of recentMessages) {
63
- if (msg.role !== 'assistant')
64
- continue;
65
- const content = msg.content;
66
- // 결정 사항 패턴
67
- const decisionPatterns = [
68
- /(?:decided|결정|선택)[^.]*\./gi,
69
- /(?:will use|사용할)[^.]*\./gi,
70
- /(?:approach|방식)[^.]*\./gi,
56
+ function stripMarkdown(text) {
57
+ return text
58
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
59
+ .replace(/`([^`]+)`/g, '$1')
60
+ .replace(/#{1,6}\s*/g, '')
61
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
62
+ .trim();
63
+ }
64
+ /**
65
+ * 대화 transcript에서 구조화된 핸드오버 컨텍스트를 빌드합니다.
66
+ */
67
+ function buildHandoverContext(transcript) {
68
+ const context = {
69
+ workSummary: '',
70
+ activeFile: null,
71
+ pendingAction: null,
72
+ keyFacts: [],
73
+ recentErrors: []
74
+ };
75
+ const userMessages = transcript.filter(m => m.role === 'user');
76
+ const assistantMessages = transcript.filter(m => m.role === 'assistant');
77
+ // 1. workSummary: 첫 user 메시지 = 작업 요청
78
+ if (userMessages.length > 0) {
79
+ const first = userMessages[0].content;
80
+ // 코드블록, 테이블 제거 후 첫 의미있는 라인
81
+ const cleaned = first
82
+ .replace(/```[\s\S]*?```/g, '')
83
+ .split('\n')
84
+ .map(l => l.trim())
85
+ .filter(l => l.length > 10 && !l.startsWith('|') && !l.startsWith('---'));
86
+ if (cleaned.length > 0) {
87
+ context.workSummary = stripMarkdown(cleaned[0]).slice(0, 200);
88
+ }
89
+ }
90
+ // 2. activeFile: 최근 메시지에서 파일 경로 추출
91
+ const recentAll = transcript.slice(-10);
92
+ for (const msg of recentAll.reverse()) {
93
+ const filePatterns = [
94
+ /(?:file_path|파일)[:\s]*["']?([^\s"',]+\.\w{1,6})/,
95
+ /(?:Edit|Write|Read|수정|생성|읽기)\s+.*?(\S+\.\w{1,6})/,
96
+ /`([^`]+\.\w{1,6})`/,
71
97
  ];
72
- for (const pattern of decisionPatterns) {
73
- const matches = content.match(pattern);
74
- if (matches) {
75
- keyPoints.push(...matches.slice(0, 2));
98
+ for (const pattern of filePatterns) {
99
+ const match = msg.content.match(pattern);
100
+ if (match?.[1] && !match[1].includes('http')) {
101
+ context.activeFile = match[1];
102
+ break;
76
103
  }
77
104
  }
78
- // 에러 해결 패턴
79
- const errorPatterns = [
80
- /(?:fixed|수정|해결)[^.]*(?:error|bug|issue|오류|버그)[^.]*\./gi,
81
- /(?:error|bug|issue|오류|버그)[^.]*(?:fixed|수정|해결)[^.]*\./gi,
82
- ];
83
- for (const pattern of errorPatterns) {
84
- const matches = content.match(pattern);
85
- if (matches) {
86
- keyPoints.push(...matches.slice(0, 2));
105
+ if (context.activeFile)
106
+ break;
107
+ }
108
+ // 3. pendingAction: 마지막 메시지가 user면 미완료 요청
109
+ if (transcript.length > 0 && transcript[transcript.length - 1].role === 'user') {
110
+ const lastUser = transcript[transcript.length - 1].content;
111
+ const cleaned = stripMarkdown(lastUser.split('\n')[0] || lastUser);
112
+ if (cleaned.length > 5) {
113
+ context.pendingAction = cleaned.slice(0, 150);
114
+ }
115
+ }
116
+ // 4. keyFacts: assistant 메시지에서 설정값, 포트, 버전 등 추출
117
+ const factPatterns = [
118
+ /(?:port|포트)\s*(?:is|=|:|→)\s*(\d{2,5})/gi,
119
+ /(?:version|버전)\s*(?:is|=|:|→)\s*([\d.]+)/gi,
120
+ /(?:IP|ip)\s*(?:is|=|:|→)\s*([\d.]+)/gi,
121
+ /(?:using|사용)\s+([\w\s.-]+?\s+v[\d.]+)/gi,
122
+ ];
123
+ for (const msg of assistantMessages.slice(-10)) {
124
+ for (const pattern of factPatterns) {
125
+ pattern.lastIndex = 0;
126
+ const match = msg.content.match(pattern);
127
+ if (match) {
128
+ context.keyFacts.push(stripMarkdown(match[0]).slice(0, 100));
87
129
  }
88
130
  }
89
131
  }
90
- // 중복 제거 길이 제한
91
- const unique = [...new Set(keyPoints)].slice(0, 5);
92
- return unique.map(p => p.slice(0, 200));
132
+ context.keyFacts = [...new Set(context.keyFacts)].slice(0, 5);
133
+ // 5. recentErrors: 에러 패턴 추출
134
+ for (const msg of transcript.slice(-15)) {
135
+ const errorMatch = msg.content.match(/(?:Error|error|ERROR|오류|실패|FAILED|Exception)[:\s](.{10,100})/);
136
+ if (errorMatch) {
137
+ const err = stripMarkdown(errorMatch[0]).slice(0, 100);
138
+ if (!context.recentErrors.includes(err)) {
139
+ context.recentErrors.push(err);
140
+ }
141
+ }
142
+ }
143
+ context.recentErrors = context.recentErrors.slice(0, 3);
144
+ return context;
93
145
  }
94
146
  async function main() {
95
147
  try {
@@ -106,23 +158,23 @@ async function main() {
106
158
  process.exit(0);
107
159
  }
108
160
  const db = new Database(dbPath);
109
- // transcript에서 핵심 포인트 추출
110
- const keyPoints = input.transcript ? extractKeyPoints(input.transcript) : [];
111
- if (keyPoints.length > 0) {
112
- // 중요 메모리로 저장
113
- db.prepare(`
114
- INSERT INTO memories (content, memory_type, project, importance, tags)
115
- VALUES (?, 'pattern', ?, 8, 'auto-compact,session-summary')
116
- `).run(`[Pre-Compact Summary] ${keyPoints.join(' | ')}`, project);
117
- // 활성 컨텍스트 업데이트
161
+ // 핸드오버 컨텍스트 빌드
162
+ const handover = input.transcript ? buildHandoverContext(input.transcript) : null;
163
+ // active_context 업데이트 (memories에는 저장하지 않음)
164
+ if (handover?.workSummary) {
165
+ const stateStr = [
166
+ handover.workSummary,
167
+ handover.activeFile ? `file: ${handover.activeFile}` : '',
168
+ handover.pendingAction ? `pending: ${handover.pendingAction.slice(0, 50)}` : ''
169
+ ].filter(Boolean).join(' | ');
118
170
  db.prepare(`
119
171
  INSERT OR REPLACE INTO active_context (project, current_state, updated_at)
120
172
  VALUES (?, ?, datetime('now'))
121
- `).run(project, `Compacted: ${keyPoints[0]?.slice(0, 50) || 'Session context saved'}`);
173
+ `).run(project, stateStr.slice(0, 300));
122
174
  }
123
175
  // === 컨텍스트 재주입: systemMessage로 반환 ===
124
- const recoveryLines = [`# ${project} - Recovered Context\n`];
125
- // 사용자 지시사항 (HIGH 우선)
176
+ const recoveryLines = [`# ${project} - Compact Recovery\n`];
177
+ // 사용자 지시사항
126
178
  try {
127
179
  const directives = db.prepare(`
128
180
  SELECT directive, priority FROM user_directives
@@ -154,27 +206,25 @@ async function main() {
154
206
  if (active.blockers)
155
207
  recoveryLines.push(`**Blocker**: ${active.blockers}`);
156
208
  }
157
- // Hot paths (상위 5개)
158
- try {
159
- const hotPaths = db.prepare(`
160
- SELECT file_path, access_count FROM hot_paths
161
- WHERE project = ? AND last_accessed > datetime('now', '-7 days')
162
- ORDER BY access_count DESC LIMIT 5
163
- `).all(project);
164
- if (hotPaths.length > 0) {
165
- recoveryLines.push(`**Hot Files**: ${hotPaths.map(h => h.file_path.split('/').pop()).join(', ')}`);
209
+ // 핸드오버 컨텍스트
210
+ if (handover) {
211
+ recoveryLines.push(`\n## Handover`);
212
+ if (handover.workSummary)
213
+ recoveryLines.push(`**Working on**: ${handover.workSummary}`);
214
+ if (handover.activeFile)
215
+ recoveryLines.push(`**Active file**: ${handover.activeFile}`);
216
+ if (handover.pendingAction)
217
+ recoveryLines.push(`**Pending**: ${handover.pendingAction}`);
218
+ if (handover.keyFacts.length > 0) {
219
+ recoveryLines.push('**Key facts**:');
220
+ handover.keyFacts.forEach(f => recoveryLines.push(`- ${f}`));
166
221
  }
167
- }
168
- catch { /* table may not exist */ }
169
- // Key points from this session
170
- if (keyPoints.length > 0) {
171
- recoveryLines.push(`\n## Session Key Points`);
172
- for (const kp of keyPoints) {
173
- recoveryLines.push(`- ${kp}`);
222
+ if (handover.recentErrors.length > 0) {
223
+ recoveryLines.push('**Recent errors**:');
224
+ handover.recentErrors.forEach(e => recoveryLines.push(`- ${e}`));
174
225
  }
175
226
  }
176
227
  db.close();
177
- // systemMessage로 반환 → 컴팩션 후에도 유지
178
228
  const output = {
179
229
  continue: true,
180
230
  systemMessage: recoveryLines.join('\n')
@@ -192,15 +192,13 @@ async function readRecentAssistantMessages(transcriptPath, maxMessages = 5) {
192
192
  }
193
193
  /**
194
194
  * 다음 할 일 추출 (텍스트에서)
195
- * 테이블 행, 코드블록 내부는 제외
196
195
  */
197
196
  function extractNextTasks(content) {
198
197
  const nextTasks = [];
199
- // 코드블록과 테이블 행 제거
200
198
  const cleaned = content
201
- .replace(/```[\s\S]*?```/g, '') // 코드블록 제거
199
+ .replace(/```[\s\S]*?```/g, '')
202
200
  .split('\n')
203
- .filter(line => !line.trim().startsWith('|')) // 테이블 행 제거
201
+ .filter(line => !line.trim().startsWith('|'))
204
202
  .join('\n');
205
203
  const nextPatterns = [
206
204
  /(?:next steps?|todo|remaining|다음 (?:단계|작업|할 일)|남은 작업|해야 할)[:\s]*([^.!?\n]{10,})/gi,
@@ -217,6 +215,144 @@ function extractNextTasks(content) {
217
215
  }
218
216
  return nextTasks;
219
217
  }
218
+ /**
219
+ * JSONL transcript에서 git commit 메시지 추출
220
+ */
221
+ async function extractCommitMessages(transcriptPath) {
222
+ if (!transcriptPath || !fs.existsSync(transcriptPath))
223
+ return [];
224
+ const commits = [];
225
+ try {
226
+ const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
227
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
228
+ for await (const line of rl) {
229
+ if (!line.trim())
230
+ continue;
231
+ try {
232
+ const content = line;
233
+ // git commit -m "message" 또는 heredoc 패턴
234
+ const commitPatterns = [
235
+ /git commit.*?-m\s*["']([^"']{10,150})["']/,
236
+ /git commit.*?-m\s*"\$\(cat <<'?EOF'?\n([\s\S]{10,150}?)(?:\n\s*Co-Authored|\nEOF)/,
237
+ ];
238
+ for (const pattern of commitPatterns) {
239
+ const match = content.match(pattern);
240
+ if (match?.[1]) {
241
+ const msg = match[1].trim().split('\n')[0]; // 첫 줄만
242
+ if (msg.length > 10 && !msg.startsWith('Co-Authored')) {
243
+ commits.push(msg.slice(0, 150));
244
+ }
245
+ }
246
+ }
247
+ }
248
+ catch { /* skip */ }
249
+ }
250
+ }
251
+ catch { /* file read error */ }
252
+ return [...new Set(commits)].slice(0, 5);
253
+ }
254
+ /**
255
+ * JSONL transcript에서 에러→해결 쌍 추출
256
+ */
257
+ async function extractErrorFixPairs(transcriptPath) {
258
+ if (!transcriptPath || !fs.existsSync(transcriptPath))
259
+ return [];
260
+ const pairs = [];
261
+ const entries = [];
262
+ try {
263
+ const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
264
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
265
+ for await (const line of rl) {
266
+ if (!line.trim())
267
+ continue;
268
+ try {
269
+ const entry = JSON.parse(line);
270
+ const role = entry.type || entry.role || '';
271
+ let text = '';
272
+ if (typeof entry.message?.content === 'string') {
273
+ text = entry.message.content;
274
+ }
275
+ else if (Array.isArray(entry.message?.content)) {
276
+ text = entry.message.content
277
+ .filter((b) => b.type === 'text')
278
+ .map((b) => b.text)
279
+ .join('\n');
280
+ }
281
+ if (text.length > 5)
282
+ entries.push({ role, text: text.slice(0, 500) });
283
+ }
284
+ catch { /* skip */ }
285
+ }
286
+ }
287
+ catch { /* file error */ }
288
+ const recent = entries.slice(-30);
289
+ const errorRe = /(?:error|Error|ERROR|오류|실패|FAILED|Exception)[:\s](.{5,80})/;
290
+ const fixRe = /(?:fixed|resolved|수정|해결|Added|수정 완료)/i;
291
+ for (let i = 0; i < recent.length - 1; i++) {
292
+ const errorMatch = recent[i].text.match(errorRe);
293
+ if (errorMatch) {
294
+ for (let j = i + 1; j < Math.min(i + 4, recent.length); j++) {
295
+ if (recent[j].role === 'assistant' && fixRe.test(recent[j].text)) {
296
+ const errorStr = stripMarkdown(errorMatch[0]).slice(0, 80);
297
+ const fixLine = recent[j].text.split('\n')
298
+ .find(l => fixRe.test(l));
299
+ const fixStr = fixLine ? stripMarkdown(fixLine).slice(0, 80) : 'resolved';
300
+ pairs.push(`${errorStr} → ${fixStr}`);
301
+ break;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ return [...new Set(pairs)].slice(0, 3);
307
+ }
308
+ /**
309
+ * JSONL transcript에서 결정 사항 추출
310
+ */
311
+ async function extractDecisions(transcriptPath) {
312
+ const messages = await readRecentAssistantMessages(transcriptPath, 10);
313
+ const decisions = [];
314
+ const decisionPatterns = [
315
+ /(?:chose|using|switched to|went with)\s+(.{10,80})\s+(?:because|since|instead of|over)/gi,
316
+ /(?:instead of|rather than)\s+(.{10,60})/gi,
317
+ /(.{10,60})(?:으로|로)\s+(?:결정|변경|전환)(?:했|함|합니다)/g,
318
+ /(.{10,60})(?:대신|말고)\s+(.{10,60})(?:사용|적용)/g,
319
+ ];
320
+ for (const msg of messages) {
321
+ for (const pattern of decisionPatterns) {
322
+ pattern.lastIndex = 0;
323
+ const matches = msg.match(pattern);
324
+ if (matches) {
325
+ decisions.push(...matches.slice(0, 1).map(m => stripMarkdown(m).slice(0, 150)));
326
+ }
327
+ }
328
+ }
329
+ return [...new Set(decisions)].slice(0, 3);
330
+ }
331
+ /**
332
+ * 최근 assistant 메시지에서 액션 동사 포함 라인 추출 (lastWork 폴백)
333
+ */
334
+ async function extractWorkFromRecentMessages(transcriptPath) {
335
+ const messages = await readRecentAssistantMessages(transcriptPath, 5);
336
+ const actionVerbs = /(?:created|modified|added|removed|fixed|updated|implemented|deployed|configured|refactored|만들|수정|추가|삭제|구현|배포|설정|완료)/i;
337
+ const filePath = /(?:\/[\w.-]+){2,}|[\w.-]+\.\w{1,4}/;
338
+ for (const msg of messages.reverse()) {
339
+ const lines = msg.split('\n').filter(l => !isNoiseLine(l));
340
+ for (const line of lines) {
341
+ if (actionVerbs.test(line) && line.length > 20) {
342
+ const cleaned = stripMarkdown(line).trim();
343
+ if (cleaned.length > 15)
344
+ return cleaned.slice(0, 200);
345
+ }
346
+ }
347
+ // 파일 경로 포함 라인도 시도
348
+ for (const line of lines) {
349
+ if (filePath.test(line) && actionVerbs.test(line)) {
350
+ return stripMarkdown(line).trim().slice(0, 200);
351
+ }
352
+ }
353
+ }
354
+ return '';
355
+ }
220
356
  async function main() {
221
357
  try {
222
358
  let inputData = '';
@@ -238,15 +374,32 @@ async function main() {
238
374
  process.exit(0);
239
375
  }
240
376
  const db = new Database(dbPath);
241
- // === last_work 추출 ===
377
+ // === 추출 시작 ===
242
378
  let lastWork = '';
243
379
  let nextTasks = [];
244
- // 소스 1: last_assistant_message (Stop 이벤트에서 직접 제공)
245
- if (input.last_assistant_message) {
380
+ let commitMessages = [];
381
+ let errorsSolved = [];
382
+ let decisions = [];
383
+ // Phase 1: transcript_path에서 고품질 데이터 추출
384
+ if (input.transcript_path) {
385
+ // git commit 메시지 추출 (가장 고품질 요약)
386
+ commitMessages = await extractCommitMessages(input.transcript_path);
387
+ // 에러→해결 쌍 추출
388
+ errorsSolved = await extractErrorFixPairs(input.transcript_path);
389
+ // 결정 사항 추출
390
+ decisions = await extractDecisions(input.transcript_path);
391
+ }
392
+ // Phase 2: lastWork 결정 (우선순위 폴백)
393
+ // 2a: 커밋 메시지 기반 (가장 신뢰도 높음)
394
+ if (commitMessages.length > 0) {
395
+ lastWork = commitMessages.slice(0, 3).join('; ');
396
+ }
397
+ // 2b: last_assistant_message에서 추출
398
+ if (!lastWork && input.last_assistant_message) {
246
399
  lastWork = extractSummaryFromText(input.last_assistant_message);
247
400
  nextTasks = extractNextTasks(input.last_assistant_message);
248
401
  }
249
- // 소스 2: transcript_path에서 마지막 assistant 메시지들 읽기 (소스 1 실패 시)
402
+ // 2c: transcript에서 최근 assistant 메시지 스캔
250
403
  if (!lastWork && input.transcript_path) {
251
404
  const recentMessages = await readRecentAssistantMessages(input.transcript_path);
252
405
  for (let i = recentMessages.length - 1; i >= 0; i--) {
@@ -254,12 +407,15 @@ async function main() {
254
407
  if (lastWork)
255
408
  break;
256
409
  }
257
- // next tasks도 마지막 메시지에서 추출
258
- if (recentMessages.length > 0) {
410
+ if (recentMessages.length > 0 && nextTasks.length === 0) {
259
411
  nextTasks = extractNextTasks(recentMessages[recentMessages.length - 1]);
260
412
  }
261
413
  }
262
- // 소스 3: 레거시 transcript 배열 (이전 버전 호환)
414
+ // 2d: 액션 동사 기반 폴백
415
+ if (!lastWork && input.transcript_path) {
416
+ lastWork = await extractWorkFromRecentMessages(input.transcript_path);
417
+ }
418
+ // 2e: 레거시 transcript 배열
263
419
  if (!lastWork && input.transcript) {
264
420
  const assistantMsgs = input.transcript.filter(m => m.role === 'assistant');
265
421
  if (assistantMsgs.length < 2) {
@@ -273,7 +429,7 @@ async function main() {
273
429
  break;
274
430
  }
275
431
  }
276
- // === modified_files: active_context에서 PostToolUse가 실시간 저장한 파일 목록 ===
432
+ // === modified_files ===
277
433
  let modifiedFiles = [];
278
434
  try {
279
435
  const activeCtx = db.prepare('SELECT recent_files FROM active_context WHERE project = ?').get(project);
@@ -282,7 +438,7 @@ async function main() {
282
438
  }
283
439
  }
284
440
  catch { /* active_context may not exist */ }
285
- // last_work 폴백: 파일 목록 기반
441
+ // last_work 최종 폴백: 파일 목록 기반
286
442
  if (!lastWork && modifiedFiles.length > 0) {
287
443
  const fileNames = modifiedFiles.slice(0, 5).map(f => path.basename(f)).join(', ');
288
444
  lastWork = `Modified files: ${fileNames}`;
@@ -293,7 +449,7 @@ async function main() {
293
449
  db.close();
294
450
  process.exit(0);
295
451
  }
296
- // 중복 저장 방지: 최근 60초 이내 동일 last_work
452
+ // 중복 저장 방지
297
453
  const recentDup = db.prepare(`
298
454
  SELECT id FROM sessions
299
455
  WHERE project = ? AND last_work = ? AND timestamp > datetime('now', '-60 seconds')
@@ -304,11 +460,18 @@ async function main() {
304
460
  db.close();
305
461
  process.exit(0);
306
462
  }
463
+ // 구조화 메타데이터 (issues 컬럼 활용)
464
+ const metadata = {
465
+ commits: commitMessages,
466
+ decisions,
467
+ errorsSolved
468
+ };
469
+ const hasMetadata = commitMessages.length > 0 || decisions.length > 0 || errorsSolved.length > 0;
307
470
  // 세션 기록 저장
308
471
  db.prepare(`
309
- INSERT INTO sessions (project, last_work, next_tasks, modified_files)
310
- VALUES (?, ?, ?, ?)
311
- `).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)));
472
+ INSERT INTO sessions (project, last_work, next_tasks, modified_files, issues)
473
+ VALUES (?, ?, ?, ?, ?)
474
+ `).run(project, lastWork, JSON.stringify([...new Set(nextTasks)].slice(0, 5)), JSON.stringify(modifiedFiles.slice(0, 15)), hasMetadata ? JSON.stringify(metadata) : null);
312
475
  // 활성 컨텍스트 업데이트
313
476
  db.prepare(`
314
477
  INSERT OR REPLACE INTO active_context (project, current_state, recent_files, updated_at)
@@ -317,6 +480,7 @@ async function main() {
317
480
  db.close();
318
481
  console.log(`[SessionEnd] Saved session for ${project}`);
319
482
  console.log(` Last work: ${lastWork.slice(0, 80)}`);
483
+ console.log(` Commits: ${commitMessages.length}, Decisions: ${decisions.length}, Errors: ${errorsSolved.length}`);
320
484
  console.log(` Modified files: ${modifiedFiles.length}`);
321
485
  console.log(` Next tasks: ${nextTasks.length}`);
322
486
  process.exit(0);
@@ -44,44 +44,77 @@ function getProject(cwd, workspaceRoot) {
44
44
  // 워크스페이스 루트 (모노레포 포함) → 폴더명 반환
45
45
  return path.basename(workspaceRoot);
46
46
  }
47
+ function cleanupNoiseMemories(db) {
48
+ try {
49
+ // 3일+ auto-tracked 관찰 메모리 삭제
50
+ db.prepare(`
51
+ DELETE FROM memories
52
+ WHERE memory_type = 'observation'
53
+ AND tags LIKE '%auto-tracked%'
54
+ AND created_at < datetime('now', '-3 days')
55
+ `).run();
56
+ // 14일+ auto-compact 패턴 메모리 삭제
57
+ db.prepare(`
58
+ DELETE FROM memories
59
+ WHERE tags LIKE '%auto-compact%'
60
+ AND created_at < datetime('now', '-14 days')
61
+ `).run();
62
+ }
63
+ catch { /* ignore */ }
64
+ }
47
65
  function loadContext(dbPath, project) {
48
66
  if (!fs.existsSync(dbPath))
49
67
  return null;
50
68
  try {
51
- const db = new Database(dbPath, { readonly: true });
69
+ const db = new Database(dbPath);
70
+ // 노이즈 메모리 자동 정리
71
+ cleanupNoiseMemories(db);
52
72
  const lines = [`# ${project} - Session Resumed\n`];
53
- // 기술 스택
54
- const fixed = db.prepare('SELECT tech_stack FROM project_context WHERE project = ?').get(project);
55
- if (fixed?.tech_stack) {
56
- const stack = JSON.parse(fixed.tech_stack);
57
- const stackStr = Object.entries(stack).map(([k, v]) => `**${k}**: ${v}`).join(', ');
58
- lines.push(`## Tech Stack\n${stackStr}\n`);
59
- }
60
73
  // 현재 상태
61
74
  const active = db.prepare('SELECT current_state, blockers FROM active_context WHERE project = ?').get(project);
62
75
  if (active?.current_state) {
63
- lines.push(`## Current State\n📍 ${active.current_state}`);
76
+ lines.push(`📍 **State**: ${active.current_state}`);
64
77
  if (active.blockers)
65
78
  lines.push(`🚧 **Blocker**: ${active.blockers}`);
66
79
  lines.push('');
67
80
  }
68
- // 마지막 세션 (빈 세션 skip)
69
- const last = db.prepare(`
70
- SELECT last_work, next_tasks, timestamp FROM sessions
81
+ // 최근 3개 세션 (빈 세션 skip)
82
+ const recentSessions = db.prepare(`
83
+ SELECT last_work, next_tasks, issues, timestamp FROM sessions
71
84
  WHERE project = ?
72
85
  AND last_work != 'Session ended'
73
86
  AND last_work != 'Session work completed'
74
87
  AND last_work != 'Session started'
75
88
  AND last_work != ''
76
- ORDER BY timestamp DESC LIMIT 1
77
- `).get(project);
78
- if (last?.last_work) {
79
- lines.push(`## Last Session (${last.timestamp?.slice(0, 10) || 'unknown'})`);
80
- lines.push(`**Work**: ${last.last_work}`);
81
- if (last.next_tasks) {
82
- const next = JSON.parse(last.next_tasks);
83
- if (next.length > 0)
84
- lines.push(`**Next**: ${next.slice(0, 3).join(' ')}`);
89
+ AND length(last_work) > 15
90
+ ORDER BY timestamp DESC LIMIT 3
91
+ `).all(project);
92
+ if (recentSessions.length > 0) {
93
+ lines.push('## Recent Sessions');
94
+ for (const session of recentSessions) {
95
+ lines.push(`### ${session.timestamp?.slice(0, 10) || 'unknown'}`);
96
+ lines.push(`**Work**: ${session.last_work}`);
97
+ // 구조화 메타데이터 파싱 (session-end v2에서 저장)
98
+ if (session.issues) {
99
+ try {
100
+ const meta = JSON.parse(session.issues);
101
+ if (meta.commits?.length > 0) {
102
+ lines.push(`**Commits**: ${meta.commits.slice(0, 3).join('; ')}`);
103
+ }
104
+ if (meta.decisions?.length > 0) {
105
+ lines.push(`**Decisions**: ${meta.decisions.join('; ')}`);
106
+ }
107
+ }
108
+ catch { /* plain text issues or empty, skip */ }
109
+ }
110
+ if (session.next_tasks) {
111
+ try {
112
+ const next = JSON.parse(session.next_tasks);
113
+ if (next.length > 0)
114
+ lines.push(`**Next**: ${next.slice(0, 2).join(', ')}`);
115
+ }
116
+ catch { /* skip */ }
117
+ }
85
118
  }
86
119
  lines.push('');
87
120
  }
@@ -92,7 +125,7 @@ function loadContext(dbPath, project) {
92
125
  WHERE project = ? ORDER BY priority DESC, created_at DESC LIMIT 10
93
126
  `).all(project);
94
127
  if (directives.length > 0) {
95
- lines.push('## 📌 Directives');
128
+ lines.push('## Directives');
96
129
  for (const d of directives) {
97
130
  const icon = d.priority === 'high' ? '🔴' : '📎';
98
131
  lines.push(`- ${icon} ${d.directive}`);
@@ -101,58 +134,51 @@ function loadContext(dbPath, project) {
101
134
  }
102
135
  }
103
136
  catch { /* table may not exist yet */ }
104
- // Hot Files (최근 7일, 상위 10개)
137
+ // 미완료 태스크
105
138
  try {
106
- const hotPaths = db.prepare(`
107
- SELECT file_path, access_count FROM hot_paths
108
- WHERE project = ? AND last_accessed > datetime('now', '-7 days')
109
- ORDER BY access_count DESC LIMIT 10
139
+ const tasks = db.prepare(`
140
+ SELECT title, priority, status FROM tasks
141
+ WHERE project = ? AND status IN ('pending', 'in_progress')
142
+ ORDER BY priority DESC LIMIT 5
110
143
  `).all(project);
111
- if (hotPaths.length > 0) {
112
- lines.push('## 🔥 Hot Files');
113
- for (const h of hotPaths) {
114
- const fileName = h.file_path.split('/').pop() || h.file_path;
115
- lines.push(`- ${fileName} (${h.access_count}x)`);
144
+ if (tasks.length > 0) {
145
+ lines.push('## Pending Tasks');
146
+ for (const t of tasks) {
147
+ const icon = t.status === 'in_progress' ? '🔄' : '⏳';
148
+ lines.push(`- ${icon} [P${t.priority}] ${t.title}`);
116
149
  }
117
150
  lines.push('');
118
151
  }
119
152
  }
120
- catch { /* table may not exist yet */ }
121
- // 미완료 태스크
122
- const tasks = db.prepare(`
123
- SELECT title, priority, status FROM tasks
124
- WHERE project = ? AND status IN ('pending', 'in_progress')
125
- ORDER BY priority DESC LIMIT 5
126
- `).all(project);
127
- if (tasks.length > 0) {
128
- lines.push('## 📋 Pending Tasks');
129
- for (const t of tasks) {
130
- const icon = t.status === 'in_progress' ? '🔄' : '⏳';
131
- lines.push(`- ${icon} [P${t.priority}] ${t.title}`);
132
- }
133
- lines.push('');
134
- }
135
- // 중요 메모리
136
- const memories = db.prepare(`
137
- SELECT content, memory_type FROM memories
138
- WHERE project = ?
139
- ORDER BY importance DESC, created_at DESC LIMIT 5
140
- `).all(project);
141
- if (memories.length > 0) {
142
- const typeIcons = {
143
- observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
144
- };
145
- lines.push('## 🧠 Key Memories');
146
- for (const m of memories) {
147
- const icon = typeIcons[m.memory_type] || '💭';
148
- const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
149
- lines.push(`- ${icon} [${m.memory_type}] ${content}`);
153
+ catch { /* table may not exist */ }
154
+ // 중요 메모리 (노이즈 필터링)
155
+ try {
156
+ const memories = db.prepare(`
157
+ SELECT content, memory_type FROM memories
158
+ WHERE project = ?
159
+ AND memory_type IN ('decision', 'learning', 'error', 'preference')
160
+ AND importance >= 5
161
+ AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
162
+ AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
163
+ ORDER BY importance DESC, accessed_at DESC LIMIT 5
164
+ `).all(project);
165
+ if (memories.length > 0) {
166
+ const typeIcons = {
167
+ decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
168
+ };
169
+ lines.push('## Key Memories');
170
+ for (const m of memories) {
171
+ const icon = typeIcons[m.memory_type] || '💭';
172
+ const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
173
+ lines.push(`- ${icon} ${content}`);
174
+ }
175
+ lines.push('');
150
176
  }
151
- lines.push('');
152
177
  }
178
+ catch { /* ignore */ }
153
179
  db.close();
154
180
  lines.push('---');
155
- lines.push('_Auto-injected by session-continuity. Use `session_end` when done._');
181
+ lines.push('_Auto-injected by session-continuity v2. Use `session_end` when done._');
156
182
  return lines.join('\n');
157
183
  }
158
184
  catch (e) {
@@ -286,21 +286,25 @@ function loadContext(dbPath, project, prompt) {
286
286
  }
287
287
  lines.push('');
288
288
  }
289
- // 중요 메모리
289
+ // 중요 메모리 (노이즈 필터링 - v1.10.0)
290
290
  const memories = db.prepare(`
291
291
  SELECT content, memory_type, importance FROM memories
292
292
  WHERE project = ?
293
- ORDER BY importance DESC, created_at DESC LIMIT 5
293
+ AND memory_type IN ('decision', 'learning', 'error', 'preference')
294
+ AND importance >= 5
295
+ AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
296
+ AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
297
+ ORDER BY importance DESC, accessed_at DESC LIMIT 5
294
298
  `).all(project);
295
299
  if (memories.length > 0) {
296
300
  const typeIcons = {
297
- observation: '👀', decision: '🎯', learning: '📚', error: '⚠️', pattern: '🔄'
301
+ decision: '🎯', learning: '📚', error: '⚠️', preference: '💡'
298
302
  };
299
- lines.push('## 🧠 Key Memories');
303
+ lines.push('## Key Memories');
300
304
  for (const m of memories) {
301
305
  const icon = typeIcons[m.memory_type] || '💭';
302
306
  const content = m.content.length > 100 ? m.content.slice(0, 100) + '...' : m.content;
303
- lines.push(`- ${icon} [${m.memory_type}] ${content}`);
307
+ lines.push(`- ${icon} ${content}`);
304
308
  }
305
309
  lines.push('');
306
310
  }
package/dist/index.js CHANGED
@@ -256,6 +256,17 @@ async function generateEmbedding(text, type = 'query') {
256
256
  return null;
257
257
  }
258
258
  }
259
+ function parseTags(tags) {
260
+ if (!tags)
261
+ return [];
262
+ try {
263
+ const parsed = JSON.parse(tags);
264
+ return Array.isArray(parsed) ? parsed : [String(parsed)];
265
+ }
266
+ catch {
267
+ return tags.split(',').map(t => t.trim()).filter(Boolean);
268
+ }
269
+ }
259
270
  function cosineSimilarity(a, b) {
260
271
  if (a.length !== b.length)
261
272
  return 0;
@@ -1482,7 +1493,7 @@ async function handleTool(name, args) {
1482
1493
  content: m.content.substring(0, 300) + (m.content.length > 300 ? '...' : ''),
1483
1494
  type: m.memory_type,
1484
1495
  project: m.project || 'global',
1485
- tags: m.tags ? JSON.parse(m.tags) : [],
1496
+ tags: parseTags(m.tags),
1486
1497
  importance: m.importance,
1487
1498
  similarity: m.similarity ? Math.round(m.similarity * 100) + '%' : undefined,
1488
1499
  created: m.created_at
@@ -1866,23 +1877,27 @@ async function generateProjectContext(project) {
1866
1877
  }
1867
1878
  lines.push('');
1868
1879
  }
1869
- // 5. 최근 관련 메모리 (중요도 높은 5개)
1880
+ // 5. 중요 메모리 (노이즈 필터링 - v1.10.0)
1870
1881
  const recentMemories = db.prepare(`
1871
1882
  SELECT id, content, memory_type, importance FROM memories
1872
1883
  WHERE project = ?
1873
- ORDER BY importance DESC, created_at DESC LIMIT 5
1884
+ AND memory_type IN ('decision', 'learning', 'error', 'preference')
1885
+ AND importance >= 5
1886
+ AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL)
1887
+ AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)
1888
+ ORDER BY importance DESC, accessed_at DESC LIMIT 5
1874
1889
  `).all(project);
1875
1890
  if (recentMemories.length > 0) {
1876
- lines.push(`## 🧠 중요 메모리`);
1891
+ lines.push(`## 🧠 Key Memories`);
1877
1892
  for (const mem of recentMemories) {
1878
1893
  const typeIcon = {
1879
- observation: '👀',
1880
1894
  decision: '🎯',
1881
1895
  learning: '📚',
1882
1896
  error: '⚠️',
1883
- pattern: '🔄'
1884
- }[mem.memory_type] || '💭';
1885
- lines.push(`- ${typeIcon} [${mem.memory_type}] ${mem.content.substring(0, 100)}${mem.content.length > 100 ? '...' : ''}`);
1897
+ preference: '💡'
1898
+ };
1899
+ const icon = typeIcon[mem.memory_type] || '💭';
1900
+ lines.push(`- ${icon} ${mem.content.substring(0, 100)}${mem.content.length > 100 ? '...' : ''}`);
1886
1901
  }
1887
1902
  lines.push('');
1888
1903
  }
@@ -1904,9 +1919,10 @@ async function generateProjectContext(project) {
1904
1919
  }
1905
1920
  async function generateRecentMemories(project, limit = 10) {
1906
1921
  const lines = ['# 🧠 최근 메모리\n'];
1922
+ const noiseFilter = `AND memory_type IN ('decision','learning','error','preference') AND importance >= 5 AND (tags NOT LIKE '%auto-tracked%' OR tags IS NULL) AND (tags NOT LIKE '%auto-compact%' OR tags IS NULL)`;
1907
1923
  const sql = project
1908
- ? `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE project = ? ORDER BY importance DESC, created_at DESC LIMIT ?`
1909
- : `SELECT id, content, memory_type, project, importance, created_at FROM memories ORDER BY importance DESC, created_at DESC LIMIT ?`;
1924
+ ? `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE project = ? ${noiseFilter} ORDER BY importance DESC, created_at DESC LIMIT ?`
1925
+ : `SELECT id, content, memory_type, project, importance, created_at FROM memories WHERE 1=1 ${noiseFilter} ORDER BY importance DESC, created_at DESC LIMIT ?`;
1910
1926
  const memories = project
1911
1927
  ? db.prepare(sql).all(project, limit)
1912
1928
  : db.prepare(sql).all(limit);
@@ -1,6 +1,19 @@
1
1
  // 메모리 시스템 도구 (7개)
2
2
  import { db } from '../db/database.js';
3
3
  import { generateEmbedding, embeddingToBuffer } from '../utils/embedding.js';
4
+ // 태그 파싱 헬퍼 (JSON 배열 또는 콤마구분 문자열 모두 처리)
5
+ function parseTags(tags) {
6
+ if (!tags)
7
+ return [];
8
+ try {
9
+ const parsed = JSON.parse(tags);
10
+ return Array.isArray(parsed) ? parsed : [String(parsed)];
11
+ }
12
+ catch {
13
+ // 콤마구분 문자열 fallback (e.g. "auto-tracked,code,ts")
14
+ return tags.split(',').map(t => t.trim()).filter(Boolean);
15
+ }
16
+ }
4
17
  // ===== 도구 정의 =====
5
18
  export const memoryTools = [
6
19
  {
@@ -168,7 +181,7 @@ export function recallMemory(query, memoryType, project, limit = 10, minImportan
168
181
  ? row.content.slice(0, maxContentLength) + '...'
169
182
  : row.content,
170
183
  type: row.memory_type,
171
- tags: row.tags ? JSON.parse(row.tags) : [],
184
+ tags: parseTags(row.tags),
172
185
  project: row.project,
173
186
  importance: row.importance,
174
187
  createdAt: row.created_at,
@@ -220,7 +233,7 @@ export function recallByTimeframe(timeframe, memoryType, project, limit = 20) {
220
233
  id: row.id,
221
234
  content: row.content,
222
235
  type: row.memory_type,
223
- tags: row.tags ? JSON.parse(row.tags) : [],
236
+ tags: parseTags(row.tags),
224
237
  project: row.project,
225
238
  importance: row.importance,
226
239
  createdAt: row.created_at
@@ -249,14 +262,14 @@ export function searchByTag(tags, matchAll = false, limit = 20) {
249
262
  const conditions = tags.map(() => `tags LIKE ?`);
250
263
  sql += matchAll ? conditions.join(' AND ') : conditions.join(' OR ');
251
264
  sql += ` ORDER BY importance DESC, created_at DESC LIMIT ?`;
252
- const params = [...tags.map(t => `%"${t}"%`), limit];
265
+ const params = [...tags.map(t => `%${t}%`), limit];
253
266
  const stmt = db.prepare(sql);
254
267
  const rows = stmt.all(...params);
255
268
  const memories = rows.map(row => ({
256
269
  id: row.id,
257
270
  content: row.content,
258
271
  type: row.memory_type,
259
- tags: row.tags ? JSON.parse(row.tags) : [],
272
+ tags: parseTags(row.tags),
260
273
  project: row.project,
261
274
  importance: row.importance,
262
275
  createdAt: row.created_at
@@ -4,6 +4,18 @@ import { db } from '../db/database.js';
4
4
  import { generateEmbedding, embeddingToBuffer, bufferToEmbedding, cosineSimilarity } from '../utils/embedding.js';
5
5
  import { logger } from '../utils/logger.js';
6
6
  import { MemoryStoreSchema, MemorySearchSchema, MemoryDeleteSchema } from '../schemas.js';
7
+ // 태그 파싱 헬퍼 (JSON 배열 또는 콤마구분 문자열 모두 처리)
8
+ function parseTags(tags) {
9
+ if (!tags)
10
+ return [];
11
+ try {
12
+ const parsed = JSON.parse(tags);
13
+ return Array.isArray(parsed) ? parsed : [String(parsed)];
14
+ }
15
+ catch {
16
+ return tags.split(',').map(t => t.trim()).filter(Boolean);
17
+ }
18
+ }
7
19
  // ===== 도구 정의 =====
8
20
  export const memoryTools = [
9
21
  {
@@ -178,7 +190,7 @@ async function performFTSSearch(query, type, project, limit = 10, minImportance
178
190
  id: row.id,
179
191
  content: row.content.length > 300 ? row.content.slice(0, 300) + '...' : row.content,
180
192
  type: row.memory_type,
181
- tags: row.tags ? JSON.parse(row.tags) : [],
193
+ tags: parseTags(row.tags),
182
194
  project: row.project,
183
195
  importance: row.importance,
184
196
  createdAt: row.created_at
@@ -241,7 +253,7 @@ async function performSemanticSearch(query, type, project, limit = 10, minImport
241
253
  id: row.id,
242
254
  content: row.content.length > 300 ? row.content.slice(0, 300) + '...' : row.content,
243
255
  type: row.memory_type,
244
- tags: row.tags ? JSON.parse(row.tags) : [],
256
+ tags: parseTags(row.tags),
245
257
  project: row.project,
246
258
  importance: row.importance,
247
259
  similarity: Math.round(row.similarity * 100) / 100,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.9.6",
3
+ "version": "1.10.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",