Files
msd-core/gsd-memory/dist/tools/stack.js
Lex Christopherson af7a057294 feat: add GSD Memory cross-project knowledge system
Implements MCP server for semantic search across GSD projects:

MCP Tools:
- gsd_memory_search: Search across all registered projects
- gsd_memory_decisions: Find decisions from SUMMARY/PROJECT.md
- gsd_memory_patterns: Find patterns from SUMMARY.md
- gsd_memory_pitfalls: Find pitfalls from RESEARCH.md
- gsd_memory_stack: Find tech stack entries
- gsd_memory_register: Register project with memory
- gsd_memory_index: Trigger indexing/update
- gsd_memory_status: Show system status

Architecture:
- QMD wrapper with grep fallback when QMD unavailable
- YAML frontmatter extractors for GSD document types
- Project registry at ~/.gsd/projects.json
- Auto-install via bin/install.js for Claude Code

Workflow integrations:
- new-project: Registers project with memory
- plan-phase: Indexes after planning
- execute-phase: Indexes after execution
- complete-milestone: Indexes milestone completion
- Researcher agents query memory before Context7

Tests: 117 passing (66 unit + 51 integration)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 13:13:22 -06:00

137 lines
4.6 KiB
JavaScript

import { getAllProjects } from '../registry.js';
import { extractSummary } from '../extractors/summary.js';
import { extractResearch } from '../extractors/research.js';
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
/**
* Search for tech stack entries across all registered GSD projects
*/
export async function findStack(options) {
const projects = options.project
? getAllProjects().filter(p => p.name === options.project)
: getAllProjects();
const allStack = [];
for (const project of projects) {
const planningPath = join(project.path, '.planning');
if (!existsSync(planningPath))
continue;
// Extract from SUMMARY.md files
const summaryStack = extractFromSummaries(planningPath, project.name);
allStack.push(...summaryStack);
// Extract from RESEARCH.md files
const researchStack = extractFromResearch(planningPath, project.name);
allStack.push(...researchStack);
}
// Filter by query if provided
let filtered = allStack;
if (options.query) {
const query = options.query.toLowerCase();
filtered = allStack.filter(s => s.library.toLowerCase().includes(query) ||
s.purpose?.toLowerCase().includes(query));
}
// Deduplicate by library name within each project
const seen = new Set();
const deduplicated = filtered.filter(s => {
const key = `${s.project}:${s.library}`;
if (seen.has(key))
return false;
seen.add(key);
return true;
});
// Apply limit
const limit = options.limit || 100;
return deduplicated.slice(0, limit);
}
/**
* Extract tech stack from all SUMMARY.md files
*/
function extractFromSummaries(planningPath, projectName) {
const stack = [];
const phasesPath = join(planningPath, 'phases');
if (!existsSync(phasesPath))
return stack;
const summaryFiles = findFiles(phasesPath, 'SUMMARY');
for (const file of summaryFiles) {
try {
const content = readFileSync(file, 'utf-8');
const summaryData = extractSummary(content);
// Extract phase from path
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
const phase = phaseMatch ? phaseMatch[1] : undefined;
for (const entry of summaryData.techStack) {
stack.push({
library: entry.library,
version: entry.version,
purpose: entry.purpose,
project: projectName,
phase,
source: file,
sourceType: 'summary'
});
}
}
catch {
// Skip files that can't be parsed
}
}
return stack;
}
/**
* Extract tech stack from all RESEARCH.md files
*/
function extractFromResearch(planningPath, projectName) {
const stack = [];
const researchFiles = findFiles(planningPath, 'RESEARCH');
for (const file of researchFiles) {
try {
const content = readFileSync(file, 'utf-8');
const researchData = extractResearch(content);
// Extract phase from path
const phaseMatch = file.match(/phases\/(\d+-[^/]+)/);
const phase = phaseMatch ? phaseMatch[1] : undefined;
for (const entry of researchData.standardStack) {
stack.push({
library: entry.library,
version: entry.version,
purpose: entry.purpose,
project: projectName,
phase,
source: file,
sourceType: 'research'
});
}
}
catch {
// Skip files that can't be parsed
}
}
return stack;
}
/**
* Recursively find files matching a pattern
*/
function findFiles(dir, pattern, files = []) {
try {
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
try {
const stat = statSync(fullPath);
if (stat.isDirectory() && !entry.startsWith('.')) {
findFiles(fullPath, pattern, files);
}
else if (entry.toUpperCase().includes(pattern) && entry.endsWith('.md')) {
files.push(fullPath);
}
}
catch {
// Skip entries that can't be accessed
}
}
}
catch {
// Return empty if directory can't be read
}
return files;
}
//# sourceMappingURL=stack.js.map