@ekkos/cli 1.0.24 → 1.0.26

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.
@@ -283,9 +283,8 @@ function truncateToolResult(line) {
283
283
  return line;
284
284
  }
285
285
  }
286
- // Evicted content store (for retrieval by ccDNA)
286
+ // Local eviction debug store (best-effort forensic log)
287
287
  const EVICTED_STORE = path.join(os.homedir(), '.ekkos', 'evicted-context.jsonl');
288
- const EKKOS_CAPTURE_ENDPOINT = 'https://mcp.ekkos.dev/api/v1/context/evict';
289
288
  const EKKOS_API_URL = process.env.EKKOS_API_URL || 'https://mcp.ekkos.dev';
290
289
  /**
291
290
  * Evict messages using the Handshake Protocol (two-phase commit)
@@ -406,80 +405,16 @@ async function isHandshakeEvictionAvailable() {
406
405
  }
407
406
  return (0, eviction_client_js_1.checkEvictionHealth)(EKKOS_API_URL, authToken);
408
407
  }
409
- /**
410
- * Send evicted content to ekkOS cloud for later retrieval (async, non-blocking)
411
- */
412
- async function captureEvictedToCloud(lines, sessionId) {
413
- try {
414
- // Get auth token from environment
415
- const authToken = process.env.EKKOS_AUTH_TOKEN || process.env.SUPABASE_AUTH_TOKEN;
416
- if (!authToken) {
417
- debugLog('CLOUD_CAPTURE_SKIP', 'No auth token available');
418
- return;
419
- }
420
- // Parse messages from JSONL lines
421
- const messages = [];
422
- for (const line of lines) {
423
- try {
424
- const parsed = JSON.parse(line);
425
- if (parsed.message) {
426
- messages.push({
427
- role: parsed.message.role || 'unknown',
428
- content: parsed.message.content,
429
- });
430
- }
431
- }
432
- catch {
433
- // Skip unparseable lines
434
- }
435
- }
436
- if (messages.length === 0)
437
- return;
438
- // Estimate tokens (rough: 1 token ≈ 4 chars)
439
- const totalChars = lines.reduce((sum, l) => sum + l.length, 0);
440
- const estimatedTokens = Math.ceil(totalChars / 4);
441
- const payload = {
442
- session_id: sessionId || 'unknown',
443
- chunk_index: Date.now(), // Use timestamp as unique chunk index
444
- messages,
445
- token_count: estimatedTokens,
446
- eviction_reason: 'sliding_window',
447
- };
448
- // Non-blocking fetch - don't await, let it happen in background
449
- // 10s timeout to prevent lingering connections from burning resources
450
- fetch(EKKOS_CAPTURE_ENDPOINT, {
451
- method: 'POST',
452
- headers: {
453
- 'Content-Type': 'application/json',
454
- 'Authorization': `Bearer ${authToken}`,
455
- },
456
- body: JSON.stringify(payload),
457
- signal: AbortSignal.timeout(10000),
458
- }).then(res => {
459
- if (res.ok) {
460
- debugLog('CLOUD_CAPTURE_OK', `Sent ${messages.length} msgs to cloud`);
461
- }
462
- else {
463
- debugLog('CLOUD_CAPTURE_FAIL', `HTTP ${res.status}`);
464
- }
465
- }).catch(err => {
466
- debugLog('CLOUD_CAPTURE_ERROR', err.message);
467
- });
468
- }
469
- catch (err) {
470
- debugLog('CLOUD_CAPTURE_ERROR', err.message);
471
- }
472
- }
473
408
  /**
474
409
  * Save evicted content for later retrieval
475
410
  * Now supports handshake eviction when available
476
411
  *
477
412
  * @param lines - JSONL lines being evicted
478
- * @param sessionId - Session ID (for legacy capture)
479
- * @param sessionName - Session name (for handshake eviction)
480
- * @param indices - Original indices of evicted lines
413
+ * @param _sessionId - Session ID (reserved for future diagnostics)
414
+ * @param _sessionName - Session name (reserved for future diagnostics)
415
+ * @param _indices - Original indices of evicted lines (reserved for future diagnostics)
481
416
  */
482
- function saveEvictedContent(lines, sessionId, sessionName, indices) {
417
+ function saveEvictedContent(lines, _sessionId, _sessionName, _indices) {
483
418
  if (lines.length === 0)
484
419
  return;
485
420
  try {
@@ -516,8 +451,8 @@ function saveEvictedContent(lines, sessionId, sessionName, indices) {
516
451
  const entries = content.split('\n').filter(l => l.trim());
517
452
  fs.writeFileSync(EVICTED_STORE, entries.slice(-100).join('\n') + '\n');
518
453
  }
519
- // Send to cloud for cross-session retrieval (non-blocking)
520
- captureEvictedToCloud(lines, sessionId);
454
+ // No direct cloud capture here. Proxy/R2 handshake owns remote persistence.
455
+ // Keep this local file only for operator debugging.
521
456
  }
522
457
  catch {
523
458
  // Silent fail
@@ -502,6 +502,7 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
502
502
  let sessionName = initialSessionName;
503
503
  const blessed = require('blessed');
504
504
  const contrib = require('blessed-contrib');
505
+ const inTmux = process.env.TMUX !== undefined;
505
506
  // ══════════════════════════════════════════════════════════════════════════
506
507
  // TMUX SPLIT PANE ISOLATION
507
508
  // When dashboard runs in a separate tmux pane from `ekkos run`, blessed must
@@ -661,11 +662,11 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
661
662
  scrollable: true,
662
663
  alwaysScroll: true,
663
664
  scrollbar: { ch: '│', style: { fg: 'cyan' } },
664
- keys: true, // Enable keyboard scrolling
665
- vi: true, // Enable vi-style keys (j/k for scroll)
665
+ keys: !inTmux, // In tmux split mode keep dashboard passive
666
+ vi: !inTmux, // Avoid single-key handlers interfering with paste
666
667
  mouse: false, // Mouse disabled (use keyboard for scrolling, allows text selection)
667
- input: true,
668
- interactive: true, // Make box interactive for scrolling
668
+ input: !inTmux,
669
+ interactive: !inTmux, // Standalone only; passive in tmux split
669
670
  label: ' Turns (scroll: ↑↓/k/j, page: PgUp/u, home/end: g/G) ',
670
671
  border: { type: 'line', fg: 'cyan' },
671
672
  style: { fg: 'white', border: { fg: 'cyan' } },
@@ -1034,7 +1035,9 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
1034
1035
  ` ${routingStr}` +
1035
1036
  ` R[A:${data.replayAppliedCount} SZ:${data.replaySkippedSizeCount} ST:${data.replaySkipStoreCount}]` +
1036
1037
  savingsStr +
1037
- ` {gray-fg}? help q quit r refresh{/gray-fg}`);
1038
+ (inTmux
1039
+ ? ` {gray-fg}Ctrl+C quit{/gray-fg}`
1040
+ : ` {gray-fg}? help q quit r refresh{/gray-fg}`));
1038
1041
  }
1039
1042
  catch (err) {
1040
1043
  dlog(`Footer: ${err.message}`);
@@ -1155,90 +1158,99 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
1155
1158
  // KEYBOARD SHORTCUTS - Only capture when dashboard pane has focus
1156
1159
  // In tmux split mode, this prevents capturing keys from Claude Code pane
1157
1160
  // ══════════════════════════════════════════════════════════════════════════
1158
- screen.key(['q', 'C-c'], () => {
1161
+ screen.key(['C-c'], () => {
1159
1162
  clearInterval(pollInterval);
1160
1163
  clearInterval(windowPollInterval);
1161
1164
  clearTimeout(sparkleTimer);
1162
1165
  screen.destroy();
1163
1166
  process.exit(0);
1164
1167
  });
1165
- screen.key(['r'], () => {
1166
- lastFileSize = 0;
1167
- updateDashboard();
1168
- updateWindowBox();
1169
- });
1168
+ if (!inTmux) {
1169
+ screen.key(['q'], () => {
1170
+ clearInterval(pollInterval);
1171
+ clearInterval(windowPollInterval);
1172
+ clearTimeout(sparkleTimer);
1173
+ screen.destroy();
1174
+ process.exit(0);
1175
+ });
1176
+ screen.key(['r'], () => {
1177
+ lastFileSize = 0;
1178
+ updateDashboard();
1179
+ updateWindowBox();
1180
+ });
1181
+ }
1170
1182
  // ══════════════════════════════════════════════════════════════════════════
1171
1183
  // FOCUS MANAGEMENT: In tmux split mode, DON'T auto-focus the turnBox
1172
1184
  // This prevents the dashboard from stealing focus from Claude Code on startup
1173
1185
  // User can manually focus by clicking into the dashboard pane
1174
1186
  // ══════════════════════════════════════════════════════════════════════════
1175
- // Check if we're in a tmux session
1176
- const inTmux = process.env.TMUX !== undefined;
1177
1187
  if (!inTmux) {
1178
1188
  // Only auto-focus when running standalone (not in tmux split)
1179
1189
  turnBox.focus();
1180
1190
  }
1181
1191
  // Scroll controls for turn table
1182
- screen.key(['up', 'k'], () => {
1183
- turnBox.scroll(-1);
1184
- screen.render();
1185
- });
1186
- screen.key(['down', 'j'], () => {
1187
- turnBox.scroll(1);
1188
- screen.render();
1189
- });
1190
- screen.key(['pageup', 'u'], () => {
1191
- turnBox.scroll(-(turnBox.height - 2));
1192
- screen.render();
1193
- });
1194
- screen.key(['pagedown', 'd'], () => {
1195
- turnBox.scroll((turnBox.height - 2));
1196
- screen.render();
1197
- });
1198
- screen.key(['home', 'g'], () => {
1199
- turnBox.setScrollPerc(0);
1200
- screen.render();
1201
- });
1202
- screen.key(['end', 'G'], () => {
1203
- turnBox.setScrollPerc(100);
1204
- screen.render();
1205
- });
1206
- screen.key(['?', 'h'], () => {
1207
- // Quick help overlay
1208
- const help = blessed.box({
1209
- top: 'center',
1210
- left: 'center',
1211
- width: 50,
1212
- height: 16,
1213
- content: ('{bold}Navigation{/bold}\n' +
1214
- ' ↑/k/j/↓ Scroll line\n' +
1215
- ' PgUp/u Scroll page up\n' +
1216
- ' PgDn/d Scroll page down\n' +
1217
- ' g/Home Scroll to top\n' +
1218
- ' G/End Scroll to bottom\n' +
1219
- '\n' +
1220
- '{bold}Controls{/bold}\n' +
1221
- ' r Refresh now\n' +
1222
- ' q/Ctrl+C Quit\n' +
1223
- '\n' +
1224
- '{gray-fg}Press any key to close{/gray-fg}'),
1225
- tags: true,
1226
- border: 'line',
1227
- style: { border: { fg: 'cyan' } },
1228
- padding: 1,
1192
+ if (!inTmux) {
1193
+ screen.key(['up', 'k'], () => {
1194
+ turnBox.scroll(-1);
1195
+ screen.render();
1229
1196
  });
1230
- screen.append(help);
1231
- screen.render();
1232
- // Defer listener so the '?' keypress that opened help doesn't immediately close it
1233
- setImmediate(() => {
1234
- const closeHelp = () => {
1235
- help.destroy();
1236
- screen.render();
1237
- screen.removeListener('key', closeHelp);
1238
- };
1239
- screen.once('key', closeHelp);
1197
+ screen.key(['down', 'j'], () => {
1198
+ turnBox.scroll(1);
1199
+ screen.render();
1240
1200
  });
1241
- });
1201
+ screen.key(['pageup', 'u'], () => {
1202
+ turnBox.scroll(-(turnBox.height - 2));
1203
+ screen.render();
1204
+ });
1205
+ screen.key(['pagedown', 'd'], () => {
1206
+ turnBox.scroll((turnBox.height - 2));
1207
+ screen.render();
1208
+ });
1209
+ screen.key(['home', 'g'], () => {
1210
+ turnBox.setScrollPerc(0);
1211
+ screen.render();
1212
+ });
1213
+ screen.key(['end', 'G'], () => {
1214
+ turnBox.setScrollPerc(100);
1215
+ screen.render();
1216
+ });
1217
+ screen.key(['?', 'h'], () => {
1218
+ // Quick help overlay
1219
+ const help = blessed.box({
1220
+ top: 'center',
1221
+ left: 'center',
1222
+ width: 50,
1223
+ height: 16,
1224
+ content: ('{bold}Navigation{/bold}\n' +
1225
+ ' ↑/k/j/↓ Scroll line\n' +
1226
+ ' PgUp/u Scroll page up\n' +
1227
+ ' PgDn/d Scroll page down\n' +
1228
+ ' g/Home Scroll to top\n' +
1229
+ ' G/End Scroll to bottom\n' +
1230
+ '\n' +
1231
+ '{bold}Controls{/bold}\n' +
1232
+ ' r Refresh now\n' +
1233
+ ' q/Ctrl+C Quit\n' +
1234
+ '\n' +
1235
+ '{gray-fg}Press any key to close{/gray-fg}'),
1236
+ tags: true,
1237
+ border: 'line',
1238
+ style: { border: { fg: 'cyan' } },
1239
+ padding: 1,
1240
+ });
1241
+ screen.append(help);
1242
+ screen.render();
1243
+ // Defer listener so the '?' keypress that opened help doesn't immediately close it
1244
+ setImmediate(() => {
1245
+ const closeHelp = () => {
1246
+ help.destroy();
1247
+ screen.render();
1248
+ screen.removeListener('key', closeHelp);
1249
+ };
1250
+ screen.once('key', closeHelp);
1251
+ });
1252
+ });
1253
+ }
1242
1254
  // Clear terminal buffer — prevents garbage text from previous commands
1243
1255
  screen.program.clear();
1244
1256
  // Dashboard is fully passive — no widget captures keyboard input
@@ -43,139 +43,6 @@ 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
- }
179
46
  const state_1 = require("../utils/state");
180
47
  const session_binding_1 = require("../utils/session-binding");
181
48
  const doctor_1 = require("./doctor");
@@ -443,7 +310,7 @@ const isWindows = os.platform() === 'win32';
443
310
  // 'latest' = use latest version, or specify like '2.1.33' for specific version
444
311
  // Core ekkOS patches (eviction, context management) work with all recent versions
445
312
  // Cosmetic patches may fail on newer versions but don't affect functionality
446
- const PINNED_CLAUDE_VERSION = '2.1.45';
313
+ const PINNED_CLAUDE_VERSION = 'latest';
447
314
  // Max output tokens for Claude responses
448
315
  // Default: 16384 (safe for Sonnet 4.5)
449
316
  // Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
@@ -533,9 +400,51 @@ function getEkkosEnv() {
533
400
  }
534
401
  return env;
535
402
  }
536
- // ekkOS-managed Claude installation path
403
+ // ekkOS-managed Claude installation path (npm fallback)
537
404
  const EKKOS_CLAUDE_DIR = path.join(os.homedir(), '.ekkos', 'claude-code');
538
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
+ }
539
448
  /**
540
449
  * Check if a Claude installation exists and get its version
541
450
  * Returns version string if found, null otherwise
@@ -637,34 +546,32 @@ function installEkkosClaudeVersion() {
637
546
  }
638
547
  }
639
548
  /**
640
- * Resolve full path to claude executable
641
- * Returns direct path if found with correct version, otherwise 'npx:VERSION'
642
- *
643
- * IMPORTANT: We pin to a specific Claude Code version (currently 2.1.33) to ensure
644
- * consistent behavior with ekkOS context management and eviction patches.
645
- *
646
- * CRITICAL: ekkos run ONLY uses the ekkOS-managed installation at ~/.ekkos/claude-code/
647
- * This ensures complete separation from the user's existing Claude installation (Homebrew/npm).
648
- * The user's `claude` command remains untouched and can be any version.
549
+ * Resolve full path to claude executable.
649
550
  *
650
551
  * Priority:
651
- * 1. ekkOS-managed installation (~/.ekkos/claude-code) - ONLY option for ekkos run
652
- * 2. Auto-install if doesn't exist
653
- * 3. npx with pinned version (fallback if install fails)
552
+ * 1. Native installer binary (~/.local/share/claude/versions/<latest>) no npm warning
553
+ * 2. ekkOS-managed npm installation (~/.ekkos/claude-code) — existing installs
554
+ * 3. Auto-install via npm to ekkOS-managed directory
555
+ * 4. npx fallback (rare, shows npm deprecation warning)
654
556
  */
655
557
  function resolveClaudePath() {
656
- // PRIORITY 1: ekkOS-managed installation
558
+ // PRIORITY 1: Native installer binary (Anthropic's official distribution)
559
+ // These binaries do not print the npm-to-native migration warning.
560
+ const nativeBin = resolveNativeClaudeBin();
561
+ if (nativeBin) {
562
+ return nativeBin;
563
+ }
564
+ // PRIORITY 2: ekkOS-managed npm installation (existing users before native installer)
657
565
  if (fs.existsSync(EKKOS_CLAUDE_BIN) && checkClaudeVersion(EKKOS_CLAUDE_BIN)) {
658
566
  return EKKOS_CLAUDE_BIN;
659
567
  }
660
- // PRIORITY 2: Auto-install to ekkOS-managed directory (user's Claude stays untouched)
568
+ // PRIORITY 3: Auto-install to ekkOS-managed directory
661
569
  if (installEkkosClaudeVersion()) {
662
570
  if (fs.existsSync(EKKOS_CLAUDE_BIN)) {
663
571
  return EKKOS_CLAUDE_BIN;
664
572
  }
665
573
  }
666
- // PRIORITY 3: Fall back to npx with pinned version (shows update message)
667
- // This is rare - only happens if install failed
574
+ // PRIORITY 4: Fall back to npx (rare only if install failed, will show deprecation warning)
668
575
  return `npx:${PINNED_CLAUDE_VERSION}`;
669
576
  }
670
577
  /**
@@ -1074,22 +981,15 @@ async function run(options) {
1074
981
  }
1075
982
  }
1076
983
  // ══════════════════════════════════════════════════════════════════════════
1077
- // ccDNA AUTO-PATCH: Apply Claude Code customizations before spawn
1078
- // This patches the context warning, themes, and other ccDNA features
1079
- // Skip if --no-dna flag is set
984
+ // Legacy ccDNA flags are now no-ops.
985
+ // Context eviction and replay are owned by the proxy.
1080
986
  // ══════════════════════════════════════════════════════════════════════════
1081
987
  const noDna = options.noDna || false;
1082
- let ccdnaVersion = null;
1083
- if (noDna) {
1084
- if (verbose) {
1085
- console.log(chalk_1.default.yellow(' ⏭️ Skipping ccDNA injection (--no-dna)'));
1086
- }
988
+ if (verbose && noDna) {
989
+ console.log(chalk_1.default.gray(' --skip-dna is deprecated (ccDNA patching is removed)'));
1087
990
  }
1088
- else {
1089
- if (verbose && claudeCliPath) {
1090
- console.log(chalk_1.default.gray(` 🔧 Patching: ${claudeCliPath}`));
1091
- }
1092
- ccdnaVersion = applyCcdnaPatches(verbose, claudeCliPath);
991
+ if (verbose && claudeCliPath) {
992
+ console.log(chalk_1.default.gray(` 🤖 Claude CLI: ${claudeCliPath}`));
1093
993
  }
1094
994
  const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
1095
995
  const claudePath = isNpxMode ? 'npx' : rawClaudePath;
@@ -1250,10 +1150,10 @@ async function run(options) {
1250
1150
  logoLines.forEach(line => console.log(chalk_1.default.magenta(line)));
1251
1151
  console.log('');
1252
1152
  // ══════════════════════════════════════════════════════════════════════════
1253
- // ANIMATED TITLE: "Cognitive Continuity Engine" with orange/white shine
1153
+ // ANIMATED TITLE: "ekkOS_Pulse" with orange/white shine
1254
1154
  // ══════════════════════════════════════════════════════════════════════════
1255
- const titleText = 'Cognitive Continuity Engine';
1256
- const taglineText = 'Context is finite. Intelligence isn\'t.';
1155
+ const titleText = 'ekkOS_Pulse';
1156
+ const taglineText = 'Infinite context. Native model quality.';
1257
1157
  // Color palette for shine effect
1258
1158
  const whiteShine = chalk_1.default.whiteBright;
1259
1159
  // Phase 1: Typewriter effect for title
@@ -1333,9 +1233,6 @@ async function run(options) {
1333
1233
  if (bypass) {
1334
1234
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1335
1235
  }
1336
- if (noDna) {
1337
- console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1338
- }
1339
1236
  if (verbose) {
1340
1237
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1341
1238
  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)`));
@@ -1348,9 +1245,6 @@ async function run(options) {
1348
1245
  if (bypass) {
1349
1246
  console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
1350
1247
  }
1351
- if (noDna) {
1352
- console.log(chalk_1.default.yellow(' ⏭️ ccDNA injection skipped (--no-dna)'));
1353
- }
1354
1248
  if (verbose) {
1355
1249
  console.log(chalk_1.default.gray(` 📁 Debug log: ${config.debugLogPath}`));
1356
1250
  }
@@ -1791,12 +1685,8 @@ async function run(options) {
1791
1685
  console.log(chalk_1.default.yellow(' Monitor-only mode (--no-inject)'));
1792
1686
  }
1793
1687
  if (verbose) {
1794
- // Show Claude version with ccDNA version if patched
1795
1688
  const ccVersion = pinnedVersion || PINNED_CLAUDE_VERSION;
1796
- const versionStr = ccdnaVersion
1797
- ? `Claude Code v${ccVersion} + ekkOS_Continuum v${ccdnaVersion}`
1798
- : `Claude Code v${ccVersion}`;
1799
- console.log(chalk_1.default.gray(` 🤖 ${versionStr}`));
1689
+ console.log(chalk_1.default.gray(` 🤖 Claude Code v${ccVersion}`));
1800
1690
  if (currentSession) {
1801
1691
  console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
1802
1692
  }
package/dist/index.js CHANGED
@@ -246,7 +246,7 @@ commander_1.program
246
246
  .option('-d, --doctor', 'Run diagnostics before starting')
247
247
  .option('-r, --research', 'Auto-run research agent on startup (scans arXiv for new AI papers)')
248
248
  .option('--skip-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
249
- .option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
249
+ .option('--skip-dna', 'Deprecated no-op (legacy ccDNA patching has been removed)')
250
250
  .option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
251
251
  .option('--dashboard', 'Launch with live usage dashboard in an isolated 60/40 tmux split (requires tmux)')
252
252
  .option('--kickstart', 'Auto-send "test" on load to create session immediately (used internally by --dashboard)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {