@sdsrs/code-graph 0.4.0 → 0.4.3

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
@@ -4,14 +4,16 @@ A high-performance code knowledge graph server implementing the [Model Context P
4
4
 
5
5
  ## Features
6
6
 
7
- - **Multi-language parsing** — Tree-sitter based AST extraction for TypeScript, JavaScript, Go, Python, Rust, Java, C, C++, HTML, CSS
7
+ - **Multi-language parsing** — Tree-sitter AST extraction for TypeScript, JavaScript, Go, Python, Rust, Java, C, C++, HTML, CSS
8
8
  - **Semantic code search** — Hybrid BM25 full-text + vector semantic search with Reciprocal Rank Fusion (RRF), powered by sqlite-vec
9
9
  - **Call graph traversal** — Recursive CTE queries to trace callers/callees with cycle detection
10
10
  - **HTTP route tracing** — Map route paths to backend handler functions (Express, Flask/FastAPI, Go)
11
+ - **Impact analysis** — Determine the blast radius of code changes by tracing all dependents
11
12
  - **Incremental indexing** — Merkle tree change detection with file system watcher for real-time updates
12
- - **Context compression** — Token-aware snippet extraction for LLM context windows
13
+ - **Context compression** — Token-aware snippet extraction for LLM context windows (L0→full code, L1→summaries, L2→file groups, L3→directory overview)
13
14
  - **Embedding model** — Optional local embedding via Candle (feature-gated `embed-model`)
14
- - **MCP protocol** — JSON-RPC 2.0 over stdio, plug-and-play with Claude Code, Cursor, and other MCP clients
15
+ - **MCP protocol** — JSON-RPC 2.0 over stdio, plug-and-play with Claude Code, Cursor, Windsurf, and other MCP clients
16
+ - **Claude Code Plugin** — First-class plugin with slash commands (`/understand`, `/trace`, `/impact`), agents, skills, auto-indexing hooks, StatusLine integration, and self-updating
15
17
 
16
18
  ## Why code-graph-mcp?
17
19
 
@@ -23,7 +25,7 @@ BLAKE3 Merkle tree tracks every file's content hash. On re-index, only changed f
23
25
 
24
26
  ### Hybrid Search, Not Just Grep
25
27
 
26
- Combines BM25 full-text ranking (FTS5) with vector semantic similarity (sqlite-vec) via **Reciprocal Rank Fusion (RRF)** — so searching "handle user login" finds the right function even if it's named `authenticate_session`. Results are auto-compressed to fit LLM context windows (L0→full code, L1→summaries, L2→file groups, L3→directory overview).
28
+ Combines BM25 full-text ranking (FTS5) with vector semantic similarity (sqlite-vec) via **Reciprocal Rank Fusion (RRF)** — so searching "handle user login" finds the right function even if it's named `authenticate_session`. Results are auto-compressed to fit LLM context windows.
27
29
 
28
30
  ### Scope-Aware Relation Extraction
29
31
 
@@ -58,15 +60,35 @@ src/
58
60
 
59
61
  ## Installation
60
62
 
61
- ### Option 1: Claude Code (Recommended)
63
+ ### Option 1: Claude Code Plugin (Recommended)
62
64
 
63
- One-command setupregisters as an MCP server directly in Claude Code:
65
+ Install as a Claude Code plugin for the best experience includes slash commands, agents, skills, auto-indexing hooks, StatusLine health display, and automatic updates:
66
+
67
+ ```bash
68
+ # Step 1: Add the marketplace
69
+ /plugin marketplace add sdsrss/code-graph-mcp
70
+
71
+ # Step 2: Install the plugin
72
+ /plugin install code-graph@sdsrss-code-graph
73
+ ```
74
+
75
+ What you get:
76
+ - **MCP Server** — All code-graph tools available to Claude
77
+ - **Slash Commands** — `/understand <module>`, `/trace <route>`, `/impact <symbol>`
78
+ - **Code Explorer Agent** — Deep code understanding expert via `code-explorer`
79
+ - **Auto-indexing Hook** — Incremental index on every file edit (PostToolUse)
80
+ - **StatusLine** — Real-time health display (nodes, files, watch status) — compatible with other plugins' StatusLine via composite multiplexer
81
+ - **Auto-update** — Checks for new versions every 24h, updates silently
82
+
83
+ ### Option 2: Claude Code MCP Server Only
84
+
85
+ Register as an MCP server without the plugin features:
64
86
 
65
87
  ```bash
66
88
  claude mcp add code-graph-mcp -- npx -y @sdsrs/code-graph
67
89
  ```
68
90
 
69
- ### Option 2: Cursor / Windsurf / Other MCP Clients
91
+ ### Option 3: Cursor / Windsurf / Other MCP Clients
70
92
 
71
93
  Add to your MCP settings file (e.g. `~/.cursor/mcp.json`):
72
94
 
@@ -81,7 +103,7 @@ Add to your MCP settings file (e.g. `~/.cursor/mcp.json`):
81
103
  }
82
104
  ```
83
105
 
84
- ### Option 3: npx (No Install)
106
+ ### Option 4: npx (No Install)
85
107
 
86
108
  Run directly without installing:
87
109
 
@@ -89,7 +111,7 @@ Run directly without installing:
89
111
  npx -y @sdsrs/code-graph
90
112
  ```
91
113
 
92
- ### Option 4: npm (Global Install)
114
+ ### Option 5: npm (Global Install)
93
115
 
94
116
  Install globally, then run anywhere:
95
117
 
@@ -100,7 +122,20 @@ code-graph-mcp
100
122
 
101
123
  ## Uninstallation
102
124
 
103
- ### Claude Code
125
+ ### Claude Code Plugin
126
+
127
+ ```bash
128
+ # Uninstall the plugin
129
+ /plugin uninstall code-graph@sdsrss-code-graph
130
+
131
+ # (Optional) Remove the marketplace
132
+ /plugin marketplace remove sdsrss-code-graph
133
+
134
+ # (Optional) Clean up all config and cache data
135
+ node ~/.claude/plugins/cache/sdsrss/code-graph/*/scripts/lifecycle.js uninstall
136
+ ```
137
+
138
+ ### Claude Code MCP Server
104
139
 
105
140
  ```bash
106
141
  claude mcp remove code-graph-mcp
@@ -116,6 +151,47 @@ Remove the `code-graph` entry from your MCP settings file (e.g. `~/.cursor/mcp.j
116
151
  npm uninstall -g @sdsrs/code-graph
117
152
  ```
118
153
 
154
+ ## MCP Tools
155
+
156
+ | Tool | Description |
157
+ |------|-------------|
158
+ | `semantic_code_search` | Hybrid BM25 + vector + graph search for AST nodes |
159
+ | `get_call_graph` | Trace upstream/downstream call chains for a function |
160
+ | `find_http_route` | Map route path to backend handler function |
161
+ | `trace_http_chain` | Full request flow: route → handler → downstream call chain |
162
+ | `impact_analysis` | Analyze the blast radius of changing a symbol |
163
+ | `module_overview` | High-level overview of a module's structure and exports |
164
+ | `dependency_graph` | Visualize dependency relationships between modules |
165
+ | `find_similar_code` | Find code snippets similar to a given pattern |
166
+ | `get_ast_node` | Extract a specific code symbol from a file |
167
+ | `read_snippet` | Read original code snippet by node ID with context |
168
+ | `start_watch` / `stop_watch` | Start/stop file system watcher for incremental indexing |
169
+ | `get_index_status` | Query index status and health |
170
+ | `rebuild_index` | Force full index rebuild |
171
+
172
+ ## Plugin Slash Commands
173
+
174
+ Available when installed as a Claude Code plugin:
175
+
176
+ | Command | Description |
177
+ |---------|-------------|
178
+ | `/understand <module>` | Deep dive into a module or file's architecture and relationships |
179
+ | `/trace <route>` | Trace a full HTTP request flow from route to data layer |
180
+ | `/impact <symbol>` | Analyze the impact scope of changing a symbol before modifying it |
181
+
182
+ ## Supported Languages
183
+
184
+ TypeScript, JavaScript, Go, Python, Rust, Java, C, C++, HTML, CSS
185
+
186
+ ## Storage
187
+
188
+ Uses SQLite with:
189
+ - FTS5 for full-text search
190
+ - sqlite-vec extension for vector similarity search
191
+ - Merkle tree hashes for incremental change detection
192
+
193
+ Data is stored in `.code-graph/index.db` under the project root (auto-created, gitignored).
194
+
119
195
  ## Build from Source
120
196
 
121
197
  ### Prerequisites
@@ -147,33 +223,6 @@ Add the compiled binary to your MCP settings:
147
223
  }
148
224
  ```
149
225
 
150
- ## MCP Tools
151
-
152
- | Tool | Description |
153
- |------|-------------|
154
- | `semantic_code_search` | Hybrid BM25 + vector + graph search for AST nodes |
155
- | `get_call_graph` | Trace upstream/downstream call chains for a function |
156
- | `find_http_route` | Map route path to backend handler function |
157
- | `trace_http_chain` | Full request flow: route → handler → downstream call chain |
158
- | `get_ast_node` | Extract a specific code symbol from a file |
159
- | `read_snippet` | Read original code snippet by node ID with context |
160
- | `start_watch` / `stop_watch` | Start/stop file system watcher for incremental indexing |
161
- | `get_index_status` | Query index status and health |
162
- | `rebuild_index` | Force full index rebuild |
163
-
164
- ## Supported Languages
165
-
166
- TypeScript, JavaScript, Go, Python, Rust, Java, C, C++, HTML, CSS
167
-
168
- ## Storage
169
-
170
- Uses SQLite with:
171
- - FTS5 for full-text search
172
- - sqlite-vec extension for vector similarity search
173
- - Merkle tree hashes for incremental change detection
174
-
175
- Data is stored in `.code-graph/index.db` under the project root (auto-created, gitignored).
176
-
177
226
  ## Development
178
227
 
179
228
  ```bash
@@ -4,6 +4,6 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.3.0",
7
+ "version": "0.4.3",
8
8
  "keywords": ["code-graph", "ast", "navigation", "mcp", "knowledge-graph"]
9
9
  }
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "code-graph-mcp incremental-index --quiet",
9
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/incremental-index.js\"",
10
10
  "timeout": 10
11
11
  }
12
12
  ],
@@ -15,14 +15,15 @@
15
15
  ],
16
16
  "SessionStart": [
17
17
  {
18
+ "matcher": "startup",
18
19
  "hooks": [
19
20
  {
20
21
  "type": "command",
21
- "command": "code-graph-mcp health-check --format oneline",
22
- "timeout": 5
22
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
23
+ "timeout": 8
23
24
  }
24
25
  ],
25
- "description": "Verify code graph index freshness at session start"
26
+ "description": "Health check, StatusLine registration, and update check at session start"
26
27
  }
27
28
  ]
28
29
  }
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const os = require('os');
7
+ const { CACHE_DIR, PLUGIN_ID, readManifest, readJson, writeJsonAtomic } = require('./lifecycle');
8
+
9
+ // ── Configuration ──────────────────────────────────────────
10
+ const GITHUB_REPO = 'sdsrss/code-graph-mcp';
11
+ const NPM_PACKAGE = '@sdsrs/code-graph';
12
+ const STATE_FILE = path.join(CACHE_DIR, 'update-state.json');
13
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h
14
+ const RATE_LIMIT_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h if rate-limited
15
+ const FETCH_TIMEOUT_MS = 3000;
16
+
17
+ // ── State Persistence ──────────────────────────────────────
18
+
19
+ function readState() {
20
+ return readJson(STATE_FILE) || {};
21
+ }
22
+
23
+ function saveState(state) {
24
+ try {
25
+ writeJsonAtomic(STATE_FILE, state);
26
+ } catch { /* ok */ }
27
+ }
28
+
29
+ // ── Dev Mode Detection ─────────────────────────────────────
30
+
31
+ function isDevMode() {
32
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..');
33
+ // Dev mode: running from source repo (has Cargo.toml nearby)
34
+ if (fs.existsSync(path.join(pluginRoot, '..', 'Cargo.toml'))) return true;
35
+ // Dev mode: plugin root is a symlink
36
+ try { if (fs.lstatSync(pluginRoot).isSymbolicLink()) return true; } catch { /* ok */ }
37
+ return false;
38
+ }
39
+
40
+ // ── Throttle ───────────────────────────────────────────────
41
+
42
+ function shouldCheck(state) {
43
+ if (!state.lastCheck) return true;
44
+ const elapsed = Date.now() - new Date(state.lastCheck).getTime();
45
+ const interval = state.rateLimited ? RATE_LIMIT_INTERVAL_MS : CHECK_INTERVAL_MS;
46
+ return elapsed >= interval;
47
+ }
48
+
49
+ // ── Version Comparison (semver) ────────────────────────────
50
+
51
+ function compareVersions(a, b) {
52
+ const pa = a.split('.').map(Number);
53
+ const pb = b.split('.').map(Number);
54
+ for (let i = 0; i < 3; i++) {
55
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
56
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
57
+ }
58
+ return 0;
59
+ }
60
+
61
+ // ── GitHub API ─────────────────────────────────────────────
62
+
63
+ async function fetchLatestRelease() {
64
+ const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
65
+ try {
66
+ const res = await fetch(url, {
67
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
68
+ headers: {
69
+ 'Accept': 'application/vnd.github+json',
70
+ 'User-Agent': 'code-graph-auto-update/1.0',
71
+ },
72
+ });
73
+
74
+ if (res.status === 403) {
75
+ // Rate limited
76
+ const state = readState();
77
+ saveState({ ...state, rateLimited: true });
78
+ return null;
79
+ }
80
+ if (!res.ok) return null;
81
+
82
+ const data = await res.json();
83
+ return {
84
+ version: data.tag_name.replace(/^v/, ''),
85
+ tarballUrl: data.tarball_url,
86
+ };
87
+ } catch { return null; }
88
+ }
89
+
90
+ // ── Download & Install ─────────────────────────────────────
91
+
92
+ async function downloadAndInstall(latest) {
93
+ const tmpDir = path.join(os.tmpdir(), `code-graph-update-${Date.now()}`);
94
+ try {
95
+ fs.mkdirSync(tmpDir, { recursive: true });
96
+
97
+ // 1. Download and extract tarball (plugin files)
98
+ execSync(
99
+ `curl -sL -H "Accept: application/vnd.github+json" "${latest.tarballUrl}" | tar xz -C "${tmpDir}" --strip-components=1`,
100
+ { timeout: 30000, stdio: 'pipe' }
101
+ );
102
+
103
+ // 2. Copy plugin files to cache
104
+ const pluginSrc = path.join(tmpDir, 'claude-plugin');
105
+ const pluginDst = path.join(
106
+ os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph', latest.version
107
+ );
108
+
109
+ if (fs.existsSync(pluginSrc)) {
110
+ fs.mkdirSync(pluginDst, { recursive: true });
111
+ // Copy recursively
112
+ execSync(`cp -r "${pluginSrc}/." "${pluginDst}/"`, { stdio: 'pipe' });
113
+ }
114
+
115
+ // 3. Update installed_plugins.json to point to new version
116
+ const installedPath = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
117
+ try {
118
+ const installed = readJson(installedPath);
119
+ if (installed && installed.plugins && installed.plugins[PLUGIN_ID]) {
120
+ installed.plugins[PLUGIN_ID][0].installPath = pluginDst;
121
+ installed.plugins[PLUGIN_ID][0].version = latest.version;
122
+ installed.plugins[PLUGIN_ID][0].lastUpdated = new Date().toISOString();
123
+ writeJsonAtomic(installedPath, installed);
124
+ }
125
+ } catch { /* installed_plugins update failed — not fatal */ }
126
+
127
+ // 4. Update install manifest with tag version
128
+ try {
129
+ const manifest = readManifest();
130
+ manifest.version = latest.version;
131
+ manifest.updatedAt = new Date().toISOString();
132
+ writeJsonAtomic(path.join(CACHE_DIR, 'install-manifest.json'), manifest);
133
+ } catch { /* manifest update failed — not fatal */ }
134
+
135
+ // 5. Update npm binary (non-blocking, best-effort)
136
+ try {
137
+ execSync(`npm install -g ${NPM_PACKAGE}@${latest.version}`, {
138
+ timeout: 60000,
139
+ stdio: 'pipe',
140
+ });
141
+ } catch {
142
+ // npm install failed — plugin files still updated
143
+ // User can manually update binary later
144
+ }
145
+
146
+ return true;
147
+ } catch { return false; }
148
+ finally {
149
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ok */ }
150
+ }
151
+ }
152
+
153
+ // ── Main Entry ─────────────────────────────────────────────
154
+
155
+ async function checkForUpdate() {
156
+ try {
157
+ // Skip in dev mode
158
+ if (isDevMode()) return null;
159
+
160
+ const state = readState();
161
+
162
+ // Time-based throttle
163
+ if (!shouldCheck(state)) {
164
+ // Report pending update from previous check
165
+ if (state.updateAvailable && state.latestVersion) {
166
+ return { updateAvailable: true, from: state.installedVersion, to: state.latestVersion };
167
+ }
168
+ return null;
169
+ }
170
+
171
+ // Check GitHub for latest release
172
+ const latest = await fetchLatestRelease();
173
+ if (!latest) {
174
+ saveState({ ...state, lastCheck: new Date().toISOString() });
175
+ return null;
176
+ }
177
+
178
+ // Compare versions
179
+ const manifest = readManifest();
180
+ const currentVersion = manifest.version || '0.0.0';
181
+ const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
182
+
183
+ if (hasUpdate) {
184
+ // Auto-update
185
+ const success = await downloadAndInstall(latest);
186
+ const newState = {
187
+ lastCheck: new Date().toISOString(),
188
+ installedVersion: success ? latest.version : currentVersion,
189
+ latestVersion: latest.version,
190
+ updateAvailable: !success,
191
+ lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
192
+ rateLimited: false,
193
+ };
194
+ saveState(newState);
195
+
196
+ return {
197
+ updateAvailable: !success,
198
+ updated: success,
199
+ from: currentVersion,
200
+ to: latest.version,
201
+ };
202
+ }
203
+
204
+ // No update needed
205
+ saveState({
206
+ ...state,
207
+ lastCheck: new Date().toISOString(),
208
+ latestVersion: latest.version,
209
+ updateAvailable: false,
210
+ rateLimited: false,
211
+ });
212
+ return null;
213
+ } catch {
214
+ // Silent failure — never block session
215
+ return null;
216
+ }
217
+ }
218
+
219
+ module.exports = { checkForUpdate, isDevMode, readState };
220
+
221
+ // CLI: node auto-update.js [check|status]
222
+ if (require.main === module) {
223
+ (async () => {
224
+ const cmd = process.argv[2] || 'check';
225
+ if (cmd === 'status') {
226
+ const state = readState();
227
+ console.log(JSON.stringify(state, null, 2));
228
+ } else {
229
+ console.log('Checking for updates...');
230
+ const result = await checkForUpdate();
231
+ if (result && result.updated) {
232
+ console.log(`Updated: v${result.from} → v${result.to}`);
233
+ } else if (result && result.updateAvailable) {
234
+ console.log(`Update available: v${result.to} (auto-install failed)`);
235
+ } else if (isDevMode()) {
236
+ console.log('Dev mode — auto-update skipped');
237
+ } else {
238
+ const manifest = readManifest();
239
+ console.log(`Up to date (v${manifest.version || 'unknown'})`);
240
+ }
241
+ }
242
+ })();
243
+ }
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { execFileSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+
8
+ const PLATFORM = os.platform();
9
+ const CACHE_FILE = path.join(os.homedir(), '.cache', 'code-graph', 'binary-path');
10
+
11
+ /**
12
+ * Locate the code-graph-mcp binary using multiple strategies.
13
+ * Results are cached to disk so repeated calls (e.g. per-hook) are fast.
14
+ * Priority: cache > PATH > local dev build > cargo install > npm platform pkg > npx cache
15
+ * Returns the absolute path or null if not found.
16
+ */
17
+ function findBinary() {
18
+ // Try disk cache first (avoids spawning `which` on hot paths)
19
+ try {
20
+ const cached = fs.readFileSync(CACHE_FILE, 'utf8').trim();
21
+ if (cached && fs.existsSync(cached)) return cached;
22
+ } catch { /* no cache or stale */ }
23
+
24
+ const result = findBinaryUncached();
25
+
26
+ // Write cache for subsequent calls
27
+ if (result) {
28
+ try {
29
+ fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
30
+ fs.writeFileSync(CACHE_FILE, result);
31
+ } catch { /* ok */ }
32
+ }
33
+
34
+ return result;
35
+ }
36
+
37
+ function findBinaryUncached() {
38
+ const name = PLATFORM === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
39
+
40
+ // 1. PATH lookup (user has intentionally installed it)
41
+ try {
42
+ const which = PLATFORM === 'win32' ? 'where' : 'which';
43
+ const found = execFileSync(which, [name], { stdio: ['pipe', 'pipe', 'pipe'] })
44
+ .toString().trim().split('\n')[0];
45
+ if (found && fs.existsSync(found)) return found;
46
+ } catch { /* not in PATH */ }
47
+
48
+ // 2. Local dev build (target/release in project directory)
49
+ const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
50
+ const devBin = path.join(projectRoot, 'target', 'release', name);
51
+ if (fs.existsSync(devBin)) return devBin;
52
+
53
+ // 3. Cargo install (~/.cargo/bin)
54
+ const cargoBin = path.join(os.homedir(), '.cargo', 'bin', name);
55
+ if (fs.existsSync(cargoBin)) return cargoBin;
56
+
57
+ // 4. npm platform package (installed via @sdsrs/code-graph)
58
+ const platformPkg = `@sdsrs/code-graph-${PLATFORM}-${os.arch()}`;
59
+ try {
60
+ const pkgPath = require.resolve(`${platformPkg}/package.json`);
61
+ const bin = path.join(path.dirname(pkgPath), name);
62
+ if (fs.existsSync(bin)) return bin;
63
+ } catch { /* not installed via npm */ }
64
+
65
+ // 5. npx cache (last resort — may be outdated)
66
+ const npxDir = path.join(os.homedir(), '.npm', '_npx');
67
+ try {
68
+ for (const entry of fs.readdirSync(npxDir)) {
69
+ const pkgJsonPath = path.join(npxDir, entry, 'node_modules', '@sdsrs', 'code-graph', 'package.json');
70
+ if (!fs.existsSync(pkgJsonPath)) continue;
71
+ const platDir = path.join(npxDir, entry, 'node_modules', '@sdsrs', `code-graph-${PLATFORM}-${os.arch()}`);
72
+ const platBin = path.join(platDir, name);
73
+ if (fs.existsSync(platBin)) return platBin;
74
+ }
75
+ } catch { /* no npx cache */ }
76
+
77
+ return null;
78
+ }
79
+
80
+ module.exports = { findBinary };
81
+
82
+ // Allow direct invocation for testing
83
+ if (require.main === module) {
84
+ const bin = findBinary();
85
+ if (bin) {
86
+ process.stdout.write(bin);
87
+ } else {
88
+ process.stderr.write('code-graph-mcp binary not found\n');
89
+ process.exit(1);
90
+ }
91
+ }
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { execFileSync } = require('child_process');
4
+ const { findBinary } = require('./find-binary');
5
+
6
+ const bin = findBinary();
7
+ if (!bin) process.exit(0); // silent — binary not installed yet
8
+
9
+ try {
10
+ execFileSync(bin, ['incremental-index', '--quiet'], {
11
+ timeout: 8000,
12
+ stdio: ['pipe', 'pipe', 'pipe']
13
+ });
14
+ } catch { /* timeout or error — silent for hook */ }
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const PLUGIN_ID = 'code-graph@sdsrss';
8
+ const CACHE_DIR = path.join(os.homedir(), '.cache', 'code-graph');
9
+ const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..');
10
+ const MANIFEST_FILE = path.join(CACHE_DIR, 'install-manifest.json');
11
+ const SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json');
12
+ const INSTALLED_PLUGINS_PATH = path.join(os.homedir(), '.claude', 'plugins', 'installed_plugins.json');
13
+ const REGISTRY_FILE = path.join(CACHE_DIR, 'statusline-registry.json');
14
+
15
+ // --- Helpers ---
16
+
17
+ function readJson(filePath) {
18
+ try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return null; }
19
+ }
20
+
21
+ function writeJsonAtomic(filePath, data) {
22
+ const dir = path.dirname(filePath);
23
+ fs.mkdirSync(dir, { recursive: true });
24
+ const tmp = filePath + '.tmp.' + process.pid;
25
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n');
26
+ fs.renameSync(tmp, filePath);
27
+ }
28
+
29
+ function readManifest() {
30
+ return readJson(MANIFEST_FILE) || { version: null, config: {} };
31
+ }
32
+
33
+ function writeManifest(manifest) {
34
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
35
+ writeJsonAtomic(MANIFEST_FILE, manifest);
36
+ }
37
+
38
+ function getPluginVersion() {
39
+ const pj = readJson(path.join(PLUGIN_ROOT, '.claude-plugin', 'plugin.json'));
40
+ return pj ? pj.version : '0.0.0';
41
+ }
42
+
43
+ function compositeCommand() {
44
+ return `node ${JSON.stringify(path.join(PLUGIN_ROOT, 'scripts', 'statusline-composite.js'))}`;
45
+ }
46
+
47
+ function codeGraphStatuslineCommand() {
48
+ return `node ${JSON.stringify(path.join(PLUGIN_ROOT, 'scripts', 'statusline.js'))}`;
49
+ }
50
+
51
+ function isOurComposite(settings) {
52
+ return settings.statusLine &&
53
+ settings.statusLine.command &&
54
+ settings.statusLine.command.includes('statusline-composite');
55
+ }
56
+
57
+ // --- StatusLine Registry ---
58
+ // Multiple providers can register. The composite script runs them all.
59
+
60
+ function readRegistry() {
61
+ return readJson(REGISTRY_FILE) || [];
62
+ }
63
+
64
+ function writeRegistry(registry) {
65
+ writeJsonAtomic(REGISTRY_FILE, registry);
66
+ }
67
+
68
+ function registerStatuslineProvider(id, command, needsStdin) {
69
+ const registry = readRegistry();
70
+ const idx = registry.findIndex(p => p.id === id);
71
+ const entry = { id, command, needsStdin: !!needsStdin };
72
+ if (idx >= 0) {
73
+ // Update existing entry only if command changed
74
+ if (registry[idx].command === command) return false;
75
+ registry[idx] = entry;
76
+ } else {
77
+ registry.push(entry);
78
+ }
79
+ writeRegistry(registry);
80
+ return true;
81
+ }
82
+
83
+ function unregisterStatuslineProvider(id) {
84
+ const registry = readRegistry();
85
+ const filtered = registry.filter(p => p.id !== id);
86
+ if (filtered.length === registry.length) return false;
87
+ writeRegistry(filtered);
88
+ return true;
89
+ }
90
+
91
+ // --- Install (idempotent) ---
92
+
93
+ function install() {
94
+ const version = getPluginVersion();
95
+ const manifest = readManifest();
96
+ const settings = readJson(SETTINGS_PATH) || {};
97
+ let settingsChanged = false;
98
+
99
+ // 1. StatusLine — composite approach
100
+ // a. Capture existing statusline as a provider (if not already composite)
101
+ // b. Register code-graph as a provider
102
+ // c. Set statusLine to composite script
103
+ if (!isOurComposite(settings)) {
104
+ // Preserve existing statusline as first provider
105
+ if (settings.statusLine && settings.statusLine.command) {
106
+ registerStatuslineProvider('_previous', settings.statusLine.command, true);
107
+ }
108
+ // Set composite as the statusLine
109
+ settings.statusLine = { type: 'command', command: compositeCommand() };
110
+ settingsChanged = true;
111
+ manifest.config.statusLine = true;
112
+ }
113
+
114
+ // Register code-graph provider
115
+ registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
116
+
117
+ // 2. enabledPlugins — add if missing
118
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
119
+ if (!(PLUGIN_ID in settings.enabledPlugins)) {
120
+ settings.enabledPlugins[PLUGIN_ID] = true;
121
+ settingsChanged = true;
122
+ manifest.config.enabledPlugins = true;
123
+ }
124
+
125
+ // 3. Write settings atomically if changed
126
+ if (settingsChanged) {
127
+ writeJsonAtomic(SETTINGS_PATH, settings);
128
+ }
129
+
130
+ // 4. Write manifest with version
131
+ manifest.version = version;
132
+ manifest.installedAt = manifest.installedAt || new Date().toISOString();
133
+ manifest.updatedAt = new Date().toISOString();
134
+ writeManifest(manifest);
135
+
136
+ return { version, settingsChanged, statusLineClaimed: manifest.config.statusLine };
137
+ }
138
+
139
+ // --- Uninstall (clean all config) ---
140
+
141
+ function uninstall() {
142
+ const settings = readJson(SETTINGS_PATH);
143
+ let settingsChanged = false;
144
+
145
+ if (settings) {
146
+ // 1. StatusLine: remove code-graph from registry
147
+ unregisterStatuslineProvider('code-graph');
148
+ const remaining = readRegistry();
149
+
150
+ if (isOurComposite(settings)) {
151
+ if (remaining.length === 1 && remaining[0].id === '_previous') {
152
+ // Only the previous provider remains — restore it directly
153
+ settings.statusLine = { type: 'command', command: remaining[0].command };
154
+ unregisterStatuslineProvider('_previous');
155
+ settingsChanged = true;
156
+ } else if (remaining.length === 0) {
157
+ // No providers left — remove statusLine entirely
158
+ delete settings.statusLine;
159
+ settingsChanged = true;
160
+ }
161
+ // else: other providers still using composite — leave it
162
+ }
163
+
164
+ // 2. Remove from enabledPlugins
165
+ if (settings.enabledPlugins && PLUGIN_ID in settings.enabledPlugins) {
166
+ delete settings.enabledPlugins[PLUGIN_ID];
167
+ settingsChanged = true;
168
+ }
169
+
170
+ // 3. Write settings if changed
171
+ if (settingsChanged) {
172
+ writeJsonAtomic(SETTINGS_PATH, settings);
173
+ }
174
+ }
175
+
176
+ // 4. Remove from installed_plugins.json
177
+ const installedPlugins = readJson(INSTALLED_PLUGINS_PATH);
178
+ if (installedPlugins && installedPlugins.plugins && PLUGIN_ID in installedPlugins.plugins) {
179
+ delete installedPlugins.plugins[PLUGIN_ID];
180
+ writeJsonAtomic(INSTALLED_PLUGINS_PATH, installedPlugins);
181
+ }
182
+
183
+ // 5. Remove cache directory
184
+ try { fs.rmSync(CACHE_DIR, { recursive: true, force: true }); } catch { /* ok */ }
185
+
186
+ // 6. Remove plugin files from cache
187
+ const pluginCacheDir = path.join(os.homedir(), '.claude', 'plugins', 'cache', 'sdsrss', 'code-graph');
188
+ try { fs.rmSync(pluginCacheDir, { recursive: true, force: true }); } catch { /* ok */ }
189
+
190
+ return { settingsChanged };
191
+ }
192
+
193
+ // --- Update (refresh config points) ---
194
+
195
+ function update() {
196
+ const version = getPluginVersion();
197
+ const manifest = readManifest();
198
+ const oldVersion = manifest.version;
199
+ const settings = readJson(SETTINGS_PATH) || {};
200
+ let settingsChanged = false;
201
+
202
+ // 1. Update composite command path if version changed
203
+ if (isOurComposite(settings)) {
204
+ const cmd = compositeCommand();
205
+ if (settings.statusLine.command !== cmd) {
206
+ settings.statusLine.command = cmd;
207
+ settingsChanged = true;
208
+ }
209
+ }
210
+
211
+ // 2. Update code-graph provider in registry
212
+ registerStatuslineProvider('code-graph', codeGraphStatuslineCommand(), false);
213
+
214
+ // 3. Ensure enabledPlugins entry exists
215
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
216
+ if (!(PLUGIN_ID in settings.enabledPlugins)) {
217
+ settings.enabledPlugins[PLUGIN_ID] = true;
218
+ settingsChanged = true;
219
+ }
220
+
221
+ // 4. Write settings if changed
222
+ if (settingsChanged) {
223
+ writeJsonAtomic(SETTINGS_PATH, settings);
224
+ }
225
+
226
+ // 5. Clear update-check cache (force re-check after update)
227
+ const updateCache = path.join(CACHE_DIR, 'update-check');
228
+ try { fs.unlinkSync(updateCache); } catch { /* ok */ }
229
+
230
+ // 6. Update manifest
231
+ manifest.version = version;
232
+ manifest.updatedAt = new Date().toISOString();
233
+ writeManifest(manifest);
234
+
235
+ return { oldVersion, version, settingsChanged };
236
+ }
237
+
238
+ module.exports = {
239
+ install, uninstall, update,
240
+ readManifest, readJson, writeJsonAtomic,
241
+ readRegistry, writeRegistry,
242
+ getPluginVersion,
243
+ PLUGIN_ID, CACHE_DIR, REGISTRY_FILE,
244
+ };
245
+
246
+ // CLI: node lifecycle.js <install|uninstall|update>
247
+ if (require.main === module) {
248
+ const cmd = process.argv[2];
249
+ if (cmd === 'install') {
250
+ const r = install();
251
+ console.log(`Installed v${r.version} | settings=${r.settingsChanged} | statusLine=${r.statusLineClaimed}`);
252
+ } else if (cmd === 'uninstall') {
253
+ const r = uninstall();
254
+ console.log(`Uninstalled | settings cleaned=${r.settingsChanged}`);
255
+ } else if (cmd === 'update') {
256
+ const r = update();
257
+ console.log(`Updated ${r.oldVersion} → ${r.version} | settings=${r.settingsChanged}`);
258
+ } else {
259
+ console.error('Usage: lifecycle.js <install|uninstall|update>');
260
+ process.exit(1);
261
+ }
262
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ const { execFileSync } = require('child_process');
4
+ const { findBinary } = require('./find-binary');
5
+ const { install, update, readManifest, getPluginVersion } = require('./lifecycle');
6
+ const { checkForUpdate } = require('./auto-update');
7
+
8
+ const BIN = findBinary();
9
+
10
+ // --- 1. Health check (always runs) ---
11
+ if (BIN) {
12
+ try {
13
+ const out = execFileSync(BIN, ['health-check', '--format', 'oneline'], {
14
+ timeout: 2000,
15
+ stdio: ['pipe', 'pipe', 'pipe']
16
+ }).toString().trim();
17
+ if (out) process.stdout.write(out);
18
+ } catch { /* timeout — silent */ }
19
+ }
20
+
21
+ // --- 2. Lifecycle: install or update config (idempotent) ---
22
+ const manifest = readManifest();
23
+ const currentVersion = getPluginVersion();
24
+
25
+ if (!manifest.version) {
26
+ install();
27
+ } else if (manifest.version !== currentVersion) {
28
+ update();
29
+ }
30
+
31
+ // --- 3. Auto-update (throttled, non-blocking) ---
32
+ (async () => {
33
+ const result = await checkForUpdate();
34
+ if (result && result.updated) {
35
+ process.stderr.write(`[code-graph] Updated: v${result.from} \u2192 v${result.to}\n`);
36
+ } else if (result && result.updateAvailable) {
37
+ process.stderr.write(
38
+ `[code-graph] Update available: v${result.from} \u2192 v${result.to}. ` +
39
+ `Run: npx @sdsrs/code-graph@latest\n`
40
+ );
41
+ }
42
+ })();
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * Composite StatusLine — combines multiple statusline providers.
5
+ * Reads stdin (JSON context from Claude Code), pipes to the primary
6
+ * statusline (GSD), then appends code-graph status.
7
+ */
8
+ const { execFileSync } = require('child_process');
9
+ const path = require('path');
10
+ const { readRegistry } = require('./lifecycle');
11
+
12
+ const SEPARATOR = ' \x1b[2m|\x1b[0m ';
13
+
14
+ // Collect stdin (Claude Code pipes JSON context)
15
+ let stdinData = '';
16
+ let ran = false;
17
+ const stdinTimeout = setTimeout(() => { if (!ran) { ran = true; run(''); } }, 2000);
18
+ process.stdin.setEncoding('utf8');
19
+ process.stdin.on('data', (chunk) => { stdinData += chunk; });
20
+ process.stdin.on('end', () => { clearTimeout(stdinTimeout); if (!ran) { ran = true; run(stdinData); } });
21
+
22
+ function run(stdin) {
23
+ const registry = readRegistry();
24
+ if (registry.length === 0) {
25
+ // Fallback: no registry, run code-graph only
26
+ const cg = runProvider(codeGraphCommand(), false, stdin);
27
+ if (cg) process.stdout.write(cg);
28
+ return;
29
+ }
30
+
31
+ const outputs = [];
32
+ for (const provider of registry) {
33
+ const out = runProvider(provider.command, provider.needsStdin, stdin);
34
+ if (out) outputs.push(out);
35
+ }
36
+ if (outputs.length > 0) {
37
+ process.stdout.write(outputs.join(SEPARATOR));
38
+ }
39
+ }
40
+
41
+ function runProvider(command, needsStdin, stdin) {
42
+ if (!command) return null;
43
+ try {
44
+ // Parse command into executable + args
45
+ const parts = parseCommand(command);
46
+ if (!parts) return null;
47
+
48
+ const out = execFileSync(parts[0], parts.slice(1), {
49
+ timeout: 3000,
50
+ stdio: ['pipe', 'pipe', 'pipe'],
51
+ input: needsStdin ? stdin : '',
52
+ }).toString().trim();
53
+
54
+ return out || null;
55
+ } catch { return null; }
56
+ }
57
+
58
+ function parseCommand(cmd) {
59
+ // Handle: node "/path/to/script.js"
60
+ const match = cmd.match(/^(\S+)\s+"([^"]+)"(.*)$/);
61
+ if (match) {
62
+ const args = [match[2]];
63
+ if (match[3].trim()) args.push(...match[3].trim().split(/\s+/));
64
+ return [match[1], ...args];
65
+ }
66
+ // Handle: node /path/to/script.js
67
+ const parts = cmd.split(/\s+/);
68
+ return parts.length > 0 ? parts : null;
69
+ }
70
+
71
+ function codeGraphCommand() {
72
+ const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || path.resolve(__dirname, '..');
73
+ return `node "${path.join(pluginRoot, 'scripts', 'statusline.js')}"`;
74
+ }
75
+
76
+ module.exports = { run };
@@ -1,7 +1,16 @@
1
1
  #!/usr/bin/env node
2
- const { execSync } = require('child_process');
2
+ 'use strict';
3
+ const { execFileSync } = require('child_process');
4
+ const { findBinary } = require('./find-binary');
5
+
6
+ const bin = findBinary();
7
+ if (!bin) {
8
+ process.stdout.write('code-graph: offline');
9
+ process.exit(0);
10
+ }
11
+
3
12
  try {
4
- const out = execSync('code-graph-mcp health-check --format json', {
13
+ const out = execFileSync(bin, ['health-check', '--format', 'json'], {
5
14
  timeout: 3000,
6
15
  stdio: ['pipe', 'pipe', 'pipe']
7
16
  }).toString().trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.4.0",
3
+ "version": "0.4.3",
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": {
@@ -33,10 +33,10 @@
33
33
  "node": ">=16"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@sdsrs/code-graph-linux-x64": "0.4.0",
37
- "@sdsrs/code-graph-linux-arm64": "0.4.0",
38
- "@sdsrs/code-graph-darwin-x64": "0.4.0",
39
- "@sdsrs/code-graph-darwin-arm64": "0.4.0",
40
- "@sdsrs/code-graph-win32-x64": "0.4.0"
36
+ "@sdsrs/code-graph-linux-x64": "0.4.3",
37
+ "@sdsrs/code-graph-linux-arm64": "0.4.3",
38
+ "@sdsrs/code-graph-darwin-x64": "0.4.3",
39
+ "@sdsrs/code-graph-darwin-arm64": "0.4.3",
40
+ "@sdsrs/code-graph-win32-x64": "0.4.3"
41
41
  }
42
42
  }