@ekkos/cli 1.3.9 → 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.
@@ -12,6 +12,7 @@ export interface GeminiOptions {
12
12
  verbose?: boolean;
13
13
  noProxy?: boolean;
14
14
  session?: string;
15
+ dashboard?: boolean;
15
16
  }
16
17
  /**
17
18
  * Launch Gemini CLI with ekkOS proxy integration.
@@ -62,6 +62,8 @@ const isWindows = process.platform === 'win32';
62
62
  const PULSE_LOADED_TEXT = ' 🧠 ekkOS_Pulse Loaded!';
63
63
  const PULSE_SHINE_FRAME_MS = 50;
64
64
  const PULSE_SHINE_SWEEPS = 2;
65
+ const GEMINI_DEFAULT_MODEL = 'gemini-3.1-flash-lite-preview';
66
+ const GEMINI_MAX_OUTPUT_TOKENS = 65536;
65
67
  function sleep(ms) {
66
68
  return new Promise(resolve => setTimeout(resolve, ms));
67
69
  }
@@ -121,6 +123,159 @@ function resolveGeminiPath() {
121
123
  // 3. Ultimate fallback — let spawn resolve it (will error with helpful message)
122
124
  return 'gemini';
123
125
  }
126
+ function extractGeminiCliArgs() {
127
+ const geminiIdx = process.argv.indexOf('gemini');
128
+ if (geminiIdx === -1)
129
+ return [];
130
+ const rawArgs = process.argv.slice(geminiIdx + 1);
131
+ const filtered = [];
132
+ for (let i = 0; i < rawArgs.length; i += 1) {
133
+ const arg = rawArgs[i];
134
+ if (!arg)
135
+ continue;
136
+ if (arg === '--skip-proxy' || arg === '-v' || arg === '--verbose' || arg === '--dashboard') {
137
+ continue;
138
+ }
139
+ if (arg === '-s' || arg === '--session') {
140
+ i += 1;
141
+ continue;
142
+ }
143
+ filtered.push(arg);
144
+ }
145
+ return filtered;
146
+ }
147
+ function resolveGeminiLaunchModel(args) {
148
+ for (let i = 0; i < args.length; i += 1) {
149
+ const arg = args[i];
150
+ if (!arg)
151
+ continue;
152
+ if ((arg === '-m' || arg === '--model') && typeof args[i + 1] === 'string') {
153
+ return args[i + 1];
154
+ }
155
+ if (arg.startsWith('--model=')) {
156
+ const value = arg.slice('--model='.length).trim();
157
+ if (value)
158
+ return value;
159
+ }
160
+ }
161
+ const envModel = (process.env.GEMINI_MODEL || process.env.GOOGLE_GEMINI_MODEL || '').trim();
162
+ return envModel || GEMINI_DEFAULT_MODEL;
163
+ }
164
+ function resolveGeminiContextSize(model) {
165
+ const normalized = (model || '').trim().toLowerCase();
166
+ if (normalized.startsWith('gemini-3.1-pro') || normalized.startsWith('gemini-3-pro')) {
167
+ return 2097152;
168
+ }
169
+ return 1048576;
170
+ }
171
+ function resolveGeminiProjectId(projectPath) {
172
+ try {
173
+ const projectsPath = path.join(os.homedir(), '.gemini', 'projects.json');
174
+ if (!fs.existsSync(projectsPath))
175
+ return undefined;
176
+ const parsed = JSON.parse(fs.readFileSync(projectsPath, 'utf-8'));
177
+ const projectId = parsed?.projects?.[projectPath];
178
+ return typeof projectId === 'string' && projectId.length > 0 ? projectId : undefined;
179
+ }
180
+ catch {
181
+ return undefined;
182
+ }
183
+ }
184
+ function getGeminiSessionMetadata(options, args) {
185
+ const model = resolveGeminiLaunchModel(args);
186
+ return {
187
+ provider: 'gemini',
188
+ claudeModel: model,
189
+ claudeLaunchModel: model,
190
+ claudeContextWindow: '1m',
191
+ claudeContextSize: resolveGeminiContextSize(model),
192
+ claudeMaxOutputTokens: GEMINI_MAX_OUTPUT_TOKENS,
193
+ geminiProjectId: resolveGeminiProjectId(process.cwd()),
194
+ dashboardEnabled: options.dashboard === true,
195
+ };
196
+ }
197
+ function writeGeminiSessionFiles(sessionId, sessionName, metadata) {
198
+ try {
199
+ const ekkosDir = path.join(os.homedir(), '.ekkos');
200
+ if (!fs.existsSync(ekkosDir))
201
+ fs.mkdirSync(ekkosDir, { recursive: true });
202
+ const now = new Date().toISOString();
203
+ fs.writeFileSync(path.join(ekkosDir, 'current-session.json'), JSON.stringify({ session_id: sessionId, session_name: sessionName, timestamp: now, ...metadata }, null, 2));
204
+ fs.writeFileSync(path.join(ekkosDir, 'session-hint.json'), JSON.stringify({
205
+ session_id: sessionId,
206
+ session_name: sessionName,
207
+ project_path: process.cwd(),
208
+ timestamp: now,
209
+ pid: process.pid,
210
+ ...metadata,
211
+ }, null, 2));
212
+ }
213
+ catch {
214
+ // Non-fatal
215
+ }
216
+ }
217
+ function shellQuote(value) {
218
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
219
+ }
220
+ function launchGeminiWithDashboard(options, extraArgs) {
221
+ (0, state_1.getActiveSessions)();
222
+ const tmuxSession = `ekkos-gemini-${Date.now().toString(36)}`;
223
+ const sessionName = options.session || (0, state_1.uuidToWords)(crypto.randomUUID());
224
+ const sessionId = crypto.randomUUID();
225
+ options.session = sessionName;
226
+ cliSessionName = sessionName;
227
+ cliSessionId = sessionId;
228
+ const metadata = getGeminiSessionMetadata({ ...options, session: sessionName, dashboard: true }, extraArgs);
229
+ const ekkosCmd = process.argv[1];
230
+ const cwd = process.cwd();
231
+ const termCols = process.stdout.columns ?? 160;
232
+ const termRows = process.stdout.rows ?? 48;
233
+ writeGeminiSessionFiles(sessionId, sessionName, metadata);
234
+ const geminiArgs = ['gemini', '-s', sessionName];
235
+ if (options.verbose)
236
+ geminiArgs.push('-v');
237
+ if (options.noProxy)
238
+ geminiArgs.push('--skip-proxy');
239
+ geminiArgs.push(...extraArgs);
240
+ const runCommand = `EKKOS_NO_SPLASH=1 node ${shellQuote(ekkosCmd)} ${geminiArgs.map(shellQuote).join(' ')}`;
241
+ const dashCommand = `node ${shellQuote(ekkosCmd)} dashboard ${shellQuote(sessionName)} --provider gemini --refresh 2000`;
242
+ try {
243
+ (0, child_process_1.execSync)(`tmux new-session -d -s "${tmuxSession}" -x ${termCols} -y ${termRows} -n "gemini" 'sleep 86400'`, { stdio: 'pipe' });
244
+ const applyTmuxOpt = (cmd) => {
245
+ try {
246
+ (0, child_process_1.execSync)(`tmux ${cmd}`, { stdio: 'pipe' });
247
+ }
248
+ catch (err) {
249
+ if (options.verbose) {
250
+ console.log(chalk_1.default.gray(` tmux option skipped: ${cmd} (${err.message})`));
251
+ }
252
+ }
253
+ };
254
+ applyTmuxOpt(`set-option -t "${tmuxSession}" default-terminal "xterm-256color"`);
255
+ applyTmuxOpt(`set-option -sa -t "${tmuxSession}" terminal-overrides ",xterm-256color:Tc:smcup@:rmcup@"`);
256
+ applyTmuxOpt(`set-option -t "${tmuxSession}" mouse on`);
257
+ const detachCleanupHook = `if-shell -F '#{==:#{session_attached},0}' 'kill-session -t ${tmuxSession}'`;
258
+ applyTmuxOpt(`set-hook -t "${tmuxSession}" client-detached "${detachCleanupHook}"`);
259
+ applyTmuxOpt(`set-window-option -t "${tmuxSession}" history-limit 100000`);
260
+ applyTmuxOpt(`set-window-option -t "${tmuxSession}" mode-keys vi`);
261
+ applyTmuxOpt(`set-window-option -t "${tmuxSession}:gemini" synchronize-panes off`);
262
+ applyTmuxOpt(`set-window-option -t "${tmuxSession}:gemini" window-size latest`);
263
+ applyTmuxOpt(`set-window-option -t "${tmuxSession}:gemini" aggressive-resize on`);
264
+ applyTmuxOpt(`set-option -t "${tmuxSession}" remain-on-exit off`);
265
+ applyTmuxOpt(`set-option -t "${tmuxSession}" escape-time 0`);
266
+ (0, child_process_1.execSync)(`tmux split-window -t "${tmuxSession}:gemini" -h -p 40 -c "${cwd}" ${shellQuote(dashCommand)}`, { stdio: 'pipe' });
267
+ (0, child_process_1.execSync)(`tmux respawn-pane -k -t "${tmuxSession}:gemini.0" ${shellQuote(runCommand)}`, { stdio: 'pipe' });
268
+ (0, child_process_1.execSync)(`tmux select-pane -t "${tmuxSession}:gemini.0"`, { stdio: 'pipe' });
269
+ (0, child_process_1.execSync)(`tmux attach -t "${tmuxSession}"`, { stdio: 'inherit' });
270
+ return true;
271
+ }
272
+ catch (err) {
273
+ console.error(chalk_1.default.red(`\n ✘ Failed to launch Gemini dashboard split: ${err.message}\n`));
274
+ console.log(chalk_1.default.gray(' Falling back to normal Gemini mode.'));
275
+ console.log(chalk_1.default.gray(' Run "ekkos dashboard --latest --provider gemini" in another terminal.\n'));
276
+ return false;
277
+ }
278
+ }
124
279
  /**
125
280
  * Build environment for Gemini CLI with proxy routing.
126
281
  */
@@ -138,14 +293,20 @@ function buildGeminiEnv(options) {
138
293
  // Resolve userId from config or auth token
139
294
  const ekkosConfig = (0, state_1.getConfig)();
140
295
  let userId = ekkosConfig?.userId || 'anonymous';
296
+ const authToken = (0, state_1.getAuthToken)();
141
297
  if (userId === 'anonymous') {
142
- const authToken = (0, state_1.getAuthToken)();
143
298
  if (authToken?.startsWith('ekk_')) {
144
299
  const parts = authToken.split('_');
145
300
  if (parts.length >= 2 && parts[1])
146
301
  userId = parts[1];
147
302
  }
148
303
  }
304
+ // Gemini CLI strictly requires an API key to exist in the environment,
305
+ // even when routing to a proxy. If they don't have GEMINI_API_KEY set,
306
+ // provide their GOOGLE_AI_API_KEY, ekkOS auth token, or a placeholder so the CLI boots up.
307
+ if (!env.GEMINI_API_KEY) {
308
+ env.GEMINI_API_KEY = env.GOOGLE_AI_API_KEY || authToken || 'ekk_guest_token_for_proxy';
309
+ }
149
310
  // Build proxy URL — no query params (SDK concatenates baseUrl + path as strings).
150
311
  // Format: /gproxy/{userId}/{session}/{sid}/{project64}
151
312
  // Proxy extracts params from path, then routes /v1beta or /v1beta1 to googleRouter.
@@ -164,6 +325,11 @@ function buildGeminiEnv(options) {
164
325
  */
165
326
  async function gemini(options = {}) {
166
327
  (0, state_1.ensureEkkosDir)();
328
+ const extraArgs = extractGeminiCliArgs();
329
+ if (options.dashboard) {
330
+ if (launchGeminiWithDashboard(options, extraArgs))
331
+ return;
332
+ }
167
333
  console.log('');
168
334
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_') + chalk_1.default.gray(' + ') + chalk_1.default.blue('Gemini CLI'));
169
335
  // Resolve binary
@@ -176,7 +342,9 @@ async function gemini(options = {}) {
176
342
  // Register session for multi-session awareness
177
343
  const sessionId = cliSessionId || crypto.randomUUID();
178
344
  const sessionName = cliSessionName || 'gemini-session';
179
- (0, state_1.registerActiveSession)(sessionId, sessionName, process.cwd());
345
+ const metadata = getGeminiSessionMetadata(options, extraArgs);
346
+ (0, state_1.registerActiveSession)(sessionId, sessionName, process.cwd(), metadata);
347
+ writeGeminiSessionFiles(sessionId, sessionName, metadata);
180
348
  if (!options.noProxy) {
181
349
  await showPulseLoadedBanner();
182
350
  }
@@ -184,14 +352,6 @@ async function gemini(options = {}) {
184
352
  // Extract any trailing arguments meant for the inner CLI
185
353
  // We look for 'gemini' in process.argv and pass everything after it.
186
354
  // If the user did `ekkos gemini -m gemini-3.1-pro-preview`, we want to pass `-m gemini-3.1-pro-preview`
187
- let extraArgs = [];
188
- const geminiIdx = process.argv.indexOf('gemini');
189
- if (geminiIdx !== -1) {
190
- extraArgs = process.argv.slice(geminiIdx + 1).filter(a => {
191
- // Filter out ekkos wrapper options
192
- return !['--skip-proxy', '-v', '--verbose'].includes(a) && !(a === '-s' || a === '--session' || process.argv[process.argv.indexOf(a) - 1] === '-s' || process.argv[process.argv.indexOf(a) - 1] === '--session');
193
- });
194
- }
195
355
  // Spawn Gemini CLI — stdio: inherit for full terminal passthrough
196
356
  const child = (0, child_process_1.spawn)(geminiPath, extraArgs, {
197
357
  stdio: 'inherit',
@@ -0,0 +1,6 @@
1
+ export interface InitLivingDocsOptions {
2
+ path?: string;
3
+ timeZone?: string;
4
+ noSeed?: boolean;
5
+ }
6
+ export declare function initLivingDocs(options: InitLivingDocsOptions): Promise<void>;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.initLivingDocs = initLivingDocs;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const path_1 = require("path");
9
+ const living_docs_manager_js_1 = require("../local/living-docs-manager.js");
10
+ const state_1 = require("../utils/state");
11
+ const platform_js_1 = require("../utils/platform.js");
12
+ async function initLivingDocs(options) {
13
+ const targetPath = (0, path_1.resolve)(options.path || process.cwd());
14
+ const timeZone = options.timeZone || process.env.EKKOS_USER_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
15
+ const apiKey = options.noSeed ? null : (0, state_1.getAuthToken)();
16
+ const apiUrl = options.noSeed ? undefined : (process.env.EKKOS_API_URL || platform_js_1.MCP_API_URL);
17
+ console.log('\n ' + chalk_1.default.cyan.bold('Initialize ekkOS Cortex'));
18
+ console.log(chalk_1.default.gray(' ─────────────────────────────────'));
19
+ console.log(chalk_1.default.gray(` Path: ${targetPath}`));
20
+ const manager = new living_docs_manager_js_1.LocalLivingDocsManager({
21
+ targetPath,
22
+ apiUrl,
23
+ apiKey,
24
+ timeZone,
25
+ onLog: message => console.log(chalk_1.default.gray(` ${message}`)),
26
+ });
27
+ // Force a full scan and compile by ignoring the cooldown
28
+ // We'll use a slightly different flow than .start() to make it one-shot
29
+ console.log(chalk_1.default.gray(' Scanning repository structure...'));
30
+ // We hack the cooldown by temporarily setting the env var if we could,
31
+ // but better to just call the internal methods if they were public.
32
+ // Since they are private, we'll use a small trick:
33
+ // The start() method is what we want, but we want it to exit after the first pass.
34
+ // Actually, let's just use start() and wait for the initialCompilePromise
35
+ manager.start();
36
+ // Give it a moment to start the initial pass
37
+ const checkInterval = setInterval(() => {
38
+ // @ts-ignore - reaching into private for the promise
39
+ if (manager.initialCompilePromise) {
40
+ // @ts-ignore
41
+ manager.initialCompilePromise.then(() => {
42
+ clearInterval(checkInterval);
43
+ manager.stop();
44
+ console.log('\n' + chalk_1.default.green(' ✓ ekkOS Cortex initialized successfully!'));
45
+ console.log(chalk_1.default.gray(' Your project now has ekkOS_CONTEXT.md files for every discovered system.'));
46
+ console.log(chalk_1.default.gray(' Run `ekkos cortex watch` to keep them updated while you work.'));
47
+ console.log('');
48
+ process.exit(0);
49
+ });
50
+ }
51
+ }, 100);
52
+ // Safety timeout
53
+ setTimeout(() => {
54
+ console.log(chalk_1.default.red('\n Initialization timed out.'));
55
+ process.exit(1);
56
+ }, 60000);
57
+ }
@@ -11,13 +11,13 @@ const state_1 = require("../utils/state");
11
11
  const platform_js_1 = require("../utils/platform.js");
12
12
  function printStartupSummary(options) {
13
13
  console.log('');
14
- console.log(chalk_1.default.cyan.bold(' ekkOS Living Docs Watch'));
15
- console.log(chalk_1.default.gray(' ─────────────────────────'));
14
+ console.log(chalk_1.default.cyan.bold(' ekkOS Cortex Watcher'));
15
+ console.log(chalk_1.default.gray(' ──────────────────────'));
16
16
  console.log(chalk_1.default.gray(` Path: ${options.targetPath}`));
17
17
  console.log(chalk_1.default.gray(` Timezone: ${options.timeZone}`));
18
18
  console.log(chalk_1.default.gray(` Registry seed: ${options.seedingEnabled ? 'enabled' : 'disabled'}`));
19
19
  console.log('');
20
- console.log(chalk_1.default.gray(' Watching local files and rewriting ekkOS_CONTEXT.md on change.'));
20
+ console.log(chalk_1.default.gray(' Watching local files and updating Cortex docs (ekkOS_CONTEXT.md) on change.'));
21
21
  console.log(chalk_1.default.gray(' Press Ctrl+C to stop.'));
22
22
  console.log('');
23
23
  }
@@ -86,7 +86,7 @@ function getConfig(options) {
86
86
  };
87
87
  /* eslint-enable no-restricted-syntax */
88
88
  }
89
- const MAX_OUTPUT_1M_MODELS = '128000';
89
+ const MAX_OUTPUT_1M_MODELS = '64000';
90
90
  const MAX_OUTPUT_200K_OPUS_SONNET = '32768';
91
91
  let runtimeClaudeCodeVersion = null;
92
92
  let runtimeClaudeContextWindow = 'auto';
@@ -149,6 +149,62 @@ function buildClaudeLaunchModelArg(model, contextWindow) {
149
149
  }
150
150
  return normalized;
151
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
+ }
152
208
  function isTwoHundredKFixedOutputModel(model) {
153
209
  const normalized = (model || '').trim().toLowerCase();
154
210
  if (!normalized)
@@ -184,7 +240,7 @@ function renderClaudeLaunchSelectorIntro() {
184
240
  console.log(line(chalk_1.default.white.bold('ekkOS.dev // PULSE')));
185
241
  console.log(neonCyan('╠══════════════════════════════════════════════════════════════════════╣'));
186
242
  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')}`));
243
+ console.log(line(`${signalAmber(' 1M')} ${steel('wide lane')} ${chalk_1.default.white('// Opus/Sonnet 4.6 => 64,000 output')}`));
188
244
  console.log(line(steel('Pick a launch vector, then a fixed runtime profile. No hidden context prompt.')));
189
245
  console.log(neonCyan('╚══════════════════════════════════════════════════════════════════════╝'));
190
246
  console.log('');
@@ -223,7 +279,7 @@ function buildLaunchModelChoices() {
223
279
  value: 'claude-opus-4-6-200k',
224
280
  },
225
281
  {
226
- name: `${cyan.bold('Claude Opus 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 128,000')}`,
282
+ name: `${cyan.bold('Claude Opus 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 64,000')}`,
227
283
  value: 'claude-opus-4-6-1m',
228
284
  },
229
285
  {
@@ -235,7 +291,7 @@ function buildLaunchModelChoices() {
235
291
  value: 'claude-sonnet-4-6-200k',
236
292
  },
237
293
  {
238
- name: `${cyan.bold('Claude Sonnet 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 128,000')}`,
294
+ name: `${cyan.bold('Claude Sonnet 4.6')} ${amber('[1M]')} ${chalk_1.default.gray('// out 64,000')}`,
239
295
  value: 'claude-sonnet-4-6-1m',
240
296
  },
241
297
  {
@@ -615,6 +671,7 @@ function getEkkosEnv() {
615
671
  // Gateway extracts from URL: /proxy/{userId}/{sessionName}/v1/messages
616
672
  const proxyUrl = (0, proxy_url_1.buildProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId, {
617
673
  claudeCodeVersion: runtimeClaudeCodeVersion || undefined,
674
+ claudeProfile: buildClaudeLaunchProfileId(runtimeClaudeLaunchModel, runtimeClaudeContextWindow),
618
675
  claudeContextWindow: runtimeClaudeContextWindow,
619
676
  });
620
677
  env.ANTHROPIC_BASE_URL = proxyUrl;
@@ -859,7 +916,7 @@ function formatPulseContextLine(contextWindow) {
859
916
  return '200K // 200,000 token window // forced 32,768 output';
860
917
  }
861
918
  if (contextWindow === '1m') {
862
- return '1M // 1,000,000 token window // unlocked 128,000 output';
919
+ return '1M // 1,000,000 token window // safe 64,000 output cap';
863
920
  }
864
921
  return 'AUTO // ekkOS resolves 200K vs 1M from the selected profile';
865
922
  }
@@ -906,7 +963,7 @@ async function showPulseLaunchLoader(options) {
906
963
  console.log(buildPanelLine(steel, 'PATH', 'selector -> dashboard -> claude -> proxy'));
907
964
  console.log(cyan(' ╚══════════════════════════════════════════════════════════════════════════════╝'));
908
965
  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')}`);
966
+ console.log(` ${amber('1M ')} ${steel('// Opus/Sonnet 4.6 => 64,000 output')}`);
910
967
  console.log('');
911
968
  for (const stage of stages) {
912
969
  for (let filled = 0; filled <= barWidth; filled += 1) {
@@ -1131,10 +1188,16 @@ function cleanupInstanceFile(instanceId) {
1131
1188
  * Keeps ~/.ekkos/current-session.json, ~/.claude/state/current-session.json,
1132
1189
  * and ~/.ekkos/session-hint.json in sync whenever the session is known.
1133
1190
  */
1134
- function writeSessionFiles(sessionId, sessionName) {
1191
+ function writeSessionFiles(sessionId, sessionName, metadata) {
1135
1192
  try {
1136
1193
  const ekkosDir = path.join(os.homedir(), '.ekkos');
1137
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
+ });
1138
1201
  // Ensure directories exist
1139
1202
  if (!fs.existsSync(ekkosDir))
1140
1203
  fs.mkdirSync(ekkosDir, { recursive: true });
@@ -1142,16 +1205,17 @@ function writeSessionFiles(sessionId, sessionName) {
1142
1205
  fs.mkdirSync(claudeStateDir, { recursive: true });
1143
1206
  const now = new Date().toISOString();
1144
1207
  // 1. ~/.ekkos/current-session.json
1145
- 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));
1146
1209
  // 2. ~/.claude/state/current-session.json
1147
- 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));
1148
1211
  // 3. ~/.ekkos/session-hint.json (dashboard discovery)
1149
1212
  fs.writeFileSync(path.join(ekkosDir, 'session-hint.json'), JSON.stringify({
1150
1213
  session_name: sessionName,
1151
1214
  session_id: sessionId,
1152
1215
  project_path: process.cwd(),
1153
1216
  timestamp: now,
1154
- pid: process.pid
1217
+ pid: process.pid,
1218
+ ...launchMetadata,
1155
1219
  }, null, 2));
1156
1220
  }
1157
1221
  catch {
@@ -1199,7 +1263,7 @@ function launchWithDashboard(options) {
1199
1263
  const termCols = process.stdout.columns ?? 160;
1200
1264
  const termRows = process.stdout.rows ?? 48;
1201
1265
  // Write session hint immediately so dashboard can find JSONL as it appears
1202
- writeSessionFiles(crypto.randomUUID(), sessionName);
1266
+ writeSessionFiles(crypto.randomUUID(), sessionName, getClaudeSessionMetadata(options));
1203
1267
  const runCommand = `EKKOS_NO_SPLASH=1 node "${ekkosCmd}" ${runArgs.join(' ')}`;
1204
1268
  // Pass session name directly — dashboard starts rendering immediately (lazy JSONL resolution)
1205
1269
  const dashCommand = `node "${ekkosCmd}" dashboard "${sessionName}" --refresh 2000`;
@@ -1263,7 +1327,7 @@ function launchWithWindowsTerminal(options) {
1263
1327
  // Pre-generate session name so dashboard can start immediately
1264
1328
  const sessionName = options.session || (0, state_1.uuidToWords)(crypto.randomUUID());
1265
1329
  // Write session hint immediately
1266
- writeSessionFiles(crypto.randomUUID(), sessionName);
1330
+ writeSessionFiles(crypto.randomUUID(), sessionName, getClaudeSessionMetadata(options));
1267
1331
  // Build ekkos run args WITHOUT --dashboard (prevent recursion)
1268
1332
  // Always pass -s so run reuses the same session name we gave the dashboard
1269
1333
  const runArgs = ['run', '-s', sessionName];
@@ -1778,8 +1842,8 @@ async function run(options) {
1778
1842
  // ════════════════════════════════════════════════════════════════════════════
1779
1843
  const initialSessionId = cliSessionId || 'pending';
1780
1844
  const initialSessionName = currentSession || 'initializing';
1781
- (0, state_1.registerActiveSession)(initialSessionId, initialSessionName, process.cwd());
1782
- writeSessionFiles(initialSessionId, initialSessionName);
1845
+ (0, state_1.registerActiveSession)(initialSessionId, initialSessionName, process.cwd(), getClaudeSessionMetadata(options));
1846
+ writeSessionFiles(initialSessionId, initialSessionName, getClaudeSessionMetadata(options));
1783
1847
  dlog(`Registered active session (PID ${process.pid})`);
1784
1848
  // Show active sessions count if verbose
1785
1849
  if (verbose) {
@@ -1946,9 +2010,9 @@ async function run(options) {
1946
2010
  // and shown in "Continuum Loaded". Re-deriving from JSONL UUID produces a
1947
2011
  // different name since Claude Code's UUID ≠ the CLI-generated UUID.
1948
2012
  currentSession = cliSessionName || (0, state_1.uuidToWords)(sessionId);
1949
- (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
2013
+ (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession, getClaudeSessionMetadata(options));
1950
2014
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
1951
- writeSessionFiles(currentSessionId, currentSession);
2015
+ writeSessionFiles(currentSessionId, currentSession, getClaudeSessionMetadata(options));
1952
2016
  bindRealSessionToProxy(currentSession, 'fast-transcript', currentSessionId);
1953
2017
  dlog(`[TRANSCRIPT] FAST DETECT: New transcript found! ${fullPath}`);
1954
2018
  evictionDebugLog('TRANSCRIPT_SET', 'Fast poll detected new file', {
@@ -2842,10 +2906,10 @@ async function run(options) {
2842
2906
  // Keep cliSessionName if set (proxy mode) — JSONL UUID differs from CLI UUID
2843
2907
  currentSession = cliSessionName || (0, state_1.uuidToWords)(currentSessionId);
2844
2908
  // Update THIS process's session entry (not global state.json)
2845
- (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession);
2909
+ (0, state_1.updateCurrentProcessSession)(currentSessionId, currentSession, getClaudeSessionMetadata(options));
2846
2910
  // Also update global state for backwards compatibility
2847
2911
  (0, state_1.updateState)({ sessionId: currentSessionId, sessionName: currentSession });
2848
- writeSessionFiles(currentSessionId, currentSession);
2912
+ writeSessionFiles(currentSessionId, currentSession, getClaudeSessionMetadata(options));
2849
2913
  dlog(`Session detected from UUID: ${currentSession}`);
2850
2914
  resolveTranscriptFromSessionId('session-id-from-output');
2851
2915
  bindRealSessionToProxy(currentSession, 'session-id-from-output', currentSessionId || undefined);
@@ -2878,10 +2942,10 @@ async function run(options) {
2878
2942
  currentSession = lastSeenSessionName;
2879
2943
  observedSessionThisRun = true; // Mark that we've seen a session in THIS process
2880
2944
  // Update THIS process's session entry (not global state.json)
2881
- (0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession);
2945
+ (0, state_1.updateCurrentProcessSession)(currentSessionId || 'unknown', currentSession, getClaudeSessionMetadata(options));
2882
2946
  // Also update global state for backwards compatibility
2883
2947
  (0, state_1.updateState)({ sessionName: currentSession });
2884
- writeSessionFiles(currentSessionId || 'unknown', currentSession);
2948
+ writeSessionFiles(currentSessionId || 'unknown', currentSession, getClaudeSessionMetadata(options));
2885
2949
  dlog(`Session detected from status line: ${currentSession} (observedSessionThisRun=true)`);
2886
2950
  bindRealSessionToProxy(currentSession, 'status-line', currentSessionId || undefined);
2887
2951
  resolveTranscriptFromSessionId('status-line');
@@ -0,0 +1,3 @@
1
+ export declare function setupCiCommand(options: {
2
+ repoRoot?: string;
3
+ }): Promise<number>;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.setupCiCommand = setupCiCommand;
40
+ const chalk_1 = __importDefault(require("chalk"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const WORKFLOW_CONTENT = `name: ekkOS Cortex Validation
44
+
45
+ on:
46
+ push:
47
+ branches: [main, master]
48
+ paths:
49
+ - '**/ekkOS_CONTEXT.md'
50
+ pull_request:
51
+ branches: [main, master]
52
+ paths:
53
+ - '**/ekkOS_CONTEXT.md'
54
+
55
+ jobs:
56
+ validate-cortex:
57
+ name: Validate Cortex Docs
58
+ runs-on: ubuntu-latest
59
+ timeout-minutes: 10
60
+
61
+ steps:
62
+ - name: Checkout repository
63
+ uses: actions/checkout@v4
64
+
65
+ - name: Setup Node.js
66
+ uses: actions/setup-node@v4
67
+ with:
68
+ node-version: '20'
69
+
70
+ - name: Validate Cortex docs
71
+ run: npx @google/gemini-cli docs validate
72
+
73
+ - name: Add validation summary
74
+ if: always()
75
+ run: |
76
+ echo "### ekkOS Cortex Validation" >> "$GITHUB_STEP_SUMMARY"
77
+ echo "" >> "$GITHUB_STEP_SUMMARY"
78
+ echo "Validated \`ekkOS_CONTEXT.md\` files across the repository." >> "$GITHUB_STEP_SUMMARY"
79
+ if [ "\${{ job.status }}" = "success" ]; then
80
+ echo "**Result:** All Cortex checks passed." >> "$GITHUB_STEP_SUMMARY"
81
+ else
82
+ echo "**Result:** Some Cortex checks failed. See logs for details." >> "$GITHUB_STEP_SUMMARY"
83
+ fi
84
+ `;
85
+ async function setupCiCommand(options) {
86
+ const repoRoot = options.repoRoot || process.cwd();
87
+ const workflowsDir = path.join(repoRoot, '.github', 'workflows');
88
+ const workflowFile = path.join(workflowsDir, 'ekkos-cortex-validation.yml');
89
+ console.log(chalk_1.default.cyan.bold('\n ekkOS Cortex CI/CD Setup'));
90
+ console.log(chalk_1.default.gray(' ─────────────────────────'));
91
+ if (!fs.existsSync(workflowsDir)) {
92
+ fs.mkdirSync(workflowsDir, { recursive: true });
93
+ console.log(chalk_1.default.gray(` Created directory: .github/workflows`));
94
+ }
95
+ if (fs.existsSync(workflowFile)) {
96
+ console.log(chalk_1.default.yellow(` ⚠️ Workflow file already exists at: .github/workflows/ekkos-cortex-validation.yml`));
97
+ console.log(chalk_1.default.gray(` Skipping creation to avoid overwriting.`));
98
+ return 0;
99
+ }
100
+ fs.writeFileSync(workflowFile, WORKFLOW_CONTENT, 'utf-8');
101
+ console.log(chalk_1.default.green(` ✓ Created GitHub Actions workflow: .github/workflows/ekkos-cortex-validation.yml`));
102
+ console.log('');
103
+ console.log(chalk_1.default.gray(' This workflow will automatically run `ekkos docs validate` on pull requests'));
104
+ console.log(chalk_1.default.gray(' and pushes that modify any ekkOS_CONTEXT.md (Cortex) file in your repository.'));
105
+ console.log('');
106
+ return 0;
107
+ }