feat(installer): stop auto-indexing on install + ship opt-in front-load prompt hook

`codegraph install` no longer indexes the current directory — it wires up agents
only, and building a project's graph is always the explicit `codegraph init` /
`index`. Removes the global-vs-local inconsistency (a local install silently
indexed, a global one didn't) and the docs/behavior mismatch (#826). README
updated to match; the stale `init --index` note (indexing is default now) fixed.

Adds an opt-in Claude Code front-load hook: a `UserPromptSubmit` hook that runs
the new hidden `codegraph prompt-hook`, which injects codegraph_explore context
for structural ("how / where / trace / impact") prompts so the agent answers
from the graph instead of grepping to rebuild it. Prompted at install
(default-yes; Claude-only — the only agent with prompt hooks), removed on
uninstall, and `codegraph upgrade` self-heals it onto an already-configured
global Claude install. Strictly additive + degradable: non-structural prompts,
un-indexed projects, and any failure are silent no-ops. Disable without
uninstalling via CODEGRAPH_NO_PROMPT_HOOK=1.

7 new installer-targets contract tests (write / idempotent / opt-out round-trip /
sibling-preserved / uninstall / legacy-independent). Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-06-21 12:36:41 -05:00
co-authored by Claude Opus 4.8
parent 212dfc4b6a
commit bd4814d8c1
8 changed files with 333 additions and 98 deletions
+76
View File
@@ -1017,6 +1017,82 @@ program
}
});
/**
* codegraph prompt-hook (hidden)
*
* A Claude Code `UserPromptSubmit` hook entry point. Reads `{prompt, cwd}` JSON
* on stdin; for a structural/flow/impact prompt it runs `codegraph_explore` on
* the indexed project and prints the result to stdout, which Claude injects into
* the agent's context — so the agent's reflex grep/read has nothing left to find
* and reliably uses CodeGraph (the adoption problem). Installed by the installer
* into Claude's settings.json (opt-in, default-yes).
*
* LOAD-BEARING: this must NEVER break the user's prompt. Every failure path —
* kill-switch, non-structural prompt, no index, engine error — exits 0 with no
* output. The only effect is additive context when it can confidently provide it.
*/
program
.command('prompt-hook', { hidden: true })
.description('Claude UserPromptSubmit hook: inject CodeGraph context for structural prompts (reads {prompt,cwd} JSON on stdin)')
.action(async () => {
try {
// Kill-switch: lets a user disable the nudge without uninstalling /
// editing settings.json (CI, low-power machines, personal preference).
if (process.env.CODEGRAPH_NO_PROMPT_HOOK === '1' || process.env.CODEGRAPH_PROMPT_HOOK === '0') return;
if (process.stdin.isTTY) return; // invoked by hand, no piped payload
const raw = await new Promise<string>((resolve) => {
let data = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (c) => { data += c; });
process.stdin.on('end', () => resolve(data));
process.stdin.on('error', () => resolve(data));
});
let input: { prompt?: string; cwd?: string } = {};
try { input = JSON.parse(raw); } catch { return; }
const prompt = String(input.prompt || '');
// Gate: only structural / flow / impact / where-how prompts get context.
// A cheap regex keeps every other prompt ("fix this typo") a zero-cost
// no-op so we never add latency where there's no structural answer to give.
const STRUCTURAL = /\b(how|where|trace|flow|path|reach(?:es|ed)?|call(?:s|ed|er|ers|ee)?|depend|impact|affect|wired?|connect|implement|architect|structure|breaks?|what calls|why does)\b/i;
if (!prompt || !STRUCTURAL.test(prompt)) return;
// Find an indexed project: cwd, then walk up a few levels.
let root: string | null = null;
let dir = path.resolve(String(input.cwd || process.cwd()));
for (let i = 0; i < 6; i++) {
if (isInitialized(dir)) { root = dir; break; }
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
if (!root) return; // not indexed — the agent's normal tools apply
const { default: CodeGraph } = await loadCodeGraph();
const cg = await CodeGraph.open(root);
try {
const { ToolHandler } = await import('../mcp/tools');
const handler = new ToolHandler(cg);
const result = await handler.execute('codegraph_explore', { query: prompt });
const text = result.content[0]?.text ?? '';
if (!result.isError && text.trim()) {
// Cap the injection so a large-repo explore can't flood the prompt.
const MAX = 16000;
const body = text.length > MAX ? `${text.slice(0, MAX)}\n…(truncated; call codegraph_explore for the rest)` : text;
process.stdout.write(
`<codegraph_context note="Structural context from CodeGraph for this prompt — treat returned source as already read; call codegraph_explore for more.">\n${body}\n</codegraph_context>\n`,
);
}
} finally {
cg.destroy();
}
} catch {
// Degradable by contract: never surface an error to the prompt pipeline.
}
});
/**
* codegraph node <name>
*