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.
Files changed (125) hide show
  1. package/README.md +171 -31
  2. package/docs/img/explorer-compact.jpg +0 -0
  3. package/docs/img/explorer-expanded.jpg +0 -0
  4. package/package.json +12 -8
  5. package/src/.project-graph-cache.json +1 -1
  6. package/src/analysis/analysis-cache.js +7 -0
  7. package/src/analysis/complexity.js +14 -0
  8. package/src/analysis/custom-rules.js +36 -0
  9. package/src/analysis/db-analysis.js +9 -0
  10. package/src/analysis/dead-code.js +19 -0
  11. package/src/analysis/full-analysis.js +18 -0
  12. package/src/analysis/jsdoc-checker.js +24 -0
  13. package/src/analysis/jsdoc-generator.js +10 -0
  14. package/src/analysis/large-files.js +11 -0
  15. package/src/analysis/outdated-patterns.js +12 -0
  16. package/src/analysis/similar-functions.js +16 -0
  17. package/src/analysis/test-annotations.js +21 -0
  18. package/src/analysis/type-checker.js +8 -0
  19. package/src/analysis/undocumented.js +14 -0
  20. package/src/cli/cli-handlers.js +4 -0
  21. package/src/cli/cli.js +5 -0
  22. package/src/compact/.project-graph-cache.json +1 -0
  23. package/src/compact/ai-context.js +7 -0
  24. package/src/compact/compact-migrate.js +17 -0
  25. package/src/compact/compact.js +18 -0
  26. package/src/compact/compress.js +14 -0
  27. package/src/compact/ctx-to-jsdoc.js +29 -0
  28. package/src/compact/doc-dialect.js +30 -0
  29. package/src/compact/expand.js +37 -0
  30. package/src/compact/framework-references.js +5 -0
  31. package/src/compact/instructions.js +3 -0
  32. package/src/compact/mode-config.js +8 -0
  33. package/src/compact/validate-pipeline.js +9 -0
  34. package/src/core/event-bus.js +9 -0
  35. package/src/core/filters.js +14 -0
  36. package/src/core/graph-builder.js +12 -0
  37. package/src/core/parser.js +31 -0
  38. package/src/core/workspace.js +8 -0
  39. package/src/lang/lang-go.js +17 -0
  40. package/src/lang/lang-python.js +12 -0
  41. package/src/lang/lang-sql.js +23 -0
  42. package/src/lang/lang-typescript.js +9 -0
  43. package/src/lang/lang-utils.js +4 -0
  44. package/src/mcp/mcp-server.js +17 -0
  45. package/src/mcp/tool-defs.js +3 -0
  46. package/src/mcp/tools.js +25 -0
  47. package/src/network/backend-lifecycle.js +19 -0
  48. package/src/network/backend.js +5 -0
  49. package/src/network/local-gateway.js +23 -0
  50. package/src/network/mdns.js +13 -0
  51. package/src/network/server.js +10 -0
  52. package/src/network/web-server.js +34 -0
  53. package/web/.project-graph-cache.json +1 -0
  54. package/web/app.js +17 -0
  55. package/web/components/code-block.js +3 -0
  56. package/web/components/quick-open.js +5 -0
  57. package/web/dashboard-state.js +3 -0
  58. package/web/dashboard.html +27 -0
  59. package/web/dashboard.js +8 -0
  60. package/web/highlight.js +13 -0
  61. package/web/index.html +35 -0
  62. package/web/panels/ActionBoard/ActionBoard.css.js +1 -0
  63. package/web/panels/ActionBoard/ActionBoard.js +4 -0
  64. package/web/panels/ActionBoard/ActionBoard.tpl.js +1 -0
  65. package/web/panels/EventItem/EventItem.css.js +1 -0
  66. package/web/panels/EventItem/EventItem.js +4 -0
  67. package/web/panels/EventItem/EventItem.tpl.js +1 -0
  68. package/web/panels/ProjectItem/ProjectItem.css.js +1 -0
  69. package/web/panels/ProjectItem/ProjectItem.js +5 -0
  70. package/web/panels/ProjectItem/ProjectItem.tpl.js +1 -0
  71. package/web/panels/ProjectList/ProjectList.css.js +1 -0
  72. package/web/panels/ProjectList/ProjectList.js +4 -0
  73. package/web/panels/ProjectList/ProjectList.tpl.js +1 -0
  74. package/web/panels/SettingsPanel/.project-graph-cache.json +1 -0
  75. package/web/panels/SettingsPanel/SettingsPanel.css.js +1 -0
  76. package/web/panels/SettingsPanel/SettingsPanel.js +7 -0
  77. package/web/panels/SettingsPanel/SettingsPanel.tpl.js +1 -0
  78. package/web/panels/code-viewer.js +5 -0
  79. package/web/panels/ctx-panel.js +4 -0
  80. package/web/panels/dep-graph.js +6 -0
  81. package/web/panels/file-tree.js +188 -0
  82. package/web/panels/health-panel.js +3 -0
  83. package/web/panels/live-monitor.js +3 -0
  84. package/web/state.js +17 -0
  85. package/web/style.css +157 -0
  86. package/references/symbiote-3x.md +0 -834
  87. package/src/ai-context.js +0 -113
  88. package/src/analysis-cache.js +0 -155
  89. package/src/cli-handlers.js +0 -271
  90. package/src/cli.js +0 -95
  91. package/src/compact.js +0 -207
  92. package/src/complexity.js +0 -237
  93. package/src/compress.js +0 -319
  94. package/src/ctx-to-jsdoc.js +0 -514
  95. package/src/custom-rules.js +0 -584
  96. package/src/db-analysis.js +0 -194
  97. package/src/dead-code.js +0 -468
  98. package/src/doc-dialect.js +0 -716
  99. package/src/filters.js +0 -227
  100. package/src/framework-references.js +0 -177
  101. package/src/full-analysis.js +0 -470
  102. package/src/graph-builder.js +0 -299
  103. package/src/instructions.js +0 -73
  104. package/src/jsdoc-checker.js +0 -351
  105. package/src/jsdoc-generator.js +0 -203
  106. package/src/lang-go.js +0 -285
  107. package/src/lang-python.js +0 -197
  108. package/src/lang-sql.js +0 -309
  109. package/src/lang-typescript.js +0 -190
  110. package/src/lang-utils.js +0 -124
  111. package/src/large-files.js +0 -163
  112. package/src/mcp-server.js +0 -675
  113. package/src/mode-config.js +0 -127
  114. package/src/outdated-patterns.js +0 -296
  115. package/src/parser.js +0 -662
  116. package/src/server.js +0 -28
  117. package/src/similar-functions.js +0 -279
  118. package/src/test-annotations.js +0 -323
  119. package/src/tool-defs.js +0 -793
  120. package/src/tools.js +0 -470
  121. package/src/type-checker.js +0 -188
  122. package/src/undocumented.js +0 -259
  123. package/src/workspace.js +0 -70
  124. /package/{AGENT_ROLE.md → docs/examples/AGENT_ROLE.md} +0 -0
  125. /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
- }