feat(intel): add Stop hook to prune deleted files
New intel-prune.js hook runs after each Claude response. Checks if indexed files still exist, removes stale entries. Fast (only fs.existsSync checks) and silent (never blocks). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -456,6 +456,29 @@ function install(isGlobal) {
|
||||
console.log(` ${green}✓${reset} Configured intel session hook`);
|
||||
}
|
||||
|
||||
// Stop hook for pruning deleted files
|
||||
const intelPruneCommand = isGlobal
|
||||
? 'node "$HOME/.claude/hooks/intel-prune.js"'
|
||||
: 'node .claude/hooks/intel-prune.js';
|
||||
|
||||
if (!settings.hooks.Stop) {
|
||||
settings.hooks.Stop = [];
|
||||
}
|
||||
|
||||
const hasIntelPruneHook = settings.hooks.Stop.some(entry =>
|
||||
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('intel-prune'))
|
||||
);
|
||||
|
||||
if (!hasIntelPruneHook) {
|
||||
settings.hooks.Stop.push({
|
||||
hooks: [{
|
||||
type: 'command',
|
||||
command: intelPruneCommand
|
||||
}]
|
||||
});
|
||||
console.log(` ${green}✓${reset} Configured intel prune hook`);
|
||||
}
|
||||
|
||||
return { settingsPath, settings, statuslineCommand };
|
||||
}
|
||||
|
||||
|
||||
78
hooks/intel-prune.js
Normal file
78
hooks/intel-prune.js
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Intel Prune Hook (Stop event)
|
||||
*
|
||||
* Removes stale entries from index.json when files no longer exist.
|
||||
* Runs after each Claude response to keep intel fresh.
|
||||
*
|
||||
* Fast: Only does fs.existsSync checks, no file reading.
|
||||
* Silent: Never blocks or errors, always exits 0.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function pruneIndex() {
|
||||
const intelDir = path.join(process.cwd(), '.planning', 'intel');
|
||||
const indexPath = path.join(intelDir, 'index.json');
|
||||
|
||||
// Only run if intel directory exists (opt-in check)
|
||||
if (!fs.existsSync(intelDir)) {
|
||||
return { pruned: 0, total: 0 };
|
||||
}
|
||||
|
||||
// Read existing index
|
||||
let index;
|
||||
try {
|
||||
const content = fs.readFileSync(indexPath, 'utf8');
|
||||
index = JSON.parse(content);
|
||||
} catch (e) {
|
||||
// No index or invalid JSON
|
||||
return { pruned: 0, total: 0 };
|
||||
}
|
||||
|
||||
if (!index.files || typeof index.files !== 'object') {
|
||||
return { pruned: 0, total: 0 };
|
||||
}
|
||||
|
||||
// Check each file and collect deleted ones
|
||||
const filePaths = Object.keys(index.files);
|
||||
const deleted = filePaths.filter(filePath => !fs.existsSync(filePath));
|
||||
|
||||
if (deleted.length === 0) {
|
||||
return { pruned: 0, total: filePaths.length };
|
||||
}
|
||||
|
||||
// Remove deleted entries
|
||||
for (const filePath of deleted) {
|
||||
delete index.files[filePath];
|
||||
}
|
||||
index.updated = Date.now();
|
||||
|
||||
// Write updated index
|
||||
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
|
||||
|
||||
// Regenerate conventions and summary after pruning
|
||||
// Import detection logic from intel-index.js would be complex,
|
||||
// so we just update the index. Conventions/summary stay until
|
||||
// next PostToolUse or /gsd:analyze-codebase refresh.
|
||||
|
||||
return { pruned: deleted.length, total: filePaths.length };
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Stop hook receives session data, but we don't need it
|
||||
// Just prune stale entries
|
||||
pruneIndex();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
// Silent failure - never block Claude
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user