@ekkos/cli 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/commands/dashboard.js +203 -66
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.js +54 -16
  4. package/dist/commands/run.js +163 -44
  5. package/dist/commands/status.d.ts +4 -1
  6. package/dist/commands/status.js +165 -27
  7. package/dist/commands/synk.d.ts +7 -0
  8. package/dist/commands/synk.js +339 -0
  9. package/dist/deploy/settings.d.ts +6 -5
  10. package/dist/deploy/settings.js +27 -17
  11. package/dist/index.js +12 -82
  12. package/dist/lib/usage-parser.d.ts +1 -1
  13. package/dist/lib/usage-parser.js +5 -3
  14. package/dist/local/index.d.ts +14 -0
  15. package/dist/local/index.js +28 -0
  16. package/dist/local/local-embeddings.d.ts +49 -0
  17. package/dist/local/local-embeddings.js +232 -0
  18. package/dist/local/offline-fallback.d.ts +44 -0
  19. package/dist/local/offline-fallback.js +159 -0
  20. package/dist/local/sqlite-store.d.ts +126 -0
  21. package/dist/local/sqlite-store.js +393 -0
  22. package/dist/local/sync-engine.d.ts +42 -0
  23. package/dist/local/sync-engine.js +223 -0
  24. package/dist/synk/api.d.ts +22 -0
  25. package/dist/synk/api.js +133 -0
  26. package/dist/synk/auth.d.ts +7 -0
  27. package/dist/synk/auth.js +30 -0
  28. package/dist/synk/config.d.ts +18 -0
  29. package/dist/synk/config.js +37 -0
  30. package/dist/synk/daemon/control-client.d.ts +11 -0
  31. package/dist/synk/daemon/control-client.js +101 -0
  32. package/dist/synk/daemon/control-server.d.ts +24 -0
  33. package/dist/synk/daemon/control-server.js +91 -0
  34. package/dist/synk/daemon/run.d.ts +14 -0
  35. package/dist/synk/daemon/run.js +338 -0
  36. package/dist/synk/encryption.d.ts +17 -0
  37. package/dist/synk/encryption.js +133 -0
  38. package/dist/synk/index.d.ts +13 -0
  39. package/dist/synk/index.js +36 -0
  40. package/dist/synk/machine-client.d.ts +42 -0
  41. package/dist/synk/machine-client.js +218 -0
  42. package/dist/synk/persistence.d.ts +51 -0
  43. package/dist/synk/persistence.js +211 -0
  44. package/dist/synk/qr.d.ts +5 -0
  45. package/dist/synk/qr.js +33 -0
  46. package/dist/synk/session-bridge.d.ts +58 -0
  47. package/dist/synk/session-bridge.js +171 -0
  48. package/dist/synk/session-client.d.ts +46 -0
  49. package/dist/synk/session-client.js +240 -0
  50. package/dist/synk/types.d.ts +574 -0
  51. package/dist/synk/types.js +74 -0
  52. package/dist/utils/platform.d.ts +5 -1
  53. package/dist/utils/platform.js +24 -4
  54. package/dist/utils/proxy-url.d.ts +10 -0
  55. package/dist/utils/proxy-url.js +19 -0
  56. package/dist/utils/state.d.ts +1 -1
  57. package/dist/utils/state.js +11 -3
  58. package/package.json +13 -4
  59. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
  60. package/templates/claude-plugins-admin/README.md +0 -446
  61. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
  62. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
  63. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
  64. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
  65. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
  66. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
  67. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
  68. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
  69. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
  70. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
  71. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
  72. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
  73. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
  74. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
  75. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
  76. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
  77. package/templates/hooks-node/lib/state.js +0 -187
  78. package/templates/hooks-node/stop.js +0 -416
  79. package/templates/hooks-node/user-prompt-submit.js +0 -337
  80. package/templates/rules/00-hooks-contract.mdc +0 -89
  81. package/templates/rules/30-ekkos-core.mdc +0 -188
  82. package/templates/rules/31-ekkos-messages.mdc +0 -78
@@ -322,6 +322,17 @@ function displaySessionName(rawName) {
322
322
  return rawName;
323
323
  return resolveSessionAlias(rawName) || (0, state_js_1.uuidToWords)(rawName);
324
324
  }
325
+ function isStableSessionId(sessionId) {
326
+ return typeof sessionId === 'string' && UUID_REGEX.test(sessionId);
327
+ }
328
+ function isPendingSessionId(sessionId) {
329
+ if (typeof sessionId !== 'string')
330
+ return true;
331
+ const normalized = sessionId.trim().toLowerCase();
332
+ if (!normalized)
333
+ return true;
334
+ return normalized === 'pending' || normalized === '_pending' || normalized.startsWith('_pending-');
335
+ }
325
336
  // ── Resolve session to JSONL path ──
326
337
  function resolveJsonlPath(sessionName, createdAfterMs) {
327
338
  // 1) Try standard resolution (works when sessionId is a real UUID)
@@ -331,13 +342,21 @@ function resolveJsonlPath(sessionName, createdAfterMs) {
331
342
  if (fs.existsSync(jsonlPath))
332
343
  return jsonlPath;
333
344
  }
334
- // 2) Fallback: active session has "pending" UUID
345
+ // 2) Active-session fallback.
346
+ // IMPORTANT: never resolve "pending" sessions to "latest file in project" because
347
+ // that can cross-bind concurrent sessions to the same dashboard transcript.
335
348
  const activeSessionsPath = path.join(os.homedir(), '.ekkos', 'active-sessions.json');
336
349
  if (fs.existsSync(activeSessionsPath)) {
337
350
  try {
338
351
  const sessions = JSON.parse(fs.readFileSync(activeSessionsPath, 'utf-8'));
339
352
  const match = sessions.find((s) => s.sessionName === sessionName);
340
353
  if (match?.projectPath) {
354
+ if (isStableSessionId(match.sessionId)) {
355
+ return findJsonlBySessionId(match.projectPath, match.sessionId);
356
+ }
357
+ if (isPendingSessionId(match.sessionId)) {
358
+ return null;
359
+ }
341
360
  return findLatestJsonl(match.projectPath, createdAfterMs);
342
361
  }
343
362
  }
@@ -353,12 +372,15 @@ function resolveJsonlPath(sessionName, createdAfterMs) {
353
372
  function findLatestJsonl(projectPath, createdAfterMs) {
354
373
  // Claude encodes project paths by replacing separators with '-'.
355
374
  // On Windows, ':' is also illegal in directory names so it gets replaced too.
375
+ // Claude also replaces underscores and other non-alphanumeric chars with '-'.
356
376
  // Try all plausible encodings since Claude's exact scheme varies by platform.
357
377
  const candidateEncodings = new Set([
358
378
  projectPath.replace(/[\\/]/g, '-'), // C:-Users-name (backslash only)
359
379
  projectPath.replace(/[:\\/]/g, '-'), // C--Users-name (colon + backslash)
360
380
  '-' + projectPath.replace(/[:\\/]/g, '-'), // -C--Users-name (leading separator)
361
381
  projectPath.replace(/\//g, '-'), // macOS: /Users/name → -Users-name
382
+ projectPath.replace(/[^a-zA-Z0-9]/g, '-'), // Replace ALL non-alphanumeric (handles _)
383
+ `-${projectPath.replace(/^[\\/]+/, '').replace(/[^a-zA-Z0-9]/g, '-')}`, // Leading - variant
362
384
  ]);
363
385
  const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
364
386
  for (const encoded of Array.from(candidateEncodings)) {
@@ -390,6 +412,8 @@ function findJsonlBySessionId(projectPath, sessionId) {
390
412
  projectPath.replace(/[:\\/]/g, '-'),
391
413
  '-' + projectPath.replace(/[:\\/]/g, '-'),
392
414
  projectPath.replace(/\//g, '-'),
415
+ projectPath.replace(/[^a-zA-Z0-9]/g, '-'),
416
+ `-${projectPath.replace(/^[\\/]+/, '').replace(/[^a-zA-Z0-9]/g, '-')}`,
393
417
  ]);
394
418
  const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
395
419
  for (const encoded of Array.from(candidateEncodings)) {
@@ -436,8 +460,8 @@ async function waitForNewSession() {
436
460
  const hint = JSON.parse(fs.readFileSync(hintPath, 'utf-8'));
437
461
  if (hint.ts >= launchTs - 5000 && hint.sessionName && hint.projectPath) {
438
462
  candidateName = hint.sessionName;
439
- const jsonlPath = findJsonlBySessionId(hint.projectPath, hint.sessionId || '')
440
- || findLatestJsonl(hint.projectPath, launchTs)
463
+ const hintSessionId = typeof hint.sessionId === 'string' ? hint.sessionId : '';
464
+ const jsonlPath = (isStableSessionId(hintSessionId) ? findJsonlBySessionId(hint.projectPath, hintSessionId) : null)
441
465
  || resolveJsonlPath(hint.sessionName, launchTs);
442
466
  // Return immediately — JSONL may be null; dashboard will lazy-resolve
443
467
  console.log(chalk_1.default.green(` Found session (hook hint): ${hint.sessionName}`));
@@ -451,26 +475,24 @@ async function waitForNewSession() {
451
475
  for (const s of sessions) {
452
476
  const startedMs = new Date(s.startedAt).getTime();
453
477
  if (startedMs >= launchTs - 2000) {
454
- candidateName = s.sessionName;
455
- // Try to resolve JSONL immediately (may succeed if Claude already created it)
456
- let jsonlPath = resolveJsonlPath(s.sessionName, launchTs);
457
- if (!jsonlPath) {
458
- const allPaths = new Set(sessions.filter(x => x.sessionName === s.sessionName && x.projectPath)
459
- .map(x => x.projectPath));
460
- for (const pp of Array.from(allPaths)) {
461
- jsonlPath = findLatestJsonl(pp, launchTs);
462
- if (jsonlPath)
463
- break;
464
- }
478
+ if (!isStableSessionId(s.sessionId)) {
479
+ // Wait for stable session identity; pending IDs can collide across concurrent runs.
480
+ continue;
465
481
  }
482
+ candidateName = s.sessionName;
483
+ // Try exact by sessionId first, then constrained name resolution.
484
+ let jsonlPath = s.projectPath ? findJsonlBySessionId(s.projectPath, s.sessionId) : null;
485
+ if (!jsonlPath)
486
+ jsonlPath = resolveJsonlPath(s.sessionName, launchTs);
466
487
  // Return immediately with session name — JSONL may still be null
467
488
  // (Claude Code hasn't created it yet). Dashboard will lazy-resolve.
468
- console.log(chalk_1.default.green(` Found session: ${s.sessionName}`));
489
+ console.log(chalk_1.default.green(` Found session: ${s.sessionName}${jsonlPath ? '' : ' (awaiting transcript)'}`));
469
490
  return { sessionName: s.sessionName, jsonlPath, launchCwd: s.projectPath || launchCwd, launchTs };
470
491
  }
471
492
  }
472
- // Fallback: use launch CWD to find any new JSONL
473
- if (launchCwd) {
493
+ // Fallback: use launch CWD to find any new JSONL only if we don't even
494
+ // have a candidate session name yet.
495
+ if (launchCwd && !candidateName) {
474
496
  const latestJsonl = findLatestJsonl(launchCwd, launchTs);
475
497
  if (latestJsonl) {
476
498
  const name = candidateName || path.basename(latestJsonl, '.jsonl');
@@ -478,8 +500,9 @@ async function waitForNewSession() {
478
500
  return { sessionName: name, jsonlPath: latestJsonl, launchCwd, launchTs };
479
501
  }
480
502
  }
481
- // Broad fallback: scan ALL project directories for any new JSONL
482
- {
503
+ // Broad fallback: scan ALL project directories for any new JSONL only
504
+ // when no candidate name is known.
505
+ if (!candidateName) {
483
506
  const projectsRoot = path.join(os.homedir(), '.claude', 'projects');
484
507
  try {
485
508
  if (fs.existsSync(projectsRoot)) {
@@ -580,7 +603,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
580
603
  mouse: false, // Disable ALL mouse capture (allows terminal text selection)
581
604
  grabKeys: false, // Don't grab keyboard input from other panes
582
605
  sendFocus: false, // Don't send focus events (breaks paste)
583
- ignoreLocked: ['C-c'], // Only capture Ctrl+C for quit
606
+ ignoreLocked: ['C-c', 'C-q'], // Capture Ctrl+C and Ctrl+Q for quit
584
607
  input: ttyInput, // Use /dev/tty for input (isolated from stdout pipe)
585
608
  output: ttyOutput, // Use /dev/tty for output (isolated from stdout pipe)
586
609
  forceUnicode: true, // Better text rendering
@@ -678,6 +701,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
678
701
  let layout = calcLayout();
679
702
  let lastFileSize = 0;
680
703
  let lastData = null;
704
+ let sessionStartMs = launchTs || null; // epoch ms of session start, for live timer
681
705
  let lastChartSeries = null;
682
706
  let lastScrollPerc = 0; // Preserve scroll position across updates
683
707
  let fortuneIdx = Math.floor(Math.random() * activeFortunes.length);
@@ -945,14 +969,36 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
945
969
  }
946
970
  catch { }
947
971
  }
972
+ function renderPreTurnState() {
973
+ try {
974
+ contextBox.setContent(` {green-fg}Session active{/green-fg} {gray-fg}${sessionName}{/gray-fg}\n` +
975
+ ` Token and cost metrics appear after the first assistant response.`);
976
+ turnBox.setContent(`{bold}Turns{/bold}\n` +
977
+ `{gray-fg}—{/gray-fg}`);
978
+ const timerStr = sessionStartMs
979
+ ? `{cyan-fg}${formatElapsed(sessionStartMs)}{/cyan-fg} `
980
+ : '';
981
+ footerBox.setLabel(` ${sessionName} `);
982
+ footerBox.setContent(` ${timerStr}{green-fg}Ready{/green-fg}` +
983
+ (inTmux
984
+ ? ` {gray-fg}Ctrl+Q quit{/gray-fg}`
985
+ : ` {gray-fg}? help q quit r refresh{/gray-fg}`));
986
+ }
987
+ catch (err) {
988
+ dlog(`Pre-turn render: ${err.message}`);
989
+ }
990
+ }
948
991
  function updateDashboard() {
949
992
  ensureLayoutSynced();
950
993
  // ── Lazy JSONL resolution ──────────────────────────────────────────────
951
994
  // Dashboard may launch before Claude Code creates the JSONL file.
952
995
  // Keep trying to find it on each poll tick.
953
996
  if (!jsonlPath || !fs.existsSync(jsonlPath)) {
997
+ const shouldUseCwdFallback = initialSessionName === 'initializing'
998
+ || initialSessionName === 'session'
999
+ || UUID_REGEX.test(initialSessionName);
954
1000
  const resolved = resolveJsonlPath(initialSessionName, launchTs)
955
- || (launchCwd ? findLatestJsonl(launchCwd, launchTs) : null);
1001
+ || (shouldUseCwdFallback && launchCwd ? findLatestJsonl(launchCwd, launchTs) : null);
956
1002
  if (resolved) {
957
1003
  jsonlPath = resolved;
958
1004
  dlog(`Lazy-resolved JSONL: ${jsonlPath}`);
@@ -960,6 +1006,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
960
1006
  else {
961
1007
  // Still no JSONL — render the header/footer so the dashboard isn't blank
962
1008
  renderHeader();
1009
+ renderPreTurnState();
963
1010
  try {
964
1011
  screen.render();
965
1012
  }
@@ -994,6 +1041,9 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
994
1041
  lastFileSize = stat.size;
995
1042
  data = parseJsonlFile(jsonlPath, sessionName);
996
1043
  lastData = data;
1044
+ if (!sessionStartMs && data.startedAt) {
1045
+ sessionStartMs = new Date(data.startedAt).getTime();
1046
+ }
997
1047
  }
998
1048
  catch (err) {
999
1049
  dlog(`Parse error: ${err.message}`);
@@ -1089,14 +1139,15 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1089
1139
  let header = '';
1090
1140
  let separator = '';
1091
1141
  let rows = [];
1092
- // Always show all 7 columns: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
1142
+ // Always show all 8 columns: Turn, Time, Model, Context, Cache Rd, Cache Wr, Output, Cost
1093
1143
  // Shrink flex columns to fit narrow panes instead of dropping them.
1094
1144
  const colNum = 4;
1145
+ const colTime = 8; // "HH:MM:SS" or "H:MM AM"
1095
1146
  const colM = 7;
1096
1147
  const colCtx = 7;
1097
1148
  const colCost = 8;
1098
- const nDividers = 6;
1099
- const fixedW = colNum + colM + colCtx + colCost;
1149
+ const nDividers = 7;
1150
+ const fixedW = colNum + colTime + colM + colCtx + colCost;
1100
1151
  const flexTotal = Math.max(0, w - fixedW - nDividers);
1101
1152
  let rdW = Math.max(5, Math.floor(flexTotal * 0.35));
1102
1153
  let wrW = Math.max(5, Math.floor(flexTotal * 0.30));
@@ -1118,14 +1169,30 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1118
1169
  rdW -= trimRd;
1119
1170
  }
1120
1171
  }
1121
- 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}`;
1122
- separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
1172
+ // Format turn timestamp to short time string
1173
+ const fmtTime = (iso) => {
1174
+ if (!iso)
1175
+ return '--:--';
1176
+ try {
1177
+ const d = new Date(iso);
1178
+ const h = d.getHours();
1179
+ const m = d.getMinutes().toString().padStart(2, '0');
1180
+ const s = d.getSeconds().toString().padStart(2, '0');
1181
+ return `${h}:${m}:${s}`;
1182
+ }
1183
+ catch {
1184
+ return '--:--';
1185
+ }
1186
+ };
1187
+ header = `{bold}${pad('Turn', colNum)}${div}${pad('Time', colTime)}${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}`;
1188
+ separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colTime)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
1123
1189
  rows = turns.map(t => {
1124
1190
  const mTag = modelTag(t.routedModel);
1125
1191
  const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
1126
1192
  const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
1127
1193
  const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
1128
1194
  return (pad(String(t.turn), colNum) + div +
1195
+ `{gray-fg}${pad(fmtTime(t.timestamp), colTime)}{/gray-fg}` + div +
1129
1196
  `{${mColor}-fg}${pad(mTag, colM)}{/${mColor}-fg}` + div +
1130
1197
  pad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
1131
1198
  `{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
@@ -1145,13 +1212,20 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1145
1212
  dlog(`Table: ${err.message}`);
1146
1213
  }
1147
1214
  // ── Session Totals (footer) ──
1215
+ renderFooter(data);
1216
+ }
1217
+ /** Render footer bar — called from updateDashboard and the 1s timer tick */
1218
+ function renderFooter(data, skipRender = false) {
1219
+ const d = data || lastData;
1220
+ if (!d)
1221
+ return;
1148
1222
  try {
1149
- const totalTokensM = ((data.totalCacheRead + data.totalCacheCreate + data.totalOutput) / 1000000).toFixed(2);
1150
- const totalSavings = data.turns.reduce((s, t) => s + t.savings, 0);
1223
+ const totalTokensM = ((d.totalCacheRead + d.totalCacheCreate + d.totalOutput) / 1000000).toFixed(2);
1224
+ const totalSavings = d.turns.reduce((s, t) => s + t.savings, 0);
1151
1225
  // Model routing breakdown (uses routedModel for actual model counts)
1152
- const opusCount = data.turns.filter(t => t.routedModel.includes('opus')).length;
1153
- const sonnetCount = data.turns.filter(t => t.routedModel.includes('sonnet')).length;
1154
- const haikuCount = data.turns.filter(t => t.routedModel.includes('haiku')).length;
1226
+ const opusCount = d.turns.filter(t => t.routedModel.includes('opus')).length;
1227
+ const sonnetCount = d.turns.filter(t => t.routedModel.includes('sonnet')).length;
1228
+ const haikuCount = d.turns.filter(t => t.routedModel.includes('haiku')).length;
1155
1229
  const routingParts = [`{magenta-fg}O{/magenta-fg}:${opusCount}`];
1156
1230
  if (sonnetCount > 0)
1157
1231
  routingParts.push(`{blue-fg}S{/blue-fg}:${sonnetCount}`);
@@ -1160,25 +1234,30 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1160
1234
  const savingsStr = totalSavings > 0
1161
1235
  ? ` {green-fg}saved $${totalSavings.toFixed(2)}{/green-fg}`
1162
1236
  : '';
1237
+ const timerStr = sessionStartMs
1238
+ ? `{cyan-fg}${formatElapsed(sessionStartMs)}{/cyan-fg} `
1239
+ : '';
1163
1240
  footerBox.setLabel(` ${sessionName} `);
1164
- footerBox.setContent(` {green-fg}$${data.totalCost.toFixed(2)}{/green-fg}` +
1241
+ footerBox.setContent(` ${timerStr}` +
1242
+ `{green-fg}$${d.totalCost.toFixed(2)}{/green-fg}` +
1165
1243
  ` ${totalTokensM}M` +
1166
1244
  ` ${routingStr}` +
1167
- ` R[A:${data.replayAppliedCount} SZ:${data.replaySkippedSizeCount} ST:${data.replaySkipStoreCount}]` +
1245
+ ` R[A:${d.replayAppliedCount} SZ:${d.replaySkippedSizeCount} ST:${d.replaySkipStoreCount}]` +
1168
1246
  savingsStr +
1169
1247
  (inTmux
1170
- ? ` {gray-fg}Ctrl+C quit{/gray-fg}`
1248
+ ? ` {gray-fg}Ctrl+Q quit{/gray-fg}`
1171
1249
  : ` {gray-fg}? help q quit r refresh{/gray-fg}`));
1172
1250
  }
1173
1251
  catch (err) {
1174
1252
  dlog(`Footer: ${err.message}`);
1175
1253
  }
1176
- try {
1177
- screen.render();
1178
- }
1179
- catch (err) {
1180
- dlog(`Render: ${err.message}`);
1181
- }
1254
+ if (!skipRender)
1255
+ try {
1256
+ screen.render();
1257
+ }
1258
+ catch (err) {
1259
+ dlog(`Render: ${err.message}`);
1260
+ }
1182
1261
  }
1183
1262
  // ── Usage window update — calls Anthropic's OAuth usage API ──
1184
1263
  /**
@@ -1220,36 +1299,72 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1220
1299
  return null;
1221
1300
  }
1222
1301
  }
1223
- async function updateWindowBox() {
1302
+ // Cache last fetched usage data so the countdown can tick every second
1303
+ let cachedUsage = null;
1304
+ // Fetch fresh usage data from API (called every 15s)
1305
+ async function fetchAndCacheUsage() {
1306
+ try {
1307
+ cachedUsage = await fetchAnthropicUsage();
1308
+ }
1309
+ catch (err) {
1310
+ dlog(`Window fetch: ${err.message}`);
1311
+ }
1312
+ renderWindowBox();
1313
+ }
1314
+ // Render countdown from cached data (called every 1s)
1315
+ function renderWindowBox(skipRender = false) {
1224
1316
  try {
1225
- const usage = await fetchAnthropicUsage();
1317
+ const usage = cachedUsage;
1226
1318
  let line1 = ' {gray-fg}No usage data{/gray-fg}';
1227
1319
  let line2 = '';
1320
+ // Fixed-width column helpers for aligned rendering:
1321
+ // " 5h: 26% used ⏱ 1h16m12s resets Feb 27, 2026, 12:00 AM"
1322
+ // " Week: 91% used ⏱ 0h16m12s resets Thu, Feb 26, 2026, 11:00 PM"
1323
+ // ^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
1324
+ // label pct col countdown reset time
1325
+ const COL_LABEL = 6; // "5h: " or "Week: " — padded to same width
1326
+ const COL_PCT = 9; // "100% used" max
1327
+ const COL_CD = 11; // "10h00m00s " max
1328
+ const rpad = (s, w) => s.length >= w ? s : s + ' '.repeat(w - s.length);
1228
1329
  // ── 5h Window (from Anthropic OAuth API) ──
1229
1330
  if (usage?.five_hour) {
1230
1331
  const pct = usage.five_hour.utilization;
1231
1332
  const resetAt = new Date(usage.five_hour.resets_at).getTime();
1232
1333
  const remainMs = Math.max(0, resetAt - Date.now());
1233
- const remainMin = Math.round(remainMs / 60000);
1234
- const rH = Math.floor(remainMin / 60);
1235
- const rM = remainMin % 60;
1334
+ const remainSec = Math.round(remainMs / 1000);
1335
+ const rH = Math.floor(remainSec / 3600);
1336
+ const rM = Math.floor((remainSec % 3600) / 60);
1337
+ const rS = remainSec % 60;
1236
1338
  const pctColor = pct < 50 ? 'green' : pct < 80 ? 'yellow' : 'red';
1237
- const timeColor = remainMin > 120 ? 'green' : remainMin > 60 ? 'yellow' : 'red';
1238
- line1 = ` {bold}5h:{/bold}` +
1239
- ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1240
- ` {${timeColor}-fg}${rH}h${rM}m left{/${timeColor}-fg}`;
1339
+ const countdown = `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1340
+ const pctStr = `${pct.toFixed(0)}% used`;
1341
+ const resetDate = new Date(resetAt);
1342
+ const resetTime = resetDate.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1343
+ line1 = ` {bold}${rpad('5h:', COL_LABEL)}{/bold}` +
1344
+ `{${pctColor}-fg}${rpad(pctStr, COL_PCT)}{/${pctColor}-fg}` +
1345
+ ` {${pctColor}-fg}⏱ ${rpad(countdown, COL_CD)}{/${pctColor}-fg}` +
1346
+ `resets ${resetTime}`;
1241
1347
  }
1242
1348
  // ── Weekly (from Anthropic OAuth API) ──
1243
1349
  if (usage?.seven_day) {
1244
1350
  const pct = usage.seven_day.utilization;
1245
1351
  const resetAt = new Date(usage.seven_day.resets_at);
1246
- const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1247
- const resetDay = dayNames[resetAt.getDay()];
1248
- const resetHour = resetAt.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
1352
+ const remainMs = Math.max(0, resetAt.getTime() - Date.now());
1353
+ const remainSec = Math.round(remainMs / 1000);
1354
+ const rD = Math.floor(remainSec / 86400);
1355
+ const rH = Math.floor((remainSec % 86400) / 3600);
1356
+ const rM = Math.floor((remainSec % 3600) / 60);
1357
+ const rS = remainSec % 60;
1249
1358
  const pctColor = pct < 50 ? 'green' : pct < 80 ? 'yellow' : 'red';
1250
- line2 = ` {bold}Week:{/bold}` +
1251
- ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1252
- ` resets ${resetDay} ${resetHour}`;
1359
+ const countdown = rD > 0
1360
+ ? `${rD}d${rH}h${rM.toString().padStart(2, '0')}m`
1361
+ : `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1362
+ const pctStr = `${pct.toFixed(0)}% used`;
1363
+ const resetTime = resetAt.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1364
+ line2 = ` {bold}${rpad('Week:', COL_LABEL)}{/bold}` +
1365
+ `{${pctColor}-fg}${rpad(pctStr, COL_PCT)}{/${pctColor}-fg}` +
1366
+ ` {${pctColor}-fg}⏱ ${rpad(countdown, COL_CD)}{/${pctColor}-fg}` +
1367
+ `resets ${resetTime}`;
1253
1368
  }
1254
1369
  windowBox.setContent(line1 + (line2 ? '\n' + line2 : ''));
1255
1370
  }
@@ -1257,11 +1372,14 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1257
1372
  dlog(`Window: ${err.message}`);
1258
1373
  windowBox.setContent(` {gray-fg}Usage data unavailable{/gray-fg}`);
1259
1374
  }
1260
- try {
1261
- screen.render();
1262
- }
1263
- catch { }
1375
+ if (!skipRender)
1376
+ try {
1377
+ screen.render();
1378
+ }
1379
+ catch { }
1264
1380
  }
1381
+ // Legacy wrapper for backward compat
1382
+ async function updateWindowBox() { await fetchAndCacheUsage(); }
1265
1383
  // ── Handle terminal resize ──
1266
1384
  // Recalculate all widget positions from new screen.height
1267
1385
  screen.on('resize', () => {
@@ -1289,9 +1407,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1289
1407
  // KEYBOARD SHORTCUTS - Only capture when dashboard pane has focus
1290
1408
  // In tmux split mode, this prevents capturing keys from Claude Code pane
1291
1409
  // ══════════════════════════════════════════════════════════════════════════
1292
- screen.key(['C-c'], () => {
1410
+ screen.key(['C-c', 'C-q'], () => {
1293
1411
  clearInterval(pollInterval);
1294
1412
  clearInterval(windowPollInterval);
1413
+ clearInterval(tickInterval);
1295
1414
  clearInterval(headerAnimInterval);
1296
1415
  clearInterval(fortuneInterval);
1297
1416
  screen.destroy();
@@ -1301,6 +1420,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1301
1420
  screen.key(['q'], () => {
1302
1421
  clearInterval(pollInterval);
1303
1422
  clearInterval(windowPollInterval);
1423
+ clearInterval(tickInterval);
1304
1424
  clearInterval(headerAnimInterval);
1305
1425
  clearInterval(fortuneInterval);
1306
1426
  screen.destroy();
@@ -1363,7 +1483,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1363
1483
  '\n' +
1364
1484
  '{bold}Controls{/bold}\n' +
1365
1485
  ' r Refresh now\n' +
1366
- ' q/Ctrl+C Quit\n' +
1486
+ ' q/Ctrl+Q Quit\n' +
1367
1487
  '\n' +
1368
1488
  '{gray-fg}Press any key to close{/gray-fg}'),
1369
1489
  tags: true,
@@ -1408,7 +1528,17 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1408
1528
  fortuneText = activeFortunes[fortuneIdx];
1409
1529
  renderHeader();
1410
1530
  }, 30000);
1411
- const windowPollInterval = setInterval(updateWindowBox, 15000); // every 15s
1531
+ const windowPollInterval = setInterval(fetchAndCacheUsage, 15000); // fetch fresh data every 15s
1532
+ // Single 1s tick for both countdown + session timer (one screen.render instead of three)
1533
+ const tickInterval = setInterval(() => {
1534
+ renderWindowBox(true);
1535
+ if (sessionStartMs)
1536
+ renderFooter(null, true);
1537
+ try {
1538
+ screen.render();
1539
+ }
1540
+ catch { }
1541
+ }, 1000);
1412
1542
  }
1413
1543
  // ── Helpers ──
1414
1544
  function fmtK(n) {
@@ -1419,6 +1549,14 @@ function fmtK(n) {
1419
1549
  return String(n);
1420
1550
  }
1421
1551
  function formatK(n) { return fmtK(n); }
1552
+ function formatElapsed(startMs) {
1553
+ const diff = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
1554
+ const h = Math.floor(diff / 3600);
1555
+ const m = Math.floor((diff % 3600) / 60);
1556
+ const s = diff % 60;
1557
+ const pad = (n) => String(n).padStart(2, '0');
1558
+ return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${m}:${pad(s)}`;
1559
+ }
1422
1560
  // ── Session picker ──
1423
1561
  async function pickSession() {
1424
1562
  const sessions = await (0, usage_parser_js_1.listEkkosSessions)(20);
@@ -1476,9 +1614,8 @@ exports.dashboardCommand = new commander_1.Command('dashboard')
1476
1614
  }
1477
1615
  const jsonlPath = resolveJsonlPath(sessionName);
1478
1616
  if (!jsonlPath) {
1479
- console.log(chalk_1.default.red(`No JSONL found for "${sessionName}"`));
1480
- console.log(chalk_1.default.gray('Run "ekkos sessions" to see active sessions'));
1481
- process.exit(1);
1617
+ // JSONL may not exist yet (session just started) — launch with lazy resolution
1618
+ console.log(chalk_1.default.gray(`Waiting for JSONL for "${sessionName}"...`));
1482
1619
  }
1483
- await launchDashboard(sessionName, jsonlPath, refreshMs);
1620
+ await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, Date.now());
1484
1621
  });
@@ -4,6 +4,7 @@ interface InitOptions {
4
4
  force?: boolean;
5
5
  skipHooks?: boolean;
6
6
  skipSkills?: boolean;
7
+ quick?: boolean;
7
8
  }
8
9
  export declare function init(options: InitOptions): Promise<void>;
9
10
  export {};
@@ -11,8 +11,8 @@ const ora_1 = __importDefault(require("ora"));
11
11
  const open_1 = __importDefault(require("open"));
12
12
  const platform_1 = require("../utils/platform");
13
13
  const mcp_1 = require("../deploy/mcp");
14
+ const settings_1 = require("../deploy/settings");
14
15
  // DEPRECATED: Hooks removed in hookless architecture migration
15
- // import { deployClaudeSettings } from '../deploy/settings';
16
16
  // import { deployHooks } from '../deploy/hooks';
17
17
  const skills_1 = require("../deploy/skills");
18
18
  const agents_1 = require("../deploy/agents");
@@ -200,23 +200,37 @@ async function manualKeyAuth(providedKey) {
200
200
  // ═══════════════════════════════════════════════════════════════════════════
201
201
  // IDE SETUP
202
202
  // ═══════════════════════════════════════════════════════════════════════════
203
- async function selectIDEs() {
203
+ async function selectIDEs(autoSelect = false) {
204
204
  console.log(chalk_1.default.cyan('Step 2/3: IDE Setup'));
205
205
  console.log(chalk_1.default.gray('─'.repeat(40)));
206
206
  console.log('');
207
207
  const detected = (0, platform_1.detectInstalledIDEs)();
208
208
  const current = (0, platform_1.detectCurrentIDE)();
209
- if (detected.length > 0) {
210
- console.log(chalk_1.default.gray(`Detected: ${detected.join(', ')}`));
209
+ // Build the unique ordered list: current IDE first, then any others detected
210
+ const detectedSet = new Set(detected);
211
+ if (current && (current === 'claude' || current === 'cursor' || current === 'windsurf')) {
212
+ detectedSet.add(current);
213
+ }
214
+ const detectedList = Array.from(detectedSet);
215
+ if (detectedList.length > 0) {
216
+ console.log(chalk_1.default.gray(`Detected: ${detectedList.join(', ')}`));
211
217
  if (current) {
212
218
  console.log(chalk_1.default.gray(`Current: ${current}`));
213
219
  }
214
220
  console.log('');
215
221
  }
222
+ // Auto-select when exactly one IDE is detected or autoSelect flag is set
223
+ if (autoSelect || detectedList.length === 1) {
224
+ const autoIDE = detectedList.length === 1 ? detectedList[0] : (current ?? detectedList[0] ?? 'claude');
225
+ const ideName = autoIDE === 'claude' ? 'Claude Code' : autoIDE === 'cursor' ? 'Cursor' : 'Windsurf';
226
+ console.log(chalk_1.default.green(`✓ Auto-detected: ${chalk_1.default.bold(ideName)}`));
227
+ console.log('');
228
+ return [autoIDE];
229
+ }
216
230
  const ideChoices = [
217
- { name: 'Claude Code', value: 'claude', checked: detected.includes('claude') || current === 'claude' },
218
- { name: 'Cursor', value: 'cursor', checked: detected.includes('cursor') || current === 'cursor' },
219
- { name: 'Windsurf (Cascade)', value: 'windsurf', checked: detected.includes('windsurf') || current === 'windsurf' }
231
+ { name: 'Claude Code', value: 'claude', checked: detectedList.includes('claude') || current === 'claude' },
232
+ { name: 'Cursor', value: 'cursor', checked: detectedList.includes('cursor') || current === 'cursor' },
233
+ { name: 'Windsurf (Cascade)', value: 'windsurf', checked: detectedList.includes('windsurf') || current === 'windsurf' }
220
234
  ];
221
235
  // If nothing was auto-detected, default to Claude Code
222
236
  if (!ideChoices.some(c => c.checked)) {
@@ -252,10 +266,17 @@ async function deployForClaude(apiKey, userId, options) {
252
266
  catch (error) {
253
267
  spinner.fail('MCP server configuration failed');
254
268
  }
255
- // DEPRECATED: Hooks removed in hookless architecture migration
256
- // Settings.json hook registration and hook script deployment are no longer needed.
257
- // Use `ekkos run` instead.
258
- result.settings = true; // Mark as satisfied so downstream logic is unaffected
269
+ // Claude Code settings (disable auto-memory, clean up legacy hooks)
270
+ spinner = (0, ora_1.default)('Configuring Claude Code settings...').start();
271
+ try {
272
+ (0, settings_1.deployClaudeSettings)();
273
+ result.settings = true;
274
+ spinner.succeed('Claude Code settings (auto-memory disabled)');
275
+ }
276
+ catch (error) {
277
+ spinner.fail('Claude Code settings failed');
278
+ result.settings = true; // Non-critical — don't block init
279
+ }
259
280
  // Skills
260
281
  if (!options.skipSkills) {
261
282
  spinner = (0, ora_1.default)('Deploying skills...').start();
@@ -325,9 +346,15 @@ async function deployForWindsurf(apiKey, userId) {
325
346
  // MAIN INIT COMMAND
326
347
  // ═══════════════════════════════════════════════════════════════════════════
327
348
  async function init(options) {
349
+ const isQuick = options.quick ?? false;
328
350
  console.log('');
329
351
  console.log(chalk_1.default.cyan.bold('╔═══════════════════════════════════════╗'));
330
- console.log(chalk_1.default.cyan.bold('║ 🧠 ekkOS Memory System Setup ║'));
352
+ if (isQuick) {
353
+ console.log(chalk_1.default.cyan.bold('║ ⚡ ekkOS Quick Setup ║'));
354
+ }
355
+ else {
356
+ console.log(chalk_1.default.cyan.bold('║ 🧠 ekkOS Memory System Setup ║'));
357
+ }
331
358
  console.log(chalk_1.default.cyan.bold('╚═══════════════════════════════════════╝'));
332
359
  console.log('');
333
360
  // Check templates exist
@@ -347,11 +374,11 @@ async function init(options) {
347
374
  // STEP 1: Authentication
348
375
  let auth;
349
376
  if (options.key) {
350
- // Manual API key provided
377
+ // Manual API key provided — always use it (respects --quick)
351
378
  auth = await manualKeyAuth(options.key);
352
379
  }
353
380
  else if (existingConfig?.apiKey && !options.force) {
354
- // Already authenticated
381
+ // Already authenticated — skip auth step entirely (works great with --quick)
355
382
  console.log(chalk_1.default.cyan('Step 1/3: Authentication'));
356
383
  console.log(chalk_1.default.gray('─'.repeat(40)));
357
384
  console.log('');
@@ -365,6 +392,10 @@ async function init(options) {
365
392
  tier: existingConfig.tier || 'unknown'
366
393
  };
367
394
  }
395
+ else if (isQuick) {
396
+ // --quick with no stored key: fall back to manual key prompt (one question only)
397
+ auth = await manualKeyAuth();
398
+ }
368
399
  else {
369
400
  // Device auth flow
370
401
  try {
@@ -395,7 +426,8 @@ async function init(options) {
395
426
  console.log('');
396
427
  }
397
428
  else {
398
- selectedIDEs = await selectIDEs();
429
+ // Pass autoSelect=true when --quick is set so we skip the checkbox prompt
430
+ selectedIDEs = await selectIDEs(isQuick);
399
431
  }
400
432
  // STEP 3: Deployment
401
433
  console.log(chalk_1.default.cyan('Step 3/3: Deploying'));
@@ -461,7 +493,13 @@ async function init(options) {
461
493
  };
462
494
  console.log('');
463
495
  console.log(chalk_1.default.green.bold('╔═══════════════════════════════════════╗'));
464
- console.log(chalk_1.default.green.bold('║ ✓ Setup complete! ║'));
496
+ if (isQuick) {
497
+ const quickIdeName = ideNames.join(' / ') || 'Claude Code';
498
+ console.log(chalk_1.default.green.bold(`║ ⚡ Quick setup complete for ${quickIdeName.padEnd(11)}║`));
499
+ }
500
+ else {
501
+ console.log(chalk_1.default.green.bold('║ ✓ Setup complete! ║'));
502
+ }
465
503
  console.log(chalk_1.default.green.bold('╚═══════════════════════════════════════╝'));
466
504
  console.log('');
467
505
  console.log(chalk_1.default.white.bold(' MCP configured:'));