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 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
@@ -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 || {}), chat.source, chat.folder || null, chatTs);
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(), '{}', chat.source, chat.folder || null, chatTs);
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', '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:
@@ -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 };
@@ -240,20 +240,20 @@ async function getUsage() {
240
240
  };
241
241
  }
242
242
 
243
- const labels = { 'copilot-cli': 'Copilot CLI' };
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 CLI shares MCP config with VS Code (handled by vscode.js)
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 windsurf = require('./windsurf');
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, windsurf, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
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: windsurf-next, antigravity, claude-code, vscode-insiders etc.
51
+ // Match variants: devin-next, antigravity, claude-code, vscode-insiders, plus legacy aliases.
51
52
  const resolvedEditor = editor || editors.find((e) =>
52
- chat.source && (chat.source.startsWith(e.name) || (e.sources && e.sources.includes(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
- // Windsurf returns an array (one per variant), Cursor returns a single object
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 */ }