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:
Executable
+67
@@ -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 */ });
|
||||
Executable
+67
@@ -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 */ });
|
||||
@@ -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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 | 1–2 |
|
||||
| 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.
|
||||
@@ -16,3 +16,6 @@ compile_commands.json
|
||||
*.user
|
||||
.DS_Store
|
||||
.cache/
|
||||
|
||||
# graft's local graph cache — regenerable, not committed (run `graft build`).
|
||||
/graft/
|
||||
|
||||
@@ -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/
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"graft": {
|
||||
"command": "graft",
|
||||
"args": [
|
||||
"mcp"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 -->
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mcp": {
|
||||
"graft": {
|
||||
"type": "local",
|
||||
"command": [
|
||||
"graft",
|
||||
"mcp"
|
||||
],
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user