From 992040fa8723c99dca001ec46f6de2ce89bcdcbe Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Mon, 19 Jan 2026 23:13:52 -0600 Subject: [PATCH] feat(01-02): create intel-index.js PostToolUse hook - Indexes file exports/imports when Claude writes or edits JS/TS files - Extracts ES6 imports, CommonJS require, side-effect imports - Extracts named exports, default exports, declaration exports, TypeScript types - Updates .planning/intel/index.json incrementally - Silent failure on errors (always exits 0) --- hooks/intel-index.js | 189 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100755 hooks/intel-index.js diff --git a/hooks/intel-index.js b/hooks/intel-index.js new file mode 100755 index 000000000..20a60ee20 --- /dev/null +++ b/hooks/intel-index.js @@ -0,0 +1,189 @@ +#!/usr/bin/env node +// Codebase Intelligence - PostToolUse Indexing Hook +// Indexes file exports/imports when Claude writes or edits JS/TS files + +const fs = require('fs'); +const path = require('path'); + +// JS/TS file extensions to index +const INDEXABLE_EXTENSIONS = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']; + +/** + * Extract import sources from file content + * Returns array of import source paths (e.g., 'react', './utils', '@org/pkg') + */ +function extractImports(content) { + const imports = new Set(); + + // ES6 imports: import { x } from 'y', import x from 'y', import * as x from 'y' + const es6Named = /import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g; + let match; + while ((match = es6Named.exec(content)) !== null) { + imports.add(match[1]); + } + + // ES6 side-effect imports: import 'y' + const es6SideEffect = /import\s+['"]([^'"]+)['"]/g; + while ((match = es6SideEffect.exec(content)) !== null) { + // Avoid matching 'from' part of previous pattern + if (!content.slice(Math.max(0, match.index - 10), match.index).includes('from')) { + imports.add(match[1]); + } + } + + // CommonJS: require('y') + const cjs = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g; + while ((match = cjs.exec(content)) !== null) { + imports.add(match[1]); + } + + return Array.from(imports); +} + +/** + * Extract exported symbol names from file content + * Returns array of export names (e.g., 'functionA', 'ClassB', 'default') + */ +function extractExports(content) { + const exports = new Set(); + + // Named exports: export { x, y, z } + const namedExport = /export\s*\{([^}]+)\}/g; + let match; + while ((match = namedExport.exec(content)) !== null) { + const names = match[1].split(',').map(n => { + // Handle "x as y" syntax - export the alias + const parts = n.trim().split(/\s+as\s+/); + return parts[parts.length - 1].trim(); + }).filter(n => n); + names.forEach(n => exports.add(n)); + } + + // Declaration exports: export const|let|var|function|async function|class + const declExport = /export\s+(?:const|let|var|function\*?|async\s+function|class)\s+(\w+)/g; + while ((match = declExport.exec(content)) !== null) { + exports.add(match[1]); + } + + // Default export: export default (with optional identifier) + const defaultExport = /export\s+default\s+(?:function\s*\*?\s*|class\s+)?(\w+)?/g; + while ((match = defaultExport.exec(content)) !== null) { + exports.add('default'); + if (match[1]) { + exports.add(match[1]); + } + } + + // CommonJS: module.exports = { x, y } + const cjsExport = /module\.exports\s*=\s*\{([^}]+)\}/g; + while ((match = cjsExport.exec(content)) !== null) { + const names = match[1].split(',').map(n => { + // Handle "x: y" syntax - export the key + const parts = n.trim().split(/\s*:\s*/); + return parts[0].trim(); + }).filter(n => n && /^\w+$/.test(n)); + names.forEach(n => exports.add(n)); + } + + // CommonJS: module.exports = identifier + const cjsSingleExport = /module\.exports\s*=\s*(\w+)\s*[;\n]/g; + while ((match = cjsSingleExport.exec(content)) !== null) { + exports.add('default'); + exports.add(match[1]); + } + + // TypeScript: export type X, export interface X + const tsExport = /export\s+(?:type|interface)\s+(\w+)/g; + while ((match = tsExport.exec(content)) !== null) { + exports.add(match[1]); + } + + return Array.from(exports); +} + +/** + * Update the index.json file with new file entry + * Uses read-modify-write pattern with synchronous operations + */ +function updateIndex(filePath, exports, imports) { + const indexPath = path.join(process.cwd(), '.planning', 'intel', 'index.json'); + + // Read existing index or create new + let index = { version: 1, updated: null, files: {} }; + try { + const content = fs.readFileSync(indexPath, 'utf8'); + index = JSON.parse(content); + } catch (e) { + // File doesn't exist or invalid JSON - start fresh + } + + // Normalize file path to absolute + const normalizedPath = path.resolve(filePath); + + // Update single file entry (incremental) + index.files[normalizedPath] = { + exports, + imports, + indexed: Date.now() + }; + index.updated = Date.now(); + + // Ensure directory exists + fs.mkdirSync(path.dirname(indexPath), { recursive: true }); + + // Write atomically + fs.writeFileSync(indexPath, JSON.stringify(index, null, 2)); +} + +// Read JSON from stdin (standard hook pattern) +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', () => { + try { + const data = JSON.parse(input); + + // Only process Write and Edit tools + if (!['Write', 'Edit'].includes(data.tool_name)) { + process.exit(0); + } + + const filePath = data.tool_input?.file_path; + if (!filePath) { + process.exit(0); + } + + // Only index JS/TS files + const ext = path.extname(filePath).toLowerCase(); + if (!INDEXABLE_EXTENSIONS.includes(ext)) { + process.exit(0); + } + + // Get file content + // Write tool provides content in tool_input + // Edit tool only provides old_string/new_string, so read from disk + let content = data.tool_input?.content; + if (!content) { + // Edit tool - read file from disk + const resolvedPath = path.resolve(filePath); + if (fs.existsSync(resolvedPath)) { + content = fs.readFileSync(resolvedPath, 'utf8'); + } else { + // File doesn't exist (shouldn't happen after Edit, but be safe) + process.exit(0); + } + } + + // Extract imports and exports + const exports = extractExports(content); + const imports = extractImports(content); + + // Update index + updateIndex(filePath, exports, imports); + + process.exit(0); + } catch (error) { + // Silent failure - never block Claude + process.exit(0); + } +});