@ekkos/cli 1.3.9 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dashboard.js +520 -42
- package/dist/commands/gemini.d.ts +1 -0
- package/dist/commands/gemini.js +170 -10
- package/dist/commands/init-living-docs.d.ts +6 -0
- package/dist/commands/init-living-docs.js +57 -0
- package/dist/commands/living-docs.js +3 -3
- package/dist/commands/run.js +85 -21
- package/dist/commands/setup-ci.d.ts +3 -0
- package/dist/commands/setup-ci.js +107 -0
- package/dist/commands/validate-living-docs.d.ts +27 -0
- package/dist/commands/validate-living-docs.js +489 -0
- package/dist/index.js +109 -82
- package/dist/utils/state.d.ts +16 -3
- package/dist/utils/state.js +10 -3
- package/package.json +1 -1
|
@@ -87,10 +87,31 @@ const MODEL_PRICING = {
|
|
|
87
87
|
'claude-sonnet-4': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
88
88
|
'claude-haiku-4-5-20251001': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
|
|
89
89
|
'claude-haiku-4-5': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
|
|
90
|
+
'gemini-3.1-flash-lite-preview': { input: 0.05, output: 0.20, cacheWrite: 0, cacheRead: 0.05 },
|
|
91
|
+
'gemini-3.1-pro-preview': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
|
|
92
|
+
'gemini-3-pro-preview': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
|
|
93
|
+
'gemini-3-flash-preview': { input: 0.075, output: 0.30, cacheWrite: 0, cacheRead: 0.075 },
|
|
94
|
+
'gemini-2.5-flash-lite': { input: 0.05, output: 0.20, cacheWrite: 0, cacheRead: 0.05 },
|
|
95
|
+
'gemini-2.5-pro': { input: 1.25, output: 5.00, cacheWrite: 0, cacheRead: 1.25 },
|
|
96
|
+
'gemini-2.5-flash': { input: 0.075, output: 0.30, cacheWrite: 0, cacheRead: 0.075 },
|
|
90
97
|
};
|
|
91
98
|
function getModelPricing(modelId) {
|
|
92
99
|
if (MODEL_PRICING[modelId])
|
|
93
100
|
return MODEL_PRICING[modelId];
|
|
101
|
+
if (modelId.includes('gemini-3.1-pro'))
|
|
102
|
+
return MODEL_PRICING['gemini-3.1-pro-preview'];
|
|
103
|
+
if (modelId.includes('gemini-3-pro'))
|
|
104
|
+
return MODEL_PRICING['gemini-3-pro-preview'];
|
|
105
|
+
if (modelId.includes('gemini-3.1-flash-lite'))
|
|
106
|
+
return MODEL_PRICING['gemini-3.1-flash-lite-preview'];
|
|
107
|
+
if (modelId.includes('gemini-3-flash'))
|
|
108
|
+
return MODEL_PRICING['gemini-3-flash-preview'];
|
|
109
|
+
if (modelId.includes('gemini-2.5-pro'))
|
|
110
|
+
return MODEL_PRICING['gemini-2.5-pro'];
|
|
111
|
+
if (modelId.includes('gemini-2.5-flash-lite'))
|
|
112
|
+
return MODEL_PRICING['gemini-2.5-flash-lite'];
|
|
113
|
+
if (modelId.includes('gemini-2.5-flash'))
|
|
114
|
+
return MODEL_PRICING['gemini-2.5-flash'];
|
|
94
115
|
if (modelId.includes('opus'))
|
|
95
116
|
return MODEL_PRICING['claude-opus-4-6'];
|
|
96
117
|
if (modelId.includes('sonnet'))
|
|
@@ -130,12 +151,15 @@ function getModelCtxSize(model, contextTierHint) {
|
|
|
130
151
|
const normalized = (model || '').toLowerCase();
|
|
131
152
|
if (contextTierHint === '1m')
|
|
132
153
|
return 1000000;
|
|
154
|
+
if (contextTierHint === '200k')
|
|
155
|
+
return 200000;
|
|
133
156
|
// Claude 4.6 has GA 1M context in Claude Code/API.
|
|
134
157
|
if (/^claude-(?:opus|sonnet)-4-6(?:$|-)/.test(normalized))
|
|
135
158
|
return 1000000;
|
|
136
|
-
|
|
159
|
+
if (normalized.startsWith('gemini-3.1-pro') || normalized.startsWith('gemini-3-pro'))
|
|
160
|
+
return 2097152;
|
|
137
161
|
if (normalized.startsWith('gemini-'))
|
|
138
|
-
return
|
|
162
|
+
return 1048576;
|
|
139
163
|
if (normalized.includes('1m'))
|
|
140
164
|
return 1000000;
|
|
141
165
|
if (normalized.includes('opus'))
|
|
@@ -146,6 +170,100 @@ function getModelCtxSize(model, contextTierHint) {
|
|
|
146
170
|
return 200000;
|
|
147
171
|
return 200000; // Default Anthropic context
|
|
148
172
|
}
|
|
173
|
+
function parseExplicitContextWindowSize(value) {
|
|
174
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
175
|
+
return Math.floor(value);
|
|
176
|
+
}
|
|
177
|
+
if (typeof value === 'string') {
|
|
178
|
+
const parsed = Number(value.trim());
|
|
179
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
180
|
+
return Math.floor(parsed);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
function resolveDashboardContextSize(model, contextTierHint, launchMetadata, explicitContextWindow) {
|
|
186
|
+
const explicitSize = parseExplicitContextWindowSize(explicitContextWindow);
|
|
187
|
+
if (explicitSize)
|
|
188
|
+
return explicitSize;
|
|
189
|
+
if (typeof launchMetadata?.claudeContextSize === 'number' && launchMetadata.claudeContextSize > 0) {
|
|
190
|
+
return launchMetadata.claudeContextSize;
|
|
191
|
+
}
|
|
192
|
+
const launchWindowHint = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow);
|
|
193
|
+
return getModelCtxSize(model, contextTierHint || launchWindowHint);
|
|
194
|
+
}
|
|
195
|
+
function humanizeModelName(model, includeVendor = false) {
|
|
196
|
+
if (!model)
|
|
197
|
+
return null;
|
|
198
|
+
const normalized = model.toLowerCase();
|
|
199
|
+
const prefix = includeVendor ? 'Claude ' : '';
|
|
200
|
+
if (normalized.includes('claude-opus-4-6'))
|
|
201
|
+
return `${prefix}Opus 4.6`;
|
|
202
|
+
if (normalized.includes('claude-opus-4-5'))
|
|
203
|
+
return `${prefix}Opus 4.5`;
|
|
204
|
+
if (normalized.includes('claude-sonnet-4-6'))
|
|
205
|
+
return `${prefix}Sonnet 4.6`;
|
|
206
|
+
if (normalized.includes('claude-sonnet-4-5'))
|
|
207
|
+
return `${prefix}Sonnet 4.5`;
|
|
208
|
+
if (normalized.includes('claude-haiku-4-5'))
|
|
209
|
+
return `${prefix}Haiku 4.5`;
|
|
210
|
+
if (normalized.includes('gemini-3.1-pro'))
|
|
211
|
+
return includeVendor ? 'Gemini 3.1 Pro' : 'Gemini 3.1 Pro';
|
|
212
|
+
if (normalized.includes('gemini-3-pro'))
|
|
213
|
+
return includeVendor ? 'Gemini 3 Pro' : 'Gemini 3 Pro';
|
|
214
|
+
if (normalized.includes('gemini-3.1-flash-lite'))
|
|
215
|
+
return includeVendor ? 'Gemini 3.1 Flash Lite' : 'Gemini 3.1 Flash Lite';
|
|
216
|
+
if (normalized.includes('gemini-3-flash'))
|
|
217
|
+
return includeVendor ? 'Gemini 3 Flash' : 'Gemini 3 Flash';
|
|
218
|
+
if (normalized.includes('gemini-2.5-pro'))
|
|
219
|
+
return includeVendor ? 'Gemini 2.5 Pro' : 'Gemini 2.5 Pro';
|
|
220
|
+
if (normalized.includes('gemini-2.5-flash-lite'))
|
|
221
|
+
return includeVendor ? 'Gemini 2.5 Flash Lite' : 'Gemini 2.5 Flash Lite';
|
|
222
|
+
if (normalized.includes('gemini-2.5-flash'))
|
|
223
|
+
return includeVendor ? 'Gemini 2.5 Flash' : 'Gemini 2.5 Flash';
|
|
224
|
+
if (normalized.startsWith('gemini-'))
|
|
225
|
+
return includeVendor ? 'Gemini' : 'Gemini';
|
|
226
|
+
if (normalized.startsWith('gpt-'))
|
|
227
|
+
return model;
|
|
228
|
+
return model;
|
|
229
|
+
}
|
|
230
|
+
function formatCompactLaunchModel(metadata) {
|
|
231
|
+
const model = metadata?.claudeModel || metadata?.claudeLaunchModel;
|
|
232
|
+
const label = humanizeModelName(model, false);
|
|
233
|
+
if (!label)
|
|
234
|
+
return null;
|
|
235
|
+
const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
|
|
236
|
+
if (window === '200k')
|
|
237
|
+
return `${label} [200K]`;
|
|
238
|
+
if (window === '1m')
|
|
239
|
+
return `${label} [1M]`;
|
|
240
|
+
return label;
|
|
241
|
+
}
|
|
242
|
+
function formatActiveRoutedModel(model) {
|
|
243
|
+
return humanizeModelName(model, false);
|
|
244
|
+
}
|
|
245
|
+
function formatContextLaneCompact(contextSize, metadata) {
|
|
246
|
+
const explicitSize = parseExplicitContextWindowSize(contextSize);
|
|
247
|
+
const launchSize = typeof metadata?.claudeContextSize === 'number' ? metadata.claudeContextSize : undefined;
|
|
248
|
+
const resolvedSize = explicitSize || launchSize;
|
|
249
|
+
const maxOutput = metadata?.claudeMaxOutputTokens
|
|
250
|
+
|| (resolvedSize && resolvedSize >= 1000000 ? 128000 : 32768);
|
|
251
|
+
if (resolvedSize && resolvedSize >= 1000000) {
|
|
252
|
+
return `1M / out ${fmtK(maxOutput)}`;
|
|
253
|
+
}
|
|
254
|
+
if (resolvedSize && resolvedSize > 0) {
|
|
255
|
+
return `200K / out ${fmtK(maxOutput)}`;
|
|
256
|
+
}
|
|
257
|
+
const launchWindow = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
|
|
258
|
+
if (launchWindow === '1m')
|
|
259
|
+
return `1M / out ${fmtK(maxOutput)}`;
|
|
260
|
+
if (launchWindow === '200k')
|
|
261
|
+
return `200K / out ${fmtK(maxOutput)}`;
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
function buildRuntimeSignal(label, color, value) {
|
|
265
|
+
return `{${color}-fg}[${label}]{/${color}-fg} ${value}`;
|
|
266
|
+
}
|
|
149
267
|
/** Model tag for dashboard display */
|
|
150
268
|
function modelTag(model) {
|
|
151
269
|
if (model.includes('opus'))
|
|
@@ -154,6 +272,8 @@ function modelTag(model) {
|
|
|
154
272
|
return 'Sonnet';
|
|
155
273
|
if (model.includes('haiku'))
|
|
156
274
|
return 'Haiku';
|
|
275
|
+
if (model.includes('gemini'))
|
|
276
|
+
return 'Gem';
|
|
157
277
|
return '?';
|
|
158
278
|
}
|
|
159
279
|
function parseCacheHintValue(cacheHint, key) {
|
|
@@ -169,14 +289,15 @@ function parseCacheHintValue(cacheHint, key) {
|
|
|
169
289
|
}
|
|
170
290
|
return null;
|
|
171
291
|
}
|
|
172
|
-
function parseJsonlFile(jsonlPath, sessionName) {
|
|
292
|
+
function parseJsonlFile(jsonlPath, sessionName, launchMetadata) {
|
|
173
293
|
const content = fs.readFileSync(jsonlPath, 'utf-8');
|
|
174
294
|
const lines = content.trim().split('\n');
|
|
175
295
|
const turnsByMsgId = new Map();
|
|
176
296
|
const msgIdOrder = [];
|
|
177
297
|
let startedAt = '';
|
|
178
|
-
let model = 'unknown';
|
|
298
|
+
let model = launchMetadata?.claudeModel || launchMetadata?.claudeLaunchModel || 'unknown';
|
|
179
299
|
const toolsByMessage = new Map();
|
|
300
|
+
const fallbackContextWindow = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow);
|
|
180
301
|
for (const line of lines) {
|
|
181
302
|
try {
|
|
182
303
|
const entry = JSON.parse(line);
|
|
@@ -226,7 +347,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
226
347
|
: (parseCacheHintValue(cacheHint, 'eviction') || 'unknown');
|
|
227
348
|
const contextTierHint = typeof entry.message._ekkos_context_tier === 'string'
|
|
228
349
|
? entry.message._ekkos_context_tier.trim().toLowerCase()
|
|
229
|
-
:
|
|
350
|
+
: fallbackContextWindow;
|
|
230
351
|
const explicitExtraUsage = entry.message._ekkos_extra_usage === true
|
|
231
352
|
|| entry.message._ekkos_extra_usage === '1'
|
|
232
353
|
|| entry.message._ekkos_extra_usage === 'true';
|
|
@@ -235,7 +356,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
235
356
|
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
236
357
|
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
237
358
|
const contextTokens = inputTokens + cacheReadTokens + cacheCreationTokens;
|
|
238
|
-
const modelCtxSize =
|
|
359
|
+
const modelCtxSize = resolveDashboardContextSize(routedModel, contextTierHint, launchMetadata, entry.message._ekkos_context_window);
|
|
239
360
|
const contextPct = (contextTokens / modelCtxSize) * 100;
|
|
240
361
|
const ts = entry.timestamp || new Date().toISOString();
|
|
241
362
|
if (!startedAt)
|
|
@@ -325,7 +446,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
325
446
|
: 0;
|
|
326
447
|
const modelContextSize = lastTurn
|
|
327
448
|
? lastTurn.modelContextSize
|
|
328
|
-
:
|
|
449
|
+
: resolveDashboardContextSize(model, fallbackContextWindow, launchMetadata);
|
|
329
450
|
return {
|
|
330
451
|
sessionName,
|
|
331
452
|
model,
|
|
@@ -350,6 +471,106 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
350
471
|
turns,
|
|
351
472
|
};
|
|
352
473
|
}
|
|
474
|
+
function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
475
|
+
const raw = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
476
|
+
const messages = Array.isArray(raw.messages) ? raw.messages : [];
|
|
477
|
+
const turns = [];
|
|
478
|
+
const fallbackWindow = normalizeLaunchContextWindow(launchMetadata?.claudeContextWindow) || '1m';
|
|
479
|
+
let model = launchMetadata?.claudeModel || launchMetadata?.claudeLaunchModel || 'gemini';
|
|
480
|
+
const startedAt = raw.startTime || messages.find(m => typeof m.timestamp === 'string')?.timestamp || '';
|
|
481
|
+
for (const message of messages) {
|
|
482
|
+
if (message?.type !== 'gemini' || !message.tokens)
|
|
483
|
+
continue;
|
|
484
|
+
const tokenInput = Math.max(0, Number(message.tokens.input || 0));
|
|
485
|
+
const tokenCached = Math.max(0, Math.min(tokenInput, Number(message.tokens.cached || 0)));
|
|
486
|
+
const tokenOutput = Math.max(0, Number(message.tokens.output || 0));
|
|
487
|
+
const tokenThoughts = Math.max(0, Number(message.tokens.thoughts || 0));
|
|
488
|
+
const tokenTool = Math.max(0, Number(message.tokens.tool || 0));
|
|
489
|
+
const uncachedInput = Math.max(0, tokenInput - tokenCached);
|
|
490
|
+
const effectiveOutput = tokenOutput + tokenThoughts + tokenTool;
|
|
491
|
+
const routedModel = message.model || model;
|
|
492
|
+
model = routedModel;
|
|
493
|
+
const modelCtxSize = resolveDashboardContextSize(routedModel, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize);
|
|
494
|
+
const contextPct = modelCtxSize > 0 ? (tokenInput / modelCtxSize) * 100 : 0;
|
|
495
|
+
const toolNames = Array.isArray(message.toolCalls)
|
|
496
|
+
? Array.from(new Set(message.toolCalls
|
|
497
|
+
.map(tool => typeof tool?.name === 'string' ? tool.name : '')
|
|
498
|
+
.filter(Boolean)))
|
|
499
|
+
: [];
|
|
500
|
+
const usageData = {
|
|
501
|
+
input_tokens: uncachedInput,
|
|
502
|
+
output_tokens: effectiveOutput,
|
|
503
|
+
cache_read_tokens: tokenCached,
|
|
504
|
+
cache_creation_tokens: 0,
|
|
505
|
+
};
|
|
506
|
+
turns.push({
|
|
507
|
+
turn: turns.length + 1,
|
|
508
|
+
contextPct,
|
|
509
|
+
modelContextSize: modelCtxSize,
|
|
510
|
+
input: uncachedInput,
|
|
511
|
+
cacheRead: tokenCached,
|
|
512
|
+
cacheCreate: 0,
|
|
513
|
+
output: effectiveOutput,
|
|
514
|
+
cost: calculateTurnCost(routedModel, usageData),
|
|
515
|
+
opusCost: calculateTurnCost(routedModel, usageData),
|
|
516
|
+
savings: 0,
|
|
517
|
+
tools: toolNames.length > 0 ? toolNames.join(',') : '-',
|
|
518
|
+
model: routedModel,
|
|
519
|
+
routedModel,
|
|
520
|
+
replayState: 'n/a',
|
|
521
|
+
replayStore: 'n/a',
|
|
522
|
+
evictionState: 'n/a',
|
|
523
|
+
timestamp: message.timestamp || raw.lastUpdated || raw.startTime || new Date().toISOString(),
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
const totalCost = turns.reduce((sum, turn) => sum + turn.cost, 0);
|
|
527
|
+
const totalInput = turns.reduce((sum, turn) => sum + turn.input, 0);
|
|
528
|
+
const totalCacheRead = turns.reduce((sum, turn) => sum + turn.cacheRead, 0);
|
|
529
|
+
const totalCacheCreate = turns.reduce((sum, turn) => sum + turn.cacheCreate, 0);
|
|
530
|
+
const totalOutput = turns.reduce((sum, turn) => sum + turn.output, 0);
|
|
531
|
+
const maxContextPct = turns.length > 0 ? Math.max(...turns.map(turn => turn.contextPct)) : 0;
|
|
532
|
+
const currentContextPct = turns.length > 0 ? turns[turns.length - 1].contextPct : 0;
|
|
533
|
+
const avgCostPerTurn = turns.length > 0 ? totalCost / turns.length : 0;
|
|
534
|
+
const cacheHitRate = tokenSafePercent(totalCacheRead, totalInput + totalCacheRead);
|
|
535
|
+
let duration = '0m';
|
|
536
|
+
if (startedAt && turns.length > 0) {
|
|
537
|
+
const start = new Date(startedAt).getTime();
|
|
538
|
+
const end = new Date(turns[turns.length - 1].timestamp).getTime();
|
|
539
|
+
const mins = Math.max(0, Math.round((end - start) / 60000));
|
|
540
|
+
duration = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60}m` : `${mins}m`;
|
|
541
|
+
}
|
|
542
|
+
const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
543
|
+
return {
|
|
544
|
+
sessionName,
|
|
545
|
+
model,
|
|
546
|
+
turnCount: turns.length,
|
|
547
|
+
totalCost,
|
|
548
|
+
totalTokens: totalInput + totalCacheRead + totalCacheCreate + totalOutput,
|
|
549
|
+
totalInput,
|
|
550
|
+
totalCacheRead,
|
|
551
|
+
totalCacheCreate,
|
|
552
|
+
totalOutput,
|
|
553
|
+
avgCostPerTurn,
|
|
554
|
+
maxContextPct,
|
|
555
|
+
currentContextPct,
|
|
556
|
+
currentContextTokens: lastTurn ? lastTurn.input + lastTurn.cacheRead + lastTurn.cacheCreate : 0,
|
|
557
|
+
modelContextSize: lastTurn
|
|
558
|
+
? lastTurn.modelContextSize
|
|
559
|
+
: resolveDashboardContextSize(model, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize),
|
|
560
|
+
cacheHitRate,
|
|
561
|
+
replayAppliedCount: 0,
|
|
562
|
+
replaySkippedSizeCount: 0,
|
|
563
|
+
replaySkipStoreCount: 0,
|
|
564
|
+
startedAt,
|
|
565
|
+
duration,
|
|
566
|
+
turns,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
function tokenSafePercent(numerator, denominator) {
|
|
570
|
+
if (!Number.isFinite(denominator) || denominator <= 0)
|
|
571
|
+
return 0;
|
|
572
|
+
return (numerator / denominator) * 100;
|
|
573
|
+
}
|
|
353
574
|
function readJsonFile(filePath) {
|
|
354
575
|
try {
|
|
355
576
|
if (!fs.existsSync(filePath))
|
|
@@ -360,6 +581,94 @@ function readJsonFile(filePath) {
|
|
|
360
581
|
return null;
|
|
361
582
|
}
|
|
362
583
|
}
|
|
584
|
+
function normalizeLaunchContextWindow(value) {
|
|
585
|
+
const normalized = (value || '').trim().toLowerCase();
|
|
586
|
+
if (!normalized)
|
|
587
|
+
return undefined;
|
|
588
|
+
if (normalized === 'auto')
|
|
589
|
+
return 'auto';
|
|
590
|
+
if (normalized === '200k' || normalized === '200000')
|
|
591
|
+
return '200k';
|
|
592
|
+
if (normalized === '1m' || normalized === '1000000')
|
|
593
|
+
return '1m';
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
function normalizeDashboardProvider(value) {
|
|
597
|
+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
598
|
+
if (normalized === 'gemini' || normalized === 'google')
|
|
599
|
+
return 'gemini';
|
|
600
|
+
if (normalized === 'claude' || normalized === 'anthropic')
|
|
601
|
+
return 'claude';
|
|
602
|
+
return undefined;
|
|
603
|
+
}
|
|
604
|
+
function readSessionLaunchMetadata(sessionName) {
|
|
605
|
+
const candidates = [];
|
|
606
|
+
const active = (0, state_js_1.getActiveSessions)()
|
|
607
|
+
.filter((session) => session.sessionName === sessionName)
|
|
608
|
+
.sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
|
|
609
|
+
if (active.length > 0)
|
|
610
|
+
candidates.push(active[0]);
|
|
611
|
+
const sessionHint = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'session-hint.json'));
|
|
612
|
+
if (!sessionHint || !sessionHint.session_name || sessionHint.session_name === sessionName) {
|
|
613
|
+
if (sessionHint)
|
|
614
|
+
candidates.push(sessionHint);
|
|
615
|
+
}
|
|
616
|
+
const current = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'current-session.json'));
|
|
617
|
+
if (!current || !current.session_name || current.session_name === sessionName) {
|
|
618
|
+
if (current)
|
|
619
|
+
candidates.push(current);
|
|
620
|
+
}
|
|
621
|
+
for (const candidate of candidates) {
|
|
622
|
+
if (candidate.provider
|
|
623
|
+
|| candidate.geminiProjectId
|
|
624
|
+
|| candidate.claudeModel
|
|
625
|
+
|| candidate.claudeLaunchModel
|
|
626
|
+
|| candidate.claudeProfile
|
|
627
|
+
|| candidate.claudeContextWindow
|
|
628
|
+
|| candidate.claudeContextSize) {
|
|
629
|
+
return {
|
|
630
|
+
provider: normalizeDashboardProvider(candidate.provider),
|
|
631
|
+
claudeModel: typeof candidate.claudeModel === 'string' ? candidate.claudeModel : undefined,
|
|
632
|
+
claudeLaunchModel: typeof candidate.claudeLaunchModel === 'string' ? candidate.claudeLaunchModel : undefined,
|
|
633
|
+
claudeProfile: typeof candidate.claudeProfile === 'string' ? candidate.claudeProfile : undefined,
|
|
634
|
+
claudeContextWindow: normalizeLaunchContextWindow(typeof candidate.claudeContextWindow === 'string' ? candidate.claudeContextWindow : undefined),
|
|
635
|
+
claudeContextSize: typeof candidate.claudeContextSize === 'number' ? candidate.claudeContextSize : undefined,
|
|
636
|
+
claudeMaxOutputTokens: typeof candidate.claudeMaxOutputTokens === 'number' ? candidate.claudeMaxOutputTokens : undefined,
|
|
637
|
+
claudeCodeVersion: typeof candidate.claudeCodeVersion === 'string' ? candidate.claudeCodeVersion : undefined,
|
|
638
|
+
geminiProjectId: typeof candidate.geminiProjectId === 'string' ? candidate.geminiProjectId : undefined,
|
|
639
|
+
dashboardEnabled: candidate.dashboardEnabled === true,
|
|
640
|
+
bypassEnabled: candidate.bypassEnabled === true,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
function formatLaunchModelLabel(metadata) {
|
|
647
|
+
const model = metadata?.claudeModel || metadata?.claudeLaunchModel;
|
|
648
|
+
if (!model)
|
|
649
|
+
return null;
|
|
650
|
+
const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
|
|
651
|
+
const label = humanizeModelName(model, true) || model;
|
|
652
|
+
if (window === '200k')
|
|
653
|
+
return `${label} [200K]`;
|
|
654
|
+
if (window === '1m')
|
|
655
|
+
return `${label} [1M]`;
|
|
656
|
+
return label;
|
|
657
|
+
}
|
|
658
|
+
function formatLaunchWindowLabel(metadata) {
|
|
659
|
+
const window = normalizeLaunchContextWindow(metadata?.claudeContextWindow);
|
|
660
|
+
const exactSize = typeof metadata?.claudeContextSize === 'number' && metadata.claudeContextSize > 0
|
|
661
|
+
? metadata.claudeContextSize
|
|
662
|
+
: undefined;
|
|
663
|
+
if (window === '200k')
|
|
664
|
+
return `Window ${fmtK(exactSize || 200000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || 32768)}`;
|
|
665
|
+
if (window === '1m')
|
|
666
|
+
return `Window ${fmtK(exactSize || 1000000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || 128000)}`;
|
|
667
|
+
if (typeof metadata?.claudeContextSize === 'number' && metadata.claudeContextSize > 0) {
|
|
668
|
+
return `Window ${fmtK(metadata.claudeContextSize)}`;
|
|
669
|
+
}
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
363
672
|
function resolveSessionAlias(sessionId) {
|
|
364
673
|
const normalized = sessionId.toLowerCase();
|
|
365
674
|
// Project-local hook state (most reliable for the active session)
|
|
@@ -410,7 +719,73 @@ function isPendingSessionId(sessionId) {
|
|
|
410
719
|
return true;
|
|
411
720
|
return normalized === 'pending' || normalized === '_pending' || normalized.startsWith('_pending-');
|
|
412
721
|
}
|
|
413
|
-
|
|
722
|
+
function inferDashboardProvider(explicitProvider, launchMetadata, transcriptPath) {
|
|
723
|
+
if (explicitProvider)
|
|
724
|
+
return explicitProvider;
|
|
725
|
+
const metadataProvider = normalizeDashboardProvider(launchMetadata?.provider);
|
|
726
|
+
if (metadataProvider)
|
|
727
|
+
return metadataProvider;
|
|
728
|
+
if (transcriptPath?.endsWith('.json'))
|
|
729
|
+
return 'gemini';
|
|
730
|
+
return 'claude';
|
|
731
|
+
}
|
|
732
|
+
function resolveGeminiProjectId(projectPath) {
|
|
733
|
+
const registry = readJsonFile(path.join(os.homedir(), '.gemini', 'projects.json'));
|
|
734
|
+
const projectId = registry?.projects?.[projectPath];
|
|
735
|
+
return typeof projectId === 'string' && projectId.length > 0 ? projectId : null;
|
|
736
|
+
}
|
|
737
|
+
function findLatestGeminiSession(projectPath, createdAfterMs) {
|
|
738
|
+
const projectId = resolveGeminiProjectId(projectPath);
|
|
739
|
+
if (!projectId)
|
|
740
|
+
return null;
|
|
741
|
+
const chatsDir = path.join(os.homedir(), '.gemini', 'tmp', projectId, 'chats');
|
|
742
|
+
if (!fs.existsSync(chatsDir))
|
|
743
|
+
return null;
|
|
744
|
+
try {
|
|
745
|
+
const files = fs.readdirSync(chatsDir)
|
|
746
|
+
.filter(file => file.startsWith('session-') && file.endsWith('.json'))
|
|
747
|
+
.map(file => {
|
|
748
|
+
const filePath = path.join(chatsDir, file);
|
|
749
|
+
const stat = fs.statSync(filePath);
|
|
750
|
+
return { filePath, createdMs: stat.birthtimeMs || stat.mtimeMs };
|
|
751
|
+
})
|
|
752
|
+
.filter(entry => !createdAfterMs || entry.createdMs >= createdAfterMs)
|
|
753
|
+
.sort((a, b) => b.createdMs - a.createdMs);
|
|
754
|
+
return files[0]?.filePath || null;
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
function resolveGeminiSessionPath(sessionName, createdAfterMs, launchCwd) {
|
|
761
|
+
const active = (0, state_js_1.getActiveSessions)()
|
|
762
|
+
.filter(session => session.sessionName === sessionName && normalizeDashboardProvider(session.provider) === 'gemini')
|
|
763
|
+
.sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
|
|
764
|
+
const sessionHint = readJsonFile(path.join(state_js_1.EKKOS_DIR, 'session-hint.json'));
|
|
765
|
+
const hintedProjectPath = !sessionHint?.session_name || sessionHint.session_name === sessionName
|
|
766
|
+
? sessionHint?.project_path
|
|
767
|
+
: undefined;
|
|
768
|
+
const candidateProjectPath = active[0]?.projectPath
|
|
769
|
+
|| hintedProjectPath
|
|
770
|
+
|| launchCwd
|
|
771
|
+
|| null;
|
|
772
|
+
if (!candidateProjectPath)
|
|
773
|
+
return null;
|
|
774
|
+
return findLatestGeminiSession(candidateProjectPath, createdAfterMs);
|
|
775
|
+
}
|
|
776
|
+
function resolveTranscriptPath(provider, sessionName, createdAfterMs, launchCwd) {
|
|
777
|
+
if (provider === 'gemini') {
|
|
778
|
+
return resolveGeminiSessionPath(sessionName, createdAfterMs, launchCwd);
|
|
779
|
+
}
|
|
780
|
+
return resolveJsonlPath(sessionName, createdAfterMs);
|
|
781
|
+
}
|
|
782
|
+
function parseTranscriptFile(transcriptPath, provider, sessionName, launchMetadata) {
|
|
783
|
+
if (provider === 'gemini') {
|
|
784
|
+
return parseGeminiSessionFile(transcriptPath, sessionName, launchMetadata);
|
|
785
|
+
}
|
|
786
|
+
return parseJsonlFile(transcriptPath, sessionName, launchMetadata);
|
|
787
|
+
}
|
|
788
|
+
// ── Resolve session to transcript path ──
|
|
414
789
|
function resolveJsonlPath(sessionName, createdAfterMs) {
|
|
415
790
|
// 1) Try standard resolution (works when sessionId is a real UUID)
|
|
416
791
|
const resolved = (0, usage_parser_js_1.resolveSessionName)(sessionName);
|
|
@@ -651,9 +1026,11 @@ function sleep(ms) {
|
|
|
651
1026
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
652
1027
|
}
|
|
653
1028
|
// ── TUI Dashboard ──
|
|
654
|
-
async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs, launchCwd, launchTs) {
|
|
1029
|
+
async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs, launchCwd, launchTs, initialProvider) {
|
|
655
1030
|
let jsonlPath = initialJsonlPath;
|
|
656
1031
|
let sessionName = displaySessionName(initialSessionName);
|
|
1032
|
+
let launchMetadata = readSessionLaunchMetadata(sessionName) || readSessionLaunchMetadata(initialSessionName);
|
|
1033
|
+
let provider = inferDashboardProvider(initialProvider, launchMetadata, initialJsonlPath);
|
|
657
1034
|
const blessed = require('blessed');
|
|
658
1035
|
const contrib = require('blessed-contrib');
|
|
659
1036
|
const inTmux = process.env.TMUX !== undefined;
|
|
@@ -784,7 +1161,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
784
1161
|
const CONTEXT_H = 5;
|
|
785
1162
|
const USAGE_H = 4;
|
|
786
1163
|
const FOOTER_H = 3;
|
|
787
|
-
const
|
|
1164
|
+
const MASCOT_W = 16; // Width reserved for runtime mascot in context box
|
|
788
1165
|
const FIXED_H = HEADER_H + CONTEXT_H + USAGE_H + FOOTER_H; // 15
|
|
789
1166
|
function resolveChartRatio(height) {
|
|
790
1167
|
if (height >= 62)
|
|
@@ -824,7 +1201,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
824
1201
|
tags: true,
|
|
825
1202
|
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
826
1203
|
border: { type: 'line' },
|
|
827
|
-
label: '
|
|
1204
|
+
label: ' ekkOS_PULSE ',
|
|
828
1205
|
});
|
|
829
1206
|
// Explicit header message row: with HEADER_H=3 and a border, this is the
|
|
830
1207
|
// single inner content row (visual line 2 of the widget).
|
|
@@ -845,24 +1222,41 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
845
1222
|
tags: true,
|
|
846
1223
|
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
847
1224
|
border: { type: 'line' },
|
|
848
|
-
label: '
|
|
1225
|
+
label: ' PULSE // Runtime ',
|
|
849
1226
|
});
|
|
850
|
-
|
|
851
|
-
const clawdBox = blessed.box({
|
|
1227
|
+
const mascotBox = blessed.box({
|
|
852
1228
|
parent: contextBox,
|
|
853
1229
|
top: 0,
|
|
854
1230
|
right: 2, // Keep a small visual gap from the context border
|
|
855
1231
|
width: 10,
|
|
856
1232
|
height: 3,
|
|
857
|
-
content: '
|
|
858
|
-
tags:
|
|
859
|
-
style: { fg: '
|
|
1233
|
+
content: '',
|
|
1234
|
+
tags: true,
|
|
1235
|
+
style: { fg: 'cyan', bold: true },
|
|
860
1236
|
});
|
|
1237
|
+
const GEMINI_MASCOT_COLORS = ['cyan', 'blue', 'magenta', 'yellow', 'white', 'green'];
|
|
1238
|
+
let geminiMascotPhase = 0;
|
|
1239
|
+
function renderRuntimeMascot() {
|
|
1240
|
+
if (provider === 'gemini') {
|
|
1241
|
+
const base = GEMINI_MASCOT_COLORS[geminiMascotPhase % GEMINI_MASCOT_COLORS.length];
|
|
1242
|
+
const wing = GEMINI_MASCOT_COLORS[(geminiMascotPhase + 2) % GEMINI_MASCOT_COLORS.length];
|
|
1243
|
+
const core = GEMINI_MASCOT_COLORS[(geminiMascotPhase + 4) % GEMINI_MASCOT_COLORS.length];
|
|
1244
|
+
mascotBox.setContent(` {${base}-fg}✦{/${base}-fg}\n` +
|
|
1245
|
+
` {${wing}-fg}✦{/${wing}-fg}{${core}-fg}◆{/${core}-fg}{${wing}-fg}✦{/${wing}-fg}\n` +
|
|
1246
|
+
` {${base}-fg}✦{/${base}-fg}`);
|
|
1247
|
+
mascotBox.style = { fg: 'cyan', bold: true };
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
// Clawd mascot — official Claude Code mascot.
|
|
1251
|
+
mascotBox.setContent(' ▐▛███▜▌\n ▝▜█████▛▘\n ▘▘ ▝▝');
|
|
1252
|
+
mascotBox.style = { fg: 'red', bold: true };
|
|
1253
|
+
}
|
|
1254
|
+
renderRuntimeMascot();
|
|
861
1255
|
// Token chart (fills 40% of remaining)
|
|
862
1256
|
function createTokenChart(top, left, width, height) {
|
|
863
1257
|
return contrib.line({
|
|
864
1258
|
top, left, width, height,
|
|
865
|
-
label: ' Tokens
|
|
1259
|
+
label: ' Signal // Tokens (K) ',
|
|
866
1260
|
showLegend: true,
|
|
867
1261
|
legend: { width: 8 },
|
|
868
1262
|
style: {
|
|
@@ -893,7 +1287,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
893
1287
|
mouse: false, // Mouse disabled (use keyboard for scrolling, allows text selection)
|
|
894
1288
|
input: !inTmux,
|
|
895
1289
|
interactive: !inTmux, // Standalone only; passive in tmux split
|
|
896
|
-
label: ' Turns (scroll: ↑↓/k/j, page: PgUp/u, home/end: g/G) ',
|
|
1290
|
+
label: ' Trace // Turns (scroll: ↑↓/k/j, page: PgUp/u, home/end: g/G) ',
|
|
897
1291
|
border: { type: 'line', fg: 'cyan' },
|
|
898
1292
|
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
899
1293
|
});
|
|
@@ -904,7 +1298,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
904
1298
|
tags: true,
|
|
905
1299
|
style: { fg: 'white', border: { fg: 'magenta' } },
|
|
906
1300
|
border: { type: 'line' },
|
|
907
|
-
label: '
|
|
1301
|
+
label: ' Pulse Limits ',
|
|
908
1302
|
});
|
|
909
1303
|
// Footer (totals + routing + keys)
|
|
910
1304
|
const footerBox = blessed.box({
|
|
@@ -913,7 +1307,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
913
1307
|
tags: true,
|
|
914
1308
|
style: { fg: 'white', border: { fg: 'cyan' } },
|
|
915
1309
|
border: { type: 'line' },
|
|
916
|
-
label: ' Session ',
|
|
1310
|
+
label: ' Session Bus ',
|
|
917
1311
|
});
|
|
918
1312
|
// Add all widgets to screen
|
|
919
1313
|
screen.append(headerBox);
|
|
@@ -1082,8 +1476,13 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1082
1476
|
}
|
|
1083
1477
|
function renderPreTurnState() {
|
|
1084
1478
|
try {
|
|
1479
|
+
const launchModelLabel = formatLaunchModelLabel(launchMetadata);
|
|
1480
|
+
const launchWindowLabel = formatLaunchWindowLabel(launchMetadata);
|
|
1481
|
+
const compactSelected = formatCompactLaunchModel(launchMetadata) || 'Awaiting profile';
|
|
1482
|
+
const compactLane = formatContextLaneCompact(launchMetadata?.claudeContextSize, launchMetadata) || 'Awaiting lane';
|
|
1085
1483
|
contextBox.setContent(` {green-fg}Session active{/green-fg} {gray-fg}${sessionName}{/gray-fg}\n` +
|
|
1086
|
-
`
|
|
1484
|
+
` ${buildRuntimeSignal('SEL', 'cyan', compactSelected)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', 'standby')} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', compactLane)}\n` +
|
|
1485
|
+
` ${[launchModelLabel, launchWindowLabel].filter(Boolean).join(' ') || 'Token and cost metrics appear after the first assistant response.'}`);
|
|
1087
1486
|
turnBox.setContent(`{bold}Turns{/bold}\n` +
|
|
1088
1487
|
`{gray-fg}—{/gray-fg}`);
|
|
1089
1488
|
const timerStr = sessionStartMs
|
|
@@ -1108,10 +1507,15 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1108
1507
|
const shouldUseCwdFallback = initialSessionName === 'initializing'
|
|
1109
1508
|
|| initialSessionName === 'session'
|
|
1110
1509
|
|| UUID_REGEX.test(initialSessionName);
|
|
1111
|
-
const resolved =
|
|
1112
|
-
|| (
|
|
1510
|
+
const resolved = resolveTranscriptPath(provider, initialSessionName, launchTs, launchCwd)
|
|
1511
|
+
|| (provider === 'claude'
|
|
1512
|
+
&& shouldUseCwdFallback
|
|
1513
|
+
&& launchCwd
|
|
1514
|
+
? findLatestJsonl(launchCwd, launchTs)
|
|
1515
|
+
: null);
|
|
1113
1516
|
if (resolved) {
|
|
1114
1517
|
jsonlPath = resolved;
|
|
1518
|
+
provider = inferDashboardProvider(initialProvider, launchMetadata, resolved);
|
|
1115
1519
|
dlog(`Lazy-resolved JSONL: ${jsonlPath}`);
|
|
1116
1520
|
}
|
|
1117
1521
|
else {
|
|
@@ -1131,6 +1535,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1131
1535
|
const basename = path.basename(jsonlPath, '.jsonl');
|
|
1132
1536
|
if (/^[0-9a-f]{8}-/.test(basename)) {
|
|
1133
1537
|
sessionName = displaySessionName(basename);
|
|
1538
|
+
launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
|
|
1539
|
+
provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
|
|
1134
1540
|
screen.title = `ekkOS - ${sessionName}`;
|
|
1135
1541
|
}
|
|
1136
1542
|
}
|
|
@@ -1141,6 +1547,9 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1141
1547
|
const resolvedName = displaySessionName(sessionName);
|
|
1142
1548
|
if (resolvedName !== sessionName) {
|
|
1143
1549
|
sessionName = resolvedName;
|
|
1550
|
+
launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
|
|
1551
|
+
provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
|
|
1552
|
+
renderRuntimeMascot();
|
|
1144
1553
|
screen.title = `ekkOS - ${sessionName}`;
|
|
1145
1554
|
}
|
|
1146
1555
|
}
|
|
@@ -1150,7 +1559,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1150
1559
|
if (stat.size === lastFileSize && lastData)
|
|
1151
1560
|
return;
|
|
1152
1561
|
lastFileSize = stat.size;
|
|
1153
|
-
|
|
1562
|
+
launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
|
|
1563
|
+
provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
|
|
1564
|
+
renderRuntimeMascot();
|
|
1565
|
+
data = parseTranscriptFile(jsonlPath, provider, sessionName, launchMetadata);
|
|
1154
1566
|
lastData = data;
|
|
1155
1567
|
if (!sessionStartMs && data.startedAt) {
|
|
1156
1568
|
sessionStartMs = new Date(data.startedAt).getTime();
|
|
@@ -1173,10 +1585,17 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1173
1585
|
const ctxColor = ctxPct < 50 ? 'green' : ctxPct < 80 ? 'yellow' : 'red';
|
|
1174
1586
|
const tokensLabel = fmtK(data.currentContextTokens);
|
|
1175
1587
|
const maxLabel = fmtK(data.modelContextSize);
|
|
1588
|
+
const lastTurn = data.turns.length > 0 ? data.turns[data.turns.length - 1] : null;
|
|
1589
|
+
const selectedProfile = formatCompactLaunchModel(launchMetadata)
|
|
1590
|
+
|| humanizeModelName(data.model, false)
|
|
1591
|
+
|| 'Auto';
|
|
1592
|
+
const activeModel = formatActiveRoutedModel(lastTurn?.routedModel || data.model) || 'Unknown';
|
|
1593
|
+
const laneLabel = formatContextLaneCompact(data.modelContextSize, launchMetadata)
|
|
1594
|
+
|| `${maxLabel} window`;
|
|
1176
1595
|
// Visual progress bar (fills available width)
|
|
1177
1596
|
const contextInnerWidth = Math.max(10, contextBox.width - 2);
|
|
1178
1597
|
// Extend bar slightly closer to mascot while keeping a small visual gap.
|
|
1179
|
-
const barWidth = Math.max(10, contextInnerWidth - 4 -
|
|
1598
|
+
const barWidth = Math.max(10, contextInnerWidth - 4 - MASCOT_W);
|
|
1180
1599
|
const filled = Math.round((ctxPct / 100) * barWidth);
|
|
1181
1600
|
const bar = `{${ctxColor}-fg}${'█'.repeat(filled)}{/${ctxColor}-fg}${'░'.repeat(barWidth - filled)}`;
|
|
1182
1601
|
// Cost breakdown by actual routed model per turn.
|
|
@@ -1192,12 +1611,13 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1192
1611
|
const hitColor = data.cacheHitRate >= 80 ? 'green' : data.cacheHitRate >= 50 ? 'yellow' : 'red';
|
|
1193
1612
|
const cappedMax = Math.min(data.maxContextPct, 100);
|
|
1194
1613
|
contextBox.setContent(` ${bar}\n` +
|
|
1614
|
+
` ${buildRuntimeSignal('SEL', 'cyan', selectedProfile)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', activeModel)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', laneLabel)}\n` +
|
|
1195
1615
|
` {${ctxColor}-fg}${ctxPct.toFixed(0)}%{/${ctxColor}-fg} ${tokensLabel}/${maxLabel}` +
|
|
1196
|
-
` {white-fg}
|
|
1616
|
+
` {white-fg}In{/white-fg} $${breakdown.input.toFixed(2)}` +
|
|
1197
1617
|
` {green-fg}Read{/green-fg} $${breakdown.read.toFixed(2)}` +
|
|
1198
1618
|
` {yellow-fg}Write{/yellow-fg} $${breakdown.write.toFixed(2)}` +
|
|
1199
|
-
` {cyan-fg}
|
|
1200
|
-
`
|
|
1619
|
+
` {cyan-fg}Out{/cyan-fg} $${breakdown.output.toFixed(2)}` +
|
|
1620
|
+
` {${hitColor}-fg}${data.cacheHitRate.toFixed(0)}% cache{/${hitColor}-fg}` +
|
|
1201
1621
|
` peak:${cappedMax.toFixed(0)}%` +
|
|
1202
1622
|
` avg:$${data.avgCostPerTurn.toFixed(2)}/t` +
|
|
1203
1623
|
` replay A:${data.replayAppliedCount}` +
|
|
@@ -1773,6 +2193,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1773
2193
|
}
|
|
1774
2194
|
// Fetch fresh usage data from API (called on interval)
|
|
1775
2195
|
async function fetchAndCacheUsage() {
|
|
2196
|
+
if (provider === 'gemini') {
|
|
2197
|
+
renderWindowBox();
|
|
2198
|
+
return;
|
|
2199
|
+
}
|
|
1776
2200
|
if (usageFetchInFlight)
|
|
1777
2201
|
return;
|
|
1778
2202
|
// Respect shared state across dashboard processes.
|
|
@@ -1819,6 +2243,35 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1819
2243
|
// Render countdown from cached data (called every 1s)
|
|
1820
2244
|
function renderWindowBox(skipRender = false) {
|
|
1821
2245
|
try {
|
|
2246
|
+
if (provider === 'gemini') {
|
|
2247
|
+
windowBox.setLabel(' Gemini Runtime ');
|
|
2248
|
+
const runtimeModel = formatLaunchModelLabel(launchMetadata)
|
|
2249
|
+
|| humanizeModelName(lastData?.model, true)
|
|
2250
|
+
|| 'Gemini';
|
|
2251
|
+
const contextSize = fmtK(lastData?.modelContextSize
|
|
2252
|
+
|| launchMetadata?.claudeContextSize
|
|
2253
|
+
|| 1048576);
|
|
2254
|
+
const turnCount = lastData?.turnCount || 0;
|
|
2255
|
+
const tokenCount = fmtK(lastData?.currentContextTokens || 0);
|
|
2256
|
+
const projectId = launchMetadata?.geminiProjectId || resolveGeminiProjectId(launchCwd || process.cwd()) || 'unknown';
|
|
2257
|
+
const transcriptLabel = jsonlPath
|
|
2258
|
+
? path.basename(jsonlPath)
|
|
2259
|
+
: 'awaiting chat transcript';
|
|
2260
|
+
const line1 = ` {bold}Model:{/bold} ${runtimeModel}` +
|
|
2261
|
+
` {bold}Ctx:{/bold} ${contextSize}` +
|
|
2262
|
+
` {bold}Turns:{/bold} ${turnCount}` +
|
|
2263
|
+
` {bold}In:{/bold} ${tokenCount}`;
|
|
2264
|
+
const line2 = ` {bold}Project:{/bold} ${projectId}` +
|
|
2265
|
+
` {bold}Chat:{/bold} ${transcriptLabel}`;
|
|
2266
|
+
windowBox.setContent(line1 + '\n' + line2);
|
|
2267
|
+
if (!skipRender)
|
|
2268
|
+
try {
|
|
2269
|
+
screen.render();
|
|
2270
|
+
}
|
|
2271
|
+
catch { }
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
windowBox.setLabel(' Pulse Limits ');
|
|
1822
2275
|
const usage = cachedUsage;
|
|
1823
2276
|
let line1 = ' {gray-fg}No usage data{/gray-fg}';
|
|
1824
2277
|
let line2 = '';
|
|
@@ -2123,6 +2576,10 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2123
2576
|
const headerAnimInterval = setInterval(() => {
|
|
2124
2577
|
// Keep advancing across the full session label; wrap at a large value.
|
|
2125
2578
|
waveOffset = (waveOffset + 1) % 1000000;
|
|
2579
|
+
if (provider === 'gemini') {
|
|
2580
|
+
geminiMascotPhase = (geminiMascotPhase + 1) % GEMINI_MASCOT_COLORS.length;
|
|
2581
|
+
renderRuntimeMascot();
|
|
2582
|
+
}
|
|
2126
2583
|
renderHeader();
|
|
2127
2584
|
}, animMs);
|
|
2128
2585
|
const fortuneInterval = setInterval(() => {
|
|
@@ -2165,9 +2622,27 @@ function formatElapsed(startMs) {
|
|
|
2165
2622
|
// ── Session picker ──
|
|
2166
2623
|
async function pickSession() {
|
|
2167
2624
|
const sessions = await (0, usage_parser_js_1.listEkkosSessions)(20);
|
|
2168
|
-
if (sessions.length
|
|
2169
|
-
|
|
2170
|
-
|
|
2625
|
+
if (sessions.length > 0) {
|
|
2626
|
+
const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
|
|
2627
|
+
const answer = await inquirer.prompt([
|
|
2628
|
+
{
|
|
2629
|
+
type: 'list',
|
|
2630
|
+
name: 'session',
|
|
2631
|
+
message: 'Choose session:',
|
|
2632
|
+
choices: sessions.map(s => ({
|
|
2633
|
+
name: `${s.name} ($${s.cost.toFixed(2)}, ${s.turnCount}t)`,
|
|
2634
|
+
value: s.name,
|
|
2635
|
+
})),
|
|
2636
|
+
},
|
|
2637
|
+
]);
|
|
2638
|
+
return answer.session;
|
|
2639
|
+
}
|
|
2640
|
+
const active = (0, state_js_1.getActiveSessions)()
|
|
2641
|
+
.filter(session => session.sessionName && !UUID_REGEX.test(session.sessionName))
|
|
2642
|
+
.sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''));
|
|
2643
|
+
if (active.length === 0) {
|
|
2644
|
+
console.log(chalk_1.default.yellow('No active sessions found.'));
|
|
2645
|
+
console.log(chalk_1.default.gray('Start a session with "ekkos run" or "ekkos gemini" first.'));
|
|
2171
2646
|
return null;
|
|
2172
2647
|
}
|
|
2173
2648
|
const { default: inquirer } = await Promise.resolve().then(() => __importStar(require('inquirer')));
|
|
@@ -2175,10 +2650,10 @@ async function pickSession() {
|
|
|
2175
2650
|
{
|
|
2176
2651
|
type: 'list',
|
|
2177
2652
|
name: 'session',
|
|
2178
|
-
message: 'Choose session:',
|
|
2179
|
-
choices:
|
|
2180
|
-
name: `${
|
|
2181
|
-
value:
|
|
2653
|
+
message: 'Choose active session:',
|
|
2654
|
+
choices: active.map(session => ({
|
|
2655
|
+
name: `${session.sessionName} [${normalizeDashboardProvider(session.provider) === 'gemini' ? 'Gemini' : 'Claude'}]`,
|
|
2656
|
+
value: session.sessionName,
|
|
2182
2657
|
})),
|
|
2183
2658
|
},
|
|
2184
2659
|
]);
|
|
@@ -2186,18 +2661,20 @@ async function pickSession() {
|
|
|
2186
2661
|
}
|
|
2187
2662
|
// ── Commander command ──
|
|
2188
2663
|
exports.dashboardCommand = new commander_1.Command('dashboard')
|
|
2189
|
-
.description('Live TUI dashboard for monitoring
|
|
2664
|
+
.description('Live TUI dashboard for monitoring ekkOS session usage')
|
|
2190
2665
|
.argument('[session-name]', 'ekkOS session name (e.g., dew-pod-hum)')
|
|
2191
2666
|
.option('--latest', 'Auto-detect latest active session')
|
|
2192
2667
|
.option('--wait-for-new', 'Wait for a new session to start (used by ekkos run --dashboard)')
|
|
2193
2668
|
.option('--refresh <ms>', 'Polling interval in ms', '2000')
|
|
2194
2669
|
.option('--compact', 'Minimal layout for small terminals')
|
|
2670
|
+
.option('--provider <name>', 'Session provider override (claude|gemini)')
|
|
2195
2671
|
.action(async (sessionNameArg, options) => {
|
|
2196
2672
|
const refreshMs = parseInt(options.refresh) || 2000;
|
|
2673
|
+
const explicitProvider = normalizeDashboardProvider(options.provider);
|
|
2197
2674
|
// --wait-for-new: poll until session name appears (JSONL may not exist yet)
|
|
2198
2675
|
if (options.waitForNew) {
|
|
2199
2676
|
const result = await waitForNewSession();
|
|
2200
|
-
await launchDashboard(result.sessionName, result.jsonlPath, refreshMs, result.launchCwd, result.launchTs);
|
|
2677
|
+
await launchDashboard(result.sessionName, result.jsonlPath, refreshMs, result.launchCwd, result.launchTs, explicitProvider);
|
|
2201
2678
|
return;
|
|
2202
2679
|
}
|
|
2203
2680
|
let sessionName = null;
|
|
@@ -2221,10 +2698,11 @@ exports.dashboardCommand = new commander_1.Command('dashboard')
|
|
|
2221
2698
|
// stale JSONL files from previous sessions (the lazy resolution already does
|
|
2222
2699
|
// this correctly — this ensures the INITIAL resolve matches).
|
|
2223
2700
|
const launchTs = Date.now();
|
|
2224
|
-
const
|
|
2701
|
+
const launchMetadata = readSessionLaunchMetadata(sessionName);
|
|
2702
|
+
const provider = inferDashboardProvider(explicitProvider, launchMetadata, null);
|
|
2703
|
+
const jsonlPath = resolveTranscriptPath(provider, sessionName, launchTs, null);
|
|
2225
2704
|
if (!jsonlPath) {
|
|
2226
|
-
|
|
2227
|
-
console.log(chalk_1.default.gray(`Waiting for JSONL for "${sessionName}"...`));
|
|
2705
|
+
console.log(chalk_1.default.gray(`Waiting for transcript for "${sessionName}"...`));
|
|
2228
2706
|
}
|
|
2229
|
-
await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, launchTs);
|
|
2707
|
+
await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, launchTs, provider);
|
|
2230
2708
|
});
|