@ekkos/cli 0.2.1 → 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 +123 -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, spawn Claude directly
735
- // IMPORTANT: npx mode doesn't work well on Windows because Claude Code
736
- // detects non-TTY and defaults to --print mode expecting stdin input
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('═══════════════════════════════════════════════════════════════════'));
@@ -746,18 +829,38 @@ async function runWithSpawn(claudePath, args, options, state) {
746
829
  console.log(chalk_1.default.yellow(' Then try again:'));
747
830
  console.log(chalk_1.default.cyan(' ekkos run -b'));
748
831
  console.log('');
749
- console.log(chalk_1.default.gray(' Why? Windows spawn mode doesn\'t provide a proper TTY,'));
750
- console.log(chalk_1.default.gray(' causing Claude Code to expect piped input instead of interactive.'));
832
+ process.exit(1);
833
+ }
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
838
+ const fullCmd = args.length > 0
839
+ ? `"${claudePath}" ${args.join(' ')}`
840
+ : `"${claudePath}"`;
841
+ // spawnSync with stdio: 'inherit' = full console passthrough
842
+ // This is the ONLY way to keep Claude TUI working on Windows without PTY
843
+ const { spawnSync } = require('child_process');
844
+ try {
845
+ const result = spawnSync('cmd.exe', ['/c', fullCmd], {
846
+ stdio: 'inherit', // CRITICAL: Full passthrough, no piping
847
+ cwd: process.cwd(),
848
+ env: process.env,
849
+ windowsHide: false,
850
+ shell: false
851
+ });
852
+ (0, state_1.clearAutoClearFlag)();
853
+ process.exit(result.status || 0);
854
+ }
855
+ catch (err) {
856
+ console.error(chalk_1.default.red('Failed to launch Claude:'), err.message);
857
+ console.log('');
858
+ console.log(chalk_1.default.yellow('Try running claude directly:'));
859
+ console.log(chalk_1.default.cyan(' claude'));
751
860
  console.log('');
752
861
  process.exit(1);
753
862
  }
754
- console.log(chalk_1.default.gray('Windows mode: running Claude directly'));
755
- claude = (0, child_process_1.spawn)(claudePath, args, {
756
- stdio: 'inherit', // Full inherit for proper interactive mode
757
- cwd: process.cwd(),
758
- env: process.env,
759
- shell: true
760
- });
863
+ return; // Unreachable due to spawnSync + process.exit, but explicit
761
864
  }
762
865
  else {
763
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.1",
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": {