@colbymchenry/codegraph 0.9.3 → 0.9.4

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.
Files changed (3) hide show
  1. package/README.md +91 -66
  2. package/npm-shim.js +215 -27
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -10,15 +10,15 @@
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
11
  [![Self-contained](https://img.shields.io/badge/Node.js-bundled%20%C2%B7%20none%20required-brightgreen.svg)](https://nodejs.org/)
12
12
 
13
- [![Windows](https://img.shields.io/badge/Windows-supported-blue.svg)](#)
14
- [![macOS](https://img.shields.io/badge/macOS-supported-blue.svg)](#)
15
- [![Linux](https://img.shields.io/badge/Linux-supported-blue.svg)](#)
13
+ [![Windows](https://img.shields.io/badge/Windows-supported-blue.svg)](#supported-platforms)
14
+ [![macOS](https://img.shields.io/badge/macOS-supported-blue.svg)](#supported-platforms)
15
+ [![Linux](https://img.shields.io/badge/Linux-supported-blue.svg)](#supported-platforms)
16
16
 
17
- [![Claude Code](https://img.shields.io/badge/Claude_Code-supported-blueviolet.svg)](#)
18
- [![Cursor](https://img.shields.io/badge/Cursor-supported-blueviolet.svg)](#)
19
- [![Codex CLI](https://img.shields.io/badge/Codex_CLI-supported-blueviolet.svg)](#)
20
- [![opencode](https://img.shields.io/badge/opencode-supported-blueviolet.svg)](#)
21
- [![Hermes Agent](https://img.shields.io/badge/Hermes_Agent-supported-blueviolet.svg)](#)
17
+ [![Claude Code](https://img.shields.io/badge/Claude_Code-supported-blueviolet.svg)](#supported-agents)
18
+ [![Cursor](https://img.shields.io/badge/Cursor-supported-blueviolet.svg)](#supported-agents)
19
+ [![Codex CLI](https://img.shields.io/badge/Codex_CLI-supported-blueviolet.svg)](#supported-agents)
20
+ [![opencode](https://img.shields.io/badge/opencode-supported-blueviolet.svg)](#supported-agents)
21
+ [![Hermes Agent](https://img.shields.io/badge/Hermes_Agent-supported-blueviolet.svg)](#supported-agents)
22
22
 
23
23
  </div>
24
24
 
@@ -56,6 +56,16 @@ codegraph init -i
56
56
 
57
57
  </div>
58
58
 
59
+ ### Uninstall
60
+
61
+ Changed your mind? One command removes CodeGraph from every agent it configured:
62
+
63
+ ```bash
64
+ codegraph uninstall
65
+ ```
66
+
67
+ <sub>Reverses the installer — strips CodeGraph's MCP server config, instructions, and permissions from each configured agent. Your project indexes (`.codegraph/`) are left untouched; remove those per-project with `codegraph uninit`. Use `--target` to remove from specific agents, or `--yes` to run non-interactively.</sub>
68
+
59
69
  ---
60
70
 
61
71
  ## Why CodeGraph?
@@ -66,26 +76,26 @@ When Claude Code explores a codebase, it spawns **Explore agents** that scan fil
66
76
 
67
77
  ### Benchmark Results
68
78
 
69
- Tested across **7 real-world open-source codebases** spanning 7 languages, comparing an agent (Claude Code, headless) answering one architecture question **with** and **without** CodeGraph. Each cell is the savings at the **median of 4 runs per arm**.
79
+ Tested across **7 real-world open-source codebases** spanning 7 languages, comparing an agent (Claude Code, headless) answering one architecture question **with** and **without** CodeGraph. Each cell is the savings at the **median of 4 runs per arm**. _Re-validated on **v0.9.4** (2026-05-24)._
70
80
 
71
- > **Average: 35% cheaper · 59% fewer tokens · 49% faster · 70% fewer tool calls**
81
+ > **Average: 35% cheaper · 57% fewer tokens · 46% faster · 71% fewer tool calls**
72
82
 
73
83
  | Codebase | Language | Cost | Tokens | Time | Tool calls |
74
84
  |----------|----------|------|--------|------|------------|
75
- | **VS Code** | TypeScript · ~10k files | 35% cheaper | 73% fewer | 41% faster | 72% fewer |
76
- | **Excalidraw** | TypeScript · ~600 | 47% cheaper | 73% fewer | 60% faster | 86% fewer |
77
- | **Django** | Python · ~2.7k | 34% cheaper | 64% fewer | 59% faster | 81% fewer |
78
- | **Tokio** | Rust · ~700 | 52% cheaper | 81% fewer | 63% faster | 89% fewer |
79
- | **OkHttp** | Java · ~640 | 17% cheaper | 41% fewer | 36% faster | 64% fewer |
80
- | **Gin** | Go · ~150 | 22% cheaper | 23% fewer | 34% faster | 19% fewer |
81
- | **Alamofire** | Swift · ~100 | 38% cheaper | 59% fewer | 51% faster | 77% fewer |
85
+ | **VS Code** | TypeScript · ~10k files | 26% cheaper | 78% fewer | 52% faster | 85% fewer |
86
+ | **Excalidraw** | TypeScript · ~640 | 52% cheaper | 90% fewer | 73% faster | 96% fewer |
87
+ | **Django** | Python · ~3k | 12% cheaper | 36% fewer | 19% faster | 53% fewer |
88
+ | **Tokio** | Rust · ~790 | 82% cheaper | 86% fewer | 71% faster | 92% fewer |
89
+ | **OkHttp** | Java · ~645 | 2% cheaper | 13% fewer | 31% faster | 45% fewer |
90
+ | **Gin** | Go · ~110 | 21% cheaper | 34% fewer | 27% faster | 40% fewer |
91
+ | **Alamofire** | Swift · ~110 | 47% cheaper | 64% fewer | 48% faster | 83% fewer |
82
92
 
83
93
  The gains scale with codebase size: on large repos the agent answers from the index in a handful of calls with **zero file reads**, while the no-CodeGraph agent fans out across grep/find/Read (and the sub-agents it spawns). On a small repo like Gin (~150 files) native search is already cheap, so the margin narrows.
84
94
 
85
95
  <details>
86
96
  <summary><strong>Full benchmark details</strong></summary>
87
97
 
88
- **Methodology.** Each arm is `claude -p` (Claude Opus 4.7, Claude Code v2.1.145) run headlessly against the repo with `--strict-mcp-config`: **WITH** = CodeGraph's MCP server enabled, **WITHOUT** = an empty MCP config. Built-in Read/Grep/Bash stay available to both. Same question per repo, **4 runs per arm, median reported**. Cost = the run's `total_cost_usd`; Tokens = total tokens processed (input incl. cached + output); Time = wall-clock; Tool calls = every tool invocation, including those inside any sub-agents the model spawns. Repos cloned at `--depth 1` and indexed by the same CodeGraph build that served them.
98
+ **Methodology.** Each arm is `claude -p` (Claude Opus 4.7) run headlessly against the repo with `--strict-mcp-config`: **WITH** = CodeGraph's MCP server enabled, **WITHOUT** = an empty MCP config. Built-in Read/Grep/Bash stay available to both. Same question per repo, **4 runs per arm, median reported**. Cost = the run's `total_cost_usd`; Tokens = total tokens processed (input incl. cached + output); Time = wall-clock; Tool calls = every tool invocation, including those inside any sub-agents the model spawns. Repos cloned at `--depth 1` and indexed by the same CodeGraph build that served them. Re-validated on codegraph **v0.9.4** (2026-05-24); per-repo numbers move run-to-run with how hard the without-arm thrashes (the median-of-4 smooths it, but tails remain — e.g. Tokio's without-arm hit $2.41/3m one batch).
89
99
 
90
100
  **Queries:**
91
101
  | Codebase | Query |
@@ -101,13 +111,13 @@ The gains scale with codebase size: on large repos the agent answers from the in
101
111
  **Raw medians — WITH → WITHOUT:**
102
112
  | Codebase | Cost | Tokens | Time | Tool calls |
103
113
  |----------|------|--------|------|------------|
104
- | VS Code | $0.42 → $0.64 | 393k1.4M | 1m 0s1m 43s | 723 |
105
- | Excalidraw | $0.54 → $1.02 | 851k → 3.2M | 1m 17s 3m 14s | 1283 |
106
- | Django | $0.41 → $0.62 | 499k → 1.4M | 1m 0s2m 25s | 9 → 48 |
107
- | Tokio | $0.50 → $1.04 | 657k3.4M | 1m 5s 2m 56s | 975 |
108
- | OkHttp | $0.36 → $0.44 | 352k596k | 45s → 1m 11s | 514 |
109
- | Gin | $0.36 → $0.46 | 431k562k | 47s → 1m 11s | 78 |
110
- | Alamofire | $0.61 → $0.99 | 1.1M → 2.6M | 1m 19s → 2m 41s | 1564 |
114
+ | VS Code | $0.60 → $0.80 | 601k2.8M | 1m 10s2m 26s | 855 |
115
+ | Excalidraw | $0.43 → $0.90 | 344k → 3.5M | 48s2m 58s | 379 |
116
+ | Django | $0.59 → $0.67 | 739k → 1.2M | 1m 19s1m 38s | 9 → 19 |
117
+ | Tokio | $0.42 → $2.41 | 379k2.6M | 53s3m 2s | 453 |
118
+ | OkHttp | $0.47 → $0.47 | 636k730k | 42s → 1m 1s | 611 |
119
+ | Gin | $0.37 → $0.47 | 444k675k | 44s → 1m 0s | 610 |
120
+ | Alamofire | $0.61 → $1.14 | 1.0M → 2.8M | 1m 17s → 2m 27s | 1269 |
111
121
 
112
122
  **Why CodeGraph wins:** with the index available, the agent answers directly — `codegraph_context` to map the area, then one `codegraph_explore` for the relevant source — and stops, usually with zero file reads. Without it, the agent (and the Explore sub-agents it spawns) spends most of its budget on discovery (find/ls/grep) before reading the right code. CodeGraph only helps when queried *directly*, so its instructions steer agents to answer directly rather than delegate exploration to file-reading sub-agents — otherwise a sub-agent reads files regardless and CodeGraph becomes overhead.
113
123
 
@@ -253,25 +263,21 @@ CodeGraph builds a semantic knowledge graph of codebases for faster, smarter cod
253
263
 
254
264
  ### If `.codegraph/` exists in the project
255
265
 
256
- **NEVER call `codegraph_explore` or `codegraph_context` directly in the main session.** These tools return large amounts of source code that fills up main session context. Instead, ALWAYS spawn an Explore agent for any exploration question (e.g., "how does X work?", "explain the Y system", "where is Z implemented?").
257
-
258
- **When spawning Explore agents**, include this instruction in the prompt:
266
+ **Answer directly with CodeGraph don't delegate exploration to a file-reading sub-agent or a grep/read loop.** CodeGraph *is* the pre-built search index; re-deriving its answers with grep + Read repeats work it already did and costs more for the same result. For "how does X work?", architecture, trace, or where-is-X questions, answer in a handful of CodeGraph calls and stop — typically with **zero file reads**. The returned source is complete and authoritative: treat it as already read and do not re-open those files. Reach for raw Read/Grep only to confirm a specific detail CodeGraph didn't cover.
259
267
 
260
- > This project has CodeGraph initialized (.codegraph/ exists). Use `codegraph_explore` as your PRIMARY tool — it returns full source code sections from all relevant files in one call.
261
- >
262
- > **Rules:**
263
- > 1. Follow the explore call budget in the `codegraph_explore` tool description — it scales automatically based on project size.
264
- > 2. Do NOT re-read files that codegraph_explore already returned source code for. The source sections are complete and authoritative.
265
- > 3. Only fall back to grep/glob/read for files listed under "Additional relevant files" if you need more detail, or if codegraph returned no results.
266
-
267
- **The main session may only use these lightweight tools directly** (for targeted lookups before making edits, not for exploration):
268
+ **Tool selection by intent:**
268
269
 
269
270
  | Tool | Use For |
270
271
  |------|---------|
271
- | `codegraph_search` | Find symbols by name |
272
- | `codegraph_callers` / `codegraph_callees` | Trace call flow |
272
+ | `codegraph_context` | Map a task / feature / area first — composes search + node + callers + callees in one call |
273
+ | `codegraph_trace` | "How does X reach Y" — the call path, each hop's body inline (follows dynamic-dispatch hops grep can't) |
274
+ | `codegraph_explore` | Survey several related symbols' source in ONE budget-capped call |
275
+ | `codegraph_search` | Find a symbol by name |
276
+ | `codegraph_callers` / `codegraph_callees` | Walk call flow one hop at a time |
273
277
  | `codegraph_impact` | Check what's affected before editing |
274
- | `codegraph_node` | Get a single symbol's details |
278
+ | `codegraph_node` | Get a single symbol's source / signature |
279
+
280
+ A direct CodeGraph answer is a handful of calls; a grep/read exploration is dozens.
275
281
 
276
282
  ### If `.codegraph/` does NOT exist
277
283
 
@@ -287,34 +293,23 @@ At the start of a session, ask the user if they'd like to initialize CodeGraph:
287
293
  ## How It Works
288
294
 
289
295
  ```
290
- ┌─────────────────────────────────────────────────────────────────┐
291
- │ Claude Code │
292
- │ │
293
- │ "Implement user authentication" │
294
- │ │ │
295
- │ ▼ │
296
- │ ┌─────────────────┐ ┌─────────────────┐ │
297
- │ │ Explore Agent │ ──── │ Explore Agent │ │
298
- │ └────────┬────────┘ └────────┬────────┘ │
299
- │ │ │ │
300
- └───────────┼────────────────────────┼─────────────────────────────┘
301
- │ │
302
- ▼ ▼
303
296
  ┌───────────────────────────────────────────────────────────────────┐
304
- CodeGraph MCP Server
305
- ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
306
- Search │ Callers │ │ Context │ │
307
- │ "auth" │ │ "login()" │ │ for task
308
- └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
309
- │ │ │ │ │
310
- └────────────────┼────────────────┘ │
311
-
312
- │ ┌───────────────────────┐ │
313
- │ SQLite Graph DB
314
- • 387 symbols │ │
315
- │ • 1,204 edges
316
- • Instant lookups
317
- └───────────────────────┘
297
+ Claude Code
298
+
299
+ "How does a request reach the database?"
300
+ calls CodeGraph tools directly — no Explore sub-agent
301
+
302
+ └─────────────────────────────────┬─────────────────────────────────┘
303
+
304
+
305
+ ┌───────────────────────────────────────────────────────────────────┐
306
+ CodeGraph MCP Server
307
+
308
+ context · trace · explore · callers · callees · impact
309
+
310
+
311
+ │ SQLite knowledge graph │
312
+ │ symbols · edges · files · FTS5 full-text search │
318
313
  └───────────────────────────────────────────────────────────────────┘
319
314
  ```
320
315
 
@@ -333,6 +328,7 @@ At the start of a session, ask the user if they'd like to initialize CodeGraph:
333
328
  ```bash
334
329
  codegraph # Run interactive installer
335
330
  codegraph install # Run installer (explicit)
331
+ codegraph uninstall # Remove CodeGraph from your agents (inverse of install)
336
332
  codegraph init [path] # Initialize in a project (--index to also index)
337
333
  codegraph uninit [path] # Remove CodeGraph from a project (--force to skip prompt)
338
334
  codegraph index [path] # Full index (--force to re-index, --quiet for less output)
@@ -341,6 +337,9 @@ codegraph status [path] # Show statistics
341
337
  codegraph query <search> # Search symbols (--kind, --limit, --json)
342
338
  codegraph files [path] # Show file structure (--format, --filter, --max-depth, --json)
343
339
  codegraph context <task> # Build context for AI (--format, --max-nodes)
340
+ codegraph callers <symbol> # Find what calls a function/method (--limit, --json)
341
+ codegraph callees <symbol> # Find what a function/method calls (--limit, --json)
342
+ codegraph impact <symbol> # Analyze what code is affected by changing a symbol (--depth, --json)
344
343
  codegraph affected [files...] # Find test files affected by changes (see below)
345
344
  codegraph serve --mcp # Start MCP server
346
345
  ```
@@ -383,10 +382,12 @@ When running as an MCP server, CodeGraph exposes these tools to Claude Code:
383
382
  |------|---------|
384
383
  | `codegraph_search` | Find symbols by name across the codebase |
385
384
  | `codegraph_context` | Build relevant code context for a task |
385
+ | `codegraph_trace` | Trace the call path between two symbols ("how does X reach Y") in one call — each hop with its body inline, following dynamic-dispatch hops (callbacks, React re-render, interface→impl) that grep can't |
386
386
  | `codegraph_callers` | Find what calls a function |
387
387
  | `codegraph_callees` | Find what a function calls |
388
388
  | `codegraph_impact` | Analyze what code is affected by changing a symbol |
389
389
  | `codegraph_node` | Get details about a specific symbol (optionally with source code) |
390
+ | `codegraph_explore` | Return source for several related symbols grouped by file, plus a relationship map, in one call |
390
391
  | `codegraph_files` | Get indexed file structure (faster than filesystem scanning) |
391
392
  | `codegraph_status` | Check index health and statistics |
392
393
 
@@ -436,6 +437,30 @@ What that means in practice:
436
437
  > committed `dist/`. If you commit a dependency or build directory you don't want
437
438
  > in the graph, add it to `.gitignore`.
438
439
 
440
+ ## Supported Platforms
441
+
442
+ Every release ships a self-contained build (bundled Node runtime — nothing to
443
+ compile) for all three desktop OSes, on both Intel/AMD (x64) and ARM (arm64):
444
+
445
+ | Platform | Architectures | Install |
446
+ |----------|---------------|---------|
447
+ | Windows | x64, arm64 | PowerShell installer or npm |
448
+ | macOS | x64, arm64 | shell installer or npm |
449
+ | Linux | x64, arm64 | shell installer or npm |
450
+
451
+ See [Get Started](#get-started) for the one-line install commands.
452
+
453
+ ## Supported Agents
454
+
455
+ The interactive installer auto-detects and configures each of these — wiring up
456
+ the MCP server and writing its instructions file:
457
+
458
+ - **Claude Code**
459
+ - **Cursor**
460
+ - **Codex CLI**
461
+ - **opencode**
462
+ - **Hermes Agent**
463
+
439
464
  ## Supported Languages
440
465
 
441
466
  | Language | Extension | Status |
package/npm-shim.js CHANGED
@@ -11,48 +11,236 @@
11
11
  // (with node:sqlite), regardless of the user's Node version. The user's Node is
12
12
  // only ever a launcher; even an ancient version can run this file.
13
13
  //
14
+ // Self-heal (issue #303): some registries — notably the npmmirror/cnpm mirrors,
15
+ // and some corporate proxies — don't reliably mirror the per-platform
16
+ // optionalDependencies. npm treats an unfetchable optional dep as success and
17
+ // silently skips it, so the bundle goes missing and every command fails. When
18
+ // the installed bundle can't be resolved, this shim falls back to downloading
19
+ // the matching bundle straight from GitHub Releases — the very archive
20
+ // install.sh uses — into a cache dir, then runs that. Knobs:
21
+ // CODEGRAPH_NO_DOWNLOAD=1 disable the network fallback (print guidance)
22
+ // CODEGRAPH_INSTALL_DIR=DIR cache location (default: ~/.codegraph)
23
+ // CODEGRAPH_DOWNLOAD_BASE=URL release-download base (for mirrors/air-gapped)
24
+ //
14
25
  // Wired up at release time as the main package's `bin`:
15
- // "bin": { "codegraph": "scripts/npm-shim.js" }
26
+ // "bin": { "codegraph": "npm-shim.js" }
16
27
  // with the platform packages listed in `optionalDependencies`.
17
28
 
18
29
  var childProcess = require('child_process');
30
+ var fs = require('fs');
31
+ var os = require('os');
32
+ var path = require('path');
19
33
 
20
34
  var target = process.platform + '-' + process.arch; // e.g. darwin-arm64, linux-x64
21
35
  var pkg = '@colbymchenry/codegraph-' + target;
22
36
  var isWindows = process.platform === 'win32';
37
+ var REPO = 'colbymchenry/codegraph';
38
+
39
+ main().catch(function (e) {
40
+ process.stderr.write('codegraph: ' + (e && e.message ? e.message : String(e)) + '\n');
41
+ process.exit(1);
42
+ });
43
+
44
+ async function main() {
45
+ // Happy path: the npm-installed optional dependency. Fall back to a download
46
+ // when the registry didn't deliver it.
47
+ var resolved = resolveInstalledBundle() || (await selfHealBundle());
48
+ var res = childProcess.spawnSync(resolved.command, resolved.args, { stdio: 'inherit' });
49
+ if (res.error) {
50
+ process.stderr.write('codegraph: ' + res.error.message + '\n');
51
+ process.exit(1);
52
+ }
53
+ process.exit(res.status === null ? 1 : res.status);
54
+ }
23
55
 
24
- // On Windows the bundle's launcher is a .cmd batch file. Modern Node refuses to
25
- // spawn .cmd/.bat directly spawnSync throws EINVAL (the CVE-2024-27980
26
- // hardening, observed on Node 24). So on Windows we skip the .cmd and invoke the
27
- // bundled node.exe against the app entry point directly. On unix the bin launcher
28
- // is a shell script that spawns cleanly.
29
- var command, args;
30
- try {
56
+ // Resolve the launcher from the installed per-platform optionalDependency.
57
+ // Returns {command, args} or null if the package isn't installed.
58
+ function resolveInstalledBundle() {
59
+ try {
60
+ if (isWindows) {
61
+ // Modern Node refuses to spawn the bundle's .cmd directly (EINVAL, the
62
+ // CVE-2024-27980 hardening on Node 24), so invoke the bundled node.exe
63
+ // against the app entry point and pass --liftoff-only here.
64
+ var nodeExe = require.resolve(pkg + '/node.exe');
65
+ var entry = require.resolve(pkg + '/lib/dist/bin/codegraph.js');
66
+ return { command: nodeExe, args: liftoff(entry) };
67
+ }
68
+ return { command: require.resolve(pkg + '/bin/codegraph'), args: process.argv.slice(2) };
69
+ } catch (e) {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ // Locate the launcher inside an extracted GitHub bundle directory (same
75
+ // node/lib/bin layout as the npm platform package). Returns {command, args} or
76
+ // null when the directory doesn't hold a usable bundle yet.
77
+ function launcherIn(dir) {
31
78
  if (isWindows) {
32
- command = require.resolve(pkg + '/node.exe');
33
- var entry = require.resolve(pkg + '/lib/dist/bin/codegraph.js');
34
- // --liftoff-only: keep tree-sitter's WASM grammars off V8's turboshaft tier
35
- // to avoid the Zone OOM on Node >= 22 (issues #293/#298). The unix launcher
36
- // passes this too; on Windows we invoke node.exe directly so add it here.
37
- args = ['--liftoff-only', entry].concat(process.argv.slice(2));
79
+ var nodeExe = path.join(dir, 'node.exe');
80
+ var entry = path.join(dir, 'lib', 'dist', 'bin', 'codegraph.js');
81
+ if (fs.existsSync(nodeExe) && fs.existsSync(entry)) {
82
+ return { command: nodeExe, args: liftoff(entry) };
83
+ }
38
84
  } else {
39
- command = require.resolve(pkg + '/bin/codegraph');
40
- args = process.argv.slice(2);
85
+ var launcher = path.join(dir, 'bin', 'codegraph');
86
+ if (fs.existsSync(launcher)) return { command: launcher, args: process.argv.slice(2) };
87
+ }
88
+ return null;
89
+ }
90
+
91
+ // --liftoff-only keeps tree-sitter's WASM grammars off V8's turboshaft tier to
92
+ // avoid the Zone OOM on Node >= 22 (issues #293/#298). The unix bin/codegraph
93
+ // launcher already passes it; on Windows we invoke node.exe directly so add it.
94
+ function liftoff(entry) {
95
+ return ['--liftoff-only', entry].concat(process.argv.slice(2));
96
+ }
97
+
98
+ // Download + cache the platform bundle from GitHub Releases. Returns
99
+ // {command, args}; exits the process with guidance if it can't.
100
+ async function selfHealBundle() {
101
+ var version = readVersion();
102
+ var bundlesDir = path.join(process.env.CODEGRAPH_INSTALL_DIR || path.join(os.homedir(), '.codegraph'), 'bundles');
103
+ var dest = path.join(bundlesDir, target + '-' + version);
104
+
105
+ // Already downloaded by a previous run? Use it even when downloads are
106
+ // disabled — CODEGRAPH_NO_DOWNLOAD blocks fetching, not a cached bundle.
107
+ var cached = launcherIn(dest);
108
+ if (cached) return cached;
109
+
110
+ if (process.env.CODEGRAPH_NO_DOWNLOAD) {
111
+ fail('the network fallback is disabled (CODEGRAPH_NO_DOWNLOAD is set).');
41
112
  }
42
- } catch (e) {
113
+
114
+ var asset = 'codegraph-' + target + (isWindows ? '.zip' : '.tar.gz');
115
+ var base = process.env.CODEGRAPH_DOWNLOAD_BASE || ('https://github.com/' + REPO + '/releases/download');
116
+ var url = base + '/v' + version + '/' + asset;
117
+
43
118
  process.stderr.write(
44
- 'codegraph: no prebuilt bundle for ' + target + '.\n' +
45
- 'Expected the optional package ' + pkg + ' to be installed.\n' +
46
- 'Try reinstalling: npm i -g @colbymchenry/codegraph\n' +
47
- 'Or use the standalone installer (no Node required):\n' +
48
- ' curl -fsSL https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh | sh\n'
119
+ 'codegraph: platform bundle missing (registry did not provide ' + pkg + ').\n' +
120
+ 'codegraph: downloading ' + asset + ' from GitHub Releases (' + version + ')...\n'
49
121
  );
50
- process.exit(1);
122
+
123
+ // Stage inside bundlesDir so the final rename is on the same filesystem (atomic,
124
+ // no EXDEV across tmpfs). Strip the archive's top-level codegraph-<target>/ dir.
125
+ fs.mkdirSync(bundlesDir, { recursive: true });
126
+ var stage = fs.mkdtempSync(path.join(bundlesDir, '.dl-'));
127
+ try {
128
+ var archivePath = path.join(stage, asset);
129
+ await download(url, archivePath, 6);
130
+ await verifyChecksum(archivePath, asset, base, version);
131
+ var extracted = path.join(stage, 'bundle');
132
+ fs.mkdirSync(extracted);
133
+ extract(archivePath, extracted);
134
+
135
+ var raced = launcherIn(dest); // another process may have finished meanwhile
136
+ if (raced) { rmrf(stage); return raced; }
137
+ try {
138
+ fs.renameSync(extracted, dest);
139
+ } catch (e) {
140
+ var other = launcherIn(dest); // lost the race but theirs is valid
141
+ if (other) { rmrf(stage); return other; }
142
+ throw e;
143
+ }
144
+ } catch (e) {
145
+ rmrf(stage);
146
+ fail('download failed (' + e.message + ').\n URL: ' + url);
147
+ }
148
+ rmrf(stage);
149
+
150
+ var ready = launcherIn(dest);
151
+ if (!ready) fail('downloaded bundle is missing its launcher under ' + dest + '.');
152
+ process.stderr.write('codegraph: bundle ready.\n');
153
+ return ready;
154
+ }
155
+
156
+ function readVersion() {
157
+ try {
158
+ return require(path.join(__dirname, 'package.json')).version;
159
+ } catch (e) {
160
+ fail('could not read this package\'s version to locate a matching release.');
161
+ }
51
162
  }
52
163
 
53
- var res = childProcess.spawnSync(command, args, { stdio: 'inherit' });
54
- if (res.error) {
55
- process.stderr.write('codegraph: ' + res.error.message + '\n');
164
+ // GET with manual redirect following (GitHub release URLs redirect to a CDN).
165
+ function download(url, dest, redirectsLeft) {
166
+ return new Promise(function (resolve, reject) {
167
+ var https = require('https');
168
+ // timeout is an idle/inactivity timeout — it won't kill a slow-but-progressing
169
+ // download, only a stalled connection (so a blocked mirror fails fast with
170
+ // guidance instead of hanging the user's command forever).
171
+ var req = https.get(url, { headers: { 'User-Agent': 'codegraph-npm-shim' }, timeout: 30000 }, function (res) {
172
+ var status = res.statusCode;
173
+ if (status >= 300 && status < 400 && res.headers.location) {
174
+ res.resume();
175
+ if (redirectsLeft <= 0) { reject(new Error('too many redirects')); return; }
176
+ download(new URL(res.headers.location, url).toString(), dest, redirectsLeft - 1).then(resolve, reject);
177
+ return;
178
+ }
179
+ if (status !== 200) { res.resume(); reject(new Error('HTTP ' + status)); return; }
180
+ var file = fs.createWriteStream(dest);
181
+ res.on('error', reject);
182
+ res.pipe(file);
183
+ file.on('error', reject);
184
+ file.on('finish', function () { file.close(function () { resolve(); }); });
185
+ });
186
+ req.on('timeout', function () { req.destroy(new Error('connection timed out')); });
187
+ req.on('error', reject);
188
+ });
189
+ }
190
+
191
+ // Best-effort integrity check. When the release publishes a SHA256SUMS file, the
192
+ // downloaded archive MUST match its listed hash or we abort. When that file is
193
+ // absent (older releases) or simply unreachable, we proceed — the archive still
194
+ // arrived from GitHub over TLS. So tampering/corruption is caught, while a
195
+ // missing checksum never breaks an install.
196
+ async function verifyChecksum(archivePath, asset, base, version) {
197
+ var sumsPath = archivePath + '.SHA256SUMS';
198
+ try {
199
+ await download(base + '/v' + version + '/SHA256SUMS', sumsPath, 6);
200
+ } catch (e) {
201
+ return; // not published / unreachable → skip
202
+ }
203
+ var expected = null;
204
+ var lines = fs.readFileSync(sumsPath, 'utf8').split('\n');
205
+ for (var i = 0; i < lines.length; i++) {
206
+ var m = lines[i].trim().match(/^([0-9a-fA-F]{64})\s+\*?(.+)$/);
207
+ if (m && path.basename(m[2].trim()) === asset) { expected = m[1].toLowerCase(); break; }
208
+ }
209
+ if (!expected) return; // asset not listed → nothing to check
210
+ var actual = require('crypto').createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
211
+ if (actual !== expected) {
212
+ throw new Error('checksum mismatch for ' + asset +
213
+ ' (expected ' + expected.slice(0, 12) + '…, got ' + actual.slice(0, 12) + '…)');
214
+ }
215
+ process.stderr.write('codegraph: checksum verified.\n');
216
+ }
217
+
218
+ // Extract via the system tar — present on macOS, Linux, and Windows 10+
219
+ // (bsdtar reads .zip too). No third-party dependency in the shim.
220
+ function extract(archive, destDir) {
221
+ var args = isWindows
222
+ ? ['-xf', archive, '-C', destDir, '--strip-components=1']
223
+ : ['-xzf', archive, '-C', destDir, '--strip-components=1'];
224
+ var res = childProcess.spawnSync('tar', args, { stdio: 'ignore' });
225
+ if (res.error) throw new Error('tar unavailable: ' + res.error.message);
226
+ if (res.status !== 0) throw new Error('tar exited ' + res.status);
227
+ }
228
+
229
+ function rmrf(p) {
230
+ try { fs.rmSync(p, { recursive: true, force: true }); } catch (e) { /* best effort */ }
231
+ }
232
+
233
+ function fail(reason) {
234
+ process.stderr.write(
235
+ 'codegraph: no prebuilt bundle for ' + target + '.\n' +
236
+ (reason ? 'codegraph: ' + reason + '\n' : '') +
237
+ 'Expected the optional package ' + pkg + ' to be installed.\n' +
238
+ 'A registry mirror (e.g. npmmirror/cnpm) that did not mirror the per-platform\n' +
239
+ 'package is the usual cause. Fixes:\n' +
240
+ ' - install from the official registry:\n' +
241
+ ' npm i -g @colbymchenry/codegraph --registry=https://registry.npmjs.org\n' +
242
+ ' - or use the standalone installer (no Node required):\n' +
243
+ ' curl -fsSL https://raw.githubusercontent.com/' + REPO + '/main/install.sh | sh\n'
244
+ );
56
245
  process.exit(1);
57
246
  }
58
- process.exit(res.status === null ? 1 : res.status);
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@colbymchenry/codegraph",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Local-first code intelligence for AI agents (MCP). Self-contained — bundles its own runtime.",
5
5
  "bin": {
6
6
  "codegraph": "npm-shim.js"
7
7
  },
8
8
  "optionalDependencies": {
9
- "@colbymchenry/codegraph-darwin-arm64": "0.9.3",
10
- "@colbymchenry/codegraph-darwin-x64": "0.9.3",
11
- "@colbymchenry/codegraph-linux-arm64": "0.9.3",
12
- "@colbymchenry/codegraph-linux-x64": "0.9.3",
13
- "@colbymchenry/codegraph-win32-arm64": "0.9.3",
14
- "@colbymchenry/codegraph-win32-x64": "0.9.3"
9
+ "@colbymchenry/codegraph-darwin-arm64": "0.9.4",
10
+ "@colbymchenry/codegraph-darwin-x64": "0.9.4",
11
+ "@colbymchenry/codegraph-linux-arm64": "0.9.4",
12
+ "@colbymchenry/codegraph-linux-x64": "0.9.4",
13
+ "@colbymchenry/codegraph-win32-arm64": "0.9.4",
14
+ "@colbymchenry/codegraph-win32-x64": "0.9.4"
15
15
  },
16
16
  "files": [
17
17
  "npm-shim.js",