@ekkos/cli 1.3.1 → 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 +147 -57
  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}`);
@@ -1162,13 +1212,20 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1162
1212
  dlog(`Table: ${err.message}`);
1163
1213
  }
1164
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;
1165
1222
  try {
1166
- const totalTokensM = ((data.totalCacheRead + data.totalCacheCreate + data.totalOutput) / 1000000).toFixed(2);
1167
- 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);
1168
1225
  // Model routing breakdown (uses routedModel for actual model counts)
1169
- const opusCount = data.turns.filter(t => t.routedModel.includes('opus')).length;
1170
- const sonnetCount = data.turns.filter(t => t.routedModel.includes('sonnet')).length;
1171
- 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;
1172
1229
  const routingParts = [`{magenta-fg}O{/magenta-fg}:${opusCount}`];
1173
1230
  if (sonnetCount > 0)
1174
1231
  routingParts.push(`{blue-fg}S{/blue-fg}:${sonnetCount}`);
@@ -1177,25 +1234,30 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1177
1234
  const savingsStr = totalSavings > 0
1178
1235
  ? ` {green-fg}saved $${totalSavings.toFixed(2)}{/green-fg}`
1179
1236
  : '';
1237
+ const timerStr = sessionStartMs
1238
+ ? `{cyan-fg}${formatElapsed(sessionStartMs)}{/cyan-fg} `
1239
+ : '';
1180
1240
  footerBox.setLabel(` ${sessionName} `);
1181
- footerBox.setContent(` {green-fg}$${data.totalCost.toFixed(2)}{/green-fg}` +
1241
+ footerBox.setContent(` ${timerStr}` +
1242
+ `{green-fg}$${d.totalCost.toFixed(2)}{/green-fg}` +
1182
1243
  ` ${totalTokensM}M` +
1183
1244
  ` ${routingStr}` +
1184
- ` R[A:${data.replayAppliedCount} SZ:${data.replaySkippedSizeCount} ST:${data.replaySkipStoreCount}]` +
1245
+ ` R[A:${d.replayAppliedCount} SZ:${d.replaySkippedSizeCount} ST:${d.replaySkipStoreCount}]` +
1185
1246
  savingsStr +
1186
1247
  (inTmux
1187
- ? ` {gray-fg}Ctrl+C quit{/gray-fg}`
1248
+ ? ` {gray-fg}Ctrl+Q quit{/gray-fg}`
1188
1249
  : ` {gray-fg}? help q quit r refresh{/gray-fg}`));
1189
1250
  }
1190
1251
  catch (err) {
1191
1252
  dlog(`Footer: ${err.message}`);
1192
1253
  }
1193
- try {
1194
- screen.render();
1195
- }
1196
- catch (err) {
1197
- dlog(`Render: ${err.message}`);
1198
- }
1254
+ if (!skipRender)
1255
+ try {
1256
+ screen.render();
1257
+ }
1258
+ catch (err) {
1259
+ dlog(`Render: ${err.message}`);
1260
+ }
1199
1261
  }
1200
1262
  // ── Usage window update — calls Anthropic's OAuth usage API ──
1201
1263
  /**
@@ -1250,11 +1312,20 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1250
1312
  renderWindowBox();
1251
1313
  }
1252
1314
  // Render countdown from cached data (called every 1s)
1253
- function renderWindowBox() {
1315
+ function renderWindowBox(skipRender = false) {
1254
1316
  try {
1255
1317
  const usage = cachedUsage;
1256
1318
  let line1 = ' {gray-fg}No usage data{/gray-fg}';
1257
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);
1258
1329
  // ── 5h Window (from Anthropic OAuth API) ──
1259
1330
  if (usage?.five_hour) {
1260
1331
  const pct = usage.five_hour.utilization;
@@ -1266,12 +1337,13 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1266
1337
  const rS = remainSec % 60;
1267
1338
  const pctColor = pct < 50 ? 'green' : pct < 80 ? 'yellow' : 'red';
1268
1339
  const countdown = `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1340
+ const pctStr = `${pct.toFixed(0)}% used`;
1269
1341
  const resetDate = new Date(resetAt);
1270
1342
  const resetTime = resetDate.toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1271
- line1 = ` {bold}5h:{/bold}` +
1272
- ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1273
- ` {${pctColor}-fg}⏱ ${countdown}{/${pctColor}-fg}` +
1274
- ` resets ${resetTime}`;
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}`;
1275
1347
  }
1276
1348
  // ── Weekly (from Anthropic OAuth API) ──
1277
1349
  if (usage?.seven_day) {
@@ -1287,11 +1359,12 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1287
1359
  const countdown = rD > 0
1288
1360
  ? `${rD}d${rH}h${rM.toString().padStart(2, '0')}m`
1289
1361
  : `${rH}h${rM.toString().padStart(2, '0')}m${rS.toString().padStart(2, '0')}s`;
1362
+ const pctStr = `${pct.toFixed(0)}% used`;
1290
1363
  const resetTime = resetAt.toLocaleString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' });
1291
- line2 = ` {bold}Week:{/bold}` +
1292
- ` {${pctColor}-fg}${pct.toFixed(0)}% used{/${pctColor}-fg}` +
1293
- ` {${pctColor}-fg}⏱ ${countdown}{/${pctColor}-fg}` +
1294
- ` resets ${resetTime}`;
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}`;
1295
1368
  }
1296
1369
  windowBox.setContent(line1 + (line2 ? '\n' + line2 : ''));
1297
1370
  }
@@ -1299,10 +1372,11 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1299
1372
  dlog(`Window: ${err.message}`);
1300
1373
  windowBox.setContent(` {gray-fg}Usage data unavailable{/gray-fg}`);
1301
1374
  }
1302
- try {
1303
- screen.render();
1304
- }
1305
- catch { }
1375
+ if (!skipRender)
1376
+ try {
1377
+ screen.render();
1378
+ }
1379
+ catch { }
1306
1380
  }
1307
1381
  // Legacy wrapper for backward compat
1308
1382
  async function updateWindowBox() { await fetchAndCacheUsage(); }
@@ -1333,10 +1407,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1333
1407
  // KEYBOARD SHORTCUTS - Only capture when dashboard pane has focus
1334
1408
  // In tmux split mode, this prevents capturing keys from Claude Code pane
1335
1409
  // ══════════════════════════════════════════════════════════════════════════
1336
- screen.key(['C-c'], () => {
1410
+ screen.key(['C-c', 'C-q'], () => {
1337
1411
  clearInterval(pollInterval);
1338
1412
  clearInterval(windowPollInterval);
1339
- clearInterval(countdownInterval);
1413
+ clearInterval(tickInterval);
1340
1414
  clearInterval(headerAnimInterval);
1341
1415
  clearInterval(fortuneInterval);
1342
1416
  screen.destroy();
@@ -1346,7 +1420,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1346
1420
  screen.key(['q'], () => {
1347
1421
  clearInterval(pollInterval);
1348
1422
  clearInterval(windowPollInterval);
1349
- clearInterval(countdownInterval);
1423
+ clearInterval(tickInterval);
1350
1424
  clearInterval(headerAnimInterval);
1351
1425
  clearInterval(fortuneInterval);
1352
1426
  screen.destroy();
@@ -1409,7 +1483,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1409
1483
  '\n' +
1410
1484
  '{bold}Controls{/bold}\n' +
1411
1485
  ' r Refresh now\n' +
1412
- ' q/Ctrl+C Quit\n' +
1486
+ ' q/Ctrl+Q Quit\n' +
1413
1487
  '\n' +
1414
1488
  '{gray-fg}Press any key to close{/gray-fg}'),
1415
1489
  tags: true,
@@ -1455,7 +1529,16 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1455
1529
  renderHeader();
1456
1530
  }, 30000);
1457
1531
  const windowPollInterval = setInterval(fetchAndCacheUsage, 15000); // fetch fresh data every 15s
1458
- const countdownInterval = setInterval(renderWindowBox, 1000); // tick countdown every 1s
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);
1459
1542
  }
1460
1543
  // ── Helpers ──
1461
1544
  function fmtK(n) {
@@ -1466,6 +1549,14 @@ function fmtK(n) {
1466
1549
  return String(n);
1467
1550
  }
1468
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
+ }
1469
1560
  // ── Session picker ──
1470
1561
  async function pickSession() {
1471
1562
  const sessions = await (0, usage_parser_js_1.listEkkosSessions)(20);
@@ -1523,9 +1614,8 @@ exports.dashboardCommand = new commander_1.Command('dashboard')
1523
1614
  }
1524
1615
  const jsonlPath = resolveJsonlPath(sessionName);
1525
1616
  if (!jsonlPath) {
1526
- console.log(chalk_1.default.red(`No JSONL found for "${sessionName}"`));
1527
- console.log(chalk_1.default.gray('Run "ekkos sessions" to see active sessions'));
1528
- 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}"...`));
1529
1619
  }
1530
- await launchDashboard(sessionName, jsonlPath, refreshMs);
1620
+ await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, Date.now());
1531
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:'));