@optave/codegraph 3.0.3 → 3.1.0
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 +60 -53
- package/package.json +9 -6
- package/src/ast.js +24 -24
- package/src/builder.js +318 -163
- package/src/cfg.js +39 -18
- package/src/cli.js +35 -0
- package/src/dataflow.js +42 -36
- package/src/db.js +7 -0
- package/src/flow.js +3 -70
- package/src/index.js +2 -1
- package/src/mcp.js +60 -0
- package/src/native.js +23 -3
- package/src/parser.js +58 -75
- package/src/queries.js +60 -21
- package/src/resolve.js +11 -2
- package/src/sequence.js +369 -0
package/src/cfg.js
CHANGED
|
@@ -1046,15 +1046,29 @@ export function buildFunctionCFG(functionNode, langId) {
|
|
|
1046
1046
|
export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
1047
1047
|
// Lazily init WASM parsers if needed
|
|
1048
1048
|
let parsers = null;
|
|
1049
|
-
let extToLang = null;
|
|
1050
1049
|
let needsFallback = false;
|
|
1051
1050
|
|
|
1051
|
+
// Always build ext→langId map so native-only builds (where _langId is unset)
|
|
1052
|
+
// can still derive the language from the file extension.
|
|
1053
|
+
const extToLang = new Map();
|
|
1054
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
1055
|
+
for (const ext of entry.extensions) {
|
|
1056
|
+
extToLang.set(ext, entry.id);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1052
1060
|
for (const [relPath, symbols] of fileSymbols) {
|
|
1053
1061
|
if (!symbols._tree) {
|
|
1054
1062
|
const ext = path.extname(relPath).toLowerCase();
|
|
1055
1063
|
if (CFG_EXTENSIONS.has(ext)) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1064
|
+
// Check if all function/method defs already have native CFG data
|
|
1065
|
+
const hasNativeCfg = symbols.definitions
|
|
1066
|
+
.filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
|
|
1067
|
+
.every((d) => d.cfg === null || d.cfg?.blocks?.length);
|
|
1068
|
+
if (!hasNativeCfg) {
|
|
1069
|
+
needsFallback = true;
|
|
1070
|
+
break;
|
|
1071
|
+
}
|
|
1058
1072
|
}
|
|
1059
1073
|
}
|
|
1060
1074
|
}
|
|
@@ -1062,12 +1076,6 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1062
1076
|
if (needsFallback) {
|
|
1063
1077
|
const { createParsers } = await import('./parser.js');
|
|
1064
1078
|
parsers = await createParsers();
|
|
1065
|
-
extToLang = new Map();
|
|
1066
|
-
for (const entry of LANGUAGE_REGISTRY) {
|
|
1067
|
-
for (const ext of entry.extensions) {
|
|
1068
|
-
extToLang.set(ext, entry.id);
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
1079
|
}
|
|
1072
1080
|
|
|
1073
1081
|
let getParserFn = null;
|
|
@@ -1102,9 +1110,14 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1102
1110
|
let tree = symbols._tree;
|
|
1103
1111
|
let langId = symbols._langId;
|
|
1104
1112
|
|
|
1105
|
-
// WASM
|
|
1106
|
-
|
|
1107
|
-
|
|
1113
|
+
// Check if all defs already have native CFG — skip WASM parse if so
|
|
1114
|
+
const allNative = symbols.definitions
|
|
1115
|
+
.filter((d) => (d.kind === 'function' || d.kind === 'method') && d.line)
|
|
1116
|
+
.every((d) => d.cfg === null || d.cfg?.blocks?.length);
|
|
1117
|
+
|
|
1118
|
+
// WASM fallback if no cached tree and not all native
|
|
1119
|
+
if (!tree && !allNative) {
|
|
1120
|
+
if (!getParserFn) continue;
|
|
1108
1121
|
langId = extToLang.get(ext);
|
|
1109
1122
|
if (!langId || !CFG_LANG_IDS.has(langId)) continue;
|
|
1110
1123
|
|
|
@@ -1127,7 +1140,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1127
1140
|
}
|
|
1128
1141
|
|
|
1129
1142
|
if (!langId) {
|
|
1130
|
-
langId = extToLang
|
|
1143
|
+
langId = extToLang.get(ext);
|
|
1131
1144
|
if (!langId) continue;
|
|
1132
1145
|
}
|
|
1133
1146
|
|
|
@@ -1135,7 +1148,7 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1135
1148
|
if (!cfgRules) continue;
|
|
1136
1149
|
|
|
1137
1150
|
const complexityRules = COMPLEXITY_RULES.get(langId);
|
|
1138
|
-
|
|
1151
|
+
// complexityRules only needed for WASM fallback path
|
|
1139
1152
|
|
|
1140
1153
|
for (const def of symbols.definitions) {
|
|
1141
1154
|
if (def.kind !== 'function' && def.kind !== 'method') continue;
|
|
@@ -1144,11 +1157,19 @@ export async function buildCFGData(db, fileSymbols, rootDir, _engineOpts) {
|
|
|
1144
1157
|
const row = getNodeId.get(def.name, relPath, def.line);
|
|
1145
1158
|
if (!row) continue;
|
|
1146
1159
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1160
|
+
// Native path: use pre-computed CFG from Rust engine
|
|
1161
|
+
let cfg = null;
|
|
1162
|
+
if (def.cfg?.blocks?.length) {
|
|
1163
|
+
cfg = def.cfg;
|
|
1164
|
+
} else {
|
|
1165
|
+
// WASM fallback: compute CFG from tree-sitter AST
|
|
1166
|
+
if (!tree || !complexityRules) continue;
|
|
1167
|
+
const funcNode = findFunctionNode(tree.rootNode, def.line, def.endLine, complexityRules);
|
|
1168
|
+
if (!funcNode) continue;
|
|
1169
|
+
cfg = buildFunctionCFG(funcNode, langId);
|
|
1170
|
+
}
|
|
1149
1171
|
|
|
1150
|
-
|
|
1151
|
-
if (cfg.blocks.length === 0) continue;
|
|
1172
|
+
if (!cfg || cfg.blocks.length === 0) continue;
|
|
1152
1173
|
|
|
1153
1174
|
// Clear old CFG data for this function
|
|
1154
1175
|
deleteEdges.run(row.id);
|
package/src/cli.js
CHANGED
|
@@ -277,6 +277,7 @@ program
|
|
|
277
277
|
.option('--limit <number>', 'Max results to return')
|
|
278
278
|
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
279
279
|
.option('--ndjson', 'Newline-delimited JSON output')
|
|
280
|
+
.option('--unused', 'Show only exports with zero consumers')
|
|
280
281
|
.action((file, opts) => {
|
|
281
282
|
fileExports(file, opts.db, {
|
|
282
283
|
noTests: resolveNoTests(opts),
|
|
@@ -284,6 +285,7 @@ program
|
|
|
284
285
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
285
286
|
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
286
287
|
ndjson: opts.ndjson,
|
|
288
|
+
unused: opts.unused,
|
|
287
289
|
});
|
|
288
290
|
});
|
|
289
291
|
|
|
@@ -1137,6 +1139,39 @@ program
|
|
|
1137
1139
|
});
|
|
1138
1140
|
});
|
|
1139
1141
|
|
|
1142
|
+
program
|
|
1143
|
+
.command('sequence <name>')
|
|
1144
|
+
.description('Generate a Mermaid sequence diagram from call graph edges (participants = files)')
|
|
1145
|
+
.option('--depth <n>', 'Max forward traversal depth', '10')
|
|
1146
|
+
.option('--dataflow', 'Annotate with parameter names and return arrows from dataflow table')
|
|
1147
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
1148
|
+
.option('-f, --file <path>', 'Scope to a specific file (partial match)')
|
|
1149
|
+
.option('-k, --kind <kind>', 'Filter by symbol kind')
|
|
1150
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
1151
|
+
.option('--include-tests', 'Include test/spec files (overrides excludeTests config)')
|
|
1152
|
+
.option('-j, --json', 'Output as JSON')
|
|
1153
|
+
.option('--limit <number>', 'Max results to return')
|
|
1154
|
+
.option('--offset <number>', 'Skip N results (default: 0)')
|
|
1155
|
+
.option('--ndjson', 'Newline-delimited JSON output')
|
|
1156
|
+
.action(async (name, opts) => {
|
|
1157
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
1158
|
+
console.error(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
1159
|
+
process.exit(1);
|
|
1160
|
+
}
|
|
1161
|
+
const { sequence } = await import('./sequence.js');
|
|
1162
|
+
sequence(name, opts.db, {
|
|
1163
|
+
depth: parseInt(opts.depth, 10),
|
|
1164
|
+
file: opts.file,
|
|
1165
|
+
kind: opts.kind,
|
|
1166
|
+
noTests: resolveNoTests(opts),
|
|
1167
|
+
json: opts.json,
|
|
1168
|
+
dataflow: opts.dataflow,
|
|
1169
|
+
limit: opts.limit ? parseInt(opts.limit, 10) : undefined,
|
|
1170
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
1171
|
+
ndjson: opts.ndjson,
|
|
1172
|
+
});
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1140
1175
|
program
|
|
1141
1176
|
.command('dataflow <name>')
|
|
1142
1177
|
.description('Show data flow for a function: parameters, return consumers, mutations')
|
package/src/dataflow.js
CHANGED
|
@@ -1005,11 +1005,19 @@ function collectIdentifiers(node, out, rules) {
|
|
|
1005
1005
|
export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) {
|
|
1006
1006
|
// Lazily init WASM parsers if needed
|
|
1007
1007
|
let parsers = null;
|
|
1008
|
-
let extToLang = null;
|
|
1009
1008
|
let needsFallback = false;
|
|
1010
1009
|
|
|
1010
|
+
// Always build ext→langId map so native-only builds (where _langId is unset)
|
|
1011
|
+
// can still derive the language from the file extension.
|
|
1012
|
+
const extToLang = new Map();
|
|
1013
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
1014
|
+
for (const ext of entry.extensions) {
|
|
1015
|
+
extToLang.set(ext, entry.id);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1011
1019
|
for (const [relPath, symbols] of fileSymbols) {
|
|
1012
|
-
if (!symbols._tree) {
|
|
1020
|
+
if (!symbols._tree && !symbols.dataflow) {
|
|
1013
1021
|
const ext = path.extname(relPath).toLowerCase();
|
|
1014
1022
|
if (DATAFLOW_EXTENSIONS.has(ext)) {
|
|
1015
1023
|
needsFallback = true;
|
|
@@ -1021,12 +1029,6 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
|
|
|
1021
1029
|
if (needsFallback) {
|
|
1022
1030
|
const { createParsers } = await import('./parser.js');
|
|
1023
1031
|
parsers = await createParsers();
|
|
1024
|
-
extToLang = new Map();
|
|
1025
|
-
for (const entry of LANGUAGE_REGISTRY) {
|
|
1026
|
-
for (const ext of entry.extensions) {
|
|
1027
|
-
extToLang.set(ext, entry.id);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
1032
|
}
|
|
1031
1033
|
|
|
1032
1034
|
let getParserFn = null;
|
|
@@ -1061,41 +1063,45 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
|
|
|
1061
1063
|
const ext = path.extname(relPath).toLowerCase();
|
|
1062
1064
|
if (!DATAFLOW_EXTENSIONS.has(ext)) continue;
|
|
1063
1065
|
|
|
1064
|
-
|
|
1065
|
-
let
|
|
1066
|
+
// Use native dataflow data if available — skip WASM extraction
|
|
1067
|
+
let data = symbols.dataflow;
|
|
1068
|
+
if (!data) {
|
|
1069
|
+
let tree = symbols._tree;
|
|
1070
|
+
let langId = symbols._langId;
|
|
1071
|
+
|
|
1072
|
+
// WASM fallback if no cached tree
|
|
1073
|
+
if (!tree) {
|
|
1074
|
+
if (!getParserFn) continue;
|
|
1075
|
+
langId = extToLang.get(ext);
|
|
1076
|
+
if (!langId || !DATAFLOW_LANG_IDS.has(langId)) continue;
|
|
1077
|
+
|
|
1078
|
+
const absPath = path.join(rootDir, relPath);
|
|
1079
|
+
let code;
|
|
1080
|
+
try {
|
|
1081
|
+
code = fs.readFileSync(absPath, 'utf-8');
|
|
1082
|
+
} catch {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1066
1085
|
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
if (!extToLang || !getParserFn) continue;
|
|
1070
|
-
langId = extToLang.get(ext);
|
|
1071
|
-
if (!langId || !DATAFLOW_LANG_IDS.has(langId)) continue;
|
|
1086
|
+
const parser = getParserFn(parsers, absPath);
|
|
1087
|
+
if (!parser) continue;
|
|
1072
1088
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
continue;
|
|
1089
|
+
try {
|
|
1090
|
+
tree = parser.parse(code);
|
|
1091
|
+
} catch {
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1079
1094
|
}
|
|
1080
1095
|
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
try {
|
|
1085
|
-
tree = parser.parse(code);
|
|
1086
|
-
} catch {
|
|
1087
|
-
continue;
|
|
1096
|
+
if (!langId) {
|
|
1097
|
+
langId = extToLang.get(ext);
|
|
1098
|
+
if (!langId) continue;
|
|
1088
1099
|
}
|
|
1089
|
-
}
|
|
1090
1100
|
|
|
1091
|
-
|
|
1092
|
-
langId = extToLang ? extToLang.get(ext) : null;
|
|
1093
|
-
if (!langId) continue;
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
if (!DATAFLOW_RULES.has(langId)) continue;
|
|
1101
|
+
if (!DATAFLOW_RULES.has(langId)) continue;
|
|
1097
1102
|
|
|
1098
|
-
|
|
1103
|
+
data = extractDataflow(tree, relPath, symbols.definitions, langId);
|
|
1104
|
+
}
|
|
1099
1105
|
|
|
1100
1106
|
// Resolve function names to node IDs in this file first, then globally
|
|
1101
1107
|
function resolveNode(funcName) {
|
package/src/db.js
CHANGED
|
@@ -225,6 +225,13 @@ export const MIGRATIONS = [
|
|
|
225
225
|
CREATE INDEX IF NOT EXISTS idx_ast_kind_name ON ast_nodes(kind, name);
|
|
226
226
|
`,
|
|
227
227
|
},
|
|
228
|
+
{
|
|
229
|
+
version: 14,
|
|
230
|
+
up: `
|
|
231
|
+
ALTER TABLE nodes ADD COLUMN exported INTEGER DEFAULT 0;
|
|
232
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_exported ON nodes(exported);
|
|
233
|
+
`,
|
|
234
|
+
},
|
|
228
235
|
];
|
|
229
236
|
|
|
230
237
|
export function getBuildMeta(db, key) {
|
package/src/flow.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { openReadonlyOrFail } from './db.js';
|
|
9
9
|
import { paginateResult, printNdjson } from './paginate.js';
|
|
10
|
-
import { isTestFile, kindIcon } from './queries.js';
|
|
10
|
+
import { findMatchingNodes, isTestFile, kindIcon } from './queries.js';
|
|
11
11
|
import { FRAMEWORK_ENTRY_PREFIXES } from './structure.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -95,12 +95,12 @@ export function flowData(name, dbPath, opts = {}) {
|
|
|
95
95
|
const noTests = opts.noTests || false;
|
|
96
96
|
|
|
97
97
|
// Phase 1: Direct LIKE match on full name
|
|
98
|
-
let matchNode =
|
|
98
|
+
let matchNode = findMatchingNodes(db, name, opts)[0] ?? null;
|
|
99
99
|
|
|
100
100
|
// Phase 2: Prefix-stripped matching — try adding framework prefixes
|
|
101
101
|
if (!matchNode) {
|
|
102
102
|
for (const prefix of FRAMEWORK_ENTRY_PREFIXES) {
|
|
103
|
-
matchNode =
|
|
103
|
+
matchNode = findMatchingNodes(db, `${prefix}${name}`, opts)[0] ?? null;
|
|
104
104
|
if (matchNode) break;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
@@ -219,73 +219,6 @@ export function flowData(name, dbPath, opts = {}) {
|
|
|
219
219
|
return paginateResult(base, 'steps', { limit: opts.limit, offset: opts.offset });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
/**
|
|
223
|
-
* Find the best matching node using the same relevance scoring as queries.js findMatchingNodes.
|
|
224
|
-
*/
|
|
225
|
-
function findBestMatch(db, name, opts = {}) {
|
|
226
|
-
const kinds = opts.kind
|
|
227
|
-
? [opts.kind]
|
|
228
|
-
: [
|
|
229
|
-
'function',
|
|
230
|
-
'method',
|
|
231
|
-
'class',
|
|
232
|
-
'interface',
|
|
233
|
-
'type',
|
|
234
|
-
'struct',
|
|
235
|
-
'enum',
|
|
236
|
-
'trait',
|
|
237
|
-
'record',
|
|
238
|
-
'module',
|
|
239
|
-
];
|
|
240
|
-
const placeholders = kinds.map(() => '?').join(', ');
|
|
241
|
-
const params = [`%${name}%`, ...kinds];
|
|
242
|
-
|
|
243
|
-
let fileCondition = '';
|
|
244
|
-
if (opts.file) {
|
|
245
|
-
fileCondition = ' AND n.file LIKE ?';
|
|
246
|
-
params.push(`%${opts.file}%`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const rows = db
|
|
250
|
-
.prepare(
|
|
251
|
-
`SELECT n.*, COALESCE(fi.cnt, 0) AS fan_in
|
|
252
|
-
FROM nodes n
|
|
253
|
-
LEFT JOIN (
|
|
254
|
-
SELECT target_id, COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY target_id
|
|
255
|
-
) fi ON fi.target_id = n.id
|
|
256
|
-
WHERE n.name LIKE ? AND n.kind IN (${placeholders})${fileCondition}`,
|
|
257
|
-
)
|
|
258
|
-
.all(...params);
|
|
259
|
-
|
|
260
|
-
const noTests = opts.noTests || false;
|
|
261
|
-
const nodes = noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
|
|
262
|
-
|
|
263
|
-
if (nodes.length === 0) return null;
|
|
264
|
-
|
|
265
|
-
const lowerQuery = name.toLowerCase();
|
|
266
|
-
for (const node of nodes) {
|
|
267
|
-
const lowerName = node.name.toLowerCase();
|
|
268
|
-
const bareName = lowerName.includes('.') ? lowerName.split('.').pop() : lowerName;
|
|
269
|
-
|
|
270
|
-
let matchScore;
|
|
271
|
-
if (lowerName === lowerQuery || bareName === lowerQuery) {
|
|
272
|
-
matchScore = 100;
|
|
273
|
-
} else if (lowerName.startsWith(lowerQuery) || bareName.startsWith(lowerQuery)) {
|
|
274
|
-
matchScore = 60;
|
|
275
|
-
} else if (lowerName.includes(`.${lowerQuery}`) || lowerName.includes(`${lowerQuery}.`)) {
|
|
276
|
-
matchScore = 40;
|
|
277
|
-
} else {
|
|
278
|
-
matchScore = 10;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const fanInBonus = Math.min(Math.log2(node.fan_in + 1) * 5, 25);
|
|
282
|
-
node._relevance = matchScore + fanInBonus;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
nodes.sort((a, b) => b._relevance - a._relevance);
|
|
286
|
-
return nodes[0];
|
|
287
|
-
}
|
|
288
|
-
|
|
289
222
|
/**
|
|
290
223
|
* CLI formatter — text or JSON output.
|
|
291
224
|
*/
|
package/src/index.js
CHANGED
|
@@ -121,7 +121,6 @@ export { isNativeAvailable } from './native.js';
|
|
|
121
121
|
export { matchOwners, owners, ownersData, ownersForFiles, parseCodeowners } from './owners.js';
|
|
122
122
|
// Pagination utilities
|
|
123
123
|
export { MCP_DEFAULTS, MCP_MAX_LIMIT, paginate, paginateResult, printNdjson } from './paginate.js';
|
|
124
|
-
|
|
125
124
|
// Unified parser API
|
|
126
125
|
export { getActiveEngine, isWasmAvailable, parseFileAuto, parseFilesAuto } from './parser.js';
|
|
127
126
|
// Query functions (data-returning)
|
|
@@ -170,6 +169,8 @@ export {
|
|
|
170
169
|
saveRegistry,
|
|
171
170
|
unregisterRepo,
|
|
172
171
|
} from './registry.js';
|
|
172
|
+
// Sequence diagram generation
|
|
173
|
+
export { sequence, sequenceData, sequenceToMermaid } from './sequence.js';
|
|
173
174
|
// Snapshot management
|
|
174
175
|
export {
|
|
175
176
|
snapshotDelete,
|
package/src/mcp.js
CHANGED
|
@@ -113,6 +113,11 @@ const BASE_TOOLS = [
|
|
|
113
113
|
properties: {
|
|
114
114
|
file: { type: 'string', description: 'File path (partial match supported)' },
|
|
115
115
|
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
116
|
+
unused: {
|
|
117
|
+
type: 'boolean',
|
|
118
|
+
description: 'Show only exports with zero consumers',
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
116
121
|
...PAGINATION_PROPS,
|
|
117
122
|
},
|
|
118
123
|
required: ['file'],
|
|
@@ -418,6 +423,43 @@ const BASE_TOOLS = [
|
|
|
418
423
|
},
|
|
419
424
|
},
|
|
420
425
|
},
|
|
426
|
+
{
|
|
427
|
+
name: 'sequence',
|
|
428
|
+
description:
|
|
429
|
+
'Generate a Mermaid sequence diagram from call graph edges. Participants are files, messages are function calls between them.',
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: 'object',
|
|
432
|
+
properties: {
|
|
433
|
+
name: {
|
|
434
|
+
type: 'string',
|
|
435
|
+
description: 'Entry point or function name to trace from (partial match)',
|
|
436
|
+
},
|
|
437
|
+
depth: { type: 'number', description: 'Max forward traversal depth', default: 10 },
|
|
438
|
+
format: {
|
|
439
|
+
type: 'string',
|
|
440
|
+
enum: ['mermaid', 'json'],
|
|
441
|
+
description: 'Output format (default: mermaid)',
|
|
442
|
+
},
|
|
443
|
+
dataflow: {
|
|
444
|
+
type: 'boolean',
|
|
445
|
+
description: 'Annotate with parameter names and return arrows',
|
|
446
|
+
default: false,
|
|
447
|
+
},
|
|
448
|
+
file: {
|
|
449
|
+
type: 'string',
|
|
450
|
+
description: 'Scope search to functions in this file (partial match)',
|
|
451
|
+
},
|
|
452
|
+
kind: {
|
|
453
|
+
type: 'string',
|
|
454
|
+
enum: EVERY_SYMBOL_KIND,
|
|
455
|
+
description: 'Filter to a specific symbol kind',
|
|
456
|
+
},
|
|
457
|
+
no_tests: { type: 'boolean', description: 'Exclude test files', default: false },
|
|
458
|
+
...PAGINATION_PROPS,
|
|
459
|
+
},
|
|
460
|
+
required: ['name'],
|
|
461
|
+
},
|
|
462
|
+
},
|
|
421
463
|
{
|
|
422
464
|
name: 'complexity',
|
|
423
465
|
description:
|
|
@@ -902,6 +944,7 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
902
944
|
case 'file_exports':
|
|
903
945
|
result = exportsData(args.file, dbPath, {
|
|
904
946
|
noTests: args.no_tests,
|
|
947
|
+
unused: args.unused,
|
|
905
948
|
limit: Math.min(args.limit ?? MCP_DEFAULTS.file_exports, MCP_MAX_LIMIT),
|
|
906
949
|
offset: args.offset ?? 0,
|
|
907
950
|
});
|
|
@@ -1165,6 +1208,23 @@ export async function startMCPServer(customDbPath, options = {}) {
|
|
|
1165
1208
|
}
|
|
1166
1209
|
break;
|
|
1167
1210
|
}
|
|
1211
|
+
case 'sequence': {
|
|
1212
|
+
const { sequenceData, sequenceToMermaid } = await import('./sequence.js');
|
|
1213
|
+
const seqResult = sequenceData(args.name, dbPath, {
|
|
1214
|
+
depth: args.depth,
|
|
1215
|
+
file: args.file,
|
|
1216
|
+
kind: args.kind,
|
|
1217
|
+
dataflow: args.dataflow,
|
|
1218
|
+
noTests: args.no_tests,
|
|
1219
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, MCP_MAX_LIMIT),
|
|
1220
|
+
offset: args.offset ?? 0,
|
|
1221
|
+
});
|
|
1222
|
+
result =
|
|
1223
|
+
args.format === 'json'
|
|
1224
|
+
? seqResult
|
|
1225
|
+
: { text: sequenceToMermaid(seqResult), ...seqResult };
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1168
1228
|
case 'complexity': {
|
|
1169
1229
|
const { complexityData } = await import('./complexity.js');
|
|
1170
1230
|
result = complexityData(dbPath, {
|
package/src/native.js
CHANGED
|
@@ -12,9 +12,27 @@ import os from 'node:os';
|
|
|
12
12
|
let _cached; // undefined = not yet tried, null = failed, object = module
|
|
13
13
|
let _loadError = null;
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Detect whether the current Linux environment uses glibc or musl.
|
|
17
|
+
* Returns 'gnu' for glibc, 'musl' for musl, 'gnu' as fallback.
|
|
18
|
+
*/
|
|
19
|
+
function detectLibc() {
|
|
20
|
+
try {
|
|
21
|
+
const { readdirSync } = require('node:fs');
|
|
22
|
+
const files = readdirSync('/lib');
|
|
23
|
+
if (files.some((f) => f.startsWith('ld-musl-') && f.endsWith('.so.1'))) {
|
|
24
|
+
return 'musl';
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
return 'gnu';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Map of (platform-arch[-libc]) → npm package name. */
|
|
16
31
|
const PLATFORM_PACKAGES = {
|
|
17
|
-
'linux-x64': '@optave/codegraph-linux-x64-gnu',
|
|
32
|
+
'linux-x64-gnu': '@optave/codegraph-linux-x64-gnu',
|
|
33
|
+
'linux-x64-musl': '@optave/codegraph-linux-x64-musl',
|
|
34
|
+
'linux-arm64-gnu': '@optave/codegraph-linux-arm64-gnu',
|
|
35
|
+
'linux-arm64-musl': '@optave/codegraph-linux-arm64-musl', // not yet published — placeholder for future CI target
|
|
18
36
|
'darwin-arm64': '@optave/codegraph-darwin-arm64',
|
|
19
37
|
'darwin-x64': '@optave/codegraph-darwin-x64',
|
|
20
38
|
'win32-x64': '@optave/codegraph-win32-x64-msvc',
|
|
@@ -29,7 +47,9 @@ export function loadNative() {
|
|
|
29
47
|
|
|
30
48
|
const require = createRequire(import.meta.url);
|
|
31
49
|
|
|
32
|
-
const
|
|
50
|
+
const platform = os.platform();
|
|
51
|
+
const arch = os.arch();
|
|
52
|
+
const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
|
|
33
53
|
const pkg = PLATFORM_PACKAGES[key];
|
|
34
54
|
if (pkg) {
|
|
35
55
|
try {
|