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.
- package/dist/hooks/session-end.js +157 -25
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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.
|
|
236
|
+
if (!line.includes('git commit'))
|
|
230
237
|
continue;
|
|
231
238
|
try {
|
|
232
|
-
const
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
394
|
-
|
|
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
|
-
//
|
|
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);
|