@ekkos/cli 1.0.34 → 1.0.36

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/capture/jsonl-rewriter.js +72 -7
  2. package/dist/commands/dashboard.js +186 -557
  3. package/dist/commands/init.js +3 -15
  4. package/dist/commands/run.js +222 -256
  5. package/dist/commands/setup.js +0 -47
  6. package/dist/commands/swarm-dashboard.js +4 -13
  7. package/dist/deploy/instructions.d.ts +2 -5
  8. package/dist/deploy/instructions.js +8 -11
  9. package/dist/deploy/settings.js +21 -15
  10. package/dist/deploy/skills.d.ts +0 -8
  11. package/dist/deploy/skills.js +0 -26
  12. package/dist/index.js +2 -2
  13. package/dist/lib/usage-parser.js +1 -2
  14. package/dist/utils/platform.d.ts +0 -3
  15. package/dist/utils/platform.js +1 -4
  16. package/dist/utils/session-binding.d.ts +1 -1
  17. package/dist/utils/session-binding.js +2 -3
  18. package/package.json +1 -1
  19. package/templates/agents/README.md +182 -0
  20. package/templates/agents/code-reviewer.md +166 -0
  21. package/templates/agents/debug-detective.md +169 -0
  22. package/templates/agents/ekkOS_Vercel.md +99 -0
  23. package/templates/agents/extension-manager.md +229 -0
  24. package/templates/agents/git-companion.md +185 -0
  25. package/templates/agents/github-test-agent.md +321 -0
  26. package/templates/agents/railway-manager.md +179 -0
  27. package/templates/hooks/assistant-response.ps1 +26 -94
  28. package/templates/hooks/lib/count-tokens.cjs +0 -0
  29. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  30. package/templates/hooks/session-start.ps1 +224 -61
  31. package/templates/hooks/session-start.sh +1 -1
  32. package/templates/hooks/stop.ps1 +249 -103
  33. package/templates/hooks/stop.sh +1 -1
  34. package/templates/hooks/user-prompt-submit.ps1 +519 -129
  35. package/templates/hooks/user-prompt-submit.sh +2 -2
  36. package/templates/plan-template.md +0 -0
  37. package/templates/spec-template.md +0 -0
  38. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  39. package/templates/windsurf-hooks/install.sh +0 -0
  40. package/templates/windsurf-hooks/lib/contract.sh +0 -0
  41. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  42. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  43. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  44. package/README.md +0 -57
@@ -43,6 +43,139 @@ const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
44
  const os = __importStar(require("os"));
45
45
  const child_process_1 = require("child_process");
46
+ // ═══════════════════════════════════════════════════════════════════════════
47
+ // ccDNA AUTO-LOAD: Apply Claude Code patches before spawning
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+ const CCDNA_PATHS = [
50
+ // Development path (DEV sibling directory)
51
+ // From: EKKOS/packages/ekkos-cli/dist/commands/ → DEV/ekkos-ccdna/
52
+ path.join(__dirname, '..', '..', '..', '..', '..', 'ekkos-ccdna', 'dist', 'index.mjs'),
53
+ // User install path
54
+ path.join(os.homedir(), '.ekkos', 'ccdna', 'dist', 'index.mjs'),
55
+ // npm global (homebrew)
56
+ '/opt/homebrew/lib/node_modules/ekkos-ccdna/dist/index.mjs',
57
+ // npm global (standard)
58
+ path.join(os.homedir(), '.npm-global', 'lib', 'node_modules', 'ekkos-ccdna', 'dist', 'index.mjs'),
59
+ ];
60
+ /**
61
+ * Find ccDNA installation path
62
+ */
63
+ function findCcdnaPath() {
64
+ for (const p of CCDNA_PATHS) {
65
+ if (fs.existsSync(p)) {
66
+ return p;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+ /**
72
+ * Apply ccDNA patches silently before Claude spawns
73
+ * Returns version string if patches were applied, null otherwise
74
+ *
75
+ * @param verbose - Show detailed output
76
+ * @param claudePath - Path to Claude Code to patch (if different from default)
77
+ */
78
+ function applyCcdnaPatches(verbose, claudePath) {
79
+ // DISABLED: ccDNA patching is currently corrupting cli.js (JSON parse error at position 7945)
80
+ // See: https://github.com/anthropics/ekkos/issues/2856
81
+ // The patching process is injecting code that breaks the minified cli.js
82
+ // Temporarily disabled until ccDNA is fixed upstream
83
+ if (verbose) {
84
+ console.log(chalk_1.default.gray(' ccDNA patching disabled (see issue #2856)'));
85
+ }
86
+ return null;
87
+ // Original implementation (disabled):
88
+ /*
89
+ const ccdnaPath = findCcdnaPath();
90
+ if (!ccdnaPath) {
91
+ if (verbose) {
92
+ console.log(chalk.gray(' ccDNA not found - skipping patches'));
93
+ }
94
+ return null;
95
+ }
96
+
97
+ // Read ccDNA version from package.json FIRST
98
+ let ccdnaVersion = 'unknown';
99
+ try {
100
+ const pkgPath = path.join(path.dirname(ccdnaPath), '..', 'package.json');
101
+ if (fs.existsSync(pkgPath)) {
102
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
103
+ ccdnaVersion = pkg.version || 'unknown';
104
+ }
105
+ } catch {
106
+ // Ignore version detection errors
107
+ }
108
+
109
+ try {
110
+ // Set env var to tell ccDNA which Claude to patch
111
+ // eslint-disable-next-line no-restricted-syntax
112
+ const env = { ...process.env };
113
+ if (claudePath) {
114
+ // ccDNA checks CCDNA_CC_INSTALLATION_PATH to override default detection
115
+ env.CCDNA_CC_INSTALLATION_PATH = claudePath;
116
+ }
117
+
118
+ // Run ccDNA in apply mode (non-interactive)
119
+ execSync(`node "${ccdnaPath}" -a`, {
120
+ stdio: verbose ? 'inherit' : 'pipe',
121
+ timeout: 30000, // 30 second timeout
122
+ env,
123
+ });
124
+
125
+ if (verbose) {
126
+ console.log(chalk.green(` ✓ ccDNA v${ccdnaVersion} patches applied`));
127
+ }
128
+ return ccdnaVersion;
129
+ } catch (err) {
130
+ if (verbose) {
131
+ console.log(chalk.yellow(` ⚠ ccDNA patch failed: ${(err as Error).message}`));
132
+ }
133
+ return null;
134
+ }
135
+ */
136
+ }
137
+ /**
138
+ * Restore original Claude Code (remove ccDNA patches) on exit
139
+ * This restores the ekkOS-managed installation (~/.ekkos/claude-code/) to its base state
140
+ *
141
+ * NOTE: We intentionally DON'T restore on exit anymore because:
142
+ * 1. ekkOS uses a SEPARATE installation (~/.ekkos/claude-code/) from homebrew
143
+ * 2. The homebrew `claude` command should always be vanilla (untouched)
144
+ * 3. The ekkOS installation can stay patched - it's only used by `ekkos run`
145
+ *
146
+ * This function is kept for manual/explicit restore scenarios.
147
+ */
148
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
149
+ function restoreCcdnaPatches(verbose, claudePath) {
150
+ const ccdnaPath = findCcdnaPath();
151
+ if (!ccdnaPath) {
152
+ return false;
153
+ }
154
+ try {
155
+ // Set env var to tell ccDNA which Claude to restore
156
+ // eslint-disable-next-line no-restricted-syntax
157
+ const env = { ...process.env };
158
+ if (claudePath) {
159
+ env.CCDNA_CC_INSTALLATION_PATH = claudePath;
160
+ }
161
+ // Run ccDNA in restore mode (non-interactive)
162
+ (0, child_process_1.execSync)(`node "${ccdnaPath}" -r`, {
163
+ stdio: verbose ? 'inherit' : 'pipe',
164
+ timeout: 30000, // 30 second timeout
165
+ env,
166
+ });
167
+ if (verbose) {
168
+ console.log(chalk_1.default.green(' ✓ ccDNA patches removed (vanilla restored)'));
169
+ }
170
+ return true;
171
+ }
172
+ catch (err) {
173
+ if (verbose) {
174
+ console.log(chalk_1.default.yellow(` ⚠ ccDNA restore failed: ${err.message}`));
175
+ }
176
+ return false;
177
+ }
178
+ }
46
179
  const state_1 = require("../utils/state");
47
180
  const session_binding_1 = require("../utils/session-binding");
48
181
  const doctor_1 = require("./doctor");
@@ -115,17 +248,11 @@ const INTERRUPTED_REGEX = /interrupted.*what should claude do instead/i;
115
248
  const PALETTE_INDICATOR_REGEX = /\/(clear|continue|compact|help|bug|config)/i;
116
249
  // ═══════════════════════════════════════════════════════════════════════════
117
250
  // SESSION NAME DETECTION (3-word slug: word-word-word)
118
- // Claude prints session name in footer:
119
- // · 🧠 ekkOS_™ · Turn N · groovy-koala-saves · 📅
120
- // · 🧠 ekkOS_™ · groovy-koala-saves · 📅
251
+ // Claude prints session name in footer: · Turn N · groovy-koala-saves · 📅
121
252
  // ═══════════════════════════════════════════════════════════════════════════
122
- // Strong signal: ekkOS-branded footer with optional turn segment.
123
- // Supports both:
124
- // "· ekkOS_™ · Turn N · <session> · 📅"
125
- // "· ekkOS_™ · <session> · 📅"
126
- const SESSION_NAME_IN_EKKOS_FOOTER_REGEX = /ekkos[^\n·]*·\s*(?:turn\s+\d+\s*·\s*)?([a-z]+-[a-z]+-[a-z]+)\s*·/i;
127
- // Legacy fallback: plain Claude status line with explicit turn marker.
128
- const SESSION_NAME_IN_TURN_FOOTER_REGEX = /turn\s+\d+\s*·\s*([a-z]+-[a-z]+-[a-z]+)\s*·/i;
253
+ // Strong signal: explicit turn footer emitted by Claude/ekkOS status line.
254
+ // Requires "Turn <n> · <session> ·" to avoid matching arbitrary slug text.
255
+ const SESSION_NAME_IN_STATUS_REGEX = /turn\s+\d+\s*·\s*([a-z]+-[a-z]+-[a-z]+)\s*·/i;
129
256
  // Weaker signal: any 3-word slug (word-word-word pattern)
130
257
  const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
131
258
  // Orphan tool_result marker emitted by ccDNA validate mode
@@ -306,8 +433,7 @@ const isWindows = os.platform() === 'win32';
306
433
  // 'latest' = use latest version, or specify like '2.1.33' for specific version
307
434
  // Core ekkOS patches (eviction, context management) work with all recent versions
308
435
  // Cosmetic patches may fail on newer versions but don't affect functionality
309
- const PINNED_CLAUDE_VERSION = 'latest';
310
- const MIN_CLAUDE_VERSION_FOR_LATEST = process.env.EKKOS_MIN_CLAUDE_VERSION || '2.1.49';
436
+ const PINNED_CLAUDE_VERSION = '2.1.37';
311
437
  // Max output tokens for Claude responses
312
438
  // Default: 16384 (safe for Sonnet 4.5)
313
439
  // Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
@@ -347,8 +473,9 @@ function getEkkosEnv() {
347
473
  // Let Claude Code use its own default max_tokens (don't override)
348
474
  };
349
475
  /* eslint-enable no-restricted-syntax */
350
- // Check if proxy is disabled (proxyModeEnabled already includes CLI + env decisions)
351
- const proxyDisabled = !proxyModeEnabled;
476
+ // Check if proxy is disabled via env var or options
477
+ // eslint-disable-next-line no-restricted-syntax -- Feature flag, not API key
478
+ const proxyDisabled = process.env.EKKOS_DISABLE_PROXY === '1' || !proxyModeEnabled;
352
479
  if (!proxyDisabled) {
353
480
  env.EKKOS_PROXY_MODE = '1';
354
481
  // Enable ultra-minimal mode by default (30%→20% eviction for constant-cost infinite context)
@@ -357,14 +484,10 @@ function getEkkosEnv() {
357
484
  // This fixes the mismatch where CLI generated one name but Claude Code used another
358
485
  // The hook calls POST /proxy/session/bind with Claude's actual session name
359
486
  if (!cliSessionName) {
360
- const pendingSeed = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
361
- cliSessionName = `_pending-${pendingSeed}`; // Unique placeholder per CLI run
362
- cliSessionId = `pending-${pendingSeed}`;
363
- console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName} (will bind to Claude session)`));
364
- }
365
- env.EKKOS_PENDING_SESSION = cliSessionName;
366
- if (cliSessionId)
367
- env.EKKOS_PENDING_SESSION_ID = cliSessionId;
487
+ cliSessionName = '_pending'; // Placeholder - hook will bind real name
488
+ cliSessionId = `pending-${Date.now()}`;
489
+ console.log(chalk_1.default.gray(` 📂 Session: pending (will bind to Claude session)`));
490
+ }
368
491
  // Get full userId from config (NOT the truncated version from auth token)
369
492
  // Config has full UUID like "d4532ba0-0a86-42ce-bab4-22aa62b55ce6"
370
493
  // This matches the turns/ R2 structure: turns/{fullUserId}/{sessionName}/
@@ -385,8 +508,8 @@ function getEkkosEnv() {
385
508
  // Format: https://mcp.ekkos.dev/proxy/{userId}/{sessionName}?project={base64(cwd)}
386
509
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
387
510
  // Project path is base64-encoded to handle special chars safely
388
- const projectPath = process.cwd().replace(/\\/g, '/');
389
- const projectPathEncoded = encodeURIComponent(Buffer.from(projectPath, 'utf-8').toString('base64'));
511
+ const projectPath = process.cwd();
512
+ const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
390
513
  const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}`;
391
514
  env.ANTHROPIC_BASE_URL = proxyUrl;
392
515
  // Proxy URL contains userId + project path — don't leak to terminal
@@ -396,59 +519,16 @@ function getEkkosEnv() {
396
519
  }
397
520
  return env;
398
521
  }
399
- // ekkOS-managed Claude installation path (npm fallback)
522
+ // ekkOS-managed Claude installation path
400
523
  const EKKOS_CLAUDE_DIR = path.join(os.homedir(), '.ekkos', 'claude-code');
401
- const EKKOS_CLAUDE_BIN = path.join(EKKOS_CLAUDE_DIR, 'node_modules', '.bin', isWindows ? 'claude.cmd' : 'claude');
402
- // Native installer versions directory (Anthropic's official distribution — no npm warning)
403
- // Claude Code uses XDG_DATA_HOME/.local/share on all platforms (no Windows-specific AppData branch).
404
- // On Windows this resolves to %USERPROFILE%\.local\share\claude\versions\<version>
405
- const NATIVE_CLAUDE_VERSIONS_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'claude', 'versions');
406
- /**
407
- * Find the latest native-installed Claude binary.
408
- * Anthropic's native installer puts versioned binaries under ~/.local/share/claude/versions/<version>
409
- * These do NOT print the npm-to-native migration warning.
410
- * Returns the path to the latest binary, or null if none found.
411
- */
412
- function resolveNativeClaudeBin() {
413
- try {
414
- if (!fs.existsSync(NATIVE_CLAUDE_VERSIONS_DIR))
415
- return null;
416
- const entries = fs.readdirSync(NATIVE_CLAUDE_VERSIONS_DIR)
417
- .filter(v => !v.endsWith('.tmp') && !v.endsWith('.lock'));
418
- // Strip .exe suffix for sorting, then pick latest semver
419
- const versions = entries
420
- .map(v => v.replace(/\.exe$/i, ''))
421
- .filter(v => /^\d+\.\d+\.\d+$/.test(v))
422
- .sort((a, b) => {
423
- const pa = a.split('.').map(Number);
424
- const pb = b.split('.').map(Number);
425
- for (let i = 0; i < 3; i++) {
426
- if ((pa[i] || 0) !== (pb[i] || 0))
427
- return (pb[i] || 0) - (pa[i] || 0);
428
- }
429
- return 0;
430
- });
431
- if (versions.length === 0)
432
- return null;
433
- // On Windows the binary is named "<version>.exe", on Unix just "<version>"
434
- const binName = isWindows ? `${versions[0]}.exe` : versions[0];
435
- const bin = path.join(NATIVE_CLAUDE_VERSIONS_DIR, binName);
436
- if (fs.existsSync(bin) && fs.statSync(bin).isFile())
437
- return bin;
438
- return null;
439
- }
440
- catch {
441
- return null;
442
- }
443
- }
524
+ const EKKOS_CLAUDE_BIN = path.join(EKKOS_CLAUDE_DIR, 'node_modules', '.bin', 'claude');
444
525
  /**
445
526
  * Check if a Claude installation exists and get its version
446
527
  * Returns version string if found, null otherwise
447
528
  */
448
529
  function getClaudeVersion(claudePath) {
449
530
  try {
450
- const suppressStderr = isWindows ? '2>NUL' : '2>/dev/null';
451
- const version = (0, child_process_1.execSync)(`"${claudePath}" --version ${suppressStderr}`, { encoding: 'utf-8' }).trim();
531
+ const version = (0, child_process_1.execSync)(`"${claudePath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
452
532
  // Look for pattern like "2.1.6 (Claude Code)" or just "2.1.6" anywhere in output
453
533
  const match = version.match(/(\d+\.\d+\.\d+)\s*\(Claude Code\)/);
454
534
  if (match)
@@ -462,24 +542,6 @@ function getClaudeVersion(claudePath) {
462
542
  return null;
463
543
  }
464
544
  }
465
- function compareSemver(a, b) {
466
- const parse = (v) => {
467
- const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
468
- if (!m)
469
- return [0, 0, 0];
470
- return [Number(m[1]), Number(m[2]), Number(m[3])];
471
- };
472
- const pa = parse(a);
473
- const pb = parse(b);
474
- for (let i = 0; i < 3; i++) {
475
- if (pa[i] !== pb[i])
476
- return pa[i] - pb[i];
477
- }
478
- return 0;
479
- }
480
- function isVersionAtLeast(version, minVersion) {
481
- return compareSemver(version, minVersion) >= 0;
482
- }
483
545
  /**
484
546
  * Check if a Claude installation matches our required version
485
547
  * When PINNED_CLAUDE_VERSION is 'latest', any version is acceptable
@@ -489,9 +551,8 @@ function checkClaudeVersion(claudePath) {
489
551
  if (!version)
490
552
  return false;
491
553
  // 'latest' means any version is acceptable
492
- if (PINNED_CLAUDE_VERSION === 'latest') {
493
- return isVersionAtLeast(version, MIN_CLAUDE_VERSION_FOR_LATEST);
494
- }
554
+ if (PINNED_CLAUDE_VERSION === 'latest')
555
+ return true;
495
556
  return version === PINNED_CLAUDE_VERSION;
496
557
  }
497
558
  /**
@@ -562,37 +623,34 @@ function installEkkosClaudeVersion() {
562
623
  }
563
624
  }
564
625
  /**
565
- * Resolve full path to claude executable.
626
+ * Resolve full path to claude executable
627
+ * Returns direct path if found with correct version, otherwise 'npx:VERSION'
628
+ *
629
+ * IMPORTANT: We pin to a specific Claude Code version (currently 2.1.33) to ensure
630
+ * consistent behavior with ekkOS context management and eviction patches.
631
+ *
632
+ * CRITICAL: ekkos run ONLY uses the ekkOS-managed installation at ~/.ekkos/claude-code/
633
+ * This ensures complete separation from the user's existing Claude installation (Homebrew/npm).
634
+ * The user's `claude` command remains untouched and can be any version.
566
635
  *
567
636
  * Priority:
568
- * 1. Native installer binary (~/.local/share/claude/versions/<latest>) no npm warning
569
- * 2. ekkOS-managed npm installation (~/.ekkos/claude-code) — existing installs
570
- * 3. Auto-install via npm to ekkOS-managed directory
571
- * 4. npx fallback (rare, shows npm deprecation warning)
637
+ * 1. ekkOS-managed installation (~/.ekkos/claude-code) - ONLY option for ekkos run
638
+ * 2. Auto-install if doesn't exist
639
+ * 3. npx with pinned version (fallback if install fails)
572
640
  */
573
641
  function resolveClaudePath() {
574
- // PRIORITY 1: Native installer binary (Anthropic's official distribution)
575
- // These binaries do not print the npm-to-native migration warning.
576
- const nativeBin = resolveNativeClaudeBin();
577
- if (nativeBin) {
578
- const nativeVersion = getClaudeVersion(nativeBin);
579
- if (PINNED_CLAUDE_VERSION !== 'latest' ||
580
- (nativeVersion && isVersionAtLeast(nativeVersion, MIN_CLAUDE_VERSION_FOR_LATEST))) {
581
- return nativeBin;
582
- }
583
- console.log(chalk_1.default.yellow(` Native Claude ${nativeVersion || 'unknown'} is below minimum ${MIN_CLAUDE_VERSION_FOR_LATEST}; using managed latest.`));
584
- }
585
- // PRIORITY 2: ekkOS-managed npm installation (existing users before native installer)
642
+ // PRIORITY 1: ekkOS-managed installation
586
643
  if (fs.existsSync(EKKOS_CLAUDE_BIN) && checkClaudeVersion(EKKOS_CLAUDE_BIN)) {
587
644
  return EKKOS_CLAUDE_BIN;
588
645
  }
589
- // PRIORITY 3: Auto-install to ekkOS-managed directory
646
+ // PRIORITY 2: Auto-install to ekkOS-managed directory (user's Claude stays untouched)
590
647
  if (installEkkosClaudeVersion()) {
591
648
  if (fs.existsSync(EKKOS_CLAUDE_BIN)) {
592
649
  return EKKOS_CLAUDE_BIN;
593
650
  }
594
651
  }
595
- // PRIORITY 4: Fall back to npx (rare only if install failed, will show deprecation warning)
652
+ // PRIORITY 3: Fall back to npx with pinned version (shows update message)
653
+ // This is rare - only happens if install failed
596
654
  return `npx:${PINNED_CLAUDE_VERSION}`;
597
655
  }
598
656
  /**
@@ -639,8 +697,7 @@ function resolveGlobalClaudePath() {
639
697
  }
640
698
  }
641
699
  try {
642
- const whichCmd = isWindows ? 'where' : 'which';
643
- const result = (0, child_process_1.execSync)(`${whichCmd} claude`, { encoding: 'utf-8' }).trim().split('\n')[0];
700
+ const result = (0, child_process_1.execSync)('which claude', { encoding: 'utf-8' }).trim();
644
701
  if (result && fs.existsSync(result)) {
645
702
  return result;
646
703
  }
@@ -648,7 +705,7 @@ function resolveGlobalClaudePath() {
648
705
  catch {
649
706
  // Ignore errors
650
707
  }
651
- return isWindows ? 'claude.cmd' : 'claude';
708
+ return 'claude';
652
709
  }
653
710
  /**
654
711
  * Sleep helper
@@ -746,72 +803,6 @@ function cleanupInstanceFile(instanceId) {
746
803
  // Ignore cleanup errors
747
804
  }
748
805
  }
749
- /**
750
- * Launch ekkos run + dashboard using Windows Terminal split panes (Windows equivalent of tmux)
751
- */
752
- function launchWithDashboardWindows(options) {
753
- const launchTime = Date.now();
754
- // Build the ekkos run command WITHOUT --dashboard (prevent recursion)
755
- const runArgs = ['run'];
756
- if (options.session)
757
- runArgs.push('-s', options.session);
758
- if (options.bypass)
759
- runArgs.push('-b');
760
- if (options.verbose)
761
- runArgs.push('-v');
762
- if (options.doctor)
763
- runArgs.push('-d');
764
- if (options.research)
765
- runArgs.push('-r');
766
- if (options.noInject)
767
- runArgs.push('--skip-inject');
768
- if (options.noDna)
769
- runArgs.push('--skip-dna');
770
- if (options.noProxy)
771
- runArgs.push('--skip-proxy');
772
- const ekkosCmd = process.argv[1];
773
- const cwd = process.cwd();
774
- // Write dashboard launch marker
775
- const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
776
- try {
777
- fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
778
- }
779
- catch { }
780
- // Use -EncodedCommand (UTF-16LE Base64) to pass PowerShell scripts to wt panes.
781
- // This completely avoids nested quote hell: cmd.exe sees no " inside the wt command string,
782
- // and PowerShell decodes the base64 itself, preserving backslashes and special chars exactly.
783
- const ekkosCmdEscaped = ekkosCmd.replace(/'/g, "''");
784
- const cwdEscaped = cwd.replace(/'/g, "''");
785
- function toPsEncoded(script) {
786
- // PowerShell -EncodedCommand expects UTF-16LE Base64
787
- return Buffer.from(script, 'utf16le').toString('base64');
788
- }
789
- // cd to original CWD first so ekkos run registers the correct projectPath
790
- const runScript = `Set-Location '${cwdEscaped}'; & node '${ekkosCmdEscaped}' ${runArgs.join(' ')}`;
791
- const dashScript = `& node '${ekkosCmdEscaped}' dashboard --wait-for-new --refresh 2000`;
792
- const runEncoded = toPsEncoded(runScript);
793
- const dashEncoded = toPsEncoded(dashScript);
794
- // Windows Terminal split pane command.
795
- // No nested double-quotes in the PowerShell portion — only the WT --title/--startingDirectory
796
- // values need quoting, which cmd.exe handles cleanly.
797
- const wtCmd = [
798
- 'wt',
799
- `new-tab --startingDirectory "${cwd}" --title ekkOS powershell -NoExit -EncodedCommand ${runEncoded}`,
800
- `; split-pane -V --size 0.4 --title Dashboard powershell -NoExit -EncodedCommand ${dashEncoded}`
801
- ].join(' ');
802
- try {
803
- (0, child_process_1.execSync)(wtCmd, { stdio: 'inherit', shell: true });
804
- console.log(chalk_1.default.cyan('\n Dashboard launched in right pane (40%)'));
805
- console.log(chalk_1.default.gray(' Switch panes: Alt+Left / Alt+Right in Windows Terminal'));
806
- // Exit current process — the new WT window takes over
807
- process.exit(0);
808
- }
809
- catch (err) {
810
- console.log(chalk_1.default.red(`Windows Terminal error: ${err.message}`));
811
- console.log(chalk_1.default.gray('Tip: Install Windows Terminal from the Microsoft Store for split-pane support.'));
812
- console.log(chalk_1.default.gray('Falling back to normal mode. Run "ekkos dashboard --latest" in another terminal.'));
813
- }
814
- }
815
806
  /**
816
807
  * Launch ekkos run + dashboard in isolated tmux panes (60/40 split)
817
808
  */
@@ -836,6 +827,7 @@ function launchWithDashboard(options) {
836
827
  runArgs.push('--skip-dna');
837
828
  if (options.noProxy)
838
829
  runArgs.push('--skip-proxy');
830
+ runArgs.push('--kickstart'); // Auto-send "test" to create session immediately for dashboard
839
831
  const ekkosCmd = process.argv[1]; // Path to ekkos CLI
840
832
  const cwd = process.cwd();
841
833
  const termCols = process.stdout.columns ?? 160;
@@ -891,65 +883,32 @@ function launchWithDashboard(options) {
891
883
  }
892
884
  }
893
885
  async function run(options) {
894
- // ══════════════════════════════════════════════════════════════════════════
895
- // AUTO-SETUP: Run setup inline if this is a fresh install
896
- // New users can just type `ekkos run` and get the full onboarding flow
897
- // ══════════════════════════════════════════════════════════════════════════
898
- const configFile = path.join(os.homedir(), '.ekkos', 'config.json');
899
- if (!fs.existsSync(configFile)) {
900
- console.log(chalk_1.default.cyan('\n👋 Welcome to ekkOS! Setting up your environment...\n'));
901
- const { setup } = await Promise.resolve().then(() => __importStar(require('./setup.js')));
902
- await setup({ ide: 'all' });
903
- console.log('');
904
- }
905
886
  const verbose = options.verbose || false;
906
887
  const bypass = options.bypass || false;
907
888
  const noInject = options.noInject || false;
908
- // Set proxy mode based on options + env gate (used by getEkkosEnv)
909
- const proxyDisabledByEnv = process.env.EKKOS_DISABLE_PROXY === '1';
910
- proxyModeEnabled = !(options.noProxy || false || proxyDisabledByEnv);
889
+ // Set proxy mode based on options (used by getEkkosEnv)
890
+ proxyModeEnabled = !(options.noProxy || false);
911
891
  if (proxyModeEnabled) {
912
892
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
913
893
  }
914
- else if (proxyDisabledByEnv) {
915
- console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled by EKKOS_DISABLE_PROXY=1'));
916
- console.log(chalk_1.default.gray(' Unset EKKOS_DISABLE_PROXY to re-enable proxy routing.'));
917
- }
918
894
  else if (verbose) {
919
- console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--skip-proxy)'));
895
+ console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
920
896
  }
921
897
  // ══════════════════════════════════════════════════════════════════════════
922
- // DASHBOARD MODE: tmux (Mac/Linux) or Windows Terminal (Windows)
898
+ // DASHBOARD MODE: Launch via tmux with isolated dashboard pane (60/40)
923
899
  // ══════════════════════════════════════════════════════════════════════════
924
900
  if (options.dashboard) {
925
- if (isWindows) {
926
- // Windows: use Windows Terminal split panes
927
- try {
928
- (0, child_process_1.execSync)('where wt', { stdio: 'pipe' });
929
- launchWithDashboardWindows(options);
901
+ try {
902
+ const tmuxPath = (0, child_process_1.execSync)('which tmux', { encoding: 'utf-8' }).trim();
903
+ if (tmuxPath) {
904
+ launchWithDashboard(options);
930
905
  return;
931
906
  }
932
- catch {
933
- console.log(chalk_1.default.yellow(' Windows Terminal not found.'));
934
- console.log(chalk_1.default.gray(' Install from Microsoft Store: https://aka.ms/terminal'));
935
- console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
936
- console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
937
- }
938
907
  }
939
- else {
940
- // Mac/Linux: use tmux
941
- try {
942
- const tmuxPath = (0, child_process_1.execSync)('which tmux', { encoding: 'utf-8' }).trim();
943
- if (tmuxPath) {
944
- launchWithDashboard(options);
945
- return;
946
- }
947
- }
948
- catch {
949
- console.log(chalk_1.default.yellow(' tmux not found. Install: brew install tmux'));
950
- console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
951
- console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
952
- }
908
+ catch {
909
+ console.log(chalk_1.default.yellow(' tmux not found. Install: brew install tmux'));
910
+ console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
911
+ console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
953
912
  }
954
913
  }
955
914
  // Generate instance ID for this run
@@ -1005,15 +964,22 @@ async function run(options) {
1005
964
  }
1006
965
  }
1007
966
  // ══════════════════════════════════════════════════════════════════════════
1008
- // Legacy ccDNA flags are now no-ops.
1009
- // Context eviction and replay are owned by the proxy.
967
+ // ccDNA AUTO-PATCH: Apply Claude Code customizations before spawn
968
+ // This patches the context warning, themes, and other ccDNA features
969
+ // Skip if --no-dna flag is set
1010
970
  // ══════════════════════════════════════════════════════════════════════════
1011
971
  const noDna = options.noDna || false;
1012
- if (verbose && noDna) {
1013
- console.log(chalk_1.default.gray(' --skip-dna is deprecated (ccDNA patching is removed)'));
972
+ let ccdnaVersion = null;
973
+ if (noDna) {
974
+ if (verbose) {
975
+ console.log(chalk_1.default.yellow(' ⏭️ Skipping ccDNA injection (--no-dna)'));
976
+ }
1014
977
  }
1015
- if (verbose && claudeCliPath) {
1016
- console.log(chalk_1.default.gray(` 🤖 Claude CLI: ${claudeCliPath}`));
978
+ else {
979
+ if (verbose && claudeCliPath) {
980
+ console.log(chalk_1.default.gray(` 🔧 Patching: ${claudeCliPath}`));
981
+ }
982
+ ccdnaVersion = applyCcdnaPatches(verbose, claudeCliPath);
1017
983
  }
1018
984
  const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
1019
985
  const claudePath = isNpxMode ? 'npx' : rawClaudePath;
@@ -1045,7 +1011,7 @@ async function run(options) {
1045
1011
  // Handler reference for early spawn (allows replacing buffer handler with real handler)
1046
1012
  let earlyDataHandler = null;
1047
1013
  // Start spawning Claude NOW (before animation) if PTY available
1048
- if (usePty && pty) {
1014
+ if (usePty && pty && !isWindows) {
1049
1015
  try {
1050
1016
  earlySpawnedShell = pty.spawn(claudePath, earlyArgs, {
1051
1017
  name: 'xterm-256color',
@@ -1174,10 +1140,10 @@ async function run(options) {
1174
1140
  logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
1175
1141
  console.log('');
1176
1142
  // ══════════════════════════════════════════════════════════════════════════
1177
- // ANIMATED TITLE: "ekkOS_Pulse" with orange/white shine
1143
+ // ANIMATED TITLE: "Cognitive Continuity Engine" with orange/white shine
1178
1144
  // ══════════════════════════════════════════════════════════════════════════
1179
- const titleText = 'ekkOS_Pulse';
1180
- const taglineText = 'Infinite context. Native model quality.';
1145
+ const titleText = 'Cognitive Continuity Engine';
1146
+ const taglineText = 'Context is finite. Intelligence isn\'t.';
1181
1147
  // Color palette for shine effect
1182
1148
  const whiteShine = chalk_1.default.whiteBright;
1183
1149
  // Phase 1: Typewriter effect for title
@@ -1257,6 +1223,9 @@ async function run(options) {
1257
1223
  if (bypass) {
1258
1224
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1259
1225
  }
1226
+ if (noDna) {
1227
+ console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1228
+ }
1260
1229
  if (verbose) {
1261
1230
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1262
1231
  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)`));
@@ -1269,6 +1238,9 @@ async function run(options) {
1269
1238
  if (bypass) {
1270
1239
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1271
1240
  }
1241
+ if (noDna) {
1242
+ console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1243
+ }
1272
1244
  if (verbose) {
1273
1245
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1274
1246
  }
@@ -1355,7 +1327,7 @@ async function run(options) {
1355
1327
  // Claude creates the transcript file BEFORE outputting the session name
1356
1328
  // So we watch for new files rather than parsing TUI output (which is slower)
1357
1329
  // ════════════════════════════════════════════════════════════════════════════
1358
- const encodedCwd = process.cwd().replace(/[\\/]/g, '-');
1330
+ const encodedCwd = process.cwd().replace(/\//g, '-');
1359
1331
  const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
1360
1332
  const launchTime = Date.now();
1361
1333
  // Track existing jsonl files at startup
@@ -1526,10 +1498,8 @@ async function run(options) {
1526
1498
  });
1527
1499
  return false;
1528
1500
  }
1529
- // Check it's an absolute path (Unix: / or ~, Windows: C:\ or \\)
1530
- const isAbsolutePath = pathToCheck.startsWith('/') || pathToCheck.startsWith('~') ||
1531
- /^[A-Za-z]:[\\/]/.test(pathToCheck) || pathToCheck.startsWith('\\\\');
1532
- if (!isAbsolutePath) {
1501
+ // Check it starts with / or ~ (absolute path)
1502
+ if (!pathToCheck.startsWith('/') && !pathToCheck.startsWith('~')) {
1533
1503
  evictionDebugLog('PATH_INVALID', 'Transcript path is not absolute - clearing', {
1534
1504
  path: pathToCheck.slice(0, 100),
1535
1505
  });
@@ -1561,7 +1531,7 @@ async function run(options) {
1561
1531
  function bindRealSessionToProxy(sessionName, source) {
1562
1532
  if (!proxyModeEnabled)
1563
1533
  return;
1564
- if (!sessionName || sessionName === '_pending' || sessionName === 'pending' || sessionName.startsWith('_pending-'))
1534
+ if (!sessionName || sessionName === '_pending')
1565
1535
  return;
1566
1536
  if (boundProxySession === sessionName || bindingSessionInFlight === sessionName)
1567
1537
  return;
@@ -1569,8 +1539,7 @@ async function run(options) {
1569
1539
  void (async () => {
1570
1540
  const maxAttempts = 3;
1571
1541
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1572
- const pendingSession = cliSessionName && cliSessionName.startsWith('_pending') ? cliSessionName : undefined;
1573
- const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd(), pendingSession);
1542
+ const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd());
1574
1543
  if (success) {
1575
1544
  boundProxySession = sessionName;
1576
1545
  bindingSessionInFlight = null;
@@ -1711,8 +1680,12 @@ async function run(options) {
1711
1680
  console.log(chalk_1.default.yellow(' Monitor-only mode (--no-inject)'));
1712
1681
  }
1713
1682
  if (verbose) {
1683
+ // Show Claude version with ccDNA version if patched
1714
1684
  const ccVersion = pinnedVersion || PINNED_CLAUDE_VERSION;
1715
- console.log(chalk_1.default.gray(` 🤖 Claude Code v${ccVersion}`));
1685
+ const versionStr = ccdnaVersion
1686
+ ? `Claude Code v${ccVersion} + ekkOS_Continuum v${ccdnaVersion}`
1687
+ : `Claude Code v${ccVersion}`;
1688
+ console.log(chalk_1.default.gray(` 🤖 ${versionStr}`));
1716
1689
  if (currentSession) {
1717
1690
  console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
1718
1691
  }
@@ -1774,8 +1747,7 @@ async function run(options) {
1774
1747
  const spawnedProcess = (0, child_process_1.spawn)(claudePath, args, {
1775
1748
  stdio: 'inherit',
1776
1749
  cwd: process.cwd(),
1777
- env: getEkkosEnv(),
1778
- shell: isWindows // Required on Windows to execute .cmd files
1750
+ env: getEkkosEnv()
1779
1751
  });
1780
1752
  spawnedProcess.on('exit', (code) => process.exit(code ?? 0));
1781
1753
  spawnedProcess.on('error', (e) => {
@@ -1799,8 +1771,7 @@ async function run(options) {
1799
1771
  const spawnedProcess = (0, child_process_1.spawn)(claudePath, args, {
1800
1772
  stdio: 'inherit',
1801
1773
  cwd: process.cwd(),
1802
- env: getEkkosEnv(),
1803
- shell: isWindows // Required on Windows to execute .cmd files
1774
+ env: getEkkosEnv()
1804
1775
  });
1805
1776
  spawnedProcess.on('exit', (code) => {
1806
1777
  process.exit(code ?? 0);
@@ -2304,9 +2275,7 @@ async function run(options) {
2304
2275
  if (transcriptMatch) {
2305
2276
  const candidatePath = transcriptMatch[1];
2306
2277
  // Validate it's an actual path (not garbage from terminal output)
2307
- const isAbsCandidate = candidatePath.startsWith('/') || candidatePath.startsWith('~') ||
2308
- /^[A-Za-z]:[\\/]/.test(candidatePath);
2309
- if (isAbsCandidate) {
2278
+ if (candidatePath.startsWith('/') || candidatePath.startsWith('~')) {
2310
2279
  const resolvedPath = candidatePath.startsWith('~')
2311
2280
  ? path.join(os.homedir(), candidatePath.slice(1))
2312
2281
  : candidatePath;
@@ -2353,15 +2322,12 @@ async function run(options) {
2353
2322
  }
2354
2323
  // ════════════════════════════════════════════════════════════════════════
2355
2324
  // SESSION NAME DETECTION (PRIMARY METHOD)
2356
- // Claude footer examples:
2357
- // "· 🧠 ekkOS_™ · Turn N · groovy-koala-saves · 📅 2026-01-17"
2358
- // "· 🧠 ekkOS_™ · groovy-koala-saves · 📅 2026-01-17"
2325
+ // Claude footer: "· Turn N · groovy-koala-saves · 📅 2026-01-17"
2359
2326
  // This is MORE reliable than UUID extraction
2360
2327
  // ════════════════════════════════════════════════════════════════════════
2361
2328
  const plain = stripAnsi(data);
2362
- // Strong signal: session name in branded footer (with/without turn marker)
2363
- const statusMatch = plain.match(SESSION_NAME_IN_EKKOS_FOOTER_REGEX) ||
2364
- plain.match(SESSION_NAME_IN_TURN_FOOTER_REGEX);
2329
+ // Strong signal: session name between dot separators in status/footer line
2330
+ const statusMatch = plain.match(SESSION_NAME_IN_STATUS_REGEX);
2365
2331
  if (statusMatch) {
2366
2332
  const detectedSession = statusMatch[1].toLowerCase();
2367
2333
  // Validate against word lists (SOURCE OF TRUTH)