@ekkos/cli 0.2.9 → 0.2.11

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 (44) hide show
  1. package/dist/agent/daemon.d.ts +86 -0
  2. package/dist/agent/daemon.js +297 -0
  3. package/dist/agent/pty-runner.d.ts +51 -0
  4. package/dist/agent/pty-runner.js +184 -0
  5. package/dist/cache/LocalSessionStore.d.ts +34 -21
  6. package/dist/cache/LocalSessionStore.js +169 -53
  7. package/dist/cache/capture.d.ts +19 -11
  8. package/dist/cache/capture.js +243 -76
  9. package/dist/cache/types.d.ts +14 -1
  10. package/dist/commands/agent.d.ts +44 -0
  11. package/dist/commands/agent.js +300 -0
  12. package/dist/commands/doctor.d.ts +10 -0
  13. package/dist/commands/doctor.js +175 -87
  14. package/dist/commands/hooks.d.ts +109 -0
  15. package/dist/commands/hooks.js +668 -0
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +357 -85
  18. package/dist/commands/setup-remote.d.ts +20 -0
  19. package/dist/commands/setup-remote.js +467 -0
  20. package/dist/index.js +116 -1
  21. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  22. package/dist/restore/RestoreOrchestrator.js +64 -22
  23. package/dist/utils/paths.d.ts +125 -0
  24. package/dist/utils/paths.js +283 -0
  25. package/dist/utils/state.d.ts +2 -0
  26. package/package.json +1 -1
  27. package/templates/ekkos-manifest.json +223 -0
  28. package/templates/helpers/json-parse.cjs +101 -0
  29. package/templates/hooks/assistant-response.ps1 +256 -0
  30. package/templates/hooks/assistant-response.sh +124 -64
  31. package/templates/hooks/session-start.ps1 +107 -2
  32. package/templates/hooks/session-start.sh +201 -166
  33. package/templates/hooks/stop.ps1 +124 -3
  34. package/templates/hooks/stop.sh +470 -843
  35. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  36. package/templates/hooks/user-prompt-submit.sh +403 -393
  37. package/templates/project-stubs/session-start.ps1 +63 -0
  38. package/templates/project-stubs/session-start.sh +55 -0
  39. package/templates/project-stubs/stop.ps1 +63 -0
  40. package/templates/project-stubs/stop.sh +55 -0
  41. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  42. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  43. package/templates/shared/hooks-enabled.json +22 -0
  44. package/templates/shared/session-words.json +45 -0
@@ -62,9 +62,10 @@ function getConfig(options) {
62
62
  postEnterDelayMs: options.postEnterDelayMs ??
63
63
  parseInt(process.env.EKKOS_POST_ENTER_DELAY_MS || '300', 10), // was 500
64
64
  clearWaitMs: options.clearWaitMs ??
65
- parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '2500', 10), // was 5000
65
+ parseInt(process.env.EKKOS_CLEAR_WAIT_MS || '1111', 10), // 1111 = symbolic ✨
66
66
  idlePromptMs: parseInt(process.env.EKKOS_IDLE_PROMPT_MS || '250', 10),
67
67
  paletteRetryMs: parseInt(process.env.EKKOS_PALETTE_RETRY_MS || '400', 10), // was 500
68
+ maxIdleWaitMs: parseInt(process.env.EKKOS_MAX_IDLE_WAIT_MS || '2000', 10), // was 10000
68
69
  debugLogPath: options.debugLogPath ??
69
70
  process.env.EKKOS_DEBUG_LOG_PATH ??
70
71
  path.join(os.homedir(), '.ekkos', 'auto-continue.debug.log')
@@ -167,7 +168,8 @@ async function typeSlowly(shell, text, charDelayMs) {
167
168
  * that hasn't changed for idlePromptMs. This prevents injecting while Claude
168
169
  * is busy generating ("Channelling...").
169
170
  */
170
- async function waitForIdlePrompt(getOutputBuffer, config, maxWaitMs = 10000) {
171
+ async function waitForIdlePrompt(getOutputBuffer, config) {
172
+ const maxWaitMs = config.maxIdleWaitMs;
171
173
  const startTime = Date.now();
172
174
  let lastOutput = '';
173
175
  let stableTime = 0;
@@ -380,9 +382,45 @@ async function emergencyCapture(transcriptPath, sessionId) {
380
382
  dlog('Warning: Could not capture context');
381
383
  }
382
384
  }
385
+ /**
386
+ * Generate a unique instance ID for this run
387
+ * Used for namespacing storage paths
388
+ */
389
+ function generateInstanceId() {
390
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
391
+ }
392
+ /**
393
+ * Write/update instance file
394
+ */
395
+ function writeInstanceFile(instanceId, data) {
396
+ const instancesDir = path.join(state_1.EKKOS_DIR, 'instances');
397
+ if (!fs.existsSync(instancesDir)) {
398
+ fs.mkdirSync(instancesDir, { recursive: true });
399
+ }
400
+ const instanceFile = path.join(instancesDir, `${instanceId}.json`);
401
+ fs.writeFileSync(instanceFile, JSON.stringify(data, null, 2));
402
+ }
403
+ /**
404
+ * Clean up instance file on exit
405
+ */
406
+ function cleanupInstanceFile(instanceId) {
407
+ try {
408
+ const instanceFile = path.join(state_1.EKKOS_DIR, 'instances', `${instanceId}.json`);
409
+ if (fs.existsSync(instanceFile)) {
410
+ fs.unlinkSync(instanceFile);
411
+ }
412
+ }
413
+ catch {
414
+ // Ignore cleanup errors
415
+ }
416
+ }
383
417
  async function run(options) {
384
418
  const verbose = options.verbose || false;
385
419
  const bypass = options.bypass || false;
420
+ const noInject = options.noInject || false;
421
+ // Generate instance ID for this run
422
+ const instanceId = generateInstanceId();
423
+ process.env.EKKOS_INSTANCE_ID = instanceId;
386
424
  // ══════════════════════════════════════════════════════════════════════════
387
425
  // PRE-FLIGHT DIAGNOSTICS (--doctor flag)
388
426
  // ══════════════════════════════════════════════════════════════════════════
@@ -407,6 +445,72 @@ async function run(options) {
407
445
  const config = getConfig(options);
408
446
  setDebugLogPath(config.debugLogPath);
409
447
  // ══════════════════════════════════════════════════════════════════════════
448
+ // EARLY SETUP: Resolve paths and prepare spawn BEFORE animation
449
+ // This lets us start Claude while animations run (perceived performance)
450
+ // ══════════════════════════════════════════════════════════════════════════
451
+ (0, state_1.ensureEkkosDir)();
452
+ (0, state_1.clearAutoClearFlag)();
453
+ const rawClaudePath = resolveClaudePath();
454
+ const isNpxMode = rawClaudePath.startsWith('npx:');
455
+ const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
456
+ const claudePath = isNpxMode ? 'npx' : rawClaudePath;
457
+ // Build args early
458
+ const earlyArgs = [];
459
+ if (isNpxMode) {
460
+ earlyArgs.push(`@anthropic-ai/claude-code@${pinnedVersion}`);
461
+ }
462
+ if (bypass) {
463
+ earlyArgs.push('--dangerously-skip-permissions');
464
+ }
465
+ // Check PTY availability early
466
+ const usePty = pty !== null;
467
+ const monitorOnlyMode = noInject || (isWindows && !usePty);
468
+ // ══════════════════════════════════════════════════════════════════════════
469
+ // CONCURRENT STARTUP: Spawn Claude while animation runs
470
+ // Buffer output until animation completes, then flush
471
+ // ══════════════════════════════════════════════════════════════════════════
472
+ let earlySpawnedShell = null;
473
+ let bufferedOutput = [];
474
+ let animationComplete = false;
475
+ let shellReady = false;
476
+ let shellError = null;
477
+ // Handler reference for early spawn (allows replacing buffer handler with real handler)
478
+ let earlyDataHandler = null;
479
+ // Start spawning Claude NOW (before animation) if PTY available
480
+ if (usePty && pty && !isWindows) {
481
+ try {
482
+ earlySpawnedShell = pty.spawn(claudePath, earlyArgs, {
483
+ name: 'xterm-256color',
484
+ cols: process.stdout.columns || 80,
485
+ rows: process.stdout.rows || 24,
486
+ cwd: process.cwd(),
487
+ env: process.env
488
+ });
489
+ // Buffer output until animation completes using delegating handler
490
+ earlyDataHandler = (data) => {
491
+ bufferedOutput.push(data);
492
+ };
493
+ earlySpawnedShell.onData((data) => {
494
+ // Delegate to current handler (buffer during animation, real handler after)
495
+ if (earlyDataHandler) {
496
+ earlyDataHandler(data);
497
+ }
498
+ });
499
+ earlySpawnedShell.onExit(() => {
500
+ if (!animationComplete) {
501
+ // Claude exited during animation - will handle after
502
+ shellError = new Error('Claude exited during startup');
503
+ }
504
+ });
505
+ shellReady = true;
506
+ dlog('Claude spawned early (buffering output during animation)');
507
+ }
508
+ catch (err) {
509
+ dlog(`Early spawn failed: ${err.message}, will retry after animation`);
510
+ shellError = err;
511
+ }
512
+ }
513
+ // ══════════════════════════════════════════════════════════════════════════
410
514
  // STARTUP BANNER WITH COLOR PULSE ANIMATION
411
515
  // ══════════════════════════════════════════════════════════════════════════
412
516
  const logoLines = [
@@ -494,23 +598,121 @@ async function run(options) {
494
598
  process.stdout.write('\x1B[6A');
495
599
  logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
496
600
  console.log('');
497
- console.log(chalk_1.default.cyan.bold(' Auto-Continue Wrapper'));
498
- console.log(chalk_1.default.gray(' Monitors for context limit and auto-restores sessions'));
499
- console.log(chalk_1.default.gray(' Uses full 200K context window - triggers on actual limit'));
601
+ // ══════════════════════════════════════════════════════════════════════════
602
+ // ANIMATED TITLE: "Cognitive Continuity Engine" with orange/white shine
603
+ // ══════════════════════════════════════════════════════════════════════════
604
+ const titleText = 'Cognitive Continuity Engine';
605
+ const taglineText = 'Context is finite. Intelligence isn\'t.';
606
+ // Color palette for shine effect
607
+ const whiteShine = chalk_1.default.whiteBright;
608
+ // Phase 1: Typewriter effect for title
609
+ process.stdout.write(' ');
610
+ for (let i = 0; i < titleText.length; i++) {
611
+ const char = titleText[i];
612
+ // Flash white then settle to orange
613
+ process.stdout.write(whiteShine(char));
614
+ await sleep(25);
615
+ process.stdout.write('\b' + chalk_1.default.hex('#FF6B35').bold(char));
616
+ }
617
+ console.log('');
618
+ // Phase 2: Shine sweep across title (3 passes)
619
+ const SHINE_PASSES = 3;
620
+ const SHINE_WIDTH = 4;
621
+ for (let pass = 0; pass < SHINE_PASSES; pass++) {
622
+ for (let shinePos = -SHINE_WIDTH; shinePos <= titleText.length + SHINE_WIDTH; shinePos++) {
623
+ process.stdout.write('\x1B[1A'); // Move up one line
624
+ process.stdout.write('\r '); // Return to start
625
+ let output = '';
626
+ for (let i = 0; i < titleText.length; i++) {
627
+ const distFromShine = Math.abs(i - shinePos);
628
+ if (distFromShine === 0) {
629
+ output += whiteShine.bold(titleText[i]);
630
+ }
631
+ else if (distFromShine === 1) {
632
+ output += chalk_1.default.hex('#FFFFFF')(titleText[i]);
633
+ }
634
+ else if (distFromShine === 2) {
635
+ output += chalk_1.default.hex('#FFD700')(titleText[i]);
636
+ }
637
+ else if (distFromShine === 3) {
638
+ output += chalk_1.default.hex('#FFA500')(titleText[i]);
639
+ }
640
+ else {
641
+ output += chalk_1.default.hex('#FF6B35').bold(titleText[i]);
642
+ }
643
+ }
644
+ process.stdout.write(output + '\n'); // Write and move down for next frame
645
+ await sleep(15);
646
+ }
647
+ }
648
+ // Final title state
649
+ process.stdout.write('\x1B[1A\r');
650
+ console.log(' ' + chalk_1.default.hex('#FF6B35').bold(titleText));
651
+ // Phase 3: Tagline fade-in with shimmer
652
+ await sleep(100);
653
+ // Build up tagline with wave effect
654
+ const taglineColors = [
655
+ chalk_1.default.hex('#444444'),
656
+ chalk_1.default.hex('#666666'),
657
+ chalk_1.default.hex('#888888'),
658
+ chalk_1.default.hex('#AAAAAA'),
659
+ chalk_1.default.hex('#CCCCCC'),
660
+ chalk_1.default.hex('#EEEEEE'),
661
+ chalk_1.default.gray,
662
+ ];
663
+ for (let wave = 0; wave < taglineColors.length; wave++) {
664
+ process.stdout.write('\r ');
665
+ process.stdout.write(taglineColors[wave](taglineText));
666
+ await sleep(40);
667
+ }
668
+ console.log('');
669
+ // Phase 4: Quick orange accent pulse on tagline
670
+ for (let pulse = 0; pulse < 2; pulse++) {
671
+ await sleep(80);
672
+ process.stdout.write('\x1B[1A\r');
673
+ console.log(' ' + chalk_1.default.hex('#FF8C00')(taglineText));
674
+ await sleep(80);
675
+ process.stdout.write('\x1B[1A\r');
676
+ console.log(' ' + chalk_1.default.gray(taglineText));
677
+ }
678
+ // Final tagline state with subtle orange tint
679
+ process.stdout.write('\x1B[1A\r');
680
+ console.log(' ' + chalk_1.default.hex('#B8860B')(taglineText));
681
+ console.log('');
500
682
  if (bypass) {
501
683
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
502
684
  }
503
685
  if (verbose) {
504
686
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
505
- console.log(chalk_1.default.gray(` ⏱ Timing: slash=${config.slashOpenDelayMs}ms, char=${config.charDelayMs}ms, clear=${config.clearWaitMs}ms`));
687
+ console.log(chalk_1.default.gray(` ⏱ Timing: clear=${config.clearWaitMs}ms, idleMax=${config.maxIdleWaitMs}ms (~${Math.round((config.clearWaitMs + config.maxIdleWaitMs * 2 + 1700) / 1000)}s total)`));
506
688
  }
507
689
  console.log('');
508
- // Ensure .ekkos directory exists
509
- (0, state_1.ensureEkkosDir)();
510
- // Clear any stale flags
511
- (0, state_1.clearAutoClearFlag)();
690
+ // ══════════════════════════════════════════════════════════════════════════
691
+ // ANIMATION COMPLETE: Mark ready and flush buffered Claude output
692
+ // ══════════════════════════════════════════════════════════════════════════
693
+ animationComplete = true;
694
+ dlog(`Animation complete. shellReady=${shellReady}, buffered=${bufferedOutput.length} chunks`);
695
+ // Show loading indicator if Claude is still initializing
696
+ if (shellReady && bufferedOutput.length === 0) {
697
+ process.stdout.write(chalk_1.default.gray(' Connecting to Claude...'));
698
+ // Brief wait for any buffered output
699
+ await sleep(100);
700
+ process.stdout.write('\r' + ' '.repeat(30) + '\r'); // Clear the line
701
+ }
512
702
  // Track state
513
703
  let currentSession = options.session || (0, state_1.getCurrentSessionName)();
704
+ // Write initial instance file
705
+ const startedAt = new Date().toISOString();
706
+ writeInstanceFile(instanceId, {
707
+ pid: process.pid,
708
+ projectPath: process.cwd(),
709
+ startedAt,
710
+ lastHeartbeat: startedAt,
711
+ sessionName: currentSession || undefined
712
+ });
713
+ if (verbose) {
714
+ console.log(chalk_1.default.gray(` 🆔 Instance: ${instanceId}`));
715
+ }
514
716
  // ════════════════════════════════════════════════════════════════════════════
515
717
  // MULTI-SESSION SUPPORT: Register this process as an active session
516
718
  // This prevents state collision when multiple Claude Code instances run
@@ -530,7 +732,8 @@ async function run(options) {
530
732
  let currentSessionId = null;
531
733
  // Stream tailer for mid-turn context capture
532
734
  let streamTailer = null;
533
- const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
735
+ // Instance-namespaced cache directory per spec v1.2
736
+ const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions', instanceId);
534
737
  // Helper to start stream tailer when we have transcript path
535
738
  function startStreamTailer(tPath, sId, sName) {
536
739
  if (streamTailer)
@@ -576,103 +779,110 @@ async function run(options) {
576
779
  // Debounce tracking to prevent double triggers
577
780
  let lastDetectionTime = 0;
578
781
  const DETECTION_COOLDOWN = 30000; // 30 seconds cooldown
579
- // Resolve claude path
580
- const rawClaudePath = resolveClaudePath();
581
- // Handle npx:VERSION format for pinned version
582
- const isNpxMode = rawClaudePath.startsWith('npx:');
583
- const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
584
- const claudePath = isNpxMode ? 'npx' : rawClaudePath;
585
- if (verbose) {
586
- if (isNpxMode) {
587
- console.log(chalk_1.default.gray(` 🤖 Using claude-code@${pinnedVersion} via npx (pinned for better context)`));
588
- }
589
- else {
590
- console.log(chalk_1.default.gray(` 🤖 Using claude at: ${claudePath}`));
591
- }
592
- if (currentSession) {
593
- console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
594
- }
595
- }
596
- // Build args - prepend package name if using npx
597
- const args = [];
598
- if (isNpxMode) {
599
- args.push(`@anthropic-ai/claude-code@${pinnedVersion}`);
600
- }
601
- if (bypass) {
602
- args.push('--dangerously-skip-permissions');
603
- }
604
- // Determine which mode to use
605
- const usePty = pty !== null;
782
+ // Use args from early setup
783
+ const args = earlyArgs;
606
784
  // ══════════════════════════════════════════════════════════════════════════
607
- // WINDOWS: HARD FAIL WITHOUT PTY
785
+ // WINDOWS: MONITOR-ONLY MODE WITHOUT PTY (Per Spec v1.2 Addendum)
608
786
  // Without node-pty/ConPTY, auto-continue cannot work on Windows.
609
- // Claude switches to --print mode without a real PTY, breaking the TUI.
787
+ // Instead of hard-failing, we enter monitor-only mode.
610
788
  // ══════════════════════════════════════════════════════════════════════════
611
789
  if (isWindows && !usePty) {
612
790
  console.log('');
613
- console.log(chalk_1.default.red.bold(' ekkos run requires PTY support on Windows'));
791
+ console.log(chalk_1.default.yellow.bold('⚠️ Monitor-only mode (PTY not available)'));
614
792
  console.log('');
615
- console.log(chalk_1.default.yellow('Without node-pty (ConPTY), Claude Code runs in --print mode'));
616
- console.log(chalk_1.default.yellow('which prevents auto-continue from working.'));
793
+ console.log(chalk_1.default.gray('Without node-pty (ConPTY), auto-continue cannot inject commands.'));
794
+ console.log(chalk_1.default.gray('ekkOS will monitor context usage and provide instructions when needed.'));
617
795
  console.log('');
618
- console.log(chalk_1.default.cyan('To fix:'));
619
- console.log('');
620
- console.log(chalk_1.default.white(' Option 1: Use Node 20 or 22 LTS (recommended)'));
796
+ console.log(chalk_1.default.cyan('To enable auto-continue:'));
797
+ console.log(chalk_1.default.white(' Option 1: Use Node 20 or 22 LTS'));
621
798
  console.log(chalk_1.default.gray(' winget install OpenJS.NodeJS.LTS'));
622
- console.log(chalk_1.default.gray(' npm install -g @ekkos/cli'));
623
- console.log('');
624
- console.log(chalk_1.default.white(' Option 2: Install prebuilt PTY'));
625
- console.log(chalk_1.default.gray(' npm install node-pty-prebuilt-multiarch'));
626
- console.log('');
627
- console.log(chalk_1.default.white(' Option 3: Build node-pty from source'));
628
- console.log(chalk_1.default.gray(' 1. Install VS Build Tools (Desktop C++ workload)'));
629
- console.log(chalk_1.default.gray(' 2. npm rebuild node-pty --build-from-source'));
799
+ console.log(chalk_1.default.white(' Option 2: npm install node-pty-prebuilt-multiarch'));
630
800
  console.log('');
631
801
  console.log(chalk_1.default.gray('Run `ekkos doctor` for detailed diagnostics.'));
632
802
  console.log('');
633
- process.exit(1);
803
+ }
804
+ else if (noInject) {
805
+ console.log(chalk_1.default.yellow(' Monitor-only mode (--no-inject)'));
634
806
  }
635
807
  if (verbose) {
808
+ if (isNpxMode) {
809
+ console.log(chalk_1.default.gray(` 🤖 Using claude-code@${pinnedVersion} via npx (pinned for better context)`));
810
+ }
811
+ else {
812
+ console.log(chalk_1.default.gray(` 🤖 Using claude at: ${claudePath}`));
813
+ }
814
+ if (currentSession) {
815
+ console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
816
+ }
636
817
  console.log(chalk_1.default.gray(` 💻 PTY mode: ${usePty ? 'node-pty' : 'spawn+script (fallback)'}`));
637
818
  console.log('');
638
819
  }
639
820
  let shell;
821
+ // ══════════════════════════════════════════════════════════════════════════
822
+ // USE EARLY-SPAWNED SHELL OR CREATE NEW ONE
823
+ // If we spawned Claude during animation, reuse it. Otherwise spawn now.
824
+ // ══════════════════════════════════════════════════════════════════════════
640
825
  if (usePty && pty) {
641
- // Use node-pty for proper PTY control
642
- try {
643
- const ptyShell = pty.spawn(claudePath, args, {
644
- name: 'xterm-256color',
645
- cols: process.stdout.columns || 80,
646
- rows: process.stdout.rows || 24,
647
- cwd: process.cwd(),
648
- env: process.env
649
- });
826
+ if (earlySpawnedShell && shellReady && !shellError) {
827
+ // REUSE early-spawned shell - Claude loaded during animation!
828
+ dlog('Reusing early-spawned shell (Claude loaded during animation)');
650
829
  shell = {
651
- write: (data) => ptyShell.write(data),
652
- kill: () => ptyShell.kill(),
653
- resize: (cols, rows) => ptyShell.resize(cols, rows),
654
- onData: (callback) => ptyShell.onData(callback),
655
- onExit: (callback) => ptyShell.onExit(callback)
830
+ write: (data) => earlySpawnedShell.write(data),
831
+ kill: () => earlySpawnedShell.kill(),
832
+ resize: (cols, rows) => earlySpawnedShell.resize(cols, rows),
833
+ onData: (callback) => {
834
+ // Switch the delegating handler from buffer mode to real handler
835
+ // This replaces buffering with actual output processing
836
+ earlyDataHandler = callback;
837
+ },
838
+ onExit: (callback) => earlySpawnedShell.onExit(callback)
656
839
  };
657
- // Handle terminal resize
658
- process.stdout.on('resize', () => {
659
- shell.resize?.(process.stdout.columns || 80, process.stdout.rows || 24);
660
- });
840
+ // Flush buffered output NOW (before switching handler)
841
+ if (bufferedOutput.length > 0) {
842
+ dlog(`Flushing ${bufferedOutput.length} buffered output chunks`);
843
+ for (const chunk of bufferedOutput) {
844
+ process.stdout.write(chunk);
845
+ // Also populate output buffer for pattern detection
846
+ outputBuffer += chunk;
847
+ }
848
+ bufferedOutput = [];
849
+ }
661
850
  }
662
- catch (err) {
663
- console.log(chalk_1.default.yellow(`node-pty failed, falling back to spawn+script mode`));
664
- if (verbose) {
665
- console.log(chalk_1.default.gray(`Error: ${err.message}`));
851
+ else {
852
+ // Spawn fresh (early spawn failed or wasn't attempted)
853
+ dlog('Spawning Claude fresh (early spawn not available)');
854
+ try {
855
+ const ptyShell = pty.spawn(claudePath, args, {
856
+ name: 'xterm-256color',
857
+ cols: process.stdout.columns || 80,
858
+ rows: process.stdout.rows || 24,
859
+ cwd: process.cwd(),
860
+ env: process.env
861
+ });
862
+ shell = {
863
+ write: (data) => ptyShell.write(data),
864
+ kill: () => ptyShell.kill(),
865
+ resize: (cols, rows) => ptyShell.resize(cols, rows),
866
+ onData: (callback) => ptyShell.onData(callback),
867
+ onExit: (callback) => ptyShell.onExit(callback)
868
+ };
869
+ }
870
+ catch (err) {
871
+ dlog(`node-pty spawn failed: ${err.message}`);
872
+ // Fall through to spawn mode
873
+ return runWithSpawn(claudePath, args, options, {
874
+ currentSession,
875
+ isAutoClearInProgress,
876
+ transcriptPath,
877
+ currentSessionId,
878
+ outputBuffer
879
+ });
666
880
  }
667
- // Fall through to spawn mode
668
- return runWithSpawn(claudePath, args, options, {
669
- currentSession,
670
- isAutoClearInProgress,
671
- transcriptPath,
672
- currentSessionId,
673
- outputBuffer
674
- });
675
881
  }
882
+ // Handle terminal resize
883
+ process.stdout.on('resize', () => {
884
+ shell.resize?.(process.stdout.columns || 80, process.stdout.rows || 24);
885
+ });
676
886
  }
677
887
  else {
678
888
  // Fallback: use spawn+script for PTY emulation
@@ -977,11 +1187,72 @@ async function run(options) {
977
1187
  }
978
1188
  }
979
1189
  });
1190
+ // ══════════════════════════════════════════════════════════════════════════
1191
+ // RESEARCH MODE: Auto-type research prompt after Claude is ready
1192
+ // Triggers: `ekkos run -r` or `ekkos run --research`
1193
+ // Works like /clear continue - waits for idle prompt, then injects text
1194
+ // ══════════════════════════════════════════════════════════════════════════
1195
+ if (options.research) {
1196
+ dlog('Research mode enabled - will auto-inject research prompt');
1197
+ // Research prompt - comprehensive autonomous research scanning
1198
+ const researchPrompt = `You are the ekkOS Research Scout agent. Your task:
1199
+
1200
+ 1. **Scan arXiv** (cs.AI, cs.LG, cs.CL) for papers from the last 7 days
1201
+ 2. **Filter for relevance** to ekkOS memory systems:
1202
+ - Memory-augmented neural networks
1203
+ - Retrieval-augmented generation (RAG)
1204
+ - Long-term memory for LLMs
1205
+ - Pattern learning and extraction
1206
+ - Cognitive architectures
1207
+ - Knowledge graphs
1208
+ - Episodic/semantic memory systems
1209
+ 3. **Analyze each relevant paper**:
1210
+ - Core technique and novelty
1211
+ - Applicability to ekkOS 11-layer architecture
1212
+ - Implementation complexity (low/medium/high)
1213
+ - Potential impact (1-5 scale)
1214
+ 4. **Update docs/EKKOS_RESEARCH_ROADMAP.md** with findings
1215
+ 5. **Forge high-value insights as patterns** using ekkOS_Forge
1216
+
1217
+ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
1218
+ // Start research injection after a delay to let Claude initialize
1219
+ setTimeout(async () => {
1220
+ dlog('Starting research prompt injection...');
1221
+ // Wait for idle prompt (same gate as /clear continue uses)
1222
+ const readiness = await waitForIdlePrompt(getOutputBuffer, config);
1223
+ if (!readiness.ready || readiness.interrupted) {
1224
+ dlog('Claude not ready for research prompt - aborting');
1225
+ return;
1226
+ }
1227
+ // PAUSE STDIN during injection to prevent user interference
1228
+ process.stdin.off('data', onStdinData);
1229
+ dlog('Stdin paused during research injection');
1230
+ try {
1231
+ // Clear any existing input
1232
+ shell.write('\x15'); // Ctrl+U
1233
+ await sleep(60);
1234
+ // Type the research prompt character by character (faster for long prompts)
1235
+ dlog('Typing research prompt...');
1236
+ const fastCharDelay = Math.min(config.charDelayMs, 10); // Faster typing for long prompt
1237
+ await typeSlowly(shell, researchPrompt, fastCharDelay);
1238
+ // Send Enter to execute
1239
+ await sleep(100);
1240
+ shell.write('\r');
1241
+ dlog('Research prompt sent');
1242
+ }
1243
+ finally {
1244
+ // RESUME STDIN
1245
+ process.stdin.on('data', onStdinData);
1246
+ dlog('Stdin resumed after research injection');
1247
+ }
1248
+ }, 4000); // Wait 4 seconds for Claude to fully initialize
1249
+ }
980
1250
  // Handle PTY exit
981
1251
  shell.onExit(({ exitCode }) => {
982
1252
  (0, state_1.clearAutoClearFlag)();
983
1253
  stopStreamTailer(); // Stop stream capture
984
1254
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
1255
+ cleanupInstanceFile(instanceId); // Clean up instance file
985
1256
  // Restore terminal
986
1257
  if (process.stdin.isTTY) {
987
1258
  process.stdin.setRawMode(false);
@@ -996,6 +1267,7 @@ async function run(options) {
996
1267
  (0, state_1.clearAutoClearFlag)();
997
1268
  stopStreamTailer(); // Stop stream capture
998
1269
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
1270
+ cleanupInstanceFile(instanceId); // Clean up instance file
999
1271
  if (process.stdin.isTTY) {
1000
1272
  process.stdin.setRawMode(false);
1001
1273
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ekkos setup-remote - One-command setup for remote terminal access
3
+ *
4
+ * This command does everything automatically:
5
+ * 1. Checks if user is logged in (prompts login if not)
6
+ * 2. Generates unique device ID and fingerprint
7
+ * 3. Opens browser for device pairing approval
8
+ * 4. Installs background agent as system service
9
+ * 5. Starts agent immediately
10
+ * 6. Verifies connection to cloud relay
11
+ */
12
+ export interface SetupRemoteOptions {
13
+ force?: boolean;
14
+ skipService?: boolean;
15
+ verbose?: boolean;
16
+ }
17
+ /**
18
+ * Main setup command
19
+ */
20
+ export declare function setupRemote(options?: SetupRemoteOptions): Promise<void>;