claude-session-continuity-mcp 1.7.0 β†’ 1.8.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.6.4)
1
+ # claude-session-continuity-mcp (v1.8.0)
2
2
 
3
3
  > **Zero Re-explanation Session Continuity for Claude Code** β€” Automatic context capture + semantic search
4
4
 
@@ -100,7 +100,7 @@ npm install claude-session-continuity-mcp
100
100
  | Hook | Command | Function |
101
101
  |------|---------|----------|
102
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 |
103
+ | `UserPromptSubmit` | `claude-hook-user-prompt` | Auto-injects relevant memories + past reference search |
104
104
  | `PostToolUse` | `claude-hook-post-tool` | Tracks file changes (Edit, Write) automatically |
105
105
  | `PreCompact` | `claude-hook-pre-compact` | Saves important context before compression |
106
106
  | `Stop` | `claude-hook-session-end` | Auto-saves session on exit (no manual call needed) |
@@ -142,6 +142,8 @@ After installation, restart Claude Code to activate the hooks.
142
142
  | πŸ’Ύ **Auto Backup** | **(v1.5.0)** Daily SQLite backup (max 5) |
143
143
  | πŸ›‘οΈ **PreCompact Save** | **(v1.5.0)** Save context before compression |
144
144
  | πŸšͺ **Auto Session End** | **(v1.5.0)** No manual session_end needed |
145
+ | πŸ” **Past Reference Detection** | **(v1.8.0)** "μ €λ²ˆμ— X μ–΄λ–»κ²Œ ν–ˆμ–΄?" auto-searches DB |
146
+ | πŸ“ **User Directive Extraction** | **(v1.8.0)** Auto-extracts "always/never" rules from prompts |
145
147
 
146
148
  ---
147
149
 
@@ -195,6 +197,42 @@ npx claude-session-hooks uninstall
195
197
  export MCP_HOOKS_DISABLED=true
196
198
  ```
197
199
 
200
+ ### Past Reference Detection (v1.8.0)
201
+
202
+ When you ask about past work, the `UserPromptSubmit` hook automatically searches the database:
203
+
204
+ ```
205
+ You: "μ €λ²ˆμ— μΈμ•±κ²°μ œ μ–΄λ–»κ²Œ ν–ˆμ–΄?"
206
+ β†’ Hook detects "μ €λ²ˆμ—" + extracts keyword "μΈμ•±κ²°μ œ"
207
+ β†’ Searches sessions, memories (FTS5), and solutions
208
+ β†’ Injects matching results into context automatically
209
+ ```
210
+
211
+ **Supported patterns (Korean & English):**
212
+
213
+ | Pattern | Example |
214
+ |---------|---------|
215
+ | μ €λ²ˆμ—/전에/이전에 ... μ–΄λ–»κ²Œ | "μ €λ²ˆμ— CORS μ—λŸ¬ μ–΄λ–»κ²Œ ν•΄κ²°ν–ˆμ§€?" |
216
+ | ~ν–ˆλ˜/λ§Œλ“€μ—ˆλ˜/ν•΄κ²°ν–ˆλ˜ | "μˆ˜μ •ν–ˆλ˜ 둜그인 둜직" |
217
+ | μ§€λ‚œ μ„Έμ…˜/μž‘μ—…μ—μ„œ | "μ§€λ‚œ μ„Έμ…˜μ—μ„œ 결제 κ΅¬ν˜„" |
218
+ | last time/before/previously | "How did we handle auth last time?" |
219
+ | did we/did I ... before | "Did we fix the database migration before?" |
220
+ | remember when/recall when | "Remember when we set up CI?" |
221
+
222
+ **Output example:**
223
+ ```markdown
224
+ ## Related Past Work (auto-detected from your question)
225
+
226
+ ### Sessions
227
+ - [2/14] 카카였 둜그인 μ•±ν‚€ μˆ˜μ •, μΈμ•±κ²°μ œ IAP ν”Œλ‘œμš° μˆ˜μ •
228
+
229
+ ### Memories
230
+ - 🎯 [decision] ν…ŒμŠ€νŠΈ: μΈμ•±κ²°μ œ μƒν’ˆ 등둝 μ™„λ£Œ
231
+
232
+ ### Solutions
233
+ - **IAP_BILLING_ERROR**: StoreKit 2 migration으둜 ν•΄κ²°
234
+ ```
235
+
198
236
  ### Why npm exec? (v1.4.3+)
199
237
 
200
238
  Previous versions used absolute paths or `npx`:
@@ -455,6 +493,9 @@ npm run test:coverage
455
493
  - [x] Auto-migrate legacy hooks (v1.6.1)
456
494
  - [x] Fix PostToolUse matcher format to string (v1.6.3)
457
495
  - [x] Fix README documentation for new hook format (v1.6.4)
496
+ - [x] Empty session skip and techStack save improvements (v1.7.1)
497
+ - [x] Past reference auto-detection in UserPromptSubmit hook (v1.8.0)
498
+ - [x] User directive extraction ("always/never" rules) (v1.8.0)
458
499
  - [ ] sqlite-vec native vector search (v2 - when data > 1000 records)
459
500
  - [ ] Web dashboard
460
501
  - [ ] Cloud sync option
@@ -31,6 +31,123 @@ function getProject(cwd, workspaceRoot) {
31
31
  }
32
32
  return null;
33
33
  }
34
+ // ===== κ³Όκ±° μ°Έμ‘° μžλ™ 감지 =====
35
+ const PAST_REFERENCE_PATTERNS = [
36
+ // ν•œκ΅­μ–΄
37
+ /(?:μ €λ²ˆμ—|전에|이전에|κ·Έλ•Œ|μ§€λ‚œλ²ˆμ—|μ˜ˆμ „μ—|μ•„κΉŒ)\s+(.+?)(?:\s*(?:μ–΄λ–»κ²Œ|뭐|무엇|μ™œ|μ–΄λ””|μ–Έμ œ))/,
38
+ /(?:ν–ˆλ˜|ν–ˆμ—ˆλ˜|λ§Œλ“€μ—ˆλ˜|μˆ˜μ •ν–ˆλ˜|κ΅¬ν˜„ν–ˆλ˜|ν•΄κ²°ν–ˆλ˜)\s*(.+)/,
39
+ /(?:μ§€λ‚œ|이전|μ „)\s*(?:μ„Έμ…˜|μž‘μ—…|μ‹œκ°„|번).*?(?:μ—μ„œ|λ•Œ)\s*(.+)/,
40
+ // μ˜μ–΄
41
+ /(?:last time|before|previously|earlier)\s+(?:.*?)\s*((?:how|what|why|where|when).*)/i,
42
+ /(?:did we|did I|have we|have I)\s+(.+)\s+(?:before|last time|earlier)/i,
43
+ /(?:remember when|recall when)\s+(.+)/i,
44
+ ];
45
+ function extractPastKeywords(prompt) {
46
+ for (const pattern of PAST_REFERENCE_PATTERNS) {
47
+ const match = prompt.match(pattern);
48
+ if (match?.[1]) {
49
+ // μΆ”μΆœλœ ν‚€μ›Œλ“œμ—μ„œ 쑰사/μ˜λ¬Έμ‚¬ 제거, 핡심 λ‹¨μ–΄λ§Œ
50
+ return match[1].trim().replace(/[??\s]+$/g, '').slice(0, 50);
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ function searchPastWork(db, keyword) {
56
+ const result = { sessions: [], memories: [], solutions: [] };
57
+ const likeKeyword = `%${keyword}%`;
58
+ // 1. sessions 검색 (졜근 30일, μƒμœ„ 3건)
59
+ try {
60
+ const sessions = db.prepare(`
61
+ SELECT last_work, timestamp FROM sessions
62
+ WHERE last_work LIKE ?
63
+ AND last_work != 'Session ended'
64
+ AND last_work != 'Session work completed'
65
+ AND last_work != 'Session started'
66
+ AND last_work != ''
67
+ AND timestamp > datetime('now', '-30 days')
68
+ ORDER BY timestamp DESC LIMIT 3
69
+ `).all(likeKeyword);
70
+ for (const s of sessions) {
71
+ const work = s.last_work.length > 80 ? s.last_work.slice(0, 80) + '...' : s.last_work;
72
+ result.sessions.push({ date: s.timestamp?.slice(0, 10) || 'unknown', work });
73
+ }
74
+ }
75
+ catch { /* ignore */ }
76
+ // 2. memories FTS5 검색 (μƒμœ„ 2건)
77
+ try {
78
+ const memories = db.prepare(`
79
+ SELECT m.content, m.memory_type FROM memories m
80
+ JOIN memories_fts fts ON m.id = fts.rowid
81
+ WHERE memories_fts MATCH ?
82
+ ORDER BY rank LIMIT 2
83
+ `).all(keyword);
84
+ for (const m of memories) {
85
+ const content = m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content;
86
+ result.memories.push({ type: m.memory_type, content });
87
+ }
88
+ }
89
+ catch {
90
+ // FTS5 λ§€μΉ­ μ‹€νŒ¨ μ‹œ LIKE 폴백
91
+ try {
92
+ const memories = db.prepare(`
93
+ SELECT content, memory_type FROM memories
94
+ WHERE content LIKE ?
95
+ ORDER BY importance DESC, created_at DESC LIMIT 2
96
+ `).all(likeKeyword);
97
+ for (const m of memories) {
98
+ const content = m.content.length > 80 ? m.content.slice(0, 80) + '...' : m.content;
99
+ result.memories.push({ type: m.memory_type, content });
100
+ }
101
+ }
102
+ catch { /* ignore */ }
103
+ }
104
+ // 3. solutions 검색 (μƒμœ„ 2건)
105
+ try {
106
+ const solutions = db.prepare(`
107
+ SELECT error_signature, solution FROM solutions
108
+ WHERE error_signature LIKE ? OR solution LIKE ?
109
+ ORDER BY created_at DESC LIMIT 2
110
+ `).all(likeKeyword, likeKeyword);
111
+ for (const s of solutions) {
112
+ const sol = s.solution.length > 80 ? s.solution.slice(0, 80) + '...' : s.solution;
113
+ result.solutions.push({ signature: s.error_signature, solution: sol });
114
+ }
115
+ }
116
+ catch { /* ignore */ }
117
+ return result;
118
+ }
119
+ function formatPastWork(pastWork) {
120
+ const { sessions, memories, solutions } = pastWork;
121
+ if (sessions.length === 0 && memories.length === 0 && solutions.length === 0)
122
+ return null;
123
+ const lines = ['## Related Past Work (auto-detected from your question)\n'];
124
+ if (sessions.length > 0) {
125
+ lines.push('### Sessions');
126
+ for (const s of sessions) {
127
+ lines.push(`- [${s.date}] ${s.work}`);
128
+ }
129
+ lines.push('');
130
+ }
131
+ if (memories.length > 0) {
132
+ const typeIcons = {
133
+ observation: 'πŸ‘€', decision: '🎯', learning: 'πŸ“š', error: '⚠️', pattern: 'πŸ”„'
134
+ };
135
+ lines.push('### Memories');
136
+ for (const m of memories) {
137
+ const icon = typeIcons[m.type] || 'πŸ’­';
138
+ lines.push(`- ${icon} [${m.type}] ${m.content}`);
139
+ }
140
+ lines.push('');
141
+ }
142
+ if (solutions.length > 0) {
143
+ lines.push('### Solutions');
144
+ for (const s of solutions) {
145
+ lines.push(`- **${s.signature}**: ${s.solution}`);
146
+ }
147
+ lines.push('');
148
+ }
149
+ return lines.join('\n');
150
+ }
34
151
  // ===== μ‚¬μš©μž μ§€μ‹œμ‚¬ν•­ μžλ™ μΆ”μΆœ =====
35
152
  const DIRECTIVE_PATTERNS = [
36
153
  { pattern: /(?:μ ˆλŒ€|never)\s+(.+)/i, priority: 'high' },
@@ -80,7 +197,7 @@ function extractAndSaveDirectives(dbPath, project, prompt) {
80
197
  // ν…Œμ΄λΈ” 미쑴재 λ“± λ¬΄μ‹œ
81
198
  }
82
199
  }
83
- function loadContext(dbPath, project) {
200
+ function loadContext(dbPath, project, prompt) {
84
201
  if (!fs.existsSync(dbPath))
85
202
  return null;
86
203
  try {
@@ -175,19 +292,30 @@ function loadContext(dbPath, project) {
175
292
  lines.push('');
176
293
  }
177
294
  // 졜근 μ—λŸ¬ μ†”λ£¨μ…˜
178
- const solutions = db.prepare(`
295
+ const recentSolutions = db.prepare(`
179
296
  SELECT error_signature, solution FROM solutions
180
297
  WHERE project = ?
181
298
  ORDER BY created_at DESC LIMIT 3
182
299
  `).all(project);
183
- if (solutions.length > 0) {
300
+ if (recentSolutions.length > 0) {
184
301
  lines.push('## πŸ”§ Recent Error Solutions');
185
- for (const s of solutions) {
302
+ for (const s of recentSolutions) {
186
303
  const sol = s.solution.length > 80 ? s.solution.slice(0, 80) + '...' : s.solution;
187
304
  lines.push(`- **${s.error_signature}**: ${sol}`);
188
305
  }
189
306
  lines.push('');
190
307
  }
308
+ // κ³Όκ±° μ°Έμ‘° μžλ™ 검색 (ν”„λ‘¬ν”„νŠΈμ—μ„œ κ³Όκ±° μ°Έμ‘° νŒ¨ν„΄ 감지 μ‹œ)
309
+ if (prompt) {
310
+ const keyword = extractPastKeywords(prompt);
311
+ if (keyword) {
312
+ const pastWork = searchPastWork(db, keyword);
313
+ const pastSection = formatPastWork(pastWork);
314
+ if (pastSection) {
315
+ lines.push(pastSection);
316
+ }
317
+ }
318
+ }
191
319
  db.close();
192
320
  lines.push('---');
193
321
  lines.push('_Auto-injected by MCP v5. Use `session_end` when done._');
@@ -228,16 +356,18 @@ async function main() {
228
356
  }
229
357
  const dbPath = path.join(workspaceRoot, '.claude', 'sessions.db');
230
358
  // μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ—μ„œ μ§€μ‹œμ‚¬ν•­ μΆ”μΆœ
359
+ let userPrompt;
231
360
  if (inputData) {
232
361
  try {
233
362
  const parsed = JSON.parse(inputData);
234
363
  if (parsed.prompt) {
364
+ userPrompt = parsed.prompt;
235
365
  extractAndSaveDirectives(dbPath, project, parsed.prompt);
236
366
  }
237
367
  }
238
368
  catch { /* ignore */ }
239
369
  }
240
- const context = loadContext(dbPath, project);
370
+ const context = loadContext(dbPath, project, userPrompt);
241
371
  if (context) {
242
372
  console.log(`\n<project-context project="${project}">\n${context}\n</project-context>\n`);
243
373
  }
package/dist/index.js CHANGED
@@ -830,9 +830,15 @@ async function handleTool(name, args) {
830
830
  const rawModifiedFiles = args.modifiedFiles || args.recentFiles;
831
831
  const modifiedFiles = Array.isArray(rawModifiedFiles) ? rawModifiedFiles : undefined;
832
832
  const blockers = args.blockers;
833
+ const techStack = args.techStack;
833
834
  if (!project) {
834
835
  return { content: [{ type: 'text', text: 'Error: project is required' }] };
835
836
  }
837
+ // 빈 μ„Έμ…˜ λ°©μ§€
838
+ const emptyPatterns = ['Session ended', 'Session work completed', 'Session started', ''];
839
+ if (emptyPatterns.includes(summary) && (!modifiedFiles || modifiedFiles.length === 0)) {
840
+ return { content: [{ type: 'text', text: `⏭️ Skipped empty session for ${project}` }] };
841
+ }
836
842
  // μ„Έμ…˜ μ €μž₯
837
843
  db.prepare(`
838
844
  INSERT INTO sessions (project, last_work, current_status, next_tasks, modified_files, issues)
@@ -843,7 +849,19 @@ async function handleTool(name, args) {
843
849
  INSERT OR REPLACE INTO active_context (project, current_state, recent_files, blockers, updated_at)
844
850
  VALUES (?, ?, ?, ?, datetime('now'))
845
851
  `).run(project, summary, modifiedFiles ? JSON.stringify(modifiedFiles) : null, blockers || null);
846
- return { content: [{ type: 'text', text: `βœ… Session saved for ${project}\nSummary: ${summary}\nWork: ${workDone || summary}\nFiles: ${modifiedFiles?.length || 0}\nBlockers: ${blockers || 'none'}` }] };
852
+ // techStack μ €μž₯ (있으면)
853
+ if (techStack && Object.keys(techStack).length > 0) {
854
+ const existing = db.prepare('SELECT tech_stack FROM project_context WHERE project = ?').get(project);
855
+ let merged = existing?.tech_stack ? JSON.parse(existing.tech_stack) : {};
856
+ merged = { ...merged, ...techStack };
857
+ const json = JSON.stringify(merged);
858
+ db.prepare(`
859
+ INSERT INTO project_context (project, tech_stack, updated_at)
860
+ VALUES (?, ?, CURRENT_TIMESTAMP)
861
+ ON CONFLICT(project) DO UPDATE SET tech_stack = ?, updated_at = CURRENT_TIMESTAMP
862
+ `).run(project, json, json);
863
+ }
864
+ return { content: [{ type: 'text', text: `βœ… Session saved for ${project}\nSummary: ${summary}\nWork: ${workDone || summary}\nFiles: ${modifiedFiles?.length || 0}\nBlockers: ${blockers || 'none'}${techStack ? '\nTech stack updated' : ''}` }] };
847
865
  }
848
866
  case 'session_history': {
849
867
  const project = args.project;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.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",