activo 0.2.1 → 0.3.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 (73) hide show
  1. package/README.md +79 -3
  2. package/dist/core/commands.d.ts +11 -0
  3. package/dist/core/commands.d.ts.map +1 -0
  4. package/dist/core/commands.js +90 -0
  5. package/dist/core/commands.js.map +1 -0
  6. package/dist/core/llm/ollama.d.ts +2 -0
  7. package/dist/core/llm/ollama.d.ts.map +1 -1
  8. package/dist/core/llm/ollama.js +26 -0
  9. package/dist/core/llm/ollama.js.map +1 -1
  10. package/dist/core/tools/ast.d.ts +81 -0
  11. package/dist/core/tools/ast.d.ts.map +1 -0
  12. package/dist/core/tools/ast.js +700 -0
  13. package/dist/core/tools/ast.js.map +1 -0
  14. package/dist/core/tools/cache.d.ts +19 -0
  15. package/dist/core/tools/cache.d.ts.map +1 -0
  16. package/dist/core/tools/cache.js +497 -0
  17. package/dist/core/tools/cache.js.map +1 -0
  18. package/dist/core/tools/cssAnalysis.d.ts +3 -0
  19. package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
  20. package/dist/core/tools/cssAnalysis.js +270 -0
  21. package/dist/core/tools/cssAnalysis.js.map +1 -0
  22. package/dist/core/tools/embeddings.d.ts +8 -0
  23. package/dist/core/tools/embeddings.d.ts.map +1 -0
  24. package/dist/core/tools/embeddings.js +631 -0
  25. package/dist/core/tools/embeddings.js.map +1 -0
  26. package/dist/core/tools/frontendAst.d.ts +6 -0
  27. package/dist/core/tools/frontendAst.d.ts.map +1 -0
  28. package/dist/core/tools/frontendAst.js +680 -0
  29. package/dist/core/tools/frontendAst.js.map +1 -0
  30. package/dist/core/tools/htmlAnalysis.d.ts +3 -0
  31. package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
  32. package/dist/core/tools/htmlAnalysis.js +398 -0
  33. package/dist/core/tools/htmlAnalysis.js.map +1 -0
  34. package/dist/core/tools/index.d.ts +10 -0
  35. package/dist/core/tools/index.d.ts.map +1 -1
  36. package/dist/core/tools/index.js +21 -1
  37. package/dist/core/tools/index.js.map +1 -1
  38. package/dist/core/tools/javaAst.d.ts +6 -0
  39. package/dist/core/tools/javaAst.d.ts.map +1 -0
  40. package/dist/core/tools/javaAst.js +678 -0
  41. package/dist/core/tools/javaAst.js.map +1 -0
  42. package/dist/core/tools/memory.d.ts +11 -0
  43. package/dist/core/tools/memory.d.ts.map +1 -0
  44. package/dist/core/tools/memory.js +551 -0
  45. package/dist/core/tools/memory.js.map +1 -0
  46. package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
  47. package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
  48. package/dist/core/tools/mybatisAnalysis.js +251 -0
  49. package/dist/core/tools/mybatisAnalysis.js.map +1 -0
  50. package/dist/core/tools/sqlAnalysis.d.ts +3 -0
  51. package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
  52. package/dist/core/tools/sqlAnalysis.js +250 -0
  53. package/dist/core/tools/sqlAnalysis.js.map +1 -0
  54. package/dist/ui/App.d.ts.map +1 -1
  55. package/dist/ui/App.js +31 -2
  56. package/dist/ui/App.js.map +1 -1
  57. package/package.json +2 -1
  58. package/src/core/commands.ts +118 -0
  59. package/src/core/llm/ollama.ts +30 -0
  60. package/src/core/tools/ast.ts +826 -0
  61. package/src/core/tools/cache.ts +570 -0
  62. package/src/core/tools/cssAnalysis.ts +324 -0
  63. package/src/core/tools/embeddings.ts +746 -0
  64. package/src/core/tools/frontendAst.ts +802 -0
  65. package/src/core/tools/htmlAnalysis.ts +466 -0
  66. package/src/core/tools/index.ts +21 -1
  67. package/src/core/tools/javaAst.ts +812 -0
  68. package/src/core/tools/memory.ts +655 -0
  69. package/src/core/tools/mybatisAnalysis.ts +322 -0
  70. package/src/core/tools/sqlAnalysis.ts +298 -0
  71. package/src/ui/App.tsx +38 -2
  72. package/FINAL_SIMPLIFIED_SPEC.md +0 -456
  73. package/TODO.md +0 -193
@@ -0,0 +1,826 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import ts from "typescript";
4
+ import { Tool, ToolResult } from "./types.js";
5
+
6
+ // AST Node info interface
7
+ export interface ASTNodeInfo {
8
+ name: string;
9
+ kind: string;
10
+ line: number;
11
+ column: number;
12
+ children?: ASTNodeInfo[];
13
+ params?: string[];
14
+ returnType?: string;
15
+ modifiers?: string[];
16
+ jsdoc?: string;
17
+ }
18
+
19
+ // Function info interface
20
+ export interface FunctionInfo {
21
+ name: string;
22
+ kind: "function" | "method" | "arrow" | "constructor";
23
+ line: number;
24
+ params: Array<{ name: string; type: string; optional: boolean }>;
25
+ returnType: string;
26
+ async: boolean;
27
+ exported: boolean;
28
+ className?: string;
29
+ complexity: number;
30
+ calls: string[];
31
+ }
32
+
33
+ // Class info interface
34
+ export interface ClassInfo {
35
+ name: string;
36
+ line: number;
37
+ exported: boolean;
38
+ extends?: string;
39
+ implements: string[];
40
+ methods: FunctionInfo[];
41
+ properties: Array<{ name: string; type: string; visibility: string }>;
42
+ }
43
+
44
+ // File analysis result
45
+ export interface FileAnalysis {
46
+ filepath: string;
47
+ language: string;
48
+ imports: Array<{ module: string; names: string[] }>;
49
+ exports: string[];
50
+ functions: FunctionInfo[];
51
+ classes: ClassInfo[];
52
+ interfaces: Array<{ name: string; line: number; properties: string[] }>;
53
+ types: Array<{ name: string; line: number }>;
54
+ variables: Array<{ name: string; line: number; type: string; exported: boolean }>;
55
+ complexity: {
56
+ total: number;
57
+ average: number;
58
+ highest: { name: string; value: number };
59
+ };
60
+ }
61
+
62
+ // Calculate cyclomatic complexity
63
+ function calculateComplexity(node: ts.Node): number {
64
+ let complexity = 1;
65
+
66
+ function visit(n: ts.Node) {
67
+ switch (n.kind) {
68
+ case ts.SyntaxKind.IfStatement:
69
+ case ts.SyntaxKind.ConditionalExpression: // ternary
70
+ case ts.SyntaxKind.ForStatement:
71
+ case ts.SyntaxKind.ForInStatement:
72
+ case ts.SyntaxKind.ForOfStatement:
73
+ case ts.SyntaxKind.WhileStatement:
74
+ case ts.SyntaxKind.DoStatement:
75
+ case ts.SyntaxKind.CatchClause:
76
+ case ts.SyntaxKind.CaseClause:
77
+ case ts.SyntaxKind.DefaultClause:
78
+ complexity++;
79
+ break;
80
+ case ts.SyntaxKind.BinaryExpression:
81
+ const binary = n as ts.BinaryExpression;
82
+ if (
83
+ binary.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
84
+ binary.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
85
+ binary.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken
86
+ ) {
87
+ complexity++;
88
+ }
89
+ break;
90
+ }
91
+ ts.forEachChild(n, visit);
92
+ }
93
+
94
+ visit(node);
95
+ return complexity;
96
+ }
97
+
98
+ // Extract function calls from a node
99
+ function extractCalls(node: ts.Node, sourceFile: ts.SourceFile): string[] {
100
+ const calls: string[] = [];
101
+
102
+ function visit(n: ts.Node) {
103
+ if (ts.isCallExpression(n)) {
104
+ const expr = n.expression;
105
+ if (ts.isIdentifier(expr)) {
106
+ calls.push(expr.text);
107
+ } else if (ts.isPropertyAccessExpression(expr)) {
108
+ calls.push(expr.name.text);
109
+ }
110
+ }
111
+ ts.forEachChild(n, visit);
112
+ }
113
+
114
+ visit(node);
115
+ return [...new Set(calls)];
116
+ }
117
+
118
+ // Get type string from TypeNode
119
+ function getTypeString(typeNode: ts.TypeNode | undefined, sourceFile: ts.SourceFile): string {
120
+ if (!typeNode) return "any";
121
+ return typeNode.getText(sourceFile);
122
+ }
123
+
124
+ // Get modifiers as strings
125
+ function getModifiers(node: ts.Node): string[] {
126
+ const modifiers: string[] = [];
127
+ const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
128
+ if (mods) {
129
+ for (const mod of mods) {
130
+ modifiers.push(ts.SyntaxKind[mod.kind].replace("Keyword", "").toLowerCase());
131
+ }
132
+ }
133
+ return modifiers;
134
+ }
135
+
136
+ // Analyze TypeScript/JavaScript file
137
+ function analyzeTypeScript(content: string, filepath: string): FileAnalysis {
138
+ const sourceFile = ts.createSourceFile(
139
+ filepath,
140
+ content,
141
+ ts.ScriptTarget.Latest,
142
+ true,
143
+ filepath.endsWith(".tsx") || filepath.endsWith(".jsx")
144
+ ? ts.ScriptKind.TSX
145
+ : filepath.endsWith(".ts")
146
+ ? ts.ScriptKind.TS
147
+ : ts.ScriptKind.JS
148
+ );
149
+
150
+ const analysis: FileAnalysis = {
151
+ filepath,
152
+ language: filepath.endsWith(".ts") || filepath.endsWith(".tsx") ? "typescript" : "javascript",
153
+ imports: [],
154
+ exports: [],
155
+ functions: [],
156
+ classes: [],
157
+ interfaces: [],
158
+ types: [],
159
+ variables: [],
160
+ complexity: { total: 0, average: 0, highest: { name: "", value: 0 } },
161
+ };
162
+
163
+ function visit(node: ts.Node) {
164
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
165
+ const lineNum = line + 1;
166
+ const modifiers = getModifiers(node);
167
+ const isExported = modifiers.includes("export");
168
+
169
+ // Import declarations
170
+ if (ts.isImportDeclaration(node)) {
171
+ const moduleSpecifier = node.moduleSpecifier;
172
+ if (ts.isStringLiteral(moduleSpecifier)) {
173
+ const names: string[] = [];
174
+ const importClause = node.importClause;
175
+ if (importClause) {
176
+ if (importClause.name) {
177
+ names.push(importClause.name.text);
178
+ }
179
+ if (importClause.namedBindings) {
180
+ if (ts.isNamedImports(importClause.namedBindings)) {
181
+ for (const element of importClause.namedBindings.elements) {
182
+ names.push(element.name.text);
183
+ }
184
+ } else if (ts.isNamespaceImport(importClause.namedBindings)) {
185
+ names.push(`* as ${importClause.namedBindings.name.text}`);
186
+ }
187
+ }
188
+ }
189
+ analysis.imports.push({ module: moduleSpecifier.text, names });
190
+ }
191
+ }
192
+
193
+ // Export declarations
194
+ if (ts.isExportDeclaration(node)) {
195
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
196
+ for (const element of node.exportClause.elements) {
197
+ analysis.exports.push(element.name.text);
198
+ }
199
+ }
200
+ }
201
+
202
+ // Function declarations
203
+ if (ts.isFunctionDeclaration(node) && node.name) {
204
+ const funcInfo: FunctionInfo = {
205
+ name: node.name.text,
206
+ kind: "function",
207
+ line: lineNum,
208
+ params: node.parameters.map((p) => ({
209
+ name: ts.isIdentifier(p.name) ? p.name.text : p.name.getText(sourceFile),
210
+ type: getTypeString(p.type, sourceFile),
211
+ optional: !!p.questionToken,
212
+ })),
213
+ returnType: getTypeString(node.type, sourceFile),
214
+ async: modifiers.includes("async"),
215
+ exported: isExported,
216
+ complexity: calculateComplexity(node),
217
+ calls: extractCalls(node, sourceFile),
218
+ };
219
+ analysis.functions.push(funcInfo);
220
+ if (isExported) analysis.exports.push(node.name.text);
221
+ }
222
+
223
+ // Arrow functions in variable declarations
224
+ if (ts.isVariableStatement(node)) {
225
+ const isVarExported = getModifiers(node).includes("export");
226
+ for (const decl of node.declarationList.declarations) {
227
+ if (ts.isIdentifier(decl.name)) {
228
+ const varName = decl.name.text;
229
+
230
+ if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
231
+ const func = decl.initializer;
232
+ const funcInfo: FunctionInfo = {
233
+ name: varName,
234
+ kind: "arrow",
235
+ line: lineNum,
236
+ params: func.parameters.map((p) => ({
237
+ name: ts.isIdentifier(p.name) ? p.name.text : p.name.getText(sourceFile),
238
+ type: getTypeString(p.type, sourceFile),
239
+ optional: !!p.questionToken,
240
+ })),
241
+ returnType: getTypeString(func.type, sourceFile),
242
+ async: !!(ts.canHaveModifiers(func) && ts.getModifiers(func)?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword)),
243
+ exported: isVarExported,
244
+ complexity: calculateComplexity(func),
245
+ calls: extractCalls(func, sourceFile),
246
+ };
247
+ analysis.functions.push(funcInfo);
248
+ } else {
249
+ // Regular variable
250
+ analysis.variables.push({
251
+ name: varName,
252
+ line: lineNum,
253
+ type: decl.type ? getTypeString(decl.type, sourceFile) : "inferred",
254
+ exported: isVarExported,
255
+ });
256
+ }
257
+
258
+ if (isVarExported) analysis.exports.push(varName);
259
+ }
260
+ }
261
+ }
262
+
263
+ // Class declarations
264
+ if (ts.isClassDeclaration(node) && node.name) {
265
+ const classInfo: ClassInfo = {
266
+ name: node.name.text,
267
+ line: lineNum,
268
+ exported: isExported,
269
+ implements: [],
270
+ methods: [],
271
+ properties: [],
272
+ };
273
+
274
+ // Heritage (extends, implements)
275
+ if (node.heritageClauses) {
276
+ for (const clause of node.heritageClauses) {
277
+ if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
278
+ classInfo.extends = clause.types[0]?.expression.getText(sourceFile);
279
+ } else if (clause.token === ts.SyntaxKind.ImplementsKeyword) {
280
+ classInfo.implements = clause.types.map((t) => t.expression.getText(sourceFile));
281
+ }
282
+ }
283
+ }
284
+
285
+ // Class members
286
+ for (const member of node.members) {
287
+ const memberMods = getModifiers(member);
288
+ const visibility = memberMods.includes("private")
289
+ ? "private"
290
+ : memberMods.includes("protected")
291
+ ? "protected"
292
+ : "public";
293
+
294
+ if (ts.isMethodDeclaration(member) && member.name) {
295
+ const methodName = member.name.getText(sourceFile);
296
+ const methodInfo: FunctionInfo = {
297
+ name: methodName,
298
+ kind: "method",
299
+ line: sourceFile.getLineAndCharacterOfPosition(member.getStart()).line + 1,
300
+ params: member.parameters.map((p) => ({
301
+ name: ts.isIdentifier(p.name) ? p.name.text : p.name.getText(sourceFile),
302
+ type: getTypeString(p.type, sourceFile),
303
+ optional: !!p.questionToken,
304
+ })),
305
+ returnType: getTypeString(member.type, sourceFile),
306
+ async: memberMods.includes("async"),
307
+ exported: false,
308
+ className: node.name!.text,
309
+ complexity: calculateComplexity(member),
310
+ calls: extractCalls(member, sourceFile),
311
+ };
312
+ classInfo.methods.push(methodInfo);
313
+ } else if (ts.isConstructorDeclaration(member)) {
314
+ const ctorInfo: FunctionInfo = {
315
+ name: "constructor",
316
+ kind: "constructor",
317
+ line: sourceFile.getLineAndCharacterOfPosition(member.getStart()).line + 1,
318
+ params: member.parameters.map((p) => ({
319
+ name: ts.isIdentifier(p.name) ? p.name.text : p.name.getText(sourceFile),
320
+ type: getTypeString(p.type, sourceFile),
321
+ optional: !!p.questionToken,
322
+ })),
323
+ returnType: node.name!.text,
324
+ async: false,
325
+ exported: false,
326
+ className: node.name!.text,
327
+ complexity: calculateComplexity(member),
328
+ calls: extractCalls(member, sourceFile),
329
+ };
330
+ classInfo.methods.push(ctorInfo);
331
+ } else if (ts.isPropertyDeclaration(member) && member.name) {
332
+ classInfo.properties.push({
333
+ name: member.name.getText(sourceFile),
334
+ type: getTypeString(member.type, sourceFile),
335
+ visibility,
336
+ });
337
+ }
338
+ }
339
+
340
+ analysis.classes.push(classInfo);
341
+ if (isExported) analysis.exports.push(node.name.text);
342
+ }
343
+
344
+ // Interface declarations
345
+ if (ts.isInterfaceDeclaration(node)) {
346
+ const props = node.members
347
+ .filter((m) => ts.isPropertySignature(m) && m.name)
348
+ .map((m) => (m as ts.PropertySignature).name!.getText(sourceFile));
349
+
350
+ analysis.interfaces.push({
351
+ name: node.name.text,
352
+ line: lineNum,
353
+ properties: props,
354
+ });
355
+ if (isExported) analysis.exports.push(node.name.text);
356
+ }
357
+
358
+ // Type alias declarations
359
+ if (ts.isTypeAliasDeclaration(node)) {
360
+ analysis.types.push({
361
+ name: node.name.text,
362
+ line: lineNum,
363
+ });
364
+ if (isExported) analysis.exports.push(node.name.text);
365
+ }
366
+
367
+ ts.forEachChild(node, visit);
368
+ }
369
+
370
+ visit(sourceFile);
371
+
372
+ // Calculate complexity stats
373
+ const allFuncs = [...analysis.functions, ...analysis.classes.flatMap((c) => c.methods)];
374
+ if (allFuncs.length > 0) {
375
+ const complexities = allFuncs.map((f) => ({ name: f.name, value: f.complexity }));
376
+ analysis.complexity.total = complexities.reduce((sum, c) => sum + c.value, 0);
377
+ analysis.complexity.average = Math.round(analysis.complexity.total / complexities.length * 10) / 10;
378
+ analysis.complexity.highest = complexities.reduce(
379
+ (max, c) => (c.value > max.value ? c : max),
380
+ { name: "", value: 0 }
381
+ );
382
+ }
383
+
384
+ // Deduplicate exports
385
+ analysis.exports = [...new Set(analysis.exports)];
386
+
387
+ return analysis;
388
+ }
389
+
390
+ // Format analysis as readable text
391
+ function formatAnalysis(analysis: FileAnalysis): string {
392
+ const lines: string[] = [];
393
+
394
+ lines.push(`=== ${path.basename(analysis.filepath)} (${analysis.language}) ===`);
395
+ lines.push("");
396
+
397
+ // Imports
398
+ if (analysis.imports.length > 0) {
399
+ lines.push("📥 Imports:");
400
+ for (const imp of analysis.imports) {
401
+ const names = imp.names.length > 0 ? ` { ${imp.names.join(", ")} }` : "";
402
+ lines.push(` ${imp.module}${names}`);
403
+ }
404
+ lines.push("");
405
+ }
406
+
407
+ // Exports
408
+ if (analysis.exports.length > 0) {
409
+ lines.push("📤 Exports:");
410
+ lines.push(` ${analysis.exports.join(", ")}`);
411
+ lines.push("");
412
+ }
413
+
414
+ // Classes
415
+ if (analysis.classes.length > 0) {
416
+ lines.push("🏛️ Classes:");
417
+ for (const cls of analysis.classes) {
418
+ const ext = cls.extends ? ` extends ${cls.extends}` : "";
419
+ const impl = cls.implements.length > 0 ? ` implements ${cls.implements.join(", ")}` : "";
420
+ lines.push(` L${cls.line}: ${cls.exported ? "export " : ""}class ${cls.name}${ext}${impl}`);
421
+
422
+ for (const prop of cls.properties) {
423
+ lines.push(` • ${prop.visibility} ${prop.name}: ${prop.type}`);
424
+ }
425
+ for (const method of cls.methods) {
426
+ const params = method.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
427
+ const async = method.async ? "async " : "";
428
+ lines.push(` → ${async}${method.name}(${params}): ${method.returnType} [복잡도: ${method.complexity}]`);
429
+ }
430
+ }
431
+ lines.push("");
432
+ }
433
+
434
+ // Functions
435
+ if (analysis.functions.length > 0) {
436
+ lines.push("⚡ Functions:");
437
+ for (const func of analysis.functions) {
438
+ const params = func.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ");
439
+ const async = func.async ? "async " : "";
440
+ const exp = func.exported ? "export " : "";
441
+ const arrow = func.kind === "arrow" ? "=> " : "";
442
+ lines.push(` L${func.line}: ${exp}${async}${func.name}(${params}) ${arrow}: ${func.returnType}`);
443
+ lines.push(` 복잡도: ${func.complexity} | 호출: ${func.calls.slice(0, 5).join(", ") || "(없음)"}`);
444
+ }
445
+ lines.push("");
446
+ }
447
+
448
+ // Interfaces
449
+ if (analysis.interfaces.length > 0) {
450
+ lines.push("📋 Interfaces:");
451
+ for (const iface of analysis.interfaces) {
452
+ lines.push(` L${iface.line}: interface ${iface.name} { ${iface.properties.slice(0, 5).join(", ")}${iface.properties.length > 5 ? ", ..." : ""} }`);
453
+ }
454
+ lines.push("");
455
+ }
456
+
457
+ // Types
458
+ if (analysis.types.length > 0) {
459
+ lines.push("🏷️ Types:");
460
+ for (const t of analysis.types) {
461
+ lines.push(` L${t.line}: type ${t.name}`);
462
+ }
463
+ lines.push("");
464
+ }
465
+
466
+ // Variables
467
+ if (analysis.variables.length > 0) {
468
+ lines.push("📦 Variables:");
469
+ for (const v of analysis.variables) {
470
+ const exp = v.exported ? "export " : "";
471
+ lines.push(` L${v.line}: ${exp}${v.name}: ${v.type}`);
472
+ }
473
+ lines.push("");
474
+ }
475
+
476
+ // Complexity summary
477
+ lines.push("📊 Complexity:");
478
+ lines.push(` 총합: ${analysis.complexity.total} | 평균: ${analysis.complexity.average}`);
479
+ if (analysis.complexity.highest.name) {
480
+ lines.push(` 최고: ${analysis.complexity.highest.name} (${analysis.complexity.highest.value})`);
481
+ }
482
+
483
+ return lines.join("\n");
484
+ }
485
+
486
+ // AST Analyze Tool
487
+ export const astAnalyzeTool: Tool = {
488
+ name: "ast_analyze",
489
+ description: "Deep code analysis using AST parser (AST 분석, 심층 분석). Returns functions, classes, imports, exports, complexity. More accurate than outline. Use when user asks: 'analyze code', 'deep analysis', 'complexity', 'AST', '심층 분석', '복잡도'.",
490
+ parameters: {
491
+ type: "object",
492
+ required: ["filepath"],
493
+ properties: {
494
+ filepath: {
495
+ type: "string",
496
+ description: "Path to TypeScript/JavaScript file",
497
+ },
498
+ format: {
499
+ type: "string",
500
+ description: "Output format: 'text' (readable) or 'json' (structured)",
501
+ enum: ["text", "json"],
502
+ },
503
+ },
504
+ },
505
+ handler: async (args): Promise<ToolResult> => {
506
+ try {
507
+ const filepath = path.resolve(args.filepath as string);
508
+ const format = (args.format as string) || "text";
509
+
510
+ if (!fs.existsSync(filepath)) {
511
+ return { success: false, content: "", error: `File not found: ${filepath}` };
512
+ }
513
+
514
+ const ext = path.extname(filepath).toLowerCase();
515
+ if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
516
+ return {
517
+ success: false,
518
+ content: "",
519
+ error: `Unsupported file type: ${ext}. Supported: .ts, .tsx, .js, .jsx, .mjs, .cjs`,
520
+ };
521
+ }
522
+
523
+ const content = fs.readFileSync(filepath, "utf-8");
524
+ const analysis = analyzeTypeScript(content, filepath);
525
+
526
+ if (format === "json") {
527
+ return { success: true, content: JSON.stringify(analysis, null, 2) };
528
+ }
529
+
530
+ return { success: true, content: formatAnalysis(analysis) };
531
+ } catch (error) {
532
+ return { success: false, content: "", error: String(error) };
533
+ }
534
+ },
535
+ };
536
+
537
+ // Find function calls (call graph)
538
+ export const getCallGraphTool: Tool = {
539
+ name: "get_call_graph",
540
+ description: "Find what functions a file/function calls (호출 그래프, 의존성). Shows function call relationships. Use when user asks: 'what does it call', 'call graph', 'dependencies', '호출 관계', '의존성'.",
541
+ parameters: {
542
+ type: "object",
543
+ required: ["filepath"],
544
+ properties: {
545
+ filepath: {
546
+ type: "string",
547
+ description: "Path to TypeScript/JavaScript file",
548
+ },
549
+ functionName: {
550
+ type: "string",
551
+ description: "Optional: specific function name to analyze",
552
+ },
553
+ },
554
+ },
555
+ handler: async (args): Promise<ToolResult> => {
556
+ try {
557
+ const filepath = path.resolve(args.filepath as string);
558
+ const targetFunc = args.functionName as string | undefined;
559
+
560
+ if (!fs.existsSync(filepath)) {
561
+ return { success: false, content: "", error: `File not found: ${filepath}` };
562
+ }
563
+
564
+ const content = fs.readFileSync(filepath, "utf-8");
565
+ const analysis = analyzeTypeScript(content, filepath);
566
+
567
+ const lines: string[] = [];
568
+ lines.push(`=== 호출 그래프: ${path.basename(filepath)} ===`);
569
+ lines.push("");
570
+
571
+ const allFuncs = [...analysis.functions, ...analysis.classes.flatMap((c) => c.methods)];
572
+
573
+ if (targetFunc) {
574
+ const func = allFuncs.find((f) => f.name === targetFunc);
575
+ if (!func) {
576
+ return { success: false, content: "", error: `Function not found: ${targetFunc}` };
577
+ }
578
+ lines.push(`📍 ${func.name}() 호출:`);
579
+ if (func.calls.length > 0) {
580
+ func.calls.forEach((c) => lines.push(` → ${c}()`));
581
+ } else {
582
+ lines.push(" (다른 함수 호출 없음)");
583
+ }
584
+ } else {
585
+ for (const func of allFuncs) {
586
+ const prefix = func.className ? `${func.className}.` : "";
587
+ lines.push(`📍 ${prefix}${func.name}():`);
588
+ if (func.calls.length > 0) {
589
+ func.calls.forEach((c) => lines.push(` → ${c}()`));
590
+ } else {
591
+ lines.push(" (다른 함수 호출 없음)");
592
+ }
593
+ lines.push("");
594
+ }
595
+ }
596
+
597
+ return { success: true, content: lines.join("\n") };
598
+ } catch (error) {
599
+ return { success: false, content: "", error: String(error) };
600
+ }
601
+ },
602
+ };
603
+
604
+ // Find references (where a symbol is used)
605
+ export const findReferencesTool: Tool = {
606
+ name: "find_symbol_usage",
607
+ description: "Find where a symbol (function, class, variable) is used in files (심볼 사용처 찾기). Use when user asks: 'where is X used', 'find usages', 'references', '어디서 사용', '참조'.",
608
+ parameters: {
609
+ type: "object",
610
+ required: ["symbol", "pattern"],
611
+ properties: {
612
+ symbol: {
613
+ type: "string",
614
+ description: "Symbol name to find (function, class, variable)",
615
+ },
616
+ pattern: {
617
+ type: "string",
618
+ description: "Glob pattern for files to search (e.g., src/**/*.ts)",
619
+ },
620
+ },
621
+ },
622
+ handler: async (args): Promise<ToolResult> => {
623
+ try {
624
+ const { glob } = await import("glob");
625
+ const symbol = args.symbol as string;
626
+ const pattern = args.pattern as string;
627
+
628
+ const files = await glob(pattern, {
629
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
630
+ });
631
+
632
+ const results: Array<{ file: string; line: number; context: string; type: string }> = [];
633
+
634
+ for (const file of files) {
635
+ const ext = path.extname(file).toLowerCase();
636
+ if (![".ts", ".tsx", ".js", ".jsx"].includes(ext)) continue;
637
+
638
+ try {
639
+ const content = fs.readFileSync(file, "utf-8");
640
+ const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
641
+
642
+ function visit(node: ts.Node) {
643
+ // Check identifiers
644
+ if (ts.isIdentifier(node) && node.text === symbol) {
645
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
646
+ const lineText = content.split("\n")[line].trim();
647
+
648
+ // Determine usage type
649
+ let type = "reference";
650
+ const parent = node.parent;
651
+ if (ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) {
652
+ type = "definition";
653
+ } else if (ts.isClassDeclaration(parent)) {
654
+ type = "definition";
655
+ } else if (ts.isVariableDeclaration(parent) && parent.name === node) {
656
+ type = "definition";
657
+ } else if (ts.isCallExpression(parent) && parent.expression === node) {
658
+ type = "call";
659
+ } else if (ts.isImportSpecifier(parent) || ts.isImportClause(parent)) {
660
+ type = "import";
661
+ }
662
+
663
+ results.push({
664
+ file: path.relative(process.cwd(), file),
665
+ line: line + 1,
666
+ context: lineText.slice(0, 80),
667
+ type,
668
+ });
669
+ }
670
+
671
+ ts.forEachChild(node, visit);
672
+ }
673
+
674
+ visit(sourceFile);
675
+ } catch {
676
+ // Skip files that can't be parsed
677
+ }
678
+ }
679
+
680
+ if (results.length === 0) {
681
+ return { success: true, content: `"${symbol}" 사용처를 찾을 수 없습니다.` };
682
+ }
683
+
684
+ const lines: string[] = [];
685
+ lines.push(`=== "${symbol}" 사용처 (${results.length}개) ===`);
686
+ lines.push("");
687
+
688
+ // Group by type
689
+ const grouped = {
690
+ definition: results.filter((r) => r.type === "definition"),
691
+ import: results.filter((r) => r.type === "import"),
692
+ call: results.filter((r) => r.type === "call"),
693
+ reference: results.filter((r) => r.type === "reference"),
694
+ };
695
+
696
+ if (grouped.definition.length > 0) {
697
+ lines.push("📍 정의:");
698
+ grouped.definition.forEach((r) => lines.push(` ${r.file}:${r.line} - ${r.context}`));
699
+ lines.push("");
700
+ }
701
+ if (grouped.import.length > 0) {
702
+ lines.push("📥 Import:");
703
+ grouped.import.forEach((r) => lines.push(` ${r.file}:${r.line}`));
704
+ lines.push("");
705
+ }
706
+ if (grouped.call.length > 0) {
707
+ lines.push("⚡ 호출:");
708
+ grouped.call.forEach((r) => lines.push(` ${r.file}:${r.line} - ${r.context}`));
709
+ lines.push("");
710
+ }
711
+ if (grouped.reference.length > 0) {
712
+ lines.push("🔗 참조:");
713
+ grouped.reference.slice(0, 20).forEach((r) => lines.push(` ${r.file}:${r.line} - ${r.context}`));
714
+ if (grouped.reference.length > 20) {
715
+ lines.push(` ... 외 ${grouped.reference.length - 20}개`);
716
+ }
717
+ }
718
+
719
+ return { success: true, content: lines.join("\n") };
720
+ } catch (error) {
721
+ return { success: false, content: "", error: String(error) };
722
+ }
723
+ },
724
+ };
725
+
726
+ // Get complexity report
727
+ export const complexityReportTool: Tool = {
728
+ name: "complexity_report",
729
+ description: "Calculate code complexity for files (복잡도 분석 리포트). Shows cyclomatic complexity for all functions. Use when user asks: 'complexity', 'code quality', '복잡도', '코드 품질'.",
730
+ parameters: {
731
+ type: "object",
732
+ required: ["pattern"],
733
+ properties: {
734
+ pattern: {
735
+ type: "string",
736
+ description: "Glob pattern (e.g., src/**/*.ts)",
737
+ },
738
+ threshold: {
739
+ type: "number",
740
+ description: "Only show functions with complexity >= threshold (default: 5)",
741
+ },
742
+ },
743
+ },
744
+ handler: async (args): Promise<ToolResult> => {
745
+ try {
746
+ const { glob } = await import("glob");
747
+ const pattern = args.pattern as string;
748
+ const threshold = (args.threshold as number) || 5;
749
+
750
+ const files = await glob(pattern, {
751
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
752
+ });
753
+
754
+ const allFuncs: Array<{
755
+ file: string;
756
+ name: string;
757
+ complexity: number;
758
+ line: number;
759
+ }> = [];
760
+
761
+ for (const file of files) {
762
+ const ext = path.extname(file).toLowerCase();
763
+ if (![".ts", ".tsx", ".js", ".jsx"].includes(ext)) continue;
764
+
765
+ try {
766
+ const content = fs.readFileSync(file, "utf-8");
767
+ const analysis = analyzeTypeScript(content, file);
768
+ const funcs = [...analysis.functions, ...analysis.classes.flatMap((c) => c.methods)];
769
+
770
+ for (const func of funcs) {
771
+ allFuncs.push({
772
+ file: path.relative(process.cwd(), file),
773
+ name: func.className ? `${func.className}.${func.name}` : func.name,
774
+ complexity: func.complexity,
775
+ line: func.line,
776
+ });
777
+ }
778
+ } catch {
779
+ // Skip unparseable files
780
+ }
781
+ }
782
+
783
+ // Sort by complexity descending
784
+ allFuncs.sort((a, b) => b.complexity - a.complexity);
785
+
786
+ const high = allFuncs.filter((f) => f.complexity >= threshold);
787
+ const total = allFuncs.reduce((sum, f) => sum + f.complexity, 0);
788
+ const avg = allFuncs.length > 0 ? Math.round(total / allFuncs.length * 10) / 10 : 0;
789
+
790
+ const lines: string[] = [];
791
+ lines.push(`=== 복잡도 리포트 ===`);
792
+ lines.push("");
793
+ lines.push(`📊 통계:`);
794
+ lines.push(` 총 함수: ${allFuncs.length}개`);
795
+ lines.push(` 평균 복잡도: ${avg}`);
796
+ lines.push(` 높은 복잡도 (>= ${threshold}): ${high.length}개`);
797
+ lines.push("");
798
+
799
+ if (high.length > 0) {
800
+ lines.push(`⚠️ 복잡도 높은 함수 (>= ${threshold}):`);
801
+ for (const func of high.slice(0, 20)) {
802
+ const bar = "█".repeat(Math.min(func.complexity, 20));
803
+ lines.push(` ${func.complexity.toString().padStart(2)} ${bar} ${func.name}`);
804
+ lines.push(` ${func.file}:${func.line}`);
805
+ }
806
+ if (high.length > 20) {
807
+ lines.push(` ... 외 ${high.length - 20}개`);
808
+ }
809
+ } else {
810
+ lines.push(`✅ 복잡도 ${threshold} 이상인 함수가 없습니다.`);
811
+ }
812
+
813
+ return { success: true, content: lines.join("\n") };
814
+ } catch (error) {
815
+ return { success: false, content: "", error: String(error) };
816
+ }
817
+ },
818
+ };
819
+
820
+ // Export all AST tools
821
+ export const astTools: Tool[] = [
822
+ astAnalyzeTool,
823
+ getCallGraphTool,
824
+ findReferencesTool,
825
+ complexityReportTool,
826
+ ];