@sdsrs/code-graph 0.4.2 → 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 +86 -37
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +5 -4
- package/claude-plugin/scripts/auto-update.js +243 -0
- package/claude-plugin/scripts/find-binary.js +91 -0
- package/claude-plugin/scripts/incremental-index.js +14 -0
- package/claude-plugin/scripts/lifecycle.js +262 -0
- package/claude-plugin/scripts/session-init.js +42 -0
- package/claude-plugin/scripts/statusline-composite.js +76 -0
- package/claude-plugin/scripts/statusline.js +11 -2
- package/package.json +6 -6
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "
|
|
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": "
|
|
22
|
-
"timeout":
|
|
22
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-init.js\"",
|
|
23
|
+
"timeout": 8
|
|
23
24
|
}
|
|
24
25
|
],
|
|
25
|
-
"description": "
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
37
|
-
"@sdsrs/code-graph-linux-arm64": "0.4.
|
|
38
|
-
"@sdsrs/code-graph-darwin-x64": "0.4.
|
|
39
|
-
"@sdsrs/code-graph-darwin-arm64": "0.4.
|
|
40
|
-
"@sdsrs/code-graph-win32-x64": "0.4.
|
|
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
|
}
|