@sdsrs/code-graph 0.7.9 → 0.7.11
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/hooks/hooks.json +5 -38
- package/claude-plugin/scripts/mcp-launcher.js +23 -0
- package/claude-plugin/scripts/pre-edit-guide.js +97 -13
- package/claude-plugin/scripts/session-init.js +66 -4
- package/claude-plugin/scripts/session-init.test.js +22 -1
- package/claude-plugin/scripts/user-prompt-context.js +92 -31
- package/package.json +6 -6
- package/claude-plugin/scripts/pre-explore-guide.js +0 -24
- package/claude-plugin/scripts/pre-glob-guide.js +0 -38
- package/claude-plugin/scripts/pre-search-guide.js +0 -39
|
@@ -2,48 +2,15 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "tool == \"
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-search-guide.js\"",
|
|
10
|
-
"timeout": 2
|
|
11
|
-
}
|
|
12
|
-
],
|
|
13
|
-
"description": "Suggest code-graph alternatives on first Grep call"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"matcher": "tool == \"Glob\"",
|
|
17
|
-
"hooks": [
|
|
18
|
-
{
|
|
19
|
-
"type": "command",
|
|
20
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-glob-guide.js\"",
|
|
21
|
-
"timeout": 2
|
|
22
|
-
}
|
|
23
|
-
],
|
|
24
|
-
"description": "Suggest code-graph alternatives on first Glob call"
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
"matcher": "tool == \"Agent\"",
|
|
28
|
-
"hooks": [
|
|
29
|
-
{
|
|
30
|
-
"type": "command",
|
|
31
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-explore-guide.js\"",
|
|
32
|
-
"timeout": 2
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
"description": "Suggest code-graph tools before spawning Explore agents"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"matcher": "tool == \"Edit\" || tool == \"Write\"",
|
|
5
|
+
"matcher": "tool == \"Edit\"",
|
|
39
6
|
"hooks": [
|
|
40
7
|
{
|
|
41
8
|
"type": "command",
|
|
42
9
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/pre-edit-guide.js\"",
|
|
43
|
-
"timeout":
|
|
10
|
+
"timeout": 4
|
|
44
11
|
}
|
|
45
12
|
],
|
|
46
|
-
"description": "
|
|
13
|
+
"description": "Auto-inject impact analysis when editing function definitions with 2+ callers"
|
|
47
14
|
}
|
|
48
15
|
],
|
|
49
16
|
"PostToolUse": [
|
|
@@ -66,10 +33,10 @@
|
|
|
66
33
|
{
|
|
67
34
|
"type": "command",
|
|
68
35
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/user-prompt-context.js\"",
|
|
69
|
-
"timeout":
|
|
36
|
+
"timeout": 5
|
|
70
37
|
}
|
|
71
38
|
],
|
|
72
|
-
"description": "Inject
|
|
39
|
+
"description": "Inject code-graph structural context (impact, overview, callgraph) based on user intent"
|
|
73
40
|
}
|
|
74
41
|
],
|
|
75
42
|
"SessionStart": [
|
|
@@ -49,6 +49,23 @@ if (!binary) {
|
|
|
49
49
|
process.exit(1);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Pre-spawn: verify binary is executable (catches macOS quarantine, permission issues)
|
|
53
|
+
try {
|
|
54
|
+
fs.accessSync(binary, fs.constants.X_OK);
|
|
55
|
+
} catch {
|
|
56
|
+
process.stderr.write(`[code-graph] Binary not executable: ${binary}\n`);
|
|
57
|
+
if (process.platform === 'darwin') {
|
|
58
|
+
process.stderr.write(
|
|
59
|
+
'macOS may be quarantining the downloaded binary. Fix with:\n' +
|
|
60
|
+
` xattr -d com.apple.quarantine "${binary}"\n` +
|
|
61
|
+
` chmod +x "${binary}"\n`
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
process.stderr.write(`Fix: chmod +x "${binary}"\n`);
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
// Spawn binary with stdio inheritance for MCP JSON-RPC
|
|
53
70
|
const child = spawn(binary, ['serve'], {
|
|
54
71
|
stdio: 'inherit',
|
|
@@ -57,6 +74,12 @@ const child = spawn(binary, ['serve'], {
|
|
|
57
74
|
|
|
58
75
|
child.on('error', (err) => {
|
|
59
76
|
process.stderr.write(`[code-graph] Failed to start: ${err.message}\n`);
|
|
77
|
+
if (process.platform === 'darwin' && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
'macOS may be blocking this binary. Try:\n' +
|
|
80
|
+
` xattr -d com.apple.quarantine "${binary}"\n`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
60
83
|
process.exit(1);
|
|
61
84
|
});
|
|
62
85
|
|
|
@@ -1,21 +1,105 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// PreToolUse hook:
|
|
4
|
-
//
|
|
3
|
+
// PreToolUse(Edit) hook: auto-inject impact analysis when editing function definitions.
|
|
4
|
+
// Only fires when:
|
|
5
|
+
// 1. The old_string contains a function/method definition (signature being modified)
|
|
6
|
+
// 2. The symbol has 2+ production callers (high impact)
|
|
7
|
+
// 3. Same symbol not queried in last 2 minutes
|
|
8
|
+
// Silently exits otherwise — zero noise for normal edits.
|
|
9
|
+
const { execFileSync } = require('child_process');
|
|
5
10
|
const fs = require('fs');
|
|
6
11
|
const path = require('path');
|
|
7
12
|
const os = require('os');
|
|
8
13
|
|
|
9
|
-
const
|
|
10
|
-
const
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
16
|
+
if (!fs.existsSync(dbPath)) process.exit(0);
|
|
11
17
|
|
|
18
|
+
// --- Parse tool input ---
|
|
19
|
+
let input;
|
|
12
20
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
22
|
+
} catch { process.exit(0); }
|
|
23
|
+
|
|
24
|
+
const oldStr = (input.tool_input && input.tool_input.old_string) || '';
|
|
25
|
+
if (!oldStr || oldStr.length < 10) process.exit(0);
|
|
26
|
+
|
|
27
|
+
// --- Extract function/method signature from the edited text ---
|
|
28
|
+
// Match function definitions across languages: Rust, JS/TS, Python, Go, Java/C#/Kotlin, Ruby, PHP
|
|
29
|
+
const fnPatterns = [
|
|
30
|
+
/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/, // Rust
|
|
31
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // JS/TS
|
|
32
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|_)\s*=>/, // JS arrow
|
|
33
|
+
/(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // JS method / Go func
|
|
34
|
+
/def\s+(\w+)/, // Python/Ruby
|
|
35
|
+
/func\s+(\w+)/, // Go/Swift
|
|
36
|
+
/(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S+\s+(\w+)\s*\(/, // Java/C#/Kotlin
|
|
37
|
+
/(?:public\s+)?function\s+(\w+)/, // PHP
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
let symbol = null;
|
|
41
|
+
for (const pat of fnPatterns) {
|
|
42
|
+
const m = oldStr.match(pat);
|
|
43
|
+
if (m) {
|
|
44
|
+
// Find the first captured group
|
|
45
|
+
symbol = m[1] || m[2];
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!symbol || symbol.length < 3) process.exit(0);
|
|
51
|
+
|
|
52
|
+
// Skip common patterns that aren't real function names
|
|
53
|
+
if (/^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(symbol)) {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Per-symbol cooldown: 2 minutes ---
|
|
58
|
+
const cooldownFile = path.join(os.tmpdir(), `.cg-impact-${symbol}`);
|
|
59
|
+
try {
|
|
60
|
+
if (Date.now() - fs.statSync(cooldownFile).mtimeMs < 120000) process.exit(0);
|
|
61
|
+
} catch { /* first time for this symbol */ }
|
|
62
|
+
|
|
63
|
+
// --- Run impact analysis (JSON mode for programmatic parsing) ---
|
|
64
|
+
let jsonResult;
|
|
65
|
+
try {
|
|
66
|
+
const raw = execFileSync('code-graph-mcp', ['impact', symbol, '--json'], {
|
|
67
|
+
cwd,
|
|
68
|
+
timeout: 2500,
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
71
|
+
});
|
|
72
|
+
jsonResult = JSON.parse(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
// Symbol not found, timeout, or parse error — exit silently
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- Only inject if high-impact (2+ production callers) ---
|
|
79
|
+
const directCallers = jsonResult.direct_callers || 0;
|
|
80
|
+
const totalCallers = jsonResult.total_callers || 0;
|
|
81
|
+
const affectedFiles = jsonResult.affected_files || 0;
|
|
82
|
+
const risk = jsonResult.risk || 'low';
|
|
83
|
+
|
|
84
|
+
if (directCallers < 2) process.exit(0);
|
|
85
|
+
|
|
86
|
+
// Mark cooldown
|
|
87
|
+
try { fs.writeFileSync(cooldownFile, ''); } catch { /* ok */ }
|
|
88
|
+
|
|
89
|
+
// --- Inject compact impact summary ---
|
|
90
|
+
const routeCount = jsonResult.affected_routes || 0;
|
|
91
|
+
const testCount = jsonResult.tests_affected || 0;
|
|
92
|
+
|
|
93
|
+
let summary = `[code-graph:impact] ${symbol}() — Risk: ${risk}\n`;
|
|
94
|
+
summary += ` ${directCallers} direct callers, ${totalCallers} total across ${affectedFiles} files`;
|
|
95
|
+
if (routeCount > 0) summary += `, ${routeCount} routes affected`;
|
|
96
|
+
if (testCount > 0) summary += ` (${testCount} tests)`;
|
|
97
|
+
summary += '\n';
|
|
98
|
+
|
|
99
|
+
// List direct callers compactly
|
|
100
|
+
const callers = (jsonResult.callers || []).filter(c => c.depth === 1);
|
|
101
|
+
if (callers.length > 0) {
|
|
102
|
+
summary += ' Callers: ' + callers.map(c => `${c.name} (${c.file})`).join(', ') + '\n';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write(summary);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
const { spawn, execSync } = require('child_process');
|
|
3
|
+
const { spawn, execSync, execFileSync } = require('child_process');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs');
|
|
@@ -82,6 +82,63 @@ function ensureIndexFresh() {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Verify binary is available and executable.
|
|
87
|
+
* On macOS, detect Gatekeeper quarantine (common after npm/GitHub download).
|
|
88
|
+
* Returns { available, binary, issue? }.
|
|
89
|
+
*/
|
|
90
|
+
function verifyBinary() {
|
|
91
|
+
const { findBinary } = require('./find-binary');
|
|
92
|
+
const binary = findBinary();
|
|
93
|
+
if (!binary) {
|
|
94
|
+
process.stderr.write(
|
|
95
|
+
'[code-graph] Binary not found — MCP server cannot start.\n' +
|
|
96
|
+
'Install: npm install -g @sdsrs/code-graph\n'
|
|
97
|
+
);
|
|
98
|
+
return { available: false, binary: null };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check executable permission
|
|
102
|
+
try {
|
|
103
|
+
fs.accessSync(binary, fs.constants.X_OK);
|
|
104
|
+
} catch {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`[code-graph] Binary not executable: ${binary}\n` +
|
|
107
|
+
`Fix: chmod +x "${binary}"\n`
|
|
108
|
+
);
|
|
109
|
+
if (process.platform === 'darwin') {
|
|
110
|
+
process.stderr.write(`Also try: xattr -d com.apple.quarantine "${binary}"\n`);
|
|
111
|
+
}
|
|
112
|
+
return { available: false, binary, issue: 'not-executable' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// On macOS, verify the binary can actually run (Gatekeeper may block it)
|
|
116
|
+
if (process.platform === 'darwin') {
|
|
117
|
+
try {
|
|
118
|
+
execFileSync(binary, ['--version'], { timeout: 3000, stdio: 'pipe' });
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const msg = (err.message || '') + (err.stderr ? err.stderr.toString() : '');
|
|
121
|
+
if (msg.includes('quarantine') || msg.includes('not permitted') ||
|
|
122
|
+
msg.includes('killed') || err.status === 137 || err.signal === 'SIGKILL') {
|
|
123
|
+
process.stderr.write(
|
|
124
|
+
`[code-graph] macOS Gatekeeper is blocking the binary: ${binary}\n` +
|
|
125
|
+
`Fix: xattr -d com.apple.quarantine "${binary}"\n` +
|
|
126
|
+
`Then restart Claude Code to reconnect the MCP server.\n`
|
|
127
|
+
);
|
|
128
|
+
return { available: false, binary, issue: 'quarantine' };
|
|
129
|
+
}
|
|
130
|
+
// Other errors (e.g., missing libs) — still report
|
|
131
|
+
process.stderr.write(
|
|
132
|
+
`[code-graph] Binary found but failed to run: ${binary}\n` +
|
|
133
|
+
`Error: ${msg.slice(0, 200)}\n`
|
|
134
|
+
);
|
|
135
|
+
return { available: false, binary, issue: 'runtime-error' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { available: true, binary };
|
|
140
|
+
}
|
|
141
|
+
|
|
85
142
|
function runSessionInit() {
|
|
86
143
|
if (isPluginInactive()) {
|
|
87
144
|
cleanupDisabledStatusline();
|
|
@@ -97,10 +154,14 @@ function runSessionInit() {
|
|
|
97
154
|
}
|
|
98
155
|
|
|
99
156
|
const lifecycle = syncLifecycleConfig();
|
|
157
|
+
|
|
158
|
+
// Verify binary availability — catch issues early with actionable diagnostics
|
|
159
|
+
const binaryCheck = verifyBinary();
|
|
160
|
+
|
|
100
161
|
const autoUpdateLaunched = launchBackgroundAutoUpdate();
|
|
101
|
-
const indexFreshness = ensureIndexFresh();
|
|
102
|
-
const mapInjected = injectProjectMap();
|
|
103
|
-
return { inactive: false, lifecycle, autoUpdateLaunched, indexFreshness, mapInjected };
|
|
162
|
+
const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
|
|
163
|
+
const mapInjected = binaryCheck.available ? injectProjectMap() : false;
|
|
164
|
+
return { inactive: false, lifecycle, autoUpdateLaunched, indexFreshness, mapInjected, binaryCheck };
|
|
104
165
|
}
|
|
105
166
|
|
|
106
167
|
/**
|
|
@@ -137,6 +198,7 @@ module.exports = {
|
|
|
137
198
|
syncLifecycleConfig,
|
|
138
199
|
ensureIndexFresh,
|
|
139
200
|
injectProjectMap,
|
|
201
|
+
verifyBinary,
|
|
140
202
|
runSessionInit,
|
|
141
203
|
};
|
|
142
204
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const test = require('node:test');
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
4
|
|
|
5
|
-
const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh } = require('./session-init');
|
|
5
|
+
const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary } = require('./session-init');
|
|
6
6
|
|
|
7
7
|
test('syncLifecycleConfig is exported as a callable helper', () => {
|
|
8
8
|
assert.equal(typeof syncLifecycleConfig, 'function');
|
|
@@ -24,6 +24,27 @@ test('ensureIndexFresh returns skipped when no index exists', () => {
|
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test('verifyBinary returns available:true when binary is found and executable', () => {
|
|
28
|
+
const result = verifyBinary();
|
|
29
|
+
// In dev repo, binary should be found (target/release/code-graph-mcp)
|
|
30
|
+
if (result.available) {
|
|
31
|
+
assert.equal(typeof result.binary, 'string');
|
|
32
|
+
assert.ok(result.binary.length > 0);
|
|
33
|
+
} else {
|
|
34
|
+
// Binary not built — still verify the return shape
|
|
35
|
+
assert.equal(result.available, false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('verifyBinary returns structured result with expected shape', () => {
|
|
40
|
+
const result = verifyBinary();
|
|
41
|
+
assert.equal(typeof result.available, 'boolean');
|
|
42
|
+
assert.ok('binary' in result);
|
|
43
|
+
if (!result.available && result.binary) {
|
|
44
|
+
assert.ok('issue' in result);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
27
48
|
test('launchBackgroundAutoUpdate spawns detached silent updater', () => {
|
|
28
49
|
const calls = [];
|
|
29
50
|
|
|
@@ -1,20 +1,57 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// UserPromptSubmit hook: inject relevant code-graph
|
|
4
|
-
//
|
|
5
|
-
// This is a CODE INDEX
|
|
3
|
+
// UserPromptSubmit hook: inject relevant code-graph RESULTS based on user's intent.
|
|
4
|
+
// Strategy: PUSH structural context (not suggestions) that Grep/Read cannot provide.
|
|
5
|
+
// This is a CODE INDEX — only inject structural code context (impact, overview, callgraph).
|
|
6
6
|
const { execFileSync } = require('child_process');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
// ---
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
// --- Mid-session install detection ---
|
|
12
|
+
// If hooks are running but lifecycle install() hasn't executed yet (no manifest),
|
|
13
|
+
// the plugin was installed mid-session and the MCP server isn't connected.
|
|
14
|
+
// Claude Code only starts MCP servers at session startup; /mcp reconnect cannot
|
|
15
|
+
// start servers that were never initialized.
|
|
16
|
+
const MANIFEST_PATH = path.join(os.homedir(), '.cache', 'code-graph', 'install-manifest.json');
|
|
17
|
+
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
18
|
+
const noticeFile = path.join(os.tmpdir(), '.code-graph-mcp-restart-notice');
|
|
19
|
+
try {
|
|
20
|
+
// Show once per hour to avoid spam
|
|
21
|
+
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) process.exit(0);
|
|
22
|
+
} catch { /* first notice */ }
|
|
23
|
+
try { fs.writeFileSync(noticeFile, ''); } catch { /* ok */ }
|
|
24
|
+
process.stdout.write(
|
|
25
|
+
'[code-graph] Plugin installed — MCP server requires a session restart to connect.\n' +
|
|
26
|
+
'MCP servers are only initialized at session startup. To activate:\n' +
|
|
27
|
+
' 1. Press Ctrl+C to exit the current session\n' +
|
|
28
|
+
' 2. Re-run `claude` to start a new session\n' +
|
|
29
|
+
'Meanwhile, CLI tools work directly: code-graph-mcp search <query>, code-graph-mcp map, etc.\n'
|
|
30
|
+
);
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Per-type rate limiting (replaces single global cooldown) ---
|
|
35
|
+
const COOLDOWNS = {
|
|
36
|
+
impact: 30 * 1000, // 30s — impact context changes during rapid edits
|
|
37
|
+
overview: 5 * 60 * 1000, // 5min — module structure rarely changes mid-session
|
|
38
|
+
callgraph: 60 * 1000, // 1min
|
|
39
|
+
search: 60 * 1000, // 1min
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function isCoolingDown(type) {
|
|
43
|
+
try {
|
|
44
|
+
const flag = path.join(os.tmpdir(), `.code-graph-ctx-${type}`);
|
|
45
|
+
const stat = fs.statSync(flag);
|
|
46
|
+
return Date.now() - stat.mtimeMs < (COOLDOWNS[type] || 60000);
|
|
47
|
+
} catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function markCooldown(type) {
|
|
51
|
+
try {
|
|
52
|
+
fs.writeFileSync(path.join(os.tmpdir(), `.code-graph-ctx-${type}`), '');
|
|
53
|
+
} catch { /* ok */ }
|
|
54
|
+
}
|
|
18
55
|
|
|
19
56
|
// --- Read user message ---
|
|
20
57
|
let message;
|
|
@@ -64,51 +101,75 @@ const symbolCandidates = (message.match(/\b(?:[A-Z]\w*(?:::\w+)+|[a-z]\w*(?:_\w+
|
|
|
64
101
|
.filter(s => !STOP_WORDS.has(s.toLowerCase()))
|
|
65
102
|
.slice(0, 3);
|
|
66
103
|
|
|
104
|
+
// Fallback: plain lowercase words (8+ chars) likely to be function/type names.
|
|
105
|
+
// Only when strict patterns found nothing — avoids false positives from English prose.
|
|
106
|
+
// Minimum 8 chars filters most common English words while keeping technical terms
|
|
107
|
+
// (authenticate, serialize, initialize, dispatch, resolver, etc.)
|
|
108
|
+
if (symbolCandidates.length === 0) {
|
|
109
|
+
const plain = (message.match(/\b[a-z][a-z]{7,}\b/g) || [])
|
|
110
|
+
.filter(s => !STOP_WORDS.has(s))
|
|
111
|
+
.filter(s => !/^(possible|together|actually|something|different|important|following|available|necessary|currently|implement|operation|otherwise|beginning|knowledge|attention|according|certainly|sometimes|direction|recommend|structure|describe|question|complete|generate|anything|continue|consider|response|approach|happened|recently|probably|expected|previous|original|specific|directly|received|required|supposed|separate|designed|finished|provided|included|prepared|combined|properly|remember|whatever|although|document|handling|existing|everyone|standard|research|personal|relative|absolute|practice|language|thousand|national|evidence)$/.test(s));
|
|
112
|
+
symbolCandidates.push(...plain.slice(0, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
67
115
|
// Detect intent keywords (EN + ZH, derived from user's actual prompt history)
|
|
68
116
|
const intentImpact = /(?:impact|影响|修改前|改之前|blast radius|before (?:edit|chang|modif)|risk|风险|改动范围|波及|问题在|bug|干扰|冲突|卡)/i.test(message);
|
|
117
|
+
const intentModify = /(?:改(?!变)|修改|重构|\brefactor\b|\bchange\b|\brename\b|移动|\bmove\b|删(?!除文件)|\bremove\b|替换|\breplace\b|\bupdate\b|升级|\bmigrate\b|迁移|拆分|\bsplit\b|合并|\bmerge\b|提取|\bextract\b|改成|改为|换成|转为|异步|同步)/i.test(message);
|
|
69
118
|
const intentUnderstand = /(?:how does|怎么工作|怎么实现|怎么做|什么|理解|看看|看一下|了解|分析|explain|understand|架构|architecture|structure|overview|模块|概览|干什么|做什么|工作原理|逻辑|机制|流程|功能|结合度|效率|评估|调研|是什么|有什么|能用不|高效不|达标|起作用|科学|深入思考|源码)/i.test(message);
|
|
70
119
|
const intentCallgraph = /(?:who calls|what calls|调用|call(?:graph|er|ee)|trace|链路|追踪|谁调|被谁调|调了谁|上下游|依赖关系|触发|路径|覆盖|介入)/i.test(message);
|
|
71
120
|
const intentSearch = /(?:where is|在哪|find|search|搜索|找|locate|哪里用|哪里定义|定义在|实现在|处理没|在源码|加不加)/i.test(message);
|
|
72
121
|
|
|
73
122
|
// Need entities AND intent, or strong entity signal (qualified names like Foo::bar)
|
|
74
123
|
const hasQualifiedSymbol = symbolCandidates.some(s => s.includes('::'));
|
|
75
|
-
const hasIntent = intentImpact || intentUnderstand || intentCallgraph || intentSearch;
|
|
124
|
+
const hasIntent = intentImpact || intentModify || intentUnderstand || intentCallgraph || intentSearch;
|
|
76
125
|
if (!hasIntent && !hasQualifiedSymbol && filePaths.length === 0) {
|
|
77
126
|
process.exit(0);
|
|
78
127
|
}
|
|
79
128
|
|
|
80
|
-
// ---
|
|
129
|
+
// --- Semantic output prefixes ---
|
|
130
|
+
const PREFIXES = {
|
|
131
|
+
impact: '[code-graph:impact] Blast radius — review before editing:',
|
|
132
|
+
overview: '[code-graph:structure] Module structure:',
|
|
133
|
+
callgraph: '[code-graph:callgraph] Call relationships:',
|
|
134
|
+
search: '[code-graph:search] Relevant code:',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// --- Run ONE targeted CLI query (per-type cooldown allows different types to fire) ---
|
|
138
|
+
let queryType = null;
|
|
81
139
|
let result = '';
|
|
82
140
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
result = run(
|
|
91
|
-
} else if ((
|
|
92
|
-
|
|
93
|
-
} else if (filePaths.length > 0) {
|
|
141
|
+
// Priority: impact/modify > callgraph > understand/overview > search
|
|
142
|
+
// intentModify + symbol → inject impact so Claude knows blast radius before editing
|
|
143
|
+
if ((intentImpact || intentModify) && symbolCandidates.length > 0 && !isCoolingDown('impact')) {
|
|
144
|
+
queryType = 'impact';
|
|
145
|
+
result = run('code-graph-mcp', ['impact', symbolCandidates[0]]);
|
|
146
|
+
} else if (intentCallgraph && symbolCandidates.length > 0 && !isCoolingDown('callgraph')) {
|
|
147
|
+
queryType = 'callgraph';
|
|
148
|
+
result = run('code-graph-mcp', ['callgraph', symbolCandidates[0], '--depth', '2']);
|
|
149
|
+
} else if (filePaths.length > 0 && (intentUnderstand || !hasIntent) && !isCoolingDown('overview')) {
|
|
150
|
+
queryType = 'overview';
|
|
94
151
|
const dir = filePaths[0].replace(/\/[^/]+$/, '/');
|
|
95
|
-
result = run(
|
|
152
|
+
result = run('code-graph-mcp', ['overview', dir]);
|
|
153
|
+
} else if ((intentSearch || hasQualifiedSymbol) && symbolCandidates.length > 0 && !isCoolingDown('search')) {
|
|
154
|
+
queryType = 'search';
|
|
155
|
+
result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
|
|
156
|
+
} else if (intentUnderstand && symbolCandidates.length > 0 && !isCoolingDown('search')) {
|
|
157
|
+
queryType = 'search';
|
|
158
|
+
result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
|
|
96
159
|
}
|
|
97
160
|
} catch {
|
|
98
161
|
process.exit(0);
|
|
99
162
|
}
|
|
100
163
|
|
|
101
|
-
if (result && result.trim()) {
|
|
102
|
-
|
|
103
|
-
process.stdout.write(result.trim()
|
|
164
|
+
if (result && result.trim() && queryType) {
|
|
165
|
+
markCooldown(queryType);
|
|
166
|
+
process.stdout.write(`${PREFIXES[queryType]}\n${result.trim()}\n`);
|
|
104
167
|
}
|
|
105
168
|
|
|
106
169
|
// --- Helpers ---
|
|
107
170
|
|
|
108
|
-
function run(cmd) {
|
|
109
|
-
|
|
110
|
-
const args = parts.slice(1).map(a => a.replace(/^"|"$/g, ''));
|
|
111
|
-
return execFileSync(parts[0], args, {
|
|
171
|
+
function run(cmd, args) {
|
|
172
|
+
return execFileSync(cmd, args, {
|
|
112
173
|
cwd,
|
|
113
174
|
timeout: 3000,
|
|
114
175
|
encoding: 'utf8',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.11",
|
|
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.7.
|
|
37
|
-
"@sdsrs/code-graph-linux-arm64": "0.7.
|
|
38
|
-
"@sdsrs/code-graph-darwin-x64": "0.7.
|
|
39
|
-
"@sdsrs/code-graph-darwin-arm64": "0.7.
|
|
40
|
-
"@sdsrs/code-graph-win32-x64": "0.7.
|
|
36
|
+
"@sdsrs/code-graph-linux-x64": "0.7.11",
|
|
37
|
+
"@sdsrs/code-graph-linux-arm64": "0.7.11",
|
|
38
|
+
"@sdsrs/code-graph-darwin-x64": "0.7.11",
|
|
39
|
+
"@sdsrs/code-graph-darwin-arm64": "0.7.11",
|
|
40
|
+
"@sdsrs/code-graph-win32-x64": "0.7.11"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// PreToolUse hook: On FIRST Agent call per session window, suggest
|
|
4
|
-
// code-graph CLI commands for structural code understanding before spawning agents.
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const os = require('os');
|
|
8
|
-
|
|
9
|
-
const flag = path.join(os.tmpdir(), '.code-graph-explore-guided');
|
|
10
|
-
const WINDOW_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const stat = fs.statSync(flag);
|
|
14
|
-
if (Date.now() - stat.mtimeMs < WINDOW_MS) process.exit(0);
|
|
15
|
-
} catch { /* first time */ }
|
|
16
|
-
|
|
17
|
-
fs.writeFileSync(flag, '');
|
|
18
|
-
process.stdout.write(
|
|
19
|
-
'[code-graph] For code structure understanding, try CLI first (one Bash call vs agent):\n' +
|
|
20
|
-
' code-graph-mcp map \u2190 full architecture overview\n' +
|
|
21
|
-
' code-graph-mcp overview src/module \u2190 module structure and exports\n' +
|
|
22
|
-
' code-graph-mcp callgraph symbol \u2190 trace call chains\n' +
|
|
23
|
-
'Explore agents remain best for: non-code files, runtime behavior, open-ended investigation.\n'
|
|
24
|
-
);
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// PreToolUse hook: On FIRST Glob call per session window, suggest
|
|
4
|
-
// code-graph CLI commands — but only when exploring project structure,
|
|
5
|
-
// NOT finding specific files by name.
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const os = require('os');
|
|
9
|
-
|
|
10
|
-
const flag = path.join(os.tmpdir(), '.code-graph-glob-guided');
|
|
11
|
-
const WINDOW_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const stat = fs.statSync(flag);
|
|
15
|
-
if (Date.now() - stat.mtimeMs < WINDOW_MS) process.exit(0);
|
|
16
|
-
} catch { /* first time */ }
|
|
17
|
-
|
|
18
|
-
// Parse tool input to detect intent — skip for specific file lookups
|
|
19
|
-
try {
|
|
20
|
-
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
21
|
-
const pattern = (input && input.tool_input && input.tool_input.pattern) || '';
|
|
22
|
-
// Skip suggestion for: specific file patterns (has extension), config files, specific names
|
|
23
|
-
if (/\.(json|yaml|yml|toml|md|txt|env|lock|config|rc)$/i.test(pattern)) {
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
26
|
-
// Skip for patterns with specific filenames (not just wildcards like **/*.ts)
|
|
27
|
-
if (!pattern.includes('*') && /[\w-]+\.\w{1,5}$/.test(pattern)) {
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
} catch { /* stdin not available or parse error — show guide anyway */ }
|
|
31
|
-
|
|
32
|
-
fs.writeFileSync(flag, '');
|
|
33
|
-
process.stdout.write(
|
|
34
|
-
'[code-graph] If exploring project structure (not finding specific files):\n' +
|
|
35
|
-
' code-graph-mcp map \u2190 project architecture (modules, deps, entry points)\n' +
|
|
36
|
-
' code-graph-mcp overview src/mcp \u2190 module symbols grouped by file and type\n' +
|
|
37
|
-
'Glob remains best for: finding specific files, configs, non-code assets.\n'
|
|
38
|
-
);
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
// PreToolUse hook: On FIRST Grep call per session window, suggest
|
|
4
|
-
// code-graph CLI commands — but only when the pattern looks like code understanding
|
|
5
|
-
// (function names, module patterns), NOT exact string/constant searches.
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const os = require('os');
|
|
9
|
-
|
|
10
|
-
const flag = path.join(os.tmpdir(), '.code-graph-search-guided');
|
|
11
|
-
const WINDOW_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const stat = fs.statSync(flag);
|
|
15
|
-
if (Date.now() - stat.mtimeMs < WINDOW_MS) process.exit(0);
|
|
16
|
-
} catch { /* first time */ }
|
|
17
|
-
|
|
18
|
-
// Parse tool input to detect intent — skip for literal/constant searches
|
|
19
|
-
try {
|
|
20
|
-
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
21
|
-
const pattern = (input && input.tool_input && input.tool_input.pattern) || '';
|
|
22
|
-
// Skip suggestion for: quoted strings, TODO/FIXME, constants, exact literals, error messages
|
|
23
|
-
if (/^["']|^(TODO|FIXME|HACK|WARN|ERROR|const )|^\w+[=:]/i.test(pattern)) {
|
|
24
|
-
process.exit(0);
|
|
25
|
-
}
|
|
26
|
-
// Skip for very short patterns (likely exact match)
|
|
27
|
-
if (pattern.length <= 3) {
|
|
28
|
-
process.exit(0);
|
|
29
|
-
}
|
|
30
|
-
} catch { /* stdin not available or parse error — show guide anyway */ }
|
|
31
|
-
|
|
32
|
-
fs.writeFileSync(flag, '');
|
|
33
|
-
process.stdout.write(
|
|
34
|
-
'[code-graph] CLI commands for code understanding (via Bash):\n' +
|
|
35
|
-
' code-graph-mcp grep "pattern" \u2190 AST context grep (match + containing function/class)\n' +
|
|
36
|
-
' code-graph-mcp search "concept" \u2190 semantic search (find code by concept, not exact name)\n' +
|
|
37
|
-
' code-graph-mcp callgraph symbol \u2190 call chain tracing\n' +
|
|
38
|
-
'Grep remains best for: exact strings, constants, regex, non-code files.\n'
|
|
39
|
-
);
|