@ekkos/cli 1.2.14 → 1.2.15
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/cache/capture.js +0 -0
- package/dist/commands/dashboard.js +85 -69
- package/dist/commands/run.js +31 -25
- package/dist/cron/promoter.js +0 -0
- package/dist/index.js +68 -5
- package/dist/utils/session-binding.d.ts +9 -2
- package/dist/utils/session-binding.js +41 -16
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +2 -2
- package/package.json +1 -1
- package/templates/CLAUDE.md +6 -5
- package/templates/hooks/stop.ps1 +43 -29
- package/templates/hooks/stop.sh +18 -14
- package/templates/hooks/user-prompt-submit.ps1 +121 -33
- package/templates/hooks/user-prompt-submit.sh +97 -94
package/dist/cache/capture.js
CHANGED
|
File without changes
|
|
@@ -565,24 +565,21 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
565
565
|
// 3. Claude Code pane remains fully independent
|
|
566
566
|
// 4. Text selection works across panes without interference
|
|
567
567
|
// ══════════════════════════════════════════════════════════════════════════
|
|
568
|
-
// Open /dev/tty for blessed to use (gives it direct terminal access)
|
|
569
|
-
// On macOS/Linux in tmux, this isolates the dashboard from stdout piping.
|
|
570
|
-
// On Windows (no /dev/tty), stdin/stdout is correct — blessed handles the
|
|
571
|
-
// alternate screen buffer natively for clean rendering.
|
|
568
|
+
// Open /dev/tty for blessed to use (gives it direct terminal access)
|
|
572
569
|
const fs = require('fs');
|
|
573
570
|
let ttyInput = process.stdin;
|
|
574
571
|
let ttyOutput = process.stdout;
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
572
|
+
try {
|
|
573
|
+
// Create readable/writable streams from /dev/tty file descriptors
|
|
574
|
+
const ttyFd = fs.openSync('/dev/tty', 'r+');
|
|
575
|
+
ttyInput = new (require('tty').ReadStream)(ttyFd);
|
|
576
|
+
ttyOutput = new (require('tty').WriteStream)(ttyFd);
|
|
577
|
+
}
|
|
578
|
+
catch (e) {
|
|
579
|
+
// Fallback: if /dev/tty unavailable (some CI/remote environments),
|
|
580
|
+
// use stdin/stdout but disable all event handling
|
|
581
|
+
ttyInput = process.stdin;
|
|
582
|
+
ttyOutput = process.stdout;
|
|
586
583
|
}
|
|
587
584
|
const screen = blessed.screen({
|
|
588
585
|
smartCSR: true,
|
|
@@ -599,17 +596,16 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
599
596
|
resizeTimeout: 300, // Debounce resize events
|
|
600
597
|
});
|
|
601
598
|
// ══════════════════════════════════════════════════════════════════════════
|
|
602
|
-
// DISABLE TERMINAL CONTROL SEQUENCE HIJACKING
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
// alternate buffer and raw mode for clean rendering without artifacts.
|
|
599
|
+
// DISABLE TERMINAL CONTROL SEQUENCE HIJACKING
|
|
600
|
+
// Prevent blessed from sending control sequences that might interfere with
|
|
601
|
+
// other panes in tmux. The /dev/tty approach already isolates this, but we
|
|
602
|
+
// also disable the raw mode entirely to be extra safe.
|
|
607
603
|
// ══════════════════════════════════════════════════════════════════════════
|
|
608
|
-
if (
|
|
609
|
-
// Override alternateBuffer to do nothing
|
|
604
|
+
if (screen.program) {
|
|
605
|
+
// Override alternateBuffer to do nothing
|
|
610
606
|
screen.program.alternateBuffer = () => { };
|
|
611
607
|
screen.program.normalBuffer = () => { };
|
|
612
|
-
// Don't enter raw mode — let the OS handle it
|
|
608
|
+
// Don't enter raw mode — let the OS handle it
|
|
613
609
|
if (screen.program.setRawMode) {
|
|
614
610
|
screen.program.setRawMode = (enabled) => {
|
|
615
611
|
// Silently ignore raw mode requests
|
|
@@ -1081,50 +1077,72 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1081
1077
|
let header = '';
|
|
1082
1078
|
let separator = '';
|
|
1083
1079
|
let rows = [];
|
|
1084
|
-
//
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1080
|
+
// Responsive table layouts keep headers visible even in very narrow panes.
|
|
1081
|
+
if (w >= 60) {
|
|
1082
|
+
// Full mode: Turn, Model, Context, Cache Rd, Cache Wr, Output, Cost
|
|
1083
|
+
const colNum = 4;
|
|
1084
|
+
const colM = 7;
|
|
1085
|
+
const colCtx = 7;
|
|
1086
|
+
const colCost = 8;
|
|
1087
|
+
const nDividers = 6;
|
|
1088
|
+
const fixedW = colNum + colM + colCtx + colCost;
|
|
1089
|
+
const flexTotal = w - fixedW - nDividers;
|
|
1090
|
+
const rdW = Math.max(10, Math.floor(flexTotal * 0.35));
|
|
1091
|
+
const wrW = Math.max(11, Math.floor(flexTotal * 0.30));
|
|
1092
|
+
const outW = Math.max(6, flexTotal - rdW - wrW);
|
|
1093
|
+
header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Contex', colCtx)}${div}${rpad('Cache Read', rdW)}${div}${rpad('Cache Write', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
|
|
1094
|
+
separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
|
|
1095
|
+
rows = turns.map(t => {
|
|
1096
|
+
const mTag = modelTag(t.routedModel);
|
|
1097
|
+
const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
|
|
1098
|
+
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1099
|
+
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1100
|
+
return (pad(String(t.turn), colNum) + div +
|
|
1101
|
+
`{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
|
|
1102
|
+
rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1103
|
+
`{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
|
|
1104
|
+
`{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
|
|
1105
|
+
`{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
|
|
1106
|
+
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
else if (w >= 30) {
|
|
1110
|
+
// Compact mode: drop cache split columns to preserve readable headers.
|
|
1111
|
+
const colNum = 4;
|
|
1112
|
+
const colM = 6;
|
|
1113
|
+
const colCtx = 6;
|
|
1114
|
+
const colCost = 6;
|
|
1115
|
+
const nDividers = 4;
|
|
1116
|
+
const outW = Math.max(4, w - (colNum + colM + colCtx + colCost + nDividers));
|
|
1117
|
+
header = `${pad('Turn', colNum)}${div}${rpad('Model', colM)}${div}${rpad('Context', colCtx)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}`;
|
|
1118
|
+
separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}`;
|
|
1119
|
+
rows = turns.map(t => {
|
|
1120
|
+
const mTag = modelTag(t.routedModel);
|
|
1121
|
+
const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
|
|
1122
|
+
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1123
|
+
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1124
|
+
return (pad(String(t.turn), colNum) + div +
|
|
1125
|
+
`{${mColor}-fg}${cpad(mTag, colM)}{/${mColor}-fg}` + div +
|
|
1126
|
+
rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1127
|
+
`{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
|
|
1128
|
+
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
// Minimal mode: guaranteed no-wrap fallback.
|
|
1133
|
+
const colNum = 4;
|
|
1134
|
+
const colCtx = 6;
|
|
1135
|
+
const colCost = 6;
|
|
1136
|
+
header = `${pad('Turn', colNum)}${div}${rpad('Context', colCtx)}${div}${rpad('Cost', colCost)}`;
|
|
1137
|
+
separator = `${'─'.repeat(colNum)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(colCost)}`;
|
|
1138
|
+
rows = turns.map(t => {
|
|
1139
|
+
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1140
|
+
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1141
|
+
return (pad(String(t.turn), colNum) + div +
|
|
1142
|
+
rpad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1143
|
+
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1144
|
+
});
|
|
1112
1145
|
}
|
|
1113
|
-
header = `{bold}${pad('Turn', colNum)}${div}${pad('Model', colM)}${div}${pad('Context', colCtx)}${div}${rpad('Cache Rd', rdW)}${div}${rpad('Cache Wr', wrW)}${div}${rpad('Output', outW)}${div}${rpad('Cost', colCost)}{/bold}`;
|
|
1114
|
-
separator = `{gray-fg}${'─'.repeat(colNum)}┼${'─'.repeat(colM)}┼${'─'.repeat(colCtx)}┼${'─'.repeat(rdW)}┼${'─'.repeat(wrW)}┼${'─'.repeat(outW)}┼${'─'.repeat(colCost)}{/gray-fg}`;
|
|
1115
|
-
rows = turns.map(t => {
|
|
1116
|
-
const mTag = modelTag(t.routedModel);
|
|
1117
|
-
const mColor = t.routedModel.includes('haiku') ? 'green' : t.routedModel.includes('sonnet') ? 'blue' : 'magenta';
|
|
1118
|
-
const costFlag = t.cost > 1.0 ? '{red-fg}' : t.cost > 0.5 ? '{yellow-fg}' : '{white-fg}';
|
|
1119
|
-
const costEnd = t.cost > 1.0 ? '{/red-fg}' : t.cost > 0.5 ? '{/yellow-fg}' : '{/white-fg}';
|
|
1120
|
-
return (pad(String(t.turn), colNum) + div +
|
|
1121
|
-
`{${mColor}-fg}${pad(mTag, colM)}{/${mColor}-fg}` + div +
|
|
1122
|
-
pad(`${t.contextPct.toFixed(0)}%`, colCtx) + div +
|
|
1123
|
-
`{green-fg}${rpad(fmtK(t.cacheRead), rdW)}{/green-fg}` + div +
|
|
1124
|
-
`{yellow-fg}${rpad(fmtK(t.cacheCreate), wrW)}{/yellow-fg}` + div +
|
|
1125
|
-
`{cyan-fg}${rpad(fmtK(t.output), outW)}{/cyan-fg}` + div +
|
|
1126
|
-
costFlag + rpad(`$${t.cost.toFixed(2)}`, colCost) + costEnd);
|
|
1127
|
-
});
|
|
1128
1146
|
const lines = [header, separator, ...rows];
|
|
1129
1147
|
turnBox.setContent(lines.join('\n'));
|
|
1130
1148
|
// Restore scroll position AFTER content update
|
|
@@ -1386,13 +1404,11 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1386
1404
|
// Delay first ccusage call — let blessed render first, then load heavy data
|
|
1387
1405
|
setTimeout(() => updateWindowBox(), 2000);
|
|
1388
1406
|
const pollInterval = setInterval(updateDashboard, refreshMs);
|
|
1389
|
-
// Animation interval — slower on Windows to reduce blessed redraw flicker
|
|
1390
|
-
const animMs = process.platform === 'win32' ? 1000 : 500;
|
|
1391
1407
|
const headerAnimInterval = setInterval(() => {
|
|
1392
1408
|
// Keep advancing across the full session label; wrap at a large value.
|
|
1393
1409
|
waveOffset = (waveOffset + 1) % 1000000;
|
|
1394
1410
|
renderHeader();
|
|
1395
|
-
},
|
|
1411
|
+
}, 500);
|
|
1396
1412
|
const fortuneInterval = setInterval(() => {
|
|
1397
1413
|
if (activeFortunes.length === 0)
|
|
1398
1414
|
return;
|
package/dist/commands/run.js
CHANGED
|
@@ -439,7 +439,7 @@ let proxyModeEnabled = true;
|
|
|
439
439
|
/**
|
|
440
440
|
* Generate a unique session UUID and convert to human-readable name
|
|
441
441
|
* Each CLI invocation gets a NEW session (not tied to project path)
|
|
442
|
-
* Uses uuidToWords from state.ts for consistency
|
|
442
|
+
* Uses uuidToWords from state.ts for consistency across ekkOS components
|
|
443
443
|
*/
|
|
444
444
|
function generateCliSessionName() {
|
|
445
445
|
const sessionUuid = crypto.randomUUID();
|
|
@@ -469,12 +469,12 @@ function getEkkosEnv() {
|
|
|
469
469
|
env.EKKOS_PROXY_MODE = '1';
|
|
470
470
|
// Enable ultra-minimal mode by default (30%→20% eviction for constant-cost infinite context)
|
|
471
471
|
env.EKKOS_ULTRA_MINIMAL = '1';
|
|
472
|
-
// Use
|
|
473
|
-
//
|
|
474
|
-
// The hook calls POST /proxy/session/bind with Claude's actual session name
|
|
472
|
+
// Use a unique pending scope until Claude exposes its real session UUID.
|
|
473
|
+
// run.ts binds this scope to the real session immediately on transcript detect.
|
|
475
474
|
if (!cliSessionName) {
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
const pendingToken = crypto.randomBytes(8).toString('hex');
|
|
476
|
+
cliSessionName = `_pending-${pendingToken}`;
|
|
477
|
+
cliSessionId = `pending-${pendingToken}`;
|
|
478
478
|
console.log(chalk_1.default.gray(` 📂 Session: pending (will bind to Claude session)`));
|
|
479
479
|
}
|
|
480
480
|
// Get full userId from config (NOT the truncated version from auth token)
|
|
@@ -912,11 +912,10 @@ function launchWithWindowsTerminal(options) {
|
|
|
912
912
|
if (options.noProxy)
|
|
913
913
|
runArgs.push('--skip-proxy');
|
|
914
914
|
// Write a temp batch file to avoid all quoting issues
|
|
915
|
-
// wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
|
|
916
915
|
const batPath = path.join(os.tmpdir(), `ekkos-wt-${launchTime}.cmd`);
|
|
917
916
|
const batContent = [
|
|
918
917
|
'@echo off',
|
|
919
|
-
`wt --title "Claude Code" -d "${cwd}"
|
|
918
|
+
`wt --title "Claude Code" -d "${cwd}" ekkos ${runArgs.join(' ')} ; split-pane -V -s 0.4 --title "ekkOS Dashboard" -d "${cwd}" ekkos dashboard --wait-for-new --refresh 2000`,
|
|
920
919
|
].join('\r\n');
|
|
921
920
|
try {
|
|
922
921
|
fs.writeFileSync(batPath, batContent);
|
|
@@ -1084,10 +1083,7 @@ async function run(options) {
|
|
|
1084
1083
|
}
|
|
1085
1084
|
}
|
|
1086
1085
|
// Check PTY availability early (deterministic, no async race)
|
|
1087
|
-
|
|
1088
|
-
// (hide/show/move), causing ghost ▊ block cursor artifacts. Use stdio:'inherit' spawn
|
|
1089
|
-
// instead so Ink gets direct terminal access. Hooks still handle session tracking.
|
|
1090
|
-
const loadedPty = isWindows ? null : await loadPty();
|
|
1086
|
+
const loadedPty = await loadPty();
|
|
1091
1087
|
const usePty = loadedPty !== null;
|
|
1092
1088
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1093
1089
|
// CONCURRENT STARTUP: Spawn Claude while animation runs
|
|
@@ -1323,10 +1319,8 @@ async function run(options) {
|
|
|
1323
1319
|
console.log('');
|
|
1324
1320
|
}
|
|
1325
1321
|
else {
|
|
1326
|
-
// Static banner for Windows / remote / no-splash — no cursor manipulation
|
|
1327
|
-
console.log('');
|
|
1328
|
-
console.log(chalk_1.default.hex('#FF6B35').bold(' ekkOS_Pulse') + chalk_1.default.gray(' — Context is finite. Intelligence isn\'t.'));
|
|
1329
1322
|
console.log('');
|
|
1323
|
+
console.log(chalk_1.default.cyan(' ekkOS remote session ready'));
|
|
1330
1324
|
if (bypass) {
|
|
1331
1325
|
console.log(chalk_1.default.yellow(' ⚡ Bypass permissions mode enabled'));
|
|
1332
1326
|
}
|
|
@@ -1496,13 +1490,17 @@ async function run(options) {
|
|
|
1496
1490
|
const sessionId = file.replace('.jsonl', '');
|
|
1497
1491
|
transcriptPath = fullPath;
|
|
1498
1492
|
currentSessionId = sessionId;
|
|
1493
|
+
currentSession = (0, state_1.uuidToWords)(sessionId);
|
|
1494
|
+
(0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
|
|
1495
|
+
(0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
|
|
1496
|
+
bindRealSessionToProxy(currentSession, 'fast-transcript', currentSessionId);
|
|
1499
1497
|
dlog(`[TRANSCRIPT] FAST DETECT: New transcript found! ${fullPath}`);
|
|
1500
1498
|
evictionDebugLog('TRANSCRIPT_SET', 'Fast poll detected new file', {
|
|
1501
1499
|
transcriptPath,
|
|
1502
1500
|
currentSessionId,
|
|
1503
1501
|
elapsedMs: Date.now() - launchTime
|
|
1504
1502
|
});
|
|
1505
|
-
startStreamTailer(transcriptPath, currentSessionId);
|
|
1503
|
+
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
1506
1504
|
// Stop polling
|
|
1507
1505
|
if (transcriptPollInterval) {
|
|
1508
1506
|
clearInterval(transcriptPollInterval);
|
|
@@ -1516,10 +1514,6 @@ async function run(options) {
|
|
|
1516
1514
|
// Project dir doesn't exist yet, keep polling
|
|
1517
1515
|
}
|
|
1518
1516
|
}
|
|
1519
|
-
// Start polling immediately
|
|
1520
|
-
transcriptPollInterval = setInterval(pollForNewTranscript, 500);
|
|
1521
|
-
pollForNewTranscript(); // Also run once immediately
|
|
1522
|
-
dlog('[TRANSCRIPT] Fast polling started - looking for new jsonl files');
|
|
1523
1517
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1524
1518
|
// SESSION NAME TRACKING (from live TUI output)
|
|
1525
1519
|
// Claude prints: "· Turn N · groovy-koala-saves · 📅"
|
|
@@ -1561,6 +1555,10 @@ async function run(options) {
|
|
|
1561
1555
|
let turnEndTimeout = null;
|
|
1562
1556
|
const TURN_END_STABLE_MS = 500; // Must see idle prompt for 500ms
|
|
1563
1557
|
let pendingClearAfterEviction = false; // Flag to trigger /clear after eviction
|
|
1558
|
+
// Start polling after session-tracking state is initialized.
|
|
1559
|
+
transcriptPollInterval = setInterval(pollForNewTranscript, 500);
|
|
1560
|
+
pollForNewTranscript(); // Also run once immediately
|
|
1561
|
+
dlog('[TRANSCRIPT] Fast polling started - looking for new jsonl files');
|
|
1564
1562
|
// Debug log to eviction-debug.log for 400 error diagnosis
|
|
1565
1563
|
function evictionDebugLog(category, msg, data) {
|
|
1566
1564
|
try {
|
|
@@ -1615,22 +1613,30 @@ async function run(options) {
|
|
|
1615
1613
|
dlog(`[TRANSCRIPT] Resolved by session ID (${source}): ${candidate}`);
|
|
1616
1614
|
startStreamTailer(transcriptPath, currentSessionId, currentSession || undefined);
|
|
1617
1615
|
}
|
|
1618
|
-
function bindRealSessionToProxy(sessionName, source) {
|
|
1616
|
+
function bindRealSessionToProxy(sessionName, source, sessionIdHint) {
|
|
1619
1617
|
if (!proxyModeEnabled)
|
|
1620
1618
|
return;
|
|
1621
1619
|
if (!sessionName || sessionName === '_pending')
|
|
1622
1620
|
return;
|
|
1623
1621
|
if (boundProxySession === sessionName || bindingSessionInFlight === sessionName)
|
|
1624
1622
|
return;
|
|
1623
|
+
const pendingSession = cliSessionName?.startsWith('_pending') ? cliSessionName : undefined;
|
|
1624
|
+
const bindSessionId = sessionIdHint || currentSessionId || undefined;
|
|
1625
1625
|
bindingSessionInFlight = sessionName;
|
|
1626
1626
|
void (async () => {
|
|
1627
1627
|
const maxAttempts = 3;
|
|
1628
1628
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
1629
|
-
const success = await (0, session_binding_1.bindSession)(
|
|
1629
|
+
const success = await (0, session_binding_1.bindSession)({
|
|
1630
|
+
realSession: sessionName,
|
|
1631
|
+
projectPath: process.cwd(),
|
|
1632
|
+
pendingSession,
|
|
1633
|
+
sessionId: bindSessionId,
|
|
1634
|
+
});
|
|
1630
1635
|
if (success) {
|
|
1631
1636
|
boundProxySession = sessionName;
|
|
1632
1637
|
bindingSessionInFlight = null;
|
|
1633
1638
|
cliSessionName = sessionName;
|
|
1639
|
+
cliSessionId = bindSessionId || cliSessionId;
|
|
1634
1640
|
dlog(`[SESSION_BIND] Bound ${sessionName} from ${source} (attempt ${attempt}/${maxAttempts})`);
|
|
1635
1641
|
return;
|
|
1636
1642
|
}
|
|
@@ -2329,7 +2335,7 @@ async function run(options) {
|
|
|
2329
2335
|
(0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
|
|
2330
2336
|
dlog(`Session detected from UUID: ${currentSession}`);
|
|
2331
2337
|
resolveTranscriptFromSessionId('session-id-from-output');
|
|
2332
|
-
bindRealSessionToProxy(currentSession, 'session-id-from-output');
|
|
2338
|
+
bindRealSessionToProxy(currentSession, 'session-id-from-output', currentSessionId || undefined);
|
|
2333
2339
|
}
|
|
2334
2340
|
// ════════════════════════════════════════════════════════════════════════
|
|
2335
2341
|
// SESSION NAME DETECTION (PRIMARY METHOD)
|
|
@@ -2363,7 +2369,7 @@ async function run(options) {
|
|
|
2363
2369
|
// Also update global state for backwards compatibility
|
|
2364
2370
|
(0, state_1.updateState)({ sessionName: currentSession });
|
|
2365
2371
|
dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
|
|
2366
|
-
bindRealSessionToProxy(currentSession, 'status-line');
|
|
2372
|
+
bindRealSessionToProxy(currentSession, 'status-line', currentSessionId || undefined);
|
|
2367
2373
|
resolveTranscriptFromSessionId('status-line');
|
|
2368
2374
|
}
|
|
2369
2375
|
}
|
|
@@ -2371,7 +2377,7 @@ async function run(options) {
|
|
|
2371
2377
|
// Same session, just update timestamp
|
|
2372
2378
|
lastSeenSessionAt = Date.now();
|
|
2373
2379
|
if (boundProxySession !== detectedSession) {
|
|
2374
|
-
bindRealSessionToProxy(detectedSession, 'status-line-refresh');
|
|
2380
|
+
bindRealSessionToProxy(detectedSession, 'status-line-refresh', currentSessionId || undefined);
|
|
2375
2381
|
}
|
|
2376
2382
|
}
|
|
2377
2383
|
}
|
package/dist/cron/promoter.js
CHANGED
|
File without changes
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,9 @@ const agent_1 = require("./commands/agent");
|
|
|
51
51
|
const state_1 = require("./utils/state");
|
|
52
52
|
const index_1 = require("./commands/usage/index");
|
|
53
53
|
const dashboard_1 = require("./commands/dashboard");
|
|
54
|
+
const swarm_1 = require("./commands/swarm");
|
|
55
|
+
const swarm_dashboard_1 = require("./commands/swarm-dashboard");
|
|
56
|
+
const swarm_setup_1 = require("./commands/swarm-setup");
|
|
54
57
|
const chalk_1 = __importDefault(require("chalk"));
|
|
55
58
|
const fs = __importStar(require("fs"));
|
|
56
59
|
const path = __importStar(require("path"));
|
|
@@ -76,7 +79,7 @@ commander_1.program
|
|
|
76
79
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
|
|
77
80
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
|
|
78
81
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
|
|
79
|
-
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos
|
|
82
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos swarm launch -t "build X"')} ${chalk_1.default.gray('Launch parallel workers on a task')}`,
|
|
80
83
|
'',
|
|
81
84
|
chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
|
|
82
85
|
'',
|
|
@@ -154,7 +157,7 @@ commander_1.program
|
|
|
154
157
|
title: 'Running',
|
|
155
158
|
icon: '▸',
|
|
156
159
|
commands: [
|
|
157
|
-
{ name: 'run', desc: '
|
|
160
|
+
{ name: 'run', desc: 'Launch Claude Code with ekkOS memory + auto-continue', note: 'default' },
|
|
158
161
|
{ name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
|
|
159
162
|
{ name: 'sessions', desc: 'List active Claude Code sessions' },
|
|
160
163
|
],
|
|
@@ -177,6 +180,13 @@ commander_1.program
|
|
|
177
180
|
{ name: 'agent', desc: 'Manage the remote terminal agent (start, stop, status, logs)' },
|
|
178
181
|
],
|
|
179
182
|
},
|
|
183
|
+
{
|
|
184
|
+
title: 'Swarm (Multi-Agent)',
|
|
185
|
+
icon: '▸',
|
|
186
|
+
commands: [
|
|
187
|
+
{ name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
|
|
188
|
+
],
|
|
189
|
+
},
|
|
180
190
|
];
|
|
181
191
|
const padCmd = 18;
|
|
182
192
|
let output = '';
|
|
@@ -229,7 +239,7 @@ commander_1.program
|
|
|
229
239
|
// Run command - launches Claude with auto-continue wrapper
|
|
230
240
|
commander_1.program
|
|
231
241
|
.command('run')
|
|
232
|
-
.description('
|
|
242
|
+
.description('Launch Claude Code with auto-continue (auto /clear + /continue when context is high)')
|
|
233
243
|
.option('-s, --session <name>', 'Session name to restore on clear')
|
|
234
244
|
.option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
|
|
235
245
|
.option('-v, --verbose', 'Show debug output')
|
|
@@ -342,10 +352,10 @@ hooksCmd
|
|
|
342
352
|
(0, index_1.registerUsageCommand)(commander_1.program);
|
|
343
353
|
// Dashboard command - live TUI for monitoring session usage
|
|
344
354
|
commander_1.program.addCommand(dashboard_1.dashboardCommand);
|
|
345
|
-
// Sessions command - list active Claude Code sessions
|
|
355
|
+
// Sessions command - list active Claude Code sessions (swarm support)
|
|
346
356
|
commander_1.program
|
|
347
357
|
.command('sessions')
|
|
348
|
-
.description('List active Claude Code sessions')
|
|
358
|
+
.description('List active Claude Code sessions (for swarm/multi-session support)')
|
|
349
359
|
.option('-j, --json', 'Output machine-readable JSON')
|
|
350
360
|
.action((options) => {
|
|
351
361
|
const sessions = (0, state_1.getActiveSessions)();
|
|
@@ -470,6 +480,59 @@ agentCmd
|
|
|
470
480
|
.action((options) => {
|
|
471
481
|
(0, agent_1.agentHealth)({ json: options.json });
|
|
472
482
|
});
|
|
483
|
+
// Swarm command - manage Q-learning routing
|
|
484
|
+
const swarmCmd = commander_1.program
|
|
485
|
+
.command('swarm')
|
|
486
|
+
.description('Manage Swarm Q-learning model routing');
|
|
487
|
+
swarmCmd
|
|
488
|
+
.command('status')
|
|
489
|
+
.description('Show Q-table stats (states, visits, epsilon, top actions)')
|
|
490
|
+
.action(swarm_1.swarmStatus);
|
|
491
|
+
swarmCmd
|
|
492
|
+
.command('reset')
|
|
493
|
+
.description('Clear Q-table from Redis (routing reverts to static rules)')
|
|
494
|
+
.action(swarm_1.swarmReset);
|
|
495
|
+
swarmCmd
|
|
496
|
+
.command('export')
|
|
497
|
+
.description('Export Q-table to .swarm/q-learning-model.json')
|
|
498
|
+
.action(swarm_1.swarmExport);
|
|
499
|
+
swarmCmd
|
|
500
|
+
.command('import')
|
|
501
|
+
.description('Import Q-table from .swarm/q-learning-model.json into Redis')
|
|
502
|
+
.action(swarm_1.swarmImport);
|
|
503
|
+
swarmCmd
|
|
504
|
+
.command('launch')
|
|
505
|
+
.description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
|
|
506
|
+
.option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
|
|
507
|
+
.option('-t, --task <task>', 'Task description to decompose and execute')
|
|
508
|
+
.option('--no-bypass', 'Disable bypass permissions mode')
|
|
509
|
+
.option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
|
|
510
|
+
.option('--no-queen', 'Skip launching the Python Queen coordinator')
|
|
511
|
+
.option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
|
|
512
|
+
.option('-v, --verbose', 'Show debug output')
|
|
513
|
+
.action((options) => {
|
|
514
|
+
// Auto-open wizard when --task is missing
|
|
515
|
+
if (!options.task) {
|
|
516
|
+
(0, swarm_setup_1.swarmSetup)();
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
(0, swarm_1.swarmLaunch)({
|
|
520
|
+
workers: options.workers || 4,
|
|
521
|
+
task: options.task,
|
|
522
|
+
bypass: options.bypass !== false,
|
|
523
|
+
noDecompose: options.decompose === false,
|
|
524
|
+
noQueen: options.queen === false,
|
|
525
|
+
queenStrategy: options.queenStrategy,
|
|
526
|
+
verbose: options.verbose,
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
swarmCmd
|
|
530
|
+
.command('setup')
|
|
531
|
+
.description('Interactive TUI wizard for configuring and launching a swarm')
|
|
532
|
+
.action(() => {
|
|
533
|
+
(0, swarm_setup_1.swarmSetup)();
|
|
534
|
+
});
|
|
535
|
+
swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
|
|
473
536
|
// Handle `-help` (single dash) — rewrite to `--help` for Commander compatibility
|
|
474
537
|
const helpIdx = process.argv.indexOf('-help');
|
|
475
538
|
if (helpIdx !== -1) {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
interface BindSessionPayload {
|
|
2
|
+
realSession: string;
|
|
3
|
+
projectPath: string;
|
|
4
|
+
pendingSession?: string;
|
|
5
|
+
sessionId?: string;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* Bind the real session name to the proxy
|
|
3
|
-
* This replaces the
|
|
9
|
+
* This replaces the pending placeholder for this user, enabling proper eviction.
|
|
4
10
|
*/
|
|
5
|
-
export declare function bindSession(realSession
|
|
11
|
+
export declare function bindSession({ realSession, projectPath, pendingSession, sessionId, }: BindSessionPayload): Promise<boolean>;
|
|
12
|
+
export {};
|
|
@@ -3,42 +3,67 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.bindSession = bindSession;
|
|
4
4
|
const state_1 = require("./state");
|
|
5
5
|
const MEMORY_API_URL = process.env.EKKOS_PROXY_URL || 'https://proxy.ekkos.dev';
|
|
6
|
+
function extractUserIdFromToken(token) {
|
|
7
|
+
const normalized = token.trim();
|
|
8
|
+
if (!normalized)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (normalized.startsWith('ekk_')) {
|
|
11
|
+
const parts = normalized.split('_');
|
|
12
|
+
if (parts.length >= 2 && parts[1]) {
|
|
13
|
+
return parts[1];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const colonParts = normalized.split(':');
|
|
17
|
+
if (colonParts.length >= 2 && colonParts[0]) {
|
|
18
|
+
return colonParts[0];
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
6
22
|
/**
|
|
7
23
|
* Bind the real session name to the proxy
|
|
8
|
-
* This replaces the
|
|
24
|
+
* This replaces the pending placeholder for this user, enabling proper eviction.
|
|
9
25
|
*/
|
|
10
|
-
async function bindSession(realSession, projectPath) {
|
|
26
|
+
async function bindSession({ realSession, projectPath, pendingSession, sessionId, }) {
|
|
11
27
|
try {
|
|
12
28
|
// Get userId same way as run.ts
|
|
13
29
|
const config = (0, state_1.getConfig)();
|
|
30
|
+
const authToken = (0, state_1.getAuthToken)();
|
|
14
31
|
let userId = config?.userId;
|
|
15
32
|
if (!userId || userId === 'anonymous') {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const parts = token.split('_');
|
|
19
|
-
if (parts.length >= 2) {
|
|
20
|
-
userId = parts[1];
|
|
21
|
-
}
|
|
33
|
+
if (authToken) {
|
|
34
|
+
userId = extractUserIdFromToken(authToken);
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
|
-
if (!userId || userId === 'anonymous')
|
|
37
|
+
if (!userId || userId === 'anonymous' || !authToken)
|
|
25
38
|
return false;
|
|
39
|
+
const bindAuthToken = authToken.startsWith('ekk_')
|
|
40
|
+
? authToken
|
|
41
|
+
: `${userId}:${authToken}`;
|
|
42
|
+
const body = {
|
|
43
|
+
userId,
|
|
44
|
+
realSession,
|
|
45
|
+
projectPath,
|
|
46
|
+
};
|
|
47
|
+
if (pendingSession) {
|
|
48
|
+
body.pendingSession = pendingSession;
|
|
49
|
+
}
|
|
50
|
+
if (sessionId) {
|
|
51
|
+
body.sessionId = sessionId;
|
|
52
|
+
}
|
|
26
53
|
// Use global fetch (Node 18+)
|
|
27
54
|
const response = await fetch(`${MEMORY_API_URL}/proxy/session/bind`, {
|
|
28
55
|
method: 'POST',
|
|
29
56
|
signal: AbortSignal.timeout(2500),
|
|
30
57
|
headers: {
|
|
31
|
-
'Content-Type': 'application/json'
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'x-ekkos-auth-token': bindAuthToken,
|
|
60
|
+
Authorization: `Bearer ${bindAuthToken}`,
|
|
32
61
|
},
|
|
33
|
-
body: JSON.stringify(
|
|
34
|
-
userId,
|
|
35
|
-
realSession,
|
|
36
|
-
projectPath
|
|
37
|
-
})
|
|
62
|
+
body: JSON.stringify(body),
|
|
38
63
|
});
|
|
39
64
|
return response.ok;
|
|
40
65
|
}
|
|
41
|
-
catch
|
|
66
|
+
catch {
|
|
42
67
|
// Fail silently - don't crash CLI
|
|
43
68
|
return false;
|
|
44
69
|
}
|
package/dist/utils/state.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare function getMostRecentSession(): {
|
|
|
80
80
|
*/
|
|
81
81
|
export declare function getActiveSessions(): ActiveSession[];
|
|
82
82
|
/**
|
|
83
|
-
* Register a new active session
|
|
83
|
+
* Register a new active session (for swarm tracking)
|
|
84
84
|
*/
|
|
85
85
|
export declare function registerActiveSession(sessionId: string, sessionName: string, projectPath: string): ActiveSession;
|
|
86
86
|
/**
|
package/dist/utils/state.js
CHANGED
|
@@ -244,7 +244,7 @@ function getMostRecentSession() {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
247
|
-
// MULTI-SESSION SUPPORT
|
|
247
|
+
// MULTI-SESSION/SWARM SUPPORT
|
|
248
248
|
// Track multiple concurrent Claude Code sessions without state collision
|
|
249
249
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
250
250
|
/**
|
|
@@ -271,7 +271,7 @@ function getActiveSessions() {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
/**
|
|
274
|
-
* Register a new active session
|
|
274
|
+
* Register a new active session (for swarm tracking)
|
|
275
275
|
*/
|
|
276
276
|
function registerActiveSession(sessionId, sessionName, projectPath) {
|
|
277
277
|
ensureEkkosDir();
|
package/package.json
CHANGED
package/templates/CLAUDE.md
CHANGED
|
@@ -364,20 +364,21 @@ Call `ekkOS_Directive` when user says:
|
|
|
364
364
|
**EVERY response MUST end with this footer:**
|
|
365
365
|
```
|
|
366
366
|
---
|
|
367
|
-
{IDE} ({Model}) · 🧠 **ekkOS_™** · {SessionName} · 📅 {Timestamp}
|
|
367
|
+
{IDE} ({Model}) · 🧠 **ekkOS_™** · Turn {N} · {SessionName} · 📅 {Timestamp}
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
**How to detect values:**
|
|
371
371
|
- **IDE**: Claude Code, Cursor, Windsurf, etc. (from environment)
|
|
372
372
|
- **Model**: Sonnet 4.5, Opus 4.5, etc. (from your model name)
|
|
373
|
+
- **Turn Number**: From hook header (e.g., "Turn 47") - starts at 0 for each new session
|
|
373
374
|
- **Session Name**: From hook header (e.g., "sol-gem-dig") - human-readable session identifier
|
|
374
|
-
- **Timestamp**: From hook header (accurate local time in EST
|
|
375
|
+
- **Timestamp**: From hook header (accurate local time in EST)
|
|
375
376
|
|
|
376
377
|
**Examples:**
|
|
377
|
-
- `Claude Code (
|
|
378
|
-
- `Cursor (Claude Sonnet 4) · 🧠 **ekkOS_™** · bright-falcon-soars · 📅 2026-01-09 10:15 AM EST`
|
|
378
|
+
- `Claude Code (Sonnet 4.5) · 🧠 **ekkOS_™** · Turn 12 · cosmic-penguin-runs · 📅 2026-01-09 4:50 PM EST`
|
|
379
|
+
- `Cursor (Claude Sonnet 4) · 🧠 **ekkOS_™** · Turn 5 · bright-falcon-soars · 📅 2026-01-09 10:15 AM EST`
|
|
379
380
|
|
|
380
|
-
**The hook header shows:** `🧠 ekkOS Memory | {SessionName} | {timestamp}`
|
|
381
|
+
**The hook header shows:** `🧠 ekkOS Memory | Turn {N} | {Context%} | {SessionName} | {timestamp}`
|
|
381
382
|
|
|
382
383
|
---
|
|
383
384
|
|
package/templates/hooks/stop.ps1
CHANGED
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
14
|
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# PROXY MODE DETECTION
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
$ProxyMode = $false
|
|
19
|
+
if ($env:ANTHROPIC_BASE_URL -and $env:ANTHROPIC_BASE_URL -like "*proxy.ekkos.dev*") {
|
|
20
|
+
$ProxyMode = $true
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
24
|
# CONFIG PATHS
|
|
17
25
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -87,6 +95,14 @@ function Convert-UuidToWords {
|
|
|
87
95
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
88
96
|
$inputJson = [Console]::In.ReadToEnd()
|
|
89
97
|
|
|
98
|
+
if ($ProxyMode) {
|
|
99
|
+
$projectState = Join-Path (Join-Path ((Get-Location).Path) ".claude\state") "hook-state.json"
|
|
100
|
+
$globalState = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
|
|
101
|
+
if (Test-Path $projectState) { Remove-Item $projectState -Force }
|
|
102
|
+
if (Test-Path $globalState) { Remove-Item $globalState -Force }
|
|
103
|
+
exit 0
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
# Get session ID from state
|
|
91
107
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
|
|
92
108
|
$sessionFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
@@ -110,39 +126,37 @@ if ($rawSessionId -eq "unknown" -and (Test-Path $sessionFile)) {
|
|
|
110
126
|
|
|
111
127
|
$sessionName = Convert-UuidToWords $rawSessionId
|
|
112
128
|
|
|
129
|
+
function Get-PendingSessionFromProxyBaseUrl {
|
|
130
|
+
param([string]$baseUrl)
|
|
131
|
+
if (-not $baseUrl) { return $null }
|
|
132
|
+
try {
|
|
133
|
+
$uri = [System.Uri]$baseUrl
|
|
134
|
+
$segments = $uri.AbsolutePath.Trim('/') -split '/'
|
|
135
|
+
if (-not $segments -or $segments.Count -lt 3) { return $null }
|
|
136
|
+
$proxyIndex = [Array]::IndexOf($segments, 'proxy')
|
|
137
|
+
if ($proxyIndex -lt 0 -or $segments.Count -le ($proxyIndex + 2)) { return $null }
|
|
138
|
+
$candidate = [System.Uri]::UnescapeDataString($segments[$proxyIndex + 2])
|
|
139
|
+
if ($candidate -eq '_pending' -or $candidate -eq 'pending' -or $candidate.StartsWith('_pending-')) {
|
|
140
|
+
return $candidate
|
|
141
|
+
}
|
|
142
|
+
} catch {}
|
|
143
|
+
return $null
|
|
144
|
+
}
|
|
145
|
+
# Session binding removed — proxy now self-resolves _pending → word-based
|
|
146
|
+
# names inline via uuidToSessionName(). No external bind call needed.
|
|
147
|
+
|
|
113
148
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
-
#
|
|
115
|
-
# Windows has no PTY so run.ts can't detect the session name. The stop hook
|
|
116
|
-
# is the first place we have a confirmed session name, so we bind here.
|
|
117
|
-
# Mac does this in stop.sh (lines 171-179). Logic is identical.
|
|
149
|
+
# AUTH LOADING (direct mode only)
|
|
118
150
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
151
|
+
$authToken = $null
|
|
152
|
+
$userId = $null
|
|
119
153
|
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
120
|
-
if (
|
|
154
|
+
if (Test-Path $configFile) {
|
|
121
155
|
try {
|
|
122
|
-
$
|
|
123
|
-
$
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if ($userId -and $authToken) {
|
|
127
|
-
$projectPath = (Get-Location).Path
|
|
128
|
-
$pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
|
|
129
|
-
|
|
130
|
-
$projectPath = $projectPath -replace '\\', '/'
|
|
131
|
-
$bindBody = @{
|
|
132
|
-
userId = $userId
|
|
133
|
-
realSession = $sessionName
|
|
134
|
-
projectPath = $projectPath
|
|
135
|
-
pendingSession = $pendingSession
|
|
136
|
-
} | ConvertTo-Json -Depth 10
|
|
137
|
-
|
|
138
|
-
Start-Job -ScriptBlock {
|
|
139
|
-
param($body, $token)
|
|
140
|
-
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
|
|
141
|
-
-Method POST `
|
|
142
|
-
-Headers @{ "Content-Type" = "application/json" } `
|
|
143
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
|
|
144
|
-
} -ArgumentList $bindBody, $authToken | Out-Null
|
|
145
|
-
}
|
|
156
|
+
$cfg = Get-Content $configFile -Raw | ConvertFrom-Json
|
|
157
|
+
$authToken = $cfg.hookApiKey
|
|
158
|
+
if (-not $authToken) { $authToken = $cfg.apiKey }
|
|
159
|
+
$userId = $cfg.userId
|
|
146
160
|
} catch {}
|
|
147
161
|
}
|
|
148
162
|
|
package/templates/hooks/stop.sh
CHANGED
|
@@ -18,6 +18,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
18
18
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
19
19
|
STATE_DIR="$PROJECT_ROOT/.claude/state"
|
|
20
20
|
|
|
21
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
# PROXY MODE DETECTION
|
|
23
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
PROXY_MODE=false
|
|
25
|
+
if [[ "$ANTHROPIC_BASE_URL" == *"proxy.ekkos.dev"* ]]; then
|
|
26
|
+
PROXY_MODE=true
|
|
27
|
+
fi
|
|
28
|
+
|
|
21
29
|
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
22
30
|
|
|
23
31
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -53,6 +61,14 @@ TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
|
|
|
53
61
|
MODEL_USED=$(parse_json_value "$INPUT" '.model')
|
|
54
62
|
[ -z "$MODEL_USED" ] && MODEL_USED="claude-sonnet-4-5"
|
|
55
63
|
|
|
64
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
# PROXY MODE: Slim hook path (local cleanup only)
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
if [ "$PROXY_MODE" = "true" ]; then
|
|
68
|
+
rm -f "$STATE_DIR/hook-state.json" "$HOME/.claude/state/hook-state.json" 2>/dev/null || true
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
56
72
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
57
73
|
# Session ID
|
|
58
74
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -163,20 +179,8 @@ fi
|
|
|
163
179
|
|
|
164
180
|
MEMORY_API_URL="https://mcp.ekkos.dev"
|
|
165
181
|
|
|
166
|
-
#
|
|
167
|
-
#
|
|
168
|
-
# The CLI may be in spawn pass-through mode (no PTY = blind to TUI output),
|
|
169
|
-
# so the stop hook (which IS sighted) must bind the session.
|
|
170
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
171
|
-
if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session-starts" ] && [ -n "$USER_ID" ]; then
|
|
172
|
-
PROJECT_PATH_FOR_BIND=$(pwd)
|
|
173
|
-
PENDING_SESSION_FOR_BIND="${EKKOS_PENDING_SESSION:-_pending}"
|
|
174
|
-
curl -s -X POST "$MEMORY_API_URL/proxy/session/bind" \
|
|
175
|
-
-H "Content-Type: application/json" \
|
|
176
|
-
-d "{\"userId\":\"$USER_ID\",\"realSession\":\"$SESSION_NAME\",\"projectPath\":\"$PROJECT_PATH_FOR_BIND\",\"pendingSession\":\"$PENDING_SESSION_FOR_BIND\"}" \
|
|
177
|
-
--connect-timeout 1 \
|
|
178
|
-
--max-time 2 >/dev/null 2>&1 &
|
|
179
|
-
fi
|
|
182
|
+
# Session binding removed — proxy now self-resolves _pending → word-based
|
|
183
|
+
# names inline via uuidToSessionName(). No external bind call needed.
|
|
180
184
|
|
|
181
185
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
182
186
|
# EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
|
|
@@ -12,6 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
14
|
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# PROXY MODE DETECTION
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
$ProxyMode = $false
|
|
19
|
+
if ($env:ANTHROPIC_BASE_URL -and $env:ANTHROPIC_BASE_URL -like "*proxy.ekkos.dev*") {
|
|
20
|
+
$ProxyMode = $true
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
24
|
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
17
25
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -77,6 +85,100 @@ if ($rawSessionId -eq "unknown") {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
89
|
+
# PROXY MODE: Slim hook path (cosmetic output + local state only)
|
|
90
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
91
|
+
if ($ProxyMode) {
|
|
92
|
+
function Get-ProxySessionName {
|
|
93
|
+
param([string]$sessionId)
|
|
94
|
+
if (-not $sessionId -or $sessionId -eq "unknown") { return "unknown-session" }
|
|
95
|
+
if ($sessionId -notmatch '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') {
|
|
96
|
+
return $sessionId
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (-not $script:SessionWords) { Load-SessionWords }
|
|
100
|
+
if (-not $script:SessionWords) { return $sessionId }
|
|
101
|
+
|
|
102
|
+
$adjectives = $script:SessionWords.adjectives
|
|
103
|
+
$nouns = $script:SessionWords.nouns
|
|
104
|
+
$verbs = $script:SessionWords.verbs
|
|
105
|
+
if (-not $adjectives -or -not $nouns -or -not $verbs) { return $sessionId }
|
|
106
|
+
|
|
107
|
+
$clean = $sessionId -replace "-", ""
|
|
108
|
+
if ($clean.Length -lt 12) { return $sessionId }
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
$adj = [Convert]::ToInt32($clean.Substring(0, 4), 16) % $adjectives.Length
|
|
112
|
+
$noun = [Convert]::ToInt32($clean.Substring(4, 4), 16) % $nouns.Length
|
|
113
|
+
$verb = [Convert]::ToInt32($clean.Substring(8, 4), 16) % $verbs.Length
|
|
114
|
+
return "$($adjectives[$adj])-$($nouns[$noun])-$($verbs[$verb])"
|
|
115
|
+
} catch {
|
|
116
|
+
return $sessionId
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$sessionName = Get-ProxySessionName $rawSessionId
|
|
121
|
+
$timestampUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
122
|
+
$displayTime = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt zzz")
|
|
123
|
+
$projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
124
|
+
|
|
125
|
+
New-Item -ItemType Directory -Path $EkkosConfigDir -Force | Out-Null
|
|
126
|
+
New-Item -ItemType Directory -Path (Join-Path $env:USERPROFILE ".claude\state") -Force | Out-Null
|
|
127
|
+
|
|
128
|
+
$globalState = @{
|
|
129
|
+
session_id = $rawSessionId
|
|
130
|
+
session_name = $sessionName
|
|
131
|
+
project = $projectPath
|
|
132
|
+
timestamp = $timestampUtc
|
|
133
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
134
|
+
Set-Content -Path (Join-Path $EkkosConfigDir "current-session.json") -Value $globalState -Force
|
|
135
|
+
|
|
136
|
+
$localState = @{
|
|
137
|
+
session_id = $rawSessionId
|
|
138
|
+
session_name = $sessionName
|
|
139
|
+
timestamp = $timestampUtc
|
|
140
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
141
|
+
Set-Content -Path (Join-Path $env:USERPROFILE ".claude\state\current-session.json") -Value $localState -Force
|
|
142
|
+
|
|
143
|
+
$activeSessionsPath = Join-Path $EkkosConfigDir "active-sessions.json"
|
|
144
|
+
$sessions = @()
|
|
145
|
+
if (Test-Path $activeSessionsPath) {
|
|
146
|
+
try { $sessions = @(Get-Content $activeSessionsPath -Raw | ConvertFrom-Json) } catch { $sessions = @() }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
$entry = @{
|
|
150
|
+
sessionId = $rawSessionId
|
|
151
|
+
sessionName = $sessionName
|
|
152
|
+
projectPath = $projectPath
|
|
153
|
+
startedAt = $timestampUtc
|
|
154
|
+
lastHeartbeat = $timestampUtc
|
|
155
|
+
pid = 0
|
|
156
|
+
}
|
|
157
|
+
$idx = -1
|
|
158
|
+
for ($i = 0; $i -lt $sessions.Count; $i++) {
|
|
159
|
+
if ($sessions[$i].sessionId -eq $rawSessionId) { $idx = $i; break }
|
|
160
|
+
}
|
|
161
|
+
if ($idx -ge 0) {
|
|
162
|
+
$entry.startedAt = if ($sessions[$idx].startedAt) { $sessions[$idx].startedAt } else { $timestampUtc }
|
|
163
|
+
$sessions[$idx] = $entry
|
|
164
|
+
} else {
|
|
165
|
+
$sessions += $entry
|
|
166
|
+
}
|
|
167
|
+
Set-Content -Path $activeSessionsPath -Value ($sessions | ConvertTo-Json -Depth 10) -Force
|
|
168
|
+
|
|
169
|
+
$hint = @{
|
|
170
|
+
sessionName = $sessionName
|
|
171
|
+
sessionId = $rawSessionId
|
|
172
|
+
projectPath = $projectPath
|
|
173
|
+
ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
174
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
175
|
+
Set-Content -Path (Join-Path $EkkosConfigDir "hook-session-hint.json") -Value $hint -Force
|
|
176
|
+
|
|
177
|
+
$esc = [char]27
|
|
178
|
+
Write-Output "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $displayTime${esc}[0m"
|
|
179
|
+
exit 0
|
|
180
|
+
}
|
|
181
|
+
|
|
80
182
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
81
183
|
# INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
|
|
82
184
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -139,39 +241,24 @@ function Convert-UuidToWords {
|
|
|
139
241
|
|
|
140
242
|
$sessionName = Convert-UuidToWords $rawSessionId
|
|
141
243
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
$bindBody = @{
|
|
158
|
-
userId = $userId
|
|
159
|
-
realSession = $sessionName
|
|
160
|
-
projectPath = $projectPath
|
|
161
|
-
pendingSession = $pendingSession
|
|
162
|
-
} | ConvertTo-Json -Depth 10 -Compress
|
|
163
|
-
|
|
164
|
-
Start-Job -ScriptBlock {
|
|
165
|
-
param($body)
|
|
166
|
-
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
|
|
167
|
-
-Method POST `
|
|
168
|
-
-Headers @{ "Content-Type" = "application/json" } `
|
|
169
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
|
|
170
|
-
} -ArgumentList $bindBody | Out-Null
|
|
171
|
-
}
|
|
172
|
-
} catch {}
|
|
173
|
-
}
|
|
244
|
+
function Get-PendingSessionFromProxyBaseUrl {
|
|
245
|
+
param([string]$baseUrl)
|
|
246
|
+
if (-not $baseUrl) { return $null }
|
|
247
|
+
try {
|
|
248
|
+
$uri = [System.Uri]$baseUrl
|
|
249
|
+
$segments = $uri.AbsolutePath.Trim('/') -split '/'
|
|
250
|
+
if (-not $segments -or $segments.Count -lt 3) { return $null }
|
|
251
|
+
$proxyIndex = [Array]::IndexOf($segments, 'proxy')
|
|
252
|
+
if ($proxyIndex -lt 0 -or $segments.Count -le ($proxyIndex + 2)) { return $null }
|
|
253
|
+
$candidate = [System.Uri]::UnescapeDataString($segments[$proxyIndex + 2])
|
|
254
|
+
if ($candidate -eq '_pending' -or $candidate -eq 'pending' -or $candidate.StartsWith('_pending-')) {
|
|
255
|
+
return $candidate
|
|
256
|
+
}
|
|
257
|
+
} catch {}
|
|
258
|
+
return $null
|
|
174
259
|
}
|
|
260
|
+
# Session binding removed — proxy now self-resolves _pending → word-based
|
|
261
|
+
# names inline via uuidToSessionName(). No external bind call needed.
|
|
175
262
|
|
|
176
263
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
177
264
|
# SESSION CURRENT: Update Redis with current session name
|
|
@@ -325,7 +412,8 @@ if ($skillReminders.Count -gt 0) {
|
|
|
325
412
|
|
|
326
413
|
$output += @"
|
|
327
414
|
|
|
328
|
-
<footer-format>End responses with: Claude Code ({Model}) · 🧠 ekkOS_™ · $sessionName ·
|
|
415
|
+
<footer-format>End responses with: Claude Code ({Model}) · 🧠 ekkOS_™ · $sessionName · $timestamp</footer-format>
|
|
416
|
+
<footer-note>Do not include a turn counter in the footer.</footer-note>
|
|
329
417
|
"@
|
|
330
418
|
|
|
331
419
|
Write-Output $output
|
|
@@ -13,6 +13,14 @@ set +e
|
|
|
13
13
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
14
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
15
15
|
|
|
16
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
+
# PROXY MODE DETECTION
|
|
18
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
PROXY_MODE=false
|
|
20
|
+
if [[ "$ANTHROPIC_BASE_URL" == *"proxy.ekkos.dev"* ]]; then
|
|
21
|
+
PROXY_MODE=true
|
|
22
|
+
fi
|
|
23
|
+
|
|
16
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
17
25
|
# CONFIG PATHS - No jq dependency (v1.2 spec)
|
|
18
26
|
# Session words live in ~/.ekkos/ so they work in ANY project
|
|
@@ -64,6 +72,92 @@ if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "
|
|
|
64
72
|
fi
|
|
65
73
|
fi
|
|
66
74
|
|
|
75
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
+
# PROXY MODE: Slim hook path (cosmetic output + local state only)
|
|
77
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
+
if [ "$PROXY_MODE" = "true" ]; then
|
|
79
|
+
SESSION_ID="$RAW_SESSION_ID"
|
|
80
|
+
UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
81
|
+
SESSION_NAME="$SESSION_ID"
|
|
82
|
+
|
|
83
|
+
if [[ "$SESSION_ID" =~ $UUID_REGEX ]] && [ -f "$JSON_PARSE_HELPER" ]; then
|
|
84
|
+
SESSION_NAME=$(node -e "
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
const sid = process.argv[1] || '';
|
|
87
|
+
const wordsFile = process.argv[2];
|
|
88
|
+
const fallbackFile = process.argv[3];
|
|
89
|
+
const helper = process.argv[4];
|
|
90
|
+
let wordsPath = wordsFile;
|
|
91
|
+
if (!fs.existsSync(wordsPath)) wordsPath = fallbackFile;
|
|
92
|
+
if (!fs.existsSync(wordsPath) || !fs.existsSync(helper)) {
|
|
93
|
+
console.log(sid || 'unknown-session');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
const cp = require('child_process');
|
|
97
|
+
function readList(path) {
|
|
98
|
+
const out = cp.spawnSync('node', [helper, wordsPath, path], { encoding: 'utf8' });
|
|
99
|
+
if (out.status !== 0) return [];
|
|
100
|
+
return (out.stdout || '').split('\\n').map(s => s.trim()).filter(Boolean);
|
|
101
|
+
}
|
|
102
|
+
const adjectives = readList('.adjectives');
|
|
103
|
+
const nouns = readList('.nouns');
|
|
104
|
+
const verbs = readList('.verbs');
|
|
105
|
+
if (!adjectives.length || !nouns.length || !verbs.length) {
|
|
106
|
+
console.log(sid);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
const hex = sid.replace(/-/g, '').slice(0, 12);
|
|
110
|
+
if (!/^[0-9a-fA-F]{12}$/.test(hex)) {
|
|
111
|
+
console.log(sid);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
const adj = parseInt(hex.slice(0, 4), 16) % adjectives.length;
|
|
115
|
+
const noun = parseInt(hex.slice(4, 8), 16) % nouns.length;
|
|
116
|
+
const verb = parseInt(hex.slice(8, 12), 16) % verbs.length;
|
|
117
|
+
console.log(adjectives[adj] + '-' + nouns[noun] + '-' + verbs[verb]);
|
|
118
|
+
" "$SESSION_ID" "$SESSION_WORDS_JSON" "$SESSION_WORDS_DEFAULT" "$JSON_PARSE_HELPER" 2>/dev/null || echo "$SESSION_ID")
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
TIMESTAMP_UTC=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
122
|
+
DISPLAY_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
|
|
123
|
+
mkdir -p "$HOME/.ekkos" "$HOME/.claude/state" 2>/dev/null || true
|
|
124
|
+
echo "{\"session_id\":\"$SESSION_ID\",\"session_name\":\"$SESSION_NAME\",\"project\":\"$PROJECT_ROOT\",\"timestamp\":\"$TIMESTAMP_UTC\"}" > "$HOME/.ekkos/current-session.json" 2>/dev/null || true
|
|
125
|
+
echo "{\"session_id\":\"$SESSION_ID\",\"session_name\":\"$SESSION_NAME\",\"timestamp\":\"$TIMESTAMP_UTC\"}" > "$HOME/.claude/state/current-session.json" 2>/dev/null || true
|
|
126
|
+
|
|
127
|
+
ACTIVE_SESSIONS_FILE="$HOME/.ekkos/active-sessions.json"
|
|
128
|
+
node -e "
|
|
129
|
+
const fs = require('fs');
|
|
130
|
+
const file = process.argv[1];
|
|
131
|
+
const sid = process.argv[2];
|
|
132
|
+
const sname = process.argv[3];
|
|
133
|
+
const ts = process.argv[4];
|
|
134
|
+
const project = process.argv[5];
|
|
135
|
+
let sessions = [];
|
|
136
|
+
try {
|
|
137
|
+
if (fs.existsSync(file)) {
|
|
138
|
+
sessions = JSON.parse(fs.readFileSync(file, 'utf8') || '[]');
|
|
139
|
+
}
|
|
140
|
+
} catch {}
|
|
141
|
+
const index = sessions.findIndex(s => s.sessionId === sid);
|
|
142
|
+
if (index >= 0) {
|
|
143
|
+
sessions[index] = { ...sessions[index], sessionName: sname, projectPath: project, lastHeartbeat: ts };
|
|
144
|
+
} else {
|
|
145
|
+
sessions.push({ sessionId: sid, sessionName: sname, projectPath: project, startedAt: ts, lastHeartbeat: ts, pid: 0 });
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(file, JSON.stringify(sessions, null, 2));
|
|
148
|
+
" "$ACTIVE_SESSIONS_FILE" "$SESSION_ID" "$SESSION_NAME" "$TIMESTAMP_UTC" "$PROJECT_ROOT" 2>/dev/null || true
|
|
149
|
+
|
|
150
|
+
# Dashboard/local tooling hint file
|
|
151
|
+
echo "{\"sessionName\":\"$SESSION_NAME\",\"sessionId\":\"$SESSION_ID\",\"projectPath\":\"$PROJECT_ROOT\",\"ts\":$(date +%s)000}" > "$HOME/.ekkos/hook-session-hint.json" 2>/dev/null || true
|
|
152
|
+
|
|
153
|
+
CYAN='\033[0;36m'
|
|
154
|
+
DIM='\033[2m'
|
|
155
|
+
BOLD='\033[1m'
|
|
156
|
+
RESET='\033[0m'
|
|
157
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${DISPLAY_TIME}${RESET}"
|
|
158
|
+
exit 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
67
161
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
68
162
|
# SKILL AUTO-FIRE: Detect keywords and inject skill reminders
|
|
69
163
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -138,7 +232,6 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
|
|
|
138
232
|
PROJECT_SESSION_DIR="$STATE_DIR/sessions"
|
|
139
233
|
mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
|
|
140
234
|
TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
|
|
141
|
-
CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
|
|
142
235
|
|
|
143
236
|
# Count API round-trips from transcript to match TUI turn counter
|
|
144
237
|
TURN_NUMBER=1
|
|
@@ -159,28 +252,7 @@ fi
|
|
|
159
252
|
|
|
160
253
|
echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
|
|
161
254
|
|
|
162
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
-
# Context size tracking - Uses tokenizer script (single source)
|
|
164
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
165
|
-
PREV_CONTEXT_PERCENT=0
|
|
166
|
-
[ -f "$CONTEXT_SIZE_FILE" ] && PREV_CONTEXT_PERCENT=$(cat "$CONTEXT_SIZE_FILE" 2>/dev/null || echo "0")
|
|
167
|
-
|
|
168
|
-
TOKEN_PERCENT=0
|
|
169
|
-
MAX_TOKENS=200000
|
|
170
|
-
TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
|
|
171
|
-
|
|
172
|
-
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
|
|
173
|
-
TOKEN_COUNT=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
|
|
174
|
-
if [[ "$TOKEN_COUNT" =~ ^[0-9]+$ ]] && [ "$TOKEN_COUNT" -gt 0 ]; then
|
|
175
|
-
TOKEN_PERCENT=$((TOKEN_COUNT * 100 / MAX_TOKENS))
|
|
176
|
-
[ "$TOKEN_PERCENT" -gt 100 ] && TOKEN_PERCENT=100
|
|
177
|
-
# In proxy mode, IPC compresses ~65-70% — show estimated post-compression %
|
|
178
|
-
IPC_PERCENT=$((TOKEN_PERCENT * 30 / 100))
|
|
179
|
-
[ "$IPC_PERCENT" -lt 1 ] && IPC_PERCENT=1
|
|
180
|
-
fi
|
|
181
|
-
fi
|
|
182
255
|
|
|
183
|
-
echo "$TOKEN_PERCENT" > "$CONTEXT_SIZE_FILE"
|
|
184
256
|
|
|
185
257
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
186
258
|
# COLORS
|
|
@@ -311,32 +383,8 @@ try {
|
|
|
311
383
|
--max-time 3 >/dev/null 2>&1 &
|
|
312
384
|
fi
|
|
313
385
|
|
|
314
|
-
#
|
|
315
|
-
|
|
316
|
-
if [ -f "$HOME/.ekkos/config.json" ] && [ -f "$JSON_PARSE_HELPER" ]; then
|
|
317
|
-
EKKOS_USER_ID=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.userId' 2>/dev/null || echo "")
|
|
318
|
-
fi
|
|
319
|
-
if [ -n "$EKKOS_USER_ID" ] && [ -n "$SESSION_NAME" ]; then
|
|
320
|
-
PENDING_SESSION="${EKKOS_PENDING_SESSION:-_pending}"
|
|
321
|
-
node -e "
|
|
322
|
-
const https = require('https');
|
|
323
|
-
const body = JSON.stringify({
|
|
324
|
-
userId: process.argv[1],
|
|
325
|
-
realSession: process.argv[2],
|
|
326
|
-
projectPath: process.argv[3],
|
|
327
|
-
pendingSession: process.argv[4]
|
|
328
|
-
});
|
|
329
|
-
const req = https.request({
|
|
330
|
-
hostname: 'mcp.ekkos.dev',
|
|
331
|
-
path: '/proxy/session/bind',
|
|
332
|
-
method: 'POST',
|
|
333
|
-
headers: {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body)}
|
|
334
|
-
}, () => {});
|
|
335
|
-
req.on('error', () => {});
|
|
336
|
-
req.write(body);
|
|
337
|
-
req.end();
|
|
338
|
-
" "$EKKOS_USER_ID" "$SESSION_NAME" "$PROJECT_ROOT" "$PENDING_SESSION" 2>/dev/null &
|
|
339
|
-
fi
|
|
386
|
+
# Session binding removed — proxy now self-resolves _pending → word-based
|
|
387
|
+
# names inline via uuidToSessionName(). No external bind call needed.
|
|
340
388
|
fi
|
|
341
389
|
|
|
342
390
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -410,49 +458,7 @@ turns.slice(0, -1).forEach(t => {
|
|
|
410
458
|
fi
|
|
411
459
|
fi
|
|
412
460
|
|
|
413
|
-
|
|
414
|
-
# COMPACTION DETECTION: If context dropped dramatically, auto-restore
|
|
415
|
-
# Was >50% last turn, now <15% = compaction happened
|
|
416
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
417
|
-
if [ "$PREV_CONTEXT_PERCENT" -gt 50 ] && [ "$TOKEN_PERCENT" -lt 15 ] && [ -n "$AUTH_TOKEN" ]; then
|
|
418
|
-
echo ""
|
|
419
|
-
echo -e "${GREEN}${BOLD}🔄 CONTEXT RESTORED${RESET} ${DIM}| Compaction detected | Auto-loading recent turns...${RESET}"
|
|
420
|
-
|
|
421
|
-
RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
|
|
422
|
-
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
423
|
-
-H "Content-Type: application/json" \
|
|
424
|
-
-d "{\"session_id\": \"${SESSION_ID}\", \"last_n\": 10, \"format\": \"summary\"}" \
|
|
425
|
-
--connect-timeout 3 \
|
|
426
|
-
--max-time 5 2>/dev/null || echo '{"turns":[]}')
|
|
427
|
-
|
|
428
|
-
RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
|
|
429
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
430
|
-
console.log((d.turns || []).length);
|
|
431
|
-
" 2>/dev/null || echo "0")
|
|
432
|
-
|
|
433
|
-
if [ "$RESTORED_COUNT" -gt 0 ]; then
|
|
434
|
-
echo -e "${GREEN} ✓${RESET} Restored ${RESTORED_COUNT} turns from Layer 2"
|
|
435
|
-
echo ""
|
|
436
|
-
echo -e "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
|
|
437
|
-
echo ""
|
|
438
|
-
|
|
439
|
-
echo "$RESTORE_RESPONSE" | node -e "
|
|
440
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
441
|
-
(d.turns || []).forEach(t => {
|
|
442
|
-
const q = (t.user_query || '...').substring(0, 120);
|
|
443
|
-
const a = (t.assistant_response || '...').substring(0, 250);
|
|
444
|
-
console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
|
|
445
|
-
});
|
|
446
|
-
" 2>/dev/null || true
|
|
447
|
-
|
|
448
|
-
echo ""
|
|
449
|
-
echo -e "${DIM}Full history: \"turns 1-${TURN_NUMBER}\" or \"recall yesterday\"${RESET}"
|
|
450
|
-
fi
|
|
451
|
-
|
|
452
|
-
echo ""
|
|
453
|
-
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
454
|
-
|
|
455
|
-
elif [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
|
|
461
|
+
if [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
|
|
456
462
|
# /clear detected - show visible restoration banner
|
|
457
463
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
|
|
458
464
|
echo -e "${GREEN}${BOLD}🔄 SESSION CONTINUED${RESET} ${DIM}| ${TURN_NUMBER} turns preserved | Context restored${RESET}" >&2
|
|
@@ -497,9 +503,6 @@ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
|
497
503
|
echo ""
|
|
498
504
|
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
499
505
|
|
|
500
|
-
elif [ "$TOKEN_PERCENT" -ge 50 ]; then
|
|
501
|
-
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ~${IPC_PERCENT:-0}% IPC | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
502
|
-
|
|
503
506
|
else
|
|
504
507
|
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
505
508
|
fi
|