project-graph-mcp 1.5.0 → 2.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 +171 -31
- package/docs/img/explorer-compact.jpg +0 -0
- package/docs/img/explorer-expanded.jpg +0 -0
- package/package.json +12 -8
- package/src/.project-graph-cache.json +1 -1
- package/src/analysis/analysis-cache.js +7 -0
- package/src/analysis/complexity.js +14 -0
- package/src/analysis/custom-rules.js +36 -0
- package/src/analysis/db-analysis.js +9 -0
- package/src/analysis/dead-code.js +19 -0
- package/src/analysis/full-analysis.js +18 -0
- package/src/analysis/jsdoc-checker.js +24 -0
- package/src/analysis/jsdoc-generator.js +10 -0
- package/src/analysis/large-files.js +11 -0
- package/src/analysis/outdated-patterns.js +12 -0
- package/src/analysis/similar-functions.js +16 -0
- package/src/analysis/test-annotations.js +21 -0
- package/src/analysis/type-checker.js +8 -0
- package/src/analysis/undocumented.js +14 -0
- package/src/cli/cli-handlers.js +4 -0
- package/src/cli/cli.js +5 -0
- package/src/compact/.project-graph-cache.json +1 -0
- package/src/compact/ai-context.js +7 -0
- package/src/compact/compact-migrate.js +17 -0
- package/src/compact/compact.js +18 -0
- package/src/compact/compress.js +14 -0
- package/src/compact/ctx-to-jsdoc.js +29 -0
- package/src/compact/doc-dialect.js +30 -0
- package/src/compact/expand.js +37 -0
- package/src/compact/framework-references.js +5 -0
- package/src/compact/instructions.js +3 -0
- package/src/compact/mode-config.js +8 -0
- package/src/compact/validate-pipeline.js +9 -0
- package/src/core/event-bus.js +9 -0
- package/src/core/filters.js +14 -0
- package/src/core/graph-builder.js +12 -0
- package/src/core/parser.js +31 -0
- package/src/core/workspace.js +8 -0
- package/src/lang/lang-go.js +17 -0
- package/src/lang/lang-python.js +12 -0
- package/src/lang/lang-sql.js +23 -0
- package/src/lang/lang-typescript.js +9 -0
- package/src/lang/lang-utils.js +4 -0
- package/src/mcp/mcp-server.js +17 -0
- package/src/mcp/tool-defs.js +3 -0
- package/src/mcp/tools.js +25 -0
- package/src/network/backend-lifecycle.js +19 -0
- package/src/network/backend.js +5 -0
- package/src/network/local-gateway.js +23 -0
- package/src/network/mdns.js +13 -0
- package/src/network/server.js +10 -0
- package/src/network/web-server.js +34 -0
- package/web/.project-graph-cache.json +1 -0
- package/web/app.js +17 -0
- package/web/components/code-block.js +3 -0
- package/web/components/quick-open.js +5 -0
- package/web/dashboard-state.js +3 -0
- package/web/dashboard.html +27 -0
- package/web/dashboard.js +8 -0
- package/web/highlight.js +13 -0
- package/web/index.html +35 -0
- package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
- package/web/panels/ActionBoard/ActionBoard.js +4 -0
- package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
- package/web/panels/EventItem/EventItem.css.js +1 -0
- package/web/panels/EventItem/EventItem.js +4 -0
- package/web/panels/EventItem/EventItem.tpl.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
- package/web/panels/ProjectItem/ProjectItem.js +5 -0
- package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
- package/web/panels/ProjectList/ProjectList.css.js +1 -0
- package/web/panels/ProjectList/ProjectList.js +4 -0
- package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
- package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
- package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
- package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
- package/web/panels/code-viewer.js +5 -0
- package/web/panels/ctx-panel.js +4 -0
- package/web/panels/dep-graph.js +6 -0
- package/web/panels/file-tree.js +188 -0
- package/web/panels/health-panel.js +3 -0
- package/web/panels/live-monitor.js +3 -0
- package/web/state.js +17 -0
- package/web/style.css +157 -0
- package/references/symbiote-3x.md +0 -834
- package/src/ai-context.js +0 -113
- package/src/analysis-cache.js +0 -155
- package/src/cli-handlers.js +0 -271
- package/src/cli.js +0 -95
- package/src/compact.js +0 -207
- package/src/complexity.js +0 -237
- package/src/compress.js +0 -319
- package/src/ctx-to-jsdoc.js +0 -514
- package/src/custom-rules.js +0 -584
- package/src/db-analysis.js +0 -194
- package/src/dead-code.js +0 -468
- package/src/doc-dialect.js +0 -716
- package/src/filters.js +0 -227
- package/src/framework-references.js +0 -177
- package/src/full-analysis.js +0 -470
- package/src/graph-builder.js +0 -299
- package/src/instructions.js +0 -73
- package/src/jsdoc-checker.js +0 -351
- package/src/jsdoc-generator.js +0 -203
- package/src/lang-go.js +0 -285
- package/src/lang-python.js +0 -197
- package/src/lang-sql.js +0 -309
- package/src/lang-typescript.js +0 -190
- package/src/lang-utils.js +0 -124
- package/src/large-files.js +0 -163
- package/src/mcp-server.js +0 -675
- package/src/mode-config.js +0 -127
- package/src/outdated-patterns.js +0 -296
- package/src/parser.js +0 -662
- package/src/server.js +0 -28
- package/src/similar-functions.js +0 -279
- package/src/test-annotations.js +0 -323
- package/src/tool-defs.js +0 -793
- package/src/tools.js +0 -470
- package/src/type-checker.js +0 -188
- package/src/undocumented.js +0 -259
- package/src/workspace.js +0 -70
- /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
- /package/{AGENT_ROLE_MINIMAL.md → docs/examples/AGENT_ROLE_MINIMAL.md} +0 -0
package/src/parser.js
DELETED
|
@@ -1,662 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AST Parser for JavaScript files using Acorn
|
|
3
|
-
* Extracts classes, functions, methods, properties, imports, calls, and SQL queries
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, readdirSync, statSync, existsSync } from 'fs';
|
|
7
|
-
import { join, relative, resolve } from 'path';
|
|
8
|
-
import { parse } from '../vendor/acorn.mjs';
|
|
9
|
-
import * as walk from '../vendor/walk.mjs';
|
|
10
|
-
import { shouldExcludeDir, shouldExcludeFile, parseGitignore } from './filters.js';
|
|
11
|
-
import { parseTypeScript } from './lang-typescript.js';
|
|
12
|
-
import { parsePython } from './lang-python.js';
|
|
13
|
-
import { parseGo } from './lang-go.js';
|
|
14
|
-
import { parseSQL, extractSQLFromString, isSQLString } from './lang-sql.js';
|
|
15
|
-
|
|
16
|
-
/** Supported source file extensions */
|
|
17
|
-
const SOURCE_EXTENSIONS = ['.js', '.ts', '.tsx', '.py', '.go', '.sql'];
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {Object} ClassInfo
|
|
21
|
-
* @property {string} name
|
|
22
|
-
* @property {string} [extends]
|
|
23
|
-
* @property {string[]} methods
|
|
24
|
-
* @property {string[]} properties
|
|
25
|
-
* @property {string[]} calls
|
|
26
|
-
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
27
|
-
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
28
|
-
* @property {string} file
|
|
29
|
-
* @property {number} line
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* @typedef {Object} FunctionInfo
|
|
34
|
-
* @property {string} name
|
|
35
|
-
* @property {boolean} exported
|
|
36
|
-
* @property {string[]} calls
|
|
37
|
-
* @property {string[]} [dbReads] - Tables read by SQL queries
|
|
38
|
-
* @property {string[]} [dbWrites] - Tables written by SQL queries
|
|
39
|
-
* @property {string} file
|
|
40
|
-
* @property {number} line
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* @typedef {Object} ParseResult
|
|
45
|
-
* @property {string[]} files
|
|
46
|
-
* @property {ClassInfo[]} classes
|
|
47
|
-
* @property {FunctionInfo[]} functions
|
|
48
|
-
* @property {string[]} imports
|
|
49
|
-
* @property {string[]} exports
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Parse a JavaScript file content using AST
|
|
54
|
-
* @param {string} code
|
|
55
|
-
* @param {string} filename
|
|
56
|
-
* @returns {Promise<ParseResult>}
|
|
57
|
-
*/
|
|
58
|
-
export async function parseFile(code, filename) {
|
|
59
|
-
const result = {
|
|
60
|
-
file: filename,
|
|
61
|
-
classes: [],
|
|
62
|
-
functions: [],
|
|
63
|
-
imports: [],
|
|
64
|
-
exports: [],
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Collect JSDoc comments for type extraction
|
|
68
|
-
const comments = [];
|
|
69
|
-
let ast;
|
|
70
|
-
try {
|
|
71
|
-
ast = parse(code, {
|
|
72
|
-
ecmaVersion: 'latest',
|
|
73
|
-
sourceType: 'module',
|
|
74
|
-
locations: true,
|
|
75
|
-
onComment: comments,
|
|
76
|
-
});
|
|
77
|
-
} catch (e) {
|
|
78
|
-
// If parsing fails, return empty result
|
|
79
|
-
console.warn(`Parse error in ${filename}:`, e.message);
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Build JSDoc type map: endLine → { params: [{name, type}], returns: string }
|
|
84
|
-
const jsdocMap = buildJSDocTypeMap(comments, code);
|
|
85
|
-
|
|
86
|
-
// Track exported names
|
|
87
|
-
const exportedNames = new Set();
|
|
88
|
-
|
|
89
|
-
// Walk the AST
|
|
90
|
-
walk.simple(ast, {
|
|
91
|
-
// Import declarations
|
|
92
|
-
ImportDeclaration(node) {
|
|
93
|
-
for (const spec of node.specifiers) {
|
|
94
|
-
if (spec.type === 'ImportDefaultSpecifier') {
|
|
95
|
-
result.imports.push(spec.local.name);
|
|
96
|
-
} else if (spec.type === 'ImportSpecifier') {
|
|
97
|
-
result.imports.push(spec.imported.name);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
|
|
102
|
-
// Export declarations
|
|
103
|
-
ExportNamedDeclaration(node) {
|
|
104
|
-
if (node.declaration) {
|
|
105
|
-
if (node.declaration.id) {
|
|
106
|
-
exportedNames.add(node.declaration.id.name);
|
|
107
|
-
} else if (node.declaration.declarations) {
|
|
108
|
-
for (const decl of node.declaration.declarations) {
|
|
109
|
-
exportedNames.add(decl.id.name);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (node.specifiers) {
|
|
114
|
-
for (const spec of node.specifiers) {
|
|
115
|
-
exportedNames.add(spec.exported.name);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
ExportDefaultDeclaration(node) {
|
|
121
|
-
if (node.declaration && node.declaration.id) {
|
|
122
|
-
exportedNames.add(node.declaration.id.name);
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
// Class declarations
|
|
127
|
-
ClassDeclaration(node) {
|
|
128
|
-
const classInfo = {
|
|
129
|
-
name: node.id.name,
|
|
130
|
-
extends: node.superClass ? node.superClass.name : null,
|
|
131
|
-
methods: [],
|
|
132
|
-
properties: [],
|
|
133
|
-
calls: [],
|
|
134
|
-
dbReads: [],
|
|
135
|
-
dbWrites: [],
|
|
136
|
-
file: filename,
|
|
137
|
-
line: node.loc.start.line,
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// Extract methods and properties from class body
|
|
141
|
-
for (const element of node.body.body) {
|
|
142
|
-
if (element.type === 'MethodDefinition' && element.key.name !== 'constructor') {
|
|
143
|
-
classInfo.methods.push(element.key.name);
|
|
144
|
-
|
|
145
|
-
// Extract calls and SQL from method body
|
|
146
|
-
extractCallsAndSQL(element.value.body, classInfo.calls, classInfo.dbReads, classInfo.dbWrites);
|
|
147
|
-
} else if (element.type === 'PropertyDefinition') {
|
|
148
|
-
const propName = element.key.name;
|
|
149
|
-
|
|
150
|
-
// Check for init$ object properties
|
|
151
|
-
if (propName === 'init$' && element.value && element.value.type === 'ObjectExpression') {
|
|
152
|
-
for (const prop of element.value.properties) {
|
|
153
|
-
if (prop.key && prop.key.name) {
|
|
154
|
-
classInfo.properties.push(prop.key.name);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
result.classes.push(classInfo);
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
// Standalone function declarations
|
|
165
|
-
FunctionDeclaration(node) {
|
|
166
|
-
if (node.id) {
|
|
167
|
-
const rawParams = node.params.map(p => {
|
|
168
|
-
if (p.type === 'Identifier') return p.name;
|
|
169
|
-
if (p.type === 'AssignmentPattern' && p.left.type === 'Identifier') return p.left.name + '=';
|
|
170
|
-
if (p.type === 'RestElement' && p.argument.type === 'Identifier') return '...' + p.argument.name;
|
|
171
|
-
if (p.type === 'ObjectPattern') return 'options';
|
|
172
|
-
return '?';
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// Enrich params with JSDoc types
|
|
176
|
-
const jsdoc = findJSDocForNode(jsdocMap, node.loc.start.line);
|
|
177
|
-
const typedParams = enrichParamsWithTypes(rawParams, jsdoc);
|
|
178
|
-
|
|
179
|
-
const funcInfo = {
|
|
180
|
-
name: node.id.name,
|
|
181
|
-
exported: false, // Will be updated later
|
|
182
|
-
params: typedParams,
|
|
183
|
-
async: node.async || false,
|
|
184
|
-
returns: jsdoc?.returns || null,
|
|
185
|
-
calls: [],
|
|
186
|
-
dbReads: [],
|
|
187
|
-
dbWrites: [],
|
|
188
|
-
file: filename,
|
|
189
|
-
line: node.loc.start.line,
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
extractCallsAndSQL(node.body, funcInfo.calls, funcInfo.dbReads, funcInfo.dbWrites);
|
|
193
|
-
result.functions.push(funcInfo);
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// Mark exported functions
|
|
199
|
-
for (const func of result.functions) {
|
|
200
|
-
func.exported = exportedNames.has(func.name);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Collect exports
|
|
204
|
-
result.exports = [...exportedNames];
|
|
205
|
-
|
|
206
|
-
return result;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/** DB client method names that accept SQL as first argument */
|
|
210
|
-
const DB_METHODS = new Set(['query', 'execute', 'raw', 'exec', 'queryFile', 'none', 'one', 'many', 'any', 'oneOrNone', 'manyOrNone', 'result']);
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Extract method calls AND SQL queries from AST node in a single walk.
|
|
214
|
-
* Combines what was previously two separate walk.simple() calls.
|
|
215
|
-
* @param {Object} node
|
|
216
|
-
* @param {string[]} calls
|
|
217
|
-
* @param {string[]} [dbReads]
|
|
218
|
-
* @param {string[]} [dbWrites]
|
|
219
|
-
*/
|
|
220
|
-
function extractCallsAndSQL(node, calls, dbReads, dbWrites) {
|
|
221
|
-
if (!node) return;
|
|
222
|
-
|
|
223
|
-
walk.simple(node, {
|
|
224
|
-
CallExpression(callNode) {
|
|
225
|
-
const callee = callNode.callee;
|
|
226
|
-
|
|
227
|
-
// === Call extraction ===
|
|
228
|
-
if (callee.type === 'MemberExpression') {
|
|
229
|
-
const object = callee.object;
|
|
230
|
-
const property = callee.property;
|
|
231
|
-
|
|
232
|
-
if (property.type === 'Identifier') {
|
|
233
|
-
if (object.type === 'Identifier') {
|
|
234
|
-
const call = `${object.name}.${property.name}`;
|
|
235
|
-
if (!calls.includes(call)) calls.push(call);
|
|
236
|
-
} else if (object.type === 'MemberExpression' && object.property.type === 'Identifier') {
|
|
237
|
-
const call = `${object.property.name}.${property.name}`;
|
|
238
|
-
if (!calls.includes(call)) calls.push(call);
|
|
239
|
-
} else if (object.type === 'ThisExpression') {
|
|
240
|
-
const call = property.name;
|
|
241
|
-
if (!calls.includes(call)) calls.push(call);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
} else if (callee.type === 'Identifier') {
|
|
245
|
-
const call = callee.name;
|
|
246
|
-
if (!calls.includes(call)) calls.push(call);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// === SQL extraction from DB client calls ===
|
|
250
|
-
if (dbReads && dbWrites) {
|
|
251
|
-
const methodName = getCallMethodName(callNode);
|
|
252
|
-
if (methodName && DB_METHODS.has(methodName) && callNode.arguments.length > 0) {
|
|
253
|
-
const sqlStr = extractStringValue(callNode.arguments[0]);
|
|
254
|
-
if (sqlStr && isSQLString(sqlStr)) {
|
|
255
|
-
const ext = extractSQLFromString(sqlStr);
|
|
256
|
-
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
257
|
-
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
// === SQL: Tagged templates ===
|
|
264
|
-
TaggedTemplateExpression(tagNode) {
|
|
265
|
-
if (!dbReads || !dbWrites) return;
|
|
266
|
-
const tagName = getTagName(tagNode.tag);
|
|
267
|
-
if (tagName && /sql/i.test(tagName)) {
|
|
268
|
-
const sqlStr = templateToString(tagNode.quasi);
|
|
269
|
-
if (sqlStr) {
|
|
270
|
-
const ext = extractSQLFromString(sqlStr);
|
|
271
|
-
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
272
|
-
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
|
|
277
|
-
// === SQL: Standalone template literals ===
|
|
278
|
-
TemplateLiteral(tplNode) {
|
|
279
|
-
if (!dbReads || !dbWrites) return;
|
|
280
|
-
const sqlStr = templateToString(tplNode);
|
|
281
|
-
if (sqlStr && isSQLString(sqlStr)) {
|
|
282
|
-
const ext = extractSQLFromString(sqlStr);
|
|
283
|
-
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
284
|
-
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
285
|
-
}
|
|
286
|
-
},
|
|
287
|
-
|
|
288
|
-
// === SQL: String literals ===
|
|
289
|
-
Literal(litNode) {
|
|
290
|
-
if (!dbReads || !dbWrites) return;
|
|
291
|
-
if (typeof litNode.value === 'string' && isSQLString(litNode.value)) {
|
|
292
|
-
const ext = extractSQLFromString(litNode.value);
|
|
293
|
-
ext.reads.forEach(t => { if (!dbReads.includes(t)) dbReads.push(t); });
|
|
294
|
-
ext.writes.forEach(t => { if (!dbWrites.includes(t)) dbWrites.push(t); });
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Get tag name from tagged template expression.
|
|
302
|
-
* Handles: sql`...`, Prisma.sql`...`, db.sql`...`
|
|
303
|
-
* @param {Object} tag - AST node
|
|
304
|
-
* @returns {string|null}
|
|
305
|
-
*/
|
|
306
|
-
function getTagName(tag) {
|
|
307
|
-
if (tag.type === 'Identifier') return tag.name;
|
|
308
|
-
if (tag.type === 'MemberExpression' && tag.property.type === 'Identifier') {
|
|
309
|
-
return tag.property.name;
|
|
310
|
-
}
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/**
|
|
315
|
-
* Get method name from a CallExpression callee.
|
|
316
|
-
* @param {Object} callNode
|
|
317
|
-
* @returns {string|null}
|
|
318
|
-
*/
|
|
319
|
-
function getCallMethodName(callNode) {
|
|
320
|
-
const callee = callNode.callee;
|
|
321
|
-
if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier') {
|
|
322
|
-
return callee.property.name;
|
|
323
|
-
}
|
|
324
|
-
return null;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Extract string value from AST node (Literal or TemplateLiteral).
|
|
329
|
-
* For templates with expressions, substitutes $N placeholders.
|
|
330
|
-
* @param {Object} node
|
|
331
|
-
* @returns {string|null}
|
|
332
|
-
*/
|
|
333
|
-
function extractStringValue(node) {
|
|
334
|
-
if (!node) return null;
|
|
335
|
-
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
336
|
-
return node.value;
|
|
337
|
-
}
|
|
338
|
-
if (node.type === 'TemplateLiteral') {
|
|
339
|
-
return templateToString(node);
|
|
340
|
-
}
|
|
341
|
-
return null;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Convert TemplateLiteral AST node to string.
|
|
346
|
-
* Expressions are replaced with $N placeholders.
|
|
347
|
-
* @param {Object} tplNode
|
|
348
|
-
* @returns {string}
|
|
349
|
-
*/
|
|
350
|
-
function templateToString(tplNode) {
|
|
351
|
-
if (!tplNode || !tplNode.quasis) return '';
|
|
352
|
-
let result = '';
|
|
353
|
-
for (let i = 0; i < tplNode.quasis.length; i++) {
|
|
354
|
-
result += tplNode.quasis[i].value.cooked || tplNode.quasis[i].value.raw || '';
|
|
355
|
-
if (i < tplNode.expressions?.length) {
|
|
356
|
-
result += '$' + (i + 1);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Discover sub-projects in a monorepo directory structure
|
|
364
|
-
* @param {string} rootDir
|
|
365
|
-
* @returns {Array<{name: string, path: string, absolutePath: string}>}
|
|
366
|
-
*/
|
|
367
|
-
export function discoverSubProjects(rootDir) {
|
|
368
|
-
const resolvedRoot = resolve(rootDir);
|
|
369
|
-
const subProjects = [];
|
|
370
|
-
|
|
371
|
-
// Known monorepo directory conventions
|
|
372
|
-
const MONO_DIRS = ['packages', 'apps', 'services', 'modules', 'libs', 'plugins'];
|
|
373
|
-
|
|
374
|
-
for (const monoDir of MONO_DIRS) {
|
|
375
|
-
const monoPath = join(resolvedRoot, monoDir);
|
|
376
|
-
if (!existsSync(monoPath)) continue;
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
for (const entry of readdirSync(monoPath)) {
|
|
380
|
-
const entryPath = join(monoPath, entry);
|
|
381
|
-
const pkgPath = join(entryPath, 'package.json');
|
|
382
|
-
if (statSync(entryPath).isDirectory() && existsSync(pkgPath)) {
|
|
383
|
-
try {
|
|
384
|
-
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
385
|
-
subProjects.push({
|
|
386
|
-
name: pkg.name || entry,
|
|
387
|
-
path: relative(resolvedRoot, entryPath),
|
|
388
|
-
absolutePath: entryPath,
|
|
389
|
-
});
|
|
390
|
-
} catch {
|
|
391
|
-
subProjects.push({ name: entry, path: relative(resolvedRoot, entryPath), absolutePath: entryPath });
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
} catch { /* dir not readable */ }
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return subProjects;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Parse all JS files in a directory
|
|
403
|
-
* @param {string} dir
|
|
404
|
-
* @param {Object} [options={}]
|
|
405
|
-
* @param {boolean} [options.recursive=false]
|
|
406
|
-
* @returns {Promise<ParseResult>}
|
|
407
|
-
*/
|
|
408
|
-
export async function parseProject(dir, options = {}) {
|
|
409
|
-
const result = {
|
|
410
|
-
files: [],
|
|
411
|
-
classes: [],
|
|
412
|
-
functions: [],
|
|
413
|
-
imports: [],
|
|
414
|
-
exports: [],
|
|
415
|
-
tables: [],
|
|
416
|
-
};
|
|
417
|
-
|
|
418
|
-
const resolvedDir = resolve(dir);
|
|
419
|
-
const files = findJSFiles(dir);
|
|
420
|
-
|
|
421
|
-
for (const file of files) {
|
|
422
|
-
try {
|
|
423
|
-
const content = readFileSync(file, 'utf-8');
|
|
424
|
-
const relPath = relative(resolvedDir, file);
|
|
425
|
-
const parsed = await parseFileByExtension(content, relPath);
|
|
426
|
-
|
|
427
|
-
result.files.push(relPath);
|
|
428
|
-
result.classes.push(...parsed.classes);
|
|
429
|
-
result.functions.push(...parsed.functions);
|
|
430
|
-
result.imports.push(...parsed.imports);
|
|
431
|
-
result.exports.push(...parsed.exports);
|
|
432
|
-
if (parsed.tables?.length) {
|
|
433
|
-
result.tables.push(...parsed.tables);
|
|
434
|
-
}
|
|
435
|
-
} catch (e) {
|
|
436
|
-
// Ignore unreadable files
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Recursive monorepo support
|
|
441
|
-
if (options.recursive) {
|
|
442
|
-
const subs = discoverSubProjects(dir);
|
|
443
|
-
result.subProjects = [];
|
|
444
|
-
for (const sub of subs) {
|
|
445
|
-
try {
|
|
446
|
-
const subResult = await parseProject(sub.absolutePath);
|
|
447
|
-
// Prefix all file paths with sub-project path
|
|
448
|
-
for (const f of subResult.files) {
|
|
449
|
-
result.files.push(join(sub.path, f));
|
|
450
|
-
}
|
|
451
|
-
for (const c of subResult.classes) {
|
|
452
|
-
c.file = join(sub.path, c.file);
|
|
453
|
-
result.classes.push(c);
|
|
454
|
-
}
|
|
455
|
-
for (const fn of subResult.functions) {
|
|
456
|
-
fn.file = join(sub.path, fn.file);
|
|
457
|
-
result.functions.push(fn);
|
|
458
|
-
}
|
|
459
|
-
result.imports.push(...subResult.imports);
|
|
460
|
-
result.exports.push(...subResult.exports);
|
|
461
|
-
if (subResult.tables?.length) result.tables.push(...subResult.tables);
|
|
462
|
-
result.subProjects.push({ name: sub.name, path: sub.path, files: subResult.files.length });
|
|
463
|
-
} catch { /* sub-project parse failure is non-fatal */ }
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Dedupe imports/exports
|
|
468
|
-
result.imports = [...new Set(result.imports)];
|
|
469
|
-
result.exports = [...new Set(result.exports)];
|
|
470
|
-
|
|
471
|
-
return result;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Route file to appropriate parser based on extension.
|
|
476
|
-
* @param {string} code
|
|
477
|
-
* @param {string} filename
|
|
478
|
-
* @returns {Promise<ParseResult>}
|
|
479
|
-
*/
|
|
480
|
-
async function parseFileByExtension(code, filename) {
|
|
481
|
-
if (filename.endsWith('.sql')) {
|
|
482
|
-
return parseSQL(code, filename);
|
|
483
|
-
}
|
|
484
|
-
if (filename.endsWith('.py')) {
|
|
485
|
-
return parsePython(code, filename);
|
|
486
|
-
}
|
|
487
|
-
if (filename.endsWith('.go')) {
|
|
488
|
-
return parseGo(code, filename);
|
|
489
|
-
}
|
|
490
|
-
if (filename.endsWith('.ts') || filename.endsWith('.tsx')) {
|
|
491
|
-
return parseTypeScript(code, filename);
|
|
492
|
-
}
|
|
493
|
-
// Default: JS via Acorn
|
|
494
|
-
return parseFile(code, filename);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Check if file is a supported source file.
|
|
499
|
-
* @param {string} filename
|
|
500
|
-
* @returns {boolean}
|
|
501
|
-
*/
|
|
502
|
-
function isSourceFile(filename) {
|
|
503
|
-
// Exclude Symbiote.js presentation files
|
|
504
|
-
if (filename.endsWith('.css.js') || filename.endsWith('.tpl.js')) {
|
|
505
|
-
return false;
|
|
506
|
-
}
|
|
507
|
-
return SOURCE_EXTENSIONS.some(ext => filename.endsWith(ext));
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
/**
|
|
511
|
-
* Find all JS files recursively (uses filter configuration)
|
|
512
|
-
* @param {string} dir
|
|
513
|
-
* @param {string} [rootDir] - Root directory for relative path calculation
|
|
514
|
-
* @returns {string[]}
|
|
515
|
-
*/
|
|
516
|
-
export function findJSFiles(dir, rootDir = dir) {
|
|
517
|
-
// Parse gitignore on first call
|
|
518
|
-
if (dir === rootDir) {
|
|
519
|
-
parseGitignore(rootDir);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const files = [];
|
|
523
|
-
|
|
524
|
-
try {
|
|
525
|
-
for (const entry of readdirSync(dir)) {
|
|
526
|
-
const fullPath = join(dir, entry);
|
|
527
|
-
const stat = statSync(fullPath);
|
|
528
|
-
const relativePath = relative(rootDir, dir);
|
|
529
|
-
|
|
530
|
-
if (stat.isDirectory()) {
|
|
531
|
-
if (!shouldExcludeDir(entry, relativePath)) {
|
|
532
|
-
files.push(...findJSFiles(fullPath, rootDir));
|
|
533
|
-
}
|
|
534
|
-
} else if (isSourceFile(entry)) {
|
|
535
|
-
if (!shouldExcludeFile(entry, relativePath)) {
|
|
536
|
-
files.push(fullPath);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
} catch (e) {
|
|
541
|
-
console.warn(`Cannot read directory ${dir}:`, e.message);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return files;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// ============================
|
|
548
|
-
// JSDoc Type Extraction
|
|
549
|
-
// ============================
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Build a map of JSDoc comment end-lines to their extracted type info.
|
|
553
|
-
* @param {Array} comments - Acorn onComment array
|
|
554
|
-
* @param {string} code - Full source code
|
|
555
|
-
* @returns {Map<number, {params: Array<{name: string, type: string}>, returns: string|null}>}
|
|
556
|
-
*/
|
|
557
|
-
function buildJSDocTypeMap(comments, code) {
|
|
558
|
-
const map = new Map();
|
|
559
|
-
|
|
560
|
-
for (const comment of comments) {
|
|
561
|
-
// Only process JSDoc blocks (/** ... */)
|
|
562
|
-
if (comment.type !== 'Block' || !comment.value.startsWith('*')) continue;
|
|
563
|
-
|
|
564
|
-
const text = '/*' + comment.value + '*/';
|
|
565
|
-
const endLine = code.slice(0, comment.end).split('\n').length;
|
|
566
|
-
|
|
567
|
-
// Parse @param tags with balanced brace matching
|
|
568
|
-
const params = [];
|
|
569
|
-
const paramStartRegex = /@param\s+\{/g;
|
|
570
|
-
let paramStart;
|
|
571
|
-
while ((paramStart = paramStartRegex.exec(text)) !== null) {
|
|
572
|
-
// Find matching closing brace (balanced — handles {Array<{text: string}>})
|
|
573
|
-
let depth = 1;
|
|
574
|
-
let i = paramStart.index + paramStart[0].length;
|
|
575
|
-
while (i < text.length && depth > 0) {
|
|
576
|
-
if (text[i] === '{') depth++;
|
|
577
|
-
else if (text[i] === '}') depth--;
|
|
578
|
-
i++;
|
|
579
|
-
}
|
|
580
|
-
if (depth !== 0) continue;
|
|
581
|
-
const type = text.slice(paramStart.index + paramStart[0].length, i - 1);
|
|
582
|
-
// Extract param name after the closing brace
|
|
583
|
-
const afterType = text.slice(i);
|
|
584
|
-
const nameMatch = afterType.match(/^\s+(\[?\w+(?:\.\w+)*\]?)/);
|
|
585
|
-
if (!nameMatch) continue;
|
|
586
|
-
let name = nameMatch[1];
|
|
587
|
-
// Strip [] from optional params: [opts] → opts
|
|
588
|
-
if (name.startsWith('[')) name = name.slice(1);
|
|
589
|
-
if (name.endsWith(']')) name = name.slice(0, -1);
|
|
590
|
-
// Skip dotted paths (options.x)
|
|
591
|
-
if (name.includes('.')) continue;
|
|
592
|
-
params.push({ name, type });
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Parse @returns {Type}
|
|
596
|
-
let returns = null;
|
|
597
|
-
const returnsMatch = text.match(/@returns?\s+\{([^}]+)\}/);
|
|
598
|
-
if (returnsMatch) {
|
|
599
|
-
returns = returnsMatch[1];
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
if (params.length > 0 || returns) {
|
|
603
|
-
map.set(endLine, { params, returns });
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return map;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
/**
|
|
611
|
-
* Find the JSDoc entry that applies to a function at the given line.
|
|
612
|
-
* JSDoc must end within 2 lines above the function declaration.
|
|
613
|
-
* @param {Map} jsdocMap
|
|
614
|
-
* @param {number} funcLine - Function start line
|
|
615
|
-
* @returns {{ params: Array<{name: string, type: string}>, returns: string|null }|null}
|
|
616
|
-
*/
|
|
617
|
-
function findJSDocForNode(jsdocMap, funcLine) {
|
|
618
|
-
// JSDoc can end 1 or 2 lines above (direct or with blank line)
|
|
619
|
-
for (let offset = 1; offset <= 3; offset++) {
|
|
620
|
-
const entry = jsdocMap.get(funcLine - offset);
|
|
621
|
-
if (entry) return entry;
|
|
622
|
-
}
|
|
623
|
-
return null;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
/**
|
|
627
|
-
* Enrich AST-extracted param names with types from JSDoc.
|
|
628
|
-
* Input: ['filePath', 'options='] + jsdoc.params: [{name:'filePath', type:'string'}, {name:'options', type:'Object'}]
|
|
629
|
-
* Output: ['filePath:string', 'options:Object=']
|
|
630
|
-
* @param {string[]} rawParams
|
|
631
|
-
* @param {Object|null} jsdoc
|
|
632
|
-
* @returns {string[]}
|
|
633
|
-
*/
|
|
634
|
-
function enrichParamsWithTypes(rawParams, jsdoc) {
|
|
635
|
-
if (!jsdoc || jsdoc.params.length === 0) return rawParams;
|
|
636
|
-
|
|
637
|
-
// Build name→type lookup from JSDoc
|
|
638
|
-
const typeMap = new Map();
|
|
639
|
-
for (const p of jsdoc.params) {
|
|
640
|
-
typeMap.set(p.name, p.type);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
return rawParams.map(param => {
|
|
644
|
-
// Parse: '...name', 'name=', 'name', 'options'
|
|
645
|
-
const isRest = param.startsWith('...');
|
|
646
|
-
const hasDefault = param.endsWith('=');
|
|
647
|
-
let cleanName = param;
|
|
648
|
-
if (isRest) cleanName = cleanName.slice(3);
|
|
649
|
-
if (hasDefault) cleanName = cleanName.slice(0, -1);
|
|
650
|
-
|
|
651
|
-
let type = typeMap.get(cleanName);
|
|
652
|
-
if (!type) return param; // No JSDoc type found
|
|
653
|
-
|
|
654
|
-
// Strip JSDoc rest indicator {...Type} — rest is already from AST
|
|
655
|
-
if (type.startsWith('...')) type = type.slice(3);
|
|
656
|
-
|
|
657
|
-
// Reconstruct: ...name:Type, name:Type=, name:Type
|
|
658
|
-
const prefix = isRest ? '...' : '';
|
|
659
|
-
const suffix = hasDefault ? '=' : '';
|
|
660
|
-
return `${prefix}${cleanName}:${type}${suffix}`;
|
|
661
|
-
});
|
|
662
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Entry Point for Project Graph MCP
|
|
4
|
-
*
|
|
5
|
-
* Decides whether to run in CLI mode or MCP Server mode (stdio)
|
|
6
|
-
* Usage:
|
|
7
|
-
* npx project-graph-mcp -> stdio server
|
|
8
|
-
* npx project-graph-mcp <cmd> [args] -> CLI execution
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { startStdioServer } from './mcp-server.js';
|
|
12
|
-
import { runCLI } from './cli.js';
|
|
13
|
-
|
|
14
|
-
// Main execution logic
|
|
15
|
-
// We check endsWith('server.js') to verify this is the main module being run
|
|
16
|
-
if (process.argv[1] && (process.argv[1].endsWith('server.js') || process.argv[1].endsWith('project-graph-mcp'))) {
|
|
17
|
-
const [, , command, ...args] = process.argv;
|
|
18
|
-
|
|
19
|
-
if (command) {
|
|
20
|
-
// CLI mode
|
|
21
|
-
runCLI(command, args);
|
|
22
|
-
} else {
|
|
23
|
-
// MCP stdio mode
|
|
24
|
-
// Use stderr for logs so stdout remains clean for JSON-RPC
|
|
25
|
-
console.error('Starting Project Graph MCP (stdio)...');
|
|
26
|
-
startStdioServer();
|
|
27
|
-
}
|
|
28
|
-
}
|