@sdsrs/code-graph 0.98.1 → 0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/post-grep-inject.js +16 -2
- package/claude-plugin/scripts/pre-grep-guide.js +12 -3
- package/claude-plugin/scripts/project-root.js +66 -14
- package/claude-plugin/scripts/session-init.js +17 -4
- package/claude-plugin/scripts/user-prompt-context.js +5 -2
- package/package.json +6 -6
|
@@ -309,8 +309,22 @@ function runMain() {
|
|
|
309
309
|
|
|
310
310
|
// Only inject on hits — no-hits / unavailable / no-binary stay silent (the grep
|
|
311
311
|
// already ran and produced its own output; a failed cg answer adds no value and
|
|
312
|
-
// 0 hits ≠ proof of absence given regex-dialect differences).
|
|
313
|
-
|
|
312
|
+
// 0 hits ≠ proof of absence given regex-dialect differences). But RECORD the
|
|
313
|
+
// skip (roadmap 2026-07-18 §1.6): without a record the funnel cannot tell
|
|
314
|
+
// "hook dark (binary missing)" from "ran, genuinely nothing" — the sibling
|
|
315
|
+
// hooks all record their non-answered paths. `fallthrough`+`reason` carry the
|
|
316
|
+
// status in the shapes the Rust aggregator already scores as inconclusive
|
|
317
|
+
// (`fallthrough:"no-hits"` / `reason:"unavailable"`); answered:false keeps it
|
|
318
|
+
// out of funnel arming and lands it in `inject_skipped`.
|
|
319
|
+
if (answer.status !== 'hits') {
|
|
320
|
+
recordRecommendation(root, {
|
|
321
|
+
hook: 'grep', action: 'inject', answered: false,
|
|
322
|
+
...(pattern ? { pattern } : {}),
|
|
323
|
+
fallthrough: answer.status,
|
|
324
|
+
reason: answer.status,
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
314
328
|
|
|
315
329
|
recordRecommendation(root, {
|
|
316
330
|
hook: 'grep', action: 'inject', answered: true,
|
|
@@ -376,6 +376,15 @@ function extractUnansweredTail(cmd) {
|
|
|
376
376
|
// two lines, verbatim command so the model can re-issue without thinking.
|
|
377
377
|
// Wording is path-neutral — it must stay true for BOTH the answered deny
|
|
378
378
|
// ("grep half answered, tail wasn't") and the static fallback (nothing was).
|
|
379
|
+
// Head-line marker paired with the NOTE below: the NOTE sits at the END of a
|
|
380
|
+
// long deny message and Claude Code's transcript view folds long tool errors,
|
|
381
|
+
// so a human reading the truncated view saw a clean "answered" deny with no
|
|
382
|
+
// clue the compound's tail was dropped (2026-07-18: two such denies were
|
|
383
|
+
// misdiagnosed as product bugs). The model gets the full reason either way.
|
|
384
|
+
function tailFlagSuffix(tail) {
|
|
385
|
+
return tail ? ' (compound tail NOT run — see NOTE at end)' : '';
|
|
386
|
+
}
|
|
387
|
+
|
|
379
388
|
function appendUnansweredTailNote(lines, tail) {
|
|
380
389
|
if (!tail) return;
|
|
381
390
|
const shown = tail.length > 300 ? tail.slice(0, 300) + '…' : tail;
|
|
@@ -538,7 +547,7 @@ function buildBlockReason(unansweredTail) {
|
|
|
538
547
|
// permanent prefix (adopted in 8s, reused 11×, incl. on the exact identifier
|
|
539
548
|
// searches this hook targets). The env opt-out stays documented in README.
|
|
540
549
|
const lines = [
|
|
541
|
-
|
|
550
|
+
`[code-graph] Raw \`grep -rn\` on indexed source — denied by code-graph hook.${tailFlagSuffix(unansweredTail)}`,
|
|
542
551
|
'Use the AST-aware equivalent (returns containing fn/module per hit, repo-wide):',
|
|
543
552
|
' code-graph-mcp grep "<pattern>" [paths...] # AST context per hit; -F literal, -i, -w, -l, -C N, --max-count 0',
|
|
544
553
|
' code-graph-mcp ast-search "<pattern>" --type fn # filter by node type',
|
|
@@ -558,7 +567,7 @@ function buildBlockReason(unansweredTail) {
|
|
|
558
567
|
function buildBlockReasonWithAnswer(pattern, searchPath, answer, unansweredTail) {
|
|
559
568
|
const cmdShown = `code-graph-mcp grep "${pattern}"${searchPath ? ` ${searchPath}` : ''}`;
|
|
560
569
|
const lines = [
|
|
561
|
-
|
|
570
|
+
`[code-graph] Raw \`grep\` on indexed source — denied; the AST-aware equivalent already ran for you:${tailFlagSuffix(unansweredTail)}`,
|
|
562
571
|
`$ ${cmdShown}`,
|
|
563
572
|
answer.text,
|
|
564
573
|
];
|
|
@@ -584,7 +593,7 @@ function buildBlockReasonWithAnswer(pattern, searchPath, answer, unansweredTail)
|
|
|
584
593
|
// `$ code-graph-mcp show <sym>` headers.
|
|
585
594
|
function buildShowDenyReason(answer, unansweredTail) {
|
|
586
595
|
const lines = [
|
|
587
|
-
|
|
596
|
+
`[code-graph] Raw grep for symbol definitions — denied; here are the definitions from the AST index:${tailFlagSuffix(unansweredTail)}`,
|
|
588
597
|
answer.text,
|
|
589
598
|
];
|
|
590
599
|
if (answer.truncated) {
|
|
@@ -8,12 +8,34 @@
|
|
|
8
8
|
// relative-path matching) fails silently for the rest of the session (daagu
|
|
9
9
|
// 2026-06-11: 38/40 head-greps dark; the read hook never recorded AT ALL).
|
|
10
10
|
// Walk up to the nearest ancestor holding `.code-graph/index.db`; stop at
|
|
11
|
-
// $HOME (
|
|
11
|
+
// $HOME and at `.git` boundaries (neither is crossed, and home's own index is
|
|
12
|
+
// never adopted from below).
|
|
12
13
|
|
|
13
14
|
const fs = require('fs');
|
|
14
15
|
const os = require('os');
|
|
15
16
|
const path = require('path');
|
|
16
17
|
|
|
18
|
+
// A linked git worktree's `.git` is a FILE containing
|
|
19
|
+
// `gitdir: <main>/.git/worktrees/<name>` — resolve it to the main checkout so
|
|
20
|
+
// worktree sessions (Claude Code's EnterWorktree puts them under
|
|
21
|
+
// <main>/.claude/worktrees/<slug>) reuse the main index instead of going dark.
|
|
22
|
+
// A submodule's `.git` file points at `.git/modules/…` and stays a hard
|
|
23
|
+
// boundary: its content is a DIFFERENT codebase, not a branch copy of the
|
|
24
|
+
// parent. Returns null for a regular `.git` directory (EISDIR), a missing
|
|
25
|
+
// `.git` (ENOENT), or any gitdir not under `.git/worktrees/`.
|
|
26
|
+
function worktreeMainRoot(dir) {
|
|
27
|
+
let raw;
|
|
28
|
+
try { raw = fs.readFileSync(path.join(dir, '.git'), 'utf8'); }
|
|
29
|
+
catch { return null; }
|
|
30
|
+
const m = /^gitdir:\s*(.+?)\s*$/m.exec(raw);
|
|
31
|
+
if (!m) return null;
|
|
32
|
+
const gitdir = path.resolve(dir, m[1]);
|
|
33
|
+
const marker = `${path.sep}.git${path.sep}worktrees${path.sep}`;
|
|
34
|
+
const at = gitdir.lastIndexOf(marker);
|
|
35
|
+
if (at < 0) return null;
|
|
36
|
+
return gitdir.slice(0, at) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
// Resolves to the project's CANONICAL index dir, skipping STRAY nested indexes.
|
|
18
40
|
// A monorepo subdir (`daagu/backend`, `daagu/frontend`) can carry its own
|
|
19
41
|
// `.code-graph/index.db` — a relic an older binary created — nested under the
|
|
@@ -22,8 +44,17 @@ const path = require('path');
|
|
|
22
44
|
// `✗ 0 nodes` in an empty subdir index). Mirror the Rust resolver: the start's
|
|
23
45
|
// own index wins only if it is NOT a stray nested index (no indexed ancestor) OR
|
|
24
46
|
// start is itself a project boundary (`.git`, i.e. a real submodule). Otherwise
|
|
25
|
-
// prefer the project root: the nearest indexed `.git` root, else the
|
|
26
|
-
// indexed
|
|
47
|
+
// prefer the project root: the nearest indexed `.git` root, else the nearest
|
|
48
|
+
// indexed ancestor INSIDE the boundary (git root if any, home otherwise — the
|
|
49
|
+
// walk stops at both, so a nested repo never adopts the outer project's index
|
|
50
|
+
// and a stray `~/.code-graph` never leaks into un-indexed dirs under home).
|
|
51
|
+
// `null` when nothing on start→…→boundary is indexed.
|
|
52
|
+
//
|
|
53
|
+
// Rust parity note (cli::resolve_project_root_from): the Rust resolver is the
|
|
54
|
+
// WRITE side — in a worktree it returns the worktree itself and builds a local
|
|
55
|
+
// index there. This reader prefers such a local index when present (own-index
|
|
56
|
+
// rule), falling back to the main checkout's index only while the worktree has
|
|
57
|
+
// none. Divergence is intentional; keep it documented on both sides.
|
|
27
58
|
function resolveProjectRoot(startDir, opts = {}) {
|
|
28
59
|
const home = opts.home !== undefined ? opts.home : os.homedir();
|
|
29
60
|
const exists = opts.exists || fs.existsSync;
|
|
@@ -32,17 +63,28 @@ function resolveProjectRoot(startDir, opts = {}) {
|
|
|
32
63
|
const start = path.resolve(startDir || '.');
|
|
33
64
|
|
|
34
65
|
// start's own `.git` is a hard project boundary (a real submodule / distinct
|
|
35
|
-
// repo): use its index if present,
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
66
|
+
// repo): use its index if present. With none, a linked WORKTREE falls back to
|
|
67
|
+
// its main checkout's index (a worktree is a branch copy of that codebase);
|
|
68
|
+
// anything else → `null` — never escape to an ancestor's index. Mirrors the
|
|
69
|
+
// Rust resolver's rule 1 (which returns cwd even without an index because it
|
|
70
|
+
// CREATES one; the JS reader has nothing to read → null).
|
|
71
|
+
if (hasGit(start)) {
|
|
72
|
+
if (hasIndex(start)) return start;
|
|
73
|
+
const main = worktreeMainRoot(start);
|
|
74
|
+
return main && hasIndex(main) ? main : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// start IS home: own-index rule only (deliberately indexed home dirs keep
|
|
78
|
+
// working from home itself), and never scan ancestors ABOVE home — without
|
|
79
|
+
// this, the strict-ancestor walk below starts past the home bound entirely.
|
|
80
|
+
if (start === home) return hasIndex(start) ? start : null;
|
|
39
81
|
|
|
40
82
|
// Detect whether `start` is a STRAY nested index: walk STRICT ancestors up to
|
|
41
83
|
// the nearest `.git` root (project boundary), bounded at home. An indexed
|
|
42
84
|
// ancestor within that boundary means start's own index is a monorepo-subdir
|
|
43
85
|
// relic. Stop AT the git root — an index above it (e.g. `~/.code-graph`) is an
|
|
44
86
|
// unrelated outer project and must not poison this one.
|
|
45
|
-
let
|
|
87
|
+
let gitRoot = null;
|
|
46
88
|
let ancestorIndexed = false;
|
|
47
89
|
let dir = start;
|
|
48
90
|
for (;;) {
|
|
@@ -53,23 +95,33 @@ function resolveProjectRoot(startDir, opts = {}) {
|
|
|
53
95
|
if (parent === dir || parent === home) break;
|
|
54
96
|
dir = parent;
|
|
55
97
|
if (hasIndex(dir)) ancestorIndexed = true;
|
|
56
|
-
if (hasGit(dir)) {
|
|
98
|
+
if (hasGit(dir)) { gitRoot = dir; break; }
|
|
57
99
|
}
|
|
58
100
|
|
|
59
101
|
// start's own index wins unless it is stray (an indexed ancestor within the
|
|
60
102
|
// git boundary). start's own `.git` was already handled above.
|
|
61
103
|
if (hasIndex(start) && !ancestorIndexed) return start;
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
//
|
|
104
|
+
if (gitRoot && hasIndex(gitRoot)) return gitRoot;
|
|
105
|
+
|
|
106
|
+
// Nearest indexed ancestor strictly INSIDE the boundary (git root / home are
|
|
107
|
+
// stops, never candidates — see header). Covers the legit shape where only a
|
|
108
|
+
// sub-project of an unindexed repo was indexed (repo/packages/foo).
|
|
65
109
|
let d = hasIndex(start) ? path.dirname(start) : start;
|
|
66
110
|
for (;;) {
|
|
111
|
+
if (d === home || d === gitRoot) break;
|
|
67
112
|
if (hasIndex(d)) return d;
|
|
68
|
-
if (d === home) return null;
|
|
69
113
|
const parent = path.dirname(d);
|
|
70
|
-
if (parent === d)
|
|
114
|
+
if (parent === d) break;
|
|
71
115
|
d = parent;
|
|
72
116
|
}
|
|
117
|
+
|
|
118
|
+
// Nothing indexed inside the boundary. If that boundary is a linked worktree
|
|
119
|
+
// root, subdirs resolve like the root itself does: to the main checkout.
|
|
120
|
+
if (gitRoot) {
|
|
121
|
+
const main = worktreeMainRoot(gitRoot);
|
|
122
|
+
if (main && hasIndex(main)) return main;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
73
125
|
}
|
|
74
126
|
|
|
75
127
|
module.exports = { resolveProjectRoot };
|
|
@@ -316,7 +316,12 @@ function ensureIndexFresh() {
|
|
|
316
316
|
const bin = findBinary();
|
|
317
317
|
if (!bin) return 'skipped';
|
|
318
318
|
|
|
319
|
-
|
|
319
|
+
// Canonical index root, not the bare session cwd: a session launched in a
|
|
320
|
+
// linked worktree (resolves to the main checkout) or a subdir otherwise
|
|
321
|
+
// gate-fails here and freshness never runs (sibling of the statusline/hook
|
|
322
|
+
// subdir-cwd dark class).
|
|
323
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
324
|
+
const cwd = resolveProjectRoot(process.cwd()) || process.cwd();
|
|
320
325
|
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
321
326
|
if (!fs.existsSync(dbPath)) return 'skipped';
|
|
322
327
|
|
|
@@ -592,7 +597,8 @@ function runSessionInit({ source } = {}) {
|
|
|
592
597
|
*/
|
|
593
598
|
function injectProjectMap() {
|
|
594
599
|
try {
|
|
595
|
-
const
|
|
600
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
601
|
+
const cwd = resolveProjectRoot(process.cwd()) || process.cwd();
|
|
596
602
|
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
597
603
|
if (!fs.existsSync(dbPath)) return false;
|
|
598
604
|
|
|
@@ -636,7 +642,14 @@ function injectProjectMap() {
|
|
|
636
642
|
*/
|
|
637
643
|
function injectRecentImpact({ source } = {}) {
|
|
638
644
|
try {
|
|
639
|
-
|
|
645
|
+
// Index + telemetry live at the canonical root (worktree → main checkout,
|
|
646
|
+
// subdir → project root); git WIP detection stays in the SESSION dir — the
|
|
647
|
+
// worktree's branch state is what this session edits. Repo-relative git
|
|
648
|
+
// paths translate 1:1 to root-relative index paths (a worktree mirrors the
|
|
649
|
+
// checkout layout).
|
|
650
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
651
|
+
const sessionDir = process.cwd();
|
|
652
|
+
const cwd = resolveProjectRoot(sessionDir) || sessionDir;
|
|
640
653
|
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
641
654
|
if (!fs.existsSync(dbPath)) return false;
|
|
642
655
|
|
|
@@ -646,7 +659,7 @@ function injectRecentImpact({ source } = {}) {
|
|
|
646
659
|
// last commit. Timeouts tightened (finding #1): worst-case cap sum is now
|
|
647
660
|
// status(1s) + HEAD~1(1s) + affected(1.5s) = 3.5s, comfortably under the 5s
|
|
648
661
|
// SessionStart hook budget; the old 2+2+3=7s could get the whole hook killed.
|
|
649
|
-
const gitOpts = { cwd, timeout: 1000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
662
|
+
const gitOpts = { cwd: sessionDir, timeout: 1000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
650
663
|
let changed = [];
|
|
651
664
|
let isWip = false;
|
|
652
665
|
try {
|
|
@@ -431,8 +431,11 @@ function runMain() {
|
|
|
431
431
|
// Chinese chars are ~3 bytes but 1 char; "看看 fts5_search" is only 16 chars
|
|
432
432
|
if (!message || message.length < 8) return;
|
|
433
433
|
|
|
434
|
-
// --- Check index ---
|
|
435
|
-
|
|
434
|
+
// --- Check index --- (canonical root: worktree → main checkout, subdir →
|
|
435
|
+
// project root — the bare cwd gate left this hook dark there, sibling of the
|
|
436
|
+
// pre-*-guide subdir-cwd class)
|
|
437
|
+
const { resolveProjectRoot } = require('./project-root');
|
|
438
|
+
const cwd = resolveProjectRoot(process.cwd()) || process.cwd();
|
|
436
439
|
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
437
440
|
if (!fs.existsSync(dbPath)) return;
|
|
438
441
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.1",
|
|
4
4
|
"description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"node": ">=16"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
40
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
42
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
43
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
39
|
+
"@sdsrs/code-graph-linux-x64": "0.99.1",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.99.1",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.99.1",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.99.1",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.99.1"
|
|
44
44
|
}
|
|
45
45
|
}
|