@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.
Files changed (81) 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/transcript-repair.d.ts +1 -0
  9. package/dist/capture/transcript-repair.js +12 -1
  10. package/dist/commands/agent.d.ts +6 -0
  11. package/dist/commands/agent.js +244 -0
  12. package/dist/commands/dashboard.d.ts +25 -0
  13. package/dist/commands/dashboard.js +1175 -0
  14. package/dist/commands/run.d.ts +3 -0
  15. package/dist/commands/run.js +503 -350
  16. package/dist/commands/setup-remote.js +146 -37
  17. package/dist/commands/swarm-dashboard.d.ts +20 -0
  18. package/dist/commands/swarm-dashboard.js +735 -0
  19. package/dist/commands/swarm-setup.d.ts +10 -0
  20. package/dist/commands/swarm-setup.js +956 -0
  21. package/dist/commands/swarm.d.ts +46 -0
  22. package/dist/commands/swarm.js +441 -0
  23. package/dist/commands/test-claude.d.ts +16 -0
  24. package/dist/commands/test-claude.js +156 -0
  25. package/dist/commands/usage/blocks.d.ts +8 -0
  26. package/dist/commands/usage/blocks.js +60 -0
  27. package/dist/commands/usage/daily.d.ts +9 -0
  28. package/dist/commands/usage/daily.js +96 -0
  29. package/dist/commands/usage/dashboard.d.ts +8 -0
  30. package/dist/commands/usage/dashboard.js +104 -0
  31. package/dist/commands/usage/formatters.d.ts +41 -0
  32. package/dist/commands/usage/formatters.js +147 -0
  33. package/dist/commands/usage/index.d.ts +13 -0
  34. package/dist/commands/usage/index.js +87 -0
  35. package/dist/commands/usage/monthly.d.ts +8 -0
  36. package/dist/commands/usage/monthly.js +66 -0
  37. package/dist/commands/usage/session.d.ts +11 -0
  38. package/dist/commands/usage/session.js +193 -0
  39. package/dist/commands/usage/weekly.d.ts +9 -0
  40. package/dist/commands/usage/weekly.js +61 -0
  41. package/dist/deploy/instructions.d.ts +5 -2
  42. package/dist/deploy/instructions.js +11 -8
  43. package/dist/index.js +256 -20
  44. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  45. package/dist/lib/tmux-scrollbar.js +296 -0
  46. package/dist/lib/usage-parser.d.ts +95 -5
  47. package/dist/lib/usage-parser.js +416 -71
  48. package/dist/utils/log-rotate.d.ts +18 -0
  49. package/dist/utils/log-rotate.js +74 -0
  50. package/dist/utils/platform.d.ts +2 -0
  51. package/dist/utils/platform.js +3 -1
  52. package/dist/utils/session-binding.d.ts +5 -0
  53. package/dist/utils/session-binding.js +46 -0
  54. package/dist/utils/state.js +4 -0
  55. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  56. package/dist/utils/verify-remote-terminal.js +415 -0
  57. package/package.json +16 -11
  58. package/templates/CLAUDE.md +135 -23
  59. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  60. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  61. package/templates/cursor-hooks/stop.sh +0 -0
  62. package/templates/ekkos-manifest.json +5 -5
  63. package/templates/hooks/assistant-response.sh +0 -0
  64. package/templates/hooks/lib/contract.sh +43 -31
  65. package/templates/hooks/lib/count-tokens.cjs +86 -0
  66. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  67. package/templates/hooks/lib/state.sh +53 -1
  68. package/templates/hooks/session-start.sh +0 -0
  69. package/templates/hooks/stop.sh +150 -388
  70. package/templates/hooks/user-prompt-submit.sh +353 -443
  71. package/templates/plan-template.md +0 -0
  72. package/templates/spec-template.md +0 -0
  73. package/templates/windsurf-hooks/README.md +212 -0
  74. package/templates/windsurf-hooks/hooks.json +9 -2
  75. package/templates/windsurf-hooks/install.sh +148 -0
  76. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  77. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  78. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  79. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  80. package/LICENSE +0 -21
  81. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -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
- const child_process_1 = require("child_process");
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
- // Run ccusage to get all sessions
50
- const result = (0, child_process_1.execSync)('npx -y ccusage@latest session --json --breakdown', {
51
- encoding: 'utf-8',
52
- stdio: ['pipe', 'pipe', 'ignore'], // Suppress npm warnings
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
- // Run ccusage to get all sessions (--id flag doesn't work for IDs with dashes)
70
- const result = (0, child_process_1.execSync)('npx -y ccusage@latest session --json --breakdown', {
71
- encoding: 'utf-8',
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.sessions.find(s => s.sessionId === sessionId);
329
+ const session = data.find((s) => s.sessionId === sessionId);
77
330
  if (!session) {
78
331
  return null;
79
332
  }
80
- return convertCcusageSession(session, instanceId);
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
- try {
104
- patternMetrics = await fetchPatternMetrics(session.sessionId);
105
- }
106
- catch {
107
- // Fail silently if memory API not available
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: session.totalTokens,
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
- // Try multiple possible locations for Claude Code JSONL files
134
- const possiblePaths = [
135
- path.join(os.homedir(), '.claude', 'projects', instanceId || '', `${sessionId}.jsonl`),
136
- path.join(os.homedir(), '.config', 'claude', 'projects', instanceId || '', `${sessionId}.jsonl`),
137
- path.join(os.homedir(), '.codex', instanceId || '', `${sessionId}.jsonl`),
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
- for (const filePath of possiblePaths) {
140
- if (fs.existsSync(filePath)) {
141
- try {
142
- const content = fs.readFileSync(filePath, 'utf-8');
143
- const lines = content.trim().split('\n');
144
- const turns = [];
145
- for (const line of lines) {
146
- try {
147
- const entry = JSON.parse(line);
148
- // Look for usage events
149
- if (entry.type === 'usage' || entry.usage) {
150
- const usage = entry.usage || entry;
151
- turns.push({
152
- turn_number: turns.length,
153
- timestamp: entry.timestamp || new Date().toISOString(),
154
- input_tokens: usage.input_tokens || 0,
155
- output_tokens: usage.output_tokens || 0,
156
- cache_read_tokens: usage.cache_read_input_tokens || 0,
157
- cache_creation_tokens: usage.cache_creation_input_tokens || 0,
158
- total_tokens: (usage.input_tokens || 0) +
159
- (usage.output_tokens || 0) +
160
- (usage.cache_read_input_tokens || 0) +
161
- (usage.cache_creation_input_tokens || 0),
162
- context_percentage: calculateContextPercentage(usage),
163
- model: entry.model || 'unknown',
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
- return turns;
450
+ catch {
451
+ // Skip unreadable files
452
+ }
172
453
  }
173
- catch {
174
- // Continue to next path
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
+ }
@@ -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;