memhouse 0.0.1 → 0.3.1

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.
Files changed (97) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +72 -3
  3. package/bin/memhouse.js +559 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/adapter-errors.js +52 -0
  9. package/editors/antigravity.js +1048 -0
  10. package/editors/base.js +321 -0
  11. package/editors/claude.js +386 -0
  12. package/editors/codebuff.js +338 -0
  13. package/editors/codex.js +537 -0
  14. package/editors/commandcode.js +166 -0
  15. package/editors/copilot-jetbrains.js +207 -0
  16. package/editors/copilot.js +260 -0
  17. package/editors/cursor-agent.js +202 -0
  18. package/editors/cursor.js +498 -0
  19. package/editors/gemini.js +231 -0
  20. package/editors/goose.js +386 -0
  21. package/editors/gsd.js +366 -0
  22. package/editors/index.js +274 -0
  23. package/editors/kiro.js +315 -0
  24. package/editors/opencode.js +432 -0
  25. package/editors/vscode.js +413 -0
  26. package/editors/windsurf.js +634 -0
  27. package/editors/zed.js +229 -0
  28. package/index.js +424 -2
  29. package/mcp-server.js +294 -0
  30. package/mem-house/DESIGN.md +119 -0
  31. package/mem-house/delivery/AGENT-INSTALL.md +87 -0
  32. package/mem-house/delivery/PROMPT.md +42 -0
  33. package/mem-house/delivery/install.sh +181 -0
  34. package/mem-house/delivery/kernel-install.md +83 -0
  35. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  36. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  37. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  38. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  39. package/mem-house/rls.sql +17 -0
  40. package/mem-house/schema.sql +145 -0
  41. package/mem-house/server/queries.js +878 -0
  42. package/mem-house/server/server.js +228 -0
  43. package/mem-house/shipper/ship.js +461 -0
  44. package/misc/syntax-check.js +38 -0
  45. package/package.json +59 -7
  46. package/pricing.js +88 -0
  47. package/pricing.json +992 -0
  48. package/public/assets/index-CCPh7a2Z.js +75 -0
  49. package/public/assets/index-DV6ONi_F.css +2 -0
  50. package/public/index.html +16 -0
  51. package/relay-client.js +306 -0
  52. package/relay-server.js +577 -0
  53. package/server.js +860 -0
  54. package/share-image.js +367 -0
  55. package/ui/eslint.config.js +29 -0
  56. package/ui/index.html +15 -0
  57. package/ui/package-lock.json +4229 -0
  58. package/ui/package.json +38 -0
  59. package/ui/src/App.jsx +364 -0
  60. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  61. package/ui/src/components/AiAuditCard.jsx +254 -0
  62. package/ui/src/components/AnimatedLoader.jsx +14 -0
  63. package/ui/src/components/AnimatedLogo.jsx +96 -0
  64. package/ui/src/components/ChatSidebar.jsx +226 -0
  65. package/ui/src/components/DateRangePicker.jsx +104 -0
  66. package/ui/src/components/EditorDot.jsx +11 -0
  67. package/ui/src/components/EditorIcon.jsx +117 -0
  68. package/ui/src/components/KpiCard.jsx +9 -0
  69. package/ui/src/components/LiveFeed.jsx +128 -0
  70. package/ui/src/components/LoginScreen.jsx +81 -0
  71. package/ui/src/components/MessageRenderer.jsx +148 -0
  72. package/ui/src/components/PageHeader.jsx +11 -0
  73. package/ui/src/components/SectionTitle.jsx +3 -0
  74. package/ui/src/components/ShareModal.jsx +305 -0
  75. package/ui/src/components/TokenTimeline.jsx +258 -0
  76. package/ui/src/hooks/useLive.jsx +15 -0
  77. package/ui/src/index.css +83 -0
  78. package/ui/src/lib/api.js +343 -0
  79. package/ui/src/lib/constants.js +88 -0
  80. package/ui/src/lib/theme.jsx +23 -0
  81. package/ui/src/main.jsx +16 -0
  82. package/ui/src/pages/Artifacts.jsx +575 -0
  83. package/ui/src/pages/Compare.jsx +232 -0
  84. package/ui/src/pages/CostAnalysis.jsx +354 -0
  85. package/ui/src/pages/Dashboard.jsx +529 -0
  86. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  87. package/ui/src/pages/GSD.jsx +726 -0
  88. package/ui/src/pages/MCPs.jsx +744 -0
  89. package/ui/src/pages/ProjectDetail.jsx +325 -0
  90. package/ui/src/pages/Projects.jsx +249 -0
  91. package/ui/src/pages/RelayDashboard.jsx +590 -0
  92. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  93. package/ui/src/pages/Sessions.jsx +535 -0
  94. package/ui/src/pages/Settings.jsx +241 -0
  95. package/ui/src/pages/SqlViewer.jsx +370 -0
  96. package/ui/src/pages/Subscriptions.jsx +411 -0
  97. package/ui/vite.config.js +18 -0
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
+ };
@@ -0,0 +1,274 @@
1
+ const cursor = require('./cursor');
2
+ const devin = require('./windsurf');
3
+ const antigravity = require('./antigravity');
4
+ const claude = require('./claude');
5
+ const vscode = require('./vscode');
6
+ const zed = require('./zed');
7
+ const opencode = require('./opencode');
8
+ const codex = require('./codex');
9
+ const gemini = require('./gemini');
10
+ const copilot = require('./copilot');
11
+ const copilotJetbrains = require('./copilot-jetbrains');
12
+ const cursorAgent = require('./cursor-agent');
13
+ const commandcode = require('./commandcode');
14
+ const goose = require('./goose');
15
+ const kiro = require('./kiro');
16
+ const codebuff = require('./codebuff');
17
+ const adapterErrorSink = require('./adapter-errors');
18
+
19
+ const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, copilotJetbrains, cursorAgent, commandcode, goose, kiro, codebuff];
20
+
21
+ // Build a unified source → display-label map from all editor modules
22
+ const editorLabels = {};
23
+ for (const editor of editors) {
24
+ if (editor.labels) Object.assign(editorLabels, editor.labels);
25
+ }
26
+
27
+ // Per-adapter failures from the last getAllChats() pass. An adapter that throws is
28
+ // skipped so one broken editor cannot take down a scan, but the failure is recorded
29
+ // here rather than discarded: a missing better-sqlite3 native binding and an editor
30
+ // the user simply does not have both produce zero sessions, and only this tells them
31
+ // apart. See getAdapterErrors().
32
+ let adapterErrors = [];
33
+
34
+ // better-sqlite3 ships a prebuilt binary via an install script. npm >= 12 blocks
35
+ // install scripts by default, so a plain `npm i -g memhouse` leaves no binding and
36
+ // every SQLite-backed adapter reads zero sessions.
37
+ const MISSING_BINDING = /Could not locate the bindings file|Cannot find module 'better-sqlite3'/;
38
+
39
+ // Adapters that read sessions out of a SQLite store, so a dead binding costs
40
+ // sessions. Membership is narrower than `grep -l better-sqlite3 editors/*.js`:
41
+ // windsurf/devin also requires better-sqlite3, but only in getDevinApiKey() for
42
+ // usage — its getChats() is pure language-server RPC, so its sessions survive a
43
+ // broken binding and listing it here would raise a false alarm. antigravity is
44
+ // listed because its *offline* chats come from SQLite, though its live cascades,
45
+ // like devin's, come over RPC — a failure there is partial.
46
+ // Held as module references, not strings, so the reported name always matches the
47
+ // adapter's own `name`.
48
+ const SQLITE_BACKED = [antigravity, cursor, goose, opencode, zed].map((m) => m.name);
49
+
50
+ // Construct an in-memory database. Loading the module is not enough — better-sqlite3
51
+ // resolves the native binding lazily, on first open.
52
+ //
53
+ // This only answers "can SQLite work at all". It deliberately cannot tell whether a
54
+ // given editor's real store is readable: permissions, locking, corruption, and
55
+ // SQLCipher all pass this probe and fail later. Those are reported by the adapters
56
+ // themselves through adapter-errors.
57
+ function probeSqlite() {
58
+ try {
59
+ const Database = require('better-sqlite3');
60
+ const db = new Database(':memory:');
61
+ db.close();
62
+ return { available: true };
63
+ } catch (e) {
64
+ const message = ((e && e.message) || String(e)).split('\n')[0];
65
+ return { available: false, message, missingBinding: MISSING_BINDING.test(message) };
66
+ }
67
+ }
68
+
69
+ // Cache only success. A failed probe must be retried: a long-running shipper daemon
70
+ // that warned about a missing binding should notice once the user installs it,
71
+ // rather than repeating the warning for the life of the process.
72
+ let sqliteProbe = null;
73
+ function sqliteStatus() {
74
+ if (sqliteProbe === null) {
75
+ const result = probeSqlite();
76
+ if (result.available) sqliteProbe = result;
77
+ return result;
78
+ }
79
+ return sqliteProbe;
80
+ }
81
+
82
+ /**
83
+ * Get all chats from all editor adapters, sorted by most recent first.
84
+ */
85
+ function getAllChats() {
86
+ const chats = [];
87
+ adapterErrors = [];
88
+ adapterErrorSink.reset();
89
+ for (const editor of editors) {
90
+ try {
91
+ const editorChats = editor.getChats();
92
+ chats.push(...editorChats);
93
+ } catch (e) {
94
+ const message = (e && e.message) || String(e);
95
+ adapterErrors.push({
96
+ source: editor.name,
97
+ message: message.split('\n')[0],
98
+ missingBinding: MISSING_BINDING.test(message),
99
+ });
100
+ }
101
+ }
102
+
103
+ chats.sort((a, b) => {
104
+ const ta = a.lastUpdatedAt || a.createdAt || 0;
105
+ const tb = b.lastUpdatedAt || b.createdAt || 0;
106
+ return tb - ta;
107
+ });
108
+
109
+ return chats;
110
+ }
111
+
112
+ /**
113
+ * Adapters that could not report their sessions: [{ source, message, missingBinding }].
114
+ * Empty when every adapter ran clean.
115
+ *
116
+ * Two sources, because there are two ways an adapter goes quiet. One throws out of
117
+ * getChats() and is recorded during the pass. The other is a dead SQLite binding,
118
+ * which throws nowhere at all — the adapters catch it internally — so it is probed
119
+ * for directly and reported against every SQLite-backed adapter at once.
120
+ */
121
+ function getAdapterErrors() {
122
+ const errors = adapterErrors.slice();
123
+ for (const e of adapterErrorSink.recorded()) {
124
+ errors.push({ source: e.source, message: e.message, detail: e.detail, missingBinding: MISSING_BINDING.test(e.message) });
125
+ }
126
+ const sqlite = sqliteStatus();
127
+ if (!sqlite.available) {
128
+ const already = new Set(errors.map((e) => e.source));
129
+ for (const source of SQLITE_BACKED) {
130
+ if (!already.has(source)) {
131
+ errors.push({ source, message: sqlite.message, missingBinding: sqlite.missingBinding });
132
+ }
133
+ }
134
+ }
135
+ // One line per adapter, not one per failed query — a broken store is usually hit
136
+ // many times in a single scan.
137
+ const bySource = new Map();
138
+ for (const e of errors) if (!bySource.has(e.source)) bySource.set(e.source, e);
139
+ return Array.from(bySource.values());
140
+ }
141
+
142
+ /**
143
+ * Get messages for a chat object, dispatching to the right editor adapter.
144
+ */
145
+ function getMessages(chat) {
146
+ const editor = editors.find((e) => e.name === chat.source);
147
+ // Match variants: devin-next, antigravity, claude-code, vscode-insiders, plus legacy aliases.
148
+ const resolvedEditor = editor || editors.find((e) =>
149
+ chat.source && (
150
+ chat.source.startsWith(e.name) ||
151
+ (e.sources && e.sources.includes(chat.source)) ||
152
+ (e.legacySources && e.legacySources.includes(chat.source))
153
+ )
154
+ );
155
+ if (!resolvedEditor) return [];
156
+ return resolvedEditor.getMessages(chat);
157
+ }
158
+
159
+ function resetCaches() {
160
+ for (const editor of editors) {
161
+ if (typeof editor.resetCache === 'function') editor.resetCache();
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Get usage / quota data from all editors that support it.
167
+ * Returns an array of usage objects, one per editor/variant.
168
+ */
169
+ async function getAllUsage() {
170
+ const results = [];
171
+ for (const editor of editors) {
172
+ if (typeof editor.getUsage !== 'function') continue;
173
+ try {
174
+ const usage = await editor.getUsage();
175
+ if (!usage) continue;
176
+ // Devin returns an array (one per variant), Cursor returns a single object
177
+ if (Array.isArray(usage)) results.push(...usage);
178
+ else results.push(usage);
179
+ } catch { /* skip broken adapters */ }
180
+ }
181
+ return results;
182
+ }
183
+
184
+ /**
185
+ * Get all artifacts for a given project folder from all editors.
186
+ * Also scans for general/shared artifact files (plan.md, etc.).
187
+ */
188
+ function getAllArtifacts(folder) {
189
+ const { scanArtifacts } = require('./base');
190
+ const artifacts = [];
191
+
192
+ // Collect from each editor that implements getArtifacts
193
+ for (const editor of editors) {
194
+ if (typeof editor.getArtifacts !== 'function') continue;
195
+ try {
196
+ artifacts.push(...editor.getArtifacts(folder));
197
+ } catch { /* skip broken adapters */ }
198
+ }
199
+
200
+ // General / shared artifact files (not tied to any specific editor)
201
+ if (folder) {
202
+ try {
203
+ artifacts.push(...scanArtifacts(folder, {
204
+ editor: '_general',
205
+ label: 'General',
206
+ files: ['AGENTS.md', '.mcp.json', 'plan.md', 'progress.md', 'TODO.md', 'CONVENTIONS.md', 'ARCHITECTURE.md', 'PLANNING.md'],
207
+ dirs: [],
208
+ }));
209
+ } catch { /* skip */ }
210
+ }
211
+
212
+ // Deduplicate by path — editor-specific entries take priority over general
213
+ const seen = new Map();
214
+ for (const a of artifacts) {
215
+ const existing = seen.get(a.path);
216
+ if (!existing || (existing.editor === '_general' && a.editor !== '_general')) {
217
+ seen.set(a.path, a);
218
+ }
219
+ }
220
+ return Array.from(seen.values());
221
+ }
222
+
223
+ /**
224
+ * Get all MCP servers from all editors.
225
+ * Also scans project folders for project-level MCP configs (.mcp.json, .cursor/mcp.json, etc.)
226
+ */
227
+ function getAllMCPServers(projectFolders = []) {
228
+ const { parseMcpConfigFile } = require('./base');
229
+ const path = require('path');
230
+ const fs = require('fs');
231
+ const servers = [];
232
+
233
+ // 1. Collect global MCP servers from each editor
234
+ for (const editor of editors) {
235
+ if (typeof editor.getMCPServers !== 'function') continue;
236
+ try {
237
+ servers.push(...editor.getMCPServers());
238
+ } catch { /* skip broken adapters */ }
239
+ }
240
+
241
+ // 2. Scan project folders for project-level MCP configs
242
+ const projectConfigs = [
243
+ { file: '.mcp.json', editor: 'claude-code', label: 'Claude Code' },
244
+ { file: '.cursor/mcp.json', editor: 'cursor', label: 'Cursor' },
245
+ { file: '.vscode/mcp.json', editor: 'vscode', label: 'VS Code' },
246
+ { file: '.gemini/settings.json', editor: 'gemini-cli', label: 'Gemini CLI' },
247
+ { file: '.kiro/settings/mcp.json', editor: 'kiro', label: 'Kiro' },
248
+ ];
249
+
250
+ const seenProjects = new Set();
251
+ for (const folder of projectFolders) {
252
+ if (!folder || seenProjects.has(folder)) continue;
253
+ seenProjects.add(folder);
254
+ for (const pc of projectConfigs) {
255
+ const configPath = path.join(folder, pc.file);
256
+ if (!fs.existsSync(configPath)) continue;
257
+ const found = parseMcpConfigFile(configPath, { editor: pc.editor, label: pc.label, scope: 'project' });
258
+ for (const s of found) {
259
+ s.projectFolder = folder;
260
+ }
261
+ servers.push(...found);
262
+ }
263
+ }
264
+
265
+ // 3. Deduplicate by name+editor (keep first occurrence, prefer global over project)
266
+ const seen = new Map();
267
+ for (const s of servers) {
268
+ const key = `${s.name}::${s.editor}::${s.scope}`;
269
+ if (!seen.has(key)) seen.set(key, s);
270
+ }
271
+ return Array.from(seen.values());
272
+ }
273
+
274
+ module.exports = { getAllChats, getAdapterErrors, getMessages, editors, editorLabels, resetCaches, getAllUsage, getAllArtifacts, getAllMCPServers };