Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:56:10 +08:00
commit 400ca062d1
48 changed files with 18674 additions and 0 deletions

368
hooks/context_injector.js Executable file
View 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
};