@ekkos/cli 1.1.5 → 1.2.0
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 +61 -44
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +58 -148
- package/dist/index.js +2 -4
- 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
|
}
|
|
@@ -257,18 +251,28 @@ async function deployForClaude(apiKey, userId, options) {
|
|
|
257
251
|
catch (error) {
|
|
258
252
|
spinner.fail('MCP server configuration failed');
|
|
259
253
|
}
|
|
260
|
-
// Settings.json (hook registration)
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
(
|
|
254
|
+
// Settings.json (hook registration) — skipped on Windows (proxy-only mode)
|
|
255
|
+
if (platform_1.isWindows) {
|
|
256
|
+
spinner = (0, ora_1.default)('Hooks skipped (Windows proxy-only mode)').start();
|
|
257
|
+
spinner.info('Hooks skipped (Windows uses proxy-only mode)');
|
|
264
258
|
result.settings = true;
|
|
265
|
-
spinner.succeed('Hooks configuration');
|
|
266
259
|
}
|
|
267
|
-
|
|
268
|
-
spinner.
|
|
260
|
+
else {
|
|
261
|
+
spinner = (0, ora_1.default)('Deploying hooks configuration...').start();
|
|
262
|
+
try {
|
|
263
|
+
(0, settings_1.deployClaudeSettings)();
|
|
264
|
+
result.settings = true;
|
|
265
|
+
spinner.succeed('Hooks configuration');
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
spinner.fail('Hooks configuration failed');
|
|
269
|
+
}
|
|
269
270
|
}
|
|
270
|
-
// Hook scripts
|
|
271
|
-
if (
|
|
271
|
+
// Hook scripts — skipped on Windows
|
|
272
|
+
if (platform_1.isWindows) {
|
|
273
|
+
// Skip hook scripts on Windows
|
|
274
|
+
}
|
|
275
|
+
else if (!options.skipHooks) {
|
|
272
276
|
spinner = (0, ora_1.default)('Deploying hook scripts...').start();
|
|
273
277
|
try {
|
|
274
278
|
result.hooks = (0, hooks_1.deployHooks)(apiKey);
|
|
@@ -456,23 +460,36 @@ async function init(options) {
|
|
|
456
460
|
};
|
|
457
461
|
(0, fs_1.writeFileSync)(platform_1.EKKOS_CONFIG, JSON.stringify(config, null, 2));
|
|
458
462
|
(0, fs_1.chmodSync)(platform_1.EKKOS_CONFIG, '600'); // Secure permissions
|
|
459
|
-
//
|
|
460
|
-
console.log('');
|
|
461
|
-
console.log(chalk_1.default.gray('═'.repeat(40)));
|
|
462
|
-
console.log(chalk_1.default.green.bold('✓ Setup complete!'));
|
|
463
|
-
console.log(chalk_1.default.gray('═'.repeat(40)));
|
|
463
|
+
// Verify connectivity
|
|
464
464
|
console.log('');
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
}
|
|
470
477
|
}
|
|
471
|
-
|
|
472
|
-
|
|
478
|
+
catch {
|
|
479
|
+
verifySpinner.warn('Could not reach ekkOS API — check your network');
|
|
473
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'));
|
|
474
492
|
console.log('');
|
|
475
|
-
console.log(chalk_1.default.gray(`
|
|
476
|
-
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`));
|
|
477
494
|
console.log('');
|
|
478
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;
|
|
@@ -889,6 +877,23 @@ function launchWithDashboard(options) {
|
|
|
889
877
|
}
|
|
890
878
|
}
|
|
891
879
|
async function run(options) {
|
|
880
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
881
|
+
// FIRST-RUN CHECK: If no config exists, run init first
|
|
882
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
883
|
+
const existingConfig = (0, state_1.getConfig)();
|
|
884
|
+
if (!existingConfig) {
|
|
885
|
+
console.log('');
|
|
886
|
+
console.log(chalk_1.default.yellow(' Welcome to ekkOS! Let\'s get you set up first.'));
|
|
887
|
+
console.log('');
|
|
888
|
+
const { init } = await Promise.resolve().then(() => __importStar(require('./init')));
|
|
889
|
+
await init({});
|
|
890
|
+
// Re-check after init — if user cancelled or it failed, exit gracefully
|
|
891
|
+
const postInitConfig = (0, state_1.getConfig)();
|
|
892
|
+
if (!postInitConfig) {
|
|
893
|
+
console.log(chalk_1.default.gray(' Setup incomplete. Run `ekkos init` to try again.'));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
892
897
|
const verbose = options.verbose || false;
|
|
893
898
|
const bypass = options.bypass || false;
|
|
894
899
|
const noInject = options.noInject || false;
|
|
@@ -901,26 +906,44 @@ async function run(options) {
|
|
|
901
906
|
console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
|
|
902
907
|
}
|
|
903
908
|
// ══════════════════════════════════════════════════════════════════════════
|
|
909
|
+
// WINDOWS: Disable hooks for reliability — proxy-only mode
|
|
910
|
+
// Hooks can cause PowerShell execution policy issues and hangs on Windows.
|
|
911
|
+
// The proxy handles context management; hooks aren't strictly required.
|
|
912
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
913
|
+
if (isWindows) {
|
|
914
|
+
try {
|
|
915
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
916
|
+
if (fs.existsSync(settingsPath)) {
|
|
917
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
918
|
+
if (settings.hooks) {
|
|
919
|
+
delete settings.hooks;
|
|
920
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
921
|
+
console.log(chalk_1.default.gray(' ⏭️ Hooks disabled (Windows proxy-only mode)'));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// Non-fatal — continue without hook cleanup
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
904
930
|
// DASHBOARD MODE: Launch via tmux with isolated dashboard pane (60/40)
|
|
905
931
|
// ══════════════════════════════════════════════════════════════════════════
|
|
906
932
|
if (options.dashboard) {
|
|
907
|
-
|
|
908
|
-
|
|
933
|
+
try {
|
|
934
|
+
const tmuxCheck = isWindows ? 'where tmux' : 'command -v tmux';
|
|
935
|
+
(0, child_process_1.execSync)(tmuxCheck, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
936
|
+
launchWithDashboard(options);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
const installHint = isWindows
|
|
941
|
+
? 'Install: scoop install tmux (or use WSL)'
|
|
942
|
+
: 'Install: brew install tmux';
|
|
943
|
+
console.log(chalk_1.default.yellow(` tmux not found. ${installHint}`));
|
|
909
944
|
console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
|
|
910
945
|
console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
|
|
911
946
|
}
|
|
912
|
-
else {
|
|
913
|
-
try {
|
|
914
|
-
(0, child_process_1.execSync)('command -v tmux', { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
915
|
-
launchWithDashboard(options);
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
catch {
|
|
919
|
-
console.log(chalk_1.default.yellow(' tmux not found. Install: brew install tmux'));
|
|
920
|
-
console.log(chalk_1.default.gray(' Alternative: run "ekkos dashboard --latest" in a separate terminal'));
|
|
921
|
-
console.log(chalk_1.default.gray(' Continuing without dashboard...\n'));
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
947
|
}
|
|
925
948
|
// Generate instance ID for this run
|
|
926
949
|
const instanceId = generateInstanceId();
|
|
@@ -1472,11 +1495,6 @@ async function run(options) {
|
|
|
1472
1495
|
let orphanScanCursor = 0;
|
|
1473
1496
|
const ORPHAN_SCAN_TAIL_SLACK = 256; // Keep some overlap for chunk boundary tolerance
|
|
1474
1497
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1475
|
-
// SILENT FAILURE DETECTION - Catch API errors even without ccDNA markers
|
|
1476
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
1477
|
-
let lastSilentFailureTime = 0;
|
|
1478
|
-
let silentFailureCount = 0;
|
|
1479
|
-
const MAX_SILENT_FAILURES_BEFORE_ALERT = 2; // Alert user after 2 silent failures
|
|
1480
1498
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1481
1499
|
// TURN-END EVICTION - Only clean up when Claude is idle (safe state)
|
|
1482
1500
|
// ══════════════════════════════════════════════════════════════════════════
|
|
@@ -1971,6 +1989,11 @@ async function run(options) {
|
|
|
1971
1989
|
// The in-memory state is fine; we just fix the disk and log the bug.
|
|
1972
1990
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1973
1991
|
function handleOrphanToolResult(orphan) {
|
|
1992
|
+
// Proxy is sole eviction authority — never touch JSONL in proxy mode
|
|
1993
|
+
if (proxyModeEnabled) {
|
|
1994
|
+
dlog('Orphan recovery skipped - proxy mode (proxy is sole authority)');
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1974
1997
|
const now = Date.now();
|
|
1975
1998
|
if (isOrphanRecoveryInProgress) {
|
|
1976
1999
|
dlog('Orphan recovery already in progress, ignoring');
|
|
@@ -2172,71 +2195,6 @@ async function run(options) {
|
|
|
2172
2195
|
}, TEST_POLL_MS);
|
|
2173
2196
|
}
|
|
2174
2197
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2175
|
-
// SILENT FAILURE DETECTION HANDLER
|
|
2176
|
-
// Catches API 400 errors and orphan-related messages even without ccDNA markers
|
|
2177
|
-
// This is a backup for when ccDNA validate mode isn't working or is disabled
|
|
2178
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2179
|
-
function handleSilentFailure(matchType, matchedText) {
|
|
2180
|
-
const now = Date.now();
|
|
2181
|
-
// Cooldown check
|
|
2182
|
-
if (now - lastSilentFailureTime < SILENT_FAILURE_COOLDOWN_MS) {
|
|
2183
|
-
dlog(`Silent failure suppressed by cooldown (${matchType})`);
|
|
2184
|
-
return;
|
|
2185
|
-
}
|
|
2186
|
-
// Don't trigger during active recovery
|
|
2187
|
-
if (isOrphanRecoveryInProgress || isAutoClearInProgress) {
|
|
2188
|
-
dlog(`Silent failure ignored - recovery in progress (${matchType})`);
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
lastSilentFailureTime = now;
|
|
2192
|
-
silentFailureCount++;
|
|
2193
|
-
evictionDebugLog('SILENT_FAILURE_DETECTED', '════════════════════════════════════════════════════════', {
|
|
2194
|
-
alert: '⚠️ SILENT FAILURE - API error detected without ccDNA marker',
|
|
2195
|
-
matchType,
|
|
2196
|
-
matchedText: matchedText.slice(0, 200),
|
|
2197
|
-
silentFailureCount,
|
|
2198
|
-
transcriptPath,
|
|
2199
|
-
diagnosis: 'Possible orphan tool_result or ccDNA not in validate mode',
|
|
2200
|
-
});
|
|
2201
|
-
console.log(`\n[ekkOS] ⚠️ Silent failure detected: ${matchType}`);
|
|
2202
|
-
// After multiple failures, alert user and suggest action
|
|
2203
|
-
if (silentFailureCount >= MAX_SILENT_FAILURES_BEFORE_ALERT) {
|
|
2204
|
-
console.log(`[ekkOS] ⚠️ Multiple API errors detected (${silentFailureCount}x)`);
|
|
2205
|
-
console.log(`[ekkOS] This may indicate orphan tool_results in the transcript`);
|
|
2206
|
-
console.log(`[ekkOS] Try: /clear then /continue to rebuild message state`);
|
|
2207
|
-
evictionDebugLog('SILENT_FAILURE_ALERT', 'Multiple silent failures - user alerted', {
|
|
2208
|
-
count: silentFailureCount,
|
|
2209
|
-
suggestion: '/clear + /continue',
|
|
2210
|
-
});
|
|
2211
|
-
// Reset counter after alerting
|
|
2212
|
-
silentFailureCount = 0;
|
|
2213
|
-
}
|
|
2214
|
-
// Attempt proactive repair if we have a transcript
|
|
2215
|
-
if (transcriptPath && validateTranscriptPath(transcriptPath) && !hasInFlightTools()) {
|
|
2216
|
-
dlog('Attempting proactive repair due to silent failure');
|
|
2217
|
-
try {
|
|
2218
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
2219
|
-
const { countOrphansInJsonl } = require('../capture/transcript-repair');
|
|
2220
|
-
const { orphans: orphanCount, orphanIds } = countOrphansInJsonl(transcriptPath);
|
|
2221
|
-
if (orphanCount > 0) {
|
|
2222
|
-
evictionDebugLog('SILENT_FAILURE_ORPHANS_FOUND', `Proactive scan found ${orphanCount} orphans`, {
|
|
2223
|
-
transcriptPath,
|
|
2224
|
-
orphanCount,
|
|
2225
|
-
orphanIds: orphanIds.slice(0, 5), // Log first 5 IDs
|
|
2226
|
-
});
|
|
2227
|
-
console.log(`[ekkOS] 🔍 Found ${orphanCount} orphan(s) in transcript - triggering repair`);
|
|
2228
|
-
// Trigger orphan recovery (reuse existing handler)
|
|
2229
|
-
void handleOrphanToolResult({ idx: -1, tool_use_id: 'silent_failure_detected' });
|
|
2230
|
-
}
|
|
2231
|
-
else {
|
|
2232
|
-
dlog('Proactive scan found no orphans - API error may be unrelated');
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
catch (err) {
|
|
2236
|
-
dlog(`Proactive repair scan failed: ${err.message}`);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
2198
|
// Monitor PTY output
|
|
2241
2199
|
shell.onData((data) => {
|
|
2242
2200
|
// Pass through to terminal
|
|
@@ -2248,38 +2206,20 @@ async function run(options) {
|
|
|
2248
2206
|
outputBuffer = outputBuffer.slice(-2000);
|
|
2249
2207
|
}
|
|
2250
2208
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2251
|
-
// ORPHAN TOOL_RESULT DETECTION
|
|
2209
|
+
// ORPHAN TOOL_RESULT DETECTION (LOCAL MODE ONLY)
|
|
2252
2210
|
// ccDNA validate mode emits [ekkOS] ORPHAN_TOOL_RESULT when it detects
|
|
2253
2211
|
// tool_results without matching tool_uses. This triggers automatic repair.
|
|
2254
|
-
//
|
|
2212
|
+
// DISABLED in proxy mode: proxy is sole eviction authority, CLI must not
|
|
2213
|
+
// touch the local JSONL. Also requires ccDNA which is currently disabled.
|
|
2255
2214
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2256
|
-
if (!isAutoClearInProgress && !isOrphanRecoveryInProgress) {
|
|
2257
|
-
// Append to orphan detection buffer (larger than main buffer to catch full markers)
|
|
2215
|
+
if (!proxyModeEnabled && !isAutoClearInProgress && !isOrphanRecoveryInProgress) {
|
|
2258
2216
|
orphanDetectionBuffer += stripAnsi(data);
|
|
2259
2217
|
if (orphanDetectionBuffer.length > 10000) {
|
|
2260
2218
|
const trimAmount = orphanDetectionBuffer.length - 8000;
|
|
2261
2219
|
orphanDetectionBuffer = orphanDetectionBuffer.slice(-8000);
|
|
2262
|
-
// Adjust cursor to account for trimmed portion
|
|
2263
2220
|
orphanScanCursor = Math.max(0, orphanScanCursor - trimAmount);
|
|
2264
2221
|
}
|
|
2265
|
-
// Run detection (extracted function for testability)
|
|
2266
2222
|
runOrphanDetection();
|
|
2267
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2268
|
-
// SILENT FAILURE DETECTION - Catch API errors without ccDNA markers
|
|
2269
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
2270
|
-
const normalizedForSilent = stripAnsi(data);
|
|
2271
|
-
// Check for API 400 errors
|
|
2272
|
-
if (API_400_REGEX.test(normalizedForSilent)) {
|
|
2273
|
-
handleSilentFailure('API_400', normalizedForSilent.match(API_400_REGEX)?.[0] || '400');
|
|
2274
|
-
}
|
|
2275
|
-
// Check for explicit orphan-related API error messages
|
|
2276
|
-
else if (ORPHAN_API_ERROR_REGEX.test(normalizedForSilent)) {
|
|
2277
|
-
handleSilentFailure('ORPHAN_API_ERROR', normalizedForSilent.match(ORPHAN_API_ERROR_REGEX)?.[0] || 'orphan');
|
|
2278
|
-
}
|
|
2279
|
-
// Check for generic invalid message errors
|
|
2280
|
-
else if (INVALID_MESSAGE_REGEX.test(normalizedForSilent)) {
|
|
2281
|
-
handleSilentFailure('INVALID_MESSAGE', normalizedForSilent.match(INVALID_MESSAGE_REGEX)?.[0] || 'invalid');
|
|
2282
|
-
}
|
|
2283
2223
|
}
|
|
2284
2224
|
// Try to extract transcript path from output (Claude shows it on startup)
|
|
2285
2225
|
// CRITICAL: Strip ANSI codes FIRST to prevent capturing terminal garbage
|
|
@@ -2499,36 +2439,6 @@ async function run(options) {
|
|
|
2499
2439
|
}
|
|
2500
2440
|
});
|
|
2501
2441
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2502
|
-
// KICKSTART MODE: Auto-send "test" to create session immediately
|
|
2503
|
-
// Used by --dashboard to eliminate wait for first user message
|
|
2504
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2505
|
-
if (options.kickstart) {
|
|
2506
|
-
dlog('Kickstart mode enabled - will auto-send "test" to create session');
|
|
2507
|
-
setTimeout(async () => {
|
|
2508
|
-
dlog('Starting kickstart injection...');
|
|
2509
|
-
const readiness = await waitForIdlePrompt(getOutputBuffer, config);
|
|
2510
|
-
if (!readiness.ready || readiness.interrupted) {
|
|
2511
|
-
dlog('Claude not ready for kickstart - aborting');
|
|
2512
|
-
return;
|
|
2513
|
-
}
|
|
2514
|
-
// PAUSE STDIN during injection
|
|
2515
|
-
process.stdin.off('data', onStdinData);
|
|
2516
|
-
dlog('Stdin paused during kickstart');
|
|
2517
|
-
try {
|
|
2518
|
-
shell.write('\x15'); // Ctrl+U - clear any existing input
|
|
2519
|
-
await sleep(60);
|
|
2520
|
-
await typeSlowly(shell, 'test', config.charDelayMs);
|
|
2521
|
-
await sleep(100);
|
|
2522
|
-
shell.write('\r'); // Enter
|
|
2523
|
-
dlog('Kickstart "test" sent - session should be created');
|
|
2524
|
-
}
|
|
2525
|
-
finally {
|
|
2526
|
-
process.stdin.on('data', onStdinData);
|
|
2527
|
-
dlog('Stdin resumed after kickstart');
|
|
2528
|
-
}
|
|
2529
|
-
}, 3000); // 3s for Claude to initialize
|
|
2530
|
-
}
|
|
2531
|
-
// ══════════════════════════════════════════════════════════════════════════
|
|
2532
2442
|
// RESEARCH MODE: Auto-type research prompt after Claude is ready
|
|
2533
2443
|
// Triggers: `ekkos run -r` or `ekkos run --research`
|
|
2534
2444
|
// Works like /clear continue - waits for idle prompt, then injects text
|
package/dist/index.js
CHANGED
|
@@ -157,7 +157,7 @@ commander_1.program
|
|
|
157
157
|
title: 'Running',
|
|
158
158
|
icon: '▸',
|
|
159
159
|
commands: [
|
|
160
|
-
{ name: 'run', desc: '
|
|
160
|
+
{ name: 'run', desc: 'Start Claude Code with ekkOS memory', note: 'default' },
|
|
161
161
|
{ name: 'test-claude', desc: 'Launch Claude with proxy only (no ccDNA/PTY) for debugging' },
|
|
162
162
|
{ name: 'sessions', desc: 'List active Claude Code sessions' },
|
|
163
163
|
],
|
|
@@ -239,7 +239,7 @@ commander_1.program
|
|
|
239
239
|
// Run command - launches Claude with auto-continue wrapper
|
|
240
240
|
commander_1.program
|
|
241
241
|
.command('run')
|
|
242
|
-
.description('
|
|
242
|
+
.description('Start Claude Code with ekkOS memory (default command)')
|
|
243
243
|
.option('-s, --session <name>', 'Session name to restore on clear')
|
|
244
244
|
.option('-b, --bypass', 'Enable bypass permissions mode (dangerously skip all permission checks)')
|
|
245
245
|
.option('-v, --verbose', 'Show debug output')
|
|
@@ -249,7 +249,6 @@ commander_1.program
|
|
|
249
249
|
.option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
|
|
250
250
|
.option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
|
|
251
251
|
.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
252
|
.option('--add-dir <dirs...>', 'Additional directories Claude Code can access (outside working directory)')
|
|
254
253
|
.action((options) => {
|
|
255
254
|
(0, run_1.run)({
|
|
@@ -262,7 +261,6 @@ commander_1.program
|
|
|
262
261
|
noDna: options.skipDna,
|
|
263
262
|
noProxy: options.skipProxy,
|
|
264
263
|
dashboard: options.dashboard,
|
|
265
|
-
kickstart: options.kickstart,
|
|
266
264
|
addDirs: options.addDir,
|
|
267
265
|
});
|
|
268
266
|
});
|