@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.
@@ -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
  }
@@ -94,9 +96,18 @@ async function showPulseLoadedBanner() {
94
96
  }
95
97
  /**
96
98
  * Resolve Gemini CLI binary path.
97
- * Checks common locations then falls back to PATH lookup.
99
+ * Checks PATH first (which/where), then falls back to common locations.
98
100
  */
99
101
  function resolveGeminiPath() {
102
+ // 1. PATH lookup first to respect nvm/volta/npm-global settings
103
+ const whichCmd = isWindows ? 'where gemini' : 'which gemini';
104
+ try {
105
+ const result = (0, child_process_1.execSync)(whichCmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
106
+ if (result)
107
+ return result.split('\n')[0];
108
+ }
109
+ catch { /* not found in PATH */ }
110
+ // 2. Fallback to common global install locations
100
111
  if (!isWindows) {
101
112
  const pathsToCheck = [
102
113
  '/opt/homebrew/bin/gemini',
@@ -109,16 +120,161 @@ function resolveGeminiPath() {
109
120
  return p;
110
121
  }
111
122
  }
112
- // PATH lookup
113
- const whichCmd = isWindows ? 'where gemini' : 'which gemini';
123
+ // 3. Ultimate fallback — let spawn resolve it (will error with helpful message)
124
+ return 'gemini';
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) {
114
172
  try {
115
- const result = (0, child_process_1.execSync)(whichCmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
116
- if (result)
117
- return result.split('\n')[0];
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;
118
277
  }
119
- catch { /* not found */ }
120
- // Fallback — let spawn resolve it (will error with helpful message)
121
- return 'gemini';
122
278
  }
123
279
  /**
124
280
  * Build environment for Gemini CLI with proxy routing.
@@ -137,14 +293,20 @@ function buildGeminiEnv(options) {
137
293
  // Resolve userId from config or auth token
138
294
  const ekkosConfig = (0, state_1.getConfig)();
139
295
  let userId = ekkosConfig?.userId || 'anonymous';
296
+ const authToken = (0, state_1.getAuthToken)();
140
297
  if (userId === 'anonymous') {
141
- const authToken = (0, state_1.getAuthToken)();
142
298
  if (authToken?.startsWith('ekk_')) {
143
299
  const parts = authToken.split('_');
144
300
  if (parts.length >= 2 && parts[1])
145
301
  userId = parts[1];
146
302
  }
147
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
+ }
148
310
  // Build proxy URL — no query params (SDK concatenates baseUrl + path as strings).
149
311
  // Format: /gproxy/{userId}/{session}/{sid}/{project64}
150
312
  // Proxy extracts params from path, then routes /v1beta or /v1beta1 to googleRouter.
@@ -163,6 +325,11 @@ function buildGeminiEnv(options) {
163
325
  */
164
326
  async function gemini(options = {}) {
165
327
  (0, state_1.ensureEkkosDir)();
328
+ const extraArgs = extractGeminiCliArgs();
329
+ if (options.dashboard) {
330
+ if (launchGeminiWithDashboard(options, extraArgs))
331
+ return;
332
+ }
166
333
  console.log('');
167
334
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_') + chalk_1.default.gray(' + ') + chalk_1.default.blue('Gemini CLI'));
168
335
  // Resolve binary
@@ -175,7 +342,9 @@ async function gemini(options = {}) {
175
342
  // Register session for multi-session awareness
176
343
  const sessionId = cliSessionId || crypto.randomUUID();
177
344
  const sessionName = cliSessionName || 'gemini-session';
178
- (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);
179
348
  if (!options.noProxy) {
180
349
  await showPulseLoadedBanner();
181
350
  }
@@ -183,14 +352,6 @@ async function gemini(options = {}) {
183
352
  // Extract any trailing arguments meant for the inner CLI
184
353
  // We look for 'gemini' in process.argv and pass everything after it.
185
354
  // If the user did `ekkos gemini -m gemini-3.1-pro-preview`, we want to pass `-m gemini-3.1-pro-preview`
186
- let extraArgs = [];
187
- const geminiIdx = process.argv.indexOf('gemini');
188
- if (geminiIdx !== -1) {
189
- extraArgs = process.argv.slice(geminiIdx + 1).filter(a => {
190
- // Filter out ekkos wrapper options
191
- 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');
192
- });
193
- }
194
355
  // Spawn Gemini CLI — stdio: inherit for full terminal passthrough
195
356
  const child = (0, child_process_1.spawn)(geminiPath, extraArgs, {
196
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
  }
@@ -2,12 +2,17 @@ interface RunOptions {
2
2
  session?: string;
3
3
  verbose?: boolean;
4
4
  bypass?: boolean;
5
+ pulse?: boolean;
5
6
  doctor?: boolean;
6
7
  noInject?: boolean;
7
8
  research?: boolean;
8
9
  noProxy?: boolean;
9
10
  dashboard?: boolean;
10
11
  addDirs?: string[];
12
+ model?: string | boolean;
13
+ contextWindow?: string;
14
+ continueLast?: boolean;
15
+ resumeSession?: string;
11
16
  slashOpenDelayMs?: number;
12
17
  charDelayMs?: number;
13
18
  postEnterDelayMs?: number;