memhouse 0.0.1 → 0.3.0
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/LICENSE +22 -0
- package/README.md +59 -3
- package/bin/memhouse.js +487 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/antigravity.js +1019 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +438 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +358 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +179 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +374 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +213 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +75 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +76 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +402 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +56 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-BsgjOSTv.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +344 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- 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
|
+
};
|
package/editors/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
|
|
18
|
+
const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, copilotJetbrains, cursorAgent, commandcode, goose, kiro, codebuff];
|
|
19
|
+
|
|
20
|
+
// Build a unified source → display-label map from all editor modules
|
|
21
|
+
const editorLabels = {};
|
|
22
|
+
for (const editor of editors) {
|
|
23
|
+
if (editor.labels) Object.assign(editorLabels, editor.labels);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all chats from all editor adapters, sorted by most recent first.
|
|
28
|
+
*/
|
|
29
|
+
function getAllChats() {
|
|
30
|
+
const chats = [];
|
|
31
|
+
for (const editor of editors) {
|
|
32
|
+
try {
|
|
33
|
+
const editorChats = editor.getChats();
|
|
34
|
+
chats.push(...editorChats);
|
|
35
|
+
} catch { /* skip broken adapters */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
chats.sort((a, b) => {
|
|
39
|
+
const ta = a.lastUpdatedAt || a.createdAt || 0;
|
|
40
|
+
const tb = b.lastUpdatedAt || b.createdAt || 0;
|
|
41
|
+
return tb - ta;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return chats;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get messages for a chat object, dispatching to the right editor adapter.
|
|
49
|
+
*/
|
|
50
|
+
function getMessages(chat) {
|
|
51
|
+
const editor = editors.find((e) => e.name === chat.source);
|
|
52
|
+
// Match variants: devin-next, antigravity, claude-code, vscode-insiders, plus legacy aliases.
|
|
53
|
+
const resolvedEditor = editor || editors.find((e) =>
|
|
54
|
+
chat.source && (
|
|
55
|
+
chat.source.startsWith(e.name) ||
|
|
56
|
+
(e.sources && e.sources.includes(chat.source)) ||
|
|
57
|
+
(e.legacySources && e.legacySources.includes(chat.source))
|
|
58
|
+
)
|
|
59
|
+
);
|
|
60
|
+
if (!resolvedEditor) return [];
|
|
61
|
+
return resolvedEditor.getMessages(chat);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resetCaches() {
|
|
65
|
+
for (const editor of editors) {
|
|
66
|
+
if (typeof editor.resetCache === 'function') editor.resetCache();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get usage / quota data from all editors that support it.
|
|
72
|
+
* Returns an array of usage objects, one per editor/variant.
|
|
73
|
+
*/
|
|
74
|
+
async function getAllUsage() {
|
|
75
|
+
const results = [];
|
|
76
|
+
for (const editor of editors) {
|
|
77
|
+
if (typeof editor.getUsage !== 'function') continue;
|
|
78
|
+
try {
|
|
79
|
+
const usage = await editor.getUsage();
|
|
80
|
+
if (!usage) continue;
|
|
81
|
+
// Devin returns an array (one per variant), Cursor returns a single object
|
|
82
|
+
if (Array.isArray(usage)) results.push(...usage);
|
|
83
|
+
else results.push(usage);
|
|
84
|
+
} catch { /* skip broken adapters */ }
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get all artifacts for a given project folder from all editors.
|
|
91
|
+
* Also scans for general/shared artifact files (plan.md, etc.).
|
|
92
|
+
*/
|
|
93
|
+
function getAllArtifacts(folder) {
|
|
94
|
+
const { scanArtifacts } = require('./base');
|
|
95
|
+
const artifacts = [];
|
|
96
|
+
|
|
97
|
+
// Collect from each editor that implements getArtifacts
|
|
98
|
+
for (const editor of editors) {
|
|
99
|
+
if (typeof editor.getArtifacts !== 'function') continue;
|
|
100
|
+
try {
|
|
101
|
+
artifacts.push(...editor.getArtifacts(folder));
|
|
102
|
+
} catch { /* skip broken adapters */ }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// General / shared artifact files (not tied to any specific editor)
|
|
106
|
+
if (folder) {
|
|
107
|
+
try {
|
|
108
|
+
artifacts.push(...scanArtifacts(folder, {
|
|
109
|
+
editor: '_general',
|
|
110
|
+
label: 'General',
|
|
111
|
+
files: ['AGENTS.md', '.mcp.json', 'plan.md', 'progress.md', 'TODO.md', 'CONVENTIONS.md', 'ARCHITECTURE.md', 'PLANNING.md'],
|
|
112
|
+
dirs: [],
|
|
113
|
+
}));
|
|
114
|
+
} catch { /* skip */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Deduplicate by path — editor-specific entries take priority over general
|
|
118
|
+
const seen = new Map();
|
|
119
|
+
for (const a of artifacts) {
|
|
120
|
+
const existing = seen.get(a.path);
|
|
121
|
+
if (!existing || (existing.editor === '_general' && a.editor !== '_general')) {
|
|
122
|
+
seen.set(a.path, a);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return Array.from(seen.values());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all MCP servers from all editors.
|
|
130
|
+
* Also scans project folders for project-level MCP configs (.mcp.json, .cursor/mcp.json, etc.)
|
|
131
|
+
*/
|
|
132
|
+
function getAllMCPServers(projectFolders = []) {
|
|
133
|
+
const { parseMcpConfigFile } = require('./base');
|
|
134
|
+
const path = require('path');
|
|
135
|
+
const fs = require('fs');
|
|
136
|
+
const servers = [];
|
|
137
|
+
|
|
138
|
+
// 1. Collect global MCP servers from each editor
|
|
139
|
+
for (const editor of editors) {
|
|
140
|
+
if (typeof editor.getMCPServers !== 'function') continue;
|
|
141
|
+
try {
|
|
142
|
+
servers.push(...editor.getMCPServers());
|
|
143
|
+
} catch { /* skip broken adapters */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Scan project folders for project-level MCP configs
|
|
147
|
+
const projectConfigs = [
|
|
148
|
+
{ file: '.mcp.json', editor: 'claude-code', label: 'Claude Code' },
|
|
149
|
+
{ file: '.cursor/mcp.json', editor: 'cursor', label: 'Cursor' },
|
|
150
|
+
{ file: '.vscode/mcp.json', editor: 'vscode', label: 'VS Code' },
|
|
151
|
+
{ file: '.gemini/settings.json', editor: 'gemini-cli', label: 'Gemini CLI' },
|
|
152
|
+
{ file: '.kiro/settings/mcp.json', editor: 'kiro', label: 'Kiro' },
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const seenProjects = new Set();
|
|
156
|
+
for (const folder of projectFolders) {
|
|
157
|
+
if (!folder || seenProjects.has(folder)) continue;
|
|
158
|
+
seenProjects.add(folder);
|
|
159
|
+
for (const pc of projectConfigs) {
|
|
160
|
+
const configPath = path.join(folder, pc.file);
|
|
161
|
+
if (!fs.existsSync(configPath)) continue;
|
|
162
|
+
const found = parseMcpConfigFile(configPath, { editor: pc.editor, label: pc.label, scope: 'project' });
|
|
163
|
+
for (const s of found) {
|
|
164
|
+
s.projectFolder = folder;
|
|
165
|
+
}
|
|
166
|
+
servers.push(...found);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 3. Deduplicate by name+editor (keep first occurrence, prefer global over project)
|
|
171
|
+
const seen = new Map();
|
|
172
|
+
for (const s of servers) {
|
|
173
|
+
const key = `${s.name}::${s.editor}::${s.scope}`;
|
|
174
|
+
if (!seen.has(key)) seen.set(key, s);
|
|
175
|
+
}
|
|
176
|
+
return Array.from(seen.values());
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage, getAllArtifacts, getAllMCPServers };
|