@ekkos/cli 0.3.3 → 1.0.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/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/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- 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/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- 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/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- 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 +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- 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/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- 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/LICENSE +0 -21
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
package/dist/lib/usage-parser.js
CHANGED
|
@@ -33,27 +33,283 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isEkkosSessionName = isEkkosSessionName;
|
|
37
|
+
exports.resolveSessionName = resolveSessionName;
|
|
38
|
+
exports.getSessionUsageByName = getSessionUsageByName;
|
|
39
|
+
exports.listEkkosSessions = listEkkosSessions;
|
|
36
40
|
exports.getAllSessions = getAllSessions;
|
|
37
41
|
exports.getSessionUsage = getSessionUsage;
|
|
38
42
|
exports.getSessionName = getSessionName;
|
|
39
43
|
exports.getCurrentSessionId = getCurrentSessionId;
|
|
40
|
-
|
|
44
|
+
exports.getDailyUsage = getDailyUsage;
|
|
45
|
+
exports.getWeeklyUsage = getWeeklyUsage;
|
|
46
|
+
exports.getMonthlyUsage = getMonthlyUsage;
|
|
47
|
+
exports.getBucketUsage = getBucketUsage;
|
|
41
48
|
const fs = __importStar(require("fs"));
|
|
42
49
|
const os = __importStar(require("os"));
|
|
43
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
|
|
44
303
|
/**
|
|
45
|
-
* Get all sessions using ccusage
|
|
304
|
+
* Get all sessions using ccusage library
|
|
46
305
|
*/
|
|
47
306
|
async function getAllSessions(instanceId) {
|
|
48
307
|
try {
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const data = JSON.parse(result);
|
|
55
|
-
// Convert ccusage format to our format
|
|
56
|
-
const sessions = await Promise.all(data.sessions.map(async (session) => convertCcusageSession(session, instanceId)));
|
|
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)));
|
|
57
313
|
return sessions.filter((s) => s !== null);
|
|
58
314
|
}
|
|
59
315
|
catch (error) {
|
|
@@ -62,22 +318,20 @@ async function getAllSessions(instanceId) {
|
|
|
62
318
|
}
|
|
63
319
|
}
|
|
64
320
|
/**
|
|
65
|
-
* Get specific session usage using ccusage
|
|
321
|
+
* Get specific session usage using ccusage library
|
|
66
322
|
*/
|
|
67
323
|
async function getSessionUsage(sessionId, instanceId) {
|
|
68
324
|
try {
|
|
69
|
-
//
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
stdio: ['pipe', 'pipe', 'ignore'], // Suppress npm warnings
|
|
73
|
-
});
|
|
74
|
-
const data = JSON.parse(result);
|
|
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();
|
|
75
328
|
// Find the specific session
|
|
76
|
-
const session = data.
|
|
329
|
+
const session = data.find((s) => s.sessionId === sessionId);
|
|
77
330
|
if (!session) {
|
|
78
331
|
return null;
|
|
79
332
|
}
|
|
80
|
-
|
|
333
|
+
// Load full turn data for specific session view
|
|
334
|
+
return convertCcusageSession(session, instanceId, false);
|
|
81
335
|
}
|
|
82
336
|
catch (error) {
|
|
83
337
|
console.error('Failed to fetch session from ccusage:', error);
|
|
@@ -86,10 +340,13 @@ async function getSessionUsage(sessionId, instanceId) {
|
|
|
86
340
|
}
|
|
87
341
|
/**
|
|
88
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)
|
|
89
346
|
*/
|
|
90
|
-
async function convertCcusageSession(session, instanceId) {
|
|
91
|
-
// Try to get turn-by-turn data from Claude Code's JSONL files
|
|
92
|
-
const turns = await getTurnMetrics(session.sessionId, instanceId);
|
|
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);
|
|
93
350
|
// Calculate context percentages if we have turn data
|
|
94
351
|
let avgContextPercentage = 0;
|
|
95
352
|
let maxContextPercentage = 0;
|
|
@@ -98,25 +355,30 @@ async function convertCcusageSession(session, instanceId) {
|
|
|
98
355
|
turns.reduce((sum, t) => sum + t.context_percentage, 0) / turns.length;
|
|
99
356
|
maxContextPercentage = Math.max(...turns.map((t) => t.context_percentage));
|
|
100
357
|
}
|
|
101
|
-
// Try to fetch ekkOS pattern metrics from memory API
|
|
358
|
+
// Try to fetch ekkOS pattern metrics from memory API (skip for list view for performance)
|
|
102
359
|
let patternMetrics;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
360
|
+
if (!skipTurns) {
|
|
361
|
+
try {
|
|
362
|
+
patternMetrics = await fetchPatternMetrics(session.sessionId);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// Fail silently if memory API not available
|
|
366
|
+
}
|
|
108
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);
|
|
109
371
|
return {
|
|
110
372
|
session_id: session.sessionId,
|
|
111
373
|
instance_id: instanceId || 'unknown',
|
|
112
374
|
session_name: getSessionName(session.sessionId),
|
|
113
375
|
turn_count: turns.length || 0,
|
|
114
|
-
total_input_tokens: session.inputTokens,
|
|
115
|
-
total_output_tokens: session.outputTokens,
|
|
116
|
-
total_cache_read_tokens: session.cacheReadTokens,
|
|
117
|
-
total_cache_creation_tokens: session.cacheCreationTokens,
|
|
118
|
-
total_tokens:
|
|
119
|
-
total_cost: session.totalCost,
|
|
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,
|
|
120
382
|
avg_context_percentage: avgContextPercentage,
|
|
121
383
|
max_context_percentage: maxContextPercentage,
|
|
122
384
|
started_at: turns[0]?.timestamp || 'unknown',
|
|
@@ -128,52 +390,79 @@ async function convertCcusageSession(session, instanceId) {
|
|
|
128
390
|
}
|
|
129
391
|
/**
|
|
130
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.
|
|
131
395
|
*/
|
|
132
396
|
async function getTurnMetrics(sessionId, instanceId) {
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
path.join(os.homedir(), '.claude', 'projects',
|
|
136
|
-
path.join(os.homedir(), '.config', 'claude', 'projects',
|
|
137
|
-
path.join(os.homedir(), '.codex',
|
|
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),
|
|
138
402
|
];
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
165
447
|
}
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
// Skip invalid JSON lines
|
|
169
448
|
}
|
|
170
449
|
}
|
|
171
|
-
|
|
450
|
+
catch {
|
|
451
|
+
// Skip unreadable files
|
|
452
|
+
}
|
|
172
453
|
}
|
|
173
|
-
|
|
174
|
-
|
|
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;
|
|
175
461
|
}
|
|
176
462
|
}
|
|
463
|
+
catch {
|
|
464
|
+
// Continue to next directory
|
|
465
|
+
}
|
|
177
466
|
}
|
|
178
467
|
return [];
|
|
179
468
|
}
|
|
@@ -236,3 +525,59 @@ function getCurrentSessionId() {
|
|
|
236
525
|
// For now, require user to pass session ID
|
|
237
526
|
return null;
|
|
238
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rotate a log file if it exceeds the maximum size.
|
|
3
|
+
* Shifts existing backups up (e.g., .1 → .2, .2 → .3) and deletes the oldest if it exceeds maxBackups.
|
|
4
|
+
*
|
|
5
|
+
* @param logPath - Path to the log file
|
|
6
|
+
* @param maxBytes - Maximum file size in bytes before rotation (default: 5MB)
|
|
7
|
+
* @param maxBackups - Maximum number of backup files to keep (default: 3)
|
|
8
|
+
*/
|
|
9
|
+
export declare function rotateLogIfNeeded(logPath: string, maxBytes?: number, maxBackups?: number): void;
|
|
10
|
+
/**
|
|
11
|
+
* Append a message to a log file, rotating it first if needed.
|
|
12
|
+
* Creates the directory if it doesn't exist.
|
|
13
|
+
*
|
|
14
|
+
* @param logPath - Path to the log file
|
|
15
|
+
* @param message - Message to append
|
|
16
|
+
* @param maxBytes - Maximum file size in bytes before rotation (default: 5MB)
|
|
17
|
+
*/
|
|
18
|
+
export declare function appendLog(logPath: string, message: string, maxBytes?: number): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.rotateLogIfNeeded = rotateLogIfNeeded;
|
|
7
|
+
exports.appendLog = appendLog;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
/**
|
|
11
|
+
* Rotate a log file if it exceeds the maximum size.
|
|
12
|
+
* Shifts existing backups up (e.g., .1 → .2, .2 → .3) and deletes the oldest if it exceeds maxBackups.
|
|
13
|
+
*
|
|
14
|
+
* @param logPath - Path to the log file
|
|
15
|
+
* @param maxBytes - Maximum file size in bytes before rotation (default: 5MB)
|
|
16
|
+
* @param maxBackups - Maximum number of backup files to keep (default: 3)
|
|
17
|
+
*/
|
|
18
|
+
function rotateLogIfNeeded(logPath, maxBytes = 5 * 1024 * 1024, maxBackups = 3) {
|
|
19
|
+
try {
|
|
20
|
+
// Check if log file exists and get its size
|
|
21
|
+
if (!fs_1.default.existsSync(logPath)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const stats = fs_1.default.statSync(logPath);
|
|
25
|
+
if (stats.size < maxBytes) {
|
|
26
|
+
return; // No rotation needed
|
|
27
|
+
}
|
|
28
|
+
// Delete the oldest backup if it would exceed maxBackups
|
|
29
|
+
const oldestBackup = `${logPath}.${maxBackups}`;
|
|
30
|
+
if (fs_1.default.existsSync(oldestBackup)) {
|
|
31
|
+
fs_1.default.unlinkSync(oldestBackup);
|
|
32
|
+
}
|
|
33
|
+
// Shift existing backups up (e.g., .2 → .3, .1 → .2)
|
|
34
|
+
for (let i = maxBackups - 1; i >= 1; i--) {
|
|
35
|
+
const currentBackup = `${logPath}.${i}`;
|
|
36
|
+
const nextBackup = `${logPath}.${i + 1}`;
|
|
37
|
+
if (fs_1.default.existsSync(currentBackup)) {
|
|
38
|
+
fs_1.default.renameSync(currentBackup, nextBackup);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Rotate the current log to .1
|
|
42
|
+
fs_1.default.renameSync(logPath, `${logPath}.1`);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// Silently fail - logging utilities should never crash the caller
|
|
46
|
+
// In a production scenario, you might want to log this to stderr or a separate error log
|
|
47
|
+
// console.error('[log-rotate] Failed to rotate log:', error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Append a message to a log file, rotating it first if needed.
|
|
52
|
+
* Creates the directory if it doesn't exist.
|
|
53
|
+
*
|
|
54
|
+
* @param logPath - Path to the log file
|
|
55
|
+
* @param message - Message to append
|
|
56
|
+
* @param maxBytes - Maximum file size in bytes before rotation (default: 5MB)
|
|
57
|
+
*/
|
|
58
|
+
function appendLog(logPath, message, maxBytes) {
|
|
59
|
+
try {
|
|
60
|
+
// Create directory if it doesn't exist
|
|
61
|
+
const dir = path_1.default.dirname(logPath);
|
|
62
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
63
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
64
|
+
}
|
|
65
|
+
// Rotate if needed
|
|
66
|
+
rotateLogIfNeeded(logPath, maxBytes);
|
|
67
|
+
// Append message
|
|
68
|
+
fs_1.default.appendFileSync(logPath, message + '\n', 'utf8');
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
// Silently fail - logging utilities should never crash the caller
|
|
72
|
+
// console.error('[log-rotate] Failed to append log:', error);
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/utils/platform.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare const CLAUDE_AGENTS_DIR: string;
|
|
|
15
15
|
export declare const CLAUDE_PLUGINS_DIR: string;
|
|
16
16
|
export declare const CLAUDE_STATE_DIR: string;
|
|
17
17
|
export declare const CLAUDE_MD: string;
|
|
18
|
+
export declare const CLAUDE_RULES_DIR: string;
|
|
19
|
+
export declare const CLAUDE_EKKOS_RULES: string;
|
|
18
20
|
export declare const CURSOR_DIR: string;
|
|
19
21
|
export declare const CURSOR_MCP: string;
|
|
20
22
|
export declare const WINDSURF_DIR: string;
|