diff --git a/.gitignore b/.gitignore index 541fce485..a6c1ae192 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,11 @@ package-lock.json .DS_Store TO-DOS.md CLAUDE.md -.planning /research.claude/ commands.html # Local test installs .claude/ + +# Build artifacts (committed to npm, not git) +hooks/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 34b77428b..1341d14d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - Orchestrators now inline file contents in Task prompts (fixes context issues with @ references) - Tech debt from milestone audit addressed +- All hooks now use `gsd-` prefix for consistency (statusline.js → gsd-statusline.js) ## [1.8.0] - 2026-01-19 diff --git a/bin/install.js b/bin/install.js index a047a81b1..189424514 100755 --- a/bin/install.js +++ b/bin/install.js @@ -162,6 +162,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix) { function cleanupOrphanedFiles(claudeDir) { const orphanedFiles = [ 'hooks/gsd-notify.sh', // Removed in v1.6.x + 'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0 ]; for (const relPath of orphanedFiles) { @@ -179,6 +180,7 @@ function cleanupOrphanedFiles(claudeDir) { function cleanupOrphanedHooks(settings) { const orphanedHookPatterns = [ 'gsd-notify.sh', // Removed in v1.6.x + 'hooks/statusline.js', // Renamed to gsd-statusline.js in v1.9.0 ]; let cleaned = false; @@ -353,19 +355,22 @@ function install(isGlobal) { failures.push('VERSION'); } - // Copy hooks - const hooksSrc = path.join(src, 'hooks'); + // Copy hooks from dist/ (bundled with dependencies) + const hooksSrc = path.join(src, 'hooks', 'dist'); if (fs.existsSync(hooksSrc)) { const hooksDest = path.join(claudeDir, 'hooks'); fs.mkdirSync(hooksDest, { recursive: true }); const hookEntries = fs.readdirSync(hooksSrc); for (const entry of hookEntries) { const srcFile = path.join(hooksSrc, entry); - const destFile = path.join(hooksDest, entry); - fs.copyFileSync(srcFile, destFile); + // Only copy files, not directories + if (fs.statSync(srcFile).isFile()) { + const destFile = path.join(hooksDest, entry); + fs.copyFileSync(srcFile, destFile); + } } if (verifyInstalled(hooksDest, 'hooks')) { - console.log(` ${green}✓${reset} Installed hooks`); + console.log(` ${green}✓${reset} Installed hooks (bundled)`); } else { failures.push('hooks'); } @@ -382,8 +387,8 @@ function install(isGlobal) { const settingsPath = path.join(claudeDir, 'settings.json'); const settings = cleanupOrphanedHooks(readSettings(settingsPath)); const statuslineCommand = isGlobal - ? 'node "$HOME/.claude/hooks/statusline.js"' - : 'node .claude/hooks/statusline.js'; + ? 'node "$HOME/.claude/hooks/gsd-statusline.js"' + : 'node .claude/hooks/gsd-statusline.js'; const updateCheckCommand = isGlobal ? 'node "$HOME/.claude/hooks/gsd-check-update.js"' : 'node .claude/hooks/gsd-check-update.js'; diff --git a/hooks/gsd-intel-index.js b/hooks/gsd-intel-index.js index b9b054afb..8fc644193 100755 --- a/hooks/gsd-intel-index.js +++ b/hooks/gsd-intel-index.js @@ -25,7 +25,9 @@ const fs = require('fs'); const path = require('path'); -const initSqlJs = require('sql.js'); +const { execSync } = require('child_process'); +// Use ASM.js version for bundling (no WASM file dependency) +const initSqlJs = require('sql.js/dist/sql-asm.js'); // Graph database schema (simple-graph pattern) const GRAPH_SCHEMA = ` @@ -609,6 +611,121 @@ function extractPurpose(content) { return null; } +/** + * Generate entity slug from file path + * e.g., 'src/lib/db.ts' -> 'src-lib-db' + */ +function generateSlug(filePath) { + return filePath + .replace(/^\/+/, '') // Remove leading slashes + .replace(/\.[^.]+$/, '') // Remove extension + .replace(/[\/\\]/g, '-') // Replace path separators with hyphens + .replace(/[^a-zA-Z0-9-]/g, '-') // Replace non-alphanumeric with hyphens + .replace(/-+/g, '-') // Collapse multiple hyphens + .replace(/^-|-$/g, '') // Remove leading/trailing hyphens + .toLowerCase(); +} + +/** + * Check if signature (exports/imports) changed + * Returns true if entity should be regenerated + */ +function signatureChanged(prevEntry, exports, imports) { + if (!prevEntry) return true; // New file + + const prevExports = JSON.stringify(prevEntry.exports || []); + const prevImports = JSON.stringify(prevEntry.imports || []); + const newExports = JSON.stringify(exports); + const newImports = JSON.stringify(imports); + + return prevExports !== newExports || prevImports !== newImports; +} + +/** + * Generate semantic entity file using Claude + * Spawns `claude -p` to analyze file and generate entity markdown + * + * @param {string} filePath - Path to the code file + * @param {string} content - File content + * @param {Array} exports - Extracted exports + * @param {Array} imports - Extracted imports + */ +async function generateEntity(filePath, content, exports, imports) { + const intelDir = path.join(process.cwd(), '.planning', 'intel'); + const entitiesDir = path.join(intelDir, 'entities'); + + // Ensure entities directory exists + if (!fs.existsSync(entitiesDir)) { + fs.mkdirSync(entitiesDir, { recursive: true }); + } + + const slug = generateSlug(filePath); + const entityPath = path.join(entitiesDir, `${slug}.md`); + const today = new Date().toISOString().split('T')[0]; + + // Build prompt for Claude + const prompt = `Analyze this code file and generate ONLY the entity markdown (no explanation, no code fences). + +Path: ${filePath} +Exports: ${exports.join(', ') || 'none'} +Imports: ${imports.join(', ') || 'none'} + +Content: +${content.slice(0, 3000)}${content.length > 3000 ? '\n... (truncated)' : ''} + +Output this EXACT format (fill in the brackets): + +--- +path: ${filePath} +type: [module|component|util|config|api|hook|service|model|test] +updated: ${today} +status: active +--- + +# ${path.basename(filePath)} + +## Purpose + +[1-2 sentences: What does this file do? Why does it exist?] + +## Exports + +[List each export with brief description, or "None" if no exports] + +## Dependencies + +[List internal deps as [[slug]] wiki-links, external as plain text, or "None"] + +## Used By + +TBD + +## Notes + +[Optional: any important patterns or gotchas, or remove this section]`; + + try { + // Spawn claude -p with timeout + const cmd = `claude -p "${prompt.replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`; + const result = execSync(cmd, { + encoding: 'utf8', + timeout: 30000, // 30 second timeout + maxBuffer: 1024 * 1024 // 1MB buffer + }); + + // Write entity file + fs.writeFileSync(entityPath, result.trim()); + + // Sync to graph + await syncEntityToGraph(entityPath); + + return entityPath; + } catch (e) { + // Silent failure - don't block on entity generation errors + return null; + } +} + /** * Generate semantic summary from graph database * Target: < 500 tokens for context injection @@ -1057,9 +1174,41 @@ process.stdin.on('end', () => { const exports = extractExports(content); const imports = extractImports(content); - // Update index + // Check if signature changed (triggers entity regeneration) + const intelDir = path.join(process.cwd(), '.planning', 'intel'); + const indexPath = path.join(intelDir, 'index.json'); + let prevEntry = null; + + if (fs.existsSync(indexPath)) { + try { + const indexContent = fs.readFileSync(indexPath, 'utf8'); + const index = JSON.parse(indexContent); + const normalizedPath = path.resolve(filePath); + prevEntry = index.files?.[normalizedPath]; + } catch (e) { + // Ignore read errors + } + } + + // Update index first (always) updateIndex(filePath, exports, imports); + // Generate entity if signature changed + if (fs.existsSync(intelDir) && signatureChanged(prevEntry, exports, imports)) { + generateEntity(filePath, content, exports, imports) + .then(async (entityPath) => { + if (entityPath) { + // Regenerate summary after new entity + await regenerateEntitySummary(); + } + process.exit(0); + }) + .catch(() => { + process.exit(0); + }); + return; // Wait for async + } + process.exit(0); } catch (error) { // Silent failure - never block Claude diff --git a/hooks/statusline.js b/hooks/gsd-statusline.js similarity index 100% rename from hooks/statusline.js rename to hooks/gsd-statusline.js diff --git a/package.json b/package.json index b1d341e06..0b8562537 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "commands", "get-shit-done", "agents", - "hooks" + "hooks/dist", + "scripts" ], "keywords": [ "claude", @@ -33,8 +34,13 @@ "engines": { "node": ">=16.7.0" }, - "dependencies": { - "sql.js": "^1.12.0", - "@anthropic-ai/sdk": "^0.52.0" + "dependencies": {}, + "devDependencies": { + "esbuild": "^0.24.0", + "sql.js": "^1.12.0" + }, + "scripts": { + "build:hooks": "node scripts/build-hooks.js", + "prepublishOnly": "npm run build:hooks" } }