@ekkos/cli 1.3.7 → 1.3.9

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.
@@ -49,6 +49,7 @@ const doctor_1 = require("./doctor");
49
49
  const stream_tailer_1 = require("../capture/stream-tailer");
50
50
  const jsonl_rewriter_1 = require("../capture/jsonl-rewriter");
51
51
  const transcript_repair_1 = require("../capture/transcript-repair");
52
+ const living_docs_manager_js_1 = require("../local/living-docs-manager.js");
52
53
  // Try to load node-pty (may fail on Node 24+)
53
54
  // IMPORTANT: This must be awaited in run() to avoid racey false fallbacks.
54
55
  let pty = null;
@@ -85,6 +86,244 @@ function getConfig(options) {
85
86
  };
86
87
  /* eslint-enable no-restricted-syntax */
87
88
  }
89
+ const MAX_OUTPUT_1M_MODELS = '128000';
90
+ const MAX_OUTPUT_200K_OPUS_SONNET = '32768';
91
+ let runtimeClaudeCodeVersion = null;
92
+ let runtimeClaudeContextWindow = 'auto';
93
+ let runtimeClaudeLaunchModel;
94
+ function normalizeContextWindowOption(value) {
95
+ const normalized = (value || '').trim().toLowerCase();
96
+ if (normalized === '200k' || normalized === '200000')
97
+ return '200k';
98
+ if (normalized === '1m' || normalized === '1000000')
99
+ return '1m';
100
+ return 'auto';
101
+ }
102
+ function normalizeRequestedLaunchModel(value) {
103
+ if (typeof value !== 'string')
104
+ return {};
105
+ const raw = (value || '').trim();
106
+ if (!raw)
107
+ return {};
108
+ if (raw.toLowerCase() === 'default') {
109
+ return {};
110
+ }
111
+ const oneMillionMatch = raw.match(/^(.*)\[1m\]$/i);
112
+ if (oneMillionMatch) {
113
+ return {
114
+ model: oneMillionMatch[1].trim(),
115
+ syntheticContextWindow: '1m',
116
+ };
117
+ }
118
+ const lower = raw.toLowerCase();
119
+ if (lower.endsWith('-200k')) {
120
+ return {
121
+ model: raw.slice(0, -5).trim(),
122
+ syntheticContextWindow: '200k',
123
+ };
124
+ }
125
+ if (lower.endsWith('-1m')) {
126
+ return {
127
+ model: raw.slice(0, -3).trim(),
128
+ syntheticContextWindow: '1m',
129
+ };
130
+ }
131
+ return { model: raw };
132
+ }
133
+ function modelSupportsOneMillionContext(model) {
134
+ const normalized = (model || 'default').trim().toLowerCase();
135
+ if (!normalized || normalized === 'default')
136
+ return true;
137
+ if (normalized === 'opus' || normalized === 'sonnet')
138
+ return true;
139
+ return normalized.includes('claude-opus-4-6') || normalized.includes('claude-sonnet-4-6');
140
+ }
141
+ function buildClaudeLaunchModelArg(model, contextWindow) {
142
+ if (!model)
143
+ return undefined;
144
+ const normalized = normalizeRequestedLaunchModel(model).model;
145
+ if (!normalized)
146
+ return undefined;
147
+ if (contextWindow === '1m' && modelSupportsOneMillionContext(normalized)) {
148
+ return `${normalized}[1m]`;
149
+ }
150
+ return normalized;
151
+ }
152
+ function isTwoHundredKFixedOutputModel(model) {
153
+ const normalized = (model || '').trim().toLowerCase();
154
+ if (!normalized)
155
+ return false;
156
+ if (normalized === 'opus' || normalized === 'sonnet')
157
+ return true;
158
+ return /^claude-(?:opus|sonnet)-4-(?:5|6)(?:$|-)/.test(normalized);
159
+ }
160
+ function resolveClaudeMaxOutputTokens(model, contextWindow) {
161
+ if (process.env.EKKOS_MAX_OUTPUT_TOKENS) {
162
+ return process.env.EKKOS_MAX_OUTPUT_TOKENS;
163
+ }
164
+ if (contextWindow === '200k' && isTwoHundredKFixedOutputModel(model)) {
165
+ return MAX_OUTPUT_200K_OPUS_SONNET;
166
+ }
167
+ return MAX_OUTPUT_1M_MODELS;
168
+ }
169
+ function shouldOpenLaunchSelector(options) {
170
+ return options.model === true;
171
+ }
172
+ function renderClaudeLaunchSelectorIntro() {
173
+ const neonCyan = chalk_1.default.hex('#00f5ff');
174
+ const acidGreen = chalk_1.default.hex('#9dff00');
175
+ const signalAmber = chalk_1.default.hex('#ffb300');
176
+ const steel = chalk_1.default.hex('#8a92a6');
177
+ const width = 68;
178
+ const line = (text) => {
179
+ const pad = Math.max(0, width - stripAnsi(text).length);
180
+ return `${neonCyan('║')} ${text}${' '.repeat(pad)}${neonCyan('║')}`;
181
+ };
182
+ console.log('');
183
+ console.log(neonCyan('╔══════════════════════════════════════════════════════════════════════╗'));
184
+ console.log(line(chalk_1.default.white.bold('ekkOS.dev // PULSE')));
185
+ console.log(neonCyan('╠══════════════════════════════════════════════════════════════════════╣'));
186
+ console.log(line(`${acidGreen('200K')} ${steel('compat lane')} ${chalk_1.default.white('// Opus/Sonnet 4.5/4.6 => 32,768 output')}`));
187
+ console.log(line(`${signalAmber(' 1M')} ${steel('wide lane')} ${chalk_1.default.white('// Opus/Sonnet 4.6 => 128,000 output')}`));
188
+ console.log(line(steel('Pick a launch vector, then a fixed runtime profile. No hidden context prompt.')));
189
+ console.log(neonCyan('╚══════════════════════════════════════════════════════════════════════╝'));
190
+ console.log('');
191
+ }
192
+ function buildLaunchModeChoices() {
193
+ return [
194
+ {
195
+ name: `${chalk_1.default.hex('#00f5ff').bold('Fresh boot')} ${chalk_1.default.gray('// new shell')}`,
196
+ value: 'fresh',
197
+ },
198
+ {
199
+ name: `${chalk_1.default.hex('#9dff00').bold('Resume recent')} ${chalk_1.default.gray('// latest thread')}`,
200
+ value: 'continue',
201
+ },
202
+ {
203
+ name: `${chalk_1.default.hex('#ffb300').bold('Resume by ID')} ${chalk_1.default.gray('// targeted reconnect')}`,
204
+ value: 'resume',
205
+ },
206
+ ];
207
+ }
208
+ function buildLaunchModelChoices() {
209
+ const cyan = chalk_1.default.hex('#00f5ff');
210
+ const amber = chalk_1.default.hex('#ffb300');
211
+ const green = chalk_1.default.hex('#00ff88');
212
+ return [
213
+ {
214
+ name: `${cyan.bold('Auto route')} ${chalk_1.default.gray('// Claude decides profile')}`,
215
+ value: 'default',
216
+ },
217
+ {
218
+ name: `${cyan.bold('Claude Opus 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
219
+ value: 'claude-opus-4-5',
220
+ },
221
+ {
222
+ name: `${cyan.bold('Claude Opus 4.6')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
223
+ value: 'claude-opus-4-6-200k',
224
+ },
225
+ {
226
+ name: `${cyan.bold('Claude Opus 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 128,000')}`,
227
+ value: 'claude-opus-4-6-1m',
228
+ },
229
+ {
230
+ name: `${cyan.bold('Claude Sonnet 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
231
+ value: 'claude-sonnet-4-5',
232
+ },
233
+ {
234
+ name: `${cyan.bold('Claude Sonnet 4.6')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
235
+ value: 'claude-sonnet-4-6-200k',
236
+ },
237
+ {
238
+ name: `${cyan.bold('Claude Sonnet 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 128,000')}`,
239
+ value: 'claude-sonnet-4-6-1m',
240
+ },
241
+ {
242
+ name: `${cyan.bold('Claude Haiku 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// fast lane')}`,
243
+ value: 'claude-haiku-4-5',
244
+ },
245
+ ];
246
+ }
247
+ async function resolveClaudeLaunchSelection(options) {
248
+ const requestedModel = normalizeRequestedLaunchModel(typeof options.model === 'string' ? options.model : process.env.EKKOS_CLAUDE_MODEL);
249
+ let model = requestedModel.model;
250
+ let contextWindow = requestedModel.syntheticContextWindow
251
+ || normalizeContextWindowOption(options.contextWindow || process.env.EKKOS_CLAUDE_CONTEXT_WINDOW);
252
+ let continueLast = options.continueLast || false;
253
+ let resumeSession = (options.resumeSession || '').trim();
254
+ const shouldShowLaunchWindow = shouldOpenLaunchSelector(options)
255
+ && process.stdin.isTTY === true
256
+ && process.stdout.isTTY === true
257
+ && process.env.EKKOS_DISABLE_LAUNCH_WINDOW !== '1';
258
+ if (!shouldShowLaunchWindow) {
259
+ if (!modelSupportsOneMillionContext(model)) {
260
+ contextWindow = '200k';
261
+ }
262
+ return {
263
+ model,
264
+ contextWindow,
265
+ continueLast: !!continueLast && !resumeSession,
266
+ resumeSession: resumeSession || undefined,
267
+ };
268
+ }
269
+ const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
270
+ renderClaudeLaunchSelectorIntro();
271
+ const defaultLaunchMode = resumeSession ? 'resume' : continueLast ? 'continue' : 'fresh';
272
+ const firstPrompt = await inquirer.prompt([
273
+ {
274
+ type: 'list',
275
+ name: 'launchMode',
276
+ message: chalk_1.default.hex('#00f5ff').bold('>> Boot vector'),
277
+ default: defaultLaunchMode,
278
+ pageSize: 3,
279
+ loop: false,
280
+ choices: buildLaunchModeChoices(),
281
+ },
282
+ {
283
+ type: 'list',
284
+ name: 'model',
285
+ message: chalk_1.default.hex('#00f5ff').bold('>> Model lane'),
286
+ default: model || 'default',
287
+ pageSize: 8,
288
+ loop: false,
289
+ choices: buildLaunchModelChoices(),
290
+ },
291
+ ]);
292
+ const selectedProfile = normalizeRequestedLaunchModel(firstPrompt.model);
293
+ model = firstPrompt.model === 'default' ? undefined : selectedProfile.model;
294
+ continueLast = firstPrompt.launchMode === 'continue';
295
+ resumeSession = firstPrompt.launchMode === 'resume' ? resumeSession : '';
296
+ if (firstPrompt.launchMode === 'resume') {
297
+ const resumePrompt = await inquirer.prompt([
298
+ {
299
+ type: 'input',
300
+ name: 'resumeSession',
301
+ message: 'Claude session ID',
302
+ default: resumeSession,
303
+ validate: (value) => value.trim().length > 0 || 'Session ID is required',
304
+ },
305
+ ]);
306
+ resumeSession = resumePrompt.resumeSession.trim();
307
+ }
308
+ if (firstPrompt.model === 'default') {
309
+ contextWindow = 'auto';
310
+ }
311
+ else if (selectedProfile.syntheticContextWindow) {
312
+ contextWindow = selectedProfile.syntheticContextWindow;
313
+ }
314
+ else if (!modelSupportsOneMillionContext(model)) {
315
+ contextWindow = '200k';
316
+ }
317
+ else {
318
+ contextWindow = 'auto';
319
+ }
320
+ return {
321
+ model,
322
+ contextWindow,
323
+ continueLast,
324
+ resumeSession: resumeSession || undefined,
325
+ };
326
+ }
88
327
  // ═══════════════════════════════════════════════════════════════════════════
89
328
  // PATTERN MATCHING
90
329
  // ═══════════════════════════════════════════════════════════════════════════
@@ -290,11 +529,6 @@ const isWindows = os.platform() === 'win32';
290
529
  // Core ekkOS patches (eviction, context management) work with all recent versions
291
530
  // Cosmetic patches may fail on newer versions but don't affect functionality
292
531
  const PINNED_CLAUDE_VERSION = 'latest';
293
- // Max output tokens for Claude responses
294
- // Default: 16384 (safe for Sonnet 4.5)
295
- // Opus 4.5 supports up to 64k - set EKKOS_MAX_OUTPUT_TOKENS=32768 or =65536 to use higher limits
296
- // Configurable via environment variable
297
- const EKKOS_MAX_OUTPUT_TOKENS = process.env.EKKOS_MAX_OUTPUT_TOKENS || '16384';
298
532
  const proxy_url_1 = require("../utils/proxy-url");
299
533
  // Track proxy mode for getEkkosEnv (set by run() based on options)
300
534
  let proxyModeEnabled = true;
@@ -315,12 +549,13 @@ let cliSessionName = null;
315
549
  let cliSessionId = null;
316
550
  /**
317
551
  * Get environment with ekkOS enhancements
318
- * - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS to 32k for longer responses
552
+ * - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS dynamically for the selected Claude profile
319
553
  * - Routes API through ekkOS proxy for seamless context eviction (when enabled)
320
554
  * - Sets EKKOS_PROXY_MODE to signal JSONL rewriter to disable eviction
321
555
  * - Passes session headers for eviction/retrieval context tracking
322
556
  */
323
557
  function getEkkosEnv() {
558
+ const maxOutputTokens = resolveClaudeMaxOutputTokens(runtimeClaudeLaunchModel, runtimeClaudeContextWindow);
324
559
  /* eslint-disable no-restricted-syntax -- System env spreading, not API key access */
325
560
  const env = {
326
561
  ...process.env,
@@ -332,8 +567,18 @@ function getEkkosEnv() {
332
567
  // Native autocompact would compact without ekkOS saving state first, causing knowledge loss.
333
568
  // Only ekkOS-wrapped sessions get this; vanilla `claude` keeps autocompact on.
334
569
  DISABLE_AUTO_COMPACT: 'true',
570
+ // Align Claude's advertised output ceiling with the chosen model/context profile.
571
+ // 1M Opus/Sonnet gets 128K; 200K Opus/Sonnet 4.5/4.6 is forced to 32,768.
572
+ // The proxy also enforces this server-side in case Claude Code ignores the env var.
573
+ CLAUDE_CODE_MAX_OUTPUT_TOKENS: maxOutputTokens,
335
574
  };
336
575
  /* eslint-enable no-restricted-syntax */
576
+ if (runtimeClaudeContextWindow === '200k') {
577
+ env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
578
+ }
579
+ else {
580
+ delete env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
581
+ }
337
582
  // Check if proxy is disabled via env var or options
338
583
  // eslint-disable-next-line no-restricted-syntax -- Feature flag, not API key
339
584
  const proxyDisabled = process.env.EKKOS_DISABLE_PROXY === '1' || !proxyModeEnabled;
@@ -346,7 +591,9 @@ function getEkkosEnv() {
346
591
  if (!cliSessionName) {
347
592
  cliSessionId = crypto.randomUUID();
348
593
  cliSessionName = (0, state_1.uuidToWords)(cliSessionId);
349
- console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName}`));
594
+ if (process.env.EKKOS_NO_SPLASH !== '1') {
595
+ console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName}`));
596
+ }
350
597
  }
351
598
  // Get full userId from config (NOT the truncated version from auth token)
352
599
  // Config has full UUID like "d4532ba0-0a86-42ce-bab4-22aa62b55ce6"
@@ -366,7 +613,10 @@ function getEkkosEnv() {
366
613
  // CRITICAL: Embed user/session in URL path since ANTHROPIC_HEADERS doesn't work
367
614
  // Claude Code SDK doesn't forward custom headers, but it DOES use ANTHROPIC_BASE_URL
368
615
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
369
- const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId);
616
+ const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId, {
617
+ claudeCodeVersion: runtimeClaudeCodeVersion || undefined,
618
+ claudeContextWindow: runtimeClaudeContextWindow,
619
+ });
370
620
  env.ANTHROPIC_BASE_URL = proxyUrl;
371
621
  // Proxy URL contains userId + project path — don't leak to terminal
372
622
  }
@@ -504,7 +754,9 @@ function resolveClaudePath() {
504
754
  // No system Claude found — fall through to ekkOS-managed install
505
755
  }
506
756
  // ekkOS-managed installation (for pinned versions or no system Claude)
507
- if (fs.existsSync(EKKOS_CLAUDE_BIN) && checkClaudeVersion(EKKOS_CLAUDE_BIN)) {
757
+ // When 'latest', skip the slow `claude --version` subprocess — any version is fine.
758
+ const ekkosClaudeExists = fs.existsSync(EKKOS_CLAUDE_BIN);
759
+ if (ekkosClaudeExists && (PINNED_CLAUDE_VERSION === 'latest' || checkClaudeVersion(EKKOS_CLAUDE_BIN))) {
508
760
  return EKKOS_CLAUDE_BIN;
509
761
  }
510
762
  // Auto-install to ekkOS-managed directory
@@ -575,6 +827,105 @@ function resolveGlobalClaudePath() {
575
827
  function sleep(ms) {
576
828
  return new Promise(resolve => setTimeout(resolve, ms));
577
829
  }
830
+ function formatPulseLaunchMode(options) {
831
+ if (options.resumeSession)
832
+ return `RESUME // ${options.resumeSession}`;
833
+ if (options.continueLast)
834
+ return 'CONTINUE // most recent Claude thread';
835
+ return 'FRESH // new interactive shell';
836
+ }
837
+ function formatPulseModel(model) {
838
+ const normalized = (model || '').trim().toLowerCase();
839
+ if (!normalized)
840
+ return 'Claude default';
841
+ if (normalized === 'opus')
842
+ return 'Opus alias';
843
+ if (normalized === 'sonnet')
844
+ return 'Sonnet alias';
845
+ if (normalized === 'claude-opus-4-5')
846
+ return 'Claude Opus 4.5';
847
+ if (normalized === 'claude-opus-4-6')
848
+ return 'Claude Opus 4.6';
849
+ if (normalized === 'claude-sonnet-4-5')
850
+ return 'Claude Sonnet 4.5';
851
+ if (normalized === 'claude-sonnet-4-6')
852
+ return 'Claude Sonnet 4.6';
853
+ if (normalized === 'claude-haiku-4-5')
854
+ return 'Claude Haiku 4.5';
855
+ return model || 'Claude default';
856
+ }
857
+ function formatPulseContextLine(contextWindow) {
858
+ if (contextWindow === '200k') {
859
+ return '200K // 200,000 token window // forced 32,768 output';
860
+ }
861
+ if (contextWindow === '1m') {
862
+ return '1M // 1,000,000 token window // unlocked 128,000 output';
863
+ }
864
+ return 'AUTO // ekkOS resolves 200K vs 1M from the selected profile';
865
+ }
866
+ async function showPulseLaunchLoader(options) {
867
+ const cyan = chalk_1.default.hex('#00f0ff');
868
+ const amber = chalk_1.default.hex('#ffb800');
869
+ const green = chalk_1.default.hex('#00ff88');
870
+ const steel = chalk_1.default.hex('#7a7a8e');
871
+ const panel = chalk_1.default.hex('#111118');
872
+ const wordmark = [
873
+ '██████╗ ██╗ ██╗██╗ ███████╗███████╗',
874
+ '██╔══██╗██║ ██║██║ ██╔════╝██╔════╝',
875
+ '██████╔╝██║ ██║██║ ███████╗█████╗ ',
876
+ '██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝ ',
877
+ '██║ ╚██████╔╝███████╗███████║███████╗',
878
+ '╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝',
879
+ ];
880
+ const stages = [
881
+ 'arming pulse launcher',
882
+ 'syncing dashboard split',
883
+ 'routing claude session',
884
+ ];
885
+ const barWidth = 26;
886
+ const panelInnerWidth = 76;
887
+ const buildPanelLine = (labelColor, label, value) => {
888
+ const content = ` ${label.padEnd(7)} ${value}`;
889
+ const pad = Math.max(0, panelInnerWidth - stripAnsi(content).length);
890
+ return ` ${cyan('║')} ${labelColor(label.padEnd(7))} ${chalk_1.default.white(value)}${' '.repeat(pad)}${cyan('║')}`;
891
+ };
892
+ const headerContent = ' ekkOS.dev // PULSE BOOTSTRAP live launch matrix for Claude + dashboard';
893
+ const headerPad = Math.max(0, panelInnerWidth - headerContent.length);
894
+ console.log('');
895
+ for (const [idx, line] of wordmark.entries()) {
896
+ const color = idx % 2 === 0 ? cyan.bold : chalk_1.default.white.bold;
897
+ console.log(' ' + color(line));
898
+ await sleep(22);
899
+ }
900
+ console.log(' ' + amber('▔').repeat(54));
901
+ console.log(cyan(' ╔══════════════════════════════════════════════════════════════════════════════╗'));
902
+ console.log(` ${cyan('║')}${chalk_1.default.white.bold(' ekkOS.dev // PULSE BOOTSTRAP')}${steel(' live launch matrix for Claude + dashboard')}${' '.repeat(headerPad)}${cyan('║')}`);
903
+ console.log(buildPanelLine(green, 'MODE', formatPulseLaunchMode(options)));
904
+ console.log(buildPanelLine(green, 'MODEL', formatPulseModel(typeof options.model === 'string' ? options.model : undefined)));
905
+ console.log(buildPanelLine(amber, 'WINDOW', formatPulseContextLine(normalizeContextWindowOption(options.contextWindow))));
906
+ console.log(buildPanelLine(steel, 'PATH', 'selector -> dashboard -> claude -> proxy'));
907
+ console.log(cyan(' ╚══════════════════════════════════════════════════════════════════════════════╝'));
908
+ console.log(` ${green('200K')} ${steel('// Opus/Sonnet 4.5/4.6 => 32,768 output')}`);
909
+ console.log(` ${amber('1M ')} ${steel('// Opus/Sonnet 4.6 => 128,000 output')}`);
910
+ console.log('');
911
+ for (const stage of stages) {
912
+ for (let filled = 0; filled <= barWidth; filled += 1) {
913
+ const complete = '█'.repeat(filled);
914
+ const pending = '░'.repeat(barWidth - filled);
915
+ process.stdout.write(`\r ${amber('[')}${cyan(complete)}${steel(pending)}${amber(']')} ${chalk_1.default.white(stage)}`);
916
+ await sleep(16);
917
+ }
918
+ process.stdout.write('\n');
919
+ }
920
+ console.log(` ${green('READY')} ${panel('dashboard split armed')} ${steel('// selector handoff imminent')}`);
921
+ if (options.bypass) {
922
+ console.log(` ${amber('BYPASS')} ${chalk_1.default.white('permission prompts disabled for this launch')}`);
923
+ }
924
+ if (options.verbose) {
925
+ console.log(` ${steel('TRACE')} ${steel('verbose telemetry enabled')}`);
926
+ }
927
+ console.log('');
928
+ }
578
929
  /**
579
930
  * Show morning dreams with WOW factor right after sparkle animation.
580
931
  * Colorful, lively, readable — real magic moment. Then pause for Enter.
@@ -811,6 +1162,9 @@ function writeSessionFiles(sessionId, sessionName) {
811
1162
  * Launch ekkos run + dashboard in isolated tmux panes (60/40 split)
812
1163
  */
813
1164
  function launchWithDashboard(options) {
1165
+ // Prune dead-PID entries from active-sessions.json so the dashboard
1166
+ // (and resolveSessionName) never cross-bind to stale sessions after restart.
1167
+ (0, state_1.getActiveSessions)();
814
1168
  const tmuxSession = `ekkos-${Date.now().toString(36)}`;
815
1169
  const launchTime = Date.now();
816
1170
  // Pre-generate session name so dashboard can start immediately (no polling).
@@ -831,6 +1185,15 @@ function launchWithDashboard(options) {
831
1185
  runArgs.push('--skip-inject');
832
1186
  if (options.noProxy)
833
1187
  runArgs.push('--skip-proxy');
1188
+ if (options.continueLast)
1189
+ runArgs.push('--continue-last');
1190
+ if (options.resumeSession)
1191
+ runArgs.push('--resume-session', options.resumeSession);
1192
+ if (typeof options.model === 'string')
1193
+ runArgs.push('--model', options.model);
1194
+ if (options.contextWindow && options.contextWindow !== 'auto') {
1195
+ runArgs.push('--context-window', options.contextWindow);
1196
+ }
834
1197
  const ekkosCmd = process.argv[1]; // Path to ekkos CLI
835
1198
  const cwd = process.cwd();
836
1199
  const termCols = process.stdout.columns ?? 160;
@@ -916,6 +1279,15 @@ function launchWithWindowsTerminal(options) {
916
1279
  runArgs.push('--skip-inject');
917
1280
  if (options.noProxy)
918
1281
  runArgs.push('--skip-proxy');
1282
+ if (options.continueLast)
1283
+ runArgs.push('--continue-last');
1284
+ if (options.resumeSession)
1285
+ runArgs.push('--resume-session', options.resumeSession);
1286
+ if (typeof options.model === 'string')
1287
+ runArgs.push('--model', options.model);
1288
+ if (options.contextWindow && options.contextWindow !== 'auto') {
1289
+ runArgs.push('--context-window', options.contextWindow);
1290
+ }
919
1291
  // Write a temp batch file to avoid all quoting issues
920
1292
  // wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
921
1293
  const batPath = path.join(os.tmpdir(), `ekkos-wt-${Date.now()}.cmd`);
@@ -963,6 +1335,14 @@ async function run(options) {
963
1335
  const verbose = options.verbose || false;
964
1336
  const bypass = options.bypass || false;
965
1337
  const noInject = options.noInject || false;
1338
+ const suppressPreClaudeOutput = process.env.EKKOS_NO_SPLASH === '1';
1339
+ const launchSelection = await resolveClaudeLaunchSelection(options);
1340
+ options.model = launchSelection.model;
1341
+ options.contextWindow = launchSelection.contextWindow;
1342
+ options.continueLast = launchSelection.continueLast;
1343
+ options.resumeSession = launchSelection.resumeSession;
1344
+ runtimeClaudeLaunchModel = launchSelection.model;
1345
+ runtimeClaudeContextWindow = launchSelection.contextWindow;
966
1346
  // Honour -s flag: lock the session name before getEkkosEnv() can mint a new one.
967
1347
  // This is critical for --dashboard mode where launchWithDashboard() pre-generates
968
1348
  // a session name and passes it via -s so the dashboard and run command agree.
@@ -973,11 +1353,28 @@ async function run(options) {
973
1353
  // Set proxy mode based on options (used by getEkkosEnv)
974
1354
  proxyModeEnabled = !(options.noProxy || false);
975
1355
  if (proxyModeEnabled) {
976
- console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
1356
+ if (!suppressPreClaudeOutput && !options.pulse) {
1357
+ console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
1358
+ }
977
1359
  }
978
- else if (verbose) {
1360
+ else if (verbose && !suppressPreClaudeOutput) {
979
1361
  console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
980
1362
  }
1363
+ let pulseIntroShown = false;
1364
+ if (options.pulse
1365
+ && process.env.EKKOS_REMOTE_SESSION !== '1'
1366
+ && process.env.EKKOS_NO_SPLASH !== '1'
1367
+ && process.stdout.isTTY === true) {
1368
+ await showPulseLaunchLoader({
1369
+ model: options.model,
1370
+ contextWindow: options.contextWindow,
1371
+ continueLast: options.continueLast,
1372
+ resumeSession: options.resumeSession,
1373
+ bypass: options.bypass,
1374
+ verbose: options.verbose,
1375
+ });
1376
+ pulseIntroShown = true;
1377
+ }
981
1378
  // ══════════════════════════════════════════════════════════════════════════
982
1379
  // DASHBOARD MODE: Launch via tmux (Unix) or Windows Terminal split pane
983
1380
  // ══════════════════════════════════════════════════════════════════════════
@@ -1043,16 +1440,45 @@ async function run(options) {
1043
1440
  // ══════════════════════════════════════════════════════════════════════════
1044
1441
  (0, state_1.ensureEkkosDir)();
1045
1442
  (0, state_1.clearAutoClearFlag)();
1443
+ // Construct manager early but defer .start() until Claude's first idle prompt
1444
+ // (filesystem scans run only after Claude is fully loaded and waiting for input)
1445
+ let localLivingDocsManager = null;
1446
+ if (process.env.EKKOS_LOCAL_LIVING_DOCS !== '0') {
1447
+ try {
1448
+ localLivingDocsManager = new living_docs_manager_js_1.LocalLivingDocsManager({
1449
+ targetPath: process.cwd(),
1450
+ apiUrl: process.env.EKKOS_API_URL || MEMORY_API_URL,
1451
+ apiKey: (0, state_1.getAuthToken)(),
1452
+ timeZone: process.env.EKKOS_USER_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone,
1453
+ onLog: message => dlog(message),
1454
+ });
1455
+ // .start() deferred to first idle prompt — see idle detection below (~line 2685)
1456
+ }
1457
+ catch (err) {
1458
+ dlog(`[LivingDocs:Local] Failed to start: ${err.message}`);
1459
+ }
1460
+ }
1046
1461
  // Resolve Claude path
1047
1462
  const rawClaudePath = resolveClaudePath();
1048
1463
  const isNpxMode = rawClaudePath.startsWith('npx:');
1049
1464
  const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
1050
1465
  const claudePath = isNpxMode ? 'npx' : rawClaudePath;
1466
+ runtimeClaudeCodeVersion = pinnedVersion || getClaudeVersion(claudePath);
1051
1467
  // Build args early
1052
1468
  const earlyArgs = [];
1053
1469
  if (isNpxMode) {
1054
1470
  earlyArgs.push(`@anthropic-ai/claude-code@${pinnedVersion}`);
1055
1471
  }
1472
+ if (options.resumeSession) {
1473
+ earlyArgs.push('--resume', options.resumeSession);
1474
+ }
1475
+ else if (options.continueLast) {
1476
+ earlyArgs.push('--continue');
1477
+ }
1478
+ const launchModelArg = buildClaudeLaunchModelArg(options.model, options.contextWindow);
1479
+ if (launchModelArg) {
1480
+ earlyArgs.push('--model', launchModelArg);
1481
+ }
1056
1482
  if (bypass) {
1057
1483
  earlyArgs.push('--dangerously-skip-permissions');
1058
1484
  }
@@ -1115,7 +1541,7 @@ async function run(options) {
1115
1541
  // ══════════════════════════════════════════════════════════════════════════
1116
1542
  // STARTUP BANNER WITH COLOR PULSE ANIMATION
1117
1543
  // ══════════════════════════════════════════════════════════════════════════
1118
- const skipFancyIntro = process.env.EKKOS_REMOTE_SESSION === '1' || process.env.EKKOS_NO_SPLASH === '1' || !!process.env.TMUX;
1544
+ const skipFancyIntro = pulseIntroShown || process.env.EKKOS_REMOTE_SESSION === '1' || suppressPreClaudeOutput || !!process.env.TMUX;
1119
1545
  if (!skipFancyIntro) {
1120
1546
  const logoLines = [
1121
1547
  ' ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ▄▄',
@@ -1297,7 +1723,7 @@ async function run(options) {
1297
1723
  }
1298
1724
  console.log('');
1299
1725
  }
1300
- else {
1726
+ else if (!suppressPreClaudeOutput) {
1301
1727
  // Static banner for Windows / remote / no-splash — no cursor manipulation
1302
1728
  console.log('');
1303
1729
  console.log(chalk_1.default.hex('#FF6B35').bold(' ekkOS_Pulse') + chalk_1.default.gray(' — Context is finite. Intelligence isn\'t.'));
@@ -1313,7 +1739,9 @@ async function run(options) {
1313
1739
  // ══════════════════════════════════════════════════════════════════════════
1314
1740
  // MAGIC MOMENT: Morning dreams right after sparkle, before Claude appears
1315
1741
  // ══════════════════════════════════════════════════════════════════════════
1316
- await showMorningDreamsIfNeeded();
1742
+ if (!suppressPreClaudeOutput && !options.pulse) {
1743
+ await showMorningDreamsIfNeeded();
1744
+ }
1317
1745
  // ══════════════════════════════════════════════════════════════════════════
1318
1746
  // ANIMATION COMPLETE: Mark ready and flush buffered Claude output
1319
1747
  // ══════════════════════════════════════════════════════════════════════════
@@ -1321,6 +1749,7 @@ async function run(options) {
1321
1749
  // Clear terminal to prevent startup banner artifacts bleeding into Claude Code's Ink TUI
1322
1750
  process.stdout.write('\x1B[2J\x1B[H');
1323
1751
  dlog(`Animation complete. shellReady=${shellReady}, buffered=${bufferedOutput.length} chunks`);
1752
+ // Living docs .start() is deferred further — triggers on first idle prompt (Claude fully loaded)
1324
1753
  // Show loading indicator if Claude is still initializing
1325
1754
  if (shellReady && bufferedOutput.length === 0) {
1326
1755
  process.stdout.write(chalk_1.default.gray(' Connecting to Claude...'));
@@ -1367,6 +1796,7 @@ async function run(options) {
1367
1796
  // Tracks idle→active transitions to print the session banner once per turn
1368
1797
  // ══════════════════════════════════════════════════════════════════════════
1369
1798
  let wasIdle = false; // Start as NOT idle — first idle→active fires after startup
1799
+ let livingDocsStarted = false; // Deferred until Claude's first idle prompt
1370
1800
  let turnCount = 0; // Incremented each time a new turn banner prints
1371
1801
  let lastBannerTime = Date.now(); // Grace period so startup output doesn't trigger banner
1372
1802
  // Stream tailer for mid-turn context capture (must be declared before polling code)
@@ -1802,9 +2232,21 @@ async function run(options) {
1802
2232
  }
1803
2233
  if (verbose) {
1804
2234
  // Show Claude version
1805
- const ccVersion = pinnedVersion || PINNED_CLAUDE_VERSION;
2235
+ const ccVersion = runtimeClaudeCodeVersion || pinnedVersion || PINNED_CLAUDE_VERSION;
1806
2236
  const versionStr = `Claude Code v${ccVersion}`;
1807
2237
  console.log(chalk_1.default.gray(` 🤖 ${versionStr}`));
2238
+ if (options.model) {
2239
+ console.log(chalk_1.default.gray(` 🧩 Model: ${buildClaudeLaunchModelArg(options.model, options.contextWindow)}`));
2240
+ }
2241
+ if (options.contextWindow && options.contextWindow !== 'auto') {
2242
+ console.log(chalk_1.default.gray(` 🪟 Context: ${options.contextWindow}`));
2243
+ }
2244
+ if (options.continueLast) {
2245
+ console.log(chalk_1.default.gray(' ↩ Continue: most recent Claude conversation'));
2246
+ }
2247
+ if (options.resumeSession) {
2248
+ console.log(chalk_1.default.gray(` ↪ Resume: ${options.resumeSession}`));
2249
+ }
1808
2250
  if (currentSession) {
1809
2251
  console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
1810
2252
  }
@@ -2324,6 +2766,20 @@ async function run(options) {
2324
2766
  // We check the raw outputBuffer (stripped) so the idle regex fires reliably.
2325
2767
  if (IDLE_PROMPT_REGEX.test(stripAnsi(outputBuffer))) {
2326
2768
  wasIdle = true;
2769
+ // Start living docs on first idle prompt — Claude is fully loaded, won't compete for I/O
2770
+ if (!livingDocsStarted && localLivingDocsManager) {
2771
+ livingDocsStarted = true;
2772
+ // Use setImmediate so the current data handler returns first
2773
+ setImmediate(() => {
2774
+ try {
2775
+ localLivingDocsManager.start();
2776
+ dlog('[LivingDocs:Local] Manager started (first idle prompt)');
2777
+ }
2778
+ catch (err) {
2779
+ dlog(`[LivingDocs:Local] Failed to start: ${err.message}`);
2780
+ }
2781
+ });
2782
+ }
2327
2783
  }
2328
2784
  // ══════════════════════════════════════════════════════════════════════════
2329
2785
  // ORPHAN TOOL_RESULT DETECTION (LOCAL MODE ONLY)
@@ -2711,6 +3167,7 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
2711
3167
  // Handle PTY exit
2712
3168
  shell.onExit(async ({ exitCode }) => {
2713
3169
  (0, state_1.clearAutoClearFlag)();
3170
+ localLivingDocsManager?.stop();
2714
3171
  stopStreamTailer(); // Stop stream capture
2715
3172
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
2716
3173
  cleanupInstanceFile(instanceId); // Clean up instance file
@@ -2730,6 +3187,7 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
2730
3187
  // Cleanup on exit signals
2731
3188
  const cleanup = () => {
2732
3189
  (0, state_1.clearAutoClearFlag)();
3190
+ localLivingDocsManager?.stop();
2733
3191
  stopStreamTailer(); // Stop stream capture
2734
3192
  (0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
2735
3193
  cleanupInstanceFile(instanceId); // Clean up instance file