Initial commit
This commit is contained in:
368
hooks/context_injector.js
Executable file
368
hooks/context_injector.js
Executable file
@@ -0,0 +1,368 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Context Injection Hook for Grounded Research
|
||||
*
|
||||
* Injects current context into research prompts:
|
||||
* - Current date (for accurate web searches)
|
||||
* - Tech stack (from package.json, etc.)
|
||||
* - Existing specifications
|
||||
* - Recent plans
|
||||
*
|
||||
* This hook runs BEFORE intent detection to ensure research is grounded.
|
||||
*
|
||||
* Requirements: Node.js (comes with Claude Code - no additional install needed!)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Get current context from environment and codebase
|
||||
*/
|
||||
function getCurrentContext() {
|
||||
// Get current date
|
||||
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
// Get working directory
|
||||
const workingDir = process.cwd();
|
||||
const projectName = path.basename(workingDir);
|
||||
|
||||
// Detect tech stack
|
||||
const techStack = detectTechStack(workingDir);
|
||||
|
||||
// Find specifications
|
||||
const specs = findSpecifications(workingDir);
|
||||
|
||||
// Find recent plans
|
||||
const recentPlans = findRecentPlans(workingDir);
|
||||
|
||||
return {
|
||||
date: currentDate,
|
||||
project: projectName,
|
||||
workingDir,
|
||||
techStack,
|
||||
specifications: specs,
|
||||
recentPlans
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect tech stack from project files
|
||||
*/
|
||||
function detectTechStack(workingDir) {
|
||||
const stack = {};
|
||||
|
||||
// Check for package.json (Node.js/TypeScript)
|
||||
const packageJsonPath = path.join(workingDir, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
|
||||
stack.language = 'TypeScript/JavaScript';
|
||||
stack.runtime = 'Node.js';
|
||||
|
||||
// Detect framework
|
||||
const deps = pkg.dependencies || {};
|
||||
if (deps.react) {
|
||||
stack.framework = 'React';
|
||||
} else if (deps.vue) {
|
||||
stack.framework = 'Vue';
|
||||
} else if (deps.svelte) {
|
||||
stack.framework = 'Svelte';
|
||||
} else if (deps.next) {
|
||||
stack.framework = 'Next.js';
|
||||
}
|
||||
|
||||
// Top dependencies
|
||||
stack.dependencies = Object.keys(deps).slice(0, 10);
|
||||
} catch (err) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pyproject.toml (Python)
|
||||
const pyprojectPath = path.join(workingDir, 'pyproject.toml');
|
||||
if (fs.existsSync(pyprojectPath)) {
|
||||
stack.language = 'Python';
|
||||
stack.packageManager = 'UV/pip';
|
||||
}
|
||||
|
||||
// Check for go.mod (Go)
|
||||
const goModPath = path.join(workingDir, 'go.mod');
|
||||
if (fs.existsSync(goModPath)) {
|
||||
stack.language = 'Go';
|
||||
}
|
||||
|
||||
// Check for Cargo.toml (Rust)
|
||||
const cargoPath = path.join(workingDir, 'Cargo.toml');
|
||||
if (fs.existsSync(cargoPath)) {
|
||||
stack.language = 'Rust';
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find existing specification documents
|
||||
*/
|
||||
function findSpecifications(workingDir) {
|
||||
const specs = [];
|
||||
|
||||
const specLocations = [
|
||||
'docs/specs',
|
||||
'docs/ARCHITECTURE.md',
|
||||
'ARCHITECTURE.md',
|
||||
'README.md',
|
||||
'CONTRIBUTING.md',
|
||||
'docs/DESIGN.md'
|
||||
];
|
||||
|
||||
for (const location of specLocations) {
|
||||
const fullPath = path.join(workingDir, location);
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
specs.push(location);
|
||||
} else if (stat.isDirectory()) {
|
||||
// Add all markdown files in specs directory
|
||||
try {
|
||||
const files = fs.readdirSync(fullPath);
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.md')) {
|
||||
specs.push(path.join(location, file));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore read errors
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return specs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find recent development plans
|
||||
*/
|
||||
function findRecentPlans(workingDir) {
|
||||
const plansDir = path.join(workingDir, '.parallel', 'plans');
|
||||
|
||||
if (!fs.existsSync(plansDir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const recentPlans = [];
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(plansDir);
|
||||
|
||||
for (const file of files) {
|
||||
if (file.startsWith('PLAN-') && file.endsWith('.md')) {
|
||||
try {
|
||||
// Extract timestamp (PLAN-YYYYMMDD-HHMMSS.md)
|
||||
const timestampStr = file.replace('PLAN-', '').replace('.md', '');
|
||||
const dateStr = timestampStr.split('-')[0]; // YYYYMMDD
|
||||
|
||||
// Parse date
|
||||
const year = parseInt(dateStr.substr(0, 4));
|
||||
const month = parseInt(dateStr.substr(4, 2)) - 1;
|
||||
const day = parseInt(dateStr.substr(6, 2));
|
||||
const planDate = new Date(year, month, day);
|
||||
|
||||
// Calculate age in days
|
||||
const ageDays = Math.floor((now - planDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Only include plans from last 30 days
|
||||
if (ageDays <= 30) {
|
||||
recentPlans.push({
|
||||
file: path.join('.parallel', 'plans', file),
|
||||
date: planDate.toISOString().split('T')[0],
|
||||
ageDays
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore read errors
|
||||
}
|
||||
|
||||
// Sort by age (newest first)
|
||||
recentPlans.sort((a, b) => a.ageDays - b.ageDays);
|
||||
|
||||
return recentPlans.slice(0, 5); // Return 5 most recent
|
||||
}
|
||||
|
||||
/**
|
||||
* Format context for injection into prompt
|
||||
*/
|
||||
function formatContextForInjection(context) {
|
||||
const lines = [];
|
||||
|
||||
lines.push('📋 RESEARCH CONTEXT (Use this information in your research!)');
|
||||
lines.push('');
|
||||
|
||||
// Current date (CRITICAL for web searches)
|
||||
lines.push(`**Current Date:** ${context.date}`);
|
||||
lines.push('⚠️ IMPORTANT: When searching the web, use THIS date, not 2024!');
|
||||
|
||||
const year = context.date.split('-')[0];
|
||||
lines.push(` Search for '${year}' or 'latest', not '2024'`);
|
||||
lines.push('');
|
||||
|
||||
// Project info
|
||||
lines.push(`**Project:** ${context.project}`);
|
||||
lines.push(`**Directory:** ${context.workingDir}`);
|
||||
lines.push('');
|
||||
|
||||
// Tech stack
|
||||
if (Object.keys(context.techStack).length > 0) {
|
||||
lines.push('**Tech Stack:**');
|
||||
for (const [key, value] of Object.entries(context.techStack)) {
|
||||
if (Array.isArray(value)) {
|
||||
lines.push(` • ${key}: ${value.slice(0, 5).join(', ')}`);
|
||||
} else {
|
||||
lines.push(` • ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Existing specifications
|
||||
if (context.specifications.length > 0) {
|
||||
lines.push('**Existing Specifications (READ THESE FIRST!):**');
|
||||
for (const spec of context.specifications) {
|
||||
lines.push(` • ${spec}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('⚠️ Do NOT research what\'s already specified!');
|
||||
lines.push(' Read these docs to understand existing decisions.');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// Recent plans
|
||||
if (context.recentPlans.length > 0) {
|
||||
lines.push('**Recent Development Plans:**');
|
||||
for (const plan of context.recentPlans) {
|
||||
lines.push(` • ${plan.file} (${plan.ageDays} days ago)`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('⚠️ Check if similar work was already planned!');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('---');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if prompt should receive context injection
|
||||
*/
|
||||
function shouldInjectContext(prompt) {
|
||||
const keywords = [
|
||||
'research',
|
||||
'plan',
|
||||
'parallel',
|
||||
'execute',
|
||||
'analyze',
|
||||
'design',
|
||||
'architect',
|
||||
'implement'
|
||||
];
|
||||
|
||||
const lowerPrompt = prompt.toLowerCase();
|
||||
return keywords.some(keyword => lowerPrompt.includes(keyword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main hook execution
|
||||
*/
|
||||
function main() {
|
||||
// Read stdin for hook input
|
||||
let inputData = '';
|
||||
|
||||
process.stdin.on('data', chunk => {
|
||||
inputData += chunk;
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
// Parse input
|
||||
const hookInput = JSON.parse(inputData);
|
||||
const prompt = hookInput.prompt || '';
|
||||
|
||||
// Only inject context if prompt mentions research/plan/execute keywords
|
||||
if (!shouldInjectContext(prompt)) {
|
||||
// Don't inject, continue normally
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current context
|
||||
const context = getCurrentContext();
|
||||
|
||||
// Format context for injection
|
||||
const contextBlock = formatContextForInjection(context);
|
||||
|
||||
// Inject at start of prompt
|
||||
const modifiedPrompt = `${contextBlock}\n${prompt}`;
|
||||
|
||||
// Build feedback message to show user what was injected
|
||||
const feedbackParts = [`ℹ️ Context injected: ${context.date}`];
|
||||
|
||||
if (context.techStack && Object.keys(context.techStack).length > 0) {
|
||||
if (context.techStack.language) {
|
||||
feedbackParts.push(context.techStack.language);
|
||||
}
|
||||
if (context.techStack.framework) {
|
||||
feedbackParts.push(context.techStack.framework);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.specifications && context.specifications.length > 0) {
|
||||
feedbackParts.push(`${context.specifications.length} spec(s)`);
|
||||
}
|
||||
|
||||
const feedback = feedbackParts.join(', ');
|
||||
|
||||
// Output modified prompt with user feedback
|
||||
const output = {
|
||||
continue: true,
|
||||
modifiedPrompt: modifiedPrompt,
|
||||
feedback: feedback,
|
||||
suppressOutput: false
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output));
|
||||
process.exit(0);
|
||||
|
||||
} catch (err) {
|
||||
// Fail silently on errors
|
||||
// Log to stderr for debugging (won't affect hook output)
|
||||
console.error('Context injection error:', err.message);
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentContext,
|
||||
detectTechStack,
|
||||
findSpecifications,
|
||||
findRecentPlans,
|
||||
formatContextForInjection
|
||||
};
|
||||
Reference in New Issue
Block a user