@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
@@ -43,6 +43,7 @@ const fs = __importStar(require("fs"));
43
43
  const path = __importStar(require("path"));
44
44
  const os = __importStar(require("os"));
45
45
  const child_process_1 = require("child_process");
46
+ const session_bridge_1 = require("../synk/session-bridge");
46
47
  // ═══════════════════════════════════════════════════════════════════════════
47
48
  // ccDNA AUTO-LOAD: Apply Claude Code patches before spawning
48
49
  // ═══════════════════════════════════════════════════════════════════════════
@@ -428,9 +429,7 @@ const PINNED_CLAUDE_VERSION = 'latest';
428
429
  // Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
429
430
  // Configurable via environment variable
430
431
  const EKKOS_MAX_OUTPUT_TOKENS = process.env.EKKOS_MAX_OUTPUT_TOKENS || '16384';
431
- // Default proxy URL for context eviction
432
- // eslint-disable-next-line no-restricted-syntax -- Config URL, not API key
433
- const EKKOS_PROXY_URL = process.env.EKKOS_PROXY_URL || 'https://proxy.ekkos.dev';
432
+ const proxy_url_1 = require("../utils/proxy-url");
434
433
  // Track proxy mode for getEkkosEnv (set by run() based on options)
435
434
  let proxyModeEnabled = true;
436
435
  // ═══════════════════════════════════════════════════════════════════════════
@@ -459,7 +458,14 @@ function getEkkosEnv() {
459
458
  /* eslint-disable no-restricted-syntax -- System env spreading, not API key access */
460
459
  const env = {
461
460
  ...process.env,
462
- // Let Claude Code use its own default max_tokens (don't override)
461
+ // Disable Claude's built-in auto-memory ekkOS replaces it with 11-layer memory.
462
+ // This env var overrides all other settings (settings.json, /memory toggle).
463
+ // Saves ~1K tokens/turn of redundant context and avoids duplicate memory writes.
464
+ CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1',
465
+ // Disable auto-compact — ekkOS handles context preservation via PreserveContext/RestoreContext.
466
+ // Native autocompact would compact without ekkOS saving state first, causing knowledge loss.
467
+ // Only ekkOS-wrapped sessions get this; vanilla `claude` keeps autocompact on.
468
+ DISABLE_AUTO_COMPACT: 'true',
463
469
  };
464
470
  /* eslint-enable no-restricted-syntax */
465
471
  // Check if proxy is disabled via env var or options
@@ -493,13 +499,8 @@ function getEkkosEnv() {
493
499
  }
494
500
  // CRITICAL: Embed user/session in URL path since ANTHROPIC_HEADERS doesn't work
495
501
  // Claude Code SDK doesn't forward custom headers, but it DOES use ANTHROPIC_BASE_URL
496
- // Format: https://mcp.ekkos.dev/proxy/{userId}/{sessionName}?project={base64(cwd)}
497
502
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
498
- // Project path is base64-encoded to handle special chars safely
499
- const projectPath = process.cwd();
500
- const projectPathEncoded = Buffer.from(projectPath).toString('base64url');
501
- const userTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
502
- const proxyUrl = `${EKKOS_PROXY_URL}/proxy/${encodeURIComponent(userId)}/${encodeURIComponent(cliSessionName)}?project=${projectPathEncoded}&sid=${encodeURIComponent(cliSessionId)}&tz=${encodeURIComponent(userTz)}`;
503
+ const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId);
503
504
  env.ANTHROPIC_BASE_URL = proxyUrl;
504
505
  // Proxy URL contains userId + project path — don't leak to terminal
505
506
  }
@@ -836,10 +837,12 @@ function writeSessionFiles(sessionId, sessionName) {
836
837
  function launchWithDashboard(options) {
837
838
  const tmuxSession = `ekkos-${Date.now().toString(36)}`;
838
839
  const launchTime = Date.now();
840
+ // Pre-generate session name so dashboard can start immediately (no polling).
841
+ // Uses the user-supplied name if provided, otherwise mints a fresh one.
842
+ const sessionName = options.session || (0, state_1.uuidToWords)(crypto.randomUUID());
839
843
  // Build the ekkos run command WITHOUT --dashboard (prevent recursion)
840
- const runArgs = ['run'];
841
- if (options.session)
842
- runArgs.push('-s', options.session);
844
+ // Always pass -s so run reuses the same session name we gave the dashboard
845
+ const runArgs = ['run', '-s', sessionName];
843
846
  if (options.bypass)
844
847
  runArgs.push('-b');
845
848
  if (options.verbose)
@@ -858,15 +861,11 @@ function launchWithDashboard(options) {
858
861
  const cwd = process.cwd();
859
862
  const termCols = process.stdout.columns ?? 160;
860
863
  const termRows = process.stdout.rows ?? 48;
861
- // Write a marker file with launch timestamp + CWD so dashboard knows to wait for NEW session
862
- const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
863
- try {
864
- fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
865
- }
866
- catch { }
867
- const runCommand = `node "${ekkosCmd}" ${runArgs.join(' ')}`;
868
- // Use --wait-for-new flag to wait for a session that started AFTER this launch
869
- const dashCommand = `node "${ekkosCmd}" dashboard --wait-for-new --refresh 2000`;
864
+ // Write session hint immediately so dashboard can find JSONL as it appears
865
+ writeSessionFiles(crypto.randomUUID(), sessionName);
866
+ const runCommand = `EKKOS_NO_SPLASH=1 node "${ekkosCmd}" ${runArgs.join(' ')}`;
867
+ // Pass session name directly — dashboard starts rendering immediately (lazy JSONL resolution)
868
+ const dashCommand = `node "${ekkosCmd}" dashboard "${sessionName}" --refresh 2000`;
870
869
  try {
871
870
  // Pane 0 (left): start with inert command (no interactive shell startup noise).
872
871
  // Claude is launched AFTER split so Ink gets final pane geometry at startup.
@@ -881,6 +880,9 @@ function launchWithDashboard(options) {
881
880
  }
882
881
  }
883
882
  };
883
+ // Terminal type and escape sequence passthrough for Ink TUI rendering
884
+ applyTmuxOpt(`set-option -t "${tmuxSession}" default-terminal "xterm-256color"`);
885
+ applyTmuxOpt(`set-option -sa -t "${tmuxSession}" terminal-overrides ",xterm-256color:Tc:smcup@:rmcup@"`);
884
886
  // Session/window isolation and quality-of-life settings
885
887
  applyTmuxOpt(`set-option -t "${tmuxSession}" mouse on`);
886
888
  applyTmuxOpt(`set-window-option -t "${tmuxSession}" history-limit 100000`);
@@ -917,18 +919,13 @@ function launchWithDashboard(options) {
917
919
  */
918
920
  function launchWithWindowsTerminal(options) {
919
921
  const cwd = process.cwd();
920
- const launchTime = Date.now();
921
- // Write marker file so dashboard knows to wait for a NEW session
922
- const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
923
- try {
924
- fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
925
- }
926
- catch { }
922
+ // Pre-generate session name so dashboard can start immediately
923
+ const sessionName = options.session || (0, state_1.uuidToWords)(crypto.randomUUID());
924
+ // Write session hint immediately
925
+ writeSessionFiles(crypto.randomUUID(), sessionName);
927
926
  // Build ekkos run args WITHOUT --dashboard (prevent recursion)
928
- // Uses `ekkos` directly it's in PATH since user ran `ekkos --dashboard`
929
- const runArgs = ['run'];
930
- if (options.session)
931
- runArgs.push('-s', options.session);
927
+ // Always pass -s so run reuses the same session name we gave the dashboard
928
+ const runArgs = ['run', '-s', sessionName];
932
929
  if (options.bypass)
933
930
  runArgs.push('-b');
934
931
  if (options.verbose)
@@ -945,10 +942,10 @@ function launchWithWindowsTerminal(options) {
945
942
  runArgs.push('--skip-proxy');
946
943
  // Write a temp batch file to avoid all quoting issues
947
944
  // wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
948
- const batPath = path.join(os.tmpdir(), `ekkos-wt-${launchTime}.cmd`);
945
+ const batPath = path.join(os.tmpdir(), `ekkos-wt-${Date.now()}.cmd`);
949
946
  const batContent = [
950
947
  '@echo off',
951
- `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`,
948
+ `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 "${sessionName}" --refresh 2000`,
952
949
  ].join('\r\n');
953
950
  try {
954
951
  fs.writeFileSync(batPath, batContent);
@@ -990,6 +987,13 @@ async function run(options) {
990
987
  const verbose = options.verbose || false;
991
988
  const bypass = options.bypass || false;
992
989
  const noInject = options.noInject || false;
990
+ // Honour -s flag: lock the session name before getEkkosEnv() can mint a new one.
991
+ // This is critical for --dashboard mode where launchWithDashboard() pre-generates
992
+ // a session name and passes it via -s so the dashboard and run command agree.
993
+ if (options.session && !cliSessionName) {
994
+ cliSessionName = options.session;
995
+ cliSessionId = crypto.randomUUID(); // pair an ID with it
996
+ }
993
997
  // Set proxy mode based on options (used by getEkkosEnv)
994
998
  proxyModeEnabled = !(options.noProxy || false);
995
999
  if (proxyModeEnabled) {
@@ -1169,7 +1173,7 @@ async function run(options) {
1169
1173
  // ══════════════════════════════════════════════════════════════════════════
1170
1174
  // STARTUP BANNER WITH COLOR PULSE ANIMATION
1171
1175
  // ══════════════════════════════════════════════════════════════════════════
1172
- const skipFancyIntro = process.env.EKKOS_REMOTE_SESSION === '1' || process.env.EKKOS_NO_SPLASH === '1';
1176
+ const skipFancyIntro = process.env.EKKOS_REMOTE_SESSION === '1' || process.env.EKKOS_NO_SPLASH === '1' || !!process.env.TMUX;
1173
1177
  if (!skipFancyIntro) {
1174
1178
  const logoLines = [
1175
1179
  ' ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ▄▄',
@@ -1374,6 +1378,8 @@ async function run(options) {
1374
1378
  // ANIMATION COMPLETE: Mark ready and flush buffered Claude output
1375
1379
  // ══════════════════════════════════════════════════════════════════════════
1376
1380
  animationComplete = true;
1381
+ // Clear terminal to prevent startup banner artifacts bleeding into Claude Code's Ink TUI
1382
+ process.stdout.write('\x1B[2J\x1B[H');
1377
1383
  dlog(`Animation complete. shellReady=${shellReady}, buffered=${bufferedOutput.length} chunks`);
1378
1384
  // Show loading indicator if Claude is still initializing
1379
1385
  if (shellReady && bufferedOutput.length === 0) {
@@ -1401,9 +1407,10 @@ async function run(options) {
1401
1407
  // MULTI-SESSION SUPPORT: Register this process as an active session
1402
1408
  // This prevents state collision when multiple Claude Code instances run
1403
1409
  // ════════════════════════════════════════════════════════════════════════════
1404
- (0, state_1.registerActiveSession)('pending', // Session ID not yet known
1405
- currentSession || 'initializing', process.cwd());
1406
- writeSessionFiles(cliSessionId || 'pending', currentSession || 'initializing');
1410
+ const initialSessionId = cliSessionId || 'pending';
1411
+ const initialSessionName = currentSession || 'initializing';
1412
+ (0, state_1.registerActiveSession)(initialSessionId, initialSessionName, process.cwd());
1413
+ writeSessionFiles(initialSessionId, initialSessionName);
1407
1414
  dlog(`Registered active session (PID ${process.pid})`);
1408
1415
  // Show active sessions count if verbose
1409
1416
  if (verbose) {
@@ -1412,6 +1419,8 @@ async function run(options) {
1412
1419
  console.log(chalk_1.default.cyan(` 📊 Active ekkOS sessions: ${activeSessions.length}`));
1413
1420
  }
1414
1421
  }
1422
+ // Synk bridge — real-time session sync (null when synk not configured)
1423
+ const synkBridge = await session_bridge_1.SynkSessionBridge.create({ cwd: process.cwd(), verbose });
1415
1424
  let isAutoClearInProgress = false;
1416
1425
  let transcriptPath = null;
1417
1426
  let currentSessionId = null;
@@ -1419,9 +1428,9 @@ async function run(options) {
1419
1428
  // PER-TURN BANNER STATE
1420
1429
  // Tracks idle→active transitions to print the session banner once per turn
1421
1430
  // ══════════════════════════════════════════════════════════════════════════
1422
- let wasIdle = true; // Start as idle (no prompt submitted yet)
1423
- let _turnCount = 0; // Incremented each time a new turn banner prints (unused beyond banner)
1424
- let lastBannerTime = 0; // Epoch ms of last banner print (debounce guard)
1431
+ let wasIdle = false; // Start as NOT idle first idle→active fires after startup
1432
+ let turnCount = 0; // Incremented each time a new turn banner prints
1433
+ let lastBannerTime = Date.now(); // Grace period so startup output doesn't trigger banner
1425
1434
  // Stream tailer for mid-turn context capture (must be declared before polling code)
1426
1435
  let streamTailer = null;
1427
1436
  const streamCacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions', instanceId);
@@ -1866,6 +1875,7 @@ async function run(options) {
1866
1875
  console.log('');
1867
1876
  }
1868
1877
  let shell;
1878
+ const sessionStartTime = Date.now();
1869
1879
  // ══════════════════════════════════════════════════════════════════════════
1870
1880
  // USE EARLY-SPAWNED SHELL OR CREATE NEW ONE
1871
1881
  // If we spawned Claude during animation, reuse it. Otherwise spawn now.
@@ -1973,6 +1983,17 @@ async function run(options) {
1973
1983
  process.stdin.on('data', onStdinData);
1974
1984
  // Helper to get current output buffer (for readiness checks)
1975
1985
  const getOutputBuffer = () => outputBuffer;
1986
+ // Synk remote message injection — receive messages from mobile/web, inject into PTY
1987
+ if (synkBridge) {
1988
+ synkBridge.on('remote-message', async (msg) => {
1989
+ const { ready } = await waitForIdlePrompt(getOutputBuffer, config);
1990
+ if (ready) {
1991
+ shell.write('\x15'); // Ctrl+U clear line
1992
+ await typeSlowly(shell, msg.text, config.charDelayMs);
1993
+ shell.write('\r');
1994
+ }
1995
+ });
1996
+ }
1976
1997
  // Handle context wall detection
1977
1998
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
1978
1999
  async function handleContextWall() {
@@ -2350,6 +2371,8 @@ async function run(options) {
2350
2371
  if (outputBuffer.length > 5000) {
2351
2372
  outputBuffer = outputBuffer.slice(-2000);
2352
2373
  }
2374
+ // Forward to synk real-time sync (batched internally)
2375
+ synkBridge?.onAgentOutput(data);
2353
2376
  // ══════════════════════════════════════════════════════════════════════════
2354
2377
  // PER-TURN BANNER (replaces hook-based terminal header)
2355
2378
  // Prints once per turn when transitioning from idle → active (AI responding).
@@ -2364,7 +2387,7 @@ async function run(options) {
2364
2387
  !trimmed.startsWith('❯') &&
2365
2388
  !trimmed.startsWith('%'); // zsh prompt artefact
2366
2389
  if (isSubstantialOutput && Date.now() - lastBannerTime > 3000) {
2367
- _turnCount++;
2390
+ turnCount++;
2368
2391
  const now = new Date();
2369
2392
  const timeStr = now.toLocaleTimeString('en-US', {
2370
2393
  hour: 'numeric',
@@ -2382,11 +2405,14 @@ async function run(options) {
2382
2405
  process.stderr.write(`${chalk_1.default.cyan.bold('🧠 ekkOS Memory')} ${chalk_1.default.dim(`| ${sessionLabel} | ${dateStr} ${timeStr} EST`)}\n`);
2383
2406
  lastBannerTime = Date.now();
2384
2407
  wasIdle = false;
2408
+ synkBridge?.onIdleChange(false);
2385
2409
  }
2386
2410
  }
2387
2411
  // Track idle state for the banner (works in both proxy and local mode).
2388
2412
  // We check the raw outputBuffer (stripped) so the idle regex fires reliably.
2389
2413
  if (IDLE_PROMPT_REGEX.test(stripAnsi(outputBuffer))) {
2414
+ if (!wasIdle)
2415
+ synkBridge?.onIdleChange(true);
2390
2416
  wasIdle = true;
2391
2417
  }
2392
2418
  // ══════════════════════════════════════════════════════════════════════════
@@ -2494,6 +2520,7 @@ async function run(options) {
2494
2520
  dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
2495
2521
  bindRealSessionToProxy(currentSession, 'status-line', currentSessionId || undefined);
2496
2522
  resolveTranscriptFromSessionId('status-line');
2523
+ synkBridge?.onSessionEstablished(currentSession, currentSessionId || 'unknown', { hostPid: process.pid });
2497
2524
  }
2498
2525
  }
2499
2526
  else {
@@ -2685,10 +2712,99 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
2685
2712
  }
2686
2713
  }, 4000); // Wait 4 seconds for Claude to fully initialize
2687
2714
  }
2715
+ // ══════════════════════════════════════════════════════════════════════════
2716
+ // SESSION-END VALUE REPORT
2717
+ // Prints a boxed summary of session stats before exit.
2718
+ // Fetches savings data from ekkOS API with a 2s timeout — skips silently on failure.
2719
+ // ══════════════════════════════════════════════════════════════════════════
2720
+ async function printSessionEndReport() {
2721
+ try {
2722
+ const sessionName = currentSession || cliSessionName || 'unknown';
2723
+ // Compute duration
2724
+ const elapsedMs = Date.now() - sessionStartTime;
2725
+ const totalSec = Math.floor(elapsedMs / 1000);
2726
+ const hours = Math.floor(totalSec / 3600);
2727
+ const minutes = Math.floor((totalSec % 3600) / 60);
2728
+ const seconds = totalSec % 60;
2729
+ let duration;
2730
+ if (hours > 0) {
2731
+ duration = `${hours}h ${minutes}m ${seconds}s`;
2732
+ }
2733
+ else if (minutes > 0) {
2734
+ duration = `${minutes}m ${seconds}s`;
2735
+ }
2736
+ else {
2737
+ duration = `${seconds}s`;
2738
+ }
2739
+ // Fetch savings stats from ekkOS API (2-second timeout)
2740
+ let patternsRecalled = 0;
2741
+ let tokensSaved = 0;
2742
+ let costSaved = '0.00';
2743
+ try {
2744
+ const mcpUrl = process.env.EKKOS_MCP_URL || MEMORY_API_URL;
2745
+ const authToken = (0, state_1.getAuthToken)();
2746
+ if (authToken) {
2747
+ const controller = new AbortController();
2748
+ const timeout = setTimeout(() => controller.abort(), 2000);
2749
+ const res = await fetch(`${mcpUrl}/api/v1/savings/me?period=day`, {
2750
+ method: 'GET',
2751
+ headers: {
2752
+ 'Authorization': `Bearer ${authToken}`,
2753
+ 'Content-Type': 'application/json',
2754
+ },
2755
+ signal: controller.signal,
2756
+ });
2757
+ clearTimeout(timeout);
2758
+ if (res.ok) {
2759
+ const data = await res.json();
2760
+ patternsRecalled = data.patterns_recalled || 0;
2761
+ tokensSaved = data.tokens_saved || 0;
2762
+ costSaved = (data.cost_saved || 0).toFixed(2);
2763
+ }
2764
+ }
2765
+ }
2766
+ catch {
2767
+ // Silent fail — API unavailable or timed out, just use defaults
2768
+ }
2769
+ // Format token count with commas
2770
+ const tokensFormatted = tokensSaved.toLocaleString('en-US');
2771
+ // Build the box content
2772
+ const title = `ekkOS_ Session · ${sessionName}`;
2773
+ const line1 = `Duration: ${duration} | Turns: ${turnCount}`;
2774
+ const line2 = `Patterns recalled: ${patternsRecalled}`;
2775
+ const line3 = `Tokens saved: ${tokensFormatted} (~$${costSaved})`;
2776
+ // Calculate box width (minimum 49, expand for long content)
2777
+ const contentLines = [title, line1, line2, line3];
2778
+ const maxContent = Math.max(...contentLines.map(l => l.length));
2779
+ const innerWidth = Math.max(47, maxContent + 4);
2780
+ const pad = (text) => {
2781
+ const padding = innerWidth - text.length - 2;
2782
+ return ` ${text}${' '.repeat(Math.max(0, padding))}`;
2783
+ };
2784
+ const top = `╭${'─'.repeat(innerWidth)}╮`;
2785
+ const sep = `├${'─'.repeat(innerWidth)}┤`;
2786
+ const bottom = `╰${'─'.repeat(innerWidth)}╯`;
2787
+ const row = (text) => `│${pad(text)}│`;
2788
+ // Print the box
2789
+ console.log('');
2790
+ console.log(chalk_1.default.dim(top));
2791
+ console.log(chalk_1.default.dim('│') + chalk_1.default.bold(pad(title)) + chalk_1.default.dim('│'));
2792
+ console.log(chalk_1.default.dim(sep));
2793
+ console.log(chalk_1.default.dim('│') + pad(line1) + chalk_1.default.dim('│'));
2794
+ console.log(chalk_1.default.dim(row(line2)));
2795
+ console.log(chalk_1.default.dim('│') + chalk_1.default.green(pad(line3)) + chalk_1.default.dim('│'));
2796
+ console.log(chalk_1.default.dim(bottom));
2797
+ console.log('');
2798
+ }
2799
+ catch {
2800
+ // Never block exit — silently skip the entire report on any error
2801
+ }
2802
+ }
2688
2803
  // Handle PTY exit
2689
- shell.onExit(({ exitCode }) => {
2804
+ shell.onExit(async ({ exitCode }) => {
2690
2805
  (0, state_1.clearAutoClearFlag)();
2691
2806
  stopStreamTailer(); // Stop stream capture
2807
+ await synkBridge?.shutdown(); // Close synk session
2692
2808
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
2693
2809
  cleanupInstanceFile(instanceId); // Clean up instance file
2694
2810
  // NOTE: No ccDNA restore needed - ekkOS uses separate installation from homebrew
@@ -2698,6 +2814,8 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
2698
2814
  process.stdin.setRawMode(false);
2699
2815
  }
2700
2816
  process.stdin.pause();
2817
+ // Print session-end value report before exiting
2818
+ await printSessionEndReport();
2701
2819
  // Log exit to file (not terminal - TUI already gone at this point)
2702
2820
  dlog(`Claude exited with code ${exitCode}`);
2703
2821
  process.exit(exitCode);
@@ -2706,6 +2824,7 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
2706
2824
  const cleanup = () => {
2707
2825
  (0, state_1.clearAutoClearFlag)();
2708
2826
  stopStreamTailer(); // Stop stream capture
2827
+ synkBridge?.shutdown().catch(() => { }); // Close synk session (best-effort)
2709
2828
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
2710
2829
  cleanupInstanceFile(instanceId); // Clean up instance file
2711
2830
  // NOTE: No ccDNA restore needed - ekkOS uses separate installation from homebrew
@@ -1 +1,4 @@
1
- export declare function status(): Promise<void>;
1
+ export declare function status(opts?: {
2
+ watch?: boolean;
3
+ json?: boolean;
4
+ }): Promise<void>;
@@ -11,40 +11,108 @@ const chalk_1 = __importDefault(require("chalk"));
11
11
  const ora_1 = __importDefault(require("ora"));
12
12
  const EKKOS_API_URL = 'https://mcp.ekkos.dev';
13
13
  const CONFIG_FILE = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'config.json');
14
- async function status() {
15
- console.log('');
16
- console.log(chalk_1.default.cyan.bold('📊 ekkOS Memory Status'));
17
- console.log(chalk_1.default.gray('─'.repeat(50)));
18
- console.log('');
19
- // Check config exists
20
- if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
21
- console.log(chalk_1.default.red('✗ Not configured'));
22
- console.log(chalk_1.default.gray(' Run: npx @ekkos/cli setup'));
23
- process.exit(1);
24
- }
25
- let config;
14
+ const METRICS_FILE = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'session-metrics.json');
15
+ /** Age (ms) after which the metrics file is considered stale (no active session). */
16
+ const METRICS_STALE_MS = 5 * 60 * 1000; // 5 minutes
17
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
18
+ function readMetrics() {
19
+ if (!(0, fs_1.existsSync)(METRICS_FILE))
20
+ return null;
26
21
  try {
27
- config = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
22
+ return JSON.parse((0, fs_1.readFileSync)(METRICS_FILE, 'utf-8'));
28
23
  }
29
24
  catch {
30
- console.log(chalk_1.default.red('✗ Invalid configuration'));
31
- process.exit(1);
25
+ return null;
32
26
  }
33
- const apiKey = config.apiKey || process.env.EKKOS_API_KEY;
34
- if (!apiKey) {
35
- console.log(chalk_1.default.red('✗ No API key'));
36
- process.exit(1);
27
+ }
28
+ function isMetricsStale(m) {
29
+ const updatedAt = new Date(m.updated_at).getTime();
30
+ return Date.now() - updatedAt > METRICS_STALE_MS;
31
+ }
32
+ function formatDuration(isoStart) {
33
+ const ms = Date.now() - new Date(isoStart).getTime();
34
+ const totalSec = Math.floor(ms / 1000);
35
+ const h = Math.floor(totalSec / 3600);
36
+ const m = Math.floor((totalSec % 3600) / 60);
37
+ const s = totalSec % 60;
38
+ if (h > 0)
39
+ return `${h}h ${m}m`;
40
+ if (m > 0)
41
+ return `${m}m ${s}s`;
42
+ return `${s}s`;
43
+ }
44
+ function formatTokens(n) {
45
+ if (n >= 1000000)
46
+ return `${(n / 1000000).toFixed(1)}M`;
47
+ if (n >= 1000)
48
+ return `${Math.round(n / 1000)}K`;
49
+ return String(n);
50
+ }
51
+ // ─── Session panel ────────────────────────────────────────────────────────────
52
+ function printSessionPanel(m) {
53
+ const sessionLabel = m.session_name || m.session_id;
54
+ const duration = formatDuration(m.started_at);
55
+ const turnLabel = m.turn > 0 ? `Turn ${m.turn}` : `${m.tool_calls} calls`;
56
+ const lines = [
57
+ `ekkOS Session: ${sessionLabel}`,
58
+ `Duration: ${duration} · ${turnLabel}`,
59
+ null, // separator
60
+ `Patterns recalled: ${m.patterns_recalled}`,
61
+ `Patterns forged: ${m.patterns_forged}`,
62
+ `Directives active: ${m.directives_active}`,
63
+ `Cache: ${m.cache_backend} (${m.cache_patterns} patterns)`,
64
+ null, // separator
65
+ `Tokens: ${formatTokens(m.tokens_in)} in · ${formatTokens(m.tokens_out)} out`,
66
+ ];
67
+ // Compute box width from longest content line
68
+ const contentLines = lines.filter(Boolean);
69
+ const maxLen = Math.max(...contentLines.map(l => l.length));
70
+ const innerWidth = Math.max(39, maxLen + 4); // 2 spaces padding each side
71
+ const pad = (text) => {
72
+ const spaces = innerWidth - text.length - 2;
73
+ return ` ${text}${' '.repeat(Math.max(0, spaces))}`;
74
+ };
75
+ const top = `┌${'─'.repeat(innerWidth)}┐`;
76
+ const sep = `├${'─'.repeat(innerWidth)}┤`;
77
+ const bottom = `└${'─'.repeat(innerWidth)}┘`;
78
+ const row = (text) => `│${pad(text)}│`;
79
+ const sepRow = sep;
80
+ console.log('');
81
+ console.log(chalk_1.default.cyan(top));
82
+ let firstSep = false;
83
+ for (const line of lines) {
84
+ if (line === null) {
85
+ // Separator after header block, then again before token block
86
+ if (!firstSep) {
87
+ console.log(chalk_1.default.cyan(sepRow));
88
+ firstSep = true;
89
+ }
90
+ else {
91
+ console.log(chalk_1.default.cyan(sepRow));
92
+ }
93
+ }
94
+ else if (!firstSep) {
95
+ // Header rows — bold
96
+ console.log(chalk_1.default.cyan('│') + chalk_1.default.bold(pad(line)) + chalk_1.default.cyan('│'));
97
+ }
98
+ else {
99
+ console.log(chalk_1.default.cyan(row(line)));
100
+ }
37
101
  }
102
+ console.log(chalk_1.default.cyan(bottom));
103
+ console.log('');
104
+ }
105
+ // ─── Memory API stats (original status content) ───────────────────────────────
106
+ async function printMemoryStats(apiKey, config) {
38
107
  const spinner = (0, ora_1.default)('Fetching memory stats...').start();
39
108
  try {
40
- // Get pattern stats
41
109
  const patternsResponse = await fetch(`${EKKOS_API_URL}/api/v1/patterns/query`, {
42
110
  method: 'POST',
43
111
  headers: {
44
112
  'Authorization': `Bearer ${apiKey}`,
45
- 'Content-Type': 'application/json'
113
+ 'Content-Type': 'application/json',
46
114
  },
47
- body: JSON.stringify({ query: '', k: 100 })
115
+ body: JSON.stringify({ query: '', k: 100 }),
48
116
  });
49
117
  let patternCount = 0;
50
118
  let avgSuccessRate = 0;
@@ -60,13 +128,11 @@ async function status() {
60
128
  }
61
129
  }
62
130
  spinner.stop();
63
- // Display stats
64
131
  console.log(chalk_1.default.cyan('Patterns:'));
65
132
  console.log(` Total: ${chalk_1.default.bold(patternCount)}`);
66
133
  console.log(` Success Rate: ${chalk_1.default.bold((avgSuccessRate * 100).toFixed(1) + '%')}`);
67
134
  console.log(` Applications: ${chalk_1.default.bold(totalApplications)}`);
68
135
  console.log('');
69
- // IDE status
70
136
  console.log(chalk_1.default.cyan('Connected IDEs:'));
71
137
  const ides = config.installedIDEs || [];
72
138
  if (ides.length === 0) {
@@ -78,13 +144,11 @@ async function status() {
78
144
  }
79
145
  }
80
146
  console.log('');
81
- // Config info
82
147
  console.log(chalk_1.default.cyan('Configuration:'));
83
148
  console.log(` Config File: ${chalk_1.default.gray(CONFIG_FILE)}`);
84
149
  console.log(` Installed: ${chalk_1.default.gray(config.installedAt || 'Unknown')}`);
85
150
  console.log(` API Key: ${chalk_1.default.gray(apiKey.substring(0, 10) + '...')}`);
86
151
  console.log('');
87
- // Golden Loop status
88
152
  console.log(chalk_1.default.cyan('Golden Loop:'));
89
153
  const loopActive = patternCount > 0 || totalApplications > 0;
90
154
  if (loopActive) {
@@ -95,7 +159,6 @@ async function status() {
95
159
  console.log(chalk_1.default.yellow(' ○ INITIALIZING - Start coding to build memory'));
96
160
  }
97
161
  console.log('');
98
- // Footer
99
162
  console.log(chalk_1.default.gray('─'.repeat(50)));
100
163
  console.log('');
101
164
  console.log(`Dashboard: ${chalk_1.default.cyan('https://ekkos.dev/dashboard')}`);
@@ -107,3 +170,78 @@ async function status() {
107
170
  process.exit(1);
108
171
  }
109
172
  }
173
+ // ─── Single render pass ───────────────────────────────────────────────────────
174
+ function renderOnce(opts) {
175
+ const metrics = readMetrics();
176
+ if (opts.json) {
177
+ if (!metrics || isMetricsStale(metrics)) {
178
+ console.log(JSON.stringify({ active: false, reason: 'No active session or session stale' }));
179
+ }
180
+ else {
181
+ console.log(JSON.stringify({ active: true, ...metrics }));
182
+ }
183
+ return;
184
+ }
185
+ if (!metrics || isMetricsStale(metrics)) {
186
+ console.log('');
187
+ console.log(chalk_1.default.gray(' No active ekkOS session detected.'));
188
+ console.log(chalk_1.default.gray(' Run `ekkos run` to start a session.'));
189
+ console.log('');
190
+ return;
191
+ }
192
+ printSessionPanel(metrics);
193
+ }
194
+ // ─── Main export ──────────────────────────────────────────────────────────────
195
+ async function status(opts = {}) {
196
+ // Watch mode: render every 2s, clear between renders
197
+ if (opts.watch) {
198
+ // Initial render
199
+ process.stdout.write('\x1B[2J\x1B[0f'); // clear screen
200
+ renderOnce({ json: false, skipMemory: true });
201
+ setInterval(() => {
202
+ process.stdout.write('\x1B[2J\x1B[0f');
203
+ renderOnce({ json: false, skipMemory: true });
204
+ }, 2000);
205
+ // Keep alive until Ctrl-C
206
+ return;
207
+ }
208
+ // JSON mode: just dump the metrics file
209
+ if (opts.json) {
210
+ renderOnce({ json: true, skipMemory: false });
211
+ return;
212
+ }
213
+ // Normal mode: show session panel (if active) then memory stats
214
+ console.log('');
215
+ console.log(chalk_1.default.cyan.bold('ekkOS Memory Status'));
216
+ console.log(chalk_1.default.gray('─'.repeat(50)));
217
+ // Session panel (live metrics from MCP server)
218
+ const metrics = readMetrics();
219
+ if (metrics && !isMetricsStale(metrics)) {
220
+ printSessionPanel(metrics);
221
+ }
222
+ else {
223
+ console.log('');
224
+ console.log(chalk_1.default.gray(' No active session'));
225
+ console.log('');
226
+ }
227
+ // Memory API stats (requires config + network)
228
+ if (!(0, fs_1.existsSync)(CONFIG_FILE)) {
229
+ console.log(chalk_1.default.red(' ✗ Not configured — run: ekkos init'));
230
+ console.log('');
231
+ return;
232
+ }
233
+ let config;
234
+ try {
235
+ config = JSON.parse((0, fs_1.readFileSync)(CONFIG_FILE, 'utf-8'));
236
+ }
237
+ catch {
238
+ console.log(chalk_1.default.red(' ✗ Invalid configuration'));
239
+ return;
240
+ }
241
+ const apiKey = config.apiKey || process.env.EKKOS_API_KEY;
242
+ if (!apiKey) {
243
+ console.log(chalk_1.default.red(' ✗ No API key — run: ekkos init'));
244
+ return;
245
+ }
246
+ await printMemoryStats(apiKey, config);
247
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ekkOS_synk commands — remote session sync for Claude Code
3
+ *
4
+ * Provides: ekkos synk auth, connect, disconnect, sessions, daemon start/stop/status, doctor
5
+ */
6
+ import { Command } from 'commander';
7
+ export declare function registerSynkCommands(program: Command): void;