@sdsrs/code-graph 0.95.1 → 0.97.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@ A high-performance code knowledge graph server implementing the [Model Context P
5
5
  ## Features
6
6
 
7
7
  - **Multi-language parsing** — Tree-sitter AST extraction across tiers of depth:
8
- - **Full** (calls + imports + inheritance + HTTP routes + test markers): TypeScript/TSX, JavaScript, Go, Python, Rust, Java
8
+ - **Full** (calls + imports + inheritance + test markers): TypeScript/TSX, JavaScript, Go, Python, Rust, Java. HTTP route extraction additionally covers TypeScript/TSX + JavaScript (Express/Connect), Go (`net/http`), and Python (Flask/FastAPI) only — Rust and Java web frameworks are not yet route-extracted
9
9
  - **Smoke-tested** (calls + imports + inheritance): C#, Kotlin, Ruby, PHP, Swift, Dart
10
10
  - **Limited** (functions + calls + `#include` imports + gtest test markers + C++ base-class inheritance; `Class::method` scope qualification deferred): C, C++
11
11
  - **Scripting**: Bash (functions + commands + `source`/`.` imports), Markdown (headings)
@@ -295,7 +295,7 @@ Available when installed as a Claude Code plugin:
295
295
  | `/status` | Show code-graph index status and embedding progress |
296
296
  | `/rebuild` | Force a full code-graph index rebuild |
297
297
 
298
- ## Supported Languages (16)
298
+ ## Supported Languages (19)
299
299
 
300
300
  | Language | Extensions | Relations Extracted |
301
301
  |----------|-----------|-------------------|
@@ -313,8 +313,11 @@ Available when installed as a Claude Code plugin:
313
313
  | Dart | .dart | calls, imports, inherits, implements |
314
314
  | C | .c, .h | calls, imports |
315
315
  | C++ | .cpp, .cc, .cxx, .hpp | calls, imports, inherits |
316
- | HTML | .html, .htm | structural parsing |
317
- | CSS | .css | structural parsing |
316
+ | Bash | .sh, .bash | functions, commands, `source`/`.` imports |
317
+ | Markdown | .md, .mdx, .markdown | headings |
318
+ | HTML | .html, .htm | file-FTS only (no AST symbols) |
319
+ | CSS | .css | file-FTS only (no AST symbols) |
320
+ | JSON | .json | file-FTS only (no AST symbols) |
318
321
 
319
322
  ## Team-shared graph snapshot
320
323
 
@@ -333,8 +336,32 @@ Skip the full local index for team members and CI runners by publishing a
333
336
  npx code-graph-mcp snapshot inspect ./code-graph-snapshot-<sha>.db.zst
334
337
  ```
335
338
 
336
- After setup, fresh clones automatically fetch the latest snapshot the
337
- first time the MCP server starts. No client-side configuration needed.
339
+ After setup, the auto-fetch is **opt-in per consumer**: an untrusted repo could
340
+ otherwise seed a misleading graph, so an unconfigured clone prints a hint and
341
+ skips the install. Enable it with one of the trust signals below. These live in
342
+ the *environment* (never in `.code-graph.toml`) so a committed/PR-injected config
343
+ file cannot set them.
344
+
345
+ | Environment variable | Effect |
346
+ |----------------------|--------|
347
+ | `CODE_GRAPH_SNAPSHOT_TRUST_ORIGIN=1` | Trust the auto-detected GitHub-release snapshot for this repo's `origin` remote, allowing auto-install. |
348
+ | `CODE_GRAPH_SNAPSHOT_PIN=<blake3 hex>` | Pin the expected artifact digest (64-char blake3 hex). When set it is the **sole** integrity authority — the download must match it, no network sidecar is consulted — and it also implicitly trusts the origin path. |
349
+ | `CODE_GRAPH_SNAPSHOT_TRUST_URL=1` | Honor a `.code-graph.toml [snapshot] url` override (an arbitrary, non-origin URL). Off by default because a committed URL could redirect the graph to an attacker-chosen database. |
350
+
351
+ Integrity **fail-closes**: with no pin set and no fetchable `<url>.blake3` sidecar,
352
+ install is refused rather than accepting unverified content. Set a pin, or have
353
+ the publisher serve the `.blake3` sidecar alongside the snapshot. (`file://`
354
+ sources, used in tests, are exempt.)
355
+
356
+ ## Offline / air-gapped usage
357
+
358
+ The optional embedding model (needed for vector semantic search) is downloaded
359
+ lazily on first use. To control that in a restricted environment:
360
+
361
+ | Environment variable | Effect |
362
+ |----------------------|--------|
363
+ | `CODE_GRAPH_MODEL_DIR=<dir>` | Load `model.safetensors` from `<dir>` instead of downloading (highest-priority lookup). If the file isn't there, it warns and falls back to the normal search paths. |
364
+ | `CODE_GRAPH_DISABLE_MODEL_DOWNLOAD=1` | Disable the automatic background model download. An already-cached model is still loaded and used; the server otherwise stays in FTS5-only mode. |
338
365
 
339
366
  ## Storage
340
367
 
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.95.1",
7
+ "version": "0.97.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -66,8 +66,11 @@ const VERB_STRIP = new RegExp(`^\\s*(?:env\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=\\S*\\
66
66
  // entities/migrations/tasks/jobs/workers/features/modules/api/web. Generic
67
67
  // terms like `core`/`utils`/`shared`/`common`/`types` deliberately omitted —
68
68
  // they appear in too many non-code contexts to be precise enough.
69
+ // v0.96 — added `skills` (Claude Code plugin / agent monorepos keep source
70
+ // under `skills/<name>/…`, e.g. `skills/moa/scripts/moa.py`); a grep there was
71
+ // invisible to the hook so it could never scope the answer to the real target.
69
72
  const SRC_PREFIXES =
70
- 'src|tests|lib|libs|scripts|claude-plugin|tools|pkg|cmd|internal|app|apps|components?|server|client|crates|packages|backend|frontend|services|models|domain|controllers|views|handlers|middleware|routes|repositories|entities|migrations|tasks|jobs|workers|features|modules|api|web';
73
+ 'src|tests|lib|libs|scripts|skills|claude-plugin|tools|pkg|cmd|internal|app|apps|components?|server|client|crates|packages|backend|frontend|services|models|domain|controllers|views|handlers|middleware|routes|repositories|entities|migrations|tasks|jobs|workers|features|modules|api|web';
71
74
  const SRC_PATH = new RegExp(`(?:^|\\s|["'])(${SRC_PREFIXES})/`);
72
75
  // Anchored variant for whole-token matching in extractSearchPath.
73
76
  const SRC_PATH_TOKEN = new RegExp(`^(?:\\./)?(${SRC_PREFIXES})/`);
@@ -88,6 +91,41 @@ const CONFIG_TARGET_ONLY = new RegExp(`(?:^|\\s)[^\\s|<>]*\\.(?:${NON_SOURCE_EXT
88
91
  // data-file tokens both match; global so every one is peeled before the SRC_PATH re-check.
89
92
  const CONFIG_TARGET_STRIP = new RegExp(`(?:^|\\s)[^\\s|<>]*\\.(?:${NON_SOURCE_EXTS})(?=\\s|$)`, 'gi');
90
93
 
94
+ // v0.96 — the grep's OWN args end at the first top-level shell separator
95
+ // (`;` `|` `&` `>` `<` newline). Everything after is a DIFFERENT command whose
96
+ // paths/flags/patterns must NOT be attributed to the grep. This closes a sibling
97
+ // hole: countNamedPaths stopped at the separator in v0.70, but the SRC_PATH gate
98
+ // in shouldHint, extractSearchPath, extractPatterns, and classifyBlock's flag
99
+ // checks all still scanned the WHOLE compound command. Real 2026-07-13 miss:
100
+ // `grep -n "VERSION" skills/moa/scripts/moa.py | head; …; python3 … scripts/bump-version.sh`
101
+ // — `skills/` was not an allowed prefix, so the gate/searchPath skipped the real
102
+ // target and latched onto `scripts/bump-version.sh` in the tail, then presented a
103
+ // confidently WRONG "already ran for you" answer for a file the user never grepped.
104
+ // Quote-aware (POSIX): a separator inside quotes is literal; inside DOUBLE quotes a
105
+ // backslash escapes the next char (so `\"` does not close) — mirrors
106
+ // splitTopLevelSegments so both share one notion of "quote-terminating vs escaped".
107
+ function firstShellClause(cmd) {
108
+ if (!cmd || typeof cmd !== 'string') return cmd;
109
+ let quote = null;
110
+ for (let i = 0; i < cmd.length; i++) {
111
+ const c = cmd[i];
112
+ if (quote) {
113
+ if (quote === '"' && c === '\\' && i + 1 < cmd.length) { i++; continue; }
114
+ if (c === quote) quote = null;
115
+ continue;
116
+ }
117
+ if (c === '"' || c === "'") { quote = c; continue; }
118
+ // Control operators END the grep's argument list → truncate. NOT redirects
119
+ // (`>` `<`): `2>&1`, `>out`, and process substitution `-f <(cat pats) src/`
120
+ // all keep grep path args AFTER them, so a redirect is not a boundary. NOT a
121
+ // single background `&` either (it collides with `2>&1`/`&>` and a
122
+ // backgrounded grep's args still precede it). `&&`/`||` DO terminate.
123
+ if (c === ';' || c === '|' || c === '\n') return cmd.slice(0, i);
124
+ if (c === '&' && cmd[i + 1] === '&') return cmd.slice(0, i);
125
+ }
126
+ return cmd;
127
+ }
128
+
91
129
  // v0.71 — `git grep --cached`/`--staged` searches the STAGED index, and a treeish
92
130
  // ref (`git grep "X" HEAD~3 -- src/`, `git grep "X" main -- src/`) searches another
93
131
  // commit/branch — a scope the working-tree inline answer (`code-graph-mcp grep`)
@@ -123,10 +161,13 @@ function shouldHint(cmd) {
123
161
  if (PIPE_INTO_GREP.test(cmd)) return false; // `cargo test | grep FAILED` is output filter
124
162
  if (!GREP_HEAD.test(cmd)) return false; // not a search command
125
163
  if (isRevisionScopedGitGrep(cmd)) return false; // v0.71 — git grep --cached/treeish: scope cg can't honor
126
- if (!SRC_PATH.test(cmd)) return false; // not against indexed source tree
164
+ // v0.96 the source-path gate must see ONLY the grep's own args, not a path in
165
+ // a non-grep tail (`grep X skills/a.py; wc scripts/b` must not fire on scripts/b).
166
+ const clause = firstShellClause(cmd);
167
+ if (!SRC_PATH.test(clause)) return false; // not against indexed source tree
127
168
  // If a config file appears AND no source path remains after stripping it, skip.
128
- if (CONFIG_TARGET_ONLY.test(cmd)) {
129
- const stripped = cmd.replace(CONFIG_TARGET_STRIP, ' ');
169
+ if (CONFIG_TARGET_ONLY.test(clause)) {
170
+ const stripped = clause.replace(CONFIG_TARGET_STRIP, ' ');
130
171
  if (!SRC_PATH.test(stripped)) return false;
131
172
  }
132
173
  return true;
@@ -164,8 +205,10 @@ const MARKER_ONLY =
164
205
  // symbol-shaped target".
165
206
  function extractPatterns(cmd) {
166
207
  if (!cmd || typeof cmd !== 'string') return [];
208
+ // v0.96 — only the grep's own clause; a quoted string in a compound tail
209
+ // (`; echo "SomeWord"`) is not a grep pattern and must not be screened.
167
210
  // Strip leading verb + env/assignment prefix (kept in sync with GREP_HEAD)
168
- const stripped = cmd.replace(VERB_STRIP, '');
211
+ const stripped = firstShellClause(cmd).replace(VERB_STRIP, '');
169
212
  // Collect every quoted argument — first one is the pattern in standard grep
170
213
  // usage; subsequent ones (e.g. `-e "second"`) are also patterns or filter
171
214
  // expressions and worth screening too.
@@ -193,12 +236,16 @@ function extractDeclSymbols(patterns) {
193
236
  /// null — hint tier (marker scans, unquoted, unanswerable flags)
194
237
  function classifyBlock(cmd) {
195
238
  if (!shouldHint(cmd)) return null; // narrower than hint
196
- if (UNANSWERABLE_FLAGS.test(cmd)) return null; // intent the answer can't honor
197
- if (MARKER_ONLY.test(cmd)) return null; // bare TODO/FIXMEno cg equivalent
198
- const patterns = extractPatterns(cmd);
239
+ // v0.96 every flag/pattern check below must see the grep's OWN clause, not a
240
+ // tail command's flags (`grep X src/a.py; grep -v Y src/b.py`the tail's -v
241
+ // must not disqualify the answerable head grep).
242
+ const clause = firstShellClause(cmd);
243
+ if (UNANSWERABLE_FLAGS.test(clause)) return null; // intent the answer can't honor
244
+ if (MARKER_ONLY.test(clause)) return null; // bare TODO/FIXME — no cg equivalent
245
+ const patterns = extractPatterns(clause);
199
246
  if (patterns.length === 0) return null; // unquoted pattern — conservative, hint
200
247
  if (!patterns.some(p => IDENTIFIER_LIKE.test(p))) return null;
201
- if (CONTEXT_FLAG.test(cmd)) {
248
+ if (CONTEXT_FLAG.test(clause)) {
202
249
  const symbols = extractDeclSymbols(patterns);
203
250
  if (symbols.length === 0) return null; // context read without named decls
204
251
  return { mode: 'show', symbols: symbols.slice(0, 3) };
@@ -208,7 +255,7 @@ function classifyBlock(cmd) {
208
255
  // first-path-only answer (the rest silently dropped) — an incomplete substitute that
209
256
  // rationally teaches CODE_GRAPH_NO_BLOCK_GREP bypass. Downgrade to hint: the model's complete
210
257
  // grep runs and the hint still nudges. (show mode above is symbol-scoped, not path → unaffected.)
211
- if (countNamedPaths(cmd, patterns) >= 2) return null;
258
+ if (countNamedPaths(clause, patterns) >= 2) return null;
212
259
  return { mode: 'grep' };
213
260
  }
214
261
 
@@ -308,6 +355,11 @@ function extractUnansweredTail(cmd) {
308
355
  for (let i = 0; i < cmd.length; i++) {
309
356
  const c = cmd[i];
310
357
  if (quote) {
358
+ // v0.96 — same POSIX escape rule the rest of the quote-parser family uses
359
+ // (firstShellClause / splitTopLevelSegments): inside DOUBLE quotes `\"` does
360
+ // not close, so a `;`/`&&` inside a `grep "a\";b" …` pattern stays literal
361
+ // and the re-issue NOTE isn't garbled by splitting mid-pattern.
362
+ if (quote === '"' && c === '\\' && i + 1 < cmd.length) { i++; continue; }
311
363
  if (c === quote) quote = null;
312
364
  continue;
313
365
  }
@@ -337,7 +389,10 @@ function appendUnansweredTailNote(lines, tail) {
337
389
  // the inline answer can scope its search the same way the raw grep would have.
338
390
  function extractSearchPath(cmd) {
339
391
  if (!cmd || typeof cmd !== 'string') return undefined;
340
- for (const raw of cmd.split(/\s+/)) {
392
+ // v0.96 scope to the grep's own clause so the answer is never scoped to a
393
+ // path in a non-grep tail (the file the user actually grepped is the only one
394
+ // the "already ran for you" answer may claim to have searched).
395
+ for (const raw of firstShellClause(cmd).split(/\s+/)) {
341
396
  const token = raw.replace(/^["']|["']$/g, '');
342
397
  if (!token || token.startsWith('-')) continue;
343
398
  if (token.includes('..')) return undefined; // traversal — don't scope, don't guess
@@ -356,17 +411,12 @@ function extractSearchPath(cmd) {
356
411
  function countNamedPaths(cmd, patterns) {
357
412
  if (!cmd || typeof cmd !== 'string') return 0;
358
413
  const pats = new Set(patterns || []);
359
- // Only the grep's OWN path args count. Stop at the first top-level command separator so a
360
- // path in a compound tail (`grep X src/a.py | sed … src/b.py`) is NOT mistaken for a second
361
- // grep target that would wrongly downgrade a complete single-file grep to a hint.
362
- let seg = cmd.replace(VERB_STRIP, '');
363
- let quote = null;
364
- for (let i = 0; i < seg.length; i++) {
365
- const c = seg[i];
366
- if (quote) { if (c === quote) quote = null; continue; }
367
- if (c === '"' || c === "'") { quote = c; continue; }
368
- if (c === ';' || c === '|' || c === '&' || c === '>' || c === '<' || c === '\n') { seg = seg.slice(0, i); break; }
369
- }
414
+ // Only the grep's OWN path args count. firstShellClause stops at the first
415
+ // top-level separator so a path in a compound tail (`grep X src/a.py | sed …
416
+ // src/b.py`) is NOT mistaken for a second grep target that would wrongly
417
+ // downgrade a complete single-file grep to a hint. (v0.96 — was an inline scan;
418
+ // now shares the ONE clause definition with shouldHint/extractSearchPath.)
419
+ const seg = firstShellClause(cmd).replace(VERB_STRIP, '');
370
420
  let n = 0;
371
421
  for (const raw of seg.split(/\s+/)) {
372
422
  const tok = raw.replace(/^["']|["']$/g, '');
@@ -776,6 +826,7 @@ module.exports = {
776
826
  shouldBlock,
777
827
  classifyBlock, // v0.49 — intent-aware block tiers
778
828
  splitTopLevelSegments, // compound-grep — quote-aware top-level segment splitter (PostToolUse reuse)
829
+ firstShellClause, // v0.96 — grep's own clause (up to first top-level separator)
779
830
  extractDeclSymbols, // v0.49 — show-mode symbol extraction
780
831
  translateBreToRg, // v0.49 — BRE→rust-regex dialect bridge
781
832
  buildShowDenyReason, // v0.49 — show-mode deny copy
@@ -119,10 +119,10 @@ code-graph-mcp ast-search "q" --type fn # 结构化筛选
119
119
  code-graph-mcp map # 项目架构
120
120
  code-graph-mcp overview src/mcp/ # 模块总览
121
121
  code-graph-mcp callgraph SYMBOL # 调用图
122
- code-graph-mcp impact SYMBOL # 影响面
122
+ code-graph-mcp impact SYMBOL # 影响面(--change-type ∈ signature|behavior|remove,默认 behavior)
123
123
  code-graph-mcp show SYMBOL # 节点详情
124
- code-graph-mcp refs SYMBOL --relation calls # 引用筛选
125
- code-graph-mcp refs SYMBOL --min-confidence extracted # 只看精确边(滤跨文件裸名 inferred/ambiguous)
124
+ code-graph-mcp refs SYMBOL --relation calls # --relation ∈ calls|imports|inherits|implements|references|all
125
+ code-graph-mcp refs SYMBOL --min-confidence extracted # extracted|inferred|ambiguous;extracted=只看精确边(callgraph/impact/trace 同款)
126
126
  code-graph-mcp centrality # 架构咽喉(betweenness 桥节点;补 map 的 caller_count)
127
127
  code-graph-mcp cycles # 循环导入依赖(文件级 import 环 / SCC)
128
128
  code-graph-mcp surprising # 可疑跨模块耦合(低置信 + 跨模块 + sole-bridge 打分)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.95.1",
3
+ "version": "0.97.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.95.1",
40
- "@sdsrs/code-graph-linux-arm64": "0.95.1",
41
- "@sdsrs/code-graph-darwin-x64": "0.95.1",
42
- "@sdsrs/code-graph-darwin-arm64": "0.95.1",
43
- "@sdsrs/code-graph-win32-x64": "0.95.1"
39
+ "@sdsrs/code-graph-linux-x64": "0.97.0",
40
+ "@sdsrs/code-graph-linux-arm64": "0.97.0",
41
+ "@sdsrs/code-graph-darwin-x64": "0.97.0",
42
+ "@sdsrs/code-graph-darwin-arm64": "0.97.0",
43
+ "@sdsrs/code-graph-win32-x64": "0.97.0"
44
44
  }
45
45
  }