@ekkos/cli 0.2.2 → 0.2.3

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.
Files changed (2) hide show
  1. package/dist/commands/run.js +102 -20
  2. package/package.json +1 -1
@@ -97,6 +97,15 @@ const INTERRUPTED_REGEX = /interrupted.*what should claude do instead/i;
97
97
  // Command palette indicator - when / is typed, Claude shows a menu
98
98
  const PALETTE_INDICATOR_REGEX = /\/(clear|continue|compact|help|bug|config)/i;
99
99
  // ═══════════════════════════════════════════════════════════════════════════
100
+ // SESSION NAME DETECTION (3-word slug: word-word-word)
101
+ // Claude prints session name in footer: · Turn N · groovy-koala-saves · 📅
102
+ // ═══════════════════════════════════════════════════════════════════════════
103
+ // Strong signal: session name between dot separators in Claude status/footer line
104
+ // Matches: "· groovy-koala-saves ·" or "· velvet-monk-skips ·"
105
+ const SESSION_NAME_IN_STATUS_REGEX = /·\s*([a-z]+-[a-z]+-[a-z]+)\s*·/i;
106
+ // Weaker signal: any 3-word slug (word-word-word pattern)
107
+ const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
108
+ // ═══════════════════════════════════════════════════════════════════════════
100
109
  // LOGGING (FILE ONLY DURING TUI - NO TERMINAL CORRUPTION)
101
110
  // ═══════════════════════════════════════════════════════════════════════════
102
111
  let _debugLogPath = path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log');
@@ -464,6 +473,14 @@ async function run(options) {
464
473
  let isAutoClearInProgress = false;
465
474
  let transcriptPath = null;
466
475
  let currentSessionId = null;
476
+ // ══════════════════════════════════════════════════════════════════════════
477
+ // SESSION NAME TRACKING (from live TUI output)
478
+ // Claude prints: "· Turn N · groovy-koala-saves · 📅"
479
+ // We parse this to always know the CURRENT session, not stale persisted state
480
+ // ══════════════════════════════════════════════════════════════════════════
481
+ let lastSeenSessionName = null;
482
+ let lastSeenSessionAt = 0;
483
+ const RECENCY_WINDOW_MS = 15000; // 15s - session name must be recent to trust
467
484
  // Output buffer for pattern detection
468
485
  let outputBuffer = '';
469
486
  // Debounce tracking to prevent double triggers
@@ -571,9 +588,24 @@ async function run(options) {
571
588
  dlog('Already in progress, skipping');
572
589
  return;
573
590
  }
574
- // CRITICAL: Capture session name IMMEDIATELY before any async operations
575
- // This prevents shell.onData from overwriting it with the NEW session after /clear
576
- let sessionToRestore = currentSession;
591
+ // ════════════════════════════════════════════════════════════════════════
592
+ // SESSION SELECTION: Prefer most recently SEEN session from TUI output
593
+ // This fixes the "groovy-koala-saves" bug where stale persisted state was used
594
+ // Priority: lastSeenSessionName (if recent) > currentSession > persisted state
595
+ // ════════════════════════════════════════════════════════════════════════
596
+ let sessionToRestore = null;
597
+ // PRIORITY 1: Most recently observed session name from TUI output
598
+ // Only trust if seen within RECENCY_WINDOW_MS (15 seconds)
599
+ if (lastSeenSessionName && (Date.now() - lastSeenSessionAt) < RECENCY_WINDOW_MS) {
600
+ sessionToRestore = lastSeenSessionName;
601
+ dlog(`Using lastSeenSessionName (${Date.now() - lastSeenSessionAt}ms ago): ${sessionToRestore}`);
602
+ }
603
+ // PRIORITY 2: currentSession (in-memory, may be stale)
604
+ if (!sessionToRestore && currentSession) {
605
+ sessionToRestore = currentSession;
606
+ dlog(`Using currentSession (in-memory): ${sessionToRestore}`);
607
+ }
608
+ // PRIORITY 3: Persisted state (fallback)
577
609
  if (!sessionToRestore) {
578
610
  const state = (0, state_1.getState)();
579
611
  sessionToRestore = state?.sessionName || null;
@@ -582,9 +614,12 @@ async function run(options) {
582
614
  if (!sessionToRestore && sessionId) {
583
615
  sessionToRestore = (0, state_1.uuidToWords)(sessionId);
584
616
  }
617
+ if (sessionToRestore) {
618
+ dlog(`Using persisted state (fallback): ${sessionToRestore}`);
619
+ }
585
620
  }
586
621
  const sessionDisplay = sessionToRestore || 'unknown-session';
587
- dlog(`Session to restore (captured before clear): ${sessionDisplay}`);
622
+ dlog(`Session to restore (final): ${sessionDisplay}`);
588
623
  // CRITICAL: Clear buffer and set flags immediately
589
624
  outputBuffer = '';
590
625
  lastDetectionTime = now;
@@ -671,13 +706,53 @@ async function run(options) {
671
706
  transcriptPath = transcriptMatch[1];
672
707
  dlog(`Detected transcript: ${transcriptPath}`);
673
708
  }
674
- // Try to extract session ID from output
709
+ // Try to extract session ID from output (fallback - Claude rarely prints this)
675
710
  const sessionMatch = data.match(/session[_\s]?(?:id)?[:\s]+([a-f0-9-]{36})/i);
676
711
  if (sessionMatch) {
677
712
  currentSessionId = sessionMatch[1];
678
713
  currentSession = (0, state_1.uuidToWords)(currentSessionId);
679
714
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
680
- dlog(`Session detected: ${currentSession}`);
715
+ dlog(`Session detected from UUID: ${currentSession}`);
716
+ }
717
+ // ════════════════════════════════════════════════════════════════════════
718
+ // SESSION NAME DETECTION (PRIMARY METHOD)
719
+ // Claude footer: "· Turn N · groovy-koala-saves · 📅 2026-01-17"
720
+ // This is MORE reliable than UUID extraction
721
+ // ════════════════════════════════════════════════════════════════════════
722
+ const plain = stripAnsi(data);
723
+ // Strong signal: session name between dot separators in status/footer line
724
+ const statusMatch = plain.match(SESSION_NAME_IN_STATUS_REGEX);
725
+ if (statusMatch) {
726
+ const detectedSession = statusMatch[1].toLowerCase();
727
+ // Only update if different (avoid log spam)
728
+ if (detectedSession !== lastSeenSessionName) {
729
+ lastSeenSessionName = detectedSession;
730
+ lastSeenSessionAt = Date.now();
731
+ currentSession = lastSeenSessionName;
732
+ (0, state_1.updateState)({ sessionName: currentSession });
733
+ dlog(`Session detected from status line: ${currentSession}`);
734
+ }
735
+ else {
736
+ // Same session, just update timestamp
737
+ lastSeenSessionAt = Date.now();
738
+ }
739
+ }
740
+ else {
741
+ // Weaker signal: any 3-word slug (only if no status match)
742
+ const anyMatch = plain.match(SESSION_NAME_REGEX);
743
+ if (anyMatch) {
744
+ const detectedSession = anyMatch[1].toLowerCase();
745
+ if (detectedSession !== lastSeenSessionName) {
746
+ lastSeenSessionName = detectedSession;
747
+ lastSeenSessionAt = Date.now();
748
+ currentSession = lastSeenSessionName;
749
+ (0, state_1.updateState)({ sessionName: currentSession });
750
+ dlog(`Session detected from generic match: ${currentSession}`);
751
+ }
752
+ else {
753
+ lastSeenSessionAt = Date.now();
754
+ }
755
+ }
681
756
  }
682
757
  // Check for context wall patterns (ANSI-stripped + regex for robustness)
683
758
  if (!isAutoClearInProgress) {
@@ -726,14 +801,22 @@ async function runWithSpawn(claudePath, args, options, state) {
726
801
  // Debounce tracking
727
802
  let lastDetectionTime = 0;
728
803
  const DETECTION_COOLDOWN = 30000;
729
- console.log(chalk_1.default.gray('Using spawn fallback mode'));
730
- console.log(chalk_1.default.yellow('Note: Auto-inject requires manual /clear + /continue'));
804
+ console.log(chalk_1.default.gray('Using spawn fallback mode (node-pty unavailable)'));
805
+ console.log(chalk_1.default.yellow('⚠️ Auto-continue requires PTY - manual /clear + /continue only'));
806
+ console.log(chalk_1.default.gray(' To enable auto-continue: npm rebuild node-pty'));
731
807
  console.log('');
732
808
  let claude;
733
809
  if (isWindows) {
734
- // On Windows, Node.js spawn doesn't provide proper TTY support
735
- // Claude Code detects non-TTY and defaults to --print mode
736
- // Solution: Use cmd.exe /c to run claude directly with proper console
810
+ // ══════════════════════════════════════════════════════════════════════════
811
+ // WINDOWS: Full TTY passthrough (no output monitoring, no auto-inject)
812
+ //
813
+ // Why: Without node-pty/ConPTY, we cannot simultaneously:
814
+ // 1. Keep Claude TUI interactive
815
+ // 2. Read output for context-wall detection
816
+ //
817
+ // Piping stdout triggers Claude's --print mode which breaks the TUI.
818
+ // Solution: stdio: 'inherit' for complete passthrough.
819
+ // ══════════════════════════════════════════════════════════════════════════
737
820
  if (claudePath === 'npx') {
738
821
  console.log('');
739
822
  console.log(chalk_1.default.red('═══════════════════════════════════════════════════════════════════'));
@@ -748,24 +831,24 @@ async function runWithSpawn(claudePath, args, options, state) {
748
831
  console.log('');
749
832
  process.exit(1);
750
833
  }
751
- console.log(chalk_1.default.gray('Windows mode: launching Claude with proper TTY...'));
752
- // Build the full command
834
+ console.log(chalk_1.default.gray('Windows mode: full TTY passthrough (no auto-inject)'));
835
+ console.log(chalk_1.default.gray('Context wall manual /clear + /continue <session>'));
836
+ console.log('');
837
+ // Build command for passthrough
753
838
  const fullCmd = args.length > 0
754
839
  ? `"${claudePath}" ${args.join(' ')}`
755
840
  : `"${claudePath}"`;
756
- // Use cmd.exe /c to get proper console behavior
757
- // This replaces the current process with claude
841
+ // spawnSync with stdio: 'inherit' = full console passthrough
842
+ // This is the ONLY way to keep Claude TUI working on Windows without PTY
758
843
  const { spawnSync } = require('child_process');
759
- // First try: Direct exec replacement
760
844
  try {
761
845
  const result = spawnSync('cmd.exe', ['/c', fullCmd], {
762
- stdio: 'inherit',
846
+ stdio: 'inherit', // CRITICAL: Full passthrough, no piping
763
847
  cwd: process.cwd(),
764
848
  env: process.env,
765
849
  windowsHide: false,
766
850
  shell: false
767
851
  });
768
- // If we get here, claude exited
769
852
  (0, state_1.clearAutoClearFlag)();
770
853
  process.exit(result.status || 0);
771
854
  }
@@ -777,8 +860,7 @@ async function runWithSpawn(claudePath, args, options, state) {
777
860
  console.log('');
778
861
  process.exit(1);
779
862
  }
780
- // This line won't be reached due to spawnSync + process.exit
781
- return;
863
+ return; // Unreachable due to spawnSync + process.exit, but explicit
782
864
  }
783
865
  else {
784
866
  // Use script command for PTY on Unix
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {