@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.
- package/dist/capture/jsonl-rewriter.js +7 -72
- package/dist/commands/dashboard.js +83 -71
- package/dist/commands/run.js +68 -178
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -283,9 +283,8 @@ function truncateToolResult(line) {
|
|
|
283
283
|
return line;
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
-
//
|
|
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
|
|
479
|
-
* @param
|
|
480
|
-
* @param
|
|
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,
|
|
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
|
-
//
|
|
520
|
-
|
|
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:
|
|
665
|
-
vi:
|
|
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:
|
|
668
|
-
interactive:
|
|
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
|
-
|
|
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(['
|
|
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
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
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.
|
|
1231
|
-
|
|
1232
|
-
|
|
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
|
package/dist/commands/run.js
CHANGED
|
@@ -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 = '
|
|
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.
|
|
652
|
-
* 2.
|
|
653
|
-
* 3.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
1078
|
-
//
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
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: "
|
|
1153
|
+
// ANIMATED TITLE: "ekkOS_Pulse" with orange/white shine
|
|
1254
1154
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1255
|
-
const titleText = '
|
|
1256
|
-
const taglineText = '
|
|
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
|
-
|
|
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', '
|
|
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)')
|