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/editors/gsd.js ADDED
@@ -0,0 +1,366 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const name = 'gsd';
5
+ const labels = { gsd: 'GSD Workflow' };
6
+
7
+ // ============================================================
8
+ // Helpers
9
+ // ============================================================
10
+
11
+ function readFileSafe(filePath) {
12
+ try { return fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
13
+ }
14
+
15
+ function statSafe(filePath) {
16
+ try { return fs.statSync(filePath); } catch { return null; }
17
+ }
18
+
19
+ function countFiles(dir) {
20
+ try { return fs.readdirSync(dir).filter(f => !f.startsWith('.')).length; } catch { return 0; }
21
+ }
22
+
23
+ /**
24
+ * Parse YAML frontmatter from STATE.md.
25
+ * Returns { status, milestone, stoppedAt, progress } or null.
26
+ */
27
+ function parseStateMd(content) {
28
+ if (!content) return null;
29
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
30
+ if (!m) return null;
31
+ const yaml = m[1];
32
+ function get(key) {
33
+ const r = yaml.match(new RegExp(`^${key}:\\s*(.+)`, 'm'));
34
+ return r ? r[1].trim().replace(/^["']|["']$/g, '') : null;
35
+ }
36
+ function getInt(key) { const v = get(key); return v ? parseInt(v) : null; }
37
+ const progressBlock = yaml.match(/^progress:\s*\n((?:[ \t]+.+\n?)*)/m);
38
+ let progress = null;
39
+ if (progressBlock) {
40
+ const pb = progressBlock[1];
41
+ function pgGet(key) {
42
+ const r = pb.match(new RegExp(`${key}:\\s*(\\d+)`));
43
+ return r ? parseInt(r[1]) : null;
44
+ }
45
+ progress = {
46
+ total_phases: pgGet('total_phases'),
47
+ completed_phases: pgGet('completed_phases'),
48
+ total_plans: pgGet('total_plans'),
49
+ completed_plans: pgGet('completed_plans'),
50
+ };
51
+ }
52
+ return {
53
+ status: get('status'),
54
+ milestone: get('milestone'),
55
+ stoppedAt: get('stopped_at'),
56
+ lastUpdated: get('last_updated'),
57
+ progress,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Parse ROADMAP.md phase checkboxes into a map of phase_number → completed.
63
+ * Phase lines look like: - [x] **Phase 1: Name** or - [ ] Phase 2: Name
64
+ */
65
+ function parseRoadmapPhaseStatus(content) {
66
+ if (!content) return new Map();
67
+ const statusMap = new Map(); // phase_number (int) → 'completed' | 'planned'
68
+ for (const line of content.split('\n')) {
69
+ const cbMatch = line.match(/^[-*]\s+\[([x ])\]\s+(.+)/i);
70
+ if (!cbMatch) continue;
71
+ const text = cbMatch[2];
72
+ if (/\d+-\d+-PLAN\.md/i.test(text) || /PLAN\.md\s*[—–-]/i.test(text)) continue;
73
+ // Extract phase number from patterns: "Phase 1:", "Phase 01:", "**Phase 2:**"
74
+ const numMatch = text.match(/phase\s+(\d+)/i);
75
+ if (!numMatch) continue;
76
+ const num = parseInt(numMatch[1]);
77
+ if (!isNaN(num)) {
78
+ statusMap.set(num, cbMatch[1].toLowerCase() === 'x' ? 'completed' : 'planned');
79
+ }
80
+ }
81
+ return statusMap;
82
+ }
83
+
84
+ /**
85
+ * Parse PROJECT.md — first # heading = project name, rest = description.
86
+ */
87
+ function parseProjectMd(content) {
88
+ if (!content) return { name: null, description: null };
89
+ const lines = content.split('\n');
90
+ let projectName = null;
91
+ const descLines = [];
92
+ for (const line of lines) {
93
+ const h1 = line.match(/^#\s+(.+)/);
94
+ if (h1 && !projectName) {
95
+ projectName = h1[1].trim();
96
+ continue;
97
+ }
98
+ if (projectName && line.trim()) descLines.push(line.trim());
99
+ }
100
+ return {
101
+ name: projectName,
102
+ description: descLines.slice(0, 3).join(' ').substring(0, 300) || null,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Parse ROADMAP.md for phase completion.
108
+ * Supports: - [ ] **Phase N: Name** ... and - [x] **Phase N: Name** ...
109
+ * Also supports emoji: ✅ completed, 🚧 in-progress, □ planned
110
+ */
111
+ function parseRoadmapMd(content) {
112
+ if (!content) return [];
113
+ const phases = [];
114
+ for (const line of content.split('\n')) {
115
+ // Checkbox style: - [ ] or - [x]
116
+ // Only count lines that look like phase entries, NOT plan file listings (e.g. "01-01-PLAN.md")
117
+ const cbMatch = line.match(/^[-*]\s+\[([x ])\]\s+(.+)/i);
118
+ if (cbMatch) {
119
+ const text = cbMatch[2];
120
+ // Skip plan file entries like "01-01-PLAN.md — description"
121
+ if (/\d+-\d+-PLAN\.md/i.test(text) || /PLAN\.md\s*[—–-]/i.test(text)) continue;
122
+ phases.push({ text: text.replace(/\*\*/g, '').trim(), completed: cbMatch[1].toLowerCase() === 'x' });
123
+ continue;
124
+ }
125
+ // Emoji style: ✅ completed, 🚧 in-progress (milestone-level)
126
+ const emojiMatch = line.match(/^[-*]\s+(✅|🚧|⬜|□)\s+(.+)/);
127
+ if (emojiMatch) {
128
+ phases.push({ text: emojiMatch[2].replace(/\*\*/g, '').trim(), completed: emojiMatch[1] === '✅' });
129
+ }
130
+ }
131
+ return phases;
132
+ }
133
+
134
+ /**
135
+ * Detect the currently active milestone name from ROADMAP.md.
136
+ * Looks for 🚧 milestone entries or the first uncompleted milestone block.
137
+ */
138
+ function detectActiveMilestone(content) {
139
+ if (!content) return null;
140
+ for (const line of content.split('\n')) {
141
+ const m = line.match(/🚧\s+\*?\*?([^*\n-]+)/);
142
+ if (m) return m[1].trim().split(' - ')[0].trim();
143
+ // Also handle "in progress" text
144
+ if (/in.progress/i.test(line)) {
145
+ const nm = line.match(/\*?\*?([vV][\d.]+[^*]*)\*?\*?/);
146
+ if (nm) return nm[1].trim();
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Extract phase number prefix from a phase directory name.
154
+ * e.g. "01-auth-ve-giris" → "01"
155
+ * e.g. "999.1-backlog-item" → "999.1"
156
+ */
157
+ function extractPhasePrefix(dirName) {
158
+ const m = dirName.match(/^(\d+(?:\.\d+)?)-/);
159
+ return m ? m[1] : null;
160
+ }
161
+
162
+ /**
163
+ * Parse checkbox tasks from a PLAN.md file.
164
+ */
165
+ function parseCheckboxes(content) {
166
+ if (!content) return { total: 0, completed: 0, tasks: [] };
167
+ const tasks = [];
168
+ for (const line of content.split('\n')) {
169
+ const m = line.match(/^[-*]\s+\[([x ])\]\s+(.+)/i);
170
+ if (!m) continue;
171
+ tasks.push({ name: m[2].trim(), completed: m[1].toLowerCase() === 'x' });
172
+ }
173
+ return { total: tasks.length, completed: tasks.filter(t => t.completed).length, tasks };
174
+ }
175
+
176
+ // ============================================================
177
+ // Public API
178
+ // ============================================================
179
+
180
+ /**
181
+ * Scan known project folders for GSD .planning/ directories.
182
+ * Returns project-level summary for each GSD project found.
183
+ */
184
+ function getGSDProjects(knownFolders) {
185
+ const results = [];
186
+
187
+ for (const folder of knownFolders) {
188
+ if (!folder) continue;
189
+ const planningDir = path.join(folder, '.planning');
190
+ if (!fs.existsSync(planningDir)) continue;
191
+
192
+ // Must have at least PROJECT.md or ROADMAP.md to be a GSD project
193
+ const hasProject = fs.existsSync(path.join(planningDir, 'PROJECT.md'));
194
+ const hasRoadmap = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
195
+ if (!hasProject && !hasRoadmap) continue;
196
+
197
+ const projectContent = readFileSafe(path.join(planningDir, 'PROJECT.md'));
198
+ const roadmapContent = readFileSafe(path.join(planningDir, 'ROADMAP.md'));
199
+ const stateContent = readFileSafe(path.join(planningDir, 'STATE.md'));
200
+ const stateData = parseStateMd(stateContent);
201
+
202
+ const { name: projectName, description } = parseProjectMd(projectContent);
203
+
204
+ // Use filesystem as source of truth for phase counts
205
+ // Filter out malformed directory names (e.g. dirs with JSON content in name)
206
+ const phases = getGSDPhases(folder, roadmapContent);
207
+ const validPhases = phases.filter(ph => ph.number !== null);
208
+
209
+ const totalPhases = validPhases.length;
210
+ const completedPhases = validPhases.filter(p => p.status === 'completed').length;
211
+ const firstIncomplete = validPhases.find(p => p.status !== 'completed');
212
+ const activePhase = stateData?.stoppedAt || (firstIncomplete ? firstIncomplete.name : null);
213
+
214
+ // Prefer STATE.md milestone, fallback to ROADMAP detection
215
+ const activeMilestone = stateData?.milestone || detectActiveMilestone(roadmapContent);
216
+
217
+ // Count todos/seeds/quick (common GSD directories)
218
+ const todos = countFiles(path.join(planningDir, 'todos'))
219
+ + countFiles(path.join(planningDir, 'seeds'));
220
+ const notes = countFiles(path.join(planningDir, 'quick'));
221
+ const backlog = countFiles(path.join(planningDir, 'backlog'));
222
+
223
+ const planStat = statSafe(planningDir);
224
+ const lastModified = planStat ? Math.round(planStat.mtimeMs) : null;
225
+
226
+ results.push({
227
+ folder,
228
+ name: projectName || path.basename(folder),
229
+ description,
230
+ milestone: activeMilestone,
231
+ totalPhases,
232
+ completedPhases,
233
+ activePhase,
234
+ todos,
235
+ backlog,
236
+ notes,
237
+ lastModified,
238
+ });
239
+ }
240
+
241
+ return results;
242
+ }
243
+
244
+ /**
245
+ * Return phase details for a single GSD project.
246
+ * Phases live in .planning/phases/<phaseDir>/
247
+ * roadmapContent is optionally passed to cross-reference checkbox status.
248
+ */
249
+ function getGSDPhases(folder, roadmapContent) {
250
+ if (roadmapContent === undefined) {
251
+ roadmapContent = readFileSafe(path.join(folder, '.planning', 'ROADMAP.md'));
252
+ }
253
+ const roadmapStatus = parseRoadmapPhaseStatus(roadmapContent);
254
+ const phasesDir = path.join(folder, '.planning', 'phases');
255
+ let phaseDirs;
256
+ try {
257
+ phaseDirs = fs.readdirSync(phasesDir)
258
+ .filter(f => {
259
+ try { return fs.statSync(path.join(phasesDir, f)).isDirectory(); } catch { return false; }
260
+ })
261
+ .sort((a, b) => {
262
+ // Sort by numeric prefix (supports decimals like 999.1)
263
+ const na = parseFloat(extractPhasePrefix(a) || '9999');
264
+ const nb = parseFloat(extractPhasePrefix(b) || '9999');
265
+ return na - nb;
266
+ });
267
+ } catch { return []; }
268
+
269
+ const phases = [];
270
+ for (const phaseDir of phaseDirs) {
271
+ const phaseFullDir = path.join(phasesDir, phaseDir);
272
+ const prefix = extractPhasePrefix(phaseDir);
273
+
274
+ // Phase name: everything after the leading number prefix
275
+ const nameRaw = phaseDir.replace(/^\d+(?:\.\d+)?-/, '').replace(/-/g, ' ');
276
+ const phaseName = nameRaw.length > 0 ? nameRaw : phaseDir;
277
+
278
+ // Phase number (numeric value for display)
279
+ const phaseNumber = prefix ? parseFloat(prefix) : null;
280
+
281
+ // Detect artifact files using the phase prefix pattern
282
+ let hasPlan = false, hasResearch = false, hasVerification = false;
283
+ let planCount = 0, summaryCount = 0;
284
+ let latestMtime = 0;
285
+
286
+ try {
287
+ const files = fs.readdirSync(phaseFullDir);
288
+ for (const f of files) {
289
+ const fPath = path.join(phaseFullDir, f);
290
+ const st = statSafe(fPath);
291
+ if (st && st.mtimeMs > latestMtime) latestMtime = st.mtimeMs;
292
+
293
+ // PLAN files: {prefix}-{n}-PLAN.md
294
+ if (f.match(/PLAN\.md$/i)) { hasPlan = true; planCount++; }
295
+ // SUMMARY files: indicates a plan was executed
296
+ if (f.match(/SUMMARY\.md$/i)) summaryCount++;
297
+ // VERIFICATION
298
+ if (f.match(/VERIFICATION\.md$/i)) hasVerification = true;
299
+ // RESEARCH
300
+ if (f.match(/RESEARCH\.md$/i)) hasResearch = true;
301
+ }
302
+ } catch { /* skip */ }
303
+
304
+ // Determine status: file-based first, then cross-reference ROADMAP.md checkboxes
305
+ let status = 'planned';
306
+ if (hasVerification) {
307
+ status = 'completed';
308
+ } else if (roadmapStatus.get(phaseNumber) === 'completed') {
309
+ // ROADMAP.md marks this phase [x] even without VERIFICATION.md
310
+ status = 'completed';
311
+ } else if (summaryCount > 0) {
312
+ status = 'executing';
313
+ } else if (hasPlan) {
314
+ status = 'planned';
315
+ }
316
+
317
+ phases.push({
318
+ phaseDir,
319
+ number: phaseNumber,
320
+ name: phaseName,
321
+ status,
322
+ tasks: { total: planCount, completed: summaryCount },
323
+ hasVerification,
324
+ hasPlan,
325
+ hasResearch,
326
+ lastModified: latestMtime || null,
327
+ });
328
+ }
329
+
330
+ return phases;
331
+ }
332
+
333
+ /**
334
+ * Return combined PLAN.md content for a phase.
335
+ * Aggregates all *-PLAN.md files in order.
336
+ */
337
+ function getGSDPlanDetail(folder, phaseDir) {
338
+ const phaseFullDir = path.join(folder, '.planning', 'phases', phaseDir);
339
+ if (!fs.existsSync(phaseFullDir)) return null;
340
+
341
+ let files;
342
+ try { files = fs.readdirSync(phaseFullDir).filter(f => f.match(/PLAN\.md$/i)).sort(); }
343
+ catch { return null; }
344
+ if (files.length === 0) return null;
345
+
346
+ const sections = [];
347
+ const allTasks = [];
348
+
349
+ for (const f of files) {
350
+ const content = readFileSafe(path.join(phaseFullDir, f));
351
+ if (!content) continue;
352
+ sections.push(`## ${f}\n\n${content}`);
353
+ const { tasks } = parseCheckboxes(content);
354
+ allTasks.push(...tasks);
355
+ }
356
+
357
+ return { content: sections.join('\n\n---\n\n'), tasks: allTasks };
358
+ }
359
+
360
+ module.exports = {
361
+ name,
362
+ labels,
363
+ getGSDProjects,
364
+ getGSDPhases,
365
+ getGSDPlanDetail,
366
+ };
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 */ }
@@ -3,10 +3,24 @@ const path = require('path');
3
3
  const os = require('os');
4
4
  const fs = require('fs');
5
5
 
6
- // Windsurf variants: Windsurf, Windsurf Next
6
+ // Devin variants. Keep legacy Windsurf identifiers only for detection/config compatibility.
7
7
  const VARIANTS = [
8
- { id: 'windsurf', matchKey: 'ide', matchVal: 'windsurf', https: false, appName: 'Windsurf', needsMetadata: true },
9
- { id: 'windsurf-next', matchKey: 'ide', matchVal: 'windsurf-next', https: false, appName: 'Windsurf - Next', needsMetadata: true },
8
+ {
9
+ id: 'devin',
10
+ matchKey: 'ide',
11
+ matchVals: ['windsurf', 'devin', 'devin-desktop'],
12
+ https: false,
13
+ appNames: ['Devin Desktop', 'Devin', 'Windsurf'],
14
+ needsMetadata: true,
15
+ },
16
+ {
17
+ id: 'devin-next',
18
+ matchKey: 'ide',
19
+ matchVals: ['windsurf-next', 'devin-next', 'devin-desktop-next'],
20
+ https: false,
21
+ appNames: ['Devin Desktop - Next', 'Devin - Next', 'Windsurf - Next'],
22
+ needsMetadata: true,
23
+ },
10
24
  ];
11
25
 
12
26
  // ============================================================
@@ -85,7 +99,7 @@ function getListeningPorts(pid) {
85
99
  }
86
100
 
87
101
  // ============================================================
88
- // Find running Windsurf language server (port + CSRF token)
102
+ // Find running Devin language server (port + CSRF token)
89
103
  // ============================================================
90
104
 
91
105
  let _lsCache = null;
@@ -101,13 +115,13 @@ function findLanguageServers() {
101
115
  ? 'language_server_macos'
102
116
  : 'language_server_linux';
103
117
 
104
- // On macOS/Linux, also check env vars for WINDSURF_CSRF_TOKEN (newer Windsurf Next passes CSRF via env, not CLI arg)
118
+ // On macOS/Linux, also check env vars for CSRF tokens (some variants pass CSRF via env, not CLI arg)
105
119
  const envCsrfByPid = {};
106
120
  if (!IS_WINDOWS) {
107
121
  try {
108
122
  const psEnv = execSync('ps eww -A', { encoding: 'utf-8', maxBuffer: 2 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
109
123
  for (const envLine of psEnv.split('\n')) {
110
- const envCsrf = envLine.match(/WINDSURF_CSRF_TOKEN=(\S+)/);
124
+ const envCsrf = envLine.match(/(?:WINDSURF|DEVIN)_CSRF_TOKEN=(\S+)/);
111
125
  if (envCsrf) {
112
126
  const envPid = envLine.match(/^\s*(\d+)/);
113
127
  if (envPid) envCsrfByPid[envPid[1]] = envCsrf[1];
@@ -162,10 +176,11 @@ function findLanguageServers() {
162
176
  function getLsForVariant(variant) {
163
177
  const servers = findLanguageServers();
164
178
  let matches;
179
+ const matchVals = variant.matchVals || [variant.matchVal];
165
180
  if (variant.matchKey === 'appDataDir') {
166
- matches = servers.filter(s => s.appDataDir?.includes(variant.matchVal));
181
+ matches = servers.filter(s => matchVals.some(matchVal => s.appDataDir?.includes(matchVal)));
167
182
  } else {
168
- matches = servers.filter(s => s.ide === variant.matchVal);
183
+ matches = servers.filter(s => matchVals.includes(s.ide));
169
184
  }
170
185
  return matches.length > 0 ? matches[0] : null;
171
186
  }
@@ -197,8 +212,9 @@ function callRpc(port, csrf, method, body, extCsrf = null) {
197
212
  // Adapter interface
198
213
  // ============================================================
199
214
 
200
- const name = 'windsurf';
201
- const sources = ['windsurf', 'windsurf-next'];
215
+ const name = 'devin';
216
+ const sources = ['devin', 'devin-next'];
217
+ const legacySources = ['windsurf', 'windsurf-next'];
202
218
 
203
219
  function getChats() {
204
220
  const chats = [];
@@ -449,29 +465,39 @@ function getMessages(chat) {
449
465
  // Usage / quota data from language server RPC
450
466
  // ============================================================
451
467
 
452
- function getWindsurfApiKey(appName) {
453
- if (!appName) return null;
468
+ function getDevinApiKey(appNames) {
469
+ if (!appNames) return null;
470
+ const names = Array.isArray(appNames) ? appNames : [appNames];
471
+ const keys = ['windsurfAuthStatus', 'devinAuthStatus'];
454
472
  try {
455
473
  const HOME = os.homedir();
456
- let dbPath;
457
- switch (process.platform) {
458
- case 'darwin':
459
- dbPath = path.join(HOME, 'Library', 'Application Support', appName, 'User', 'globalStorage', 'state.vscdb');
460
- break;
461
- case 'win32':
462
- dbPath = path.join(HOME, 'AppData', 'Roaming', appName, 'User', 'globalStorage', 'state.vscdb');
463
- break;
464
- default:
465
- dbPath = path.join(HOME, '.config', appName, 'User', 'globalStorage', 'state.vscdb');
474
+ for (const appName of names) {
475
+ let dbPath;
476
+ switch (process.platform) {
477
+ case 'darwin':
478
+ dbPath = path.join(HOME, 'Library', 'Application Support', appName, 'User', 'globalStorage', 'state.vscdb');
479
+ break;
480
+ case 'win32':
481
+ dbPath = path.join(HOME, 'AppData', 'Roaming', appName, 'User', 'globalStorage', 'state.vscdb');
482
+ break;
483
+ default:
484
+ dbPath = path.join(HOME, '.config', appName, 'User', 'globalStorage', 'state.vscdb');
485
+ }
486
+ if (!fs.existsSync(dbPath)) continue;
487
+ const Database = require('better-sqlite3');
488
+ const db = new Database(dbPath, { readonly: true });
489
+ try {
490
+ for (const key of keys) {
491
+ const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
492
+ if (!row) continue;
493
+ const parsed = JSON.parse(row.value);
494
+ if (parsed.apiKey) return parsed.apiKey;
495
+ }
496
+ } finally {
497
+ db.close();
498
+ }
466
499
  }
467
- if (!fs.existsSync(dbPath)) return null;
468
- const Database = require('better-sqlite3');
469
- const db = new Database(dbPath, { readonly: true });
470
- const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'windsurfAuthStatus'").get();
471
- db.close();
472
- if (!row) return null;
473
- const parsed = JSON.parse(row.value);
474
- return parsed.apiKey || null;
500
+ return null;
475
501
  } catch { return null; }
476
502
  }
477
503
 
@@ -485,7 +511,7 @@ function getUsage() {
485
511
  const ls = getLsForVariant(variant);
486
512
  if (!ls) continue;
487
513
 
488
- const apiKey = getWindsurfApiKey(variant.appName);
514
+ const apiKey = getDevinApiKey(variant.appNames);
489
515
  if (!apiKey) continue;
490
516
  const body = {
491
517
  metadata: {
@@ -579,13 +605,13 @@ function getUsage() {
579
605
 
580
606
  function resetCache() { _lsCache = null; }
581
607
 
582
- const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next' };
608
+ const labels = { 'devin': 'Devin', 'devin-next': 'Devin Next' };
583
609
 
584
610
  function getArtifacts(folder) {
585
611
  const { scanArtifacts } = require('./base');
586
612
  return scanArtifacts(folder, {
587
- editor: 'windsurf',
588
- label: 'Windsurf',
613
+ editor: 'devin',
614
+ label: 'Devin',
589
615
  files: ['.windsurfrules'],
590
616
  dirs: ['.windsurf/workflows', '.windsurf/rules', '.windsurf/plans', '.windsurf/skills'],
591
617
  });
@@ -595,8 +621,9 @@ function getMCPServers() {
595
621
  const { parseMcpConfigFile } = require('./base');
596
622
  const results = [];
597
623
  const configs = [
598
- { file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'), editor: 'windsurf', label: 'Windsurf' },
599
- { file: path.join(os.homedir(), '.codeium', 'windsurf-next', 'mcp_config.json'), editor: 'windsurf-next', label: 'Windsurf Next' },
624
+ { file: path.join(os.homedir(), '.windsurf', 'mcp_config.json'), editor: 'devin', label: 'Devin' },
625
+ { file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'), editor: 'devin', label: 'Devin' },
626
+ { file: path.join(os.homedir(), '.codeium', 'windsurf-next', 'mcp_config.json'), editor: 'devin-next', label: 'Devin Next' },
600
627
  ];
601
628
  for (const c of configs) {
602
629
  results.push(...parseMcpConfigFile(c.file, { editor: c.editor, label: c.label, scope: 'global' }));
@@ -604,4 +631,4 @@ function getMCPServers() {
604
631
  return results;
605
632
  }
606
633
 
607
- module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage, getArtifacts, getMCPServers };
634
+ module.exports = { name, sources, legacySources, labels, getChats, getMessages, resetCache, getUsage, getArtifacts, getMCPServers };
package/index.js CHANGED
@@ -247,11 +247,21 @@ if (noCache) {
247
247
  }
248
248
  }
249
249
 
250
- // ── Warn about installed-but-not-running Windsurf variants (macOS only) ─
250
+ // ── Warn about installed-but-not-running Devin variants (macOS only) ─
251
251
  if (process.platform === 'darwin') {
252
- const WINDSURF_VARIANTS = [
253
- { name: 'Windsurf', app: '/Applications/Windsurf.app', dataDir: path.join(HOME, '.codeium', 'windsurf'), ide: 'windsurf' },
254
- { name: 'Windsurf Next', app: '/Applications/Windsurf Next.app', dataDir: path.join(HOME, '.codeium', 'windsurf-next'), ide: 'windsurf-next' },
252
+ const DEVIN_DESKTOP_VARIANTS = [
253
+ {
254
+ name: 'Devin',
255
+ apps: ['/Applications/Devin Desktop.app', '/Applications/Devin.app', '/Applications/Windsurf.app'],
256
+ dataDirs: [path.join(HOME, '.windsurf'), path.join(HOME, '.codeium', 'windsurf')],
257
+ ides: ['devin-desktop', 'devin', 'windsurf'],
258
+ },
259
+ {
260
+ name: 'Devin Next',
261
+ apps: ['/Applications/Devin Desktop Next.app', '/Applications/Devin Next.app', '/Applications/Windsurf Next.app'],
262
+ dataDirs: [path.join(HOME, '.codeium', 'windsurf-next')],
263
+ ides: ['devin-desktop-next', 'devin-next', 'windsurf-next'],
264
+ },
255
265
  { name: 'Antigravity', app: '/Applications/Antigravity.app', dataDir: path.join(HOME, '.codeium', 'antigravity'), ide: 'antigravity' },
256
266
  ];
257
267
 
@@ -269,9 +279,12 @@ const WINDSURF_VARIANTS = [
269
279
  }
270
280
  } catch {}
271
281
 
272
- const installedNotRunning = WINDSURF_VARIANTS.filter(v => {
273
- const installed = fs.existsSync(v.app) || fs.existsSync(v.dataDir);
274
- const running = runningIdes.some(r => r === v.ide || r.includes(v.ide));
282
+ const installedNotRunning = DEVIN_DESKTOP_VARIANTS.filter(v => {
283
+ const apps = v.apps || [v.app];
284
+ const dataDirs = v.dataDirs || [v.dataDir];
285
+ const ides = v.ides || [v.ide];
286
+ const installed = apps.some(app => fs.existsSync(app)) || dataDirs.some(dataDir => fs.existsSync(dataDir));
287
+ const running = runningIdes.some(r => ides.some(ide => r === ide || r.includes(ide)));
275
288
  return installed && !running;
276
289
  });
277
290
 
@@ -304,11 +317,18 @@ allChats.sort((a, b) => (b.lastUpdatedAt || b.createdAt || 0) - (a.lastUpdatedAt
304
317
  const bySource = {};
305
318
  for (const chat of allChats) bySource[chat.source] = (bySource[chat.source] || 0) + 1;
306
319
 
307
- const displayList = Object.entries(editorLabels)
308
- .map(([src, label]) => [src, label, bySource[src] || 0])
309
- .sort((a, b) => b[2] - a[2]);
320
+ const displayByLabel = new Map();
321
+ for (const [src, label] of Object.entries(editorLabels)) {
322
+ const existing = displayByLabel.get(label) || { label, count: 0 };
323
+ existing.count += bySource[src] || 0;
324
+ displayByLabel.set(label, existing);
325
+ }
326
+
327
+ const displayList = Array.from(displayByLabel.values())
328
+ .map(({ label, count }) => [label, count])
329
+ .sort((a, b) => b[1] - a[1]);
310
330
 
311
- for (const [src, label, count] of displayList) {
331
+ for (const [label, count] of displayList) {
312
332
  if (count > 0) {
313
333
  console.log(` ${chalk.green('✓')} ${chalk.bold(label.padEnd(18))} ${chalk.dim(`${count} session${count === 1 ? '' : 's'}`)}`);
314
334
  } else {
@@ -344,7 +364,7 @@ const BOT_STYLES = [
344
364
  console.log(chalk.dim(' • Copilot – ~/.config/github-copilot/apps.json'));
345
365
  console.log(chalk.dim(' • VS Code – ~/.config/github-copilot/apps.json'));
346
366
  console.log(chalk.dim(' • Codex – local auth.json (JWT decode only)'));
347
- console.log(chalk.dim(' • Windsurf – local SQLite (state.vscdb)'));
367
+ console.log(chalk.dim(' • Devin – local SQLite (state.vscdb)'));
348
368
  console.log('');
349
369
  console.log(chalk.dim(' These tokens are used to query each editor\'s own API for'));
350
370
  console.log(chalk.dim(' your plan name and usage limits.'));