@sdsrs/code-graph 0.98.1 → 0.99.0
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.
|
@@ -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.0",
|
|
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.0",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.99.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.99.0",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.99.0",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.99.0"
|
|
44
44
|
}
|
|
45
45
|
}
|