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 +15 -62
- package/cache.js +221 -5
- package/editors/base.js +1 -1
- package/editors/codebuff.js +338 -0
- package/editors/copilot.js +3 -3
- package/editors/gsd.js +366 -0
- package/editors/index.js +10 -5
- package/editors/windsurf.js +64 -37
- package/index.js +32 -12
- package/package.json +6 -6
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/assets/index-SOQVJIDS.js +73 -0
- package/public/index.html +16 -0
- package/relay-client.js +10 -8
- package/server.js +104 -2
- package/share-image.js +9 -7
- package/ui/src/App.jsx +5 -2
- package/ui/src/components/ChatSidebar.jsx +31 -2
- package/ui/src/components/EditorIcon.jsx +60 -11
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/lib/api.js +43 -0
- package/ui/src/lib/constants.js +10 -8
- package/ui/src/pages/Artifacts.jsx +0 -12
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/Settings.jsx +1 -1
- package/ui/src/pages/Subscriptions.jsx +3 -3
- package/deno.json +0 -9
- package/mod.ts +0 -1020
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,
|
|
9
|
-
<sub>One command to turn scattered AI conversations from <b>
|
|
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-
|
|
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,
|
|
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
|
-
✓
|
|
107
|
-
✓
|
|
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,
|
|
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
|
-
| **
|
|
143
|
-
| **
|
|
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
|
|
108
|
+
| **GitHub Copilot** | ✅ | ✅ | ✅ | ✅ |
|
|
153
109
|
| **Cursor Agent** | ✅ | ❌ | ❌ | ❌ |
|
|
154
110
|
| **Command Code** | ✅ | ✅ | ❌ | ❌ |
|
|
155
111
|
| **Goose** | ✅ | ✅ | ✅ | ❌ |
|
|
156
112
|
| **Kiro** | ✅ | ✅ | ✅ | ❌ |
|
|
113
|
+
| **Codebuff** | ✅ | ✅ | ⚠️ | ⚠️ |
|
|
157
114
|
|
|
158
|
-
>
|
|
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
|
|
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
|
-
**
|
|
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 =
|
|
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 || {}),
|
|
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(), '{}',
|
|
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', '
|
|
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:
|