@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.
- package/dist/commands/dashboard.js +147 -57
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +54 -16
- package/dist/commands/run.js +163 -44
- package/dist/commands/status.d.ts +4 -1
- package/dist/commands/status.js +165 -27
- package/dist/commands/synk.d.ts +7 -0
- package/dist/commands/synk.js +339 -0
- package/dist/deploy/settings.d.ts +6 -5
- package/dist/deploy/settings.js +27 -17
- package/dist/index.js +12 -82
- package/dist/lib/usage-parser.d.ts +1 -1
- package/dist/lib/usage-parser.js +5 -3
- package/dist/local/index.d.ts +14 -0
- package/dist/local/index.js +28 -0
- package/dist/local/local-embeddings.d.ts +49 -0
- package/dist/local/local-embeddings.js +232 -0
- package/dist/local/offline-fallback.d.ts +44 -0
- package/dist/local/offline-fallback.js +159 -0
- package/dist/local/sqlite-store.d.ts +126 -0
- package/dist/local/sqlite-store.js +393 -0
- package/dist/local/sync-engine.d.ts +42 -0
- package/dist/local/sync-engine.js +223 -0
- package/dist/synk/api.d.ts +22 -0
- package/dist/synk/api.js +133 -0
- package/dist/synk/auth.d.ts +7 -0
- package/dist/synk/auth.js +30 -0
- package/dist/synk/config.d.ts +18 -0
- package/dist/synk/config.js +37 -0
- package/dist/synk/daemon/control-client.d.ts +11 -0
- package/dist/synk/daemon/control-client.js +101 -0
- package/dist/synk/daemon/control-server.d.ts +24 -0
- package/dist/synk/daemon/control-server.js +91 -0
- package/dist/synk/daemon/run.d.ts +14 -0
- package/dist/synk/daemon/run.js +338 -0
- package/dist/synk/encryption.d.ts +17 -0
- package/dist/synk/encryption.js +133 -0
- package/dist/synk/index.d.ts +13 -0
- package/dist/synk/index.js +36 -0
- package/dist/synk/machine-client.d.ts +42 -0
- package/dist/synk/machine-client.js +218 -0
- package/dist/synk/persistence.d.ts +51 -0
- package/dist/synk/persistence.js +211 -0
- package/dist/synk/qr.d.ts +5 -0
- package/dist/synk/qr.js +33 -0
- package/dist/synk/session-bridge.d.ts +58 -0
- package/dist/synk/session-bridge.js +171 -0
- package/dist/synk/session-client.d.ts +46 -0
- package/dist/synk/session-client.js +240 -0
- package/dist/synk/types.d.ts +574 -0
- package/dist/synk/types.js +74 -0
- package/dist/utils/platform.d.ts +5 -1
- package/dist/utils/platform.js +24 -4
- package/dist/utils/proxy-url.d.ts +10 -0
- package/dist/utils/proxy-url.js +19 -0
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +11 -3
- package/package.json +13 -4
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +0 -819
- package/templates/claude-plugins-admin/README.md +0 -446
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +0 -595
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +0 -798
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +0 -554
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +0 -881
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +0 -85
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +0 -569
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +0 -863
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +0 -8
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +0 -732
- package/templates/hooks-node/lib/state.js +0 -187
- package/templates/hooks-node/stop.js +0 -416
- package/templates/hooks-node/user-prompt-submit.js +0 -337
- package/templates/rules/00-hooks-contract.mdc +0 -89
- package/templates/rules/30-ekkos-core.mdc +0 -188
- package/templates/rules/31-ekkos-messages.mdc +0 -78
package/dist/commands/run.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
841
|
-
|
|
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
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
//
|
|
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-${
|
|
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
|
|
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
|
-
|
|
1405
|
-
currentSession || 'initializing'
|
|
1406
|
-
|
|
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 =
|
|
1423
|
-
let
|
|
1424
|
-
let lastBannerTime =
|
|
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
|
-
|
|
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
|
package/dist/commands/status.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
22
|
+
return JSON.parse((0, fs_1.readFileSync)(METRICS_FILE, 'utf-8'));
|
|
28
23
|
}
|
|
29
24
|
catch {
|
|
30
|
-
|
|
31
|
-
process.exit(1);
|
|
25
|
+
return null;
|
|
32
26
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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;
|