@ekkos/cli 1.2.15 → 1.2.17
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/commands/dashboard.js +69 -85
- package/dist/commands/run.js +58 -21
- package/package.json +1 -1
|
@@ -565,21 +565,24 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
565
565
|
// 3. Claude Code pane remains fully independent
|
|
566
566
|
// 4. Text selection works across panes without interference
|
|
567
567
|
// ══════════════════════════════════════════════════════════════════════════
|
|
568
|
-
// Open /dev/tty for blessed to use (gives it direct terminal access)
|
|
568
|
+
// Open /dev/tty for blessed to use (gives it direct terminal access).
|
|
569
|
+
// On macOS/Linux in tmux, this isolates the dashboard from stdout piping.
|
|
570
|
+
// On Windows (no /dev/tty), stdin/stdout is correct — blessed handles the
|
|
571
|
+
// alternate screen buffer natively for clean rendering.
|
|
569
572
|
const fs = require('fs');
|
|
570
573
|
let ttyInput = process.stdin;
|
|
571
574
|
let ttyOutput = process.stdout;
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
575
|
+
if (inTmux) {
|
|
576
|
+
try {
|
|
577
|
+
const ttyFd = fs.openSync('/dev/tty', 'r+');
|
|
578
|
+
ttyInput = new (require('tty').ReadStream)(ttyFd);
|
|
579
|
+
ttyOutput = new (require('tty').WriteStream)(ttyFd);
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
// Fallback: if /dev/tty unavailable, use stdin/stdout
|
|
583
|
+
ttyInput = process.stdin;
|
|
584
|
+
ttyOutput = process.stdout;
|
|
585
|
+
}
|
|
583
586
|
}
|
|
584
587
|
const screen = blessed.screen({
|
|
585
588
|
smartCSR: true,
|
|
@@ -596,16 +599,17 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
596
599
|
resizeTimeout: 300, // Debounce resize events
|
|
597
600
|
});
|
|
598
601
|
// ══════════════════════════════════════════════════════════════════════════
|
|
599
|
-
// DISABLE TERMINAL CONTROL SEQUENCE HIJACKING
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
602
|
+
// DISABLE TERMINAL CONTROL SEQUENCE HIJACKING (TMUX ONLY)
|
|
603
|
+
// When running in a tmux split pane alongside Claude Code, prevent blessed
|
|
604
|
+
// from sending control sequences that would interfere with the other pane.
|
|
605
|
+
// In standalone mode (including Windows Terminal split), blessed needs the
|
|
606
|
+
// alternate buffer and raw mode for clean rendering without artifacts.
|
|
603
607
|
// ══════════════════════════════════════════════════════════════════════════
|
|
604
|
-
if (screen.program) {
|
|
605
|
-
// Override alternateBuffer to do nothing
|
|
608
|
+
if (inTmux && screen.program) {
|
|
609
|
+
// Override alternateBuffer to do nothing (tmux pane isolation)
|
|
606
610
|
screen.program.alternateBuffer = () => { };
|
|
607
611
|
screen.program.normalBuffer = () => { };
|
|
608
|
-
// Don't enter raw mode — let the OS handle it
|
|
612
|
+
// Don't enter raw mode — let the OS handle it (tmux pane isolation)
|
|
609
613
|
if (screen.program.setRawMode) {
|
|
610
614
|
screen.program.setRawMode = (enabled) => {
|
|
611
615
|
// Silently ignore raw mode requests
|
|
@@ -1077,72 +1081,50 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1077
1081
|
let header = '';
|
|
1078
1082
|
let separator = '';
|
|
1079
1083
|
let rows = [];
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
else if (w >= 30) {
|
|
1110
|
-
// Compact mode: drop cache split columns to preserve readable headers.
|
|
1111
|
-
const colNum = 4;
|
|
1112
|
-
const colM = 6;
|
|
1113
|
-
const colCtx = 6;
|
|
1114
|
-
const colCost = 6;
|
|
1115
|
-
const nDividers = 4;
|
|
1116
|
-
const outW = Math.max(4, w - (colNum + colM + colCtx + colCost + nDividers));
|
|
1117
|
-
header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Context', colCtx)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
|
|
1118
|
-
separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
|
|
1119
|
-
rows = turns.map(t => {
|
|
1120
|
-
const mTag = modelTag(t.routedModel);
|
|
1121
|
-
const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
|
|
1122
|
-
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1123
|
-
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1124
|
-
return (pad(String(t.turn), colNum) + div +
|
|
1125
|
-
`{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
|
|
1126
|
-
rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1127
|
-
`{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
|
|
1128
|
-
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1129
|
-
});
|
|
1130
|
-
}
|
|
1131
|
-
else {
|
|
1132
|
-
// Minimal mode: guaranteed no-wrap fallback.
|
|
1133
|
-
const colNum = 4;
|
|
1134
|
-
const colCtx = 6;
|
|
1135
|
-
const colCost = 6;
|
|
1136
|
-
header = `${pad('Turn', colNum)}${div}${rpad('Context', colCtx)}${div}${rpad('Cost', colCost)}`;
|
|
1137
|
-
separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(colCost)}`;
|
|
1138
|
-
rows = turns.map(t => {
|
|
1139
|
-
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1140
|
-
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1141
|
-
return (pad(String(t.turn), colNum) + div +
|
|
1142
|
-
rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1143
|
-
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1144
|
-
});
|
|
1084
|
+
// Always show all 7 columns: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
|
|
1085
|
+
// Shrink flex columns to fit narrow panes instead of dropping them.
|
|
1086
|
+
const colNum = 4;
|
|
1087
|
+
const colM = 7;
|
|
1088
|
+
const colCtx = 7;
|
|
1089
|
+
const colCost = 8;
|
|
1090
|
+
const nDividers = 6;
|
|
1091
|
+
const fixedW = colNum + colM + colCtx + colCost;
|
|
1092
|
+
const flexTotal = Math.max(0, w - fixedW - nDividers);
|
|
1093
|
+
let rdW = Math.max(5, Math.floor(flexTotal * 0.35));
|
|
1094
|
+
let wrW = Math.max(5, Math.floor(flexTotal * 0.30));
|
|
1095
|
+
let outW = Math.max(6, flexTotal - rdW - wrW);
|
|
1096
|
+
// Trim flex columns if they overflow available width
|
|
1097
|
+
let totalW = fixedW + nDividers + rdW + wrW + outW;
|
|
1098
|
+
if (totalW > w) {
|
|
1099
|
+
let overflow = totalW - w;
|
|
1100
|
+
const trimOut = Math.min(overflow, Math.max(0, outW - 4));
|
|
1101
|
+
outW -= trimOut;
|
|
1102
|
+
overflow -= trimOut;
|
|
1103
|
+
if (overflow > 0) {
|
|
1104
|
+
const trimWr = Math.min(overflow, Math.max(0, wrW - 4));
|
|
1105
|
+
wrW -= trimWr;
|
|
1106
|
+
overflow -= trimWr;
|
|
1107
|
+
}
|
|
1108
|
+
if (overflow > 0) {
|
|
1109
|
+
const trimRd = Math.min(overflow, Math.max(0, rdW - 4));
|
|
1110
|
+
rdW -= trimRd;
|
|
1111
|
+
}
|
|
1145
1112
|
}
|
|
1113
|
+
header = `{bold}${pad('Turn', colNum)}${div}${pad('Model', colM)}${div}${pad('Context', colCtx)}${div}${rpad('Cache Rd', rdW)}${div}${rpad('Cache Wr', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}{/bold}`;
|
|
1114
|
+
separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
|
|
1115
|
+
rows = turns.map(t => {
|
|
1116
|
+
const mTag = modelTag(t.routedModel);
|
|
1117
|
+
const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
|
|
1118
|
+
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1119
|
+
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1120
|
+
return (pad(String(t.turn), colNum) + div +
|
|
1121
|
+
`{${mColor}-fg}${pad(mTag, colM)}{/${mColor}-fg}` + div +
|
|
1122
|
+
pad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1123
|
+
`{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
|
|
1124
|
+
`{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
|
|
1125
|
+
`{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
|
|
1126
|
+
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1127
|
+
});
|
|
1146
1128
|
const lines = [header, separator, ...rows];
|
|
1147
1129
|
turnBox.setContent(lines.join('\n'));
|
|
1148
1130
|
// Restore scroll position AFTER content update
|
|
@@ -1404,11 +1386,13 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1404
1386
|
// Delay first ccusage call — let blessed render first, then load heavy data
|
|
1405
1387
|
setTimeout(() => updateWindowBox(), 2000);
|
|
1406
1388
|
const pollInterval = setInterval(updateDashboard, refreshMs);
|
|
1389
|
+
// Animation interval — slower on Windows to reduce blessed redraw flicker
|
|
1390
|
+
const animMs = process.platform === 'win32' ? 1000 : 500;
|
|
1407
1391
|
const headerAnimInterval = setInterval(() => {
|
|
1408
1392
|
// Keep advancing across the full session label; wrap at a large value.
|
|
1409
1393
|
waveOffset = (waveOffset + 1) % 1000000;
|
|
1410
1394
|
renderHeader();
|
|
1411
|
-
},
|
|
1395
|
+
}, animMs);
|
|
1412
1396
|
const fortuneInterval = setInterval(() => {
|
|
1413
1397
|
if (activeFortunes.length === 0)
|
|
1414
1398
|
return;
|
package/dist/commands/run.js
CHANGED
|
@@ -912,10 +912,11 @@ function launchWithWindowsTerminal(options) {
|
|
|
912
912
|
if (options.noProxy)
|
|
913
913
|
runArgs.push('--skip-proxy');
|
|
914
914
|
// Write a temp batch file to avoid all quoting issues
|
|
915
|
+
// wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
|
|
915
916
|
const batPath = path.join(os.tmpdir(), `ekkos-wt-${launchTime}.cmd`);
|
|
916
917
|
const batContent = [
|
|
917
918
|
'@echo off',
|
|
918
|
-
`wt --title "Claude Code" -d "${cwd}" ekkos ${runArgs.join(' ')} ; split-pane -V -s 0.4 --title "ekkOS Dashboard" -d "${cwd}" ekkos dashboard --wait-for-new --refresh 2000`,
|
|
919
|
+
`wt --title "Claude Code" -d "${cwd}" cmd /c ekkos ${runArgs.join(' ')} ; split-pane -V -s 0.4 --title "ekkOS Dashboard" -d "${cwd}" cmd /c ekkos dashboard --wait-for-new --refresh 2000`,
|
|
919
920
|
].join('\r\n');
|
|
920
921
|
try {
|
|
921
922
|
fs.writeFileSync(batPath, batContent);
|
|
@@ -1083,7 +1084,10 @@ async function run(options) {
|
|
|
1083
1084
|
}
|
|
1084
1085
|
}
|
|
1085
1086
|
// Check PTY availability early (deterministic, no async race)
|
|
1086
|
-
|
|
1087
|
+
// Windows: SKIP node-pty entirely. ConPTY corrupts Ink's cursor management sequences
|
|
1088
|
+
// (hide/show/move), causing ghost block cursor artifacts. Use stdio:'inherit' spawn
|
|
1089
|
+
// instead so Ink gets direct terminal access. Hooks still handle session tracking.
|
|
1090
|
+
const loadedPty = isWindows ? null : await loadPty();
|
|
1087
1091
|
const usePty = loadedPty !== null;
|
|
1088
1092
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1089
1093
|
// CONCURRENT STARTUP: Spawn Claude while animation runs
|
|
@@ -1319,8 +1323,10 @@ async function run(options) {
|
|
|
1319
1323
|
console.log('');
|
|
1320
1324
|
}
|
|
1321
1325
|
else {
|
|
1326
|
+
// Static banner for Windows / remote / no-splash — no cursor manipulation
|
|
1327
|
+
console.log('');
|
|
1328
|
+
console.log(chalk_1.default.hex('#FF6B35').bold(' ekkOS_Pulse') + chalk_1.default.gray(' — Context is finite. Intelligence isn\'t.'));
|
|
1322
1329
|
console.log('');
|
|
1323
|
-
console.log(chalk_1.default.cyan(' ekkOS remote session ready'));
|
|
1324
1330
|
if (bypass) {
|
|
1325
1331
|
console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
|
|
1326
1332
|
}
|
|
@@ -1413,18 +1419,50 @@ async function run(options) {
|
|
|
1413
1419
|
// Claude creates the transcript file BEFORE outputting the session name
|
|
1414
1420
|
// So we watch for new files rather than parsing TUI output (which is slower)
|
|
1415
1421
|
// ════════════════════════════════════════════════════════════════════════════
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1422
|
+
const projectPath = process.cwd();
|
|
1423
|
+
const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
|
|
1424
|
+
const projectDirCandidates = (() => {
|
|
1425
|
+
// Claude's project-dir encoding is platform/version dependent.
|
|
1426
|
+
// Probe a small set of known-safe variants to avoid missing the session file.
|
|
1427
|
+
const encodings = new Set([
|
|
1428
|
+
projectPath.replace(/\//g, '-'),
|
|
1429
|
+
projectPath.replace(/[\\/]/g, '-'),
|
|
1430
|
+
projectPath.replace(/[:\\/]/g, '-'),
|
|
1431
|
+
`-${projectPath.replace(/[:\\/]/g, '-').replace(/^-+/, '')}`,
|
|
1432
|
+
projectPath.replace(/[^a-zA-Z0-9]/g, '-'),
|
|
1433
|
+
`-${projectPath.replace(/^[\\/]+/, '').replace(/[^a-zA-Z0-9]/g, '-')}`,
|
|
1434
|
+
]);
|
|
1435
|
+
return [...encodings]
|
|
1436
|
+
.filter(Boolean)
|
|
1437
|
+
.map(encoded => path.join(projectsRoot, encoded));
|
|
1438
|
+
})();
|
|
1418
1439
|
const launchTime = Date.now();
|
|
1440
|
+
function listCandidateJsonlFiles() {
|
|
1441
|
+
const jsonlFiles = [];
|
|
1442
|
+
for (const candidateDir of projectDirCandidates) {
|
|
1443
|
+
if (!fs.existsSync(candidateDir))
|
|
1444
|
+
continue;
|
|
1445
|
+
try {
|
|
1446
|
+
for (const file of fs.readdirSync(candidateDir)) {
|
|
1447
|
+
if (file.endsWith('.jsonl')) {
|
|
1448
|
+
jsonlFiles.push(path.join(candidateDir, file));
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
catch {
|
|
1453
|
+
// Ignore candidate-dir read errors and keep scanning others.
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
return jsonlFiles;
|
|
1457
|
+
}
|
|
1419
1458
|
// Track existing jsonl files at startup
|
|
1420
1459
|
let existingJsonlFiles = new Set();
|
|
1421
1460
|
try {
|
|
1422
|
-
|
|
1423
|
-
existingJsonlFiles = new Set(files.filter(f => f.endsWith('.jsonl')));
|
|
1461
|
+
existingJsonlFiles = new Set(listCandidateJsonlFiles());
|
|
1424
1462
|
dlog(`[TRANSCRIPT] Found ${existingJsonlFiles.size} existing jsonl files at startup`);
|
|
1425
1463
|
}
|
|
1426
1464
|
catch {
|
|
1427
|
-
dlog('[TRANSCRIPT]
|
|
1465
|
+
dlog('[TRANSCRIPT] No candidate project dir exists yet');
|
|
1428
1466
|
}
|
|
1429
1467
|
// Poll for new transcript file every 500ms for up to 30 seconds.
|
|
1430
1468
|
// Safety rule: do NOT guess using "most recent" files; that can cross-bind sessions.
|
|
@@ -1444,13 +1482,11 @@ async function run(options) {
|
|
|
1444
1482
|
// In proxy mode this is intentionally disabled to avoid cross-session mixing.
|
|
1445
1483
|
if (!proxyModeEnabled && !transcriptPath) {
|
|
1446
1484
|
try {
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
path: path.join(projectDir, f),
|
|
1453
|
-
mtime: fs.statSync(path.join(projectDir, f)).mtimeMs
|
|
1485
|
+
const jsonlFiles = listCandidateJsonlFiles()
|
|
1486
|
+
.map(fullPath => ({
|
|
1487
|
+
name: path.basename(fullPath),
|
|
1488
|
+
path: fullPath,
|
|
1489
|
+
mtime: fs.statSync(fullPath).mtimeMs
|
|
1454
1490
|
}))
|
|
1455
1491
|
.sort((a, b) => b.mtime - a.mtime);
|
|
1456
1492
|
if (jsonlFiles.length > 0) {
|
|
@@ -1480,14 +1516,13 @@ async function run(options) {
|
|
|
1480
1516
|
return;
|
|
1481
1517
|
}
|
|
1482
1518
|
try {
|
|
1483
|
-
const
|
|
1484
|
-
const jsonlFiles = currentFiles.filter(f => f.endsWith('.jsonl'));
|
|
1519
|
+
const jsonlFiles = listCandidateJsonlFiles();
|
|
1485
1520
|
// Find NEW files (created after we started)
|
|
1486
1521
|
for (const file of jsonlFiles) {
|
|
1487
1522
|
if (!existingJsonlFiles.has(file)) {
|
|
1488
1523
|
// New file! This is our transcript
|
|
1489
|
-
const fullPath =
|
|
1490
|
-
const sessionId = file.replace('.jsonl', '');
|
|
1524
|
+
const fullPath = file;
|
|
1525
|
+
const sessionId = path.basename(file).replace('.jsonl', '');
|
|
1491
1526
|
transcriptPath = fullPath;
|
|
1492
1527
|
currentSessionId = sessionId;
|
|
1493
1528
|
currentSession = (0, state_1.uuidToWords)(sessionId);
|
|
@@ -1602,8 +1637,10 @@ async function run(options) {
|
|
|
1602
1637
|
function resolveTranscriptFromSessionId(source) {
|
|
1603
1638
|
if (!currentSessionId || transcriptPath)
|
|
1604
1639
|
return;
|
|
1605
|
-
const candidate =
|
|
1606
|
-
|
|
1640
|
+
const candidate = projectDirCandidates
|
|
1641
|
+
.map(projectDir => path.join(projectDir, `${currentSessionId}.jsonl`))
|
|
1642
|
+
.find(fullPath => fs.existsSync(fullPath));
|
|
1643
|
+
if (!candidate)
|
|
1607
1644
|
return;
|
|
1608
1645
|
transcriptPath = candidate;
|
|
1609
1646
|
evictionDebugLog('TRANSCRIPT_SET', `Set from session ID (${source})`, {
|