chore: add graft repo context graph integration

Add .claude/ config (settings, graft statusline and hooks helpers,
graft skill), .mcp.json and opencode.json MCP server entries, and
AGENTS.md with graft usage instructions. Add .ignore to re-admit
graft/ to ripgrep search while excluding its cache. Add /graft/ to
.gitignore since the graph is regenerable.
This commit is contained in:
2026-09-09 14:33:58 +02:00
parent 8364737d1c
commit 5c942cad0c
9 changed files with 433 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const BAKED = "/usr/lib/node_modules/@nanonets/graft/dist/claude";
// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
function fromPkg(base) {
try {
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
return path.join(path.dirname(pkg), 'dist', 'claude');
} catch { return null; }
}
// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
function globalRoot() {
try {
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
return root || null;
} catch { return null; /* npm unavailable */ }
}
// The version of the package a dist/claude dir belongs to, or null if unreadable.
function versionOf(distClaude) {
try {
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
} catch { return null; }
}
// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
function newer(a, b) {
if (!a) return false;
if (!b) return true;
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
const pa = p(a), pb = p(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d > 0;
}
return false;
}
// The highest-versioned dir in `dirs` that actually contains `name`, or null.
function best(dirs, name) {
let bestDir = null, bestVer = null;
for (const d of dirs) {
if (!d || !fs.existsSync(path.join(d, name))) continue;
const v = versionOf(d);
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
}
return bestDir;
}
function entry(name) {
// Cheap candidates first, and only shell out to npm when every one of them misses.
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
const hit = best(cheap, name);
if (hit) return path.join(hit, name);
const gr = globalRoot();
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
}
import(pathToFileURL(entry("hooks.js")).href).then((m) => m.main(process.argv[2])).catch(() => { /* graft unavailable — no-op */ });
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const { pathToFileURL } = require('url');
const { execFileSync } = require('child_process');
const dir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
const BAKED = "/usr/lib/node_modules/@nanonets/graft/dist/claude";
// The dist/claude dir of @nanonets/graft resolved from a base whose node_modules is searched.
function fromPkg(base) {
try {
const pkg = require.resolve('@nanonets/graft/package.json', { paths: [base] });
return path.join(path.dirname(pkg), 'dist', 'claude');
} catch { return null; }
}
// The global node_modules dir per npm (handles Homebrew/Windows/volta). Queried on demand.
function globalRoot() {
try {
const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], shell: process.platform === 'win32' }).trim();
return root || null;
} catch { return null; /* npm unavailable */ }
}
// The version of the package a dist/claude dir belongs to, or null if unreadable.
function versionOf(distClaude) {
try {
return JSON.parse(fs.readFileSync(path.join(distClaude, '..', '..', 'package.json'), 'utf8')).version || null;
} catch { return null; }
}
// Numeric-dotted compare of the release part; an unreadable version loses to any known one.
function newer(a, b) {
if (!a) return false;
if (!b) return true;
const p = (v) => String(v).split('-')[0].split('.').map((n) => Number(n) || 0);
const pa = p(a), pb = p(b);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const d = (pa[i] || 0) - (pb[i] || 0);
if (d !== 0) return d > 0;
}
return false;
}
// The highest-versioned dir in `dirs` that actually contains `name`, or null.
function best(dirs, name) {
let bestDir = null, bestVer = null;
for (const d of dirs) {
if (!d || !fs.existsSync(path.join(d, name))) continue;
const v = versionOf(d);
if (bestDir === null || newer(v, bestVer)) { bestDir = d; bestVer = v; }
}
return bestDir;
}
function entry(name) {
// Cheap candidates first, and only shell out to npm when every one of them misses.
const cheap = [BAKED, fromPkg(dir), fromPkg(path.join(path.dirname(process.execPath), '..', 'lib'))];
const hit = best(cheap, name);
if (hit) return path.join(hit, name);
const gr = globalRoot();
const global = gr && path.join(gr, '@nanonets', 'graft', 'dist', 'claude');
if (global && fs.existsSync(path.join(global, name))) return path.join(global, name);
return path.join(dir, 'dist', 'claude', name); // last-ditch; import will no-op if absent
}
import(pathToFileURL(entry("statusline.js")).href).then((m) => m.main()).catch(() => { /* graft unavailable — no-op */ });
+78
View File
@@ -0,0 +1,78 @@
{
"statusLine": {
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
},
"subagentStatusLine": {
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-statusline.cjs\""
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" post-edit",
"timeout": 10000
}
]
},
{
"matcher": "Bash|mcp__graft__|Read|Grep|Glob",
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" tool-savings",
"timeout": 8000
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" prompt",
"timeout": 15000
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" session-start",
"timeout": 8000
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "node \"${CLAUDE_PROJECT_DIR:-.}/.claude/helpers/graft-hooks.cjs\" stop",
"timeout": 8000
}
]
}
]
},
"footerLinksRegexes": [
"graft/[\\w./-]+\\.md"
],
"permissions": {
"allow": [
"Bash(graft:*)",
"Bash(npx graft:*)",
"Bash(graft-dev:*)",
"Bash(node dist/cli.js:*)"
]
}
}
+150
View File
@@ -0,0 +1,150 @@
---
name: graft
description: This repo is indexed by graft/. For ANY task here, whether
understanding how something works, finding where code lives, tracing what
calls a symbol or what a change breaks, or scoping an edit, get your context
from graft before grepping or reading source files.
---
# graft
`graft/` holds a graph of this repo: small markdown nodes that each explain one
part in prose and name the exact `file:line` spans they cover, plus a wiring
graph of who-calls-what. Querying a node costs a few hundred tokens; rebuilding
that understanding by reading source costs thousands, and misses the edges.
Every command below is `$0`, needs no API key, and returns in under a second.
There are six of them. **Pick the one that fits the task, run it, act on the
answer; don't chain tools hoping for more. Most tasks need one call.**
## The tools
### 1 · `graft ask "<question>" --source`: locate + understand (the default)
Ranked retrieval over the graph, routed automatically between prose nodes and
the wiring graph, returning the top hits with exact `file:line`.
- `--source` inlines the code at each hit, the ≤8-line **crux** of each
definition, so the result IS the code you need, no follow-up file read. Add
`--full` only when the crux is too small to act on.
- `--in <path>` narrows to a subtree before ranking; `-n N` caps results (default 8).
- **Use it when** the question is conceptual or locational: "how does auth
work", "where is rate-limiting handled", "what assembles the request pipeline".
- One ask usually answers. A genuinely multi-part question needs one ask per
distinct sub-aspect, never the same question reworded. Few or weak hits mean
switch tool (grep / skeleton / callers), don't re-ask.
### 2 · `graft grep "<pattern>"`: exhaustive find
Regex (or `--fixed` for a literal) over every indexed file, hits **grouped by
enclosing symbol** and ranked by coupling; it also reports files it couldn't read.
- **Use it when** you need every occurrence: all call sites, all uses of a
constant, all providers. `ask` is ranked top-N and *will* miss instances;
grep won't. One grep replaces a spray of asks.
- Search a **short symbol name or literal**, not a full guessed signature: an
over-specific regex (`func (s *Server) GenerateHandler`) returns nothing even
when the code is indexed. If a grep misses, **loosen it** (drop the receiver
and signature, keep the bare name) and retry `graft grep` — do NOT switch to
raw `grep -rn`, which is slower and unranked.
- `-i` case-insensitive; `--in <path>` scopes to a subtree. Raw `grep -rn` is
only for files graft genuinely doesn't index (docs, configs, brand-new files).
### 3 · `graft skeleton <file>`: a file's API at a glance
Signatures-only view of one file (every function / method / type with its span)
in ~200 tokens, ~10x cheaper than reading the file.
- **Use it when** you need "what's in this file / what can I call here" before
editing or wiring into it. One skeleton is the whole answer for a file; don't
re-skeleton the same file, and don't skeleton every file `map` already named.
### 4 · `graft callers <symbol>`: the exact edges
Precomputed call/reference edges, not a text search. Symbol can be bare
(`Foo`), qualified (`Class.method`), or package-qualified (`pkg.Fn`).
- default `--direction in`: **who calls/references** this; run before you
rename, delete, or change its signature.
- `--direction out`: **what this symbol itself calls/depends on** (the old `callees`).
- `--depth N`: walk transitively N hops for the **full blast radius** (the old
`impact`); `--depth 2` is the usual "what breaks if I touch this".
- `--depth all`: the **entire connected closure** — every source reachable
through the edges. Reach for this before a **refactor, rename, or any
multi-file change**: it surfaces the sibling and downstream files (platform
variants, a module you must split out) that a single-file edit would miss.
### 5 · `graft map`: orientation for an unfamiliar repo or area
A token-budgeted tour: directory clusters, per-directory hubs, and global
hotspots, straight from the wiring graph.
- **Use it when** you land in a repo cold or are asked for "the architecture".
`map` alone is the answer: read the hub cards it names; do NOT then skeleton
or ask your way through every subsystem it lists. `--max-dirs N` widens it.
### 6 · Lifecycle: `graft build` / `graft check`
Every tool above refreshes the graph itself before answering, so what those tools
return always describes the code as it is right now — including edits you just made
and have not committed. You do **not** need to run `build` after editing.
One caveat, if you `grep` the markdown under `graft/` directly: those cards are a
projection, rebuilt at the end of the turn rather than on each query, so after an edit
they can lag. The tools above never do — prefer them, and treat a card's spans as
stale if you have edited that file this turn.
`build` is for the LLM layer (`--deep` adds a concept map; skip unless asked);
`check` fails when `graft/` is stale, for CI.
## Scenarios: the shortest path through a coding task
| When you're… | Reach for | Calls |
|---|---|---|
| Onboarding / "explain this codebase" | `graft map`, then read the named hub cards | 1 |
| Understanding a flow ("how does X work") | `graft ask "<flow>" --source` | 1 |
| Finding where a change belongs | `graft ask "where is <behavior>" --source` | 1 |
| Editing a symbol you can already name | `graft grep "<symbol>"`, edit at the `file:line` (skip `ask` — you know where it is) | 1 |
| Renaming / deleting / changing a signature | `graft callers <sym> --depth 2` first | 1 |
| Refactor / multi-file change (before editing) | `graft callers <sym> --depth all` — map every connected file, don't stop at the first | 1 |
| "What does this depend on?" | `graft callers <sym> --direction out` | 1 |
| Finding every occurrence of a pattern | `graft grep "<literal>"` | 1 |
| "What's the API of this file?" | `graft skeleton <file>` | 1 |
| Debugging a failure in area X | `graft ask "<symptom>" --source`, then `callers` on the suspect | 12 |
| Judging a diff's risk before merge | `graft callers <changed sym> --depth 2` | 1 / symbol |
| Working inside one repo of a monorepo | add `--in <scope>/` to ask / grep / callers | n/a |
In a multi-repo workspace, graft ranks fairly so the biggest repo can't drown
the rest, and every hit carries a `[scope/]` label naming its sub-project; when
you already know where you're working, narrow with `graft ask "<task>" --in <scope>/`.
## Spend the fewest calls
- A node's `covers:` list already gives exact `file:line` for every symbol, so
cite straight from it. The spans are generated from source and authoritative;
don't re-open or re-grep files to "double-check".
- When the task already names the file or symbol to change, go straight there:
`graft grep "<symbol>"` for the exact `file:line`, then edit. Reserve
`graft ask` for when you don't yet know where the code lives — an `ask`
round-trip is wasted on a target you can already name.
- Trust the answer and act. Reach for a second tool only when the first genuinely
fell short: weak hits, a truncated span, or a need to be exhaustive.
- If graft names a path that isn't on disk, its index is ahead of your checkout
(a branch switch or unpulled move). Don't read the missing file — `graft grep`
the symbol to find where it lives now, or run `graft build` to refresh.
## Report what graft saved, every turn
Each retrieval tool **opens** its output with a `[graft] tokens saved ≈ N` line:
the estimated tokens that call saved versus reading the files it covers whole.
Whenever you used any graft tool in a turn, close your reply with a one-line
tally summing those numbers across every graft call you made, e.g.
`🌱 graft saved ~12,400 tokens this turn (3 calls)`. A call with no such line
(tiny files, where the pointers cost as much as the source) saved nothing, so
skip it. This is the per-turn figure; the statusline carries the running
session total.
**Never pipe a graft command through `head`, `tail`, or `sed -n`.** Every tool
is already capped and states what it dropped; clipping it costs you hits you
asked for, and it silently drops the savings line the statusline's running
total is parsed from.
## When graft isn't enough
- Span truncated ("+N more lines"): open the file at that exact range.
- A node lacks a detail: ask a more specific question; only then read source at
the exact `file:line`, never a whole file to rebuild understanding graft gives.
- You may also grep / ls / cat inside `graft/` directly (plain markdown;
`graft/INDEX.md` indexes the nodes), but the tools above are faster and
exhaustive where it matters, so reach for them first.
When the graft MCP server is connected, these are exposed as tools too:
`graft_find_code`, `graft_find_all`, `graft_file_api`, `graft_trace_calls` (with
`direction` / `depth`), `graft_repo_map`, `graft_check_freshness`. Use whichever surface is
available; the guidance is identical.
+3
View File
@@ -16,3 +16,6 @@ compile_commands.json
*.user
.DS_Store
.cache/
# graft's local graph cache — regenerable, not committed (run `graft build`).
/graft/
+5
View File
@@ -0,0 +1,5 @@
# graft's cards are gitignored but should stay greppable: ripgrep reads
# .ignore before .gitignore, so this re-admits the tree to search only.
!graft/
graft/.cache/
graft/.graph/
+10
View File
@@ -0,0 +1,10 @@
{
"mcpServers": {
"graft": {
"command": "graft",
"args": [
"mcp"
]
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<!-- graft:start -->
## Graft — repo context graph
This repo is indexed in `graft/`: small linked markdown nodes that explain each
system and carry exact file:line spans, kept in sync with the code through git.
For ANY task here — understanding how something works, finding where code lives,
or scoping a change — get context from the graph before grepping or opening
source files. Re-ask freely (it's cheap) and reuse literal identifiers you
already have (symbol, error string, file name) as the query. New to this repo?
Run `graft map` first — a token-budgeted orientation (dir clusters, hubs,
hotspots), no LLM, no key.
- Run `graft ask "<your question>" --source` → ranked nodes with the relevant
code spans inlined (each hit's ≤8-line crux by default; `--full` for whole
definitions when the crux isn't enough). Match the tool to the task shape:
for understanding or editing, the top node IS the answer — cite its
`covers:` file:line spans and edit straight from `--source`. For
exhaustive tasks ("every occurrence / every caller of this pattern"), ranked
results are top-N, not complete — run `graft grep "<literal>"` instead
(exhaustive over indexed files, grouped by enclosing symbol), falling back
to raw `grep -rn` only for unindexed files.
- `graft skeleton <file>` → every definition's signature + span, ~10× cheaper
than reading the file; use it to skim an API surface.
- `graft callers <symbol>` gives precomputed, exact edges — who calls this.
Add `--direction out` for what it calls, or `--depth N` to walk
transitively for the full blast radius. For structural questions, skip
ranking and use this directly.
- Or browse: `graft/INDEX.md` lists every node; follow the links.
- Monorepos and folders of multiple repos rank fairly across sub-projects —
hits carry `[scope/]` labels naming which one they're from. Narrow with
`graft ask "<task>" --in <scope>/` once you know where you're working.
If a returned span is truncated ("+N more lines"), open the file at that exact
range before finalizing. Only open source files when a node genuinely lacks a
needed detail, and then at the exact file:line the node points to — never
re-read whole files.
After big code changes, refresh the graph with `graft build` (deterministic,
no API key, $0).
<!-- graft:end -->
+12
View File
@@ -0,0 +1,12 @@
{
"mcp": {
"graft": {
"type": "local",
"command": [
"graft",
"mcp"
],
"enabled": true
}
}
}