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>
This commit is contained in:
137
gsd-memory/dist/tools/stack.js
vendored
Normal file
137
gsd-memory/dist/tools/stack.js
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
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
|
||||
Reference in New Issue
Block a user