@ekkos/cli 1.3.8 → 1.4.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.
@@ -86,6 +86,300 @@ function getConfig(options) {
86
86
  };
87
87
  /* eslint-enable no-restricted-syntax */
88
88
  }
89
+ const MAX_OUTPUT_1M_MODELS = '64000';
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 buildClaudeLaunchProfileId(model, contextWindow) {
153
+ const normalized = normalizeRequestedLaunchModel(model).model;
154
+ if (!normalized)
155
+ return undefined;
156
+ const lower = normalized.toLowerCase();
157
+ if (/^claude-opus-4-6(?:$|-)/.test(lower)) {
158
+ if (contextWindow === '200k')
159
+ return 'claude-opus-4-6-200k';
160
+ if (contextWindow === '1m')
161
+ return 'claude-opus-4-6-1m';
162
+ return undefined;
163
+ }
164
+ if (/^claude-sonnet-4-6(?:$|-)/.test(lower)) {
165
+ if (contextWindow === '200k')
166
+ return 'claude-sonnet-4-6-200k';
167
+ if (contextWindow === '1m')
168
+ return 'claude-sonnet-4-6-1m';
169
+ return undefined;
170
+ }
171
+ if (/^claude-opus-4-5(?:$|-)/.test(lower))
172
+ return 'claude-opus-4-5';
173
+ if (/^claude-sonnet-4-5(?:$|-)/.test(lower))
174
+ return 'claude-sonnet-4-5';
175
+ if (/^claude-haiku-4-5(?:$|-)/.test(lower))
176
+ return 'claude-haiku-4-5';
177
+ return normalized;
178
+ }
179
+ function resolveClaudeContextWindowSize(model, contextWindow) {
180
+ if (contextWindow === '200k')
181
+ return 200000;
182
+ if (contextWindow === '1m' && modelSupportsOneMillionContext(model))
183
+ return 1000000;
184
+ if (model && !modelSupportsOneMillionContext(model))
185
+ return 200000;
186
+ return undefined;
187
+ }
188
+ function getClaudeSessionMetadata(options) {
189
+ const normalizedContextWindow = normalizeContextWindowOption(options.contextWindow);
190
+ const selectedModel = typeof options.model === 'string' ? options.model : undefined;
191
+ const launchModel = buildClaudeLaunchModelArg(selectedModel, normalizedContextWindow);
192
+ const profileId = buildClaudeLaunchProfileId(selectedModel, normalizedContextWindow);
193
+ const contextSize = resolveClaudeContextWindowSize(selectedModel, normalizedContextWindow);
194
+ const maxOutputTokens = Number.parseInt(resolveClaudeMaxOutputTokens(selectedModel, normalizedContextWindow), 10);
195
+ return {
196
+ provider: 'claude',
197
+ claudeModel: normalizeRequestedLaunchModel(selectedModel).model,
198
+ claudeLaunchModel: launchModel,
199
+ claudeProfile: profileId,
200
+ claudeContextWindow: normalizedContextWindow,
201
+ claudeContextSize: contextSize,
202
+ claudeMaxOutputTokens: Number.isFinite(maxOutputTokens) ? maxOutputTokens : undefined,
203
+ claudeCodeVersion: runtimeClaudeCodeVersion || undefined,
204
+ dashboardEnabled: options.dashboard === true,
205
+ bypassEnabled: options.bypass === true,
206
+ };
207
+ }
208
+ function isTwoHundredKFixedOutputModel(model) {
209
+ const normalized = (model || '').trim().toLowerCase();
210
+ if (!normalized)
211
+ return false;
212
+ if (normalized === 'opus' || normalized === 'sonnet')
213
+ return true;
214
+ return /^claude-(?:opus|sonnet)-4-(?:5|6)(?:$|-)/.test(normalized);
215
+ }
216
+ function resolveClaudeMaxOutputTokens(model, contextWindow) {
217
+ if (process.env.EKKOS_MAX_OUTPUT_TOKENS) {
218
+ return process.env.EKKOS_MAX_OUTPUT_TOKENS;
219
+ }
220
+ if (contextWindow === '200k' && isTwoHundredKFixedOutputModel(model)) {
221
+ return MAX_OUTPUT_200K_OPUS_SONNET;
222
+ }
223
+ return MAX_OUTPUT_1M_MODELS;
224
+ }
225
+ function shouldOpenLaunchSelector(options) {
226
+ return options.model === true;
227
+ }
228
+ function renderClaudeLaunchSelectorIntro() {
229
+ const neonCyan = chalk_1.default.hex('#00f5ff');
230
+ const acidGreen = chalk_1.default.hex('#9dff00');
231
+ const signalAmber = chalk_1.default.hex('#ffb300');
232
+ const steel = chalk_1.default.hex('#8a92a6');
233
+ const width = 68;
234
+ const line = (text) => {
235
+ const pad = Math.max(0, width - stripAnsi(text).length);
236
+ return `${neonCyan('║')} ${text}${' '.repeat(pad)}${neonCyan('║')}`;
237
+ };
238
+ console.log('');
239
+ console.log(neonCyan('╔══════════════════════════════════════════════════════════════════════╗'));
240
+ console.log(line(chalk_1.default.white.bold('ekkOS.dev // PULSE')));
241
+ console.log(neonCyan('╠══════════════════════════════════════════════════════════════════════╣'));
242
+ console.log(line(`${acidGreen('200K')} ${steel('compat lane')} ${chalk_1.default.white('// Opus/Sonnet 4.5/4.6 => 32,768 output')}`));
243
+ console.log(line(`${signalAmber(' 1M')} ${steel('wide lane')} ${chalk_1.default.white('// Opus/Sonnet 4.6 => 64,000 output')}`));
244
+ console.log(line(steel('Pick a launch vector, then a fixed runtime profile. No hidden context prompt.')));
245
+ console.log(neonCyan('╚══════════════════════════════════════════════════════════════════════╝'));
246
+ console.log('');
247
+ }
248
+ function buildLaunchModeChoices() {
249
+ return [
250
+ {
251
+ name: `${chalk_1.default.hex('#00f5ff').bold('Fresh boot')} ${chalk_1.default.gray('// new shell')}`,
252
+ value: 'fresh',
253
+ },
254
+ {
255
+ name: `${chalk_1.default.hex('#9dff00').bold('Resume recent')} ${chalk_1.default.gray('// latest thread')}`,
256
+ value: 'continue',
257
+ },
258
+ {
259
+ name: `${chalk_1.default.hex('#ffb300').bold('Resume by ID')} ${chalk_1.default.gray('// targeted reconnect')}`,
260
+ value: 'resume',
261
+ },
262
+ ];
263
+ }
264
+ function buildLaunchModelChoices() {
265
+ const cyan = chalk_1.default.hex('#00f5ff');
266
+ const amber = chalk_1.default.hex('#ffb300');
267
+ const green = chalk_1.default.hex('#00ff88');
268
+ return [
269
+ {
270
+ name: `${cyan.bold('Auto route')} ${chalk_1.default.gray('// Claude decides profile')}`,
271
+ value: 'default',
272
+ },
273
+ {
274
+ name: `${cyan.bold('Claude Opus 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
275
+ value: 'claude-opus-4-5',
276
+ },
277
+ {
278
+ name: `${cyan.bold('Claude Opus 4.6')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
279
+ value: 'claude-opus-4-6-200k',
280
+ },
281
+ {
282
+ name: `${cyan.bold('Claude Opus 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 64,000')}`,
283
+ value: 'claude-opus-4-6-1m',
284
+ },
285
+ {
286
+ name: `${cyan.bold('Claude Sonnet 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
287
+ value: 'claude-sonnet-4-5',
288
+ },
289
+ {
290
+ name: `${cyan.bold('Claude Sonnet 4.6')} ${green('[200K]')} ${chalk_1.default.gray('// out 32,768')}`,
291
+ value: 'claude-sonnet-4-6-200k',
292
+ },
293
+ {
294
+ name: `${cyan.bold('Claude Sonnet 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 64,000')}`,
295
+ value: 'claude-sonnet-4-6-1m',
296
+ },
297
+ {
298
+ name: `${cyan.bold('Claude Haiku 4.5')} ${green('[200K]')} ${chalk_1.default.gray('// fast lane')}`,
299
+ value: 'claude-haiku-4-5',
300
+ },
301
+ ];
302
+ }
303
+ async function resolveClaudeLaunchSelection(options) {
304
+ const requestedModel = normalizeRequestedLaunchModel(typeof options.model === 'string' ? options.model : process.env.EKKOS_CLAUDE_MODEL);
305
+ let model = requestedModel.model;
306
+ let contextWindow = requestedModel.syntheticContextWindow
307
+ || normalizeContextWindowOption(options.contextWindow || process.env.EKKOS_CLAUDE_CONTEXT_WINDOW);
308
+ let continueLast = options.continueLast || false;
309
+ let resumeSession = (options.resumeSession || '').trim();
310
+ const shouldShowLaunchWindow = shouldOpenLaunchSelector(options)
311
+ && process.stdin.isTTY === true
312
+ && process.stdout.isTTY === true
313
+ && process.env.EKKOS_DISABLE_LAUNCH_WINDOW !== '1';
314
+ if (!shouldShowLaunchWindow) {
315
+ if (!modelSupportsOneMillionContext(model)) {
316
+ contextWindow = '200k';
317
+ }
318
+ return {
319
+ model,
320
+ contextWindow,
321
+ continueLast: !!continueLast && !resumeSession,
322
+ resumeSession: resumeSession || undefined,
323
+ };
324
+ }
325
+ const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
326
+ renderClaudeLaunchSelectorIntro();
327
+ const defaultLaunchMode = resumeSession ? 'resume' : continueLast ? 'continue' : 'fresh';
328
+ const firstPrompt = await inquirer.prompt([
329
+ {
330
+ type: 'list',
331
+ name: 'launchMode',
332
+ message: chalk_1.default.hex('#00f5ff').bold('>> Boot vector'),
333
+ default: defaultLaunchMode,
334
+ pageSize: 3,
335
+ loop: false,
336
+ choices: buildLaunchModeChoices(),
337
+ },
338
+ {
339
+ type: 'list',
340
+ name: 'model',
341
+ message: chalk_1.default.hex('#00f5ff').bold('>> Model lane'),
342
+ default: model || 'default',
343
+ pageSize: 8,
344
+ loop: false,
345
+ choices: buildLaunchModelChoices(),
346
+ },
347
+ ]);
348
+ const selectedProfile = normalizeRequestedLaunchModel(firstPrompt.model);
349
+ model = firstPrompt.model === 'default' ? undefined : selectedProfile.model;
350
+ continueLast = firstPrompt.launchMode === 'continue';
351
+ resumeSession = firstPrompt.launchMode === 'resume' ? resumeSession : '';
352
+ if (firstPrompt.launchMode === 'resume') {
353
+ const resumePrompt = await inquirer.prompt([
354
+ {
355
+ type: 'input',
356
+ name: 'resumeSession',
357
+ message: 'Claude session ID',
358
+ default: resumeSession,
359
+ validate: (value) => value.trim().length > 0 || 'Session ID is required',
360
+ },
361
+ ]);
362
+ resumeSession = resumePrompt.resumeSession.trim();
363
+ }
364
+ if (firstPrompt.model === 'default') {
365
+ contextWindow = 'auto';
366
+ }
367
+ else if (selectedProfile.syntheticContextWindow) {
368
+ contextWindow = selectedProfile.syntheticContextWindow;
369
+ }
370
+ else if (!modelSupportsOneMillionContext(model)) {
371
+ contextWindow = '200k';
372
+ }
373
+ else {
374
+ contextWindow = 'auto';
375
+ }
376
+ return {
377
+ model,
378
+ contextWindow,
379
+ continueLast,
380
+ resumeSession: resumeSession || undefined,
381
+ };
382
+ }
89
383
  // ═══════════════════════════════════════════════════════════════════════════
90
384
  // PATTERN MATCHING
91
385
  // ═══════════════════════════════════════════════════════════════════════════
@@ -291,13 +585,6 @@ const isWindows = os.platform() === 'win32';
291
585
  // Core ekkOS patches (eviction, context management) work with all recent versions
292
586
  // Cosmetic patches may fail on newer versions but don't affect functionality
293
587
  const PINNED_CLAUDE_VERSION = 'latest';
294
- // Max output tokens for Claude responses
295
- // Default: 128000 (Opus 4.6 / Sonnet 4.6 max output)
296
- // Override via EKKOS_MAX_OUTPUT_TOKENS env var if needed
297
- // Note: Claude Code has a known bug where CLAUDE_CODE_MAX_OUTPUT_TOKENS is ignored on Opus 4.6
298
- // (see github.com/anthropics/claude-code/issues/24159). We set it anyway + the proxy also
299
- // enforces this via max_tokens override on the API request.
300
- const EKKOS_MAX_OUTPUT_TOKENS = process.env.EKKOS_MAX_OUTPUT_TOKENS || '128000';
301
588
  const proxy_url_1 = require("../utils/proxy-url");
302
589
  // Track proxy mode for getEkkosEnv (set by run() based on options)
303
590
  let proxyModeEnabled = true;
@@ -318,12 +605,13 @@ let cliSessionName = null;
318
605
  let cliSessionId = null;
319
606
  /**
320
607
  * Get environment with ekkOS enhancements
321
- * - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS to 128K (API max for Opus/Sonnet 4.6)
608
+ * - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS dynamically for the selected Claude profile
322
609
  * - Routes API through ekkOS proxy for seamless context eviction (when enabled)
323
610
  * - Sets EKKOS_PROXY_MODE to signal JSONL rewriter to disable eviction
324
611
  * - Passes session headers for eviction/retrieval context tracking
325
612
  */
326
613
  function getEkkosEnv() {
614
+ const maxOutputTokens = resolveClaudeMaxOutputTokens(runtimeClaudeLaunchModel, runtimeClaudeContextWindow);
327
615
  /* eslint-disable no-restricted-syntax -- System env spreading, not API key access */
328
616
  const env = {
329
617
  ...process.env,
@@ -335,13 +623,18 @@ function getEkkosEnv() {
335
623
  // Native autocompact would compact without ekkOS saving state first, causing knowledge loss.
336
624
  // Only ekkOS-wrapped sessions get this; vanilla `claude` keeps autocompact on.
337
625
  DISABLE_AUTO_COMPACT: 'true',
338
- // Set max output tokens to 128K (Opus 4.6 / Sonnet 4.6 API max).
339
- // Note: Claude Code has a known bug where this env var is ignored on Opus 4.6
340
- // (github.com/anthropics/claude-code/issues/24159). The proxy also enforces
341
- // this by overriding max_tokens on the API request as a fallback.
342
- CLAUDE_CODE_MAX_OUTPUT_TOKENS: EKKOS_MAX_OUTPUT_TOKENS,
626
+ // Align Claude's advertised output ceiling with the chosen model/context profile.
627
+ // 1M Opus/Sonnet gets 128K; 200K Opus/Sonnet 4.5/4.6 is forced to 32,768.
628
+ // The proxy also enforces this server-side in case Claude Code ignores the env var.
629
+ CLAUDE_CODE_MAX_OUTPUT_TOKENS: maxOutputTokens,
343
630
  };
344
631
  /* eslint-enable no-restricted-syntax */
632
+ if (runtimeClaudeContextWindow === '200k') {
633
+ env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1';
634
+ }
635
+ else {
636
+ delete env.CLAUDE_CODE_DISABLE_1M_CONTEXT;
637
+ }
345
638
  // Check if proxy is disabled via env var or options
346
639
  // eslint-disable-next-line no-restricted-syntax -- Feature flag, not API key
347
640
  const proxyDisabled = process.env.EKKOS_DISABLE_PROXY === '1' || !proxyModeEnabled;
@@ -354,7 +647,9 @@ function getEkkosEnv() {
354
647
  if (!cliSessionName) {
355
648
  cliSessionId = crypto.randomUUID();
356
649
  cliSessionName = (0, state_1.uuidToWords)(cliSessionId);
357
- console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName}`));
650
+ if (process.env.EKKOS_NO_SPLASH !== '1') {
651
+ console.log(chalk_1.default.gray(` 📂 Session: ${cliSessionName}`));
652
+ }
358
653
  }
359
654
  // Get full userId from config (NOT the truncated version from auth token)
360
655
  // Config has full UUID like "d4532ba0-0a86-42ce-bab4-22aa62b55ce6"
@@ -374,7 +669,11 @@ function getEkkosEnv() {
374
669
  // CRITICAL: Embed user/session in URL path since ANTHROPIC_HEADERS doesn't work
375
670
  // Claude Code SDK doesn't forward custom headers, but it DOES use ANTHROPIC_BASE_URL
376
671
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
377
- const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId);
672
+ const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId, {
673
+ claudeCodeVersion: runtimeClaudeCodeVersion || undefined,
674
+ claudeProfile: buildClaudeLaunchProfileId(runtimeClaudeLaunchModel, runtimeClaudeContextWindow),
675
+ claudeContextWindow: runtimeClaudeContextWindow,
676
+ });
378
677
  env.ANTHROPIC_BASE_URL = proxyUrl;
379
678
  // Proxy URL contains userId + project path — don't leak to terminal
380
679
  }
@@ -585,6 +884,105 @@ function resolveGlobalClaudePath() {
585
884
  function sleep(ms) {
586
885
  return new Promise(resolve => setTimeout(resolve, ms));
587
886
  }
887
+ function formatPulseLaunchMode(options) {
888
+ if (options.resumeSession)
889
+ return `RESUME // ${options.resumeSession}`;
890
+ if (options.continueLast)
891
+ return 'CONTINUE // most recent Claude thread';
892
+ return 'FRESH // new interactive shell';
893
+ }
894
+ function formatPulseModel(model) {
895
+ const normalized = (model || '').trim().toLowerCase();
896
+ if (!normalized)
897
+ return 'Claude default';
898
+ if (normalized === 'opus')
899
+ return 'Opus alias';
900
+ if (normalized === 'sonnet')
901
+ return 'Sonnet alias';
902
+ if (normalized === 'claude-opus-4-5')
903
+ return 'Claude Opus 4.5';
904
+ if (normalized === 'claude-opus-4-6')
905
+ return 'Claude Opus 4.6';
906
+ if (normalized === 'claude-sonnet-4-5')
907
+ return 'Claude Sonnet 4.5';
908
+ if (normalized === 'claude-sonnet-4-6')
909
+ return 'Claude Sonnet 4.6';
910
+ if (normalized === 'claude-haiku-4-5')
911
+ return 'Claude Haiku 4.5';
912
+ return model || 'Claude default';
913
+ }
914
+ function formatPulseContextLine(contextWindow) {
915
+ if (contextWindow === '200k') {
916
+ return '200K // 200,000 token window // forced 32,768 output';
917
+ }
918
+ if (contextWindow === '1m') {
919
+ return '1M // 1,000,000 token window // safe 64,000 output cap';
920
+ }
921
+ return 'AUTO // ekkOS resolves 200K vs 1M from the selected profile';
922
+ }
923
+ async function showPulseLaunchLoader(options) {
924
+ const cyan = chalk_1.default.hex('#00f0ff');
925
+ const amber = chalk_1.default.hex('#ffb800');
926
+ const green = chalk_1.default.hex('#00ff88');
927
+ const steel = chalk_1.default.hex('#7a7a8e');
928
+ const panel = chalk_1.default.hex('#111118');
929
+ const wordmark = [
930
+ '██████╗ ██╗ ██╗██╗ ███████╗███████╗',
931
+ '██╔══██╗██║ ██║██║ ██╔════╝██╔════╝',
932
+ '██████╔╝██║ ██║██║ ███████╗█████╗ ',
933
+ '██╔═══╝ ██║ ██║██║ ╚════██║██╔══╝ ',
934
+ '██║ ╚██████╔╝███████╗███████║███████╗',
935
+ '╚═╝ ╚═════╝ ╚══════╝╚══════╝╚══════╝',
936
+ ];
937
+ const stages = [
938
+ 'arming pulse launcher',
939
+ 'syncing dashboard split',
940
+ 'routing claude session',
941
+ ];
942
+ const barWidth = 26;
943
+ const panelInnerWidth = 76;
944
+ const buildPanelLine = (labelColor, label, value) => {
945
+ const content = ` ${label.padEnd(7)} ${value}`;
946
+ const pad = Math.max(0, panelInnerWidth - stripAnsi(content).length);
947
+ return ` ${cyan('║')} ${labelColor(label.padEnd(7))} ${chalk_1.default.white(value)}${' '.repeat(pad)}${cyan('║')}`;
948
+ };
949
+ const headerContent = ' ekkOS.dev // PULSE BOOTSTRAP live launch matrix for Claude + dashboard';
950
+ const headerPad = Math.max(0, panelInnerWidth - headerContent.length);
951
+ console.log('');
952
+ for (const [idx, line] of wordmark.entries()) {
953
+ const color = idx % 2 === 0 ? cyan.bold : chalk_1.default.white.bold;
954
+ console.log(' ' + color(line));
955
+ await sleep(22);
956
+ }
957
+ console.log(' ' + amber('▔').repeat(54));
958
+ console.log(cyan(' ╔══════════════════════════════════════════════════════════════════════════════╗'));
959
+ console.log(` ${cyan('║')}${chalk_1.default.white.bold(' ekkOS.dev // PULSE BOOTSTRAP')}${steel(' live launch matrix for Claude + dashboard')}${' '.repeat(headerPad)}${cyan('║')}`);
960
+ console.log(buildPanelLine(green, 'MODE', formatPulseLaunchMode(options)));
961
+ console.log(buildPanelLine(green, 'MODEL', formatPulseModel(typeof options.model === 'string' ? options.model : undefined)));
962
+ console.log(buildPanelLine(amber, 'WINDOW', formatPulseContextLine(normalizeContextWindowOption(options.contextWindow))));
963
+ console.log(buildPanelLine(steel, 'PATH', 'selector -> dashboard -> claude -> proxy'));
964
+ console.log(cyan(' ╚══════════════════════════════════════════════════════════════════════════════╝'));
965
+ console.log(` ${green('200K')} ${steel('// Opus/Sonnet 4.5/4.6 => 32,768 output')}`);
966
+ console.log(` ${amber('1M ')} ${steel('// Opus/Sonnet 4.6 => 64,000 output')}`);
967
+ console.log('');
968
+ for (const stage of stages) {
969
+ for (let filled = 0; filled <= barWidth; filled += 1) {
970
+ const complete = '█'.repeat(filled);
971
+ const pending = '░'.repeat(barWidth - filled);
972
+ process.stdout.write(`\r ${amber('[')}${cyan(complete)}${steel(pending)}${amber(']')} ${chalk_1.default.white(stage)}`);
973
+ await sleep(16);
974
+ }
975
+ process.stdout.write('\n');
976
+ }
977
+ console.log(` ${green('READY')} ${panel('dashboard split armed')} ${steel('// selector handoff imminent')}`);
978
+ if (options.bypass) {
979
+ console.log(` ${amber('BYPASS')} ${chalk_1.default.white('permission prompts disabled for this launch')}`);
980
+ }
981
+ if (options.verbose) {
982
+ console.log(` ${steel('TRACE')} ${steel('verbose telemetry enabled')}`);
983
+ }
984
+ console.log('');
985
+ }
588
986
  /**
589
987
  * Show morning dreams with WOW factor right after sparkle animation.
590
988
  * Colorful, lively, readable — real magic moment. Then pause for Enter.
@@ -790,10 +1188,16 @@ function cleanupInstanceFile(instanceId) {
790
1188
  * Keeps ~/.ekkos/current-session.json, ~/.claude/state/current-session.json,
791
1189
  * and ~/.ekkos/session-hint.json in sync whenever the session is known.
792
1190
  */
793
- function writeSessionFiles(sessionId, sessionName) {
1191
+ function writeSessionFiles(sessionId, sessionName, metadata) {
794
1192
  try {
795
1193
  const ekkosDir = path.join(os.homedir(), '.ekkos');
796
1194
  const claudeStateDir = path.join(os.homedir(), '.claude', 'state');
1195
+ const launchMetadata = metadata || getClaudeSessionMetadata({
1196
+ model: runtimeClaudeLaunchModel,
1197
+ contextWindow: runtimeClaudeContextWindow,
1198
+ dashboard: false,
1199
+ bypass: false,
1200
+ });
797
1201
  // Ensure directories exist
798
1202
  if (!fs.existsSync(ekkosDir))
799
1203
  fs.mkdirSync(ekkosDir, { recursive: true });
@@ -801,16 +1205,17 @@ function writeSessionFiles(sessionId, sessionName) {
801
1205
  fs.mkdirSync(claudeStateDir, { recursive: true });
802
1206
  const now = new Date().toISOString();
803
1207
  // 1. ~/.ekkos/current-session.json
804
- fs.writeFileSync(path.join(ekkosDir, 'current-session.json'), JSON.stringify({ session_id: sessionId, session_name: sessionName, timestamp: now }, null, 2));
1208
+ fs.writeFileSync(path.join(ekkosDir, 'current-session.json'), JSON.stringify({ session_id: sessionId, session_name: sessionName, timestamp: now, ...launchMetadata }, null, 2));
805
1209
  // 2. ~/.claude/state/current-session.json
806
- fs.writeFileSync(path.join(claudeStateDir, 'current-session.json'), JSON.stringify({ session_id: sessionId, session_name: sessionName, timestamp: now }, null, 2));
1210
+ fs.writeFileSync(path.join(claudeStateDir, 'current-session.json'), JSON.stringify({ session_id: sessionId, session_name: sessionName, timestamp: now, ...launchMetadata }, null, 2));
807
1211
  // 3. ~/.ekkos/session-hint.json (dashboard discovery)
808
1212
  fs.writeFileSync(path.join(ekkosDir, 'session-hint.json'), JSON.stringify({
809
1213
  session_name: sessionName,
810
1214
  session_id: sessionId,
811
1215
  project_path: process.cwd(),
812
1216
  timestamp: now,
813
- pid: process.pid
1217
+ pid: process.pid,
1218
+ ...launchMetadata,
814
1219
  }, null, 2));
815
1220
  }
816
1221
  catch {
@@ -844,12 +1249,21 @@ function launchWithDashboard(options) {
844
1249
  runArgs.push('--skip-inject');
845
1250
  if (options.noProxy)
846
1251
  runArgs.push('--skip-proxy');
1252
+ if (options.continueLast)
1253
+ runArgs.push('--continue-last');
1254
+ if (options.resumeSession)
1255
+ runArgs.push('--resume-session', options.resumeSession);
1256
+ if (typeof options.model === 'string')
1257
+ runArgs.push('--model', options.model);
1258
+ if (options.contextWindow && options.contextWindow !== 'auto') {
1259
+ runArgs.push('--context-window', options.contextWindow);
1260
+ }
847
1261
  const ekkosCmd = process.argv[1]; // Path to ekkos CLI
848
1262
  const cwd = process.cwd();
849
1263
  const termCols = process.stdout.columns ?? 160;
850
1264
  const termRows = process.stdout.rows ?? 48;
851
1265
  // Write session hint immediately so dashboard can find JSONL as it appears
852
- writeSessionFiles(crypto.randomUUID(), sessionName);
1266
+ writeSessionFiles(crypto.randomUUID(), sessionName, getClaudeSessionMetadata(options));
853
1267
  const runCommand = `EKKOS_NO_SPLASH=1 node "${ekkosCmd}" ${runArgs.join(' ')}`;
854
1268
  // Pass session name directly — dashboard starts rendering immediately (lazy JSONL resolution)
855
1269
  const dashCommand = `node "${ekkosCmd}" dashboard "${sessionName}" --refresh 2000`;
@@ -913,7 +1327,7 @@ function launchWithWindowsTerminal(options) {
913
1327
  // Pre-generate session name so dashboard can start immediately
914
1328
  const sessionName = options.session || (0, state_1.uuidToWords)(crypto.randomUUID());
915
1329
  // Write session hint immediately
916
- writeSessionFiles(crypto.randomUUID(), sessionName);
1330
+ writeSessionFiles(crypto.randomUUID(), sessionName, getClaudeSessionMetadata(options));
917
1331
  // Build ekkos run args WITHOUT --dashboard (prevent recursion)
918
1332
  // Always pass -s so run reuses the same session name we gave the dashboard
919
1333
  const runArgs = ['run', '-s', sessionName];
@@ -929,6 +1343,15 @@ function launchWithWindowsTerminal(options) {
929
1343
  runArgs.push('--skip-inject');
930
1344
  if (options.noProxy)
931
1345
  runArgs.push('--skip-proxy');
1346
+ if (options.continueLast)
1347
+ runArgs.push('--continue-last');
1348
+ if (options.resumeSession)
1349
+ runArgs.push('--resume-session', options.resumeSession);
1350
+ if (typeof options.model === 'string')
1351
+ runArgs.push('--model', options.model);
1352
+ if (options.contextWindow && options.contextWindow !== 'auto') {
1353
+ runArgs.push('--context-window', options.contextWindow);
1354
+ }
932
1355
  // Write a temp batch file to avoid all quoting issues
933
1356
  // wt.exe doesn't resolve PATH for npm global bins — must use `cmd /c`
934
1357
  const batPath = path.join(os.tmpdir(), `ekkos-wt-${Date.now()}.cmd`);
@@ -976,6 +1399,14 @@ async function run(options) {
976
1399
  const verbose = options.verbose || false;
977
1400
  const bypass = options.bypass || false;
978
1401
  const noInject = options.noInject || false;
1402
+ const suppressPreClaudeOutput = process.env.EKKOS_NO_SPLASH === '1';
1403
+ const launchSelection = await resolveClaudeLaunchSelection(options);
1404
+ options.model = launchSelection.model;
1405
+ options.contextWindow = launchSelection.contextWindow;
1406
+ options.continueLast = launchSelection.continueLast;
1407
+ options.resumeSession = launchSelection.resumeSession;
1408
+ runtimeClaudeLaunchModel = launchSelection.model;
1409
+ runtimeClaudeContextWindow = launchSelection.contextWindow;
979
1410
  // Honour -s flag: lock the session name before getEkkosEnv() can mint a new one.
980
1411
  // This is critical for --dashboard mode where launchWithDashboard() pre-generates
981
1412
  // a session name and passes it via -s so the dashboard and run command agree.
@@ -986,11 +1417,28 @@ async function run(options) {
986
1417
  // Set proxy mode based on options (used by getEkkosEnv)
987
1418
  proxyModeEnabled = !(options.noProxy || false);
988
1419
  if (proxyModeEnabled) {
989
- console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
1420
+ if (!suppressPreClaudeOutput && !options.pulse) {
1421
+ console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
1422
+ }
990
1423
  }
991
- else if (verbose) {
1424
+ else if (verbose && !suppressPreClaudeOutput) {
992
1425
  console.log(chalk_1.default.yellow(' ⏭️ API proxy disabled (--no-proxy)'));
993
1426
  }
1427
+ let pulseIntroShown = false;
1428
+ if (options.pulse
1429
+ && process.env.EKKOS_REMOTE_SESSION !== '1'
1430
+ && process.env.EKKOS_NO_SPLASH !== '1'
1431
+ && process.stdout.isTTY === true) {
1432
+ await showPulseLaunchLoader({
1433
+ model: options.model,
1434
+ contextWindow: options.contextWindow,
1435
+ continueLast: options.continueLast,
1436
+ resumeSession: options.resumeSession,
1437
+ bypass: options.bypass,
1438
+ verbose: options.verbose,
1439
+ });
1440
+ pulseIntroShown = true;
1441
+ }
994
1442
  // ══════════════════════════════════════════════════════════════════════════
995
1443
  // DASHBOARD MODE: Launch via tmux (Unix) or Windows Terminal split pane
996
1444
  // ══════════════════════════════════════════════════════════════════════════
@@ -1079,11 +1527,22 @@ async function run(options) {
1079
1527
  const isNpxMode = rawClaudePath.startsWith('npx:');
1080
1528
  const pinnedVersion = isNpxMode ? rawClaudePath.split(':')[1] : null;
1081
1529
  const claudePath = isNpxMode ? 'npx' : rawClaudePath;
1530
+ runtimeClaudeCodeVersion = pinnedVersion || getClaudeVersion(claudePath);
1082
1531
  // Build args early
1083
1532
  const earlyArgs = [];
1084
1533
  if (isNpxMode) {
1085
1534
  earlyArgs.push(`@anthropic-ai/claude-code@${pinnedVersion}`);
1086
1535
  }
1536
+ if (options.resumeSession) {
1537
+ earlyArgs.push('--resume', options.resumeSession);
1538
+ }
1539
+ else if (options.continueLast) {
1540
+ earlyArgs.push('--continue');
1541
+ }
1542
+ const launchModelArg = buildClaudeLaunchModelArg(options.model, options.contextWindow);
1543
+ if (launchModelArg) {
1544
+ earlyArgs.push('--model', launchModelArg);
1545
+ }
1087
1546
  if (bypass) {
1088
1547
  earlyArgs.push('--dangerously-skip-permissions');
1089
1548
  }
@@ -1146,7 +1605,7 @@ async function run(options) {
1146
1605
  // ══════════════════════════════════════════════════════════════════════════
1147
1606
  // STARTUP BANNER WITH COLOR PULSE ANIMATION
1148
1607
  // ══════════════════════════════════════════════════════════════════════════
1149
- const skipFancyIntro = process.env.EKKOS_REMOTE_SESSION === '1' || process.env.EKKOS_NO_SPLASH === '1' || !!process.env.TMUX;
1608
+ const skipFancyIntro = pulseIntroShown || process.env.EKKOS_REMOTE_SESSION === '1' || suppressPreClaudeOutput || !!process.env.TMUX;
1150
1609
  if (!skipFancyIntro) {
1151
1610
  const logoLines = [
1152
1611
  ' ▄▄▄▄▄ ▄▄▄▄▄▄▄ ▄▄▄ ▄▄ ▄▄',
@@ -1328,7 +1787,7 @@ async function run(options) {
1328
1787
  }
1329
1788
  console.log('');
1330
1789
  }
1331
- else {
1790
+ else if (!suppressPreClaudeOutput) {
1332
1791
  // Static banner for Windows / remote / no-splash — no cursor manipulation
1333
1792
  console.log('');
1334
1793
  console.log(chalk_1.default.hex('#FF6B35').bold(' ekkOS_Pulse') + chalk_1.default.gray(' — Context is finite. Intelligence isn\'t.'));
@@ -1344,7 +1803,9 @@ async function run(options) {
1344
1803
  // ══════════════════════════════════════════════════════════════════════════
1345
1804
  // MAGIC MOMENT: Morning dreams right after sparkle, before Claude appears
1346
1805
  // ══════════════════════════════════════════════════════════════════════════
1347
- await showMorningDreamsIfNeeded();
1806
+ if (!suppressPreClaudeOutput && !options.pulse) {
1807
+ await showMorningDreamsIfNeeded();
1808
+ }
1348
1809
  // ══════════════════════════════════════════════════════════════════════════
1349
1810
  // ANIMATION COMPLETE: Mark ready and flush buffered Claude output
1350
1811
  // ══════════════════════════════════════════════════════════════════════════
@@ -1381,8 +1842,8 @@ async function run(options) {
1381
1842
  // ════════════════════════════════════════════════════════════════════════════
1382
1843
  const initialSessionId = cliSessionId || 'pending';
1383
1844
  const initialSessionName = currentSession || 'initializing';
1384
- (0, state_1.registerActiveSession)(initialSessionId, initialSessionName, process.cwd());
1385
- writeSessionFiles(initialSessionId, initialSessionName);
1845
+ (0, state_1.registerActiveSession)(initialSessionId, initialSessionName, process.cwd(), getClaudeSessionMetadata(options));
1846
+ writeSessionFiles(initialSessionId, initialSessionName, getClaudeSessionMetadata(options));
1386
1847
  dlog(`Registered active session (PID ${process.pid})`);
1387
1848
  // Show active sessions count if verbose
1388
1849
  if (verbose) {
@@ -1549,9 +2010,9 @@ async function run(options) {
1549
2010
  // and shown in "Continuum Loaded". Re-deriving from JSONL UUID produces a
1550
2011
  // different name since Claude Code's UUID ≠ the CLI-generated UUID.
1551
2012
  currentSession = cliSessionName || (0, state_1.uuidToWords)(sessionId);
1552
- (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
2013
+ (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession, getClaudeSessionMetadata(options));
1553
2014
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
1554
- writeSessionFiles(currentSessionId, currentSession);
2015
+ writeSessionFiles(currentSessionId, currentSession, getClaudeSessionMetadata(options));
1555
2016
  bindRealSessionToProxy(currentSession, 'fast-transcript', currentSessionId);
1556
2017
  dlog(`[TRANSCRIPT] FAST DETECT: New transcript found! ${fullPath}`);
1557
2018
  evictionDebugLog('TRANSCRIPT_SET', 'Fast poll detected new file', {
@@ -1835,9 +2296,21 @@ async function run(options) {
1835
2296
  }
1836
2297
  if (verbose) {
1837
2298
  // Show Claude version
1838
- const ccVersion = pinnedVersion || PINNED_CLAUDE_VERSION;
2299
+ const ccVersion = runtimeClaudeCodeVersion || pinnedVersion || PINNED_CLAUDE_VERSION;
1839
2300
  const versionStr = `Claude Code v${ccVersion}`;
1840
2301
  console.log(chalk_1.default.gray(` 🤖 ${versionStr}`));
2302
+ if (options.model) {
2303
+ console.log(chalk_1.default.gray(` 🧩 Model: ${buildClaudeLaunchModelArg(options.model, options.contextWindow)}`));
2304
+ }
2305
+ if (options.contextWindow && options.contextWindow !== 'auto') {
2306
+ console.log(chalk_1.default.gray(` 🪟 Context: ${options.contextWindow}`));
2307
+ }
2308
+ if (options.continueLast) {
2309
+ console.log(chalk_1.default.gray(' ↩ Continue: most recent Claude conversation'));
2310
+ }
2311
+ if (options.resumeSession) {
2312
+ console.log(chalk_1.default.gray(` ↪ Resume: ${options.resumeSession}`));
2313
+ }
1841
2314
  if (currentSession) {
1842
2315
  console.log(chalk_1.default.green(` 📍 Session: ${currentSession}`));
1843
2316
  }
@@ -2433,10 +2906,10 @@ async function run(options) {
2433
2906
  // Keep cliSessionName if set (proxy mode) — JSONL UUID differs from CLI UUID
2434
2907
  currentSession = cliSessionName || (0, state_1.uuidToWords)(currentSessionId);
2435
2908
  // Update THIS process's session entry (not global state.json)
2436
- (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
2909
+ (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession, getClaudeSessionMetadata(options));
2437
2910
  // Also update global state for backwards compatibility
2438
2911
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
2439
- writeSessionFiles(currentSessionId, currentSession);
2912
+ writeSessionFiles(currentSessionId, currentSession, getClaudeSessionMetadata(options));
2440
2913
  dlog(`Session detected from UUID: ${currentSession}`);
2441
2914
  resolveTranscriptFromSessionId('session-id-from-output');
2442
2915
  bindRealSessionToProxy(currentSession, 'session-id-from-output', currentSessionId || undefined);
@@ -2469,10 +2942,10 @@ async function run(options) {
2469
2942
  currentSession = lastSeenSessionName;
2470
2943
  observedSessionThisRun = true; // Mark that we've seen a session in THIS process
2471
2944
  // Update THIS process's session entry (not global state.json)
2472
- (0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
2945
+ (0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession, getClaudeSessionMetadata(options));
2473
2946
  // Also update global state for backwards compatibility
2474
2947
  (0, state_1.updateState)({ sessionName: currentSession });
2475
- writeSessionFiles(currentSessionId || 'unknown', currentSession);
2948
+ writeSessionFiles(currentSessionId || 'unknown', currentSession, getClaudeSessionMetadata(options));
2476
2949
  dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
2477
2950
  bindRealSessionToProxy(currentSession, 'status-line', currentSessionId || undefined);
2478
2951
  resolveTranscriptFromSessionId('status-line');