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/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
|
|
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 */ }
|
package/editors/windsurf.js
CHANGED
|
@@ -3,10 +3,24 @@ const path = require('path');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// Devin variants. Keep legacy Windsurf identifiers only for detection/config compatibility.
|
|
7
7
|
const VARIANTS = [
|
|
8
|
-
{
|
|
9
|
-
|
|
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
|
|
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
|
|
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(/
|
|
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(
|
|
181
|
+
matches = servers.filter(s => matchVals.some(matchVal => s.appDataDir?.includes(matchVal)));
|
|
167
182
|
} else {
|
|
168
|
-
matches = servers.filter(s => s.ide
|
|
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 = '
|
|
201
|
-
const sources = ['
|
|
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
|
|
453
|
-
if (!
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
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 =
|
|
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 = { '
|
|
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: '
|
|
588
|
-
label: '
|
|
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(), '.
|
|
599
|
-
{ file: path.join(os.homedir(), '.codeium', 'windsurf
|
|
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
|
|
250
|
+
// ── Warn about installed-but-not-running Devin variants (macOS only) ─
|
|
251
251
|
if (process.platform === 'darwin') {
|
|
252
|
-
const
|
|
253
|
-
{
|
|
254
|
-
|
|
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 =
|
|
273
|
-
const
|
|
274
|
-
const
|
|
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
|
|
308
|
-
|
|
309
|
-
.
|
|
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 [
|
|
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(' •
|
|
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.'));
|