claude-session-continuity-mcp 1.10.0 → 1.10.2

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.
@@ -134,11 +134,13 @@ function extractSummaryFromText(content) {
134
134
  if (cleaned.length > 10)
135
135
  return cleaned.slice(0, 200);
136
136
  }
137
- // 전략 4: 첫 헤딩 제목 (## 제목) 짧아도 허용 (성과 요약인 경우 많음)
137
+ // 전략 4: 첫 헤딩 제목 — 단, 일반적인 섹션 헤딩은 제외
138
138
  const headingMatch = cleanedContent.match(/^#{1,3}\s+(.+)$/m);
139
139
  if (headingMatch?.[1]) {
140
140
  const title = stripMarkdown(headingMatch[1]).trim();
141
- if (title.length > 5)
141
+ // "결과 요약", "평가", "분석" 같은 일반 헤딩은 의미없는 요약이므로 건너뜀
142
+ const genericHeadings = /^(결과|요약|분석|평가|결론|테스트|현재|문제|핵심|다음|참고|MCP|Overview|Summary|Result|Analysis|Test)/i;
143
+ if (title.length > 5 && !genericHeadings.test(title))
142
144
  return title.slice(0, 200);
143
145
  }
144
146
  // 전략 5: 첫 의미있는 단락 (노이즈 라인 건너뜀)
@@ -222,30 +224,45 @@ async function extractCommitMessages(transcriptPath) {
222
224
  if (!transcriptPath || !fs.existsSync(transcriptPath))
223
225
  return [];
224
226
  const commits = [];
227
+ // git commit -m "message" 또는 heredoc 패턴 (JSON 파싱 후 적용)
228
+ const commitPatterns = [
229
+ /git commit.*?-m\s*"\$\(cat <<'?EOF'?\n(.+?)(?:\n\n|\nCo-Authored|\nEOF)/s,
230
+ /git commit.*?-m\s*["']([^"'\n]{10,150})["']/,
231
+ ];
225
232
  try {
226
233
  const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
227
234
  const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
228
235
  for await (const line of rl) {
229
- if (!line.trim())
236
+ if (!line.includes('git commit'))
230
237
  continue;
231
238
  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));
239
+ const entry = JSON.parse(line);
240
+ // tool_use 블록에서 command 추출
241
+ const content = entry.message?.content;
242
+ if (!Array.isArray(content))
243
+ continue;
244
+ for (const block of content) {
245
+ if (block.type !== 'tool_use')
246
+ continue;
247
+ const cmd = block.input?.command;
248
+ if (!cmd || !cmd.includes('-m'))
249
+ continue;
250
+ // git commit이 명령어의 시작이거나 && 이후에 나와야 함
251
+ if (!/(?:^|&&\s*)git\s+commit/.test(cmd))
252
+ continue;
253
+ for (const pattern of commitPatterns) {
254
+ const match = cmd.match(pattern);
255
+ if (match?.[1]) {
256
+ const msg = match[1].trim().split('\n')[0]; // 첫 줄만
257
+ if (msg.length > 10 && !msg.startsWith('Co-Authored')) {
258
+ commits.push(msg.slice(0, 150));
259
+ }
260
+ break;
244
261
  }
245
262
  }
246
263
  }
247
264
  }
248
- catch { /* skip */ }
265
+ catch { /* skip malformed lines */ }
249
266
  }
250
267
  }
251
268
  catch { /* file read error */ }
@@ -328,6 +345,109 @@ async function extractDecisions(transcriptPath) {
328
345
  }
329
346
  return [...new Set(decisions)].slice(0, 3);
330
347
  }
348
+ /**
349
+ * 사용자 메시지를 유효한 요청인지 필터링
350
+ */
351
+ function parseUserText(entry) {
352
+ const content = entry.message?.content;
353
+ let text = '';
354
+ if (typeof content === 'string') {
355
+ text = content;
356
+ }
357
+ else if (Array.isArray(content)) {
358
+ text = content
359
+ .filter(b => b.type === 'text')
360
+ .map(b => b.text || '')
361
+ .join('\n');
362
+ }
363
+ // system-reminder, local-command 태그 제거
364
+ text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
365
+ text = text.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g, '').trim();
366
+ text = text.replace(/<command-name>[\s\S]*?<\/command-name>/g, '').trim();
367
+ text = text.replace(/<command-message>[\s\S]*?<\/command-message>/g, '').trim();
368
+ text = text.replace(/<command-args>[\s\S]*?<\/command-args>/g, '').trim();
369
+ text = text.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, '').trim();
370
+ if (text.length < 5)
371
+ return '';
372
+ // 시스템/메타 메시지 스킵
373
+ if (text.startsWith('[Request interrupted'))
374
+ return '';
375
+ if (text.startsWith('This session is being continued'))
376
+ return '';
377
+ if (text.startsWith('No response requested'))
378
+ return '';
379
+ return text;
380
+ }
381
+ /**
382
+ * transcript에서 사용자 요청들 추출
383
+ * - firstRequest: 첫 사용자 요청 (세션 목적)
384
+ * - allRequests: 주요 사용자 메시지들 (세션 전체 요약용)
385
+ */
386
+ async function extractUserRequests(transcriptPath) {
387
+ if (!transcriptPath || !fs.existsSync(transcriptPath))
388
+ return { firstRequest: '', allRequests: [] };
389
+ const allRequests = [];
390
+ let firstRequest = '';
391
+ try {
392
+ const fileStream = fs.createReadStream(transcriptPath, { encoding: 'utf-8' });
393
+ const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
394
+ for await (const line of rl) {
395
+ if (!line.trim())
396
+ continue;
397
+ try {
398
+ const entry = JSON.parse(line);
399
+ if (entry.type !== 'human' && entry.type !== 'user')
400
+ continue;
401
+ const text = parseUserText(entry);
402
+ if (!text)
403
+ continue;
404
+ // "Implement the following plan:" → 플랜 제목만
405
+ const planMatch = text.match(/^Implement the following plan:\s*\n+#\s*(.+)/);
406
+ const cleaned = planMatch
407
+ ? planMatch[1].trim().slice(0, 100)
408
+ : stripMarkdown(text.split('\n')[0].trim()).slice(0, 100);
409
+ if (!cleaned || cleaned.length < 3)
410
+ continue;
411
+ if (!firstRequest)
412
+ firstRequest = cleaned;
413
+ allRequests.push(cleaned);
414
+ }
415
+ catch { /* skip */ }
416
+ }
417
+ }
418
+ catch { /* file read error */ }
419
+ return { firstRequest, allRequests };
420
+ }
421
+ /**
422
+ * 사용자 메시지들을 세션 요약으로 압축
423
+ * 예: ["MCP 테스트해줘", "개선해줘", "npm 배포하고 커밋해줘"] → "MCP 테스트 + 개선 + npm 배포/커밋"
424
+ */
425
+ function summarizeUserRequests(requests) {
426
+ if (requests.length === 0)
427
+ return '';
428
+ if (requests.length === 1)
429
+ return requests[0];
430
+ // 중복/유사 요청 제거 (앞 20글자 기준)
431
+ const unique = [];
432
+ const seen = new Set();
433
+ for (const req of requests) {
434
+ const key = req.slice(0, 20).toLowerCase();
435
+ if (!seen.has(key)) {
436
+ seen.add(key);
437
+ unique.push(req);
438
+ }
439
+ }
440
+ // 긴 세션: 첫 요청 + 마지막 2개 요청으로 세션 흐름 표현
441
+ if (unique.length > 5) {
442
+ const first = unique[0];
443
+ const last2 = unique.slice(-2);
444
+ const summary = `${first} ... ${last2.join(' + ')}`;
445
+ return summary.length > 250 ? summary.slice(0, 250) : summary;
446
+ }
447
+ // 짧은 세션: 전부 연결
448
+ const summary = unique.join(' + ');
449
+ return summary.length > 250 ? summary.slice(0, 250) : summary;
450
+ }
331
451
  /**
332
452
  * 최근 assistant 메시지에서 액션 동사 포함 라인 추출 (lastWork 폴백)
333
453
  */
@@ -381,20 +501,32 @@ async function main() {
381
501
  let errorsSolved = [];
382
502
  let decisions = [];
383
503
  // Phase 1: transcript_path에서 고품질 데이터 추출
504
+ let userRequests = { firstRequest: '', allRequests: [] };
384
505
  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);
506
+ [commitMessages, errorsSolved, decisions, userRequests] = await Promise.all([
507
+ extractCommitMessages(input.transcript_path),
508
+ extractErrorFixPairs(input.transcript_path),
509
+ extractDecisions(input.transcript_path),
510
+ extractUserRequests(input.transcript_path),
511
+ ]);
391
512
  }
392
513
  // Phase 2: lastWork 결정 (우선순위 폴백)
393
- // 2a: 커밋 메시지 기반 (가장 신뢰도 높음)
394
- if (commitMessages.length > 0) {
514
+ const { firstRequest, allRequests } = userRequests;
515
+ // 2a: 사용자 요청 + 커밋 메시지 조합 (가장 이상적)
516
+ if (firstRequest && commitMessages.length > 0) {
517
+ lastWork = `${firstRequest} → ${commitMessages.slice(0, 2).join('; ')}`;
518
+ if (lastWork.length > 250)
519
+ lastWork = lastWork.slice(0, 250);
520
+ }
521
+ // 2b: 커밋 메시지만 (사용자 요청 없을 때)
522
+ else if (commitMessages.length > 0) {
395
523
  lastWork = commitMessages.slice(0, 3).join('; ');
396
524
  }
397
- // 2b: last_assistant_message에서 추출
525
+ // 2c: 사용자 메시지 전체 요약 (커밋 없을 때)
526
+ else if (allRequests.length > 0) {
527
+ lastWork = summarizeUserRequests(allRequests);
528
+ }
529
+ // 2d: last_assistant_message에서 추출
398
530
  if (!lastWork && input.last_assistant_message) {
399
531
  lastWork = extractSummaryFromText(input.last_assistant_message);
400
532
  nextTasks = extractNextTasks(input.last_assistant_message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-continuity-mcp",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
4
4
  "description": "Session Continuity for Claude Code - Never re-explain your project again",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",