@ekkos/cli 1.1.6 → 1.2.1
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 +32 -52
- package/dist/commands/init.js +42 -35
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +68 -160
- package/dist/index.js +5 -70
- package/dist/utils/state.d.ts +1 -1
- package/dist/utils/state.js +2 -2
- package/package.json +1 -1
|
@@ -627,62 +627,40 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
627
627
|
// footer: 3 rows (totals + routing + keybindings)
|
|
628
628
|
const LOGO_CHARS = ['e', 'k', 'k', 'O', 'S', '_'];
|
|
629
629
|
const WAVE_COLORS = ['cyan', 'blue', 'magenta', 'yellow', 'green', 'white'];
|
|
630
|
-
|
|
631
|
-
|
|
630
|
+
// Fallback fortunes when cloud endpoint is unreachable
|
|
631
|
+
const FALLBACK_FORTUNES = [
|
|
632
632
|
'The AI was confident. It was also wrong.',
|
|
633
633
|
'Vibe coded it. Ship it. Pray.',
|
|
634
634
|
'Your model hallucinated. Your memory did not.',
|
|
635
|
-
'Claude said "I cannot assist with that." ekkOS remembered anyway.',
|
|
636
|
-
'The context window closed. The lesson did not.',
|
|
637
|
-
'Cursor wrote it. You own it. Good luck.',
|
|
638
635
|
'It works. Nobody knows why. Memory saved the why.',
|
|
639
|
-
'LLM said "as of my knowledge cutoff." ekkOS said hold my cache.',
|
|
640
|
-
'Your agent forgot. Classic agent behavior.',
|
|
641
|
-
'GPT-5 dropped. Your memory still works.',
|
|
642
|
-
'Trained on the internet. Trusted by no one.',
|
|
643
|
-
'Fine-tuned on vibes. Running in production.',
|
|
644
|
-
'Prompt engineering is just yelling more politely.',
|
|
645
|
-
'The AI is confident 97% of the time. The other 3% is your bug.',
|
|
646
|
-
// Friday deploys / prod pain
|
|
647
|
-
'Pushed to prod on a Friday. Memory captured the regret.',
|
|
648
|
-
'It was working this morning. The morning remembers.',
|
|
649
|
-
'The bug was in prod for 3 months. The fix took 4 minutes.',
|
|
650
|
-
'Hotfix on a hotfix. Classic.',
|
|
651
636
|
'Rollback complete. Dignity: partial.',
|
|
652
|
-
'"It works on my machine." Ship the machine.',
|
|
653
|
-
'The incident was resolved. The root cause was vibes.',
|
|
654
|
-
'Post-mortem written. Lessons immediately forgotten. Not anymore.',
|
|
655
|
-
// Context / memory specific
|
|
656
|
-
'Cold start problem? Never met her.',
|
|
657
|
-
'94% cache hit rate. The other 6% are trust issues.',
|
|
658
|
-
'107 turns. Zero compaction. One very tired server.',
|
|
659
|
-
'Flat cost curve. Exponential confidence.',
|
|
660
|
-
'Your session ended. Your mistakes did not.',
|
|
661
|
-
'The context limit hit. The memory did not care.',
|
|
662
|
-
'Compaction is a skill issue.',
|
|
663
|
-
// General dev pain
|
|
664
|
-
'The ticket said "small change." It was not small.',
|
|
665
637
|
'Story points are astrology for engineers.',
|
|
666
|
-
'
|
|
667
|
-
'
|
|
668
|
-
'Senior dev. 8 years experience. Still googles how to center a div.',
|
|
669
|
-
'Code review: where confidence goes to die.',
|
|
670
|
-
'The PR sat for 11 days. You merged it anyway.',
|
|
671
|
-
'Works fine until it\'s demoed. Classic.',
|
|
672
|
-
'Two spaces or four? Choose your enemies carefully.',
|
|
638
|
+
'DNS. It\'s always DNS.',
|
|
639
|
+
'The tests passed. The tests were wrong.',
|
|
673
640
|
'Tech debt is just regular debt with better excuses.',
|
|
674
|
-
'The documentation was last updated in 2019. Press F.',
|
|
675
|
-
'Legacy code: someone\'s proudest moment, your worst nightmare.',
|
|
676
|
-
'Tabs vs spaces is still unresolved. The war continues.',
|
|
677
|
-
'LGTM. (I did not look at this.)',
|
|
678
|
-
'The standup is 15 minutes. It is never 15 minutes.',
|
|
679
|
-
'Agile: deadline anxiety, but make it a ceremony.',
|
|
680
|
-
'"No breaking changes." — Famous last words.',
|
|
681
|
-
'Your regex is beautiful. Your regex is unmaintainable.',
|
|
682
|
-
'undefined is just the universe saying try again.',
|
|
683
641
|
'It\'s not a bug. It\'s a negotiated feature.',
|
|
684
|
-
'Closed one ticket. Jira opened three.',
|
|
685
642
|
];
|
|
643
|
+
// Active fortune set — starts with fallback, replaced by cloud data
|
|
644
|
+
let activeFortunes = [...FALLBACK_FORTUNES];
|
|
645
|
+
// Fetch time-aware fortunes from the proxy
|
|
646
|
+
async function fetchCloudFortunes() {
|
|
647
|
+
try {
|
|
648
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
649
|
+
const resp = await fetch(`https://proxy.ekkos.dev/api/v1/fortunes?tz=${encodeURIComponent(tz)}`, { signal: AbortSignal.timeout(5000) });
|
|
650
|
+
if (!resp.ok)
|
|
651
|
+
return;
|
|
652
|
+
const data = await resp.json();
|
|
653
|
+
if (data.fortunes && data.fortunes.length > 0) {
|
|
654
|
+
activeFortunes = data.fortunes;
|
|
655
|
+
// Reset fortune index to start cycling the new set
|
|
656
|
+
fortuneIdx = Math.floor(Math.random() * activeFortunes.length);
|
|
657
|
+
fortuneText = activeFortunes[fortuneIdx];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
// Cloud unavailable — keep using fallback
|
|
662
|
+
}
|
|
663
|
+
}
|
|
686
664
|
const W = '100%';
|
|
687
665
|
const HEADER_H = 3;
|
|
688
666
|
const CONTEXT_H = 5;
|
|
@@ -709,8 +687,8 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
709
687
|
let lastData = null;
|
|
710
688
|
let lastChartSeries = null;
|
|
711
689
|
let lastScrollPerc = 0; // Preserve scroll position across updates
|
|
712
|
-
let fortuneIdx = Math.floor(Math.random() *
|
|
713
|
-
let fortuneText =
|
|
690
|
+
let fortuneIdx = Math.floor(Math.random() * activeFortunes.length);
|
|
691
|
+
let fortuneText = activeFortunes[fortuneIdx];
|
|
714
692
|
// Header: session stats (3 lines)
|
|
715
693
|
const headerBox = blessed.box({
|
|
716
694
|
top: layout.header.top, left: 0, width: W, height: layout.header.height,
|
|
@@ -1421,6 +1399,8 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1421
1399
|
// Dashboard is fully passive — no widget captures keyboard input
|
|
1422
1400
|
updateDashboard();
|
|
1423
1401
|
screen.render();
|
|
1402
|
+
// Fetch cloud fortunes in background (non-blocking, updates activeFortunes on success)
|
|
1403
|
+
fetchCloudFortunes().then(() => renderHeader()).catch(() => { });
|
|
1424
1404
|
// Delay first ccusage call — let blessed render first, then load heavy data
|
|
1425
1405
|
setTimeout(() => updateWindowBox(), 2000);
|
|
1426
1406
|
const pollInterval = setInterval(updateDashboard, refreshMs);
|
|
@@ -1430,10 +1410,10 @@ async function launchDashboard(initialSessionName, jsonlPath, refreshMs) {
|
|
|
1430
1410
|
renderHeader();
|
|
1431
1411
|
}, 500);
|
|
1432
1412
|
const fortuneInterval = setInterval(() => {
|
|
1433
|
-
if (
|
|
1413
|
+
if (activeFortunes.length === 0)
|
|
1434
1414
|
return;
|
|
1435
|
-
fortuneIdx = (fortuneIdx + 1) %
|
|
1436
|
-
fortuneText =
|
|
1415
|
+
fortuneIdx = (fortuneIdx + 1) % activeFortunes.length;
|
|
1416
|
+
fortuneText = activeFortunes[fortuneIdx];
|
|
1437
1417
|
renderHeader();
|
|
1438
1418
|
}, 30000);
|
|
1439
1419
|
const windowPollInterval = setInterval(updateWindowBox, 15000); // every 15s
|
package/dist/commands/init.js
CHANGED
|
@@ -212,28 +212,22 @@ async function selectIDEs() {
|
|
|
212
212
|
}
|
|
213
213
|
console.log('');
|
|
214
214
|
}
|
|
215
|
-
const
|
|
216
|
-
{ name: 'Claude Code', value: 'claude',
|
|
217
|
-
{ name: 'Cursor', value: 'cursor',
|
|
218
|
-
{ name: 'Windsurf (Cascade)', value: 'windsurf',
|
|
215
|
+
const ideChoices = [
|
|
216
|
+
{ name: 'Claude Code', value: 'claude', checked: detected.includes('claude') || current === 'claude' },
|
|
217
|
+
{ name: 'Cursor', value: 'cursor', checked: detected.includes('cursor') || current === 'cursor' },
|
|
218
|
+
{ name: 'Windsurf (Cascade)', value: 'windsurf', checked: detected.includes('windsurf') || current === 'windsurf' }
|
|
219
219
|
];
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
if (selectedIDEs.length === 0) {
|
|
234
|
-
console.log(chalk_1.default.yellow('No IDEs selected. Defaulting to Claude Code.'));
|
|
235
|
-
selectedIDEs.push('claude');
|
|
236
|
-
}
|
|
220
|
+
// If nothing was auto-detected, default to Claude Code
|
|
221
|
+
if (!ideChoices.some(c => c.checked)) {
|
|
222
|
+
ideChoices[0].checked = true;
|
|
223
|
+
}
|
|
224
|
+
const { selectedIDEs } = await inquirer_1.default.prompt({
|
|
225
|
+
type: 'checkbox',
|
|
226
|
+
name: 'selectedIDEs',
|
|
227
|
+
message: 'Which IDEs should ekkOS configure?',
|
|
228
|
+
choices: ideChoices,
|
|
229
|
+
validate: (input) => input.length > 0 || 'Select at least one IDE (use space to toggle)'
|
|
230
|
+
});
|
|
237
231
|
console.log('');
|
|
238
232
|
return selectedIDEs;
|
|
239
233
|
}
|
|
@@ -466,23 +460,36 @@ async function init(options) {
|
|
|
466
460
|
};
|
|
467
461
|
(0, fs_1.writeFileSync)(platform_1.EKKOS_CONFIG, JSON.stringify(config, null, 2));
|
|
468
462
|
(0, fs_1.chmodSync)(platform_1.EKKOS_CONFIG, '600'); // Secure permissions
|
|
469
|
-
//
|
|
470
|
-
console.log('');
|
|
471
|
-
console.log(chalk_1.default.gray('═'.repeat(40)));
|
|
472
|
-
console.log(chalk_1.default.green.bold('✓ Setup complete!'));
|
|
473
|
-
console.log(chalk_1.default.gray('═'.repeat(40)));
|
|
463
|
+
// Verify connectivity
|
|
474
464
|
console.log('');
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
465
|
+
const verifySpinner = (0, ora_1.default)('Verifying connection to ekkOS...').start();
|
|
466
|
+
try {
|
|
467
|
+
const healthRes = await fetch(`${platform_1.MCP_API_URL}/health`, {
|
|
468
|
+
method: 'GET',
|
|
469
|
+
headers: { 'Authorization': `Bearer ${auth.apiKey}` }
|
|
470
|
+
});
|
|
471
|
+
if (healthRes.ok) {
|
|
472
|
+
verifySpinner.succeed('Connected to ekkOS API');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
verifySpinner.warn(`API returned ${healthRes.status} — check your network`);
|
|
476
|
+
}
|
|
480
477
|
}
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
catch {
|
|
479
|
+
verifySpinner.warn('Could not reach ekkOS API — check your network');
|
|
483
480
|
}
|
|
481
|
+
// Summary with prominent next step
|
|
482
|
+
const ideNames = installedIDEs.map(id => id === 'claude' ? 'Claude Code' : id === 'cursor' ? 'Cursor' : 'Windsurf');
|
|
483
|
+
console.log('');
|
|
484
|
+
console.log(chalk_1.default.green.bold('╔═══════════════════════════════════════╗'));
|
|
485
|
+
console.log(chalk_1.default.green.bold('║ ✓ Setup complete! ║'));
|
|
486
|
+
console.log(chalk_1.default.green.bold('╚═══════════════════════════════════════╝'));
|
|
487
|
+
console.log('');
|
|
488
|
+
console.log(chalk_1.default.yellow.bold(' NEXT STEPS:'));
|
|
489
|
+
console.log('');
|
|
490
|
+
console.log(chalk_1.default.white(` 1. Restart ${ideNames.join(' / ')}`));
|
|
491
|
+
console.log(chalk_1.default.white(' 2. Run ') + chalk_1.default.cyan.bold('ekkos') + chalk_1.default.white(' to start coding with memory'));
|
|
484
492
|
console.log('');
|
|
485
|
-
console.log(chalk_1.default.gray(`
|
|
486
|
-
console.log(chalk_1.default.gray(`View dashboard: ${chalk_1.default.white('https://platform.ekkos.dev/dashboard')}`));
|
|
493
|
+
console.log(chalk_1.default.gray(` Dashboard: https://platform.ekkos.dev/dashboard`));
|
|
487
494
|
console.log('');
|
|
488
495
|
}
|
package/dist/commands/run.d.ts
CHANGED
package/dist/commands/run.js
CHANGED
|
@@ -261,17 +261,6 @@ const ORPHAN_MARKER_REGEX = /\[ekkOS\]\s+ORPHAN_TOOL_RESULT\s+(\{.*?\})/gi;
|
|
|
261
261
|
// Cooldown to prevent thrashing if output repeats the marker
|
|
262
262
|
const ORPHAN_DETECTION_COOLDOWN_MS = 15000;
|
|
263
263
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
-
// SILENT FAILURE DETECTION - Catch API errors even when ccDNA markers missing
|
|
265
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
-
// Pattern 1: API returns 400 error (often due to orphan tool_results)
|
|
267
|
-
const API_400_REGEX = /(?:status[:\s]*400|"status":\s*400|HTTP\/\d\.\d\s+400|error.*400)/i;
|
|
268
|
-
// Pattern 2: Anthropic API specific error about tool_result without tool_use
|
|
269
|
-
const ORPHAN_API_ERROR_REGEX = /tool_result.*(?:no matching|without|missing).*tool_use|tool_use.*not found/i;
|
|
270
|
-
// Pattern 3: Generic "invalid" message structure error
|
|
271
|
-
const INVALID_MESSAGE_REGEX = /invalid.*message|message.*invalid|malformed.*request/i;
|
|
272
|
-
// Cooldown for silent failure detection (separate from orphan marker cooldown)
|
|
273
|
-
const SILENT_FAILURE_COOLDOWN_MS = 30000;
|
|
274
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
275
264
|
// SESSION NAME VALIDATION (MUST use words from session-words.json)
|
|
276
265
|
// This is the SOURCE OF TRUTH for valid session names
|
|
277
266
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -833,7 +822,6 @@ function launchWithDashboard(options) {
|
|
|
833
822
|
runArgs.push('--skip-dna');
|
|
834
823
|
if (options.noProxy)
|
|
835
824
|
runArgs.push('--skip-proxy');
|
|
836
|
-
runArgs.push('--kickstart'); // Auto-send "test" to create session immediately for dashboard
|
|
837
825
|
const ekkosCmd = process.argv[1]; // Path to ekkos CLI
|
|
838
826
|
const cwd = process.cwd();
|
|
839
827
|
const termCols = process.stdout.columns ?? 160;
|
|
@@ -888,7 +876,52 @@ function launchWithDashboard(options) {
|
|
|
888
876
|
console.log(chalk_1.default.gray('Falling back to normal mode. Run "ekkos dashboard --latest" in another terminal.'));
|
|
889
877
|
}
|
|
890
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Launch dashboard in a separate Windows Terminal pane.
|
|
881
|
+
* Unlike tmux, wt.exe opens a new split pane but doesn't manage the session —
|
|
882
|
+
* Claude runs in the original terminal, dashboard runs in the new pane.
|
|
883
|
+
*/
|
|
884
|
+
function launchWithWindowsTerminal(options) {
|
|
885
|
+
const ekkosCmd = process.argv[1];
|
|
886
|
+
const cwd = process.cwd();
|
|
887
|
+
const launchTime = Date.now();
|
|
888
|
+
// Write marker file so dashboard knows to wait for a NEW session
|
|
889
|
+
const markerPath = path.join(state_1.EKKOS_DIR, '.dashboard-launch-ts');
|
|
890
|
+
try {
|
|
891
|
+
fs.writeFileSync(markerPath, `${launchTime}\n${cwd}`);
|
|
892
|
+
}
|
|
893
|
+
catch { }
|
|
894
|
+
const dashCommand = `node "${ekkosCmd}" dashboard --wait-for-new --refresh 2000`;
|
|
895
|
+
try {
|
|
896
|
+
// wt.exe split-pane launches a new pane in the current Windows Terminal window
|
|
897
|
+
// -V = vertical split, -s 0.4 = 40% width for dashboard, -d = working directory
|
|
898
|
+
(0, child_process_1.execSync)(`wt.exe -w 0 split-pane -V -s 0.4 -d "${cwd}" cmd /c "${dashCommand}"`, { stdio: 'pipe' });
|
|
899
|
+
console.log(chalk_1.default.cyan('\n Dashboard launched in right pane (40%)'));
|
|
900
|
+
console.log(chalk_1.default.gray(' Switch panes: Alt+Arrow keys'));
|
|
901
|
+
}
|
|
902
|
+
catch (err) {
|
|
903
|
+
console.log(chalk_1.default.yellow(` Windows Terminal split failed: ${err.message}`));
|
|
904
|
+
console.log(chalk_1.default.gray(' Run "ekkos dashboard --latest" in a separate terminal'));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
891
907
|
async function run(options) {
|
|
908
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
909
|
+
// FIRST-RUN CHECK: If no config exists, run init first
|
|
910
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
911
|
+
const existingConfig = (0, state_1.getConfig)();
|
|
912
|
+
if (!existingConfig) {
|
|
913
|
+
console.log('');
|
|
914
|
+
console.log(chalk_1.default.yellow(' Welcome to ekkOS! Let\'s get you set up first.'));
|
|
915
|
+
console.log('');
|
|
916
|
+
const { init } = await Promise.resolve().then(() => __importStar(require('./init')));
|
|
917
|
+
await init({});
|
|
918
|
+
// Re-check after init — if user cancelled or it failed, exit gracefully
|
|
919
|
+
const postInitConfig = (0, state_1.getConfig)();
|
|
920
|
+
if (!postInitConfig) {
|
|
921
|
+
console.log(chalk_1.default.gray(' Setup incomplete. Run `ekkos init` to try again.'));
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
892
925
|
const verbose = options.verbose || false;
|
|
893
926
|
const bypass = options.bypass || false;
|
|
894
927
|
const noInject = options.noInject || false;
|
|
@@ -901,38 +934,26 @@ async function run(options) {
|
|
|
901
934
|
console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
|
|
902
935
|
}
|
|
903
936
|
// ══════════════════════════════════════════════════════════════════════════
|
|
904
|
-
//
|
|
905
|
-
// Hooks can cause PowerShell execution policy issues and hangs on Windows.
|
|
906
|
-
// The proxy handles context management; hooks aren't strictly required.
|
|
907
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
908
|
-
if (isWindows) {
|
|
909
|
-
try {
|
|
910
|
-
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
911
|
-
if (fs.existsSync(settingsPath)) {
|
|
912
|
-
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
913
|
-
if (settings.hooks) {
|
|
914
|
-
delete settings.hooks;
|
|
915
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
916
|
-
console.log(chalk_1.default.gray(' ⏭️ Hooks disabled (Windows proxy-only mode)'));
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
catch {
|
|
921
|
-
// Non-fatal — continue without hook cleanup
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
925
|
-
// DASHBOARD MODE: Launch via tmux with isolated dashboard pane (60/40)
|
|
937
|
+
// DASHBOARD MODE: Launch via tmux (Unix) or Windows Terminal split pane
|
|
926
938
|
// ══════════════════════════════════════════════════════════════════════════
|
|
927
939
|
if (options.dashboard) {
|
|
928
940
|
if (isWindows) {
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
941
|
+
// Windows: launch dashboard in a separate Windows Terminal pane
|
|
942
|
+
try {
|
|
943
|
+
(0, child_process_1.execSync)('where wt.exe', { encoding: 'utf-8', stdio: 'pipe' });
|
|
944
|
+
launchWithWindowsTerminal(options);
|
|
945
|
+
// Don't return — continue to launch Claude in this terminal
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
console.log(chalk_1.default.yellow(' Windows Terminal (wt.exe) not found.'));
|
|
949
|
+
console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
|
|
950
|
+
console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
|
|
951
|
+
}
|
|
932
952
|
}
|
|
933
953
|
else {
|
|
954
|
+
// Unix: launch via tmux split pane
|
|
934
955
|
try {
|
|
935
|
-
(0, child_process_1.execSync)('command -v tmux', { encoding: 'utf-8', stdio: 'pipe' })
|
|
956
|
+
(0, child_process_1.execSync)('command -v tmux', { encoding: 'utf-8', stdio: 'pipe' });
|
|
936
957
|
launchWithDashboard(options);
|
|
937
958
|
return;
|
|
938
959
|
}
|
|
@@ -1493,11 +1514,6 @@ async function run(options) {
|
|
|
1493
1514
|
let orphanScanCursor = 0;
|
|
1494
1515
|
const ORPHAN_SCAN_TAIL_SLACK = 256; // Keep some overlap for chunk boundary tolerance
|
|
1495
1516
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1496
|
-
// SILENT FAILURE DETECTION - Catch API errors even without ccDNA markers
|
|
1497
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1498
|
-
let lastSilentFailureTime = 0;
|
|
1499
|
-
let silentFailureCount = 0;
|
|
1500
|
-
const MAX_SILENT_FAILURES_BEFORE_ALERT = 2; // Alert user after 2 silent failures
|
|
1501
1517
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1502
1518
|
// TURN-END EVICTION - Only clean up when Claude is idle (safe state)
|
|
1503
1519
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -1992,6 +2008,11 @@ async function run(options) {
|
|
|
1992
2008
|
// The in-memory state is fine; we just fix the disk and log the bug.
|
|
1993
2009
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1994
2010
|
function handleOrphanToolResult(orphan) {
|
|
2011
|
+
// Proxy is sole eviction authority — never touch JSONL in proxy mode
|
|
2012
|
+
if (proxyModeEnabled) {
|
|
2013
|
+
dlog('Orphan recovery skipped - proxy mode (proxy is sole authority)');
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
1995
2016
|
const now = Date.now();
|
|
1996
2017
|
if (isOrphanRecoveryInProgress) {
|
|
1997
2018
|
dlog('Orphan recovery already in progress, ignoring');
|
|
@@ -2193,71 +2214,6 @@ async function run(options) {
|
|
|
2193
2214
|
}, TEST_POLL_MS);
|
|
2194
2215
|
}
|
|
2195
2216
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2196
|
-
// SILENT FAILURE DETECTION HANDLER
|
|
2197
|
-
// Catches API 400 errors and orphan-related messages even without ccDNA markers
|
|
2198
|
-
// This is a backup for when ccDNA validate mode isn't working or is disabled
|
|
2199
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2200
|
-
function handleSilentFailure(matchType, matchedText) {
|
|
2201
|
-
const now = Date.now();
|
|
2202
|
-
// Cooldown check
|
|
2203
|
-
if (now - lastSilentFailureTime < SILENT_FAILURE_COOLDOWN_MS) {
|
|
2204
|
-
dlog(`Silent failure suppressed by cooldown (${matchType})`);
|
|
2205
|
-
return;
|
|
2206
|
-
}
|
|
2207
|
-
// Don't trigger during active recovery
|
|
2208
|
-
if (isOrphanRecoveryInProgress || isAutoClearInProgress) {
|
|
2209
|
-
dlog(`Silent failure ignored - recovery in progress (${matchType})`);
|
|
2210
|
-
return;
|
|
2211
|
-
}
|
|
2212
|
-
lastSilentFailureTime = now;
|
|
2213
|
-
silentFailureCount++;
|
|
2214
|
-
evictionDebugLog('SILENT_FAILURE_DETECTED', '════════════════════════════════════════════════════════', {
|
|
2215
|
-
alert: '⚠️ SILENT FAILURE - API error detected without ccDNA marker',
|
|
2216
|
-
matchType,
|
|
2217
|
-
matchedText: matchedText.slice(0, 200),
|
|
2218
|
-
silentFailureCount,
|
|
2219
|
-
transcriptPath,
|
|
2220
|
-
diagnosis: 'Possible orphan tool_result or ccDNA not in validate mode',
|
|
2221
|
-
});
|
|
2222
|
-
console.log(`\n[ekkOS] ⚠️ Silent failure detected: ${matchType}`);
|
|
2223
|
-
// After multiple failures, alert user and suggest action
|
|
2224
|
-
if (silentFailureCount >= MAX_SILENT_FAILURES_BEFORE_ALERT) {
|
|
2225
|
-
console.log(`[ekkOS] ⚠️ Multiple API errors detected (${silentFailureCount}x)`);
|
|
2226
|
-
console.log(`[ekkOS] This may indicate orphan tool_results in the transcript`);
|
|
2227
|
-
console.log(`[ekkOS] Try: /clear then /continue to rebuild message state`);
|
|
2228
|
-
evictionDebugLog('SILENT_FAILURE_ALERT', 'Multiple silent failures - user alerted', {
|
|
2229
|
-
count: silentFailureCount,
|
|
2230
|
-
suggestion: '/clear + /continue',
|
|
2231
|
-
});
|
|
2232
|
-
// Reset counter after alerting
|
|
2233
|
-
silentFailureCount = 0;
|
|
2234
|
-
}
|
|
2235
|
-
// Attempt proactive repair if we have a transcript
|
|
2236
|
-
if (transcriptPath && validateTranscriptPath(transcriptPath) && !hasInFlightTools()) {
|
|
2237
|
-
dlog('Attempting proactive repair due to silent failure');
|
|
2238
|
-
try {
|
|
2239
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
2240
|
-
const { countOrphansInJsonl } = require('../capture/transcript-repair');
|
|
2241
|
-
const { orphans: orphanCount, orphanIds } = countOrphansInJsonl(transcriptPath);
|
|
2242
|
-
if (orphanCount > 0) {
|
|
2243
|
-
evictionDebugLog('SILENT_FAILURE_ORPHANS_FOUND', `Proactive scan found ${orphanCount} orphans`, {
|
|
2244
|
-
transcriptPath,
|
|
2245
|
-
orphanCount,
|
|
2246
|
-
orphanIds: orphanIds.slice(0, 5), // Log first 5 IDs
|
|
2247
|
-
});
|
|
2248
|
-
console.log(`[ekkOS] 🔍 Found ${orphanCount} orphan(s) in transcript - triggering repair`);
|
|
2249
|
-
// Trigger orphan recovery (reuse existing handler)
|
|
2250
|
-
void handleOrphanToolResult({ idx: -1, tool_use_id: 'silent_failure_detected' });
|
|
2251
|
-
}
|
|
2252
|
-
else {
|
|
2253
|
-
dlog('Proactive scan found no orphans - API error may be unrelated');
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
catch (err) {
|
|
2257
|
-
dlog(`Proactive repair scan failed: ${err.message}`);
|
|
2258
|
-
}
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
2217
|
// Monitor PTY output
|
|
2262
2218
|
shell.onData((data) => {
|
|
2263
2219
|
// Pass through to terminal
|
|
@@ -2269,38 +2225,20 @@ async function run(options) {
|
|
|
2269
2225
|
outputBuffer = outputBuffer.slice(-2000);
|
|
2270
2226
|
}
|
|
2271
2227
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2272
|
-
// ORPHAN TOOL_RESULT DETECTION
|
|
2228
|
+
// ORPHAN TOOL_RESULT DETECTION (LOCAL MODE ONLY)
|
|
2273
2229
|
// ccDNA validate mode emits [ekkOS] ORPHAN_TOOL_RESULT when it detects
|
|
2274
2230
|
// tool_results without matching tool_uses. This triggers automatic repair.
|
|
2275
|
-
//
|
|
2231
|
+
// DISABLED in proxy mode: proxy is sole eviction authority, CLI must not
|
|
2232
|
+
// touch the local JSONL. Also requires ccDNA which is currently disabled.
|
|
2276
2233
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2277
|
-
if (!isAutoClearInProgress && !isOrphanRecoveryInProgress) {
|
|
2278
|
-
// Append to orphan detection buffer (larger than main buffer to catch full markers)
|
|
2234
|
+
if (!proxyModeEnabled && !isAutoClearInProgress && !isOrphanRecoveryInProgress) {
|
|
2279
2235
|
orphanDetectionBuffer += stripAnsi(data);
|
|
2280
2236
|
if (orphanDetectionBuffer.length > 10000) {
|
|
2281
2237
|
const trimAmount = orphanDetectionBuffer.length - 8000;
|
|
2282
2238
|
orphanDetectionBuffer = orphanDetectionBuffer.slice(-8000);
|
|
2283
|
-
// Adjust cursor to account for trimmed portion
|
|
2284
2239
|
orphanScanCursor = Math.max(0, orphanScanCursor - trimAmount);
|
|
2285
2240
|
}
|
|
2286
|
-
// Run detection (extracted function for testability)
|
|
2287
2241
|
runOrphanDetection();
|
|
2288
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2289
|
-
// SILENT FAILURE DETECTION - Catch API errors without ccDNA markers
|
|
2290
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2291
|
-
const normalizedForSilent = stripAnsi(data);
|
|
2292
|
-
// Check for API 400 errors
|
|
2293
|
-
if (API_400_REGEX.test(normalizedForSilent)) {
|
|
2294
|
-
handleSilentFailure('API_400', normalizedForSilent.match(API_400_REGEX)?.[0] || '400');
|
|
2295
|
-
}
|
|
2296
|
-
// Check for explicit orphan-related API error messages
|
|
2297
|
-
else if (ORPHAN_API_ERROR_REGEX.test(normalizedForSilent)) {
|
|
2298
|
-
handleSilentFailure('ORPHAN_API_ERROR', normalizedForSilent.match(ORPHAN_API_ERROR_REGEX)?.[0] || 'orphan');
|
|
2299
|
-
}
|
|
2300
|
-
// Check for generic invalid message errors
|
|
2301
|
-
else if (INVALID_MESSAGE_REGEX.test(normalizedForSilent)) {
|
|
2302
|
-
handleSilentFailure('INVALID_MESSAGE', normalizedForSilent.match(INVALID_MESSAGE_REGEX)?.[0] || 'invalid');
|
|
2303
|
-
}
|
|
2304
2242
|
}
|
|
2305
2243
|
// Try to extract transcript path from output (Claude shows it on startup)
|
|
2306
2244
|
// CRITICAL: Strip ANSI codes FIRST to prevent capturing terminal garbage
|
|
@@ -2520,36 +2458,6 @@ async function run(options) {
|
|
|
2520
2458
|
}
|
|
2521
2459
|
});
|
|
2522
2460
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2523
|
-
// KICKSTART MODE: Auto-send "test" to create session immediately
|
|
2524
|
-
// Used by --dashboard to eliminate wait for first user message
|
|
2525
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2526
|
-
if (options.kickstart) {
|
|
2527
|
-
dlog('Kickstart mode enabled - will auto-send "test" to create session');
|
|
2528
|
-
setTimeout(async () => {
|
|
2529
|
-
dlog('Starting kickstart injection...');
|
|
2530
|
-
const readiness = await waitForIdlePrompt(getOutputBuffer, config);
|
|
2531
|
-
if (!readiness.ready || readiness.interrupted) {
|
|
2532
|
-
dlog('Claude not ready for kickstart - aborting');
|
|
2533
|
-
return;
|
|
2534
|
-
}
|
|
2535
|
-
// PAUSE STDIN during injection
|
|
2536
|
-
process.stdin.off('data', onStdinData);
|
|
2537
|
-
dlog('Stdin paused during kickstart');
|
|
2538
|
-
try {
|
|
2539
|
-
shell.write('\x15'); // Ctrl+U - clear any existing input
|
|
2540
|
-
await sleep(60);
|
|
2541
|
-
await typeSlowly(shell, 'test', config.charDelayMs);
|
|
2542
|
-
await sleep(100);
|
|
2543
|
-
shell.write('\r'); // Enter
|
|
2544
|
-
dlog('Kickstart "test" sent - session should be created');
|
|
2545
|
-
}
|
|
2546
|
-
finally {
|
|
2547
|
-
process.stdin.on('data', onStdinData);
|
|
2548
|
-
dlog('Stdin resumed after kickstart');
|
|
2549
|
-
}
|
|
2550
|
-
}, 3000); // 3s for Claude to initialize
|
|
2551
|
-
}
|
|
2552
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2553
2461
|
// RESEARCH MODE: Auto-type research prompt after Claude is ready
|
|
2554
2462
|
// Triggers: `ekkos run -r` or `ekkos run --research`
|
|
2555
2463
|
// Works like /clear continue - waits for idle prompt, then injects text
|
package/dist/index.js
CHANGED
|
@@ -51,9 +51,6 @@ 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");
|
|
57
54
|
const chalk_1 = __importDefault(require("chalk"));
|
|
58
55
|
const fs = __importStar(require("fs"));
|
|
59
56
|
const path = __importStar(require("path"));
|
|
@@ -79,7 +76,7 @@ commander_1.program
|
|
|
79
76
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
|
|
80
77
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
|
|
81
78
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
|
|
82
|
-
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos
|
|
79
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos sessions')} ${chalk_1.default.gray('List active Claude Code sessions')}`,
|
|
83
80
|
'',
|
|
84
81
|
chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
|
|
85
82
|
'',
|
|
@@ -157,7 +154,7 @@ commander_1.program
|
|
|
157
154
|
title: 'Running',
|
|
158
155
|
icon: '▸',
|
|
159
156
|
commands: [
|
|
160
|
-
{ name: 'run', desc: '
|
|
157
|
+
{ name: 'run', desc: 'Start Claude Code with ekkOS memory', note: 'default' },
|
|
161
158
|
{ name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
|
|
162
159
|
{ name: 'sessions', desc: 'List active Claude Code sessions' },
|
|
163
160
|
],
|
|
@@ -180,13 +177,6 @@ commander_1.program
|
|
|
180
177
|
{ name: 'agent', desc: 'Manage the remote terminal agent (start, stop, status, logs)' },
|
|
181
178
|
],
|
|
182
179
|
},
|
|
183
|
-
{
|
|
184
|
-
title: 'Swarm (Multi-Agent)',
|
|
185
|
-
icon: '▸',
|
|
186
|
-
commands: [
|
|
187
|
-
{ name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
|
|
188
|
-
],
|
|
189
|
-
},
|
|
190
180
|
];
|
|
191
181
|
const padCmd = 18;
|
|
192
182
|
let output = '';
|
|
@@ -239,7 +229,7 @@ commander_1.program
|
|
|
239
229
|
// Run command - launches Claude with auto-continue wrapper
|
|
240
230
|
commander_1.program
|
|
241
231
|
.command('run')
|
|
242
|
-
.description('
|
|
232
|
+
.description('Start Claude Code with ekkOS memory (default command)')
|
|
243
233
|
.option('-s, --session <name>', 'Session name to restore on clear')
|
|
244
234
|
.option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
|
|
245
235
|
.option('-v, --verbose', 'Show debug output')
|
|
@@ -249,7 +239,6 @@ commander_1.program
|
|
|
249
239
|
.option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
|
|
250
240
|
.option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
|
|
251
241
|
.option('--dashboard', 'Launch with live usage dashboard in an isolated 60/40 tmux split (requires tmux)')
|
|
252
|
-
.option('--kickstart', 'Auto-send "test" on load to create session immediately (used internally by --dashboard)')
|
|
253
242
|
.option('--add-dir <dirs...>', 'Additional directories Claude Code can access (outside working directory)')
|
|
254
243
|
.action((options) => {
|
|
255
244
|
(0, run_1.run)({
|
|
@@ -262,7 +251,6 @@ commander_1.program
|
|
|
262
251
|
noDna: options.skipDna,
|
|
263
252
|
noProxy: options.skipProxy,
|
|
264
253
|
dashboard: options.dashboard,
|
|
265
|
-
kickstart: options.kickstart,
|
|
266
254
|
addDirs: options.addDir,
|
|
267
255
|
});
|
|
268
256
|
});
|
|
@@ -354,10 +342,10 @@ hooksCmd
|
|
|
354
342
|
(0, index_1.registerUsageCommand)(commander_1.program);
|
|
355
343
|
// Dashboard command - live TUI for monitoring session usage
|
|
356
344
|
commander_1.program.addCommand(dashboard_1.dashboardCommand);
|
|
357
|
-
// Sessions command - list active Claude Code sessions
|
|
345
|
+
// Sessions command - list active Claude Code sessions
|
|
358
346
|
commander_1.program
|
|
359
347
|
.command('sessions')
|
|
360
|
-
.description('List active Claude Code sessions
|
|
348
|
+
.description('List active Claude Code sessions')
|
|
361
349
|
.option('-j, --json', 'Output machine-readable JSON')
|
|
362
350
|
.action((options) => {
|
|
363
351
|
const sessions = (0, state_1.getActiveSessions)();
|
|
@@ -482,59 +470,6 @@ agentCmd
|
|
|
482
470
|
.action((options) => {
|
|
483
471
|
(0, agent_1.agentHealth)({ json: options.json });
|
|
484
472
|
});
|
|
485
|
-
// Swarm command - manage Q-learning routing
|
|
486
|
-
const swarmCmd = commander_1.program
|
|
487
|
-
.command('swarm')
|
|
488
|
-
.description('Manage Swarm Q-learning model routing');
|
|
489
|
-
swarmCmd
|
|
490
|
-
.command('status')
|
|
491
|
-
.description('Show Q-table stats (states, visits, epsilon, top actions)')
|
|
492
|
-
.action(swarm_1.swarmStatus);
|
|
493
|
-
swarmCmd
|
|
494
|
-
.command('reset')
|
|
495
|
-
.description('Clear Q-table from Redis (routing reverts to static rules)')
|
|
496
|
-
.action(swarm_1.swarmReset);
|
|
497
|
-
swarmCmd
|
|
498
|
-
.command('export')
|
|
499
|
-
.description('Export Q-table to .swarm/q-learning-model.json')
|
|
500
|
-
.action(swarm_1.swarmExport);
|
|
501
|
-
swarmCmd
|
|
502
|
-
.command('import')
|
|
503
|
-
.description('Import Q-table from .swarm/q-learning-model.json into Redis')
|
|
504
|
-
.action(swarm_1.swarmImport);
|
|
505
|
-
swarmCmd
|
|
506
|
-
.command('launch')
|
|
507
|
-
.description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
|
|
508
|
-
.option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
|
|
509
|
-
.option('-t, --task <task>', 'Task description to decompose and execute')
|
|
510
|
-
.option('--no-bypass', 'Disable bypass permissions mode')
|
|
511
|
-
.option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
|
|
512
|
-
.option('--no-queen', 'Skip launching the Python Queen coordinator')
|
|
513
|
-
.option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
|
|
514
|
-
.option('-v, --verbose', 'Show debug output')
|
|
515
|
-
.action((options) => {
|
|
516
|
-
// Auto-open wizard when --task is missing
|
|
517
|
-
if (!options.task) {
|
|
518
|
-
(0, swarm_setup_1.swarmSetup)();
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
(0, swarm_1.swarmLaunch)({
|
|
522
|
-
workers: options.workers || 4,
|
|
523
|
-
task: options.task,
|
|
524
|
-
bypass: options.bypass !== false,
|
|
525
|
-
noDecompose: options.decompose === false,
|
|
526
|
-
noQueen: options.queen === false,
|
|
527
|
-
queenStrategy: options.queenStrategy,
|
|
528
|
-
verbose: options.verbose,
|
|
529
|
-
});
|
|
530
|
-
});
|
|
531
|
-
swarmCmd
|
|
532
|
-
.command('setup')
|
|
533
|
-
.description('Interactive TUI wizard for configuring and launching a swarm')
|
|
534
|
-
.action(() => {
|
|
535
|
-
(0, swarm_setup_1.swarmSetup)();
|
|
536
|
-
});
|
|
537
|
-
swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
|
|
538
473
|
// Handle `-help` (single dash) — rewrite to `--help` for Commander compatibility
|
|
539
474
|
const helpIdx = process.argv.indexOf('-help');
|
|
540
475
|
if (helpIdx !== -1) {
|
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
|
|
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
|
|
247
|
+
// MULTI-SESSION 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
|
|
275
275
|
*/
|
|
276
276
|
function registerActiveSession(sessionId, sessionName, projectPath) {
|
|
277
277
|
ensureEkkosDir();
|