@ekkos/cli 1.0.33 → 1.0.35

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 (51) 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 +221 -259
  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 +4 -2
  19. package/templates/CLAUDE.md +23 -135
  20. package/templates/agents/README.md +182 -0
  21. package/templates/agents/code-reviewer.md +166 -0
  22. package/templates/agents/debug-detective.md +169 -0
  23. package/templates/agents/ekkOS_Vercel.md +99 -0
  24. package/templates/agents/extension-manager.md +229 -0
  25. package/templates/agents/git-companion.md +185 -0
  26. package/templates/agents/github-test-agent.md +321 -0
  27. package/templates/agents/railway-manager.md +179 -0
  28. package/templates/ekkos-manifest.json +8 -8
  29. package/templates/hooks/assistant-response.ps1 +160 -256
  30. package/templates/hooks/assistant-response.sh +66 -130
  31. package/templates/hooks/hooks.json +0 -6
  32. package/templates/hooks/lib/contract.sh +31 -43
  33. package/templates/hooks/lib/count-tokens.cjs +0 -0
  34. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  35. package/templates/hooks/lib/state.sh +1 -53
  36. package/templates/hooks/session-start.ps1 +391 -91
  37. package/templates/hooks/session-start.sh +166 -201
  38. package/templates/hooks/stop.ps1 +341 -202
  39. package/templates/hooks/stop.sh +948 -275
  40. package/templates/hooks/user-prompt-submit.ps1 +548 -224
  41. package/templates/hooks/user-prompt-submit.sh +456 -382
  42. package/templates/plan-template.md +0 -0
  43. package/templates/spec-template.md +0 -0
  44. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  45. package/templates/windsurf-hooks/hooks.json +2 -9
  46. package/templates/windsurf-hooks/install.sh +0 -0
  47. package/templates/windsurf-hooks/lib/contract.sh +0 -2
  48. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  49. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  50. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  51. 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");
@@ -54,10 +187,6 @@ const transcript_repair_1 = require("../capture/transcript-repair");
54
187
  let pty = null;
55
188
  let ptyLoadPromise = null;
56
189
  async function loadPty() {
57
- // node-pty uses native addons that don't load cleanly on Windows.
58
- // All PTY code paths already guard with `!isWindows`, so skip the import entirely.
59
- if (isWindows)
60
- return null;
61
190
  if (pty)
62
191
  return pty;
63
192
  if (!ptyLoadPromise) {
@@ -119,17 +248,11 @@ const INTERRUPTED_REGEX = /interrupted.*what should claude do instead/i;
119
248
  const PALETTE_INDICATOR_REGEX = /\/(clear|continue|compact|help|bug|config)/i;
120
249
  // ═══════════════════════════════════════════════════════════════════════════
121
250
  // SESSION NAME DETECTION (3-word slug: word-word-word)
122
- // Claude prints session name in footer:
123
- // · 🧠 ekkOS_™ · Turn N · groovy-koala-saves · 📅
124
- // · 🧠 ekkOS_™ · groovy-koala-saves · 📅
251
+ // Claude prints session name in footer: · Turn N · groovy-koala-saves · 📅
125
252
  // ═══════════════════════════════════════════════════════════════════════════
126
- // Strong signal: ekkOS-branded footer with optional turn segment.
127
- // Supports both:
128
- // "· ekkOS_™ · Turn N · <session> · 📅"
129
- // "· ekkOS_™ · <session> · 📅"
130
- const SESSION_NAME_IN_EKKOS_FOOTER_REGEX = /ekkos[^\n·]*·\s*(?:turn\s+\d+\s*·\s*)?([a-z]+-[a-z]+-[a-z]+)\s*·/i;
131
- // Legacy fallback: plain Claude status line with explicit turn marker.
132
- 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;
133
256
  // Weaker signal: any 3-word slug (word-word-word pattern)
134
257
  const SESSION_NAME_REGEX = /\b([a-z]+-[a-z]+-[a-z]+)\b/i;
135
258
  // Orphan tool_result marker emitted by ccDNA validate mode
@@ -310,8 +433,7 @@ const isWindows = os.platform() === 'win32';
310
433
  // 'latest' = use latest version, or specify like '2.1.33' for specific version
311
434
  // Core ekkOS patches (eviction, context management) work with all recent versions
312
435
  // Cosmetic patches may fail on newer versions but don't affect functionality
313
- const PINNED_CLAUDE_VERSION = 'latest';
314
- const MIN_CLAUDE_VERSION_FOR_LATEST = process.env.EKKOS_MIN_CLAUDE_VERSION || '2.1.49';
436
+ const PINNED_CLAUDE_VERSION = '2.1.37';
315
437
  // Max output tokens for Claude responses
316
438
  // Default: 16384 (safe for Sonnet 4.5)
317
439
  // Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
@@ -351,8 +473,9 @@ function getEkkosEnv() {
351
473
  // Let Claude Code use its own default max_tokens (don't override)
352
474
  };
353
475
  /* eslint-enable no-restricted-syntax */
354
- // Check if proxy is disabled (proxyModeEnabled already includes CLI + env decisions)
355
- 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;
356
479
  if (!proxyDisabled) {
357
480
  env.EKKOS_PROXY_MODE = '1';
358
481
  // Enable ultra-minimal mode by default (30%→20% eviction for constant-cost infinite context)
@@ -361,14 +484,10 @@ function getEkkosEnv() {
361
484
  // This fixes the mismatch where CLI generated one name but Claude Code used another
362
485
  // The hook calls POST /proxy/session/bind with Claude's actual session name
363
486
  if (!cliSessionName) {
364
- const pendingSeed = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
365
- cliSessionName = `_pending-${pendingSeed}`; // Unique placeholder per CLI run
366
- cliSessionId = `pending-${pendingSeed}`;
367
- console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName} (will bind to Claude session)`));
368
- }
369
- env.EKKOS_PENDING_SESSION = cliSessionName;
370
- if (cliSessionId)
371
- 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
+ }
372
491
  // Get full userId from config (NOT the truncated version from auth token)
373
492
  // Config has full UUID like "d4532ba0-0a86-42ce-bab4-22aa62b55ce6"
374
493
  // This matches the turns/ R2 structure: turns/{fullUserId}/{sessionName}/
@@ -389,8 +508,8 @@ function getEkkosEnv() {
389
508
  // Format: https://mcp.ekkos.dev/proxy/{userId}/{sessionName}?project={base64(cwd)}
390
509
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
391
510
  // Project path is base64-encoded to handle special chars safely
392
- const projectPath = process.cwd().replace(/\\/g, '/');
393
- const projectPathEncoded = encodeURIComponent(Buffer.from(projectPath, 'utf-8').toString('base64'));
511
+ const projectPath = process.cwd();
512
+ const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
394
513
  const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}`;
395
514
  env.ANTHROPIC_BASE_URL = proxyUrl;
396
515
  // Proxy URL contains userId + project path — don't leak to terminal
@@ -400,59 +519,16 @@ function getEkkosEnv() {
400
519
  }
401
520
  return env;
402
521
  }
403
- // ekkOS-managed Claude installation path (npm fallback)
522
+ // ekkOS-managed Claude installation path
404
523
  const EKKOS_CLAUDE_DIR = path.join(os.homedir(), '.ekkos', 'claude-code');
405
- const EKKOS_CLAUDE_BIN = path.join(EKKOS_CLAUDE_DIR, 'node_modules', '.bin', isWindows ? 'claude.cmd' : 'claude');
406
- // Native installer versions directory (Anthropic's official distribution — no npm warning)
407
- // Claude Code uses XDG_DATA_HOME/.local/share on all platforms (no Windows-specific AppData branch).
408
- // On Windows this resolves to %USERPROFILE%\.local\share\claude\versions\<version>
409
- const NATIVE_CLAUDE_VERSIONS_DIR = path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share'), 'claude', 'versions');
410
- /**
411
- * Find the latest native-installed Claude binary.
412
- * Anthropic's native installer puts versioned binaries under ~/.local/share/claude/versions/<version>
413
- * These do NOT print the npm-to-native migration warning.
414
- * Returns the path to the latest binary, or null if none found.
415
- */
416
- function resolveNativeClaudeBin() {
417
- try {
418
- if (!fs.existsSync(NATIVE_CLAUDE_VERSIONS_DIR))
419
- return null;
420
- const entries = fs.readdirSync(NATIVE_CLAUDE_VERSIONS_DIR)
421
- .filter(v => !v.endsWith('.tmp') && !v.endsWith('.lock'));
422
- // Strip .exe suffix for sorting, then pick latest semver
423
- const versions = entries
424
- .map(v => v.replace(/\.exe$/i, ''))
425
- .filter(v => /^\d+\.\d+\.\d+$/.test(v))
426
- .sort((a, b) => {
427
- const pa = a.split('.').map(Number);
428
- const pb = b.split('.').map(Number);
429
- for (let i = 0; i < 3; i++) {
430
- if ((pa[i] || 0) !== (pb[i] || 0))
431
- return (pb[i] || 0) - (pa[i] || 0);
432
- }
433
- return 0;
434
- });
435
- if (versions.length === 0)
436
- return null;
437
- // On Windows the binary is named "<version>.exe", on Unix just "<version>"
438
- const binName = isWindows ? `${versions[0]}.exe` : versions[0];
439
- const bin = path.join(NATIVE_CLAUDE_VERSIONS_DIR, binName);
440
- if (fs.existsSync(bin) && fs.statSync(bin).isFile())
441
- return bin;
442
- return null;
443
- }
444
- catch {
445
- return null;
446
- }
447
- }
524
+ const EKKOS_CLAUDE_BIN = path.join(EKKOS_CLAUDE_DIR, 'node_modules', '.bin', 'claude');
448
525
  /**
449
526
  * Check if a Claude installation exists and get its version
450
527
  * Returns version string if found, null otherwise
451
528
  */
452
529
  function getClaudeVersion(claudePath) {
453
530
  try {
454
- const suppressStderr = isWindows ? '2>NUL' : '2>/dev/null';
455
- 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();
456
532
  // Look for pattern like "2.1.6 (Claude Code)" or just "2.1.6" anywhere in output
457
533
  const match = version.match(/(\d+\.\d+\.\d+)\s*\(Claude Code\)/);
458
534
  if (match)
@@ -466,24 +542,6 @@ function getClaudeVersion(claudePath) {
466
542
  return null;
467
543
  }
468
544
  }
469
- function compareSemver(a, b) {
470
- const parse = (v) => {
471
- const m = v.match(/(\d+)\.(\d+)\.(\d+)/);
472
- if (!m)
473
- return [0, 0, 0];
474
- return [Number(m[1]), Number(m[2]), Number(m[3])];
475
- };
476
- const pa = parse(a);
477
- const pb = parse(b);
478
- for (let i = 0; i < 3; i++) {
479
- if (pa[i] !== pb[i])
480
- return pa[i] - pb[i];
481
- }
482
- return 0;
483
- }
484
- function isVersionAtLeast(version, minVersion) {
485
- return compareSemver(version, minVersion) >= 0;
486
- }
487
545
  /**
488
546
  * Check if a Claude installation matches our required version
489
547
  * When PINNED_CLAUDE_VERSION is 'latest', any version is acceptable
@@ -493,9 +551,8 @@ function checkClaudeVersion(claudePath) {
493
551
  if (!version)
494
552
  return false;
495
553
  // 'latest' means any version is acceptable
496
- if (PINNED_CLAUDE_VERSION === 'latest') {
497
- return isVersionAtLeast(version, MIN_CLAUDE_VERSION_FOR_LATEST);
498
- }
554
+ if (PINNED_CLAUDE_VERSION === 'latest')
555
+ return true;
499
556
  return version === PINNED_CLAUDE_VERSION;
500
557
  }
501
558
  /**
@@ -566,37 +623,34 @@ function installEkkosClaudeVersion() {
566
623
  }
567
624
  }
568
625
  /**
569
- * 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.
570
635
  *
571
636
  * Priority:
572
- * 1. Native installer binary (~/.local/share/claude/versions/<latest>) no npm warning
573
- * 2. ekkOS-managed npm installation (~/.ekkos/claude-code) — existing installs
574
- * 3. Auto-install via npm to ekkOS-managed directory
575
- * 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)
576
640
  */
577
641
  function resolveClaudePath() {
578
- // PRIORITY 1: Native installer binary (Anthropic's official distribution)
579
- // These binaries do not print the npm-to-native migration warning.
580
- const nativeBin = resolveNativeClaudeBin();
581
- if (nativeBin) {
582
- const nativeVersion = getClaudeVersion(nativeBin);
583
- if (PINNED_CLAUDE_VERSION !== 'latest' ||
584
- (nativeVersion && isVersionAtLeast(nativeVersion, MIN_CLAUDE_VERSION_FOR_LATEST))) {
585
- return nativeBin;
586
- }
587
- console.log(chalk_1.default.yellow(` Native Claude ${nativeVersion || 'unknown'} is below minimum ${MIN_CLAUDE_VERSION_FOR_LATEST}; using managed latest.`));
588
- }
589
- // PRIORITY 2: ekkOS-managed npm installation (existing users before native installer)
642
+ // PRIORITY 1: ekkOS-managed installation
590
643
  if (fs.existsSync(EKKOS_CLAUDE_BIN) && checkClaudeVersion(EKKOS_CLAUDE_BIN)) {
591
644
  return EKKOS_CLAUDE_BIN;
592
645
  }
593
- // PRIORITY 3: Auto-install to ekkOS-managed directory
646
+ // PRIORITY 2: Auto-install to ekkOS-managed directory (user's Claude stays untouched)
594
647
  if (installEkkosClaudeVersion()) {
595
648
  if (fs.existsSync(EKKOS_CLAUDE_BIN)) {
596
649
  return EKKOS_CLAUDE_BIN;
597
650
  }
598
651
  }
599
- // 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
600
654
  return `npx:${PINNED_CLAUDE_VERSION}`;
601
655
  }
602
656
  /**
@@ -643,8 +697,7 @@ function resolveGlobalClaudePath() {
643
697
  }
644
698
  }
645
699
  try {
646
- const whichCmd = isWindows ? 'where' : 'which';
647
- 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();
648
701
  if (result && fs.existsSync(result)) {
649
702
  return result;
650
703
  }
@@ -652,7 +705,7 @@ function resolveGlobalClaudePath() {
652
705
  catch {
653
706
  // Ignore errors
654
707
  }
655
- return isWindows ? 'claude.cmd' : 'claude';
708
+ return 'claude';
656
709
  }
657
710
  /**
658
711
  * Sleep helper
@@ -750,72 +803,6 @@ function cleanupInstanceFile(instanceId) {
750
803
  // Ignore cleanup errors
751
804
  }
752
805
  }
753
- /**
754
- * Launch ekkos run + dashboard using Windows Terminal split panes (Windows equivalent of tmux)
755
- */
756
- function launchWithDashboardWindows(options) {
757
- const launchTime = Date.now();
758
- // Build the ekkos run command WITHOUT --dashboard (prevent recursion)
759
- const runArgs = ['run'];
760
- if (options.session)
761
- runArgs.push('-s', options.session);
762
- if (options.bypass)
763
- runArgs.push('-b');
764
- if (options.verbose)
765
- runArgs.push('-v');
766
- if (options.doctor)
767
- runArgs.push('-d');
768
- if (options.research)
769
- runArgs.push('-r');
770
- if (options.noInject)
771
- runArgs.push('--skip-inject');
772
- if (options.noDna)
773
- runArgs.push('--skip-dna');
774
- if (options.noProxy)
775
- runArgs.push('--skip-proxy');
776
- const ekkosCmd = process.argv[1];
777
- const cwd = process.cwd();
778
- // Write dashboard launch marker
779
- const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
780
- try {
781
- fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
782
- }
783
- catch { }
784
- // Use -EncodedCommand (UTF-16LE Base64) to pass PowerShell scripts to wt panes.
785
- // This completely avoids nested quote hell: cmd.exe sees no " inside the wt command string,
786
- // and PowerShell decodes the base64 itself, preserving backslashes and special chars exactly.
787
- const ekkosCmdEscaped = ekkosCmd.replace(/'/g, "''");
788
- const cwdEscaped = cwd.replace(/'/g, "''");
789
- function toPsEncoded(script) {
790
- // PowerShell -EncodedCommand expects UTF-16LE Base64
791
- return Buffer.from(script, 'utf16le').toString('base64');
792
- }
793
- // cd to original CWD first so ekkos run registers the correct projectPath
794
- const runScript = `Set-Location '${cwdEscaped}'; & node '${ekkosCmdEscaped}' ${runArgs.join(' ')}`;
795
- const dashScript = `& node '${ekkosCmdEscaped}' dashboard --wait-for-new --refresh 2000`;
796
- const runEncoded = toPsEncoded(runScript);
797
- const dashEncoded = toPsEncoded(dashScript);
798
- // Windows Terminal split pane command.
799
- // No nested double-quotes in the PowerShell portion — only the WT --title/--startingDirectory
800
- // values need quoting, which cmd.exe handles cleanly.
801
- const wtCmd = [
802
- 'wt',
803
- `new-tab --startingDirectory "${cwd}" --title ekkOS powershell -NoExit -EncodedCommand ${runEncoded}`,
804
- `; split-pane -V --size 0.4 --title Dashboard powershell -NoExit -EncodedCommand ${dashEncoded}`
805
- ].join(' ');
806
- try {
807
- (0, child_process_1.execSync)(wtCmd, { stdio: 'inherit', shell: true });
808
- console.log(chalk_1.default.cyan('\n Dashboard launched in right pane (40%)'));
809
- console.log(chalk_1.default.gray(' Switch panes: Alt+Left / Alt+Right in Windows Terminal'));
810
- // Exit current process — the new WT window takes over
811
- process.exit(0);
812
- }
813
- catch (err) {
814
- console.log(chalk_1.default.red(`Windows Terminal error: ${err.message}`));
815
- console.log(chalk_1.default.gray('Tip: Install Windows Terminal from the Microsoft Store for split-pane support.'));
816
- console.log(chalk_1.default.gray('Falling back to normal mode. Run "ekkos dashboard --latest" in another terminal.'));
817
- }
818
- }
819
806
  /**
820
807
  * Launch ekkos run + dashboard in isolated tmux panes (60/40 split)
821
808
  */
@@ -840,6 +827,7 @@ function launchWithDashboard(options) {
840
827
  runArgs.push('--skip-dna');
841
828
  if (options.noProxy)
842
829
  runArgs.push('--skip-proxy');
830
+ runArgs.push('--kickstart'); // Auto-send "test" to create session immediately for dashboard
843
831
  const ekkosCmd = process.argv[1]; // Path to ekkos CLI
844
832
  const cwd = process.cwd();
845
833
  const termCols = process.stdout.columns ?? 160;
@@ -895,65 +883,32 @@ function launchWithDashboard(options) {
895
883
  }
896
884
  }
897
885
  async function run(options) {
898
- // ══════════════════════════════════════════════════════════════════════════
899
- // AUTO-SETUP: Run setup inline if this is a fresh install
900
- // New users can just type `ekkos run` and get the full onboarding flow
901
- // ══════════════════════════════════════════════════════════════════════════
902
- const configFile = path.join(os.homedir(), '.ekkos', 'config.json');
903
- if (!fs.existsSync(configFile)) {
904
- console.log(chalk_1.default.cyan('\n👋 Welcome to ekkOS! Setting up your environment...\n'));
905
- const { setup } = await Promise.resolve().then(() => __importStar(require('./setup.js')));
906
- await setup({ ide: 'all' });
907
- console.log('');
908
- }
909
886
  const verbose = options.verbose || false;
910
887
  const bypass = options.bypass || false;
911
888
  const noInject = options.noInject || false;
912
- // Set proxy mode based on options + env gate (used by getEkkosEnv)
913
- const proxyDisabledByEnv = process.env.EKKOS_DISABLE_PROXY === '1';
914
- proxyModeEnabled = !(options.noProxy || false || proxyDisabledByEnv);
889
+ // Set proxy mode based on options (used by getEkkosEnv)
890
+ proxyModeEnabled = !(options.noProxy || false);
915
891
  if (proxyModeEnabled) {
916
892
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
917
893
  }
918
- else if (proxyDisabledByEnv) {
919
- console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled by EKKOS_DISABLE_PROXY=1'));
920
- console.log(chalk_1.default.gray(' Unset EKKOS_DISABLE_PROXY to re-enable proxy routing.'));
921
- }
922
894
  else if (verbose) {
923
- console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--skip-proxy)'));
895
+ console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
924
896
  }
925
897
  // ══════════════════════════════════════════════════════════════════════════
926
- // DASHBOARD MODE: tmux (Mac/Linux) or Windows Terminal (Windows)
898
+ // DASHBOARD MODE: Launch via tmux with isolated dashboard pane (60/40)
927
899
  // ══════════════════════════════════════════════════════════════════════════
928
900
  if (options.dashboard) {
929
- if (isWindows) {
930
- // Windows: use Windows Terminal split panes
931
- try {
932
- (0, child_process_1.execSync)('where wt', { stdio: 'pipe' });
933
- launchWithDashboardWindows(options);
901
+ try {
902
+ const tmuxPath = (0, child_process_1.execSync)('which tmux', { encoding: 'utf-8' }).trim();
903
+ if (tmuxPath) {
904
+ launchWithDashboard(options);
934
905
  return;
935
906
  }
936
- catch {
937
- console.log(chalk_1.default.yellow(' Windows Terminal not found.'));
938
- console.log(chalk_1.default.gray(' Install from Microsoft Store: https://aka.ms/terminal'));
939
- console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
940
- console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
941
- }
942
907
  }
943
- else {
944
- // Mac/Linux: use tmux
945
- try {
946
- const tmuxPath = (0, child_process_1.execSync)('which tmux', { encoding: 'utf-8' }).trim();
947
- if (tmuxPath) {
948
- launchWithDashboard(options);
949
- return;
950
- }
951
- }
952
- catch {
953
- console.log(chalk_1.default.yellow(' tmux not found. Install: brew install tmux'));
954
- console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
955
- console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
956
- }
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'));
957
912
  }
958
913
  }
959
914
  // Generate instance ID for this run
@@ -1009,15 +964,22 @@ async function run(options) {
1009
964
  }
1010
965
  }
1011
966
  // ══════════════════════════════════════════════════════════════════════════
1012
- // Legacy ccDNA flags are now no-ops.
1013
- // 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
1014
970
  // ══════════════════════════════════════════════════════════════════════════
1015
971
  const noDna = options.noDna || false;
1016
- if (verbose && noDna) {
1017
- 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
+ }
1018
977
  }
1019
- if (verbose && claudeCliPath) {
1020
- 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);
1021
983
  }
1022
984
  const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
1023
985
  const claudePath = isNpxMode ? 'npx' : rawClaudePath;
@@ -1178,10 +1140,10 @@ async function run(options) {
1178
1140
  logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
1179
1141
  console.log('');
1180
1142
  // ══════════════════════════════════════════════════════════════════════════
1181
- // ANIMATED TITLE: "ekkOS_Pulse" with orange/white shine
1143
+ // ANIMATED TITLE: "Cognitive Continuity Engine" with orange/white shine
1182
1144
  // ══════════════════════════════════════════════════════════════════════════
1183
- const titleText = 'ekkOS_Pulse';
1184
- const taglineText = 'Infinite context. Native model quality.';
1145
+ const titleText = 'Cognitive Continuity Engine';
1146
+ const taglineText = 'Context is finite. Intelligence isn\'t.';
1185
1147
  // Color palette for shine effect
1186
1148
  const whiteShine = chalk_1.default.whiteBright;
1187
1149
  // Phase 1: Typewriter effect for title
@@ -1261,6 +1223,9 @@ async function run(options) {
1261
1223
  if (bypass) {
1262
1224
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1263
1225
  }
1226
+ if (noDna) {
1227
+ console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1228
+ }
1264
1229
  if (verbose) {
1265
1230
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1266
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)`));
@@ -1273,6 +1238,9 @@ async function run(options) {
1273
1238
  if (bypass) {
1274
1239
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1275
1240
  }
1241
+ if (noDna) {
1242
+ console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1243
+ }
1276
1244
  if (verbose) {
1277
1245
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1278
1246
  }
@@ -1359,7 +1327,7 @@ async function run(options) {
1359
1327
  // Claude creates the transcript file BEFORE outputting the session name
1360
1328
  // So we watch for new files rather than parsing TUI output (which is slower)
1361
1329
  // ════════════════════════════════════════════════════════════════════════════
1362
- const encodedCwd = process.cwd().replace(/[\\/]/g, '-');
1330
+ const encodedCwd = process.cwd().replace(/\//g, '-');
1363
1331
  const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
1364
1332
  const launchTime = Date.now();
1365
1333
  // Track existing jsonl files at startup
@@ -1530,10 +1498,8 @@ async function run(options) {
1530
1498
  });
1531
1499
  return false;
1532
1500
  }
1533
- // Check it's an absolute path (Unix: / or ~, Windows: C:\ or \\)
1534
- const isAbsolutePath = pathToCheck.startsWith('/') || pathToCheck.startsWith('~') ||
1535
- /^[A-Za-z]:[\\/]/.test(pathToCheck) || pathToCheck.startsWith('\\\\');
1536
- if (!isAbsolutePath) {
1501
+ // Check it starts with / or ~ (absolute path)
1502
+ if (!pathToCheck.startsWith('/') && !pathToCheck.startsWith('~')) {
1537
1503
  evictionDebugLog('PATH_INVALID', 'Transcript path is not absolute - clearing', {
1538
1504
  path: pathToCheck.slice(0, 100),
1539
1505
  });
@@ -1565,7 +1531,7 @@ async function run(options) {
1565
1531
  function bindRealSessionToProxy(sessionName, source) {
1566
1532
  if (!proxyModeEnabled)
1567
1533
  return;
1568
- if (!sessionName || sessionName === '_pending' || sessionName === 'pending' || sessionName.startsWith('_pending-'))
1534
+ if (!sessionName || sessionName === '_pending')
1569
1535
  return;
1570
1536
  if (boundProxySession === sessionName || bindingSessionInFlight === sessionName)
1571
1537
  return;
@@ -1573,8 +1539,7 @@ async function run(options) {
1573
1539
  void (async () => {
1574
1540
  const maxAttempts = 3;
1575
1541
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1576
- const pendingSession = cliSessionName && cliSessionName.startsWith('_pending') ? cliSessionName : undefined;
1577
- const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd(), pendingSession);
1542
+ const success = await (0, session_binding_1.bindSession)(sessionName, process.cwd());
1578
1543
  if (success) {
1579
1544
  boundProxySession = sessionName;
1580
1545
  bindingSessionInFlight = null;
@@ -1715,8 +1680,12 @@ async function run(options) {
1715
1680
  console.log(chalk_1.default.yellow(' Monitor-only mode (--no-inject)'));
1716
1681
  }
1717
1682
  if (verbose) {
1683
+ // Show Claude version with ccDNA version if patched
1718
1684
  const ccVersion = pinnedVersion || PINNED_CLAUDE_VERSION;
1719
- 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}`));
1720
1689
  if (currentSession) {
1721
1690
  console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
1722
1691
  }
@@ -1778,8 +1747,7 @@ async function run(options) {
1778
1747
  const spawnedProcess = (0, child_process_1.spawn)(claudePath, args, {
1779
1748
  stdio: 'inherit',
1780
1749
  cwd: process.cwd(),
1781
- env: getEkkosEnv(),
1782
- shell: isWindows // Required on Windows to execute .cmd files
1750
+ env: getEkkosEnv()
1783
1751
  });
1784
1752
  spawnedProcess.on('exit', (code) => process.exit(code ?? 0));
1785
1753
  spawnedProcess.on('error', (e) => {
@@ -1803,8 +1771,7 @@ async function run(options) {
1803
1771
  const spawnedProcess = (0, child_process_1.spawn)(claudePath, args, {
1804
1772
  stdio: 'inherit',
1805
1773
  cwd: process.cwd(),
1806
- env: getEkkosEnv(),
1807
- shell: isWindows // Required on Windows to execute .cmd files
1774
+ env: getEkkosEnv()
1808
1775
  });
1809
1776
  spawnedProcess.on('exit', (code) => {
1810
1777
  process.exit(code ?? 0);
@@ -2308,9 +2275,7 @@ async function run(options) {
2308
2275
  if (transcriptMatch) {
2309
2276
  const candidatePath = transcriptMatch[1];
2310
2277
  // Validate it's an actual path (not garbage from terminal output)
2311
- const isAbsCandidate = candidatePath.startsWith('/') || candidatePath.startsWith('~') ||
2312
- /^[A-Za-z]:[\\/]/.test(candidatePath);
2313
- if (isAbsCandidate) {
2278
+ if (candidatePath.startsWith('/') || candidatePath.startsWith('~')) {
2314
2279
  const resolvedPath = candidatePath.startsWith('~')
2315
2280
  ? path.join(os.homedir(), candidatePath.slice(1))
2316
2281
  : candidatePath;
@@ -2357,15 +2322,12 @@ async function run(options) {
2357
2322
  }
2358
2323
  // ════════════════════════════════════════════════════════════════════════
2359
2324
  // SESSION NAME DETECTION (PRIMARY METHOD)
2360
- // Claude footer examples:
2361
- // "· 🧠 ekkOS_™ · Turn N · groovy-koala-saves · 📅 2026-01-17"
2362
- // "· 🧠 ekkOS_™ · groovy-koala-saves · 📅 2026-01-17"
2325
+ // Claude footer: "· Turn N · groovy-koala-saves · 📅 2026-01-17"
2363
2326
  // This is MORE reliable than UUID extraction
2364
2327
  // ════════════════════════════════════════════════════════════════════════
2365
2328
  const plain = stripAnsi(data);
2366
- // Strong signal: session name in branded footer (with/without turn marker)
2367
- const statusMatch = plain.match(SESSION_NAME_IN_EKKOS_FOOTER_REGEX) ||
2368
- 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);
2369
2331
  if (statusMatch) {
2370
2332
  const detectedSession = statusMatch[1].toLowerCase();
2371
2333
  // Validate against word lists (SOURCE OF TRUTH)