@tekmidian/pai 0.5.4 → 0.5.6

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.
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/hooks/ts/pre-compact/context-compression-hook.ts
10
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
10
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
11
11
  import { basename as basename2, dirname, join as join3 } from "path";
12
12
  import { tmpdir } from "os";
13
13
 
@@ -330,6 +330,35 @@ function addWorkToSessionNote(notePath, workItems, sectionTitle) {
330
330
  writeFileSync(notePath, content);
331
331
  console.error(`Added ${workItems.length} work item(s) to: ${basename(notePath)}`);
332
332
  }
333
+ function renameSessionNote(notePath, meaningfulName) {
334
+ if (!meaningfulName || !existsSync2(notePath)) {
335
+ return notePath;
336
+ }
337
+ const dir = join2(notePath, "..");
338
+ const oldFilename = basename(notePath);
339
+ const correctMatch = oldFilename.match(/^(\d{3,4}) - (\d{4}-\d{2}-\d{2}) - .*\.md$/);
340
+ const legacyMatch = oldFilename.match(/^(\d{3,4})_(\d{4}-\d{2}-\d{2})_.*\.md$/);
341
+ const match = correctMatch || legacyMatch;
342
+ if (!match) {
343
+ return notePath;
344
+ }
345
+ const [, noteNumber, date] = match;
346
+ const titleCaseName = meaningfulName.split(/[\s_-]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ").trim();
347
+ const paddedNumber = noteNumber.padStart(4, "0");
348
+ const newFilename = `${paddedNumber} - ${date} - ${titleCaseName}.md`;
349
+ const newPath = join2(dir, newFilename);
350
+ if (newFilename === oldFilename) {
351
+ return notePath;
352
+ }
353
+ try {
354
+ renameSync(notePath, newPath);
355
+ console.error(`Renamed note: ${oldFilename} \u2192 ${newFilename}`);
356
+ return newPath;
357
+ } catch (error) {
358
+ console.error(`Could not rename note: ${error}`);
359
+ return notePath;
360
+ }
361
+ }
333
362
  function calculateSessionTokens(jsonlPath) {
334
363
  if (!existsSync2(jsonlPath)) {
335
364
  return 0;
@@ -397,39 +426,38 @@ function ensureTodoMd(cwd) {
397
426
  }
398
427
  return todoPath;
399
428
  }
400
- function addTodoCheckpoint(cwd, checkpoint) {
429
+ function updateTodoContinue(cwd, noteFilename, state, tokenDisplay) {
401
430
  const todoPath = ensureTodoMd(cwd);
402
431
  let content = readFileSync2(todoPath, "utf-8");
403
- content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
404
- const checkpointText = `
405
- **Checkpoint (${(/* @__PURE__ */ new Date()).toISOString()}):** ${checkpoint}
432
+ content = content.replace(/## Continue\n[\s\S]*?\n---\n+/, "");
433
+ const now = (/* @__PURE__ */ new Date()).toISOString();
434
+ const stateLines = state ? state.split("\n").filter((l) => l.trim()).slice(0, 10).map((l) => `> ${l}`).join("\n") : `> Check the latest session note for details.`;
435
+ const continueSection = `## Continue
436
+
437
+ > **Last session:** ${noteFilename.replace(".md", "")}
438
+ > **Paused at:** ${now}
439
+ >
440
+ ${stateLines}
441
+
442
+ ---
406
443
 
407
444
  `;
408
- const backlogIndex = content.indexOf("## Backlog");
409
- if (backlogIndex !== -1) {
410
- content = content.substring(0, backlogIndex) + checkpointText + content.substring(backlogIndex);
445
+ content = content.replace(/^\s+/, "");
446
+ const titleMatch = content.match(/^(# [^\n]+\n+)/);
447
+ if (titleMatch) {
448
+ content = titleMatch[1] + continueSection + content.substring(titleMatch[0].length);
411
449
  } else {
412
- const continueIndex = content.indexOf("## Continue");
413
- if (continueIndex !== -1) {
414
- const afterContinue = content.indexOf("\n---", continueIndex);
415
- if (afterContinue !== -1) {
416
- const insertAt = afterContinue + 4;
417
- content = content.substring(0, insertAt) + "\n" + checkpointText + content.substring(insertAt);
418
- } else {
419
- content = content.trimEnd() + "\n" + checkpointText;
420
- }
421
- } else {
422
- content = content.trimEnd() + "\n" + checkpointText;
423
- }
450
+ content = continueSection + content;
424
451
  }
452
+ content = content.replace(/(\n---\s*)*(\n\*Last updated:.*\*\s*)+$/g, "");
425
453
  content = content.trimEnd() + `
426
454
 
427
455
  ---
428
456
 
429
- *Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
457
+ *Last updated: ${now}*
430
458
  `;
431
459
  writeFileSync(todoPath, content);
432
- console.error(`Checkpoint added to TODO.md`);
460
+ console.error("TODO.md ## Continue section updated");
433
461
  }
434
462
 
435
463
  // src/hooks/ts/pre-compact/context-compression-hook.ts
@@ -466,15 +494,19 @@ function getTranscriptStats(transcriptPath) {
466
494
  return { messageCount: 0, isLarge: false };
467
495
  }
468
496
  }
469
- function extractSessionState(transcriptPath, cwd) {
497
+ function parseTranscript(transcriptPath) {
498
+ const data = {
499
+ userMessages: [],
500
+ summaries: [],
501
+ captures: [],
502
+ lastCompleted: "",
503
+ filesModified: [],
504
+ workItems: []
505
+ };
470
506
  try {
471
507
  const raw = readFileSync3(transcriptPath, "utf-8");
472
508
  const lines = raw.trim().split("\n");
473
- const userMessages = [];
474
- const summaries = [];
475
- const captures = [];
476
- let lastCompleted = "";
477
- const filesModified = /* @__PURE__ */ new Set();
509
+ const seenSummaries = /* @__PURE__ */ new Set();
478
510
  for (const line of lines) {
479
511
  if (!line.trim()) continue;
480
512
  let entry;
@@ -485,123 +517,164 @@ function extractSessionState(transcriptPath, cwd) {
485
517
  }
486
518
  if (entry.type === "user" && entry.message?.content) {
487
519
  const text = contentToText(entry.message.content).slice(0, 300);
488
- if (text) userMessages.push(text);
520
+ if (text) data.userMessages.push(text);
489
521
  }
490
522
  if (entry.type === "assistant" && entry.message?.content) {
491
523
  const text = contentToText(entry.message.content);
492
524
  const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
493
525
  if (summaryMatch) {
494
526
  const s = summaryMatch[1].trim();
495
- if (s.length > 5 && !summaries.includes(s)) summaries.push(s);
527
+ if (s.length > 5 && !data.summaries.includes(s)) {
528
+ data.summaries.push(s);
529
+ if (!seenSummaries.has(s)) {
530
+ seenSummaries.add(s);
531
+ const details = [];
532
+ const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
533
+ if (actionsMatch) {
534
+ const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
535
+ details.push(...actionLines.slice(0, 3));
536
+ }
537
+ data.workItems.push({ title: s, details: details.length > 0 ? details : void 0, completed: true });
538
+ }
539
+ }
496
540
  }
497
541
  const captureMatch = text.match(/CAPTURE:\s*(.+?)(?:\n|$)/i);
498
542
  if (captureMatch) {
499
543
  const c = captureMatch[1].trim();
500
- if (c.length > 5 && !captures.includes(c)) captures.push(c);
544
+ if (c.length > 5 && !data.captures.includes(c)) data.captures.push(c);
501
545
  }
502
546
  const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
503
547
  if (completedMatch) {
504
- lastCompleted = completedMatch[1].trim().replace(/\*+/g, "");
548
+ data.lastCompleted = completedMatch[1].trim().replace(/\*+/g, "");
549
+ if (data.workItems.length === 0 && !seenSummaries.has(data.lastCompleted) && data.lastCompleted.length > 5) {
550
+ seenSummaries.add(data.lastCompleted);
551
+ data.workItems.push({ title: data.lastCompleted, completed: true });
552
+ }
505
553
  }
506
- }
507
- if (entry.type === "assistant" && entry.message?.content && Array.isArray(entry.message.content)) {
508
- for (const block of entry.message.content) {
509
- if (block.type === "tool_use") {
510
- const tool = block.name;
511
- if ((tool === "Edit" || tool === "Write") && block.input?.file_path) {
512
- filesModified.add(block.input.file_path);
554
+ if (Array.isArray(entry.message.content)) {
555
+ for (const block of entry.message.content) {
556
+ if (block.type === "tool_use") {
557
+ const tool = block.name;
558
+ if ((tool === "Edit" || tool === "Write") && block.input?.file_path) {
559
+ if (!data.filesModified.includes(block.input.file_path)) {
560
+ data.filesModified.push(block.input.file_path);
561
+ }
562
+ }
513
563
  }
514
564
  }
515
565
  }
516
566
  }
517
567
  }
518
- const parts = [];
519
- if (cwd) {
520
- parts.push(`Working directory: ${cwd}`);
521
- }
522
- const recentUser = userMessages.slice(-3);
523
- if (recentUser.length > 0) {
524
- parts.push("\nRecent user requests:");
525
- for (const msg of recentUser) {
526
- const firstLine = msg.split("\n")[0].slice(0, 200);
527
- parts.push(`- ${firstLine}`);
528
- }
529
- }
530
- const recentSummaries = summaries.slice(-3);
531
- if (recentSummaries.length > 0) {
532
- parts.push("\nWork summaries:");
533
- for (const s of recentSummaries) {
534
- parts.push(`- ${s.slice(0, 150)}`);
535
- }
536
- }
537
- const recentCaptures = captures.slice(-5);
538
- if (recentCaptures.length > 0) {
539
- parts.push("\nCaptured context:");
540
- for (const c of recentCaptures) {
541
- parts.push(`- ${c.slice(0, 150)}`);
542
- }
568
+ } catch (err) {
569
+ console.error(`parseTranscript error: ${err}`);
570
+ }
571
+ return data;
572
+ }
573
+ function formatSessionState(data, cwd) {
574
+ const parts = [];
575
+ if (cwd) parts.push(`Working directory: ${cwd}`);
576
+ const recentUser = data.userMessages.slice(-3);
577
+ if (recentUser.length > 0) {
578
+ parts.push("\nRecent user requests:");
579
+ for (const msg of recentUser) {
580
+ parts.push(`- ${msg.split("\n")[0].slice(0, 200)}`);
543
581
  }
544
- const files = Array.from(filesModified).slice(-10);
545
- if (files.length > 0) {
546
- parts.push("\nFiles modified this session:");
547
- for (const f of files) {
548
- parts.push(`- ${f}`);
582
+ }
583
+ const recentSummaries = data.summaries.slice(-3);
584
+ if (recentSummaries.length > 0) {
585
+ parts.push("\nWork summaries:");
586
+ for (const s of recentSummaries) parts.push(`- ${s.slice(0, 150)}`);
587
+ }
588
+ const recentCaptures = data.captures.slice(-5);
589
+ if (recentCaptures.length > 0) {
590
+ parts.push("\nCaptured context:");
591
+ for (const c of recentCaptures) parts.push(`- ${c.slice(0, 150)}`);
592
+ }
593
+ const files = data.filesModified.slice(-10);
594
+ if (files.length > 0) {
595
+ parts.push("\nFiles modified this session:");
596
+ for (const f of files) parts.push(`- ${f}`);
597
+ }
598
+ if (data.lastCompleted) {
599
+ parts.push(`
600
+ Last completed: ${data.lastCompleted.slice(0, 150)}`);
601
+ }
602
+ const result = parts.join("\n");
603
+ return result.length > 50 ? result : null;
604
+ }
605
+ function deriveTitle(data) {
606
+ let title = "";
607
+ if (data.workItems.length > 0) {
608
+ title = data.workItems[data.workItems.length - 1].title;
609
+ } else if (data.summaries.length > 0) {
610
+ title = data.summaries[data.summaries.length - 1];
611
+ } else if (data.lastCompleted && data.lastCompleted.length > 5) {
612
+ title = data.lastCompleted;
613
+ } else if (data.userMessages.length > 0) {
614
+ for (let i = data.userMessages.length - 1; i >= 0; i--) {
615
+ const msg = data.userMessages[i].split("\n")[0].trim();
616
+ if (msg.length > 10 && msg.length < 80 && !msg.toLowerCase().startsWith("yes") && !msg.toLowerCase().startsWith("ok")) {
617
+ title = msg;
618
+ break;
549
619
  }
550
620
  }
551
- if (lastCompleted) {
552
- parts.push(`
553
- Last completed: ${lastCompleted.slice(0, 150)}`);
554
- }
555
- const result = parts.join("\n");
556
- return result.length > 50 ? result : null;
557
- } catch (err) {
558
- console.error(`extractSessionState error: ${err}`);
559
- return null;
560
621
  }
622
+ if (!title && data.filesModified.length > 0) {
623
+ const basenames = data.filesModified.slice(-5).map((f) => {
624
+ const b = basename2(f);
625
+ return b.replace(/\.[^.]+$/, "");
626
+ });
627
+ const unique = [...new Set(basenames)];
628
+ title = unique.length <= 3 ? `Updated ${unique.join(", ")}` : `Modified ${data.filesModified.length} files`;
629
+ }
630
+ return title.replace(/[^\w\s-]/g, " ").replace(/\s+/g, " ").trim().substring(0, 60);
561
631
  }
562
- function extractWorkFromTranscript(transcriptPath) {
632
+ var CUMULATIVE_STATE_FILE = ".compact-state.json";
633
+ function loadCumulativeState(notesDir) {
563
634
  try {
564
- const raw = readFileSync3(transcriptPath, "utf-8");
565
- const lines = raw.trim().split("\n");
566
- const workItems = [];
567
- const seenSummaries = /* @__PURE__ */ new Set();
568
- for (const line of lines) {
569
- if (!line.trim()) continue;
570
- let entry;
571
- try {
572
- entry = JSON.parse(line);
573
- } catch {
574
- continue;
575
- }
576
- if (entry.type === "assistant" && entry.message?.content) {
577
- const text = contentToText(entry.message.content);
578
- const summaryMatch = text.match(/SUMMARY:\s*(.+?)(?:\n|$)/i);
579
- if (summaryMatch) {
580
- const summary = summaryMatch[1].trim();
581
- if (summary && !seenSummaries.has(summary) && summary.length > 5) {
582
- seenSummaries.add(summary);
583
- const details = [];
584
- const actionsMatch = text.match(/ACTIONS:\s*(.+?)(?=\n[A-Z]+:|$)/is);
585
- if (actionsMatch) {
586
- const actionLines = actionsMatch[1].split("\n").map((l) => l.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim()).filter((l) => l.length > 3 && l.length < 100);
587
- details.push(...actionLines.slice(0, 3));
588
- }
589
- workItems.push({ title: summary, details: details.length > 0 ? details : void 0, completed: true });
590
- }
591
- }
592
- const completedMatch = text.match(/COMPLETED:\s*(.+?)(?:\n|$)/i);
593
- if (completedMatch && workItems.length === 0) {
594
- const completed = completedMatch[1].trim().replace(/\*+/g, "");
595
- if (completed && !seenSummaries.has(completed) && completed.length > 5) {
596
- seenSummaries.add(completed);
597
- workItems.push({ title: completed, completed: true });
598
- }
599
- }
600
- }
601
- }
602
- return workItems;
635
+ const filePath = join3(notesDir, CUMULATIVE_STATE_FILE);
636
+ if (!existsSync3(filePath)) return null;
637
+ const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
638
+ return {
639
+ userMessages: raw.userMessages || [],
640
+ summaries: raw.summaries || [],
641
+ captures: raw.captures || [],
642
+ lastCompleted: raw.lastCompleted || "",
643
+ filesModified: raw.filesModified || [],
644
+ workItems: raw.workItems || []
645
+ };
603
646
  } catch {
604
- return [];
647
+ return null;
648
+ }
649
+ }
650
+ function mergeTranscriptData(accumulated, current) {
651
+ if (!accumulated) return current;
652
+ const mergeArrays = (a, b) => {
653
+ const seen = new Set(a);
654
+ return [...a, ...b.filter((x) => !seen.has(x))];
655
+ };
656
+ const seenTitles = new Set(accumulated.workItems.map((w) => w.title));
657
+ const newWorkItems = current.workItems.filter((w) => !seenTitles.has(w.title));
658
+ return {
659
+ userMessages: mergeArrays(accumulated.userMessages, current.userMessages).slice(-20),
660
+ summaries: mergeArrays(accumulated.summaries, current.summaries),
661
+ captures: mergeArrays(accumulated.captures, current.captures),
662
+ lastCompleted: current.lastCompleted || accumulated.lastCompleted,
663
+ filesModified: mergeArrays(accumulated.filesModified, current.filesModified),
664
+ workItems: [...accumulated.workItems, ...newWorkItems]
665
+ };
666
+ }
667
+ function saveCumulativeState(notesDir, data, notePath) {
668
+ try {
669
+ const filePath = join3(notesDir, CUMULATIVE_STATE_FILE);
670
+ writeFileSync2(filePath, JSON.stringify({
671
+ ...data,
672
+ notePath,
673
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
674
+ }, null, 2));
675
+ console.error(`Cumulative state saved (${data.workItems.length} work items, ${data.filesModified.length} files)`);
676
+ } catch (err) {
677
+ console.error(`Failed to save cumulative state: ${err}`);
605
678
  }
606
679
  }
607
680
  async function main() {
@@ -629,10 +702,22 @@ async function main() {
629
702
  const stats = getTranscriptStats(hookInput.transcript_path);
630
703
  tokenCount = calculateSessionTokens(hookInput.transcript_path);
631
704
  const tokenDisplay = tokenCount > 1e3 ? `${Math.round(tokenCount / 1e3)}k` : String(tokenCount);
632
- const state = extractSessionState(hookInput.transcript_path, hookInput.cwd);
705
+ const data = parseTranscript(hookInput.transcript_path);
706
+ let notesInfo;
707
+ try {
708
+ notesInfo = hookInput.cwd ? findNotesDir(hookInput.cwd) : { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
709
+ } catch {
710
+ notesInfo = { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
711
+ }
712
+ const accumulated = loadCumulativeState(notesInfo.path);
713
+ const merged = mergeTranscriptData(accumulated, data);
714
+ const state = formatSessionState(merged, hookInput.cwd);
715
+ if (accumulated) {
716
+ console.error(`Loaded cumulative state: ${accumulated.workItems.length} work items, ${accumulated.filesModified.length} files from previous compaction(s)`);
717
+ }
718
+ let notePath = null;
633
719
  try {
634
- const notesInfo = hookInput.cwd ? findNotesDir(hookInput.cwd) : { path: join3(dirname(hookInput.transcript_path), "Notes"), isLocal: false };
635
- let notePath = getCurrentNotePath(notesInfo.path);
720
+ notePath = getCurrentNotePath(notesInfo.path);
636
721
  if (!notePath) {
637
722
  console.error("No session note found \u2014 creating one for checkpoint");
638
723
  notePath = createSessionNote(notesInfo.path, "Recovered Session");
@@ -650,30 +735,53 @@ async function main() {
650
735
 
651
736
  ${state}` : `Context compression triggered at ~${tokenDisplay} tokens with ${stats.messageCount} messages.`;
652
737
  appendCheckpoint(notePath, checkpointBody);
653
- const workItems = extractWorkFromTranscript(hookInput.transcript_path);
654
- if (workItems.length > 0) {
655
- addWorkToSessionNote(notePath, workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
656
- console.error(`Added ${workItems.length} work item(s) to session note`);
738
+ if (merged.workItems.length > 0) {
739
+ addWorkToSessionNote(notePath, merged.workItems, `Pre-Compact (~${tokenDisplay} tokens)`);
740
+ console.error(`Added ${merged.workItems.length} work item(s) to session note`);
741
+ }
742
+ const title = deriveTitle(merged);
743
+ if (title) {
744
+ const newPath = renameSessionNote(notePath, title);
745
+ if (newPath !== notePath) {
746
+ try {
747
+ let noteContent = readFileSync3(newPath, "utf-8");
748
+ noteContent = noteContent.replace(
749
+ /^(# Session \d+:)\s*.*$/m,
750
+ `$1 ${title}`
751
+ );
752
+ writeFileSync2(newPath, noteContent);
753
+ console.error(`Updated note H1 to match rename`);
754
+ } catch {
755
+ }
756
+ notePath = newPath;
757
+ }
657
758
  }
658
759
  console.error(`Rich checkpoint saved: ${basename2(notePath)}`);
659
760
  } catch (noteError) {
660
761
  console.error(`Could not save checkpoint: ${noteError}`);
661
762
  }
662
- if (hookInput.cwd && state) {
763
+ saveCumulativeState(notesInfo.path, merged, notePath);
764
+ if (hookInput.cwd && notePath) {
663
765
  try {
664
- addTodoCheckpoint(hookInput.cwd, `Pre-compact checkpoint (~${tokenDisplay} tokens):
665
- ${state}`);
666
- console.error("TODO.md checkpoint added");
766
+ const noteFilename = basename2(notePath);
767
+ updateTodoContinue(hookInput.cwd, noteFilename, state, tokenDisplay);
768
+ console.error("TODO.md ## Continue section updated");
667
769
  } catch (todoError) {
668
770
  console.error(`Could not update TODO.md: ${todoError}`);
669
771
  }
670
772
  }
671
- if (state && hookInput.session_id) {
773
+ if (hookInput.session_id) {
774
+ const stateText = state || `Working directory: ${hookInput.cwd || "unknown"}`;
775
+ const noteInfo = notePath ? `
776
+ SESSION NOTE: ${notePath}
777
+ If this note still has a generic title (e.g. "New Session", "Context Compression"),
778
+ rename it based on actual work done and add a rich summary.` : "";
672
779
  const injection = [
673
780
  "<system-reminder>",
674
781
  `SESSION STATE RECOVERED AFTER COMPACTION (${compactType}, ~${tokenDisplay} tokens)`,
675
782
  "",
676
- state,
783
+ stateText,
784
+ noteInfo,
677
785
  "",
678
786
  "IMPORTANT: This session state was captured before context compaction.",
679
787
  "Use it to maintain continuity. Continue the conversation from where",