agentlytics 0.0.8 → 0.0.11
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 +14 -2
- package/cache.js +67 -24
- package/editors/codex.js +453 -0
- package/editors/commandcode.js +159 -0
- package/editors/index.js +3 -1
- package/index.js +13 -3
- package/package.json +3 -2
- package/server.js +15 -4
- package/ui/src/App.jsx +40 -3
- package/ui/src/components/ActivityHeatmap.jsx +11 -7
- package/ui/src/components/ChatSidebar.jsx +313 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/index.css +6 -0
- package/ui/src/lib/api.js +16 -2
- package/ui/src/lib/constants.js +16 -0
- package/ui/src/pages/Dashboard.jsx +14 -14
- package/ui/src/pages/DeepAnalysis.jsx +6 -3
- package/ui/src/pages/ProjectDetail.jsx +236 -0
- package/ui/src/pages/Projects.jsx +50 -121
- package/ui/src/pages/Sessions.jsx +58 -46
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Unified analytics for your AI coding agents</strong><br>
|
|
9
|
-
<sub>Cursor · Windsurf · Claude Code · VS Code Copilot · Zed · Antigravity · OpenCode · Gemini CLI · Copilot CLI · Cursor Agent</sub>
|
|
9
|
+
<sub>Cursor · Windsurf · Claude Code · VS Code Copilot · Zed · Antigravity · OpenCode · Codex · Gemini CLI · Copilot CLI · Cursor Agent · Command Code</sub>
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
<a href="https://www.npmjs.com/package/agentlytics"><img src="https://img.shields.io/npm/v/agentlytics?color=6366f1&label=npm" alt="npm"></a>
|
|
14
|
-
<a href="#supported-editors"><img src="https://img.shields.io/badge/editors-
|
|
14
|
+
<a href="#supported-editors"><img src="https://img.shields.io/badge/editors-14-818cf8" alt="editors"></a>
|
|
15
15
|
<a href="#license"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
|
|
16
16
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A518-brightgreen" alt="node"></a>
|
|
17
17
|
</p>
|
|
@@ -32,6 +32,14 @@ npx agentlytics
|
|
|
32
32
|
|
|
33
33
|
Opens at **http://localhost:4637**. Requires Node.js ≥ 18, macOS.
|
|
34
34
|
|
|
35
|
+
To only build the cache database without starting the server:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx agentlytics --collect
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For local development, run `npm run dev` from the repo root. That starts both the backend on `http://localhost:4637` and the Vite frontend on `http://localhost:5173`.
|
|
42
|
+
|
|
35
43
|
## Features
|
|
36
44
|
|
|
37
45
|
- **Dashboard** — KPIs, activity heatmap, editor breakdown, coding streaks, token economy, peak hours, top models & tools
|
|
@@ -54,12 +62,16 @@ Opens at **http://localhost:4637**. Requires Node.js ≥ 18, macOS.
|
|
|
54
62
|
| **VS Code Insiders** | `vscode-insiders` | ✅ | ✅ | ✅ | ✅ |
|
|
55
63
|
| **Zed** | `zed` | ✅ | ✅ | ✅ | ❌ |
|
|
56
64
|
| **OpenCode** | `opencode` | ✅ | ✅ | ✅ | ✅ |
|
|
65
|
+
| **Codex** | `codex` | ✅ | ✅ | ✅ | ✅ |
|
|
57
66
|
| **Gemini CLI** | `gemini-cli` | ✅ | ✅ | ✅ | ✅ |
|
|
58
67
|
| **Copilot CLI** | `copilot-cli` | ✅ | ✅ | ✅ | ✅ |
|
|
59
68
|
| **Cursor Agent** | `cursor-agent` | ✅ | ❌ | ❌ | ❌ |
|
|
69
|
+
| **Command Code** | `commandcode` | ✅ | ✅ | ❌ | ❌ |
|
|
60
70
|
|
|
61
71
|
> Windsurf, Windsurf Next, and Antigravity must be running during scan.
|
|
62
72
|
|
|
73
|
+
Codex sessions are read from `${CODEX_HOME:-~/.codex}/sessions/**/*.jsonl`. Reasoning summaries may appear in transcripts when Codex records them in clear text, but encrypted reasoning content is not readable. Codex Desktop and CLI sessions are aggregated into one `codex` editor in analytics.
|
|
74
|
+
|
|
63
75
|
## How It Works
|
|
64
76
|
|
|
65
77
|
```
|
package/cache.js
CHANGED
|
@@ -106,6 +106,9 @@ const insertMsg = () => db.prepare(`
|
|
|
106
106
|
INSERT INTO messages (chat_id, seq, role, content, model, input_tokens, output_tokens)
|
|
107
107
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
108
108
|
`);
|
|
109
|
+
const updateChatBubbleCount = () => db.prepare(`
|
|
110
|
+
UPDATE chats SET bubble_count = ? WHERE id = ?
|
|
111
|
+
`);
|
|
109
112
|
|
|
110
113
|
function analyzeAndStore(chat) {
|
|
111
114
|
if (chat.encrypted) return;
|
|
@@ -127,6 +130,7 @@ function analyzeAndStore(chat) {
|
|
|
127
130
|
delTc.run(chat.composerId);
|
|
128
131
|
|
|
129
132
|
const ins = insertMsg();
|
|
133
|
+
const updBubbleCount = updateChatBubbleCount();
|
|
130
134
|
const insTc = db.prepare('INSERT INTO tool_calls (chat_id, tool_name, args_json, source, folder, timestamp) VALUES (?, ?, ?, ?, ?, ?)');
|
|
131
135
|
const chatTs = chat.lastUpdatedAt || chat.createdAt || null;
|
|
132
136
|
|
|
@@ -174,6 +178,8 @@ function analyzeAndStore(chat) {
|
|
|
174
178
|
ins.run(chat.composerId, seq++, msg.role, storedContent, msg._model || null, msg._inputTokens || null, msg._outputTokens || null);
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
updBubbleCount.run(messages.length, chat.composerId);
|
|
182
|
+
|
|
177
183
|
const insStat = insertStat();
|
|
178
184
|
insStat.run(
|
|
179
185
|
chat.composerId, stats.total, stats.user, stats.assistant, stats.tool, stats.system,
|
|
@@ -205,7 +211,7 @@ function scanAll(onProgress) {
|
|
|
205
211
|
chat.composerId, chat.source, chat.name || null, chat.mode || null,
|
|
206
212
|
chat.folder || null, chat.createdAt || null, chat.lastUpdatedAt || null,
|
|
207
213
|
chat.encrypted ? 1 : 0, chat.bubbleCount || 0,
|
|
208
|
-
JSON.stringify({ _type: chat._type, _dbPath: chat._dbPath, _filePath: chat._filePath, _port: chat._port, _csrf: chat._csrf, _https: chat._https, _rootBlobId: chat._rootBlobId, _dataType: chat._dataType })
|
|
214
|
+
JSON.stringify({ _type: chat._type, _dbPath: chat._dbPath, _filePath: chat._filePath, _port: chat._port, _csrf: chat._csrf, _https: chat._https, _rootBlobId: chat._rootBlobId, _dataType: chat._dataType, _rawSource: chat._rawSource, _originator: chat._originator, _cliVersion: chat._cliVersion, _modelProvider: chat._modelProvider })
|
|
209
215
|
);
|
|
210
216
|
}
|
|
211
217
|
});
|
|
@@ -256,15 +262,30 @@ function scanAll(onProgress) {
|
|
|
256
262
|
// ============================================================
|
|
257
263
|
|
|
258
264
|
function getCachedChats(opts = {}) {
|
|
259
|
-
let sql = 'SELECT
|
|
265
|
+
let sql = 'SELECT c.*, cs.models AS _models FROM chats c LEFT JOIN chat_stats cs ON cs.chat_id = c.id WHERE 1=1';
|
|
260
266
|
const params = [];
|
|
261
|
-
if (opts.editor) { sql += ' AND source LIKE ?'; params.push(`%${opts.editor}%`); }
|
|
262
|
-
if (opts.folder) { sql += ' AND folder LIKE ?'; params.push(`%${opts.folder}%`); }
|
|
263
|
-
if (opts.named !== false) { sql += ' AND (name IS NOT NULL OR bubble_count > 0)'; }
|
|
264
|
-
sql += '
|
|
267
|
+
if (opts.editor) { sql += ' AND c.source LIKE ?'; params.push(`%${opts.editor}%`); }
|
|
268
|
+
if (opts.folder) { sql += ' AND c.folder LIKE ?'; params.push(`%${opts.folder}%`); }
|
|
269
|
+
if (opts.named !== false) { sql += ' AND (c.name IS NOT NULL OR c.bubble_count > 0)'; }
|
|
270
|
+
if (opts.dateFrom) { sql += ' AND COALESCE(c.last_updated_at, c.created_at) >= ?'; params.push(opts.dateFrom); }
|
|
271
|
+
if (opts.dateTo) { sql += ' AND COALESCE(c.last_updated_at, c.created_at) <= ?'; params.push(opts.dateTo); }
|
|
272
|
+
sql += ' ORDER BY c.last_updated_at DESC';
|
|
265
273
|
if (opts.limit) { sql += ' LIMIT ?'; params.push(opts.limit); }
|
|
266
274
|
if (opts.offset) { sql += ' OFFSET ?'; params.push(opts.offset); }
|
|
267
|
-
|
|
275
|
+
const rows = db.prepare(sql).all(params);
|
|
276
|
+
for (const r of rows) {
|
|
277
|
+
r.top_model = null;
|
|
278
|
+
try {
|
|
279
|
+
const models = JSON.parse(r._models || '[]');
|
|
280
|
+
if (models.length > 0) {
|
|
281
|
+
const freq = {};
|
|
282
|
+
for (const m of models) freq[m] = (freq[m] || 0) + 1;
|
|
283
|
+
r.top_model = Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0];
|
|
284
|
+
}
|
|
285
|
+
} catch {}
|
|
286
|
+
delete r._models;
|
|
287
|
+
}
|
|
288
|
+
return rows;
|
|
268
289
|
}
|
|
269
290
|
|
|
270
291
|
function countCachedChats(opts = {}) {
|
|
@@ -273,14 +294,20 @@ function countCachedChats(opts = {}) {
|
|
|
273
294
|
if (opts.editor) { sql += ' AND source LIKE ?'; params.push(`%${opts.editor}%`); }
|
|
274
295
|
if (opts.folder) { sql += ' AND folder LIKE ?'; params.push(`%${opts.folder}%`); }
|
|
275
296
|
if (opts.named !== false) { sql += ' AND (name IS NOT NULL OR bubble_count > 0)'; }
|
|
297
|
+
if (opts.dateFrom) { sql += ' AND COALESCE(last_updated_at, created_at) >= ?'; params.push(opts.dateFrom); }
|
|
298
|
+
if (opts.dateTo) { sql += ' AND COALESCE(last_updated_at, created_at) <= ?'; params.push(opts.dateTo); }
|
|
276
299
|
return db.prepare(sql).get(params).cnt;
|
|
277
300
|
}
|
|
278
301
|
|
|
279
302
|
function getCachedOverview(opts = {}) {
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const
|
|
283
|
-
|
|
303
|
+
// Build conditions dynamically to support editor + date range filters
|
|
304
|
+
const conditions = [];
|
|
305
|
+
const params = [];
|
|
306
|
+
if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
|
|
307
|
+
if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
|
|
308
|
+
if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
|
|
309
|
+
const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
|
|
310
|
+
const whereAnd = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : '';
|
|
284
311
|
|
|
285
312
|
const totalChats = db.prepare(`SELECT COUNT(*) as cnt FROM chats${where}`).get(...params).cnt;
|
|
286
313
|
// Editors list is always unfiltered so the breakdown remains visible
|
|
@@ -338,9 +365,12 @@ function getCachedOverview(opts = {}) {
|
|
|
338
365
|
}
|
|
339
366
|
|
|
340
367
|
function getCachedDailyActivity(opts = {}) {
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
368
|
+
const conditions = [];
|
|
369
|
+
const params = [];
|
|
370
|
+
if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
|
|
371
|
+
if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
|
|
372
|
+
if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
|
|
373
|
+
const whereAnd = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : '';
|
|
344
374
|
const rows = db.prepare(`
|
|
345
375
|
SELECT
|
|
346
376
|
date(COALESCE(last_updated_at, created_at)/1000, 'unixepoch', 'localtime') as day,
|
|
@@ -369,6 +399,8 @@ function getCachedDeepAnalytics(opts = {}) {
|
|
|
369
399
|
const params = [];
|
|
370
400
|
if (opts.editor) { sql += ' AND c.source LIKE ?'; params.push(`%${opts.editor}%`); }
|
|
371
401
|
if (opts.folder) { sql += ' AND c.folder = ?'; params.push(opts.folder); }
|
|
402
|
+
if (opts.dateFrom) { sql += ' AND COALESCE(c.last_updated_at, c.created_at) >= ?'; params.push(opts.dateFrom); }
|
|
403
|
+
if (opts.dateTo) { sql += ' AND COALESCE(c.last_updated_at, c.created_at) <= ?'; params.push(opts.dateTo); }
|
|
372
404
|
sql += ' ORDER BY cs.analyzed_at DESC';
|
|
373
405
|
if (opts.limit) { sql += ' LIMIT ?'; params.push(opts.limit); }
|
|
374
406
|
|
|
@@ -454,16 +486,22 @@ function getCachedChat(id) {
|
|
|
454
486
|
};
|
|
455
487
|
}
|
|
456
488
|
|
|
457
|
-
function getCachedProjects() {
|
|
489
|
+
function getCachedProjects(opts = {}) {
|
|
490
|
+
// Build date filter
|
|
491
|
+
let dateFilter = '';
|
|
492
|
+
const dateParams = [];
|
|
493
|
+
if (opts.dateFrom) { dateFilter += ' AND COALESCE(last_updated_at, created_at) >= ?'; dateParams.push(opts.dateFrom); }
|
|
494
|
+
if (opts.dateTo) { dateFilter += ' AND COALESCE(last_updated_at, created_at) <= ?'; dateParams.push(opts.dateTo); }
|
|
495
|
+
|
|
458
496
|
// All unique projects with their stats
|
|
459
497
|
const projects = db.prepare(`
|
|
460
498
|
SELECT folder, source, COUNT(*) as count,
|
|
461
499
|
MIN(COALESCE(last_updated_at, created_at)) as first_seen,
|
|
462
500
|
MAX(COALESCE(last_updated_at, created_at)) as last_seen
|
|
463
|
-
FROM chats WHERE folder IS NOT NULL
|
|
501
|
+
FROM chats WHERE folder IS NOT NULL${dateFilter}
|
|
464
502
|
GROUP BY folder, source
|
|
465
503
|
ORDER BY folder, count DESC
|
|
466
|
-
`).all();
|
|
504
|
+
`).all(...dateParams);
|
|
467
505
|
|
|
468
506
|
// Group by folder
|
|
469
507
|
const map = {};
|
|
@@ -478,12 +516,13 @@ function getCachedProjects() {
|
|
|
478
516
|
// For each project, get models and tools from chat_stats
|
|
479
517
|
const result = [];
|
|
480
518
|
for (const [folder, proj] of Object.entries(map)) {
|
|
519
|
+
const statsDateFilter = dateFilter.replace(/COALESCE\(last_updated_at/g, 'COALESCE(c.last_updated_at').replace(/created_at\)/g, 'c.created_at)');
|
|
481
520
|
const stats = db.prepare(`
|
|
482
521
|
SELECT cs.models, cs.tool_calls, cs.total_messages, cs.total_input_tokens, cs.total_output_tokens,
|
|
483
522
|
cs.total_user_chars, cs.total_assistant_chars, cs.total_cache_read, cs.total_cache_write
|
|
484
523
|
FROM chat_stats cs JOIN chats c ON cs.chat_id = c.id
|
|
485
|
-
WHERE c.folder =
|
|
486
|
-
`).all(folder);
|
|
524
|
+
WHERE c.folder = ?${statsDateFilter}
|
|
525
|
+
`).all(folder, ...dateParams);
|
|
487
526
|
|
|
488
527
|
const modelFreq = {};
|
|
489
528
|
const toolFreq = {};
|
|
@@ -585,7 +624,7 @@ async function scanAllAsync(onProgress) {
|
|
|
585
624
|
chat.composerId, chat.source, chat.name || null, chat.mode || null,
|
|
586
625
|
chat.folder || null, chat.createdAt || null, chat.lastUpdatedAt || null,
|
|
587
626
|
chat.encrypted ? 1 : 0, chat.bubbleCount || 0,
|
|
588
|
-
JSON.stringify({ _type: chat._type, _dbPath: chat._dbPath, _filePath: chat._filePath, _port: chat._port, _csrf: chat._csrf, _https: chat._https, _rootBlobId: chat._rootBlobId, _dataType: chat._dataType })
|
|
627
|
+
JSON.stringify({ _type: chat._type, _dbPath: chat._dbPath, _filePath: chat._filePath, _port: chat._port, _csrf: chat._csrf, _https: chat._https, _rootBlobId: chat._rootBlobId, _dataType: chat._dataType, _rawSource: chat._rawSource, _originator: chat._originator, _cliVersion: chat._cliVersion, _modelProvider: chat._modelProvider })
|
|
589
628
|
);
|
|
590
629
|
}
|
|
591
630
|
});
|
|
@@ -637,10 +676,14 @@ async function resetAndRescanAsync(onProgress) {
|
|
|
637
676
|
}
|
|
638
677
|
|
|
639
678
|
function getCachedDashboardStats(opts = {}) {
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
const
|
|
643
|
-
|
|
679
|
+
// Build conditions dynamically to support editor + date range filters
|
|
680
|
+
const conditions = [];
|
|
681
|
+
const params = [];
|
|
682
|
+
if (opts.editor) { conditions.push('source = ?'); params.push(opts.editor); }
|
|
683
|
+
if (opts.dateFrom) { conditions.push('COALESCE(last_updated_at, created_at) >= ?'); params.push(opts.dateFrom); }
|
|
684
|
+
if (opts.dateTo) { conditions.push('COALESCE(last_updated_at, created_at) <= ?'); params.push(opts.dateTo); }
|
|
685
|
+
const where = conditions.length > 0 ? ' WHERE ' + conditions.join(' AND ') : '';
|
|
686
|
+
const whereAnd = conditions.length > 0 ? ' AND ' + conditions.join(' AND ') : '';
|
|
644
687
|
|
|
645
688
|
// ── Hourly distribution (aggregate across all days) ──
|
|
646
689
|
const hourlyRows = db.prepare(`
|
package/editors/codex.js
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const name = 'codex';
|
|
6
|
+
const DEFAULT_CODEX_HOME = path.join(os.homedir(), '.codex');
|
|
7
|
+
const SESSION_SUBDIR = 'sessions';
|
|
8
|
+
const MAX_TOOL_RESULT_PREVIEW = 500;
|
|
9
|
+
|
|
10
|
+
function getChats() {
|
|
11
|
+
const sessionsDir = getSessionsDir();
|
|
12
|
+
if (!fs.existsSync(sessionsDir)) return [];
|
|
13
|
+
|
|
14
|
+
const chats = [];
|
|
15
|
+
for (const filePath of walkJsonlFiles(sessionsDir)) {
|
|
16
|
+
const chat = readChatMetadata(filePath);
|
|
17
|
+
if (chat) chats.push(chat);
|
|
18
|
+
}
|
|
19
|
+
return chats;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMessages(chat) {
|
|
23
|
+
const filePath = chat && chat._filePath;
|
|
24
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
25
|
+
return parseSessionMessages(filePath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getSessionsDir() {
|
|
29
|
+
const codexHome = process.env.CODEX_HOME && process.env.CODEX_HOME.trim()
|
|
30
|
+
? path.resolve(process.env.CODEX_HOME.trim())
|
|
31
|
+
: DEFAULT_CODEX_HOME;
|
|
32
|
+
return path.join(codexHome, SESSION_SUBDIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function walkJsonlFiles(dir) {
|
|
36
|
+
const results = [];
|
|
37
|
+
const stack = [dir];
|
|
38
|
+
|
|
39
|
+
while (stack.length > 0) {
|
|
40
|
+
const current = stack.pop();
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
44
|
+
} catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = path.join(current, entry.name);
|
|
50
|
+
if (entry.isDirectory()) {
|
|
51
|
+
stack.push(fullPath);
|
|
52
|
+
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
53
|
+
results.push(fullPath);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return results.sort();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readChatMetadata(filePath) {
|
|
62
|
+
const lines = readLines(filePath);
|
|
63
|
+
if (lines.length === 0) return null;
|
|
64
|
+
|
|
65
|
+
const first = safeParseJson(lines[0]);
|
|
66
|
+
if (!first || first.type !== 'session_meta' || !first.payload) return null;
|
|
67
|
+
|
|
68
|
+
let title = null;
|
|
69
|
+
for (let i = 1; i < lines.length; i++) {
|
|
70
|
+
const entry = safeParseJson(lines[i]);
|
|
71
|
+
if (!entry || entry.type !== 'response_item' || !entry.payload) continue;
|
|
72
|
+
const payload = entry.payload;
|
|
73
|
+
if (payload.type !== 'message' || payload.role !== 'user') continue;
|
|
74
|
+
const text = extractUserText(payload.content);
|
|
75
|
+
if (!text || isBootstrapMessage(text)) continue;
|
|
76
|
+
title = cleanPrompt(text);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let stat;
|
|
81
|
+
try {
|
|
82
|
+
stat = fs.statSync(filePath);
|
|
83
|
+
} catch {
|
|
84
|
+
stat = null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const payload = first.payload;
|
|
88
|
+
return {
|
|
89
|
+
source: 'codex',
|
|
90
|
+
composerId: payload.id || path.basename(filePath, '.jsonl'),
|
|
91
|
+
name: title,
|
|
92
|
+
createdAt: toTimestamp(payload.timestamp) || (stat ? stat.birthtimeMs : null),
|
|
93
|
+
lastUpdatedAt: stat ? stat.mtimeMs : null,
|
|
94
|
+
mode: 'codex',
|
|
95
|
+
folder: payload.cwd || null,
|
|
96
|
+
encrypted: false,
|
|
97
|
+
bubbleCount: 0,
|
|
98
|
+
_filePath: filePath,
|
|
99
|
+
_rawSource: payload.source || null,
|
|
100
|
+
_originator: payload.originator || null,
|
|
101
|
+
_cliVersion: payload.cli_version || null,
|
|
102
|
+
_modelProvider: payload.model_provider || null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseSessionMessages(filePath) {
|
|
107
|
+
const lines = readLines(filePath);
|
|
108
|
+
const messages = [];
|
|
109
|
+
|
|
110
|
+
let currentModel = null;
|
|
111
|
+
let previousTotals = null;
|
|
112
|
+
let currentTurn = createTurnState();
|
|
113
|
+
let turnHasStarted = false;
|
|
114
|
+
const toolNamesByCallId = new Map();
|
|
115
|
+
|
|
116
|
+
function flushTurn() {
|
|
117
|
+
const hasAssistantContent = currentTurn.parts.length > 0;
|
|
118
|
+
const hasTokens = currentTurn.inputTokens > 0 || currentTurn.outputTokens > 0 || currentTurn.cacheRead > 0;
|
|
119
|
+
const hasTools = currentTurn.toolCalls.length > 0;
|
|
120
|
+
if (!hasAssistantContent && !hasTokens && !hasTools) {
|
|
121
|
+
currentTurn = createTurnState();
|
|
122
|
+
toolNamesByCallId.clear();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
messages.push(composeAssistantMessage(currentTurn));
|
|
127
|
+
currentTurn = createTurnState();
|
|
128
|
+
toolNamesByCallId.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
const entry = safeParseJson(line);
|
|
133
|
+
if (!entry) continue;
|
|
134
|
+
|
|
135
|
+
if (entry.type === 'turn_context') {
|
|
136
|
+
if (turnHasStarted) flushTurn();
|
|
137
|
+
turnHasStarted = true;
|
|
138
|
+
const model = extractModel(entry.payload);
|
|
139
|
+
if (model) {
|
|
140
|
+
currentModel = model;
|
|
141
|
+
currentTurn.model = model;
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (entry.type === 'response_item' && entry.payload) {
|
|
147
|
+
const payload = entry.payload;
|
|
148
|
+
|
|
149
|
+
if (payload.type === 'message') {
|
|
150
|
+
if (payload.role === 'user') {
|
|
151
|
+
const text = extractUserText(payload.content);
|
|
152
|
+
if (!text || isBootstrapMessage(text)) continue;
|
|
153
|
+
if (turnHasStarted) flushTurn();
|
|
154
|
+
messages.push({ role: 'user', content: text });
|
|
155
|
+
} else if (payload.role === 'assistant') {
|
|
156
|
+
const text = extractAssistantText(payload.content);
|
|
157
|
+
if (text) currentTurn.parts.push(text);
|
|
158
|
+
if (!currentTurn.model && currentModel) currentTurn.model = currentModel;
|
|
159
|
+
} else if (payload.role === 'system') {
|
|
160
|
+
const text = extractAssistantText(payload.content);
|
|
161
|
+
if (text) messages.push({ role: 'system', content: text });
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (payload.type === 'reasoning') {
|
|
167
|
+
const summary = extractReasoningSummary(payload);
|
|
168
|
+
if (summary) currentTurn.parts.push(summary);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isToolCallPayload(payload.type)) {
|
|
173
|
+
const toolCall = normalizeToolCall(payload);
|
|
174
|
+
currentTurn.parts.push(toolCall.line);
|
|
175
|
+
currentTurn.toolCalls.push({ name: toolCall.name, args: toolCall.args });
|
|
176
|
+
if (payload.call_id) toolNamesByCallId.set(payload.call_id, toolCall.name);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (isToolOutputPayload(payload.type)) {
|
|
181
|
+
const lineText = normalizeToolResult(payload, toolNamesByCallId.get(payload.call_id));
|
|
182
|
+
if (lineText) currentTurn.parts.push(lineText);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (entry.type === 'event_msg' && entry.payload && entry.payload.type === 'token_count') {
|
|
190
|
+
const tokenInfo = entry.payload.info || {};
|
|
191
|
+
const lastUsage = normalizeRawUsage(tokenInfo.last_token_usage);
|
|
192
|
+
const totalUsage = normalizeRawUsage(tokenInfo.total_token_usage);
|
|
193
|
+
|
|
194
|
+
let rawUsage = lastUsage;
|
|
195
|
+
if (!rawUsage && totalUsage) rawUsage = subtractRawUsage(totalUsage, previousTotals);
|
|
196
|
+
if (totalUsage) previousTotals = totalUsage;
|
|
197
|
+
if (!rawUsage) continue;
|
|
198
|
+
|
|
199
|
+
const delta = convertToDelta(rawUsage);
|
|
200
|
+
if (delta.inputTokens === 0 && delta.outputTokens === 0 && delta.cacheRead === 0) continue;
|
|
201
|
+
|
|
202
|
+
currentTurn.inputTokens += delta.inputTokens;
|
|
203
|
+
currentTurn.outputTokens += delta.outputTokens;
|
|
204
|
+
currentTurn.cacheRead += delta.cacheRead;
|
|
205
|
+
|
|
206
|
+
const model = extractModel(tokenInfo) || extractModel(entry.payload);
|
|
207
|
+
if (model) {
|
|
208
|
+
currentModel = model;
|
|
209
|
+
currentTurn.model = model;
|
|
210
|
+
} else if (!currentTurn.model && currentModel) {
|
|
211
|
+
currentTurn.model = currentModel;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (turnHasStarted) flushTurn();
|
|
217
|
+
|
|
218
|
+
return messages;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function createTurnState() {
|
|
222
|
+
return {
|
|
223
|
+
parts: [],
|
|
224
|
+
toolCalls: [],
|
|
225
|
+
inputTokens: 0,
|
|
226
|
+
outputTokens: 0,
|
|
227
|
+
cacheRead: 0,
|
|
228
|
+
model: null,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function composeAssistantMessage(turn) {
|
|
233
|
+
const content = turn.parts.join('\n') || '[assistant activity]';
|
|
234
|
+
return {
|
|
235
|
+
role: 'assistant',
|
|
236
|
+
content,
|
|
237
|
+
_model: turn.model || undefined,
|
|
238
|
+
_inputTokens: turn.inputTokens || undefined,
|
|
239
|
+
_outputTokens: turn.outputTokens || undefined,
|
|
240
|
+
_cacheRead: turn.cacheRead || undefined,
|
|
241
|
+
_toolCalls: turn.toolCalls.length > 0 ? turn.toolCalls : undefined,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function extractUserText(content) {
|
|
246
|
+
if (!Array.isArray(content)) return '';
|
|
247
|
+
return content
|
|
248
|
+
.filter((item) => item && item.type === 'input_text' && typeof item.text === 'string')
|
|
249
|
+
.map((item) => item.text.trim())
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.join('\n')
|
|
252
|
+
.trim();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function extractAssistantText(content) {
|
|
256
|
+
if (!Array.isArray(content)) return '';
|
|
257
|
+
return content
|
|
258
|
+
.filter((item) => item && item.type === 'output_text' && typeof item.text === 'string')
|
|
259
|
+
.map((item) => item.text.trim())
|
|
260
|
+
.filter(Boolean)
|
|
261
|
+
.join('\n')
|
|
262
|
+
.trim();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function extractReasoningSummary(payload) {
|
|
266
|
+
if (!Array.isArray(payload.summary)) return '';
|
|
267
|
+
return payload.summary
|
|
268
|
+
.filter((item) => item && typeof item.text === 'string' && item.text.trim())
|
|
269
|
+
.map((item) => `[thinking] ${item.text.trim()}`)
|
|
270
|
+
.join('\n')
|
|
271
|
+
.trim();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isBootstrapMessage(text) {
|
|
275
|
+
const trimmed = text.trim();
|
|
276
|
+
return trimmed.startsWith('<user_instructions>') || trimmed.startsWith('<environment_context>');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function cleanPrompt(text) {
|
|
280
|
+
return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isToolCallPayload(type) {
|
|
284
|
+
return type === 'function_call' || type === 'custom_tool_call' || type === 'web_search_call';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isToolOutputPayload(type) {
|
|
288
|
+
return type === 'function_call_output' || type === 'custom_tool_call_output';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function normalizeToolCall(payload) {
|
|
292
|
+
const name = payload.name || (payload.type === 'web_search_call' ? 'web_search' : 'tool');
|
|
293
|
+
const args = parseToolArgs(payload);
|
|
294
|
+
const argKeys = Object.keys(args).join(', ');
|
|
295
|
+
return {
|
|
296
|
+
name,
|
|
297
|
+
args,
|
|
298
|
+
line: `[tool-call: ${name}(${argKeys})]`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function normalizeToolResult(payload, fallbackName) {
|
|
303
|
+
const name = payload.name || fallbackName || 'tool';
|
|
304
|
+
const preview = previewToolOutput(payload.output);
|
|
305
|
+
return preview ? `[tool-result: ${name}] ${preview}` : `[tool-result: ${name}]`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function parseToolArgs(payload) {
|
|
309
|
+
if (payload.type === 'function_call') {
|
|
310
|
+
return parseJsonRecord(payload.arguments);
|
|
311
|
+
}
|
|
312
|
+
if (payload.type === 'custom_tool_call') {
|
|
313
|
+
return { input: truncateSingleLine(String(payload.input || ''), 300) };
|
|
314
|
+
}
|
|
315
|
+
if (payload.type === 'web_search_call') {
|
|
316
|
+
return parseJsonRecord(payload.arguments || payload.input || payload.query);
|
|
317
|
+
}
|
|
318
|
+
return {};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function previewToolOutput(output) {
|
|
322
|
+
if (output == null) return '';
|
|
323
|
+
let value = output;
|
|
324
|
+
if (typeof value === 'string') {
|
|
325
|
+
const parsed = safeParseJson(value);
|
|
326
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.output === 'string') {
|
|
327
|
+
value = parsed.output;
|
|
328
|
+
} else {
|
|
329
|
+
value = value;
|
|
330
|
+
}
|
|
331
|
+
} else if (typeof value === 'object') {
|
|
332
|
+
value = JSON.stringify(value);
|
|
333
|
+
} else {
|
|
334
|
+
value = String(value);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const trimmed = String(value).trim();
|
|
338
|
+
if (!trimmed) return '';
|
|
339
|
+
return truncateSingleLine(trimmed, MAX_TOOL_RESULT_PREVIEW);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function truncateSingleLine(text, maxLen) {
|
|
343
|
+
const oneLine = String(text).replace(/\s+/g, ' ').trim();
|
|
344
|
+
return oneLine.length > maxLen ? oneLine.substring(0, maxLen) + '…' : oneLine;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function parseJsonRecord(value) {
|
|
348
|
+
if (value == null) return {};
|
|
349
|
+
if (typeof value === 'object' && !Array.isArray(value)) return value;
|
|
350
|
+
if (typeof value !== 'string') return {};
|
|
351
|
+
const parsed = safeParseJson(value);
|
|
352
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function normalizeRawUsage(value) {
|
|
356
|
+
if (!value || typeof value !== 'object') return null;
|
|
357
|
+
const input = ensureNumber(value.input_tokens);
|
|
358
|
+
const cached = ensureNumber(value.cached_input_tokens != null ? value.cached_input_tokens : value.cache_read_input_tokens);
|
|
359
|
+
const output = ensureNumber(value.output_tokens);
|
|
360
|
+
const total = ensureNumber(value.total_tokens);
|
|
361
|
+
return {
|
|
362
|
+
input_tokens: input,
|
|
363
|
+
cached_input_tokens: cached,
|
|
364
|
+
output_tokens: output,
|
|
365
|
+
total_tokens: total > 0 ? total : input + output,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function subtractRawUsage(current, previous) {
|
|
370
|
+
return {
|
|
371
|
+
input_tokens: Math.max(current.input_tokens - (previous ? previous.input_tokens : 0), 0),
|
|
372
|
+
cached_input_tokens: Math.max(current.cached_input_tokens - (previous ? previous.cached_input_tokens : 0), 0),
|
|
373
|
+
output_tokens: Math.max(current.output_tokens - (previous ? previous.output_tokens : 0), 0),
|
|
374
|
+
total_tokens: Math.max(current.total_tokens - (previous ? previous.total_tokens : 0), 0),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function convertToDelta(raw) {
|
|
379
|
+
return {
|
|
380
|
+
inputTokens: raw.input_tokens,
|
|
381
|
+
cacheRead: Math.min(raw.cached_input_tokens, raw.input_tokens),
|
|
382
|
+
outputTokens: raw.output_tokens,
|
|
383
|
+
totalTokens: raw.total_tokens > 0 ? raw.total_tokens : raw.input_tokens + raw.output_tokens,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractModel(value) {
|
|
388
|
+
if (!value || typeof value !== 'object') return null;
|
|
389
|
+
|
|
390
|
+
const direct = asNonEmptyString(value.model)
|
|
391
|
+
|| asNonEmptyString(value.model_name);
|
|
392
|
+
if (direct) return direct;
|
|
393
|
+
|
|
394
|
+
if (value.info && typeof value.info === 'object') {
|
|
395
|
+
const infoModel = extractModel(value.info);
|
|
396
|
+
if (infoModel) return infoModel;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (value.metadata && typeof value.metadata === 'object') {
|
|
400
|
+
const metadataModel = extractModel(value.metadata);
|
|
401
|
+
if (metadataModel) return metadataModel;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function asNonEmptyString(value) {
|
|
408
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function ensureNumber(value) {
|
|
412
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function toTimestamp(value) {
|
|
416
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
417
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
418
|
+
const parsed = Date.parse(value);
|
|
419
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function readLines(filePath) {
|
|
423
|
+
try {
|
|
424
|
+
return fs.readFileSync(filePath, 'utf-8').split(/\r?\n/).filter(Boolean);
|
|
425
|
+
} catch {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function safeParseJson(value) {
|
|
431
|
+
try {
|
|
432
|
+
return JSON.parse(value);
|
|
433
|
+
} catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
module.exports = {
|
|
439
|
+
name,
|
|
440
|
+
getChats,
|
|
441
|
+
getMessages,
|
|
442
|
+
_test: {
|
|
443
|
+
getSessionsDir,
|
|
444
|
+
readChatMetadata,
|
|
445
|
+
parseSessionMessages,
|
|
446
|
+
normalizeRawUsage,
|
|
447
|
+
subtractRawUsage,
|
|
448
|
+
convertToDelta,
|
|
449
|
+
extractModel,
|
|
450
|
+
isBootstrapMessage,
|
|
451
|
+
previewToolOutput,
|
|
452
|
+
},
|
|
453
|
+
};
|