agentlytics 0.2.11 → 0.2.13

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 CHANGED
@@ -5,16 +5,15 @@
5
5
  <h1 align="center">Agentlytics</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Your Cursor, Windsurf, Claude Code sessions — analyzed, unified, tracked.</strong><br>
9
- <sub>One command to turn scattered AI conversations from <b>16 editors</b> into a unified analytics dashboard.<br>Sessions, costs, models, tools — finally in one place. 100% local.</sub>
8
+ <strong>Your Cursor, Devin, Claude Code sessions — analyzed, unified, tracked.</strong><br>
9
+ <sub>One command to turn scattered AI conversations from <b>17 editors</b> into a unified analytics dashboard.<br>Sessions, costs, models, tools — finally in one place. 100% local.</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-16-818cf8" alt="editors"></a>
14
+ <a href="#supported-editors"><img src="https://img.shields.io/badge/editors-17-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%A520.19%20%7C%20%E2%89%A522.12-brightgreen" alt="node"></a>
17
- <a href="https://deno.land"><img src="https://img.shields.io/badge/deno-%E2%89%A52.0-000?logo=deno" alt="deno"></a>
18
17
  </p>
19
18
 
20
19
  <p align="center">
@@ -25,7 +24,7 @@
25
24
 
26
25
  ## The Problem
27
26
 
28
- You switch between Cursor, Windsurf, Claude Code, VS Code Copilot, and more — each with its own siloed conversation history.
27
+ You switch between Cursor, Devin, Claude Code, VS Code Copilot, and more — each with its own siloed conversation history.
29
28
 
30
29
  - ✗ Sessions scattered across editors, no unified view
31
30
  - ✗ No idea how much you're spending on AI tokens
@@ -50,49 +49,6 @@ bunx agentlytics
50
49
 
51
50
  Opens at **http://localhost:4637**. Requires Node.js ≥ 20.19 or ≥ 22.12, macOS. No data ever leaves your machine.
52
51
 
53
- ### Deno (Sandboxed)
54
-
55
- Run a lightweight, zero-dependency analytics scan with Deno's permission sandbox — directly from a URL, no install needed:
56
-
57
- ```bash
58
- deno run --allow-read --allow-env https://raw.githubusercontent.com/f/agentlytics/master/mod.ts
59
- ```
60
-
61
- Only `--allow-read` and `--allow-env` are required. No network access, no file writes, no code execution — just reads your local editor data and prints a summary.
62
-
63
- ```
64
- (● ●) [● ●] Agentlytics — Deno Sandboxed Edition
65
- {● ●} <● ●> Lightweight CLI analytics for AI coding agents
66
-
67
- ✓ Claude Code 8 sessions
68
- ✓ VS Code 23 sessions
69
- ✓ VS Code Insiders 66 sessions
70
- ● Cursor detected
71
- ✓ Codex CLI 3 sessions
72
- ...
73
-
74
- Summary
75
- Sessions 109
76
- Messages 459
77
- Projects 18
78
- Editors 7 of 15 checked
79
- Date range 2025-04-02 → 2026-03-09
80
- ```
81
-
82
- Add `--json` for machine-readable output:
83
-
84
- ```bash
85
- deno run --allow-read --allow-env mod.ts --json
86
- ```
87
-
88
- If you've cloned the repo, you can also use Deno tasks for the full dashboard:
89
-
90
- ```bash
91
- deno task start # Full dashboard (all permissions)
92
- deno task scan # Lightweight CLI scan
93
- deno task scan:json # JSON output
94
- ```
95
-
96
52
  ### Node.js
97
53
 
98
54
  ```
@@ -103,8 +59,8 @@ $ npx agentlytics
103
59
 
104
60
  Looking for AI coding agents...
105
61
  ✓ Cursor 498 sessions
106
- Windsurf 20 sessions
107
- Windsurf Next 56 sessions
62
+ Devin 20 sessions
63
+ Devin Next 56 sessions
108
64
  ✓ Claude Code 6 sessions
109
65
  ✓ VS Code 23 sessions
110
66
  ✓ Zed 1 session
@@ -131,7 +87,7 @@ npx agentlytics --collect
131
87
  - **Projects** — Per-project analytics: sessions, messages, tokens, models, editor breakdown, and drill-down detail views
132
88
  - **Deep Analysis** — Tool frequency heatmaps, model distribution, token breakdown, and filterable drill-down analytics
133
89
  - **Compare** — Side-by-side editor comparison with efficiency ratios, token usage, and session patterns
134
- - **Subscriptions** — Live view of your editor plans, usage quotas, remaining credits, and rate limits across Cursor, Windsurf, Claude Code, Copilot, Codex, and more
90
+ - **Subscriptions** — Live view of your editor plans, usage quotas, remaining credits, and rate limits across Cursor, Devin, Claude Code, Copilot, Codex, and more
135
91
  - **Relay** — Share AI session context across your team via MCP
136
92
 
137
93
  ## Supported Editors
@@ -139,8 +95,8 @@ npx agentlytics --collect
139
95
  | Editor | Msgs | Tools | Models | Tokens |
140
96
  |--------|:----:|:-----:|:------:|:------:|
141
97
  | **Cursor** | ✅ | ✅ | ✅ | ✅ |
142
- | **Windsurf** | ✅ | ✅ | ✅ | ✅ |
143
- | **Windsurf Next** | ✅ | ✅ | ✅ | ✅ |
98
+ | **Devin** | ✅ | ✅ | ✅ | ✅ |
99
+ | **Devin Next** | ✅ | ✅ | ✅ | ✅ |
144
100
  | **Antigravity** | ✅ | ✅ | ✅ | ✅ |
145
101
  | **Claude Code** | ✅ | ✅ | ✅ | ✅ |
146
102
  | **VS Code** | ✅ | ✅ | ✅ | ✅ |
@@ -149,13 +105,14 @@ npx agentlytics --collect
149
105
  | **OpenCode** | ✅ | ✅ | ✅ | ✅ |
150
106
  | **Codex** | ✅ | ✅ | ✅ | ✅ |
151
107
  | **Gemini CLI** | ✅ | ✅ | ✅ | ✅ |
152
- | **Copilot CLI** | ✅ | ✅ | ✅ | ✅ |
108
+ | **GitHub Copilot** | ✅ | ✅ | ✅ | ✅ |
153
109
  | **Cursor Agent** | ✅ | ❌ | ❌ | ❌ |
154
110
  | **Command Code** | ✅ | ✅ | ❌ | ❌ |
155
111
  | **Goose** | ✅ | ✅ | ✅ | ❌ |
156
112
  | **Kiro** | ✅ | ✅ | ✅ | ❌ |
113
+ | **Codebuff** | ✅ | ✅ | ⚠️ | ⚠️ |
157
114
 
158
- > Windsurf, Windsurf Next, and Antigravity must be running during scan.
115
+ > Devin, Devin Next, and Antigravity must be running during scan.
159
116
 
160
117
  ## Relay
161
118
 
@@ -241,11 +198,7 @@ Editor files/APIs → editors/*.js → cache.js (SQLite) → server.js (REST)
241
198
  Relay: join clients → POST /relay/sync → relay.db (SQLite) → MCP server → AI clients
242
199
  ```
243
200
 
244
- ```
245
- Deno: Editor files → mod.ts (zero deps) → stdout (CLI/JSON)
246
- ```
247
-
248
- All data is normalized into a local SQLite cache at `~/.agentlytics/cache.db`. The Express server exposes read-only REST endpoints consumed by the React frontend. Relay data is stored separately in `~/.agentlytics/relay.db`. The Deno sandboxed edition (`mod.ts`) bypasses SQLite entirely and reads editor files directly for a lightweight, permission-minimal CLI report.
201
+ All data is normalized into a local SQLite cache at `~/.agentlytics/cache.db`. The Express server exposes read-only REST endpoints consumed by the React frontend. Relay data is stored separately in `~/.agentlytics/relay.db`.
249
202
 
250
203
  ## API
251
204
 
@@ -265,7 +218,7 @@ All endpoints accept optional `editor` filter. See **[API.md](API.md)** for full
265
218
 
266
219
  ## Roadmap
267
220
 
268
- - [ ] **Offline Windsurf/Antigravity support** — Read cascade data from local file structure instead of requiring the app to be running (see below)
221
+ - [ ] **Offline Devin/Antigravity support** — Read cascade data from local file structure instead of requiring the app to be running (see below)
269
222
  - [ ] **LLM-powered insights** — Use an LLM to analyze session patterns, generate summaries, detect coding habits, and surface actionable recommendations
270
223
  - [ ] **Linux & Windows support** — Adapt editor paths for non-macOS platforms
271
224
  - [ ] **Export & reports** — PDF/CSV export of analytics and session data
@@ -273,7 +226,7 @@ All endpoints accept optional `editor` filter. See **[API.md](API.md)** for full
273
226
 
274
227
  ## Contributions Needed
275
228
 
276
- **Windsurf / Windsurf Next / Antigravity offline reading** — Currently these editors require their app to be running because data is fetched via ConnectRPC from the language server process. Unlike Cursor or Claude Code, there's no known local file structure to read cascade history from. If you know where Windsurf stores trajectory data on disk, or can help reverse-engineer the storage format, contributions are very welcome.
229
+ **Devin / Devin Next / Antigravity offline reading** — Currently these editors require their app to be running because data is fetched via ConnectRPC from the language server process. Unlike Cursor or Claude Code, there's no known local file structure to read cascade history from. Legacy Windsurf identifiers and `~/.windsurf` configuration are still supported for backwards compatibility.
277
230
 
278
231
  **LLM-based analytics** — We'd love to add intelligent analysis on top of the raw data — session summaries, coding pattern detection, productivity insights, and natural language queries over your agent history. If you have ideas or want to build this, open an issue or PR.
279
232
 
package/cache.js CHANGED
@@ -7,7 +7,7 @@ const { calculateCost, getModelPricing, normalizeModelName } = require('./pricin
7
7
 
8
8
  const CACHE_DIR = path.join(os.homedir(), '.agentlytics');
9
9
  const CACHE_DB = path.join(CACHE_DIR, 'cache.db');
10
- const SCHEMA_VERSION = 6; // bump this when schema changes to auto-revalidate
10
+ const SCHEMA_VERSION = 7; // bump this when schema changes to auto-revalidate
11
11
 
12
12
  /**
13
13
  * Normalize a folder path for consistent storage/lookup.
@@ -52,6 +52,12 @@ function normalizeFolder(folder) {
52
52
  return folder;
53
53
  }
54
54
 
55
+ function normalizeEditorSource(source) {
56
+ if (source === 'windsurf') return 'devin';
57
+ if (source === 'windsurf-next') return 'devin-next';
58
+ return source;
59
+ }
60
+
55
61
  let db = null;
56
62
 
57
63
  // ============================================================
@@ -154,11 +160,59 @@ function initDb() {
154
160
  CREATE INDEX IF NOT EXISTS idx_messages_chat ON messages(chat_id);
155
161
  CREATE INDEX IF NOT EXISTS idx_tool_calls_name ON tool_calls(tool_name);
156
162
  CREATE INDEX IF NOT EXISTS idx_tool_calls_chat ON tool_calls(chat_id);
163
+
164
+ CREATE TABLE IF NOT EXISTS gsd_projects (
165
+ folder TEXT PRIMARY KEY,
166
+ name TEXT,
167
+ description TEXT,
168
+ milestone TEXT,
169
+ total_phases INTEGER DEFAULT 0,
170
+ completed_phases INTEGER DEFAULT 0,
171
+ active_phase TEXT,
172
+ todos INTEGER DEFAULT 0,
173
+ backlog INTEGER DEFAULT 0,
174
+ notes INTEGER DEFAULT 0,
175
+ last_modified INTEGER,
176
+ scanned_at INTEGER
177
+ );
178
+
179
+ CREATE TABLE IF NOT EXISTS gsd_phases (
180
+ id TEXT PRIMARY KEY,
181
+ folder TEXT NOT NULL,
182
+ phase_number INTEGER,
183
+ phase_name TEXT,
184
+ status TEXT,
185
+ total_tasks INTEGER DEFAULT 0,
186
+ completed_tasks INTEGER DEFAULT 0,
187
+ has_plan INTEGER DEFAULT 0,
188
+ has_research INTEGER DEFAULT 0,
189
+ has_verification INTEGER DEFAULT 0,
190
+ last_modified INTEGER,
191
+ FOREIGN KEY (folder) REFERENCES gsd_projects(folder)
192
+ );
193
+
194
+ CREATE INDEX IF NOT EXISTS idx_gsd_phases_folder ON gsd_phases(folder);
157
195
  `);
158
196
 
159
197
  // Store schema version so future runs can detect mismatches
160
198
  db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('schema_version', SCHEMA_VERSION.toString());
161
199
 
200
+ let sourceMigrationV = 0;
201
+ try {
202
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'source_id_migration_v'").get();
203
+ if (row) sourceMigrationV = parseInt(row.value) || 0;
204
+ } catch { }
205
+ if (sourceMigrationV < 1) {
206
+ const migrateSource = db.transaction(() => {
207
+ db.prepare("UPDATE chats SET source = 'devin' WHERE source = 'windsurf'").run();
208
+ db.prepare("UPDATE chats SET source = 'devin-next' WHERE source = 'windsurf-next'").run();
209
+ db.prepare("UPDATE tool_calls SET source = 'devin' WHERE source = 'windsurf'").run();
210
+ db.prepare("UPDATE tool_calls SET source = 'devin-next' WHERE source = 'windsurf-next'").run();
211
+ db.prepare("INSERT OR REPLACE INTO meta (key, value) VALUES ('source_id_migration_v', '1')").run();
212
+ });
213
+ migrateSource();
214
+ }
215
+
162
216
  // v2 migration: normalize folder paths on Windows
163
217
  if (process.platform === 'win32') {
164
218
  let normV = 0;
@@ -229,6 +283,7 @@ function analyzeAndStore(chat) {
229
283
  const updBubbleCount = updateChatBubbleCount();
230
284
  const insTc = db.prepare('INSERT INTO tool_calls (chat_id, tool_name, args_json, source, folder, timestamp) VALUES (?, ?, ?, ?, ?, ?)');
231
285
  const chatTs = chat.lastUpdatedAt || chat.createdAt || null;
286
+ const chatSource = normalizeEditorSource(chat.source);
232
287
 
233
288
  let seq = 0;
234
289
  for (const msg of messages) {
@@ -245,7 +300,7 @@ function analyzeAndStore(chat) {
245
300
  for (const tc of msg._toolCalls) {
246
301
  stats.toolCalls.push(tc.name);
247
302
  try {
248
- insTc.run(chat.composerId, tc.name, JSON.stringify(tc.args || {}), chat.source, chat.folder || null, chatTs);
303
+ insTc.run(chat.composerId, tc.name, JSON.stringify(tc.args || {}), chatSource, chat.folder || null, chatTs);
249
304
  } catch { }
250
305
  }
251
306
  } else {
@@ -254,7 +309,7 @@ function analyzeAndStore(chat) {
254
309
  for (const m of toolMatches) {
255
310
  const name = m.match(/\[tool-call: ([^(]+)/)?.[1] || 'unknown';
256
311
  stats.toolCalls.push(name.trim());
257
- insTc.run(chat.composerId, name.trim(), '{}', chat.source, chat.folder || null, chatTs);
312
+ insTc.run(chat.composerId, name.trim(), '{}', chatSource, chat.folder || null, chatTs);
258
313
  }
259
314
  }
260
315
  }
@@ -306,7 +361,7 @@ function scanAll(onProgress, opts = {}) {
306
361
  const batchInsert = db.transaction((chatBatch) => {
307
362
  for (const chat of chatBatch) {
308
363
  ins.run(
309
- chat.composerId, chat.source, chat.name || null, chat.mode || null,
364
+ chat.composerId, normalizeEditorSource(chat.source), chat.name || null, chat.mode || null,
310
365
  chat.folder || null, chat.createdAt || null, chat.lastUpdatedAt || null,
311
366
  chat.encrypted ? 1 : 0, chat.bubbleCount || 0,
312
367
  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 })
@@ -358,6 +413,9 @@ function scanAll(onProgress, opts = {}) {
358
413
  db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('last_scan', Date.now().toString());
359
414
  db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('total_chats', total.toString());
360
415
 
416
+ // GSD scan
417
+ cacheGSDProjects();
418
+
361
419
  return { total, analyzed, skipped };
362
420
  }
363
421
 
@@ -643,7 +701,7 @@ function getCachedChat(id) {
643
701
  createdAt: chat.created_at,
644
702
  lastUpdatedAt: chat.last_updated_at,
645
703
  encrypted: !!chat.encrypted,
646
- messages: messages.map(m => ({ role: m.role, content: m.content, model: m.model })),
704
+ messages: messages.map(m => ({ role: m.role, content: m.content, model: m.model, inputTokens: m.input_tokens || 0, outputTokens: m.output_tokens || 0 })),
647
705
  stats: parsedStats,
648
706
  toolCallDetails,
649
707
  };
@@ -839,6 +897,9 @@ async function scanAllAsync(onProgress, opts = {}) {
839
897
  db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('last_scan', Date.now().toString());
840
898
  db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('total_chats', total.toString());
841
899
 
900
+ // GSD scan
901
+ cacheGSDProjects();
902
+
842
903
  return { total, analyzed, skipped };
843
904
  }
844
905
 
@@ -1300,6 +1361,156 @@ function getCostAnalytics(opts = {}) {
1300
1361
 
1301
1362
  function getDb() { return db; }
1302
1363
 
1364
+ // ============================================================
1365
+ // GSD cache functions
1366
+ // ============================================================
1367
+
1368
+ const gsd = require('./editors/gsd');
1369
+
1370
+ function cacheGSDProjects() {
1371
+ // Get all unique known folders from chats table
1372
+ const rows = db.prepare('SELECT DISTINCT folder FROM chats WHERE folder IS NOT NULL').all();
1373
+ const knownFolders = rows.map(r => r.folder);
1374
+
1375
+ const projects = gsd.getGSDProjects(knownFolders);
1376
+
1377
+ const insProject = db.prepare(`
1378
+ INSERT OR REPLACE INTO gsd_projects
1379
+ (folder, name, description, milestone, total_phases, completed_phases, active_phase, todos, backlog, notes, last_modified, scanned_at)
1380
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1381
+ `);
1382
+ const delPhases = db.prepare('DELETE FROM gsd_phases WHERE folder = ?');
1383
+ const insPhase = db.prepare(`
1384
+ INSERT OR REPLACE INTO gsd_phases
1385
+ (id, folder, phase_number, phase_name, status, total_tasks, completed_tasks, has_plan, has_research, has_verification, last_modified)
1386
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1387
+ `);
1388
+
1389
+ const tx = db.transaction(() => {
1390
+ for (const p of projects) {
1391
+ insProject.run(
1392
+ p.folder, p.name, p.description, p.milestone,
1393
+ p.totalPhases, p.completedPhases, p.activePhase,
1394
+ p.todos, p.backlog, p.notes,
1395
+ p.lastModified, Date.now()
1396
+ );
1397
+ delPhases.run(p.folder);
1398
+ const phases = gsd.getGSDPhases(p.folder);
1399
+ for (const ph of phases) {
1400
+ const id = `${p.folder}::${ph.phaseDir}`;
1401
+ insPhase.run(
1402
+ id, p.folder, ph.number, ph.name, ph.status,
1403
+ ph.tasks.total, ph.tasks.completed,
1404
+ ph.hasPlan ? 1 : 0,
1405
+ ph.hasResearch ? 1 : 0,
1406
+ ph.hasVerification ? 1 : 0,
1407
+ ph.lastModified
1408
+ );
1409
+ }
1410
+ }
1411
+ });
1412
+ tx();
1413
+ }
1414
+
1415
+ function getCachedGSDProjects() {
1416
+ const projects = db.prepare('SELECT * FROM gsd_projects ORDER BY last_modified DESC').all();
1417
+ for (const p of projects) {
1418
+ try {
1419
+ const phases = getGSDPhaseTokens(p.folder);
1420
+ p.total_cost = phases.reduce((s, r) => s + (r.cost || 0), 0);
1421
+ } catch {
1422
+ p.total_cost = 0;
1423
+ }
1424
+ }
1425
+ return projects;
1426
+ }
1427
+
1428
+ function getCachedGSDPhases(folder) {
1429
+ return db.prepare('SELECT * FROM gsd_phases WHERE folder = ? ORDER BY phase_number ASC').all(folder);
1430
+ }
1431
+
1432
+ function getGSDPhaseTokens(folder) {
1433
+ const phases = db.prepare(
1434
+ 'SELECT id, phase_number, phase_name, status, last_modified FROM gsd_phases WHERE folder = ? ORDER BY phase_number ASC'
1435
+ ).all(folder);
1436
+
1437
+ if (phases.length === 0) return [];
1438
+
1439
+ // Sort by last_modified to build sequential non-overlapping time windows.
1440
+ // Phases with no last_modified are placed at the end.
1441
+ const byTime = [...phases]
1442
+ .filter(p => p.last_modified)
1443
+ .sort((a, b) => a.last_modified - b.last_modified);
1444
+
1445
+ const windowMap = new Map();
1446
+ for (let i = 0; i < byTime.length; i++) {
1447
+ const start = i === 0 ? 0 : byTime[i - 1].last_modified;
1448
+ const end = i === byTime.length - 1 ? Date.now() : byTime[i].last_modified;
1449
+ windowMap.set(byTime[i].id, { start, end });
1450
+ }
1451
+
1452
+ const stmt = db.prepare(`
1453
+ SELECT cs.total_input_tokens, cs.total_output_tokens,
1454
+ cs.total_cache_read, cs.total_cache_write, cs.models
1455
+ FROM chats c JOIN chat_stats cs ON cs.chat_id = c.id
1456
+ WHERE c.folder = ? AND COALESCE(c.last_updated_at, c.created_at) BETWEEN ? AND ?
1457
+ `);
1458
+
1459
+ return phases.map(ph => {
1460
+ const win = windowMap.get(ph.id);
1461
+ let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0;
1462
+ let sessionCount = 0;
1463
+ const modelFreq = {};
1464
+
1465
+ if (win) {
1466
+ const rows = stmt.all(folder, win.start, win.end);
1467
+ for (const row of rows) {
1468
+ totalInput += row.total_input_tokens || 0;
1469
+ totalOutput += row.total_output_tokens || 0;
1470
+ totalCacheRead += row.total_cache_read || 0;
1471
+ totalCacheWrite += row.total_cache_write || 0;
1472
+ sessionCount++;
1473
+ try {
1474
+ const models = JSON.parse(row.models || '[]');
1475
+ for (const m of models) {
1476
+ const key = typeof m === 'string' ? m : (m && m.model);
1477
+ if (key) modelFreq[key] = (modelFreq[key] || 0) + 1;
1478
+ }
1479
+ } catch { /* skip */ }
1480
+ }
1481
+ }
1482
+
1483
+ const dominantModel = Object.entries(modelFreq).sort((a, b) => b[1] - a[1])[0]?.[0] || null;
1484
+ const cost = dominantModel
1485
+ ? (calculateCost(dominantModel, totalInput, totalOutput, totalCacheRead, totalCacheWrite) || 0)
1486
+ : 0;
1487
+ const totalTokens = totalInput + totalOutput;
1488
+
1489
+ return {
1490
+ id: ph.id,
1491
+ phase_number: ph.phase_number,
1492
+ phase_name: ph.phase_name,
1493
+ status: ph.status,
1494
+ total_tokens: totalTokens,
1495
+ cost,
1496
+ session_count: sessionCount,
1497
+ };
1498
+ });
1499
+ }
1500
+
1501
+ function getCachedGSDOverview() {
1502
+ const projects = getCachedGSDProjects();
1503
+ const totalProjects = projects.length;
1504
+ const totalPhases = projects.reduce((s, p) => s + p.total_phases, 0);
1505
+ const completedPhases = projects.reduce((s, p) => s + p.completed_phases, 0);
1506
+ const activePhases = projects
1507
+ .filter(p => p.active_phase)
1508
+ .map(p => ({ folder: p.folder, name: p.name, activePhase: p.active_phase }));
1509
+ const executingPhases = db.prepare("SELECT COUNT(*) as c FROM gsd_phases WHERE status = 'executing'").get().c;
1510
+ const plannedPhases = db.prepare("SELECT COUNT(*) as c FROM gsd_phases WHERE status = 'planned'").get().c;
1511
+ return { totalProjects, totalPhases, completedPhases, activePhases, executingPhases, plannedPhases };
1512
+ }
1513
+
1303
1514
  module.exports = {
1304
1515
  initDb,
1305
1516
  scanAll,
@@ -1317,4 +1528,9 @@ module.exports = {
1317
1528
  getCostBreakdown,
1318
1529
  getCostAnalytics,
1319
1530
  getDb,
1531
+ cacheGSDProjects,
1532
+ getCachedGSDProjects,
1533
+ getCachedGSDPhases,
1534
+ getCachedGSDOverview,
1535
+ getGSDPhaseTokens,
1320
1536
  };
package/editors/base.js CHANGED
@@ -36,7 +36,7 @@ function getAppDataPath(appName) {
36
36
  /**
37
37
  * Every editor adapter must implement:
38
38
  *
39
- * name - string identifier (e.g. 'cursor', 'windsurf')
39
+ * name - string identifier (e.g. 'cursor', 'devin')
40
40
  * getChats() - returns array of chat objects:
41
41
  * { source, composerId, name, createdAt, lastUpdatedAt, mode, folder, bubbleCount, encrypted }
42
42
  * getMessages(chat) - returns array of message objects: