agentlytics 0.2.12 → 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 +26 -3
- package/editors/base.js +1 -1
- package/editors/codebuff.js +338 -0
- package/editors/copilot.js +3 -3
- 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 +2 -2
- package/share-image.js +9 -7
- package/ui/src/App.jsx +1 -1
- package/ui/src/components/EditorIcon.jsx +60 -11
- package/ui/src/lib/constants.js +10 -8
- package/ui/src/pages/Artifacts.jsx +0 -12
- 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
|
@@ -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
|
// ============================================================
|
|
@@ -191,6 +197,22 @@ function initDb() {
|
|
|
191
197
|
// Store schema version so future runs can detect mismatches
|
|
192
198
|
db.prepare('INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)').run('schema_version', SCHEMA_VERSION.toString());
|
|
193
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
|
+
|
|
194
216
|
// v2 migration: normalize folder paths on Windows
|
|
195
217
|
if (process.platform === 'win32') {
|
|
196
218
|
let normV = 0;
|
|
@@ -261,6 +283,7 @@ function analyzeAndStore(chat) {
|
|
|
261
283
|
const updBubbleCount = updateChatBubbleCount();
|
|
262
284
|
const insTc = db.prepare('INSERT INTO tool_calls (chat_id, tool_name, args_json, source, folder, timestamp) VALUES (?, ?, ?, ?, ?, ?)');
|
|
263
285
|
const chatTs = chat.lastUpdatedAt || chat.createdAt || null;
|
|
286
|
+
const chatSource = normalizeEditorSource(chat.source);
|
|
264
287
|
|
|
265
288
|
let seq = 0;
|
|
266
289
|
for (const msg of messages) {
|
|
@@ -277,7 +300,7 @@ function analyzeAndStore(chat) {
|
|
|
277
300
|
for (const tc of msg._toolCalls) {
|
|
278
301
|
stats.toolCalls.push(tc.name);
|
|
279
302
|
try {
|
|
280
|
-
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);
|
|
281
304
|
} catch { }
|
|
282
305
|
}
|
|
283
306
|
} else {
|
|
@@ -286,7 +309,7 @@ function analyzeAndStore(chat) {
|
|
|
286
309
|
for (const m of toolMatches) {
|
|
287
310
|
const name = m.match(/\[tool-call: ([^(]+)/)?.[1] || 'unknown';
|
|
288
311
|
stats.toolCalls.push(name.trim());
|
|
289
|
-
insTc.run(chat.composerId, name.trim(), '{}',
|
|
312
|
+
insTc.run(chat.composerId, name.trim(), '{}', chatSource, chat.folder || null, chatTs);
|
|
290
313
|
}
|
|
291
314
|
}
|
|
292
315
|
}
|
|
@@ -338,7 +361,7 @@ function scanAll(onProgress, opts = {}) {
|
|
|
338
361
|
const batchInsert = db.transaction((chatBatch) => {
|
|
339
362
|
for (const chat of chatBatch) {
|
|
340
363
|
ins.run(
|
|
341
|
-
chat.composerId, chat.source, chat.name || null, chat.mode || null,
|
|
364
|
+
chat.composerId, normalizeEditorSource(chat.source), chat.name || null, chat.mode || null,
|
|
342
365
|
chat.folder || null, chat.createdAt || null, chat.lastUpdatedAt || null,
|
|
343
366
|
chat.encrypted ? 1 : 0, chat.bubbleCount || 0,
|
|
344
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 })
|
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:
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// Codebuff adapter
|
|
7
|
+
// ------------------------------------------------------------
|
|
8
|
+
// Codebuff persists chats under ~/.config/manicode (the legacy folder name
|
|
9
|
+
// — the product was previously called Manicode). Non-prod builds use
|
|
10
|
+
// manicode-dev / manicode-staging. Layout:
|
|
11
|
+
//
|
|
12
|
+
// ~/.config/manicode/projects/<projectBasename>/chats/<chatId>/
|
|
13
|
+
// ├── chat-messages.json // serialized ChatMessage[]
|
|
14
|
+
// ├── run-state.json // SDK RunState (has real `cwd`)
|
|
15
|
+
// └── log.jsonl // internal logs (ignored)
|
|
16
|
+
//
|
|
17
|
+
// chatId is an ISO timestamp with ':' replaced by '-'. We use
|
|
18
|
+
// "<projectBasename>::<chatId>" as composerId to avoid collisions when
|
|
19
|
+
// two different projects share the same folder basename.
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
const HOME = os.homedir();
|
|
23
|
+
|
|
24
|
+
function getProjectRoots() {
|
|
25
|
+
const roots = [];
|
|
26
|
+
for (const variant of ['manicode', 'manicode-dev', 'manicode-staging']) {
|
|
27
|
+
const projectsDir = path.join(HOME, '.config', variant, 'projects');
|
|
28
|
+
if (fs.existsSync(projectsDir)) roots.push({ variant, projectsDir });
|
|
29
|
+
}
|
|
30
|
+
return roots;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeReadJson(filePath) {
|
|
34
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseChatIdToTs(chatId) {
|
|
38
|
+
// Codebuff chatIds look like "2026-04-21T16-34-12.000Z" — reverse the
|
|
39
|
+
// substitution so we get a real ISO timestamp back.
|
|
40
|
+
if (!chatId) return null;
|
|
41
|
+
const iso = chatId.replace(/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})/, '$1:$2:$3');
|
|
42
|
+
const ts = Date.parse(iso);
|
|
43
|
+
return Number.isFinite(ts) ? ts : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanPrompt(text) {
|
|
47
|
+
if (!text) return null;
|
|
48
|
+
const clean = String(text).replace(/\s+/g, ' ').trim().substring(0, 120);
|
|
49
|
+
return clean || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractCwdFromRunState(runState) {
|
|
53
|
+
if (!runState) return null;
|
|
54
|
+
// Common shapes: { sessionState: { ... cwd }, output } or { cwd } at root.
|
|
55
|
+
const candidates = [
|
|
56
|
+
runState?.sessionState?.projectContext?.cwd,
|
|
57
|
+
runState?.sessionState?.fileContext?.cwd,
|
|
58
|
+
runState?.sessionState?.cwd,
|
|
59
|
+
runState?.cwd,
|
|
60
|
+
];
|
|
61
|
+
for (const c of candidates) if (typeof c === 'string' && c) return c;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Best-effort model / token extraction ---
|
|
66
|
+
|
|
67
|
+
function pickNumber(...vals) {
|
|
68
|
+
for (const v of vals) {
|
|
69
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractUsageFromMetadata(meta) {
|
|
75
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
76
|
+
const cb = meta.codebuff && typeof meta.codebuff === 'object' ? meta.codebuff : null;
|
|
77
|
+
const usage = (cb && cb.usage) || meta.usage || null;
|
|
78
|
+
if (!usage || typeof usage !== 'object') return {};
|
|
79
|
+
return {
|
|
80
|
+
inputTokens: pickNumber(usage.inputTokens, usage.promptTokens, usage.prompt_tokens, usage.input_tokens),
|
|
81
|
+
outputTokens: pickNumber(usage.outputTokens, usage.completionTokens, usage.completion_tokens, usage.output_tokens),
|
|
82
|
+
cacheRead: pickNumber(
|
|
83
|
+
usage.cacheReadInputTokens, usage.cache_read_input_tokens,
|
|
84
|
+
usage?.promptTokensDetails?.cachedTokens, usage?.prompt_tokens_details?.cached_tokens,
|
|
85
|
+
),
|
|
86
|
+
cacheWrite: pickNumber(
|
|
87
|
+
usage.cacheCreationInputTokens, usage.cache_creation_input_tokens,
|
|
88
|
+
usage.cachedTokensCreated,
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractMessageUsageAndModel(msg) {
|
|
94
|
+
// Codebuff ChatMessage shape allows metadata.runState to stash the SDK
|
|
95
|
+
// RunState after completion. Model + usage isn't guaranteed to be there
|
|
96
|
+
// but we try a few known spots.
|
|
97
|
+
const out = { model: undefined, inputTokens: undefined, outputTokens: undefined, cacheRead: undefined, cacheWrite: undefined };
|
|
98
|
+
const meta = msg?.metadata;
|
|
99
|
+
if (!meta || typeof meta !== 'object') return out;
|
|
100
|
+
|
|
101
|
+
// Direct provider hints some Codebuff builds attach.
|
|
102
|
+
if (typeof meta.model === 'string') out.model = meta.model;
|
|
103
|
+
if (typeof meta.modelId === 'string' && !out.model) out.model = meta.modelId;
|
|
104
|
+
|
|
105
|
+
// Token totals may live on metadata.usage or inside providerMetadata.
|
|
106
|
+
const usageDirect = extractUsageFromMetadata(meta);
|
|
107
|
+
Object.assign(out, {
|
|
108
|
+
inputTokens: out.inputTokens ?? usageDirect.inputTokens,
|
|
109
|
+
outputTokens: out.outputTokens ?? usageDirect.outputTokens,
|
|
110
|
+
cacheRead: out.cacheRead ?? usageDirect.cacheRead,
|
|
111
|
+
cacheWrite: out.cacheWrite ?? usageDirect.cacheWrite,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Walk the RunState stash for the most recent assistant message with
|
|
115
|
+
// providerOptions that carry OpenRouter-style usage.
|
|
116
|
+
const rs = meta.runState;
|
|
117
|
+
if (rs && typeof rs === 'object') {
|
|
118
|
+
const history = rs?.sessionState?.mainAgentState?.messageHistory;
|
|
119
|
+
if (Array.isArray(history)) {
|
|
120
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
121
|
+
const m = history[i];
|
|
122
|
+
if (m?.role !== 'assistant') continue;
|
|
123
|
+
const po = m.providerOptions;
|
|
124
|
+
const u = extractUsageFromMetadata(po);
|
|
125
|
+
if (u.inputTokens != null || u.outputTokens != null) {
|
|
126
|
+
out.inputTokens = out.inputTokens ?? u.inputTokens;
|
|
127
|
+
out.outputTokens = out.outputTokens ?? u.outputTokens;
|
|
128
|
+
out.cacheRead = out.cacheRead ?? u.cacheRead;
|
|
129
|
+
out.cacheWrite = out.cacheWrite ?? u.cacheWrite;
|
|
130
|
+
if (!out.model && typeof po?.codebuff?.model === 'string') out.model = po.codebuff.model;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Block flattening: turn Codebuff ChatMessage blocks into a single
|
|
141
|
+
// transcript-style content string + normalized tool-call list. ---
|
|
142
|
+
|
|
143
|
+
function flattenBlocks(blocks, out = { parts: [], toolCalls: [] }, depth = 0) {
|
|
144
|
+
if (!Array.isArray(blocks)) return out;
|
|
145
|
+
const indent = depth ? ' '.repeat(depth) : '';
|
|
146
|
+
for (const block of blocks) {
|
|
147
|
+
if (!block || typeof block !== 'object') continue;
|
|
148
|
+
switch (block.type) {
|
|
149
|
+
case 'text': {
|
|
150
|
+
if (typeof block.content !== 'string' || !block.content) break;
|
|
151
|
+
if (block.textType === 'reasoning') {
|
|
152
|
+
out.parts.push(`${indent}[thinking] ${block.content}`);
|
|
153
|
+
} else {
|
|
154
|
+
out.parts.push(`${indent}${block.content}`);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'tool': {
|
|
159
|
+
const toolName = block.toolName || 'tool';
|
|
160
|
+
const input = block.input || {};
|
|
161
|
+
const argKeys = (input && typeof input === 'object') ? Object.keys(input).join(', ') : '';
|
|
162
|
+
out.parts.push(`${indent}[tool-call: ${toolName}(${argKeys})]`);
|
|
163
|
+
out.toolCalls.push({ name: toolName, args: input });
|
|
164
|
+
if (typeof block.output === 'string' && block.output) {
|
|
165
|
+
out.parts.push(`${indent}[tool-result] ${block.output.substring(0, 500)}`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'agent': {
|
|
170
|
+
const name = block.agentName || block.agentType || 'agent';
|
|
171
|
+
const status = block.status ? ` (${block.status})` : '';
|
|
172
|
+
out.parts.push(`${indent}[subagent: ${name}${status}]`);
|
|
173
|
+
if (typeof block.content === 'string' && block.content) {
|
|
174
|
+
out.parts.push(`${indent} ${block.content}`);
|
|
175
|
+
}
|
|
176
|
+
flattenBlocks(block.blocks, out, depth + 1);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'plan': {
|
|
180
|
+
if (typeof block.content === 'string' && block.content) {
|
|
181
|
+
out.parts.push(`${indent}[plan]\n${block.content}`);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'mode-divider': {
|
|
186
|
+
if (block.mode) out.parts.push(`${indent}[mode: ${block.mode}]`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'ask-user': {
|
|
190
|
+
const qs = Array.isArray(block.questions) ? block.questions : [];
|
|
191
|
+
for (const q of qs) {
|
|
192
|
+
if (q?.question) out.parts.push(`${indent}[ask-user] ${q.question}`);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'image': {
|
|
197
|
+
out.parts.push(`${indent}[image${block.filename ? `: ${block.filename}` : ''}]`);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================
|
|
208
|
+
// Adapter interface
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
const name = 'codebuff';
|
|
212
|
+
const labels = { 'codebuff': 'Codebuff' };
|
|
213
|
+
|
|
214
|
+
function getChats() {
|
|
215
|
+
const chats = [];
|
|
216
|
+
const roots = getProjectRoots();
|
|
217
|
+
if (roots.length === 0) return chats;
|
|
218
|
+
|
|
219
|
+
for (const { variant, projectsDir } of roots) {
|
|
220
|
+
let projectDirs;
|
|
221
|
+
try { projectDirs = fs.readdirSync(projectsDir); } catch { continue; }
|
|
222
|
+
// Only the non-prod variants get prefixed so the prod composerId stays clean.
|
|
223
|
+
const variantPrefix = variant === 'manicode' ? '' : `${variant}::`;
|
|
224
|
+
|
|
225
|
+
for (const projectBase of projectDirs) {
|
|
226
|
+
const projectDir = path.join(projectsDir, projectBase);
|
|
227
|
+
try { if (!fs.statSync(projectDir).isDirectory()) continue; } catch { continue; }
|
|
228
|
+
|
|
229
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
230
|
+
if (!fs.existsSync(chatsDir)) continue;
|
|
231
|
+
|
|
232
|
+
let chatIds;
|
|
233
|
+
try { chatIds = fs.readdirSync(chatsDir); } catch { continue; }
|
|
234
|
+
|
|
235
|
+
for (const chatId of chatIds) {
|
|
236
|
+
const chatDir = path.join(chatsDir, chatId);
|
|
237
|
+
let dirStat;
|
|
238
|
+
try { dirStat = fs.statSync(chatDir); } catch { continue; }
|
|
239
|
+
if (!dirStat.isDirectory()) continue;
|
|
240
|
+
|
|
241
|
+
const messagesPath = path.join(chatDir, 'chat-messages.json');
|
|
242
|
+
if (!fs.existsSync(messagesPath)) continue;
|
|
243
|
+
|
|
244
|
+
// Light peek for title + message count — don't hydrate blocks here.
|
|
245
|
+
const messages = safeReadJson(messagesPath);
|
|
246
|
+
if (!Array.isArray(messages) || messages.length === 0) continue;
|
|
247
|
+
|
|
248
|
+
const firstUser = messages.find((m) => m && m.variant === 'user' && typeof m.content === 'string');
|
|
249
|
+
const title = cleanPrompt(firstUser && firstUser.content);
|
|
250
|
+
|
|
251
|
+
// Recover the real cwd so Agentlytics can group by project correctly.
|
|
252
|
+
const runState = safeReadJson(path.join(chatDir, 'run-state.json'));
|
|
253
|
+
const folder = extractCwdFromRunState(runState) || null;
|
|
254
|
+
|
|
255
|
+
chats.push({
|
|
256
|
+
source: 'codebuff',
|
|
257
|
+
composerId: `${variantPrefix}${projectBase}::${chatId}`,
|
|
258
|
+
name: title,
|
|
259
|
+
createdAt: parseChatIdToTs(chatId),
|
|
260
|
+
lastUpdatedAt: dirStat.mtime.getTime(),
|
|
261
|
+
mode: 'codebuff',
|
|
262
|
+
folder,
|
|
263
|
+
encrypted: false,
|
|
264
|
+
bubbleCount: messages.length,
|
|
265
|
+
_fullPath: chatDir,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return chats;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getMessages(chat) {
|
|
275
|
+
const chatDir = chat._fullPath;
|
|
276
|
+
if (!chatDir) return [];
|
|
277
|
+
const messagesPath = path.join(chatDir, 'chat-messages.json');
|
|
278
|
+
if (!fs.existsSync(messagesPath)) return [];
|
|
279
|
+
|
|
280
|
+
const raw = safeReadJson(messagesPath);
|
|
281
|
+
if (!Array.isArray(raw)) return [];
|
|
282
|
+
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const msg of raw) {
|
|
285
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
286
|
+
const variant = msg.variant;
|
|
287
|
+
|
|
288
|
+
if (variant === 'user') {
|
|
289
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
290
|
+
if (content) out.push({ role: 'user', content });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (variant === 'error') {
|
|
295
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
296
|
+
if (content) out.push({ role: 'system', content: `[error] ${content}` });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (variant === 'ai' || variant === 'agent') {
|
|
301
|
+
const flattened = flattenBlocks(msg.blocks);
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (typeof msg.content === 'string' && msg.content) parts.push(msg.content);
|
|
304
|
+
if (flattened.parts.length) parts.push(flattened.parts.join('\n'));
|
|
305
|
+
const content = parts.join('\n').trim();
|
|
306
|
+
if (!content) continue;
|
|
307
|
+
|
|
308
|
+
const { model, inputTokens, outputTokens, cacheRead, cacheWrite } = extractMessageUsageAndModel(msg);
|
|
309
|
+
|
|
310
|
+
out.push({
|
|
311
|
+
role: 'assistant',
|
|
312
|
+
content,
|
|
313
|
+
_model: model,
|
|
314
|
+
_inputTokens: inputTokens,
|
|
315
|
+
_outputTokens: outputTokens,
|
|
316
|
+
_cacheRead: cacheRead,
|
|
317
|
+
_cacheWrite: cacheWrite,
|
|
318
|
+
_toolCalls: flattened.toolCalls.length ? flattened.toolCalls : undefined,
|
|
319
|
+
_credits: typeof msg.credits === 'number' ? msg.credits : undefined,
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getArtifacts(folder) {
|
|
329
|
+
const { scanArtifacts } = require('./base');
|
|
330
|
+
return scanArtifacts(folder, {
|
|
331
|
+
editor: 'codebuff',
|
|
332
|
+
label: 'Codebuff',
|
|
333
|
+
files: ['.codebuffignore', '.manicodeignore', 'knowledge.md'],
|
|
334
|
+
dirs: ['.agents'],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts };
|
package/editors/copilot.js
CHANGED
|
@@ -240,20 +240,20 @@ async function getUsage() {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const labels = { 'copilot-cli': 'Copilot
|
|
243
|
+
const labels = { 'copilot-cli': 'GitHub Copilot' };
|
|
244
244
|
|
|
245
245
|
function getArtifacts(folder) {
|
|
246
246
|
const { scanArtifacts } = require('./base');
|
|
247
247
|
return scanArtifacts(folder, {
|
|
248
248
|
editor: 'copilot-cli',
|
|
249
|
-
label: 'Copilot',
|
|
249
|
+
label: 'GitHub Copilot',
|
|
250
250
|
files: ['.github/copilot-instructions.md'],
|
|
251
251
|
dirs: [],
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
function getMCPServers() {
|
|
256
|
-
// Copilot
|
|
256
|
+
// GitHub Copilot shares MCP config with VS Code (handled by vscode.js)
|
|
257
257
|
return [];
|
|
258
258
|
}
|
|
259
259
|
|
package/editors/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cursor = require('./cursor');
|
|
2
|
-
const
|
|
2
|
+
const devin = require('./windsurf');
|
|
3
3
|
const antigravity = require('./antigravity');
|
|
4
4
|
const claude = require('./claude');
|
|
5
5
|
const vscode = require('./vscode');
|
|
@@ -12,8 +12,9 @@ const cursorAgent = require('./cursor-agent');
|
|
|
12
12
|
const commandcode = require('./commandcode');
|
|
13
13
|
const goose = require('./goose');
|
|
14
14
|
const kiro = require('./kiro');
|
|
15
|
+
const codebuff = require('./codebuff');
|
|
15
16
|
|
|
16
|
-
const editors = [cursor,
|
|
17
|
+
const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro, codebuff];
|
|
17
18
|
|
|
18
19
|
// Build a unified source → display-label map from all editor modules
|
|
19
20
|
const editorLabels = {};
|
|
@@ -47,9 +48,13 @@ function getAllChats() {
|
|
|
47
48
|
*/
|
|
48
49
|
function getMessages(chat) {
|
|
49
50
|
const editor = editors.find((e) => e.name === chat.source);
|
|
50
|
-
// Match variants:
|
|
51
|
+
// Match variants: devin-next, antigravity, claude-code, vscode-insiders, plus legacy aliases.
|
|
51
52
|
const resolvedEditor = editor || editors.find((e) =>
|
|
52
|
-
chat.source && (
|
|
53
|
+
chat.source && (
|
|
54
|
+
chat.source.startsWith(e.name) ||
|
|
55
|
+
(e.sources && e.sources.includes(chat.source)) ||
|
|
56
|
+
(e.legacySources && e.legacySources.includes(chat.source))
|
|
57
|
+
)
|
|
53
58
|
);
|
|
54
59
|
if (!resolvedEditor) return [];
|
|
55
60
|
return resolvedEditor.getMessages(chat);
|
|
@@ -72,7 +77,7 @@ async function getAllUsage() {
|
|
|
72
77
|
try {
|
|
73
78
|
const usage = await editor.getUsage();
|
|
74
79
|
if (!usage) continue;
|
|
75
|
-
//
|
|
80
|
+
// Devin returns an array (one per variant), Cursor returns a single object
|
|
76
81
|
if (Array.isArray(usage)) results.push(...usage);
|
|
77
82
|
else results.push(usage);
|
|
78
83
|
} catch { /* skip broken adapters */ }
|