@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.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/eviction-client.d.ts +139 -0
- package/dist/capture/eviction-client.js +454 -0
- package/dist/capture/index.d.ts +2 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/jsonl-rewriter.d.ts +96 -0
- package/dist/capture/jsonl-rewriter.js +1369 -0
- package/dist/capture/transcript-repair.d.ts +51 -0
- package/dist/capture/transcript-repair.js +319 -0
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/doctor.js +23 -1
- package/dist/commands/run.d.ts +5 -0
- package/dist/commands/run.js +1605 -516
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/commands/usage.d.ts +7 -0
- package/dist/commands/usage.js +214 -0
- package/dist/cron/index.d.ts +7 -0
- package/dist/cron/index.js +13 -0
- package/dist/cron/promoter.d.ts +70 -0
- package/dist/cron/promoter.js +403 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +262 -5
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-monitor.d.ts +47 -0
- package/dist/lib/usage-monitor.js +124 -0
- package/dist/lib/usage-parser.d.ts +162 -0
- package/dist/lib/usage-parser.js +583 -0
- package/dist/restore/RestoreOrchestrator.d.ts +4 -0
- package/dist/restore/RestoreOrchestrator.js +118 -30
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +9 -2
- package/templates/CLAUDE.md +135 -23
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -215
- 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,
|