Files
gh-shakes-tzd-contextune/hooks/context_injector.js
2025-11-30 08:56:10 +08:00

369 lines
9.2 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
};