@sdsrs/code-graph 0.115.0 → 0.117.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.
@@ -14,7 +14,7 @@ const { cgTmpDir, cwdHash } = require('./tmp-dir');
14
14
  const { resolveProjectRoot } = require('./project-root');
15
15
  const { recordRecommendation } = require('./recommendation-log');
16
16
  const { formatCoveringTests } = require('./covering-tests');
17
- const { emitPreToolAllowContext } = require('./hook-emit');
17
+ const { emitPreToolContext } = require('./hook-emit');
18
18
  const { hidden } = require('./proc-opts');
19
19
 
20
20
  // v0.49 — walk up from the shell cwd (subdir-cwd fix). The per-cwd index.db
@@ -217,10 +217,18 @@ summary += formatCoveringTests(jsonResult.test_callers, editedFile);
217
217
  // fire with the count alone — the verdict must stay coherent either way.
218
218
  summary += ` → Before this edit: confirm each caller of ${symbol}() still holds with your change, or note why it is unaffected.\n`;
219
219
 
220
- // Compound-grep sibling sweep: deliver via the PreToolUse allow+additionalContext
221
- // envelope (shared hook-emit.js). Bare stdout on a PreToolUse exit-0 lands in the
222
- // debug log only and never reaches the model (CC docs v2026-06); additionalContext
223
- // is what actually surfaces the impact summary. Impact must stay PRE-edit (so the
224
- // reconciliation happens before the change), hence allow + additionalContext, not
225
- // a PostToolUse inject.
226
- process.stdout.write(emitPreToolAllowContext(summary) + '\n');
220
+ // Deliver via the PERMISSION-NEUTRAL PreToolUse additionalContext envelope
221
+ // (shared hook-emit.js). Bare stdout on a PreToolUse exit-0 lands in the debug
222
+ // log only and never reaches the model (CC docs v2026-06); additionalContext is
223
+ // what surfaces the impact summary, and it is delivered without any
224
+ // permissionDecision the tool's normal permission flow is untouched.
225
+ //
226
+ // It used to send `permissionDecision: 'allow'` alongside it. That is documented
227
+ // as "skip the interactive permission prompt", so on a machine that prompts for
228
+ // Edit this hook silently answered that prompt for the user, for every symbol
229
+ // with >=1 caller outside the 2-minute cooldown (audit 2026-08-16 P0-2). Context
230
+ // delivery is never worth a write consent: if a future CC requires a decision to
231
+ // carry additionalContext, this summary goes quiet rather than elevating again.
232
+ // Impact must stay PRE-edit (the reconciliation happens before the change), so a
233
+ // PostToolUse inject is not an alternative here.
234
+ process.stdout.write(emitPreToolContext(summary) + '\n');
@@ -16,6 +16,21 @@
16
16
  * ALLOCATING a console — inherited stdio handles still work, so an interactive
17
17
  * `doctor` run in a real terminal is unaffected. No-op on non-Windows.
18
18
  *
19
+ * `killSignal` is deliberately NOT defaulted here. Node's `timeout` option
20
+ * sends SIGTERM and then WAITS, so a child that traps SIGTERM makes the
21
+ * timeout unreachable (audit 2026-08-16 P1-17: one deaf third-party statusline
22
+ * provider blanked the status line on every frame). The two statusline call
23
+ * sites pass `killSignal: 'SIGKILL'` themselves — they run UNTRUSTED provider
24
+ * commands / a possibly-wedged binary on the render hot path, and nothing
25
+ * there shuts down gracefully at timeout anyway. It is not a global default
26
+ * because our other timed children DO need SIGTERM's grace: a timed-out
27
+ * `git pull` hard-killed mid-write leaves `.git/index.lock` behind and every
28
+ * later marketplace refresh then fails silently; npm has equivalent lock
29
+ * files (batch review of the P1-17 fix). New call sites that run untrusted or
30
+ * hang-prone children with a timeout should opt in the same way.
31
+ * (Caveat SIGKILL does not fix: it reaches the direct child only. A grandchild
32
+ * holding the same stdout pipe can still stall a *Sync call until it exits.)
33
+ *
19
34
  * Every child_process call site under claude-plugin/scripts/ must route through
20
35
  * here (or set windowsHide itself); `windows-hide.test.js` fails the build on a
21
36
  * new call site that doesn't.
@@ -609,7 +609,36 @@ function runSessionInit({ source } = {}) {
609
609
  // 上下文感知默认:插件模式下首次 SessionStart 自动安装(创建/注入 CLAUDE.md 块 +
610
610
  // .claude/ detail 文件),并清理旧 memory-dir 制品(升级自动迁移)。shipped 漂移
611
611
  // 时刷新。三种情况发一次 stderr 提示,让用户知道发生了什么 + 如何回退。
612
- const autoAdopt = isRelic ? { attempted: false, result: null } : maybeAutoAdopt({ scriptPath: __dirname });
612
+ // Adoption is OPTIONAL; the rest of this hook is not. It touches files the
613
+ // user owns (CLAUDE.md, .claude/) which can be unreadable, a directory, or on
614
+ // a read-only mount — and a throw here used to abort every remaining step
615
+ // (map injection, recent impact, consistency check, both hook canaries) with a
616
+ // raw stack trace (audit 2026-08-16 P1-16). adopt() now returns reasons rather
617
+ // than throwing; this is the belt to that suspenders, so a future unguarded
618
+ // read inside it cannot take the session down again.
619
+ let autoAdopt = { attempted: false, result: null };
620
+ if (!isRelic) {
621
+ try {
622
+ autoAdopt = maybeAutoAdopt({ scriptPath: __dirname });
623
+ } catch (e) {
624
+ autoAdopt = { attempted: true, reason: 'threw', result: null, error: (e && e.message) || String(e) };
625
+ process.stderr.write(
626
+ `[code-graph] Skipped CLAUDE.md adoption for this project (${(e && e.code) || (e && e.message) || 'unknown error'}).\n` +
627
+ ' Everything else in this session start continues normally.\n'
628
+ );
629
+ }
630
+ }
631
+ if (autoAdopt.result && autoAdopt.result.ok === false &&
632
+ (autoAdopt.result.reason === 'claude-md-unreadable' || autoAdopt.result.reason === 'claude-md-unwritable' ||
633
+ autoAdopt.result.reason === 'detail-unwritable')) {
634
+ // A refusal is not a silent no-op: the user's steering block is NOT
635
+ // installed/refreshed, and only this line says so.
636
+ process.stderr.write(
637
+ `[code-graph] Could not install the CLAUDE.md steering block (${autoAdopt.result.reason}: ` +
638
+ `${autoAdopt.result.error || 'unknown'}). Nothing was changed.\n` +
639
+ ' Opt out permanently: CODE_GRAPH_NO_AUTO_ADOPT=1\n'
640
+ );
641
+ }
613
642
  const migrated = autoAdopt.migrated || {};
614
643
  if (migrated.memoryIndexPruned || migrated.legacyDetailRemoved) {
615
644
  process.stderr.write(
@@ -889,5 +918,18 @@ if (require.main === module) {
889
918
  source = JSON.parse(fs.readFileSync(0, 'utf8')).source;
890
919
  }
891
920
  } catch { /* no/garbled stdin → treat as unknown source */ }
892
- runSessionInit({ source });
921
+ // Hooks FAIL OPEN. This one had no wrapper at all, so anything unhandled
922
+ // anywhere in the sequence surfaced as a node stack trace plus a non-zero exit
923
+ // in the user's session start — for work that is entirely optional
924
+ // housekeeping (audit 2026-08-16 P1-16). One `[code-graph]` line, exit 0.
925
+ try {
926
+ runSessionInit({ source });
927
+ } catch (e) {
928
+ process.stderr.write(
929
+ `[code-graph] SessionStart hook error (${(e && e.code) || (e && e.name) || 'Error'}): ` +
930
+ `${(e && e.message) || String(e)}\n` +
931
+ ' The session continues; run `code-graph-mcp doctor` if this repeats.\n'
932
+ );
933
+ process.exit(0);
934
+ }
893
935
  }
@@ -16,7 +16,21 @@
16
16
  // (this plugin's own provider). Third parties should use stable ids like
17
17
  // "gsd", "claude-mem", etc.
18
18
 
19
- const { readRegistry, registerStatuslineProvider, unregisterStatuslineProvider } = require('./lifecycle');
19
+ const {
20
+ readRegistry, readRegistryForWrite, registerStatuslineProvider, unregisterStatuslineProvider,
21
+ } = require('./lifecycle');
22
+
23
+ // A registry that EXISTS but cannot be read is a refusal, not a no-op: the
24
+ // mutation functions leave it alone (they must — it holds the user's previous
25
+ // statusline and other plugins' entries), which would otherwise surface here as
26
+ // the cheerful "unchanged <id>" / "not-found <id>" at exit 0. A third-party
27
+ // installer reading that exit code would believe it is wired in.
28
+ function bailIfRegistryUnusable(action) {
29
+ const { refuse, why } = readRegistryForWrite();
30
+ if (!refuse) return;
31
+ process.stderr.write(`error: ${why} — nothing ${action}. Repair or move that file aside and retry.\n`);
32
+ process.exit(2);
33
+ }
20
34
 
21
35
  function usage(code = 1) {
22
36
  process.stderr.write(
@@ -34,12 +48,14 @@ function runRegister(id, command, needsStdin) {
34
48
  process.exit(2);
35
49
  }
36
50
  if (!id || !command) usage();
51
+ bailIfRegistryUnusable('registered');
37
52
  const changed = registerStatuslineProvider(id, command, needsStdin);
38
53
  process.stdout.write(changed ? `registered ${id}\n` : `unchanged ${id}\n`);
39
54
  }
40
55
 
41
56
  function runUnregister(id) {
42
57
  if (!id) usage();
58
+ bailIfRegistryUnusable('unregistered');
43
59
  const changed = unregisterStatuslineProvider(id);
44
60
  process.stdout.write(changed ? `unregistered ${id}\n` : `not-found ${id}\n`);
45
61
  }
@@ -89,6 +89,9 @@ function runProvider(command, needsStdin, stdin) {
89
89
 
90
90
  const out = execFileSync(argv[0], argv.slice(1), hidden({
91
91
  timeout: 3000,
92
+ // SIGKILL, not the SIGTERM default: a provider that traps SIGTERM makes
93
+ // Node's timeout unreachable and hangs every render (audit P1-17).
94
+ killSignal: 'SIGKILL',
92
95
  stdio: ['pipe', 'pipe', 'pipe'],
93
96
  input: needsStdin ? stdin : '',
94
97
  env,
@@ -187,6 +187,9 @@ try {
187
187
  // "slow health-check" into a rendered "offline"/"updating" instead of a blank.
188
188
  report = parseReport(execFileSync(bin, ['health-check', '--format', 'json'], hidden({
189
189
  timeout: 1500,
190
+ // Render hot path: a wedged binary ignoring SIGTERM must not outlive the
191
+ // budget (same reasoning as the composite's provider spawn, audit P1-17).
192
+ killSignal: 'SIGKILL',
190
193
  stdio: ['pipe', 'pipe', 'pipe'],
191
194
  // Run the binary FROM the resolved root so its own project-root resolution
192
195
  // lands on the same DB the gate above picked (a subdir cwd would otherwise
@@ -22,15 +22,20 @@ jobs:
22
22
  permissions:
23
23
  contents: write
24
24
  steps:
25
- - uses: actions/checkout@v4
25
+ # SHA-pinned, not `@v4`: a floating major tag is mutable, so anyone who can
26
+ # move it runs their code in a job that holds `contents: write` on your
27
+ # repo. These are the same commits code-graph-mcp's own workflows pin.
28
+ # Update by copying a new SHA from the action's release page, not by
29
+ # switching back to a tag.
30
+ - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
26
31
  with:
27
32
  fetch-depth: 0
28
- - uses: actions/setup-node@v4
33
+ - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
29
34
  with:
30
35
  node-version: '20'
31
36
  - name: Build snapshot
32
37
  run: |
33
- npx -y -p @sdsrs/code-graph@0.115.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.117.0 code-graph-mcp snapshot create --out snapshot.db
34
39
  zstd -9 snapshot.db -o snapshot.db.zst
35
40
  mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
36
41
  - name: Upload to release
@@ -35,8 +35,12 @@ type: reference
35
35
  > - `module_overview include_deps=true` / `include_dead=true`
36
36
  > - `get_call_graph route_path="GET /api/x"`
37
37
  >
38
- > 旧名(`impact_analysis` 等)仍作为向后兼容 dispatcher 别名可调(raw JSON-RPC /
39
- > SDK 脚本场景),但 Claude Code 内强烈建议用上面的新 flag 形式。CLI 子命令
38
+ > `impact_analysis` **已删除**——dispatcher 里没有这个 arm,按名调用返回
39
+ > `Unknown tool`。改用 `get_ast_node include_impact=true`,或 CLI `impact`。
40
+ > 其余旧名(`read_snippet` / `dependency_graph` / `find_similar_code` /
41
+ > `find_dead_code` / `trace_http_chain` + alias `find_http_route`)仍是向后兼容
42
+ > dispatcher 别名(raw JSON-RPC / SDK 脚本场景),但 Claude Code 内一律用上面的
43
+ > 新 flag 形式。CLI 子命令
40
44
  > (`code-graph-mcp impact|similar|deps|dead-code|trace`)保持不变,给 Bash 工作流。
41
45
 
42
46
  ## 何时调用 MCP/CLI(替代多步 Grep/Read)
@@ -73,10 +77,14 @@ type: reference
73
77
  ### 旧名兼容 + CLI 速查(v0.18.4 fold 后)
74
78
 
75
79
  v0.18.4 把原"进阶 5"折叠进核心 7 的 flag。**Claude Code 内**首选上表的新 flag 形式。
76
- 旧名(`impact_analysis` / `find_similar_code` / `dependency_graph` / `find_dead_code` /
77
- `trace_http_chain` + alias `find_http_route`)作为 dispatcher 向后兼容**仍可调用**
78
- (raw JSON-RPC / MCP SDK / 既有脚本不破),但 Claude Code 的 ToolSearch 仍然不为
79
- hidden 5 加载 schema —— 实操中走新 flag。CLI 子命令保持原样:
80
+
81
+ 可按名调用的只有这 7 个:`project_map` / `semantic_code_search` / `get_call_graph` /
82
+ `get_ast_node` / `module_overview` / `ast_search` / `find_references`。
83
+ `impact_analysis` **不在其列,已从 dispatcher 删除**——按名调用返回 `Unknown tool`。
84
+ 隐藏旧名 `read_snippet` / `dependency_graph` / `find_similar_code` / `find_dead_code` /
85
+ `trace_http_chain`(+ alias `find_http_route`)仍有 dispatcher arm,raw JSON-RPC / MCP
86
+ SDK / 既有脚本不破;但它们不在 `tools/list`,Claude Code 的 ToolSearch 不为其加载
87
+ schema —— 实操中一律走新 flag。CLI 子命令保持原样:
80
88
 
81
89
  | 意图 | CLI(Bash 工作流) | 等价 MCP 新形式 |
82
90
  |------|--------------------|------------------|
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.115.0",
3
+ "version": "0.117.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": {
@@ -35,10 +35,10 @@
35
35
  "node": ">=16"
36
36
  },
37
37
  "optionalDependencies": {
38
- "@sdsrs/code-graph-linux-x64": "0.115.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.115.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.115.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.115.0",
42
- "@sdsrs/code-graph-win32-x64": "0.115.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.117.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.117.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.117.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.117.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.117.0"
43
43
  }
44
44
  }