@sdsrs/code-graph 0.140.0 → 0.142.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.140.0",
7
+ "version": "0.142.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -71,6 +71,21 @@ function writeFileAtomic(filePath, data, { followLink = false } = {}) {
71
71
  // the full table at .claude/plugin_code_graph_mcp.md (opened on demand, never
72
72
  // auto-loaded). Project-type tailoring swaps a couple of rows (web → HTTP-route
73
73
  // tracing; frontend → reference audits) — body of the detail doc is unchanged.
74
+ //
75
+ // Every row spends the bare name `code-graph-mcp`, and a plugin-only install has
76
+ // it nowhere on PATH: findBinary() resolves the plugin's own download at
77
+ // ~/.cache/code-graph/bin (auto-update.js `cachedBinaryPath`), and the global-npm
78
+ // tiers below it exist only for users who ran `npm i -g` themselves. Issue #41 is
79
+ // what that reads like from the other side — a table of commands the user's shell
80
+ // answers with "command not found". Hence the fallback line below the table.
81
+ //
82
+ // It names a path SHAPE, never a resolved one. This file goes into a git-tracked
83
+ // CLAUDE.md (the adopt output says so two screens down), so a resolved
84
+ // /home/<user>/… is right on one machine and wrong on every teammate's — and
85
+ // buildBlock's byte-determinism is exactly what needsRefresh diffs, so
86
+ // machine-varying content would rewrite the block on every clone's next
87
+ // SessionStart. No `.exe` variant for the same reason it is safe not to have one:
88
+ // platformGuard() refuses adopt on win32, so this block never exists there.
74
89
  const BLOCK_HEADING = '## Code Graph (repo-wide AST index)';
75
90
 
76
91
  function buildTriggerRows(projectType = 'generic') {
@@ -116,6 +131,9 @@ function buildBlock(projectType = 'generic') {
116
131
  '',
117
132
  table,
118
133
  '',
134
+ 'Not on PATH? A plugin-only install keeps its own copy — same commands, run',
135
+ '`~/.cache/code-graph/bin/code-graph-mcp` (or `npm i -g @sdsrs/code-graph` once).',
136
+ '',
119
137
  "Still use Grep for literal strings/regex in non-code files; still Read files you'll edit.",
120
138
  'Full command + MCP-tool table: `.claude/plugin_code_graph_mcp.md`',
121
139
  ].join('\n');
@@ -826,6 +844,48 @@ function unadopt({ cwd, home } = {}) {
826
844
  };
827
845
  }
828
846
 
847
+ /**
848
+ * The `unadopt` invocation to hand the user, spelled so their shell can run it.
849
+ *
850
+ * Issue #41: the reporter drives this plugin through `/plugin install` only and
851
+ * has no `code-graph-mcp` on PATH, so every remedy printed with the bare name is
852
+ * answered by their shell with "command not found".
853
+ *
854
+ * The obvious repair — print the binary `verifyBinary()` resolved — is WRONG,
855
+ * and a pre-ship review caught it as a net regression. `unadopt` is one of the
856
+ * three JS-dispatched subcommands (`main.rs`: doctor / adopt / unadopt): the
857
+ * native binary re-execs this file, which it locates via `$_FIND_BINARY_ROOT`
858
+ * or a path relative to its own exe. The plugin's cached binary at
859
+ * ~/.cache/code-graph/bin has no such neighbour, so `<cache>/code-graph-mcp
860
+ * unadopt` exits 1 with "adopt.js not found" — reproduced. Worse, npm-global
861
+ * users reach this file today through the `bin/cli.js` shim that sets
862
+ * `_FIND_BINARY_ROOT`, so printing a resolved platform-package binary would
863
+ * BREAK a path that currently works.
864
+ *
865
+ * So point at this script. It carries its own `require.main === module` arm, and
866
+ * `__filename` is the one locator that cannot be wrong in any install layout —
867
+ * plugin cache, npm package, dev checkout. Quoted, since a plugin cache path
868
+ * under a home directory with a space is otherwise not runnable as printed.
869
+ *
870
+ * Lives here rather than in session-init.js (0.141.0's home for it) because
871
+ * BOTH printers that hand a user this command must agree, and the other one is
872
+ * `formatResult` directly below — the site 0.141.0's sweep missed.
873
+ */
874
+ function shellQuote(value) {
875
+ // SINGLE quotes, not JSON.stringify. `JSON.stringify` emits DOUBLE quotes, and
876
+ // a POSIX shell still expands `$`, backticks and `$(...)` inside those — so a
877
+ // path holding any of them would be rewritten, or executed, when the user
878
+ // pastes the line we printed. Single quotes are literal for everything except
879
+ // `'` itself, which is closed, escaped and reopened. POSIX-only is fine here:
880
+ // `platformGuard()` refuses adopt on win32, so this string never targets cmd
881
+ // or PowerShell. (CodeRabbit on PR #46; inherited from the 0.141.0 spelling.)
882
+ return `'${value.replace(/'/g, "'\\''")}'`;
883
+ }
884
+
885
+ function unadoptCommand() {
886
+ return `node ${shellQuote(__filename)} unadopt`;
887
+ }
888
+
829
889
  function formatResult(action, result) {
830
890
  if (!result.ok && result.reason === 'windows-not-supported') {
831
891
  return '[code-graph] adopt/unadopt are POSIX-only on this build. ' +
@@ -863,7 +923,7 @@ function formatResult(action, result) {
863
923
  lines.push(`[code-graph] Detail table → ${result.detailPath}`);
864
924
  lines.push('[code-graph] CLAUDE.md is git-tracked — commit the block, or gitignore');
865
925
  lines.push('[code-graph] .claude/plugin_code_graph_mcp.md (a generated copy) as you prefer.');
866
- lines.push('[code-graph] Reverse: code-graph-mcp unadopt');
926
+ lines.push(`[code-graph] Reverse: ${unadoptCommand()}`);
867
927
  lines.push('[code-graph] Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env');
868
928
  return lines.join('\n');
869
929
  }
@@ -891,14 +951,32 @@ function formatResult(action, result) {
891
951
  }
892
952
 
893
953
  if (require.main === module) {
894
- const action = process.argv[2] === 'unadopt' ? 'unadopt' : 'adopt';
954
+ // Strict, because SessionStart now hands users this entry point directly
955
+ // (`node <…>/adopt.js unadopt`) rather than the binary, which validates its
956
+ // arguments in main.rs before dispatching here. The old `argv[2] === 'unadopt'
957
+ // ? … : 'adopt'` fell back to ADOPT for anything else, so `--help` performed
958
+ // an unadopt where the binary prints help, and a mistyped verb silently
959
+ // adopted the current project — the footgun main.rs's `reject_extra_args`
960
+ // exists to prevent.
961
+ //
962
+ // Both binary call sites stay valid: main.rs passes `["unadopt"]` for unadopt
963
+ // and no arguments at all for adopt.
964
+ const argv = process.argv.slice(2);
965
+ const action = argv.length === 0 ? 'adopt' : argv[0];
966
+ if (argv.length > 1 || (action !== 'adopt' && action !== 'unadopt')) {
967
+ process.stderr.write(
968
+ `[code-graph] Usage: node ${path.basename(__filename)} [adopt|unadopt]\n` +
969
+ ' Exactly one verb and no flags. No argument means `adopt`.\n' +
970
+ ' Refusing rather than guessing: both verbs write to your CLAUDE.md.\n');
971
+ process.exit(2);
972
+ }
895
973
  const result = action === 'unadopt' ? unadopt() : adopt();
896
974
  process.stdout.write(formatResult(action, result) + '\n');
897
975
  process.exit(result.ok === false ? 1 : 0);
898
976
  }
899
977
 
900
978
  module.exports = {
901
- adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
979
+ adopt, unadopt, memoryDir, formatResult, unadoptCommand, shellQuote, stripSentinelBlock,
902
980
  readAdoptedProjects, readAdoptedResult, recordAdopted, removeAdopted, adoptedRegistryFile,
903
981
  isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
904
982
  detectProjectType, buildBlock, buildTriggerRows, migrateLegacyMemoryDir,
@@ -12,6 +12,24 @@ const { execFileSync } = require('child_process');
12
12
  const { findBinary } = require('./find-binary');
13
13
  const { hidden } = require('./proc-opts');
14
14
 
15
+ // Default allowance for this hook's one child, spent through the hook budget
16
+ // below. The PostToolUse deadline armed at the top of this file is 10 s.
17
+ const CHILD_TIMEOUT_MS = 8000;
18
+
19
+ /**
20
+ * How long the indexer child may run: the default when nothing armed a deadline
21
+ * (a test importing this module, a manual invocation), or whatever is LEFT of
22
+ * the PostToolUse budget. `null` means the budget is exhausted — do not run.
23
+ *
24
+ * JS-32: the child spent a literal 8 s no matter how much of the 10 s deadline
25
+ * `findBinary()` had already consumed, so a cold cache overran it and Claude
26
+ * Code killed the hook on the user's own edit. Exported because `runMain()`
27
+ * cannot be reached from a test without spawning the hook (JS-18's shape).
28
+ */
29
+ function childBudgetMs() {
30
+ return require('./hook-fail-open').remainingMs(CHILD_TIMEOUT_MS);
31
+ }
32
+
15
33
  // v0.21 — gated default-off. v0.18.0 added query-time freshness
16
34
  // (ensure_file_indexed) inside MCP tools that take a file_path arg, so a
17
35
  // PostToolUse hook spawning a fresh process on every Edit/Write was redundant
@@ -37,9 +55,14 @@ function runMain() {
37
55
  const bin = findBinary();
38
56
  if (!bin) return; // silent — binary not installed yet
39
57
 
58
+ // Read AFTER findBinary(): that chain is what eats the budget on a cold
59
+ // cache, and asking before it would hand the child an allowance nobody had.
60
+ const budget = childBudgetMs();
61
+ if (budget === null) return; // exhausted — the index waits for the next edit
62
+
40
63
  try {
41
64
  execFileSync(bin, ['incremental-index', '--quiet'], hidden({
42
- timeout: 8000,
65
+ timeout: budget,
43
66
  stdio: ['pipe', 'pipe', 'pipe']
44
67
  }));
45
68
  } catch { /* timeout or error — silent for hook */ }
@@ -49,4 +72,4 @@ if (require.main === module) {
49
72
  runMain();
50
73
  }
51
74
 
52
- module.exports = { shouldRun };
75
+ module.exports = { shouldRun, childBudgetMs, CHILD_TIMEOUT_MS };
@@ -780,7 +780,24 @@ function reportUnadoptSweep(entries) {
780
780
  lines.push(' Your own text outside the block was kept; .code-graph/ index dirs are untouched.');
781
781
  }
782
782
  if (failed.length) {
783
- lines.push(`[code-graph] Could NOT clean ${failed.length} project(s) — remove the block by hand or run \`code-graph-mcp unadopt\` there:`);
783
+ // Issue #41's third printer. The other two now spend `unadoptCommand()`,
784
+ // and that IS still runnable at the instant this line prints — the reclaim
785
+ // below never touches the plugin's script dir. But this is advice acted on
786
+ // minutes or days later, in OTHER repositories, and the plugin directory is
787
+ // Claude Code's to reap once the uninstall settles. A path into it is
788
+ // correct-when-printed and possibly-stale-when-pasted, and the bare name it
789
+ // replaced was never runnable for a plugin-only install at all.
790
+ //
791
+ // So lead with the remedy that cannot go stale: the block is
792
+ // sentinel-delimited, so deleting the lines between the two markers is a
793
+ // complete instruction that needs no tooling, no network and no surviving
794
+ // install. The tool route follows it, named as optional.
795
+ const { SENTINEL_BEGIN, SENTINEL_END } = require('./adopt');
796
+ lines.push(`[code-graph] Could NOT clean ${failed.length} project(s). In each CLAUDE.md below, delete`);
797
+ lines.push(` everything from ${SENTINEL_BEGIN}`);
798
+ lines.push(` through ${SENTINEL_END}`);
799
+ lines.push(' (your own text outside those markers is not ours to remove), or run');
800
+ lines.push(' `npx -y @sdsrs/code-graph unadopt` in the project:');
784
801
  for (const p of failed.slice(0, 10)) lines.push(` ${p}`);
785
802
  }
786
803
  process.stderr.write(lines.join('\n') + '\n');
@@ -10,7 +10,7 @@ const {
10
10
  } = require('./lifecycle');
11
11
  const { UPDATE_STATE_FILE } = require('./cache-paths');
12
12
  const { readBinaryVersion, isDevMode, getNewestMtime } = require('./version-utils');
13
- const { maybeAutoAdopt, isAdopted, unadopt } = require('./adopt');
13
+ const { maybeAutoAdopt, isAdopted, unadopt, unadoptCommand } = require('./adopt');
14
14
  const { isNonProjectCwd } = require('./project-detect');
15
15
  const { hidden } = require('./proc-opts');
16
16
  const { installHookFailOpen, remainingMs } = require('./hook-fail-open');
@@ -557,6 +557,13 @@ function verifyBinary() {
557
557
  return { available: true, binary };
558
558
  }
559
559
 
560
+ // `unadoptCommand` now lives in adopt.js (issue #41's rationale travels with
561
+ // it). It moved because THIS file was not the only printer handing a user that
562
+ // command — adopt.js's own `formatResult` prints a `Reverse:` line too, and it
563
+ // was still spending the bare name after 0.141.0 fixed the announcement below.
564
+ // Two spellings of one remedy is what let that survive; re-exported here so the
565
+ // name stays importable from this module.
566
+
560
567
  /**
561
568
  * Lightweight consistency checks — called from runSessionInit().
562
569
  * Returns an array of issue objects: { id, msg, fix }.
@@ -806,7 +813,7 @@ function runSessionInit({ source } = {}) {
806
813
  '[code-graph] Installed code-graph block into project CLAUDE.md (plugin install → knowing consent).\n' +
807
814
  ' Detail table: .claude/plugin_code_graph_mcp.md (generated; safe to gitignore)\n' +
808
815
  ' Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env\n' +
809
- ' Reverse: code-graph-mcp unadopt\n'
816
+ ` Reverse: ${unadoptCommand()}\n`
810
817
  );
811
818
  }
812
819
  // `adopt()` has returned `registryRecorded` since it stopped throwing on a
@@ -820,7 +827,7 @@ function runSessionInit({ source } = {}) {
820
827
  process.stderr.write(
821
828
  '[code-graph] Note: this project could not be recorded in the adopted-projects registry,\n' +
822
829
  ' so `/plugin uninstall` will NOT strip the block from this CLAUDE.md.\n' +
823
- ' Remove it by hand with `code-graph-mcp unadopt` before uninstalling.\n'
830
+ ` Remove it by hand with \`${unadoptCommand()}\` before uninstalling.\n`
824
831
  );
825
832
  }
826
833
  }
@@ -1090,7 +1097,7 @@ module.exports = {
1090
1097
  indexNeedsRevalidation,
1091
1098
  injectProjectMap,
1092
1099
  injectRecentImpact,
1093
- verifyBinary, missingBinaryMessage,
1100
+ verifyBinary, missingBinaryMessage, unadoptCommand,
1094
1101
  consistencyCheck,
1095
1102
  runSessionInit,
1096
1103
  computeQuietHooks,
@@ -35,7 +35,7 @@ jobs:
35
35
  node-version: '20'
36
36
  - name: Build snapshot
37
37
  run: |
38
- npx -y -p @sdsrs/code-graph@0.140.0 code-graph-mcp snapshot create --out snapshot.db
38
+ npx -y -p @sdsrs/code-graph@0.142.0 code-graph-mcp snapshot create --out snapshot.db
39
39
  zstd -9 snapshot.db -o snapshot.db.zst
40
40
  mv snapshot.db.zst "code-graph-snapshot-${GITHUB_SHA:0:7}.db.zst"
41
41
  - name: Upload to release
@@ -49,6 +49,19 @@ type: reference
49
49
  > ToolSearch 加载),而 Bash 永远在线——真实编程夜(2026-06-12)观测到的全部
50
50
  > 转化都是 CLI 调用。结构化查询的最快路径是 Bash 直呼
51
51
  > `code-graph-mcp callgraph X / show X / overview <dir> / grep "pat" / impact X`。
52
+ >
53
+ > **裸名字不在 PATH 上时**(纯 `/plugin install`,没跑过 `npm i -g`):插件自己
54
+ > 下载的那份在 `~/.cache/code-graph/bin/code-graph-mcp`,本文下面所有查询类子命令
55
+ > (callgraph / impact / overview / show / search / grep / refs / similar / deps /
56
+ > dead-code / trace)都能直接用它跑;`npm i -g @sdsrs/code-graph` 则把裸名字放上
57
+ > PATH(issue #41)。
58
+ >
59
+ > **三个例外**:`doctor` / `adopt` / `unadopt` 是 JS 派发的——二进制会去自己旁边
60
+ > 找对应脚本(前者 `doctor.js`,后两者 `adopt.js`),而缓存目录里没有那些邻居,
61
+ > 所以从缓存路径调它们会报 `<脚本名> not found`。直接跑脚本即可:
62
+ > `node <插件目录>/claude-plugin/scripts/adopt.js unadopt`(SessionStart 的提示行
63
+ > 会印出这台机器上的完整路径)、`node <插件目录>/claude-plugin/scripts/doctor.js`。
64
+ >
52
65
  > `grep` 是 drop-in 替代:`-F` 字面 / `-i` / `-w` / `-l` / `-c` 计数 / `-t <lang>` 按语言筛 /
53
66
  > `-g <glob>` 路径过滤 / `-A/-B/-C N` 上下文 / `-M N` 行宽上限(默认 512,防长行刷屏)/
54
67
  > 多路径 / `-m 0` 取消每文件上限,退出码兼容 grep(0/1/2),召回达 git-grep 级
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.140.0",
3
+ "version": "0.142.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.140.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.140.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.140.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.140.0",
42
- "@sdsrs/code-graph-win32-x64": "0.140.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.142.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.142.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.142.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.142.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.142.0"
43
43
  }
44
44
  }