Add Claude Code hooks for automatic CodeGraph sync
PostToolUse(Edit|Write) marks the project dirty via .codegraph/.dirty, and Stop syncs only if dirty — batching all edits into one sync per Claude response. The installer now writes these hooks to settings.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
48cc3a8b66
commit
f46ba02b87
+72
-1
@@ -15,6 +15,8 @@
|
|||||||
* codegraph query <search> Search for symbols
|
* codegraph query <search> Search for symbols
|
||||||
* codegraph files [options] Show project file structure
|
* codegraph files [options] Show project file structure
|
||||||
* codegraph context <task> Build context for a task
|
* codegraph context <task> Build context for a task
|
||||||
|
* codegraph mark-dirty [path] Mark project as needing sync (hooks)
|
||||||
|
* codegraph sync-if-dirty [path] Sync if marked dirty (hooks)
|
||||||
*
|
*
|
||||||
* Note: Git hooks have been removed. CodeGraph sync is triggered automatically
|
* Note: Git hooks have been removed. CodeGraph sync is triggered automatically
|
||||||
* through codegraph's Claude Code hooks integration.
|
* through codegraph's Claude Code hooks integration.
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import CodeGraph from '../index';
|
import CodeGraph, { getCodeGraphDir, findNearestCodeGraphRoot } from '../index';
|
||||||
import type { IndexProgress } from '../index';
|
import type { IndexProgress } from '../index';
|
||||||
import { runInstaller } from '../installer';
|
import { runInstaller } from '../installer';
|
||||||
import { initSentry, captureException } from '../sentry';
|
import { initSentry, captureException } from '../sentry';
|
||||||
@@ -926,6 +928,75 @@ program
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* codegraph mark-dirty [path]
|
||||||
|
*
|
||||||
|
* Touches .codegraph/.dirty to signal that files have changed.
|
||||||
|
* Used by Claude Code PostToolUse hooks to batch syncs.
|
||||||
|
* Runs silently and always exits 0.
|
||||||
|
*/
|
||||||
|
program
|
||||||
|
.command('mark-dirty [path]')
|
||||||
|
.description('Mark project as needing sync (used by Claude Code hooks)')
|
||||||
|
.action(async (pathArg: string | undefined) => {
|
||||||
|
try {
|
||||||
|
const startPath = path.resolve(pathArg || process.cwd());
|
||||||
|
const projectRoot = findNearestCodeGraphRoot(startPath);
|
||||||
|
if (!projectRoot) {
|
||||||
|
// No .codegraph/ found — exit silently
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
|
||||||
|
fs.writeFileSync(dirtyPath, Date.now().toString(), 'utf-8');
|
||||||
|
} catch {
|
||||||
|
// Never fail — this runs in the background during edits
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* codegraph sync-if-dirty [path]
|
||||||
|
*
|
||||||
|
* Syncs the index only if .codegraph/.dirty exists.
|
||||||
|
* Removes the marker BEFORE syncing so edits during sync
|
||||||
|
* create a new marker for the next Stop event.
|
||||||
|
* Runs silently and always exits 0.
|
||||||
|
*/
|
||||||
|
program
|
||||||
|
.command('sync-if-dirty [path]')
|
||||||
|
.description('Sync if project was marked dirty (used by Claude Code hooks)')
|
||||||
|
.action(async (pathArg: string | undefined) => {
|
||||||
|
try {
|
||||||
|
const startPath = path.resolve(pathArg || process.cwd());
|
||||||
|
const projectRoot = findNearestCodeGraphRoot(startPath);
|
||||||
|
if (!projectRoot) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
const dirtyPath = path.join(getCodeGraphDir(projectRoot), '.dirty');
|
||||||
|
|
||||||
|
// No marker → nothing to do (sub-ms exit)
|
||||||
|
if (!fs.existsSync(dirtyPath)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove marker FIRST so edits during sync create a new one
|
||||||
|
try { fs.unlinkSync(dirtyPath); } catch { /* ignore */ }
|
||||||
|
|
||||||
|
// If not fully initialized (no DB), exit
|
||||||
|
if (!CodeGraph.isInitialized(projectRoot)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run sync
|
||||||
|
const cg = await CodeGraph.open(projectRoot);
|
||||||
|
await cg.sync();
|
||||||
|
cg.destroy();
|
||||||
|
} catch {
|
||||||
|
// Never fail — this runs at the end of Claude responses
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* codegraph install
|
* codegraph install
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ cache/
|
|||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# Hook markers
|
||||||
|
.dirty
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
fs.writeFileSync(gitignorePath, gitignoreContent, 'utf-8');
|
||||||
|
|||||||
@@ -175,6 +175,93 @@ export function hasPermissions(location: InstallLocation): boolean {
|
|||||||
return permissions.some((p: string) => p.startsWith('mcp__codegraph__'));
|
return permissions.some((p: string) => p.startsWith('mcp__codegraph__'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Hooks Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the hooks configuration for Claude Code auto-sync.
|
||||||
|
*
|
||||||
|
* PostToolUse(Edit|Write) → mark-dirty (async, non-blocking)
|
||||||
|
* Stop → sync-if-dirty (sync, ensures fresh index before next user turn)
|
||||||
|
*/
|
||||||
|
function getHooksConfig(location: InstallLocation): Record<string, any> {
|
||||||
|
const command = location === 'global' ? 'codegraph' : 'npx @colbymchenry/codegraph';
|
||||||
|
|
||||||
|
return {
|
||||||
|
PostToolUse: [
|
||||||
|
{
|
||||||
|
matcher: 'Edit|Write',
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
type: 'command',
|
||||||
|
command: `${command} mark-dirty`,
|
||||||
|
async: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
Stop: [
|
||||||
|
{
|
||||||
|
hooks: [
|
||||||
|
{
|
||||||
|
type: 'command',
|
||||||
|
command: `${command} sync-if-dirty`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if Claude Code hooks already exist for CodeGraph
|
||||||
|
*/
|
||||||
|
export function hasHooks(location: InstallLocation): boolean {
|
||||||
|
const settingsPath = getSettingsJsonPath(location);
|
||||||
|
const settings = readJsonFile(settingsPath);
|
||||||
|
const hooks = settings.hooks;
|
||||||
|
if (!hooks) return false;
|
||||||
|
|
||||||
|
// Check if any hook command references codegraph
|
||||||
|
const json = JSON.stringify(hooks);
|
||||||
|
return json.includes('codegraph mark-dirty') || json.includes('codegraph sync-if-dirty');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write Claude Code hooks to settings.json for auto-sync.
|
||||||
|
* Merges with existing hooks, deduplicating any previous codegraph entries.
|
||||||
|
*/
|
||||||
|
export function writeHooks(location: InstallLocation): void {
|
||||||
|
const settingsPath = getSettingsJsonPath(location);
|
||||||
|
const settings = readJsonFile(settingsPath);
|
||||||
|
|
||||||
|
if (!settings.hooks) {
|
||||||
|
settings.hooks = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHooks = getHooksConfig(location);
|
||||||
|
|
||||||
|
// For each hook event (PostToolUse, Stop), merge with existing entries
|
||||||
|
for (const [event, newEntries] of Object.entries(newHooks)) {
|
||||||
|
if (!Array.isArray(settings.hooks[event])) {
|
||||||
|
settings.hooks[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any existing codegraph entries for this event
|
||||||
|
settings.hooks[event] = (settings.hooks[event] as any[]).filter((entry: any) => {
|
||||||
|
// Keep entries that don't reference codegraph
|
||||||
|
const entryJson = JSON.stringify(entry);
|
||||||
|
return !entryJson.includes('codegraph mark-dirty') && !entryJson.includes('codegraph sync-if-dirty');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new codegraph entries
|
||||||
|
settings.hooks[event].push(...(newEntries as any[]));
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJsonFile(settingsPath, settings);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to CLAUDE.md
|
* Get the path to CLAUDE.md
|
||||||
* - Global: ~/.claude/CLAUDE.md
|
* - Global: ~/.claude/CLAUDE.md
|
||||||
|
|||||||
+13
-3
@@ -8,7 +8,7 @@
|
|||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
|
import { showBanner, showNextSteps, success, error, info, chalk } from './banner';
|
||||||
import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
|
import { promptInstallLocation, promptAutoAllow, InstallLocation } from './prompts';
|
||||||
import { writeMcpConfig, writePermissions, writeClaudeMd, hasMcpConfig, hasPermissions } from './config-writer';
|
import { writeMcpConfig, writePermissions, writeClaudeMd, writeHooks, hasMcpConfig, hasPermissions, hasHooks } from './config-writer';
|
||||||
import CodeGraph from '../index';
|
import CodeGraph from '../index';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +76,17 @@ export async function runInstaller(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Write CLAUDE.md instructions
|
// Step 5: Write auto-sync hooks
|
||||||
|
const alreadyHasHooks = hasHooks(location);
|
||||||
|
writeHooks(location);
|
||||||
|
|
||||||
|
if (alreadyHasHooks) {
|
||||||
|
success(`Updated auto-sync hooks in ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
|
||||||
|
} else {
|
||||||
|
success(`Added auto-sync hooks to ${location === 'global' ? '~/.claude/settings.json' : './.claude/settings.json'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Write CLAUDE.md instructions
|
||||||
const claudeMdResult = writeClaudeMd(location);
|
const claudeMdResult = writeClaudeMd(location);
|
||||||
const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
|
const claudeMdPath = location === 'global' ? '~/.claude/CLAUDE.md' : './.claude/CLAUDE.md';
|
||||||
|
|
||||||
@@ -88,7 +98,7 @@ export async function runInstaller(): Promise<void> {
|
|||||||
success(`Added CodeGraph instructions to ${claudeMdPath}`);
|
success(`Added CodeGraph instructions to ${claudeMdPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 6: For local install, initialize the project
|
// Step 7: For local install, initialize the project
|
||||||
if (location === 'local') {
|
if (location === 'local') {
|
||||||
await initializeLocalProject();
|
await initializeLocalProject();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user