@sdsrs/code-graph 0.7.13 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.7.13",
7
+ "version": "0.7.15",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -47,13 +47,56 @@ for (const pat of fnPatterns) {
47
47
  }
48
48
  }
49
49
 
50
+ // Fallback: if old_string is inside a function body (not a definition),
51
+ // extract a unique identifier from the code and grep for it to find the containing function
52
+ if (!symbol || symbol.length < 3) {
53
+ const filePath = (input.tool_input && input.tool_input.file_path) || '';
54
+ if (filePath && oldStr.length >= 10) {
55
+ try {
56
+ // Extract identifiers from old_string, try the most specific one first
57
+ const identifiers = (oldStr.match(/\b([a-z]\w*(?:_\w+)+|[a-z]\w*(?:[A-Z]\w*)+|[A-Z]\w+\.\w+|[A-Z]\w+::\w+)\b/g) || [])
58
+ .filter(id => id.length >= 6);
59
+ const skipWords = new Set(['return', 'function', 'default', 'require', 'module', 'exports', 'import', 'console']);
60
+ // Sort by length descending (longer = more specific = fewer matches)
61
+ const candidates = [...new Set(identifiers)]
62
+ .filter(id => !skipWords.has(id.toLowerCase()))
63
+ .sort((a, b) => b.length - a.length);
64
+ for (const candidate of candidates.slice(0, 5)) {
65
+ try {
66
+ const raw = execFileSync('code-graph-mcp', ['grep', candidate, filePath, '--json'], {
67
+ cwd, timeout: 2000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+ const grepResult = JSON.parse(raw);
70
+ // Pick this candidate if it has few matches (precise location)
71
+ const withContainer = (grepResult || []).filter(m => m.container && m.container.name);
72
+ if (withContainer.length > 0 && withContainer.length <= 5) {
73
+ // If multiple containers, vote for the most common one
74
+ const votes = {};
75
+ for (const m of withContainer) {
76
+ const cn = m.container.name;
77
+ votes[cn] = (votes[cn] || 0) + 1;
78
+ }
79
+ const best = Object.entries(votes).sort((a, b) => b[1] - a[1])[0][0];
80
+ symbol = best.includes('.') ? best.split('.').pop() : best.includes('::') ? best.split('::').pop() : best;
81
+ break;
82
+ }
83
+ } catch { /* try next candidate */ }
84
+ }
85
+ } catch { /* grep failed or no match — fall through */ }
86
+ }
87
+ }
88
+
50
89
  if (!symbol || symbol.length < 3) process.exit(0);
51
90
 
52
91
  // 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)) {
92
+ if (isCommonKeyword(symbol)) {
54
93
  process.exit(0);
55
94
  }
56
95
 
96
+ function isCommonKeyword(s) {
97
+ return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
98
+ }
99
+
57
100
  // --- Per-symbol cooldown: 2 minutes ---
58
101
  const cooldownFile = path.join(os.tmpdir(), `.cg-impact-${symbol}`);
59
102
  try {
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+
5
+ // Pre-edit-guide.js is a script with side effects (reads stdin, checks db).
6
+ // We test its PATTERNS directly without requiring the module.
7
+
8
+ // --- Function signature patterns (copied from pre-edit-guide.js) ---
9
+ const fnPatterns = [
10
+ /(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/, // Rust
11
+ /(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // JS/TS
12
+ /(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|_)\s*=>/, // JS arrow
13
+ /(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // JS method / Go func
14
+ /def\s+(\w+)/, // Python/Ruby
15
+ /func\s+(\w+)/, // Go/Swift
16
+ /(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S+\s+(\w+)\s*\(/, // Java/C#/Kotlin
17
+ /(?:public\s+)?function\s+(\w+)/, // PHP
18
+ ];
19
+
20
+ function extractFunctionName(code) {
21
+ for (const pat of fnPatterns) {
22
+ const m = code.match(pat);
23
+ if (m) return m[1] || m[2];
24
+ }
25
+ return null;
26
+ }
27
+
28
+ function isCommonKeyword(s) {
29
+ return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
30
+ }
31
+
32
+ // ── Rust ────────────────────────────────────────────────
33
+
34
+ test('fn-extract: Rust pub fn', () => {
35
+ assert.equal(extractFunctionName('pub fn parse_code(input: &str) -> Vec<Node> {'), 'parse_code');
36
+ });
37
+
38
+ test('fn-extract: Rust pub async fn', () => {
39
+ assert.equal(extractFunctionName('pub async fn handle_message(&self, msg: &str) -> Result<()> {'), 'handle_message');
40
+ });
41
+
42
+ test('fn-extract: Rust fn (no pub)', () => {
43
+ assert.equal(extractFunctionName('fn helper_func(x: i32) -> i32 {'), 'helper_func');
44
+ });
45
+
46
+ // ── JavaScript/TypeScript ───────────────────────────────
47
+
48
+ test('fn-extract: JS function', () => {
49
+ assert.equal(extractFunctionName('function handleRequest(req, res) {'), 'handleRequest');
50
+ });
51
+
52
+ test('fn-extract: JS export function', () => {
53
+ assert.equal(extractFunctionName('export function processData(input) {'), 'processData');
54
+ });
55
+
56
+ test('fn-extract: JS async function', () => {
57
+ assert.equal(extractFunctionName('async function fetchData(url) {'), 'fetchData');
58
+ });
59
+
60
+ test('fn-extract: JS export async function', () => {
61
+ assert.equal(extractFunctionName('export async function loadConfig(path) {'), 'loadConfig');
62
+ });
63
+
64
+ test('fn-extract: JS arrow function (const)', () => {
65
+ assert.equal(extractFunctionName('const handleError = (err) => {'), 'handleError');
66
+ });
67
+
68
+ test('fn-extract: JS arrow function (async)', () => {
69
+ assert.equal(extractFunctionName('const fetchUser = async (id) => {'), 'fetchUser');
70
+ });
71
+
72
+ test('fn-extract: JS method', () => {
73
+ assert.equal(extractFunctionName(' handleMessage(msg) {'), 'handleMessage');
74
+ });
75
+
76
+ // ── Python ──────────────────────────────────────────────
77
+
78
+ test('fn-extract: Python def', () => {
79
+ assert.equal(extractFunctionName('def process_data(self, items):'), 'process_data');
80
+ });
81
+
82
+ test('fn-extract: Python async def', () => {
83
+ assert.equal(extractFunctionName('async def fetch_data(url):'), 'fetch_data');
84
+ });
85
+
86
+ // ── Go ──────────────────────────────────────────────────
87
+
88
+ test('fn-extract: Go func', () => {
89
+ assert.equal(extractFunctionName('func HandleRequest(w http.ResponseWriter, r *http.Request) {'), 'HandleRequest');
90
+ });
91
+
92
+ // ── Java/C#/Kotlin ──────────────────────────────────────
93
+
94
+ test('fn-extract: Java public method', () => {
95
+ assert.equal(extractFunctionName('public void processItem(Item item) {'), 'processItem');
96
+ });
97
+
98
+ test('fn-extract: Java private method', () => {
99
+ assert.equal(extractFunctionName('private String formatOutput(Data data) {'), 'formatOutput');
100
+ });
101
+
102
+ test('fn-extract: C# static method', () => {
103
+ assert.equal(extractFunctionName('static int CalculateTotal(List<int> items) {'), 'CalculateTotal');
104
+ });
105
+
106
+ // ── PHP ─────────────────────────────────────────────────
107
+
108
+ test('fn-extract: PHP function', () => {
109
+ assert.equal(extractFunctionName('function handleUpload($file) {'), 'handleUpload');
110
+ });
111
+
112
+ test('fn-extract: PHP public function', () => {
113
+ assert.equal(extractFunctionName('public function getUser($id) {'), 'getUser');
114
+ });
115
+
116
+ // ── Ruby ────────────────────────────────────────────────
117
+
118
+ test('fn-extract: Ruby def', () => {
119
+ assert.equal(extractFunctionName('def process_request(params)'), 'process_request');
120
+ });
121
+
122
+ // ── Keyword filter ──────────────────────────────────────
123
+
124
+ test('keyword-filter: common keywords rejected', () => {
125
+ for (const kw of ['if', 'for', 'while', 'switch', 'catch', 'else', 'return', 'new', 'get', 'set', 'try']) {
126
+ assert.ok(isCommonKeyword(kw), `"${kw}" should be rejected`);
127
+ }
128
+ });
129
+
130
+ test('keyword-filter: real function names pass', () => {
131
+ for (const name of ['parse_code', 'handleMessage', 'process_data', 'fetchUser']) {
132
+ assert.ok(!isCommonKeyword(name), `"${name}" should pass`);
133
+ }
134
+ });
135
+
136
+ // ── No false positives ──────────────────────────────────
137
+
138
+ test('fn-extract: plain code body returns null', () => {
139
+ assert.equal(extractFunctionName('let x = 42;\nreturn x + 1;'), null);
140
+ });
141
+
142
+ test('fn-extract: comment returns null', () => {
143
+ assert.equal(extractFunctionName('// This is a comment about the function'), null);
144
+ });
145
+
146
+ test('fn-extract: short strings return null', () => {
147
+ assert.equal(extractFunctionName('x = 1'), null);
148
+ });
149
+
150
+ // ── Pattern consistency check ───────────────────────────
151
+ // Verify fnPatterns in this test match what's in pre-edit-guide.js
152
+
153
+ test('pattern-sync: fnPatterns count matches source', () => {
154
+ const fs = require('node:fs');
155
+ const path = require('node:path');
156
+ const source = fs.readFileSync(path.join(__dirname, 'pre-edit-guide.js'), 'utf8');
157
+ // Count regex pattern lines in the fnPatterns array (lines containing // Language comment)
158
+ const sourcePatternCount = (source.match(/\/\/\s*(Rust|JS|Python|Go|Java|C#|PHP|Ruby|Swift|Kotlin)/g) || []).length;
159
+ assert.ok(fnPatterns.length === 8, `Expected 8 patterns, got ${fnPatterns.length}`);
160
+ assert.ok(sourcePatternCount >= 7, `Source should have >= 7 language comments, found ${sourcePatternCount}`);
161
+ });
@@ -69,7 +69,7 @@ const cwd = process.cwd();
69
69
  const dbPath = path.join(cwd, '.code-graph', 'index.db');
70
70
  if (!fs.existsSync(dbPath)) process.exit(0);
71
71
 
72
- // --- Constants ---
72
+ // --- Pure logic (exported for testing) ---
73
73
 
74
74
  const STOP_WORDS = new Set([
75
75
  'this', 'that', 'with', 'from', 'what', 'when', 'which', 'there',
@@ -79,93 +79,110 @@ const STOP_WORDS = new Set([
79
79
  'being', 'through', 'default', 'function', 'method', 'class',
80
80
  ]);
81
81
 
82
- // --- Detect intent + entities ---
82
+ const PLAIN_WORD_EXCLUDE = /^(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|refactor|understand|validate|analysis|debugging|configure|improving|resolving|creating|building|checking|updating|removing|changing|searching|cleaning|optimize|migration|overview|introduce|reviewing|thinking|managing|starting|yourself|features|problems|breaking|requires|argument|settings|includes|examples|comments|patterns|tutorial|concepts|supports|priority|organize|scenario|tracking|internal|external|abstract|concrete|strategy|evaluate|diagnose|platform|variable|optional|multiple)$/;
83
83
 
84
- // Skip non-code prompts (commit, push, simple confirmations, chat, instructions, etc.)
85
- const trimmed = message.trim();
86
- if (/^(yes|no|ok|commit|push|y|n|done|thanks|thank you|继续|确认|好的|好|是的|不|可以|行|对|提交|推送|没问题|谢谢|发布|更新|编译|安装|卸载|重启|重连|清理)\s*[.!?。!?]?\s*$/i.test(trimmed)) {
87
- process.exit(0);
84
+ function shouldSkip(msg) {
85
+ const trimmed = msg.trim();
86
+ if (/^(yes|no|ok|commit|push|y|n|done|thanks|thank you|继续|确认|好的|好|是的|不|可以|行|对|提交|推送|没问题|谢谢|发布|更新|编译|安装|卸载|重启|重连|清理)\s*[.!?。!?]?\s*$/i.test(trimmed)) return 'simple';
87
+ if (/^(修复|实施|执行|开始|按|实测|进入|用|重新)/.test(trimmed) && !/[a-zA-Z_]{3,}/.test(trimmed)) return 'action-only';
88
+ return false;
88
89
  }
89
- // Skip action-only prompts without code entities (修复这些问题, 按优先级实施, etc.)
90
- if (/^(修复|优化|实施|执行|开始|按|实测|帮我|进入|用|重新)/.test(trimmed) && !/[a-zA-Z_]{4,}/.test(trimmed)) {
91
- process.exit(0);
90
+
91
+ function extractFilePaths(msg) {
92
+ return (msg.match(/(?:src|lib|test|pkg|cmd|internal|app|components?)\/[\w/.-]+/g) || []).slice(0, 2);
92
93
  }
93
94
 
94
- // Extract file paths from message
95
- const filePaths = (message.match(/(?:src|lib|test|pkg|cmd|internal|app|components?)\/[\w/.-]+/g) || [])
96
- .slice(0, 2);
97
-
98
- // Extract potential symbol names (camelCase, snake_case, PascalCase, qualified like Foo::bar)
99
- const symbolCandidates = (message.match(/\b(?:[A-Z]\w*(?:::\w+)+|[a-z]\w*(?:_\w+){1,}|[a-z]\w*(?:[A-Z]\w*)+|[A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || [])
100
- .filter(s => s.length > 4)
101
- .filter(s => !STOP_WORDS.has(s.toLowerCase()))
102
- .slice(0, 3);
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));
95
+ function extractSymbols(msg) {
96
+ const candidates = (msg.match(/\b(?:[A-Z]\w*(?:(?:::|\.)\w+)+|[a-z]\w*(?:_\w+){1,}|[a-z]\w*(?:[A-Z]\w*)+|[A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || [])
97
+ .filter(s => s.length > 4)
98
+ .filter(s => !STOP_WORDS.has(s.toLowerCase()))
99
+ .slice(0, 3);
100
+
101
+ if (candidates.length === 0) {
102
+ const backtickSymbols = (msg.match(/`([a-zA-Z_]\w{2,})`/g) || [])
103
+ .map(s => s.replace(/`/g, ''))
104
+ .filter(s => s.length >= 3 && !STOP_WORDS.has(s.toLowerCase()));
105
+ candidates.push(...backtickSymbols.slice(0, 3));
106
+ }
107
+
108
+ let lowConfidence = false;
109
+ if (candidates.length === 0) {
110
+ const plain = (msg.match(/\b[a-z][a-z]{7,}\b/g) || [])
111
+ .filter(s => !STOP_WORDS.has(s))
112
+ .filter(s => !PLAIN_WORD_EXCLUDE.test(s));
113
+ candidates.push(...plain.slice(0, 2));
114
+ if (candidates.length > 0) lowConfidence = true;
115
+ }
116
+
117
+ return { symbols: candidates, lowConfidence };
113
118
  }
114
119
 
115
- // Detect intent keywords (EN + ZH, derived from user's actual prompt history)
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);
118
- const intentUnderstand = /(?:how does|怎么工作|怎么实现|怎么做|什么|理解|看看|看一下|了解|分析|explain|understand|架构|architecture|structure|overview|模块|概览|干什么|做什么|工作原理|逻辑|机制|流程|功能|结合度|效率|评估|调研|是什么|有什么|能用不|高效不|达标|起作用|科学|深入思考|源码)/i.test(message);
119
- const intentCallgraph = /(?:who calls|what calls|调用|call(?:graph|er|ee)|trace|链路|追踪|谁调|被谁调|调了谁|上下游|依赖关系|触发|路径|覆盖|介入)/i.test(message);
120
- const intentSearch = /(?:where is|在哪|find|search|搜索|找|locate|哪里用|哪里定义|定义在|实现在|处理没|在源码|加不加)/i.test(message);
121
-
122
- // Need entities AND intent, or strong entity signal (qualified names like Foo::bar)
123
- const hasQualifiedSymbol = symbolCandidates.some(s => s.includes('::'));
124
- const hasIntent = intentImpact || intentModify || intentUnderstand || intentCallgraph || intentSearch;
125
- if (!hasIntent && !hasQualifiedSymbol && filePaths.length === 0) {
126
- process.exit(0);
120
+ function detectIntents(msg) {
121
+ return {
122
+ impact: /(?:impact|影响|修改前|改之前|blast radius|before (?:edit|chang|modif)|risk|风险|改动范围|波及|问题在|bug|干扰|冲突|卡)/i.test(msg),
123
+ modify: /(?:改(?!变)|修改|修复|重构|优化|简化|精简|适配|统一|修正|调整|去掉|整理|清理|解耦|更新|\brefactor\b|\bchange\b|\brename\b|\bfix\b|移动|\bmove\b|删(?!除文件)|\bremove\b|替换|\breplace\b|\bupdate\b|升级|\bmigrate\b|迁移|拆分|\bsplit\b|合并|\bmerge\b|提取|\bextract\b|改成|改为|换成|转为|异步|同步)/i.test(msg),
124
+ implement: /(?:\badd\b|\bimplement\b|\bcreate\b|\bbuild\b|\bwrite\b|新增|添加|实现|创建|编写|开发|增加|加上|加个|写|做个|搭建|补充|引入|支持|封装|接入|对接|配置)/i.test(msg),
125
+ understand: /(?:how does|怎么工作|怎么实现|怎么做|什么|理解|看看|看一下|了解|分析|explain|understand|架构|architecture|structure|overview|模块|概览|干什么|做什么|工作原理|逻辑|机制|流程|功能|结合度|效率|评估|调研|是什么|有什么|能用不|高效不|达标|起作用|科学|深入思考|源码|检查|审核|审查|验证|诊断)/i.test(msg),
126
+ callgraph: /(?:who calls|what calls|调用|call(?:graph|er|ee)|trace|链路|追踪|谁调|被谁调|调了谁|上下游|依赖关系|触发|路径|覆盖|介入)/i.test(msg),
127
+ search: /(?:where is|在哪|find|search|搜索|找|locate|哪里用|哪里定义|定义在|实现在|处理没|在源码|加不加)/i.test(msg),
128
+ };
127
129
  }
128
130
 
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
- };
131
+ function determineQueryType(intents, symbols, filePaths, isCoolingDownFn) {
132
+ const hasStrict = symbols.symbols.length > 0 && !symbols.lowConfidence;
133
+ const hasQualified = symbols.symbols.some(s => s.includes('::'));
134
+ const hasAny = intents.impact || intents.modify || intents.implement || intents.understand || intents.callgraph || intents.search;
136
135
 
137
- // --- Run ONE targeted CLI query (per-type cooldown allows different types to fire) ---
138
- let queryType = null;
139
- let result = '';
140
- try {
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';
151
- const dir = filePaths[0].replace(/\/[^/]+$/, '/');
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']);
159
- }
160
- } catch {
161
- process.exit(0);
136
+ // Gate: need intent, qualified symbol, file path, or any symbol
137
+ if (!hasAny && !hasQualified && filePaths.length === 0 && symbols.symbols.length === 0) return null;
138
+
139
+ const cd = isCoolingDownFn || (() => false);
140
+
141
+ if ((intents.impact || intents.modify) && hasStrict && !cd('impact')) return { type: 'impact', symbol: symbols.symbols[0] };
142
+ if (intents.callgraph && hasStrict && !cd('callgraph')) return { type: 'callgraph', symbol: symbols.symbols[0] };
143
+ if (filePaths.length > 0 && !cd('overview')) return { type: 'overview', path: filePaths[0].replace(/\/[^/]+$/, '/') };
144
+ if ((intents.search || intents.implement || hasQualified) && symbols.symbols.length > 0 && !cd('search')) return { type: 'search', symbol: symbols.symbols[0] };
145
+ if ((intents.understand || !hasAny) && symbols.symbols.length > 0 && !cd('search')) return { type: 'search', symbol: symbols.symbols[0] };
146
+
147
+ return null;
162
148
  }
163
149
 
164
- if (result && result.trim() && queryType) {
165
- markCooldown(queryType);
166
- process.stdout.write(`${PREFIXES[queryType]}\n${result.trim()}\n`);
150
+ // --- Main execution (only when run directly) ---
151
+ if (require.main === module) {
152
+ if (shouldSkip(message)) process.exit(0);
153
+
154
+ const filePaths = extractFilePaths(message);
155
+ const symbols = extractSymbols(message);
156
+ const intents = detectIntents(message);
157
+ const query = determineQueryType(intents, symbols, filePaths, isCoolingDown);
158
+
159
+ if (!query) process.exit(0);
160
+
161
+ const PREFIXES = {
162
+ impact: '[code-graph:impact] Blast radius — review before editing:',
163
+ overview: '[code-graph:structure] Module structure:',
164
+ callgraph: '[code-graph:callgraph] Call relationships:',
165
+ search: '[code-graph:search] Relevant code:',
166
+ };
167
+
168
+ try {
169
+ let result = '';
170
+ if (query.type === 'impact') result = run('code-graph-mcp', ['impact', query.symbol]);
171
+ else if (query.type === 'callgraph') result = run('code-graph-mcp', ['callgraph', query.symbol, '--depth', '2']);
172
+ else if (query.type === 'overview') result = run('code-graph-mcp', ['overview', query.path]);
173
+ else if (query.type === 'search') result = run('code-graph-mcp', ['search', query.symbol, '--limit', '8']);
174
+
175
+ if (result && result.trim()) {
176
+ markCooldown(query.type);
177
+ process.stdout.write(`${PREFIXES[query.type]}\n${result.trim()}\n`);
178
+ }
179
+ } catch {
180
+ process.exit(0);
181
+ }
167
182
  }
168
183
 
184
+ module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, determineQueryType, STOP_WORDS, PLAIN_WORD_EXCLUDE };
185
+
169
186
  // --- Helpers ---
170
187
 
171
188
  function run(cmd, args) {
@@ -0,0 +1,466 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+
7
+ const {
8
+ shouldSkip,
9
+ extractFilePaths,
10
+ extractSymbols,
11
+ detectIntents,
12
+ determineQueryType,
13
+ } = require('./user-prompt-context');
14
+
15
+ // ── shouldSkip ──────────────────────────────────────────
16
+
17
+ test('shouldSkip: simple confirmations (EN)', () => {
18
+ for (const msg of ['yes', 'no', 'ok', 'done', 'y', 'n', 'commit', 'push', 'thanks']) {
19
+ assert.ok(shouldSkip(msg), `should skip "${msg}"`);
20
+ }
21
+ });
22
+
23
+ test('shouldSkip: simple confirmations (ZH)', () => {
24
+ for (const msg of ['继续', '确认', '好的', '好', '是的', '不', '可以', '行', '对', '提交', '推送', '没问题', '谢谢', '发布', '更新', '清理']) {
25
+ assert.ok(shouldSkip(msg), `should skip "${msg}"`);
26
+ }
27
+ });
28
+
29
+ test('shouldSkip: with trailing punctuation', () => {
30
+ assert.ok(shouldSkip('好的。'));
31
+ assert.ok(shouldSkip('ok!'));
32
+ assert.ok(shouldSkip('确认?'));
33
+ });
34
+
35
+ test('shouldSkip: action-only without code entities', () => {
36
+ assert.equal(shouldSkip('修复这些问题'), 'action-only');
37
+ assert.equal(shouldSkip('按优先级实施'), 'action-only');
38
+ assert.equal(shouldSkip('执行这个方案'), 'action-only');
39
+ assert.equal(shouldSkip('开始吧'), 'action-only');
40
+ });
41
+
42
+ test('shouldSkip: action with 3+ Latin chars passes through', () => {
43
+ assert.equal(shouldSkip('修复 parse_code 里的bug'), false);
44
+ assert.equal(shouldSkip('修复这段逻辑的bug'), false); // "bug" = 3 chars
45
+ assert.equal(shouldSkip('修复 API 的问题'), false); // "API" = 3 chars
46
+ });
47
+
48
+ test('shouldSkip: NOT skip legitimate code tasks', () => {
49
+ assert.equal(shouldSkip('帮我写一个工具函数'), false);
50
+ assert.equal(shouldSkip('帮我优化一下这个查询'), false);
51
+ assert.equal(shouldSkip('优化 parse_code 的性能'), false);
52
+ assert.equal(shouldSkip('看看 src/mcp/ 模块的代码结构'), false);
53
+ assert.equal(shouldSkip('重构一下这个模块'), false);
54
+ });
55
+
56
+ test('shouldSkip: messages below length threshold exit early in main', () => {
57
+ // The 8-char minimum is checked in the main block, not in shouldSkip
58
+ // shouldSkip itself doesn't enforce length
59
+ assert.equal(shouldSkip('短消息很短'), false); // passes shouldSkip but would exit in main
60
+ });
61
+
62
+ // ── extractFilePaths ────────────────────────────────────
63
+
64
+ test('extractFilePaths: extracts src/ paths', () => {
65
+ assert.deepEqual(extractFilePaths('看看 src/mcp/server.rs'), ['src/mcp/server.rs']);
66
+ assert.deepEqual(extractFilePaths('修改 src/parser/relations.rs 和 src/storage/db.rs'), ['src/parser/relations.rs', 'src/storage/db.rs']);
67
+ });
68
+
69
+ test('extractFilePaths: extracts lib/test/pkg paths', () => {
70
+ assert.deepEqual(extractFilePaths('check lib/utils/helpers.js'), ['lib/utils/helpers.js']);
71
+ assert.deepEqual(extractFilePaths('test/integration.rs is failing'), ['test/integration.rs']);
72
+ });
73
+
74
+ test('extractFilePaths: limits to 2 paths', () => {
75
+ const result = extractFilePaths('src/a.rs src/b.rs src/c.rs');
76
+ assert.equal(result.length, 2);
77
+ });
78
+
79
+ test('extractFilePaths: no match for non-code paths', () => {
80
+ assert.deepEqual(extractFilePaths('这个函数有问题'), []);
81
+ assert.deepEqual(extractFilePaths('update the readme'), []);
82
+ });
83
+
84
+ // ── extractSymbols ──────────────────────────────────────
85
+
86
+ test('extractSymbols: snake_case', () => {
87
+ const r = extractSymbols('修改 parse_code 函数');
88
+ assert.deepEqual(r.symbols, ['parse_code']);
89
+ assert.equal(r.lowConfidence, false);
90
+ });
91
+
92
+ test('extractSymbols: camelCase', () => {
93
+ const r = extractSymbols('fix the handleMessage function');
94
+ assert.ok(r.symbols.includes('handleMessage'));
95
+ assert.equal(r.lowConfidence, false);
96
+ });
97
+
98
+ test('extractSymbols: PascalCase compound', () => {
99
+ const r = extractSymbols('implement McpServer class');
100
+ assert.ok(r.symbols.includes('McpServer'));
101
+ });
102
+
103
+ test('extractSymbols: qualified names (Foo::bar)', () => {
104
+ const r = extractSymbols('check Foo::bar::baz');
105
+ assert.ok(r.symbols.some(s => s.includes('::')));
106
+ });
107
+
108
+ test('extractSymbols: backtick-quoted fallback', () => {
109
+ const r = extractSymbols('修改 `parse` 函数');
110
+ assert.ok(r.symbols.includes('parse'));
111
+ });
112
+
113
+ test('extractSymbols: backtick with longer name', () => {
114
+ const r = extractSymbols('看看 `fts5_search` 怎么实现的');
115
+ assert.ok(r.symbols.includes('fts5_search'));
116
+ });
117
+
118
+ test('extractSymbols: plain word fallback (low confidence)', () => {
119
+ const r = extractSymbols('write tests for the embedding module');
120
+ assert.ok(r.symbols.includes('embedding'));
121
+ assert.equal(r.lowConfidence, true);
122
+ });
123
+
124
+ test('extractSymbols: plain words excluded (common English verbs)', () => {
125
+ const r = extractSymbols('help me understand the refactor approach');
126
+ // "understand" and "refactor" are excluded, "approach" is excluded
127
+ assert.equal(r.symbols.length, 0);
128
+ });
129
+
130
+ test('extractSymbols: stop words filtered', () => {
131
+ const r = extractSymbols('fix the default function');
132
+ // "default" and "function" are stop words
133
+ assert.equal(r.symbols.length, 0);
134
+ });
135
+
136
+ test('extractSymbols: limits to 3 symbols', () => {
137
+ const r = extractSymbols('modify parse_code and run_full_index and extract_relations and hash_file');
138
+ assert.ok(r.symbols.length <= 3);
139
+ });
140
+
141
+ // ── detectIntents ───────────────────────────────────────
142
+
143
+ // --- Impact intent ---
144
+ test('detectIntents: impact (EN)', () => {
145
+ assert.ok(detectIntents('what is the impact of this change').impact);
146
+ assert.ok(detectIntents('check the risk of modifying this').impact);
147
+ assert.ok(detectIntents('this bug is critical').impact);
148
+ });
149
+
150
+ test('detectIntents: impact (ZH)', () => {
151
+ assert.ok(detectIntents('这个改动有什么影响').impact);
152
+ assert.ok(detectIntents('改动范围有多大').impact);
153
+ assert.ok(detectIntents('会不会跟其他模块冲突').impact);
154
+ assert.ok(detectIntents('修改前先看看').impact);
155
+ assert.ok(detectIntents('有什么风险').impact);
156
+ assert.ok(detectIntents('这个bug怎么回事').impact);
157
+ });
158
+
159
+ // --- Modify intent ---
160
+ test('detectIntents: modify (EN)', () => {
161
+ assert.ok(detectIntents('refactor this module').modify);
162
+ assert.ok(detectIntents('rename the function').modify);
163
+ assert.ok(detectIntents('fix the broken test').modify);
164
+ assert.ok(detectIntents('update the config').modify);
165
+ assert.ok(detectIntents('remove deprecated code').modify);
166
+ assert.ok(detectIntents('replace with new impl').modify);
167
+ });
168
+
169
+ test('detectIntents: modify (ZH)', () => {
170
+ const words = ['修改', '修复', '重构', '优化', '简化', '精简', '适配', '统一', '修正', '调整', '去掉', '整理', '清理', '解耦', '更新', '升级', '迁移', '拆分', '合并', '提取'];
171
+ for (const w of words) {
172
+ assert.ok(detectIntents(`${w}这个模块`).modify, `"${w}" should trigger modify`);
173
+ }
174
+ });
175
+
176
+ test('detectIntents: modify (ZH compound)', () => {
177
+ assert.ok(detectIntents('把这个函数改成异步的').modify);
178
+ assert.ok(detectIntents('把返回值类型换成 Result').modify);
179
+ assert.ok(detectIntents('把同步改成异步').modify);
180
+ });
181
+
182
+ // --- Implement intent ---
183
+ test('detectIntents: implement (EN)', () => {
184
+ assert.ok(detectIntents('add a new tool').implement);
185
+ assert.ok(detectIntents('implement error handling').implement);
186
+ assert.ok(detectIntents('create a helper function').implement);
187
+ assert.ok(detectIntents('build the CI pipeline').implement);
188
+ assert.ok(detectIntents('write unit tests').implement);
189
+ });
190
+
191
+ test('detectIntents: implement (ZH)', () => {
192
+ const words = ['新增', '添加', '实现', '创建', '编写', '开发', '增加', '加上', '加个', '搭建', '补充', '引入', '支持', '封装', '接入', '对接', '配置'];
193
+ for (const w of words) {
194
+ assert.ok(detectIntents(`${w}一个功能`).implement, `"${w}" should trigger implement`);
195
+ }
196
+ });
197
+
198
+ test('detectIntents: implement - "写" variants', () => {
199
+ assert.ok(detectIntents('写个测试').implement);
200
+ assert.ok(detectIntents('写一个工具函数').implement);
201
+ assert.ok(detectIntents('帮我写一个函数').implement);
202
+ });
203
+
204
+ // --- Understand intent ---
205
+ test('detectIntents: understand (EN)', () => {
206
+ assert.ok(detectIntents('how does this module work').understand);
207
+ assert.ok(detectIntents('explain the architecture').understand);
208
+ });
209
+
210
+ test('detectIntents: understand (ZH)', () => {
211
+ const words = ['看看', '看一下', '理解', '了解', '分析', '评估', '检查', '审核', '审查', '验证', '诊断', '深入思考'];
212
+ for (const w of words) {
213
+ assert.ok(detectIntents(`${w}这段代码`).understand, `"${w}" should trigger understand`);
214
+ }
215
+ });
216
+
217
+ test('detectIntents: understand (ZH question patterns)', () => {
218
+ assert.ok(detectIntents('这个模块是干什么的').understand);
219
+ assert.ok(detectIntents('工作原理是什么').understand);
220
+ assert.ok(detectIntents('整个流程是怎么走的').understand);
221
+ assert.ok(detectIntents('这个功能怎么实现的').understand);
222
+ });
223
+
224
+ // --- Callgraph intent ---
225
+ test('detectIntents: callgraph (EN)', () => {
226
+ assert.ok(detectIntents('who calls this function').callgraph);
227
+ assert.ok(detectIntents('what calls parse_code').callgraph);
228
+ assert.ok(detectIntents('trace the request flow').callgraph);
229
+ });
230
+
231
+ test('detectIntents: callgraph (ZH)', () => {
232
+ assert.ok(detectIntents('这个函数被谁调了').callgraph);
233
+ assert.ok(detectIntents('看看调用链路').callgraph);
234
+ assert.ok(detectIntents('追踪一下请求路径').callgraph);
235
+ assert.ok(detectIntents('上下游依赖关系是什么').callgraph);
236
+ assert.ok(detectIntents('这个事件怎么触发的').callgraph);
237
+ });
238
+
239
+ // --- Search intent ---
240
+ test('detectIntents: search (EN)', () => {
241
+ assert.ok(detectIntents('where is the config defined').search);
242
+ assert.ok(detectIntents('find the error handling code').search);
243
+ assert.ok(detectIntents('search for all usages').search);
244
+ });
245
+
246
+ test('detectIntents: search (ZH)', () => {
247
+ assert.ok(detectIntents('这个函数定义在哪').search);
248
+ assert.ok(detectIntents('找一下处理错误的代码').search);
249
+ assert.ok(detectIntents('搜索所有用到这个类型的地方').search);
250
+ assert.ok(detectIntents('在哪里用了这个常量').search);
251
+ });
252
+
253
+ // --- No false positives ---
254
+ test('detectIntents: simple confirmations have no code intent', () => {
255
+ const r = detectIntents('好的');
256
+ // "什么" would match in some words, but "好的" shouldn't trigger understand
257
+ assert.equal(r.modify, false);
258
+ assert.equal(r.implement, false);
259
+ assert.equal(r.callgraph, false);
260
+ assert.equal(r.search, false);
261
+ });
262
+
263
+ // ── determineQueryType (priority logic) ─────────────────
264
+
265
+ test('priority: impact/modify + strict symbol → impact', () => {
266
+ const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
267
+ const symbols = { symbols: ['parse_code'], lowConfidence: false };
268
+ const result = determineQueryType(intents, symbols, []);
269
+ assert.equal(result.type, 'impact');
270
+ assert.equal(result.symbol, 'parse_code');
271
+ });
272
+
273
+ test('priority: modify + strict symbol → impact', () => {
274
+ const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
275
+ const symbols = { symbols: ['handleMessage'], lowConfidence: false };
276
+ const result = determineQueryType(intents, symbols, []);
277
+ assert.equal(result.type, 'impact');
278
+ });
279
+
280
+ test('priority: modify + low-confidence symbol → NOT impact (falls to overview/search)', () => {
281
+ const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
282
+ const symbols = { symbols: ['embedding'], lowConfidence: true };
283
+ const result = determineQueryType(intents, symbols, ['src/embed/']);
284
+ // Should fall through to overview (file paths exist)
285
+ assert.equal(result.type, 'overview');
286
+ });
287
+
288
+ test('priority: callgraph + strict symbol → callgraph', () => {
289
+ const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: true, search: false };
290
+ const symbols = { symbols: ['parse_code'], lowConfidence: false };
291
+ const result = determineQueryType(intents, symbols, []);
292
+ assert.equal(result.type, 'callgraph');
293
+ });
294
+
295
+ test('priority: file paths → overview (regardless of intent)', () => {
296
+ const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
297
+ const symbols = { symbols: [], lowConfidence: false };
298
+ const result = determineQueryType(intents, symbols, ['src/storage/queries.rs']);
299
+ assert.equal(result.type, 'overview');
300
+ assert.equal(result.path, 'src/storage/');
301
+ });
302
+
303
+ test('priority: search intent + symbol → search', () => {
304
+ const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: true };
305
+ const symbols = { symbols: ['parse_code'], lowConfidence: false };
306
+ const result = determineQueryType(intents, symbols, []);
307
+ assert.equal(result.type, 'search');
308
+ });
309
+
310
+ test('priority: implement intent + symbol → search', () => {
311
+ const intents = { impact: false, modify: false, implement: true, understand: false, callgraph: false, search: false };
312
+ const symbols = { symbols: ['embedding'], lowConfidence: true };
313
+ const result = determineQueryType(intents, symbols, []);
314
+ assert.equal(result.type, 'search');
315
+ });
316
+
317
+ test('priority: understand + symbol → search', () => {
318
+ const intents = { impact: false, modify: false, implement: false, understand: true, callgraph: false, search: false };
319
+ const symbols = { symbols: ['pipeline'], lowConfidence: true };
320
+ const result = determineQueryType(intents, symbols, []);
321
+ assert.equal(result.type, 'search');
322
+ });
323
+
324
+ test('priority: no intent, no symbol, no path → null', () => {
325
+ const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
326
+ const symbols = { symbols: [], lowConfidence: false };
327
+ const result = determineQueryType(intents, symbols, []);
328
+ assert.equal(result, null);
329
+ });
330
+
331
+ test('priority: cooldown blocks query', () => {
332
+ const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
333
+ const symbols = { symbols: ['parse_code'], lowConfidence: false };
334
+ const result = determineQueryType(intents, symbols, [], (type) => type === 'impact');
335
+ // Impact blocked by cooldown, falls through; no file path, no search intent → try search via understand fallback
336
+ // Actually: no understand intent and hasAny=true, so the last condition (!hasAny) is false → null
337
+ // But symbol exists and we have filePaths=[] → falls to search via implement/qualified check → no
338
+ // Actually it should return null since all fallbacks require conditions not met
339
+ assert.equal(result, null);
340
+ });
341
+
342
+ test('priority: cooldown on impact → falls to overview when file paths exist', () => {
343
+ const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
344
+ const symbols = { symbols: ['parse_code'], lowConfidence: false };
345
+ const result = determineQueryType(intents, symbols, ['src/parser/mod.rs'], (type) => type === 'impact');
346
+ assert.equal(result.type, 'overview');
347
+ });
348
+
349
+ // ── Full integration: message → query type ──────────────
350
+
351
+ function analyze(msg) {
352
+ if (shouldSkip(msg)) return { skipped: true };
353
+ const fp = extractFilePaths(msg);
354
+ const sym = extractSymbols(msg);
355
+ const intents = detectIntents(msg);
356
+ const query = determineQueryType(intents, sym, fp);
357
+ return { query, intents, symbols: sym, filePaths: fp };
358
+ }
359
+
360
+ test('integration: 修改 parse_code 函数增加错误处理 → impact', () => {
361
+ const r = analyze('修改 parse_code 函数增加错误处理');
362
+ assert.equal(r.query.type, 'impact');
363
+ assert.equal(r.query.symbol, 'parse_code');
364
+ });
365
+
366
+ test('integration: 看看 src/mcp/ 模块的代码结构 → overview', () => {
367
+ const r = analyze('看看 src/mcp/ 模块的代码结构');
368
+ assert.equal(r.query.type, 'overview');
369
+ });
370
+
371
+ test('integration: refactor src/storage/queries.rs → overview (not impact on "refactor")', () => {
372
+ const r = analyze('refactor src/storage/queries.rs to use parameterized queries');
373
+ assert.equal(r.query.type, 'overview');
374
+ assert.ok(r.query.path.includes('src/storage/'));
375
+ });
376
+
377
+ test('integration: help me understand the indexer pipeline → search', () => {
378
+ const r = analyze('help me understand the indexer pipeline');
379
+ assert.equal(r.query.type, 'search');
380
+ assert.equal(r.query.symbol, 'pipeline');
381
+ });
382
+
383
+ test('integration: write tests for the embedding module → search', () => {
384
+ const r = analyze('write tests for the embedding module');
385
+ assert.equal(r.query.type, 'search');
386
+ assert.equal(r.query.symbol, 'embedding');
387
+ });
388
+
389
+ test('integration: 修复这段逻辑的bug → not skipped (bug=3 chars)', () => {
390
+ const r = analyze('修复这段逻辑的bug');
391
+ assert.ok(!r.skipped);
392
+ assert.ok(r.intents.impact); // "bug"
393
+ assert.ok(r.intents.modify); // "修复"
394
+ });
395
+
396
+ test('integration: 按优先级修复这些问题 → skipped (no code entity)', () => {
397
+ const r = analyze('按优先级修复这些问题');
398
+ assert.ok(r.skipped);
399
+ });
400
+
401
+ test('integration: 帮我写一个工具函数 → implement intent', () => {
402
+ const r = analyze('帮我写一个工具函数');
403
+ assert.ok(!r.skipped);
404
+ assert.ok(r.intents.implement);
405
+ });
406
+
407
+ test('integration: 对整个项目进行一次完整的代码审核 → understand', () => {
408
+ const r = analyze('对整个项目进行一次完整的代码审核');
409
+ assert.ok(r.intents.understand);
410
+ });
411
+
412
+ test('integration: 更新一下readme.md → modify intent', () => {
413
+ const r = analyze('更新一下readme.md这个文件');
414
+ assert.ok(r.intents.modify);
415
+ });
416
+
417
+ test('integration: 配置 pre-commit hook → implement intent', () => {
418
+ const r = analyze('配置提交代码时的git pre-commit hook检查');
419
+ assert.ok(r.intents.implement);
420
+ });
421
+
422
+ test('integration: 检查下我们插件上下文token占用情况 → understand', () => {
423
+ const r = analyze('检查下我们插件上下文token占用情况');
424
+ assert.ok(r.intents.understand);
425
+ });
426
+
427
+ test('integration: 诊断一下性能问题 → understand', () => {
428
+ const r = analyze('诊断一下性能问题');
429
+ assert.ok(r.intents.understand);
430
+ });
431
+
432
+ test('integration: simple confirmation → skipped', () => {
433
+ assert.ok(analyze('好的').skipped);
434
+ assert.ok(analyze('继续').skipped);
435
+ assert.ok(analyze('ok').skipped);
436
+ });
437
+
438
+ // ── Skill files validation ──────────────────────────────
439
+
440
+ test('skills: explore.md has correct frontmatter', () => {
441
+ const content = fs.readFileSync(path.join(__dirname, '../skills/explore.md'), 'utf8');
442
+ assert.match(content, /^---\nname: explore/);
443
+ assert.match(content, /description:/);
444
+ });
445
+
446
+ test('skills: index.md has correct frontmatter', () => {
447
+ const content = fs.readFileSync(path.join(__dirname, '../skills/index.md'), 'utf8');
448
+ assert.match(content, /^---\nname: index/);
449
+ assert.match(content, /description:/);
450
+ });
451
+
452
+ test('skills: commands directory is empty (all converted to skills)', () => {
453
+ const commandsDir = path.join(__dirname, '../commands');
454
+ const exists = fs.existsSync(commandsDir);
455
+ if (exists) {
456
+ const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
457
+ assert.equal(files.length, 0, 'commands/ should have no .md files');
458
+ }
459
+ // Directory not existing is also valid
460
+ });
461
+
462
+ test('skills: only expected skills exist', () => {
463
+ const skillsDir = path.join(__dirname, '../skills');
464
+ const files = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md')).sort();
465
+ assert.deepEqual(files, ['explore.md', 'index.md']);
466
+ });
@@ -0,0 +1,22 @@
1
+ ---
2
+ name: explore
3
+ description: |
4
+ Understand code structure efficiently using the AST index. Use BEFORE reading
5
+ files one by one — when starting work in unfamiliar code, exploring a module
6
+ before changes, or finding the right file to edit. One overview call replaces
7
+ 5+ Read calls and saves significant context.
8
+ ---
9
+
10
+ # Explore Code (indexed project)
11
+
12
+ Use these BEFORE reading individual files:
13
+
14
+ | Need | Command | Replaces |
15
+ |------|---------|----------|
16
+ | Module structure | `code-graph-mcp overview <dir>` | 5+ Read calls |
17
+ | Project architecture | `code-graph-mcp map --compact` | ls + README |
18
+ | Who calls / what calls | `code-graph-mcp callgraph <symbol>` | Grep + manual trace |
19
+ | Find by concept | `code-graph-mcp search "concept"` | 3+ Grep attempts |
20
+ | Impact before edit | `code-graph-mcp impact <symbol>` | Grep for callers |
21
+
22
+ **Workflow**: overview first → Read only the file you will edit.
@@ -0,0 +1,24 @@
1
+ ---
2
+ name: index
3
+ description: |
4
+ Diagnose and fix code-graph index issues. Use when: search returns unexpected/empty
5
+ results, or after major codebase restructuring. These management commands are NOT
6
+ exposed via MCP tools — this skill is the only way to access them.
7
+ ---
8
+
9
+ # Index Maintenance
10
+
11
+ ## Check health
12
+ ```bash
13
+ code-graph-mcp health-check
14
+ ```
15
+
16
+ ## Rebuild (incremental — only changed files)
17
+ ```bash
18
+ code-graph-mcp incremental-index
19
+ ```
20
+
21
+ ## Full rebuild (when incremental isn't enough)
22
+ ```bash
23
+ rm -rf .code-graph/ && code-graph-mcp incremental-index
24
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.7.13",
3
+ "version": "0.7.15",
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": {
@@ -34,10 +34,10 @@
34
34
  "node": ">=16"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@sdsrs/code-graph-linux-x64": "0.7.13",
38
- "@sdsrs/code-graph-linux-arm64": "0.7.13",
39
- "@sdsrs/code-graph-darwin-x64": "0.7.13",
40
- "@sdsrs/code-graph-darwin-arm64": "0.7.13",
41
- "@sdsrs/code-graph-win32-x64": "0.7.13"
37
+ "@sdsrs/code-graph-linux-x64": "0.7.15",
38
+ "@sdsrs/code-graph-linux-arm64": "0.7.15",
39
+ "@sdsrs/code-graph-darwin-x64": "0.7.15",
40
+ "@sdsrs/code-graph-darwin-arm64": "0.7.15",
41
+ "@sdsrs/code-graph-win32-x64": "0.7.15"
42
42
  }
43
43
  }
@@ -1,9 +0,0 @@
1
- ---
2
- description: Analyze impact scope before modifying a symbol
3
- argument-hint: <symbol_name>
4
- ---
5
-
6
- ## Impact Analysis
7
- !`code-graph-mcp impact $ARGUMENTS 2>/dev/null || echo "Symbol not found or no index. Run: code-graph-mcp incremental-index"`
8
-
9
- Present the risk assessment and recommend whether it's safe to proceed.
@@ -1,7 +0,0 @@
1
- ---
2
- description: Force a full code-graph index rebuild
3
- ---
4
-
5
- Run via Bash: `code-graph-mcp incremental-index`
6
- This updates the index incrementally (only changed files).
7
- For a full rebuild, delete `.code-graph/` first, then run the MCP server.
@@ -1,5 +0,0 @@
1
- ---
2
- description: Show code-graph index status
3
- ---
4
-
5
- !`code-graph-mcp health-check --format json 2>/dev/null || echo '{"error":"No index found"}'`
@@ -1,12 +0,0 @@
1
- ---
2
- description: Trace call flow from a handler or route
3
- argument-hint: <handler_or_route>
4
- ---
5
-
6
- ## Call Graph (callees)
7
- !`code-graph-mcp callgraph $ARGUMENTS --direction callees --depth 4 2>/dev/null || echo "Symbol not found or no index."`
8
-
9
- ## Call Graph (callers)
10
- !`code-graph-mcp callgraph $ARGUMENTS --direction callers --depth 2 2>/dev/null`
11
-
12
- Map the flow and highlight error handling, auth checks, and data access points.
@@ -1,12 +0,0 @@
1
- ---
2
- description: Deep dive into a module or file's architecture
3
- argument-hint: <file_or_dir_path>
4
- ---
5
-
6
- ## Module Overview
7
- !`code-graph-mcp overview $ARGUMENTS 2>/dev/null || echo "No index or no symbols found. Run: code-graph-mcp incremental-index"`
8
-
9
- ## Call Graph (top symbols)
10
- !`code-graph-mcp search "$ARGUMENTS" --limit 5 2>/dev/null`
11
-
12
- Analyze the above and summarize: purpose, public API, key internal helpers, and hot paths.
@@ -1,20 +0,0 @@
1
- ---
2
- name: code-navigation
3
- description: Code search and understanding via CLI. Use when exploring code structure, searching by concept, or checking impact before edits.
4
- ---
5
-
6
- # Code Graph CLI
7
-
8
- Indexed project. Use Bash — one command replaces multi-file Grep/Read:
9
-
10
- | Task | Command | Replaces |
11
- |------|---------|----------|
12
- | grep + AST context | `code-graph-mcp grep "pattern" [path]` | Grep |
13
- | search by concept | `code-graph-mcp search "query"` | Grep (no exact name needed) |
14
- | structural search | `code-graph-mcp ast-search "q" --type fn --returns Result` | — |
15
- | project map | `code-graph-mcp map` | Read multiple files |
16
- | module overview | `code-graph-mcp overview src/path/` | Read directory files |
17
- | call graph | `code-graph-mcp callgraph symbol` | Grep + Read tracing |
18
- | impact analysis | `code-graph-mcp impact symbol` | — |
19
-
20
- Still use Grep for exact strings/constants/regex. Still use Read for files you'll edit.