@ekkos/cli 0.2.18 → 1.0.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.
Files changed (98) hide show
  1. package/README.md +57 -0
  2. package/dist/agent/daemon.d.ts +27 -0
  3. package/dist/agent/daemon.js +254 -29
  4. package/dist/agent/health-check.d.ts +35 -0
  5. package/dist/agent/health-check.js +243 -0
  6. package/dist/agent/pty-runner.d.ts +1 -0
  7. package/dist/agent/pty-runner.js +6 -1
  8. package/dist/capture/eviction-client.d.ts +139 -0
  9. package/dist/capture/eviction-client.js +454 -0
  10. package/dist/capture/index.d.ts +2 -0
  11. package/dist/capture/index.js +2 -0
  12. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  13. package/dist/capture/jsonl-rewriter.js +1369 -0
  14. package/dist/capture/transcript-repair.d.ts +51 -0
  15. package/dist/capture/transcript-repair.js +319 -0
  16. package/dist/commands/agent.d.ts +6 -0
  17. package/dist/commands/agent.js +244 -0
  18. package/dist/commands/dashboard.d.ts +25 -0
  19. package/dist/commands/dashboard.js +1175 -0
  20. package/dist/commands/doctor.js +23 -1
  21. package/dist/commands/run.d.ts +5 -0
  22. package/dist/commands/run.js +1605 -516
  23. package/dist/commands/setup-remote.js +146 -37
  24. package/dist/commands/swarm-dashboard.d.ts +20 -0
  25. package/dist/commands/swarm-dashboard.js +735 -0
  26. package/dist/commands/swarm-setup.d.ts +10 -0
  27. package/dist/commands/swarm-setup.js +956 -0
  28. package/dist/commands/swarm.d.ts +46 -0
  29. package/dist/commands/swarm.js +441 -0
  30. package/dist/commands/test-claude.d.ts +16 -0
  31. package/dist/commands/test-claude.js +156 -0
  32. package/dist/commands/usage/blocks.d.ts +8 -0
  33. package/dist/commands/usage/blocks.js +60 -0
  34. package/dist/commands/usage/daily.d.ts +9 -0
  35. package/dist/commands/usage/daily.js +96 -0
  36. package/dist/commands/usage/dashboard.d.ts +8 -0
  37. package/dist/commands/usage/dashboard.js +104 -0
  38. package/dist/commands/usage/formatters.d.ts +41 -0
  39. package/dist/commands/usage/formatters.js +147 -0
  40. package/dist/commands/usage/index.d.ts +13 -0
  41. package/dist/commands/usage/index.js +87 -0
  42. package/dist/commands/usage/monthly.d.ts +8 -0
  43. package/dist/commands/usage/monthly.js +66 -0
  44. package/dist/commands/usage/session.d.ts +11 -0
  45. package/dist/commands/usage/session.js +193 -0
  46. package/dist/commands/usage/weekly.d.ts +9 -0
  47. package/dist/commands/usage/weekly.js +61 -0
  48. package/dist/commands/usage.d.ts +7 -0
  49. package/dist/commands/usage.js +214 -0
  50. package/dist/cron/index.d.ts +7 -0
  51. package/dist/cron/index.js +13 -0
  52. package/dist/cron/promoter.d.ts +70 -0
  53. package/dist/cron/promoter.js +403 -0
  54. package/dist/deploy/instructions.d.ts +5 -2
  55. package/dist/deploy/instructions.js +11 -8
  56. package/dist/index.js +262 -5
  57. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  58. package/dist/lib/tmux-scrollbar.js +296 -0
  59. package/dist/lib/usage-monitor.d.ts +47 -0
  60. package/dist/lib/usage-monitor.js +124 -0
  61. package/dist/lib/usage-parser.d.ts +162 -0
  62. package/dist/lib/usage-parser.js +583 -0
  63. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  64. package/dist/restore/RestoreOrchestrator.js +118 -30
  65. package/dist/utils/log-rotate.d.ts +18 -0
  66. package/dist/utils/log-rotate.js +74 -0
  67. package/dist/utils/platform.d.ts +2 -0
  68. package/dist/utils/platform.js +3 -1
  69. package/dist/utils/session-binding.d.ts +5 -0
  70. package/dist/utils/session-binding.js +46 -0
  71. package/dist/utils/state.js +4 -0
  72. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  73. package/dist/utils/verify-remote-terminal.js +415 -0
  74. package/package.json +9 -2
  75. package/templates/CLAUDE.md +135 -23
  76. package/templates/ekkos-manifest.json +5 -5
  77. package/templates/hooks/lib/contract.sh +43 -31
  78. package/templates/hooks/lib/count-tokens.cjs +86 -0
  79. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  80. package/templates/hooks/lib/state.sh +53 -1
  81. package/templates/hooks/stop.sh +150 -388
  82. package/templates/hooks/user-prompt-submit.sh +353 -443
  83. package/templates/windsurf-hooks/README.md +212 -0
  84. package/templates/windsurf-hooks/hooks.json +9 -2
  85. package/templates/windsurf-hooks/install.sh +148 -0
  86. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  87. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  88. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  89. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  90. package/templates/agents/README.md +0 -182
  91. package/templates/agents/code-reviewer.md +0 -166
  92. package/templates/agents/debug-detective.md +0 -169
  93. package/templates/agents/ekkOS_Vercel.md +0 -99
  94. package/templates/agents/extension-manager.md +0 -229
  95. package/templates/agents/git-companion.md +0 -185
  96. package/templates/agents/github-test-agent.md +0 -321
  97. package/templates/agents/railway-manager.md +0 -215
  98. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -0,0 +1,583 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isEkkosSessionName = isEkkosSessionName;
37
+ exports.resolveSessionName = resolveSessionName;
38
+ exports.getSessionUsageByName = getSessionUsageByName;
39
+ exports.listEkkosSessions = listEkkosSessions;
40
+ exports.getAllSessions = getAllSessions;
41
+ exports.getSessionUsage = getSessionUsage;
42
+ exports.getSessionName = getSessionName;
43
+ exports.getCurrentSessionId = getCurrentSessionId;
44
+ exports.getDailyUsage = getDailyUsage;
45
+ exports.getWeeklyUsage = getWeeklyUsage;
46
+ exports.getMonthlyUsage = getMonthlyUsage;
47
+ exports.getBucketUsage = getBucketUsage;
48
+ const fs = __importStar(require("fs"));
49
+ const os = __importStar(require("os"));
50
+ const path = __importStar(require("path"));
51
+ // ── Anthropic model pricing per 1M tokens ──────────────────────────────────
52
+ const MODEL_PRICING = {
53
+ 'claude-opus-4-6': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
54
+ 'claude-opus-4-5-20250620': { input: 15, output: 75, cacheWrite: 18.75, cacheRead: 1.50 },
55
+ 'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
56
+ 'claude-sonnet-4-5-20250514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
57
+ 'claude-haiku-4-5-20251001': { input: 0.80, output: 4, cacheWrite: 1.00, cacheRead: 0.08 },
58
+ };
59
+ function getModelPricing(modelId) {
60
+ if (MODEL_PRICING[modelId])
61
+ return MODEL_PRICING[modelId];
62
+ if (modelId.includes('opus'))
63
+ return MODEL_PRICING['claude-opus-4-6'];
64
+ if (modelId.includes('sonnet'))
65
+ return MODEL_PRICING['claude-sonnet-4-5-20250929'];
66
+ if (modelId.includes('haiku'))
67
+ return MODEL_PRICING['claude-haiku-4-5-20251001'];
68
+ return MODEL_PRICING['claude-sonnet-4-5-20250929']; // fallback
69
+ }
70
+ function calculateTurnCost(model, usage) {
71
+ const p = getModelPricing(model);
72
+ return ((usage.input_tokens / 1000000) * p.input +
73
+ (usage.output_tokens / 1000000) * p.output +
74
+ (usage.cache_creation_tokens / 1000000) * p.cacheWrite +
75
+ (usage.cache_read_tokens / 1000000) * p.cacheRead);
76
+ }
77
+ /** Detect ekkOS 3-word session names like "lit-lex-zip" */
78
+ function isEkkosSessionName(name) {
79
+ return /^[a-z]+-[a-z]+-[a-z]+$/.test(name);
80
+ }
81
+ function encodeProjectPath(projectPath) {
82
+ return projectPath.replace(/\//g, '-');
83
+ }
84
+ /** Resolve an ekkOS session name to a JSONL UUID */
85
+ function resolveSessionName(name) {
86
+ // 1) Check active-sessions.json (most current)
87
+ const activeSessionsPath = path.join(os.homedir(), '.ekkos', 'active-sessions.json');
88
+ if (fs.existsSync(activeSessionsPath)) {
89
+ try {
90
+ const sessions = JSON.parse(fs.readFileSync(activeSessionsPath, 'utf-8'));
91
+ // Find first entry with a valid UUID sessionId (skip "unknown" or empty)
92
+ const match = sessions.find(s => s.sessionName === name && s.sessionId && s.sessionId !== 'unknown' && s.sessionId.length > 8);
93
+ if (match) {
94
+ return {
95
+ uuid: match.sessionId,
96
+ sessionName: match.sessionName,
97
+ projectPath: match.projectPath,
98
+ encodedProjectPath: encodeProjectPath(match.projectPath),
99
+ startedAt: match.startedAt,
100
+ };
101
+ }
102
+ }
103
+ catch { /* ignore */ }
104
+ }
105
+ // 2) Fallback: scan local cache index
106
+ const cacheDir = path.join(os.homedir(), '.ekkos', 'cache', 'sessions');
107
+ if (fs.existsSync(cacheDir)) {
108
+ try {
109
+ const instances = fs.readdirSync(cacheDir).filter(f => fs.statSync(path.join(cacheDir, f)).isDirectory());
110
+ for (const inst of instances) {
111
+ const indexPath = path.join(cacheDir, inst, 'index.json');
112
+ if (fs.existsSync(indexPath)) {
113
+ const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
114
+ if (index[name]) {
115
+ return {
116
+ uuid: index[name].session_id,
117
+ sessionName: name,
118
+ projectPath: index[name].project_path || '',
119
+ encodedProjectPath: encodeProjectPath(index[name].project_path || ''),
120
+ };
121
+ }
122
+ }
123
+ }
124
+ }
125
+ catch { /* ignore */ }
126
+ }
127
+ return null;
128
+ }
129
+ /** Parse a single JSONL file and return SessionUsage for an ekkOS-named session */
130
+ async function getSessionUsageByName(name) {
131
+ const resolved = resolveSessionName(name);
132
+ if (!resolved)
133
+ return null;
134
+ const jsonlPath = path.join(os.homedir(), '.claude', 'projects', resolved.encodedProjectPath, `${resolved.uuid}.jsonl`);
135
+ if (!fs.existsSync(jsonlPath))
136
+ return null;
137
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
138
+ const lines = content.trim().split('\n');
139
+ const turns = [];
140
+ let totalCost = 0;
141
+ const modelsUsed = new Set();
142
+ let startedAt = '';
143
+ let lastActivity = '';
144
+ const seenMessageIds = new Set();
145
+ for (const line of lines) {
146
+ try {
147
+ const entry = JSON.parse(line);
148
+ if (entry.type === 'assistant' && entry.message?.usage) {
149
+ // Deduplicate: Claude Code writes one JSONL entry per content block
150
+ // (thinking, text, tool_use) but they share the same message.id and usage stats.
151
+ const msgId = entry.message.id;
152
+ if (msgId && seenMessageIds.has(msgId))
153
+ continue;
154
+ if (msgId)
155
+ seenMessageIds.add(msgId);
156
+ const usage = entry.message.usage;
157
+ const model = entry.message.model || 'unknown';
158
+ modelsUsed.add(model);
159
+ const inputTokens = usage.input_tokens || 0;
160
+ const outputTokens = usage.output_tokens || 0;
161
+ const cacheReadTokens = usage.cache_read_input_tokens || 0;
162
+ const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
163
+ const totalTokens = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
164
+ const turnCost = calculateTurnCost(model, {
165
+ input_tokens: inputTokens,
166
+ output_tokens: outputTokens,
167
+ cache_read_tokens: cacheReadTokens,
168
+ cache_creation_tokens: cacheCreationTokens,
169
+ });
170
+ totalCost += turnCost;
171
+ const ts = entry.timestamp || new Date().toISOString();
172
+ if (!startedAt)
173
+ startedAt = ts;
174
+ lastActivity = ts;
175
+ turns.push({
176
+ turn_number: turns.length,
177
+ timestamp: ts,
178
+ input_tokens: inputTokens,
179
+ output_tokens: outputTokens,
180
+ cache_read_tokens: cacheReadTokens,
181
+ cache_creation_tokens: cacheCreationTokens,
182
+ total_tokens: totalTokens,
183
+ context_percentage: calculateContextPercentage(usage),
184
+ model,
185
+ });
186
+ }
187
+ }
188
+ catch { /* skip bad lines */ }
189
+ }
190
+ if (turns.length === 0)
191
+ return null;
192
+ const totalInputTokens = turns.reduce((s, t) => s + t.input_tokens, 0);
193
+ const totalOutputTokens = turns.reduce((s, t) => s + t.output_tokens, 0);
194
+ const totalCacheReadTokens = turns.reduce((s, t) => s + t.cache_read_tokens, 0);
195
+ const totalCacheCreationTokens = turns.reduce((s, t) => s + t.cache_creation_tokens, 0);
196
+ const totalTokens = totalInputTokens + totalOutputTokens + totalCacheReadTokens + totalCacheCreationTokens;
197
+ const avgContextPct = turns.reduce((s, t) => s + t.context_percentage, 0) / turns.length;
198
+ const maxContextPct = Math.max(...turns.map(t => t.context_percentage));
199
+ let patternMetrics;
200
+ try {
201
+ patternMetrics = await fetchPatternMetrics(resolved.uuid);
202
+ }
203
+ catch { /* ok */ }
204
+ return {
205
+ session_id: resolved.uuid,
206
+ instance_id: resolved.encodedProjectPath,
207
+ session_name: name,
208
+ turn_count: turns.length,
209
+ total_input_tokens: totalInputTokens,
210
+ total_output_tokens: totalOutputTokens,
211
+ total_cache_read_tokens: totalCacheReadTokens,
212
+ total_cache_creation_tokens: totalCacheCreationTokens,
213
+ total_tokens: totalTokens,
214
+ total_cost: totalCost,
215
+ avg_context_percentage: avgContextPct,
216
+ max_context_percentage: maxContextPct,
217
+ started_at: startedAt || resolved.startedAt || 'unknown',
218
+ last_activity: lastActivity,
219
+ models_used: Array.from(modelsUsed),
220
+ turns,
221
+ ...patternMetrics,
222
+ };
223
+ }
224
+ /** List ekkOS sessions with lightweight cost data (for session list view) */
225
+ async function listEkkosSessions(limit = 30) {
226
+ const activeSessionsPath = path.join(os.homedir(), '.ekkos', 'active-sessions.json');
227
+ if (!fs.existsSync(activeSessionsPath))
228
+ return [];
229
+ let sessions;
230
+ try {
231
+ sessions = JSON.parse(fs.readFileSync(activeSessionsPath, 'utf-8'));
232
+ }
233
+ catch {
234
+ return [];
235
+ }
236
+ // Filter to valid 3-word ekkOS names only, sort by lastHeartbeat descending
237
+ sessions = sessions
238
+ .filter(s => isEkkosSessionName(s.sessionName))
239
+ .sort((a, b) => (b.lastHeartbeat || '').localeCompare(a.lastHeartbeat || ''))
240
+ .slice(0, limit);
241
+ const results = [];
242
+ for (const s of sessions) {
243
+ const encoded = encodeProjectPath(s.projectPath);
244
+ const jsonlPath = path.join(os.homedir(), '.claude', 'projects', encoded, `${s.sessionId}.jsonl`);
245
+ let cost = 0;
246
+ let tokens = 0;
247
+ let turnCount = 0;
248
+ const models = new Set();
249
+ const seenMsgIds = new Set();
250
+ if (fs.existsSync(jsonlPath)) {
251
+ try {
252
+ const content = fs.readFileSync(jsonlPath, 'utf-8');
253
+ for (const line of content.trim().split('\n')) {
254
+ try {
255
+ const entry = JSON.parse(line);
256
+ if (entry.type === 'assistant' && entry.message?.usage) {
257
+ // Deduplicate: multiple content blocks per API call share same message.id
258
+ const msgId = entry.message.id;
259
+ if (msgId && seenMsgIds.has(msgId))
260
+ continue;
261
+ if (msgId)
262
+ seenMsgIds.add(msgId);
263
+ const u = entry.message.usage;
264
+ const model = entry.message.model || 'unknown';
265
+ models.add(model);
266
+ turnCount++;
267
+ const inp = u.input_tokens || 0;
268
+ const out = u.output_tokens || 0;
269
+ const cr = u.cache_read_input_tokens || 0;
270
+ const cc = u.cache_creation_input_tokens || 0;
271
+ tokens += inp + out + cr + cc;
272
+ cost += calculateTurnCost(model, {
273
+ input_tokens: inp,
274
+ output_tokens: out,
275
+ cache_read_tokens: cr,
276
+ cache_creation_tokens: cc,
277
+ });
278
+ }
279
+ }
280
+ catch { /* skip */ }
281
+ }
282
+ }
283
+ catch { /* skip */ }
284
+ }
285
+ // Only include sessions that have actual JSONL data
286
+ if (turnCount > 0) {
287
+ results.push({
288
+ name: s.sessionName,
289
+ uuid: s.sessionId,
290
+ projectPath: s.projectPath,
291
+ startedAt: s.startedAt,
292
+ lastHeartbeat: s.lastHeartbeat,
293
+ cost,
294
+ tokens,
295
+ models: Array.from(models),
296
+ turnCount,
297
+ });
298
+ }
299
+ }
300
+ return results;
301
+ }
302
+ // Removed CcusageOutput - using direct import from ccusage/data-loader
303
+ /**
304
+ * Get all sessions using ccusage library
305
+ */
306
+ async function getAllSessions(instanceId) {
307
+ try {
308
+ // Use dynamic import for ESM module ccusage (type assertion to bypass TS module resolution)
309
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
310
+ const data = await ccusage.loadSessionData();
311
+ // Convert ccusage format to our format (skip turn loading for performance in list view)
312
+ const sessions = await Promise.all(data.map(async (session) => convertCcusageSession(session, instanceId, true)));
313
+ return sessions.filter((s) => s !== null);
314
+ }
315
+ catch (error) {
316
+ console.error('Failed to fetch sessions from ccusage:', error);
317
+ return [];
318
+ }
319
+ }
320
+ /**
321
+ * Get specific session usage using ccusage library
322
+ */
323
+ async function getSessionUsage(sessionId, instanceId) {
324
+ try {
325
+ // Use dynamic import for ESM module ccusage (type assertion to bypass TS module resolution)
326
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
327
+ const data = await ccusage.loadSessionData();
328
+ // Find the specific session
329
+ const session = data.find((s) => s.sessionId === sessionId);
330
+ if (!session) {
331
+ return null;
332
+ }
333
+ // Load full turn data for specific session view
334
+ return convertCcusageSession(session, instanceId, false);
335
+ }
336
+ catch (error) {
337
+ console.error('Failed to fetch session from ccusage:', error);
338
+ return null;
339
+ }
340
+ }
341
+ /**
342
+ * Convert ccusage session to our SessionUsage format
343
+ * @param session - ccusage session data
344
+ * @param instanceId - project instance ID
345
+ * @param skipTurns - skip loading turn-by-turn data (for performance in list view)
346
+ */
347
+ async function convertCcusageSession(session, instanceId, skipTurns = false) {
348
+ // Try to get turn-by-turn data from Claude Code's JSONL files (unless skipped for performance)
349
+ const turns = skipTurns ? [] : await getTurnMetrics(session.sessionId, instanceId);
350
+ // Calculate context percentages if we have turn data
351
+ let avgContextPercentage = 0;
352
+ let maxContextPercentage = 0;
353
+ if (turns.length > 0) {
354
+ avgContextPercentage =
355
+ turns.reduce((sum, t) => sum + t.context_percentage, 0) / turns.length;
356
+ maxContextPercentage = Math.max(...turns.map((t) => t.context_percentage));
357
+ }
358
+ // Try to fetch ekkOS pattern metrics from memory API (skip for list view for performance)
359
+ let patternMetrics;
360
+ if (!skipTurns) {
361
+ try {
362
+ patternMetrics = await fetchPatternMetrics(session.sessionId);
363
+ }
364
+ catch {
365
+ // Fail silently if memory API not available
366
+ }
367
+ }
368
+ // Compute total tokens (ccusage doesn't provide a totalTokens field)
369
+ const totalTokens = (session.inputTokens || 0) + (session.outputTokens || 0) +
370
+ (session.cacheCreationTokens || 0) + (session.cacheReadTokens || 0);
371
+ return {
372
+ session_id: session.sessionId,
373
+ instance_id: instanceId || 'unknown',
374
+ session_name: getSessionName(session.sessionId),
375
+ turn_count: turns.length || 0,
376
+ total_input_tokens: session.inputTokens || 0,
377
+ total_output_tokens: session.outputTokens || 0,
378
+ total_cache_read_tokens: session.cacheReadTokens || 0,
379
+ total_cache_creation_tokens: session.cacheCreationTokens || 0,
380
+ total_tokens: totalTokens,
381
+ total_cost: session.totalCost || session.cost || 0,
382
+ avg_context_percentage: avgContextPercentage,
383
+ max_context_percentage: maxContextPercentage,
384
+ started_at: turns[0]?.timestamp || 'unknown',
385
+ last_activity: session.lastActivity,
386
+ models_used: session.modelsUsed,
387
+ turns,
388
+ ...patternMetrics,
389
+ };
390
+ }
391
+ /**
392
+ * Get turn-by-turn metrics from Claude Code JSONL files
393
+ * sessionId from ccusage is the project path (e.g., '-Volumes-MacMiniPort-DEV-EKKOS')
394
+ * but JSONL files are named by UUID. So we need to scan all files in the directory.
395
+ */
396
+ async function getTurnMetrics(sessionId, instanceId) {
397
+ // Possible project directories
398
+ const possibleDirs = [
399
+ path.join(os.homedir(), '.claude', 'projects', sessionId),
400
+ path.join(os.homedir(), '.config', 'claude', 'projects', sessionId),
401
+ path.join(os.homedir(), '.codex', sessionId),
402
+ ];
403
+ const allTurns = [];
404
+ const seenMsgIds = new Set();
405
+ for (const dir of possibleDirs) {
406
+ if (!fs.existsSync(dir))
407
+ continue;
408
+ try {
409
+ // Get all JSONL files in this directory
410
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl'));
411
+ for (const file of files) {
412
+ const filePath = path.join(dir, file);
413
+ try {
414
+ const content = fs.readFileSync(filePath, 'utf-8');
415
+ const lines = content.trim().split('\n');
416
+ for (const line of lines) {
417
+ try {
418
+ const entry = JSON.parse(line);
419
+ // Look for assistant messages with usage data
420
+ // Claude Code stores usage in entry.message.usage for type='assistant' entries
421
+ if (entry.type === 'assistant' && entry.message?.usage) {
422
+ // Deduplicate: multiple content blocks per API call share same message.id
423
+ const msgId = entry.message?.id;
424
+ if (msgId && seenMsgIds.has(msgId))
425
+ continue;
426
+ if (msgId)
427
+ seenMsgIds.add(msgId);
428
+ const usage = entry.message.usage;
429
+ allTurns.push({
430
+ turn_number: allTurns.length,
431
+ timestamp: entry.timestamp || new Date().toISOString(),
432
+ input_tokens: usage.input_tokens || 0,
433
+ output_tokens: usage.output_tokens || 0,
434
+ cache_read_tokens: usage.cache_read_input_tokens || 0,
435
+ cache_creation_tokens: usage.cache_creation_input_tokens || 0,
436
+ total_tokens: (usage.input_tokens || 0) +
437
+ (usage.output_tokens || 0) +
438
+ (usage.cache_read_input_tokens || 0) +
439
+ (usage.cache_creation_input_tokens || 0),
440
+ context_percentage: calculateContextPercentage(usage),
441
+ model: entry.message?.model || 'unknown',
442
+ });
443
+ }
444
+ }
445
+ catch {
446
+ // Skip invalid JSON lines
447
+ }
448
+ }
449
+ }
450
+ catch {
451
+ // Skip unreadable files
452
+ }
453
+ }
454
+ // If we found any turns, return them (no need to check other directories)
455
+ if (allTurns.length > 0) {
456
+ // Sort by timestamp
457
+ allTurns.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
458
+ // Renumber turns after sorting
459
+ allTurns.forEach((turn, i) => turn.turn_number = i);
460
+ return allTurns;
461
+ }
462
+ }
463
+ catch {
464
+ // Continue to next directory
465
+ }
466
+ }
467
+ return [];
468
+ }
469
+ /**
470
+ * Calculate context percentage from usage data
471
+ */
472
+ function calculateContextPercentage(usage) {
473
+ const totalTokens = (usage.input_tokens || 0) +
474
+ (usage.cache_read_input_tokens || 0) +
475
+ (usage.cache_creation_input_tokens || 0);
476
+ // Claude Code context window is typically 200k tokens
477
+ const contextWindow = 200000;
478
+ return (totalTokens / contextWindow) * 100;
479
+ }
480
+ /**
481
+ * Fetch pattern metrics from ekkOS memory API
482
+ */
483
+ async function fetchPatternMetrics(sessionId) {
484
+ try {
485
+ const response = await fetch(`http://localhost:3001/api/v1/session/stats?session_id=${sessionId}`, {
486
+ method: 'GET',
487
+ headers: {
488
+ 'Content-Type': 'application/json',
489
+ },
490
+ });
491
+ if (!response.ok) {
492
+ return null;
493
+ }
494
+ const data = await response.json();
495
+ return {
496
+ patterns_retrieved: data.patterns_retrieved || 0,
497
+ patterns_applied: data.patterns_applied || 0,
498
+ patterns_learned: data.patterns_learned || 0,
499
+ confidence_gain: data.confidence_gain || 0,
500
+ };
501
+ }
502
+ catch {
503
+ return null;
504
+ }
505
+ }
506
+ /**
507
+ * Get human-readable session name
508
+ */
509
+ function getSessionName(sessionId) {
510
+ // Extract last part of path-based session IDs
511
+ const parts = sessionId.split('/');
512
+ const lastPart = parts[parts.length - 1];
513
+ // If it's a UUID-like string, try to map to session name
514
+ // For now, just return a shortened version
515
+ if (lastPart.length > 20) {
516
+ return lastPart.substring(0, 8) + '...' + lastPart.substring(lastPart.length - 8);
517
+ }
518
+ return lastPart;
519
+ }
520
+ /**
521
+ * Get current session ID from active Claude Code process
522
+ */
523
+ function getCurrentSessionId() {
524
+ // TODO: Extract from process environment or Claude Code state
525
+ // For now, require user to pass session ID
526
+ return null;
527
+ }
528
+ /**
529
+ * Load daily usage data via ccusage library
530
+ */
531
+ async function getDailyUsage() {
532
+ try {
533
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
534
+ const data = await ccusage.loadDailyUsageData();
535
+ return data;
536
+ }
537
+ catch (error) {
538
+ console.error('Failed to load daily usage:', error);
539
+ return [];
540
+ }
541
+ }
542
+ /**
543
+ * Load weekly usage data via ccusage library
544
+ */
545
+ async function getWeeklyUsage() {
546
+ try {
547
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
548
+ const data = await ccusage.loadWeeklyUsageData();
549
+ return data;
550
+ }
551
+ catch (error) {
552
+ console.error('Failed to load weekly usage:', error);
553
+ return [];
554
+ }
555
+ }
556
+ /**
557
+ * Load monthly usage data via ccusage library
558
+ */
559
+ async function getMonthlyUsage() {
560
+ try {
561
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
562
+ const data = await ccusage.loadMonthlyUsageData();
563
+ return data;
564
+ }
565
+ catch (error) {
566
+ console.error('Failed to load monthly usage:', error);
567
+ return [];
568
+ }
569
+ }
570
+ /**
571
+ * Load bucket (5-hour block) usage data via ccusage library
572
+ */
573
+ async function getBucketUsage() {
574
+ try {
575
+ const ccusage = await Promise.resolve(`${'ccusage/data-loader'}`).then(s => __importStar(require(s)));
576
+ const data = await ccusage.loadBucketUsageData();
577
+ return data;
578
+ }
579
+ catch (error) {
580
+ console.error('Failed to load bucket usage:', error);
581
+ return [];
582
+ }
583
+ }
@@ -30,6 +30,10 @@ export declare class RestoreOrchestrator {
30
30
  * Main restore function - attempts tiers in order
31
31
  */
32
32
  restore(options?: RestoreOptions): Promise<CacheResult<RestorePayload>>;
33
+ /**
34
+ * Scan payload for [ekkOS:page-out:...] stubs and rehydrate from Proxy
35
+ */
36
+ private rehydratePayload;
33
37
  /**
34
38
  * Tier -1: Restore from stream log (has mid-turn content)
35
39
  * This is checked FIRST because stream logs have the most recent data,