@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.
@@ -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
- try {
573
- // Create readable/writable streams from /dev/tty file descriptors
574
- const ttyFd = fs.openSync('/dev/tty', 'r+');
575
- ttyInput = new (require('tty').ReadStream)(ttyFd);
576
- ttyOutput = new (require('tty').WriteStream)(ttyFd);
577
- }
578
- catch (e) {
579
- // Fallback: if /dev/tty unavailable (some CI/remote environments),
580
- // use stdin/stdout but disable all event handling
581
- ttyInput = process.stdin;
582
- ttyOutput = process.stdout;
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
- // Prevent blessed from sending control sequences that might interfere with
601
- // other panes in tmux. The /dev/tty approach already isolates this, but we
602
- // also disable the raw mode entirely to be extra safe.
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
- // Responsive table layouts keep headers visible even in very narrow panes.
1081
- if (w >= 60) {
1082
- // Full mode: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
1083
- const colNum = 4;
1084
- const colM = 7;
1085
- const colCtx = 7;
1086
- const colCost = 8;
1087
- const nDividers = 6;
1088
- const fixedW = colNum + colM + colCtx + colCost;
1089
- const flexTotal = w - fixedW - nDividers;
1090
- const rdW = Math.max(10, Math.floor(flexTotal * 0.35));
1091
- const wrW = Math.max(11, Math.floor(flexTotal * 0.30));
1092
- const outW = Math.max(6, flexTotal - rdW - wrW);
1093
- header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Contex', colCtx)}${div}${rpad('Cache Read', rdW)}${div}${rpad('Cache Write', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
1094
- separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
1095
- rows = turns.map(t => {
1096
- const mTag = modelTag(t.routedModel);
1097
- const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1098
- const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1099
- const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1100
- return (pad(String(t.turn), colNum) + div +
1101
- `{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
1102
- rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1103
- `{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
1104
- `{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
1105
- `{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
1106
- costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
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
- }, 500);
1395
+ }, animMs);
1412
1396
  const fortuneInterval = setInterval(() => {
1413
1397
  if (activeFortunes.length === 0)
1414
1398
  return;
@@ -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
- const loadedPty = await loadPty();
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 encodedCwd = process.cwd().replace(/\//g, '-');
1417
- const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedCwd);
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
- const files = fs.readdirSync(projectDir);
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] Project dir does not exist yet');
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 files = fs.readdirSync(projectDir);
1448
- const jsonlFiles = files
1449
- .filter(f => f.endsWith('.jsonl'))
1450
- .map(f => ({
1451
- name: f,
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 currentFiles = fs.readdirSync(projectDir);
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 = path.join(projectDir, file);
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 = path.join(projectDir, `${currentSessionId}.jsonl`);
1606
- if (!fs.existsSync(candidate))
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})`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.2.15",
3
+ "version": "1.2.17",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {