activo 0.4.3 → 0.5.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 (166) hide show
  1. package/README.md +203 -1
  2. package/data/2026-03-04_20-54.json +181 -0
  3. package/data/2026-03-04_20-56.json +181 -0
  4. package/data/apex-rulesets/egov.yaml +469 -0
  5. package/data/apex-rulesets/modernize.yaml +687 -0
  6. package/data/apex-rulesets/quality.yaml +1677 -0
  7. package/data/apex-rulesets/rule-schema.yaml +587 -0
  8. package/data/apex-rulesets/secure.yaml +1688 -0
  9. package/data/apex-rulesets/spring.yaml +455 -0
  10. package/data/apex-rulesets/sql-format.yaml +99 -0
  11. package/data/apex-rulesets/sql-oracle.yaml +281 -0
  12. package/data/apex-rulesets/sql.yaml +1660 -0
  13. package/dist/cli/headless.d.ts.map +1 -1
  14. package/dist/cli/headless.js +32 -10
  15. package/dist/cli/headless.js.map +1 -1
  16. package/dist/cli/index.js +31 -3
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/core/agent.d.ts +3 -3
  19. package/dist/core/agent.d.ts.map +1 -1
  20. package/dist/core/agent.js +255 -17
  21. package/dist/core/agent.js.map +1 -1
  22. package/dist/core/commands.d.ts +2 -1
  23. package/dist/core/commands.d.ts.map +1 -1
  24. package/dist/core/commands.js +61 -9
  25. package/dist/core/commands.js.map +1 -1
  26. package/dist/core/config.d.ts +14 -0
  27. package/dist/core/config.d.ts.map +1 -1
  28. package/dist/core/config.js +41 -4
  29. package/dist/core/config.js.map +1 -1
  30. package/dist/core/conversation.d.ts +2 -2
  31. package/dist/core/conversation.d.ts.map +1 -1
  32. package/dist/core/conversation.js.map +1 -1
  33. package/dist/core/intentRouter.d.ts +43 -0
  34. package/dist/core/intentRouter.d.ts.map +1 -0
  35. package/dist/core/intentRouter.js +804 -0
  36. package/dist/core/intentRouter.js.map +1 -0
  37. package/dist/core/llm/anthropic.d.ts +24 -0
  38. package/dist/core/llm/anthropic.d.ts.map +1 -0
  39. package/dist/core/llm/anthropic.js +226 -0
  40. package/dist/core/llm/anthropic.js.map +1 -0
  41. package/dist/core/llm/ollama.d.ts +5 -14
  42. package/dist/core/llm/ollama.d.ts.map +1 -1
  43. package/dist/core/llm/ollama.js +3 -0
  44. package/dist/core/llm/ollama.js.map +1 -1
  45. package/dist/core/llm/types.d.ts +22 -0
  46. package/dist/core/llm/types.d.ts.map +1 -0
  47. package/dist/core/llm/types.js +2 -0
  48. package/dist/core/llm/types.js.map +1 -0
  49. package/dist/core/mcp/client.d.ts +6 -0
  50. package/dist/core/mcp/client.d.ts.map +1 -1
  51. package/dist/core/mcp/client.js +16 -0
  52. package/dist/core/mcp/client.js.map +1 -1
  53. package/dist/core/mcp/init.d.ts +12 -0
  54. package/dist/core/mcp/init.d.ts.map +1 -0
  55. package/dist/core/mcp/init.js +55 -0
  56. package/dist/core/mcp/init.js.map +1 -0
  57. package/dist/core/mcp/logger.d.ts +14 -0
  58. package/dist/core/mcp/logger.d.ts.map +1 -0
  59. package/dist/core/mcp/logger.js +50 -0
  60. package/dist/core/mcp/logger.js.map +1 -0
  61. package/dist/core/tools/analyzeAll.d.ts.map +1 -1
  62. package/dist/core/tools/analyzeAll.js +16 -28
  63. package/dist/core/tools/analyzeAll.js.map +1 -1
  64. package/dist/core/tools/analyzePatterns.d.ts +3 -0
  65. package/dist/core/tools/analyzePatterns.d.ts.map +1 -0
  66. package/dist/core/tools/analyzePatterns.js +293 -0
  67. package/dist/core/tools/analyzePatterns.js.map +1 -0
  68. package/dist/core/tools/apexPaths.d.ts +14 -0
  69. package/dist/core/tools/apexPaths.d.ts.map +1 -0
  70. package/dist/core/tools/apexPaths.js +54 -0
  71. package/dist/core/tools/apexPaths.js.map +1 -0
  72. package/dist/core/tools/apexUtils.d.ts +36 -0
  73. package/dist/core/tools/apexUtils.d.ts.map +1 -0
  74. package/dist/core/tools/apexUtils.js +83 -0
  75. package/dist/core/tools/apexUtils.js.map +1 -0
  76. package/dist/core/tools/explainIssue.d.ts +3 -0
  77. package/dist/core/tools/explainIssue.d.ts.map +1 -0
  78. package/dist/core/tools/explainIssue.js +181 -0
  79. package/dist/core/tools/explainIssue.js.map +1 -0
  80. package/dist/core/tools/fixGen.d.ts +3 -0
  81. package/dist/core/tools/fixGen.d.ts.map +1 -0
  82. package/dist/core/tools/fixGen.js +338 -0
  83. package/dist/core/tools/fixGen.js.map +1 -0
  84. package/dist/core/tools/generateImprovements.d.ts +21 -0
  85. package/dist/core/tools/generateImprovements.d.ts.map +1 -0
  86. package/dist/core/tools/generateImprovements.js +602 -0
  87. package/dist/core/tools/generateImprovements.js.map +1 -0
  88. package/dist/core/tools/generateReport.d.ts +3 -0
  89. package/dist/core/tools/generateReport.d.ts.map +1 -0
  90. package/dist/core/tools/generateReport.js +315 -0
  91. package/dist/core/tools/generateReport.js.map +1 -0
  92. package/dist/core/tools/index.d.ts +7 -0
  93. package/dist/core/tools/index.d.ts.map +1 -1
  94. package/dist/core/tools/index.js +62 -23
  95. package/dist/core/tools/index.js.map +1 -1
  96. package/dist/core/tools/javaAst.d.ts.map +1 -1
  97. package/dist/core/tools/javaAst.js +191 -0
  98. package/dist/core/tools/javaAst.js.map +1 -1
  99. package/dist/core/tools/recommendProfile.d.ts +3 -0
  100. package/dist/core/tools/recommendProfile.d.ts.map +1 -0
  101. package/dist/core/tools/recommendProfile.js +334 -0
  102. package/dist/core/tools/recommendProfile.js.map +1 -0
  103. package/dist/core/tools/ruleGen.d.ts +3 -0
  104. package/dist/core/tools/ruleGen.d.ts.map +1 -0
  105. package/dist/core/tools/ruleGen.js +1103 -0
  106. package/dist/core/tools/ruleGen.js.map +1 -0
  107. package/dist/core/tools/standards.d.ts.map +1 -1
  108. package/dist/core/tools/standards.js +7 -3
  109. package/dist/core/tools/standards.js.map +1 -1
  110. package/dist/ui/App.d.ts.map +1 -1
  111. package/dist/ui/App.js +86 -35
  112. package/dist/ui/App.js.map +1 -1
  113. package/dist/ui/components/InputBox.d.ts +1 -3
  114. package/dist/ui/components/InputBox.d.ts.map +1 -1
  115. package/dist/ui/components/InputBox.js +146 -5
  116. package/dist/ui/components/InputBox.js.map +1 -1
  117. package/dist/ui/components/MessageList.d.ts +3 -1
  118. package/dist/ui/components/MessageList.d.ts.map +1 -1
  119. package/dist/ui/components/MessageList.js +13 -7
  120. package/dist/ui/components/MessageList.js.map +1 -1
  121. package/dist/ui/components/StatusBar.d.ts +1 -1
  122. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  123. package/dist/ui/components/StatusBar.js +3 -2
  124. package/dist/ui/components/StatusBar.js.map +1 -1
  125. package/dist/ui/components/ToolStatus.d.ts +3 -1
  126. package/dist/ui/components/ToolStatus.d.ts.map +1 -1
  127. package/dist/ui/components/ToolStatus.js +19 -4
  128. package/dist/ui/components/ToolStatus.js.map +1 -1
  129. package/package.json +7 -1
  130. package/demo.gif +0 -0
  131. package/demo.tape +0 -53
  132. package/screenshot.png +0 -0
  133. package/src/cli/banner.ts +0 -38
  134. package/src/cli/headless.ts +0 -63
  135. package/src/cli/index.ts +0 -57
  136. package/src/core/agent.ts +0 -237
  137. package/src/core/commands.ts +0 -118
  138. package/src/core/config.ts +0 -98
  139. package/src/core/conversation.ts +0 -235
  140. package/src/core/llm/ollama.ts +0 -351
  141. package/src/core/mcp/client.ts +0 -143
  142. package/src/core/tools/analyzeAll.ts +0 -494
  143. package/src/core/tools/ast.ts +0 -826
  144. package/src/core/tools/builtIn.ts +0 -221
  145. package/src/core/tools/cache.ts +0 -570
  146. package/src/core/tools/cssAnalysis.ts +0 -324
  147. package/src/core/tools/dependencyAnalysis.ts +0 -363
  148. package/src/core/tools/embeddings.ts +0 -746
  149. package/src/core/tools/frontendAst.ts +0 -802
  150. package/src/core/tools/htmlAnalysis.ts +0 -466
  151. package/src/core/tools/index.ts +0 -160
  152. package/src/core/tools/javaAst.ts +0 -812
  153. package/src/core/tools/memory.ts +0 -655
  154. package/src/core/tools/mybatisAnalysis.ts +0 -322
  155. package/src/core/tools/openapiAnalysis.ts +0 -431
  156. package/src/core/tools/pythonAnalysis.ts +0 -477
  157. package/src/core/tools/sqlAnalysis.ts +0 -298
  158. package/src/core/tools/standards.test.ts +0 -186
  159. package/src/core/tools/standards.ts +0 -889
  160. package/src/core/tools/types.ts +0 -38
  161. package/src/ui/App.tsx +0 -334
  162. package/src/ui/components/InputBox.tsx +0 -37
  163. package/src/ui/components/MessageList.tsx +0 -80
  164. package/src/ui/components/StatusBar.tsx +0 -36
  165. package/src/ui/components/ToolStatus.tsx +0 -38
  166. package/tsconfig.json +0 -21
@@ -0,0 +1,804 @@
1
+ /**
2
+ * Intent Router — 사용자 메시지에서 의도를 감지하고 적절한 도구를 자동 실행
3
+ *
4
+ * agent.ts에서 분리된 모듈 (v0.4.5)
5
+ */
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import { glob } from "glob";
9
+ import { getTool, executeTool } from "./tools/index.js";
10
+ import { resolveRulesetsDir, resolveSchemaPath } from "./tools/apexPaths.js";
11
+ // ─── Intent Patterns ───
12
+ // Intent patterns: keyword groups → tool + args builder
13
+ export const INTENT_PATTERNS = [
14
+ // Single file analysis (must come before directory patterns)
15
+ {
16
+ keywords: ["분석", "analyze", "검사", "check"],
17
+ tool: "_single_file", // special marker - resolved at match time
18
+ buildArgs: (path) => ({ filepath: path }),
19
+ },
20
+ // Explain issue (must come before APEX analysis)
21
+ {
22
+ keywords: ["이슈설명", "규칙설명", "왜 문제", "explain issue", "rule explain"],
23
+ tool: "explain_issue",
24
+ buildArgs: (_path, message) => {
25
+ // Extract rule_id from message (e.g., "이슈설명 quality-nc-001")
26
+ const ruleMatch = message.match(/([a-zA-Z]+-[a-zA-Z]+-\d+)/);
27
+ return { rule_id: ruleMatch ? ruleMatch[1] : message.replace(/이슈설명|규칙설명|왜\s*문제|explain\s*issue|rule\s*explain/gi, "").trim() };
28
+ },
29
+ },
30
+ // Recommend profile (must come before APEX analysis)
31
+ {
32
+ keywords: ["프로파일 추천", "어떤 프로파일", "recommend profile", "어떤 규칙"],
33
+ tool: "recommend_profile",
34
+ buildArgs: (path) => ({ path }),
35
+ },
36
+ // Analyze patterns (must come before APEX analysis)
37
+ {
38
+ keywords: ["패턴분석", "이슈패턴", "hotspot", "핫스팟", "pattern analysis"],
39
+ tool: "analyze_patterns",
40
+ buildArgs: (path) => ({ report: path }),
41
+ },
42
+ // Generate improvement report (must come before generate_report)
43
+ {
44
+ keywords: ["개선", "코드개선", "개선보고서", "개선 보고서", "improvement", "fix report", "감리", "before after"],
45
+ tool: "generate_improvement_report",
46
+ buildArgs: (path, message) => {
47
+ // Check for Excel paste: tab-separated text with rule_id pattern
48
+ if (message.includes("\t") && /[a-zA-Z]+-[a-zA-Z]+-\w+/.test(message)) {
49
+ // Extract paste text: everything after the first keyword match
50
+ const keywords = ["개선", "코드개선", "개선보고서", "improvement", "fix report", "감리", "before after", "이거"];
51
+ let pasteText = message;
52
+ for (const kw of keywords) {
53
+ const idx = message.toLowerCase().indexOf(kw);
54
+ if (idx >= 0) {
55
+ // Find the first line with a tab after the keyword
56
+ const afterKeyword = message.slice(idx + kw.length).trim();
57
+ const firstTabLine = afterKeyword.split("\n").findIndex((l) => l.includes("\t"));
58
+ if (firstTabLine >= 0) {
59
+ pasteText = afterKeyword.split("\n").slice(firstTabLine).join("\n");
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ return { report: pasteText };
65
+ }
66
+ const allPaths = extractPaths(message);
67
+ let reportPath = "";
68
+ let outputDir = ".";
69
+ for (const p of allPaths) {
70
+ try {
71
+ const stat = fs.statSync(p);
72
+ if (stat.isFile()) {
73
+ if (!reportPath)
74
+ reportPath = p;
75
+ }
76
+ else if (stat.isDirectory()) {
77
+ outputDir = p;
78
+ }
79
+ }
80
+ catch { /* skip */ }
81
+ }
82
+ if (!reportPath)
83
+ reportPath = path;
84
+ return { report: reportPath, output_dir: outputDir };
85
+ },
86
+ },
87
+ // Excel export (must come before generate_report)
88
+ {
89
+ keywords: ["엑셀", "excel", "xlsx", "엑셀출력", "엑셀보고서"],
90
+ tool: "mcp_apex_export_excel",
91
+ buildArgs: (_path, message) => {
92
+ // Extract output file path from message if present
93
+ const allPaths = extractPaths(message);
94
+ const outputFile = allPaths.find((p) => p.endsWith(".xlsx") || p.endsWith(".xls")) || "apex-report.xlsx";
95
+ return { output_file: outputFile };
96
+ },
97
+ },
98
+ // Generate report (must come before APEX analysis)
99
+ {
100
+ keywords: ["리포트", "report", "보고서", "품질보고서"],
101
+ tool: "generate_report",
102
+ buildArgs: (path, message) => {
103
+ const allPaths = extractPaths(message);
104
+ let reportPath = "";
105
+ let outputDir = "."; // 기본: activo 실행 디렉토리 (CWD)
106
+ // 기존 경로 분류: 파일 → report, 디렉토리 → output_dir
107
+ for (const p of allPaths) {
108
+ try {
109
+ const stat = fs.statSync(p);
110
+ if (stat.isFile()) {
111
+ if (!reportPath)
112
+ reportPath = p;
113
+ }
114
+ else if (stat.isDirectory()) {
115
+ outputDir = p;
116
+ }
117
+ }
118
+ catch { /* skip */ }
119
+ }
120
+ if (!reportPath)
121
+ reportPath = path;
122
+ // 존재하지 않는 디렉토리 경로 추출 (새 출력 디렉토리)
123
+ if (outputDir === ".") {
124
+ const pathLike = message.match(/(?:^|\s)(\/[^\s,;:'"]+)/g);
125
+ if (pathLike) {
126
+ for (const raw of pathLike) {
127
+ const p = raw.trim();
128
+ if (p === reportPath)
129
+ continue;
130
+ // 부모 디렉토리가 존재하면 새 출력 경로로 사용
131
+ const parentDir = p.substring(0, p.lastIndexOf("/")) || "/";
132
+ try {
133
+ if (fs.existsSync(parentDir) && fs.statSync(parentDir).isDirectory()) {
134
+ outputDir = p;
135
+ break;
136
+ }
137
+ }
138
+ catch { /* skip */ }
139
+ }
140
+ }
141
+ }
142
+ return { report: reportPath, output_dir: outputDir };
143
+ },
144
+ },
145
+ // Rule generation (must come before APEX analysis)
146
+ {
147
+ keywords: ["규칙생성", "규칙 생성", "generate rules", "커스텀 규칙", "custom rule", "규칙 YAML", "rule yaml"],
148
+ tool: "generate_apex_rules",
149
+ buildArgs: (path) => ({
150
+ standards_dir: path || ".activo/standards",
151
+ schema_path: resolveSchemaPath(),
152
+ existing_rulesets_dir: resolveRulesetsDir(),
153
+ output_dir: ".activo/generated-rules",
154
+ }),
155
+ },
156
+ // APEX static analysis — 400+ rules, ANTLR4 AST (must come before generic patterns)
157
+ {
158
+ keywords: ["개발표준", "품질검사", "apex", "시큐어코딩", "secure coding", "전자정부", "egov", "정적분석", "표준검사", "코드표준"],
159
+ tool: "mcp_apex_analyze_code",
160
+ buildArgs: (path, message) => {
161
+ const msg = message.toLowerCase();
162
+ let profile = "all";
163
+ if (/시큐어|secure|보안/.test(msg))
164
+ profile = "secure";
165
+ else if (/sql/.test(msg))
166
+ profile = "sql-all";
167
+ else if (/전자정부|egov/.test(msg))
168
+ profile = "egov-full";
169
+ else if (/spring|스프링/.test(msg))
170
+ profile = "spring";
171
+ else if (/품질|quality/.test(msg))
172
+ profile = "quality";
173
+ else if (/모더나이즈|modernize|마이그레이션|migration/.test(msg))
174
+ profile = "migration";
175
+ return { path, profile, max_issues: 30 };
176
+ },
177
+ },
178
+ // analyze_all with Java filter
179
+ {
180
+ keywords: ["자바", "java"],
181
+ tool: "analyze_all",
182
+ buildArgs: (path) => ({ path, include: ["java"] }),
183
+ },
184
+ // Spring patterns
185
+ {
186
+ keywords: ["spring", "스프링"],
187
+ tool: "analyze_all",
188
+ buildArgs: (path) => ({ path, include: ["java"] }),
189
+ },
190
+ // Dependency analysis
191
+ {
192
+ keywords: ["의존성", "dependency", "dependencies", "취약점"],
193
+ tool: "dependency_check",
194
+ buildArgs: (path) => ({ path }),
195
+ },
196
+ // Complexity
197
+ {
198
+ keywords: ["복잡도", "complexity"],
199
+ tool: "analyze_all",
200
+ buildArgs: (path) => ({ path }),
201
+ },
202
+ // Python
203
+ {
204
+ keywords: ["python", "파이썬", ".py"],
205
+ tool: "analyze_all",
206
+ buildArgs: (path) => ({ path, include: ["py"] }),
207
+ },
208
+ // Frontend
209
+ {
210
+ keywords: ["react", "리액트", "vue", "뷰", "프론트엔드", "frontend"],
211
+ tool: "analyze_all",
212
+ buildArgs: (path) => ({ path, include: ["js", "ts", "jsx", "tsx", "vue"] }),
213
+ },
214
+ // CSS
215
+ {
216
+ keywords: ["css", "scss", "less", "스타일"],
217
+ tool: "analyze_all",
218
+ buildArgs: (path) => ({ path, include: ["css"] }),
219
+ },
220
+ // HTML
221
+ {
222
+ keywords: ["html", "jsp", "접근성", "a11y", "seo"],
223
+ tool: "analyze_all",
224
+ buildArgs: (path) => ({ path, include: ["html"] }),
225
+ },
226
+ // SQL / MyBatis
227
+ {
228
+ keywords: ["sql", "mybatis", "마이바티스", "쿼리"],
229
+ tool: "analyze_all",
230
+ buildArgs: (path) => ({ path, include: ["java", "xml"] }),
231
+ },
232
+ // Broad analysis (catch-all, must be last)
233
+ {
234
+ keywords: ["전체분석", "전체 분석", "분석해", "코드품질", "코드 품질", "analyze", "분석", "검사", "check"],
235
+ tool: "analyze_all",
236
+ buildArgs: (path) => ({ path }),
237
+ },
238
+ ];
239
+ // ─── File Extension → Tool Mapping ───
240
+ // File extension → single-file tool mapping
241
+ export const FILE_TOOL_MAP = {
242
+ ".java": "java_analyze",
243
+ ".js": "ast_analyze",
244
+ ".ts": "ast_analyze",
245
+ ".jsx": "react_check",
246
+ ".tsx": "react_check",
247
+ ".vue": "vue_check",
248
+ ".py": "python_check",
249
+ ".css": "css_check",
250
+ ".scss": "css_check",
251
+ ".less": "css_check",
252
+ ".html": "html_check",
253
+ ".htm": "html_check",
254
+ ".jsp": "html_check",
255
+ };
256
+ // ─── Path Extraction ───
257
+ /**
258
+ * Expand ~ to home directory in a path string.
259
+ */
260
+ function expandHome(p) {
261
+ if (p.startsWith("~/") || p === "~") {
262
+ const home = process.env.HOME || process.env.USERPROFILE || "";
263
+ return home + p.slice(1);
264
+ }
265
+ return p;
266
+ }
267
+ /**
268
+ * Extract filesystem paths from user message.
269
+ * Handles quoted paths (with spaces), simple paths, tilde (~) paths, and greedy path expansion.
270
+ */
271
+ export function extractPaths(message) {
272
+ const paths = [];
273
+ // 1. Quoted paths: '...' or "..."
274
+ const quotedMatches = message.match(/['"]([~/\\][^'"]+)['"]/g);
275
+ if (quotedMatches) {
276
+ for (const m of quotedMatches) {
277
+ paths.push(expandHome(m.slice(1, -1))); // strip quotes + expand ~
278
+ }
279
+ }
280
+ // 2. Tilde paths: ~/... (before simple path matching)
281
+ const tildeMatches = message.match(/(?:^|\s)(~\/[^\s,;:'"]+)/g);
282
+ if (tildeMatches) {
283
+ for (const m of tildeMatches) {
284
+ paths.push(expandHome(m.trim()));
285
+ }
286
+ }
287
+ // 3. Simple paths (no spaces) - Unix & Windows
288
+ const unixMatches = message.match(/(?:^|\s)(\/[^\s,;:'"]+)/g);
289
+ if (unixMatches) {
290
+ for (const m of unixMatches) {
291
+ paths.push(m.trim());
292
+ }
293
+ }
294
+ const winMatches = message.match(/(?:^|\s)([A-Z]:\\[^\s,;:'"]+)/gi);
295
+ if (winMatches) {
296
+ for (const m of winMatches) {
297
+ paths.push(m.trim());
298
+ }
299
+ }
300
+ // 4. Greedy path expansion: if simple match doesn't exist,
301
+ // try extending with subsequent words until path is valid
302
+ if (paths.length === 0 || !paths.some((p) => { try {
303
+ return fs.existsSync(p);
304
+ }
305
+ catch {
306
+ return false;
307
+ } })) {
308
+ const words = message.split(/\s+/);
309
+ for (let i = 0; i < words.length; i++) {
310
+ const word = expandHome(words[i]);
311
+ if (word.startsWith("/") || word.startsWith("~/") || /^[A-Z]:\\/i.test(word)) {
312
+ let candidate = word;
313
+ let bestPath = "";
314
+ try {
315
+ if (fs.existsSync(candidate))
316
+ bestPath = candidate;
317
+ }
318
+ catch { /* */ }
319
+ for (let j = i + 1; j < words.length; j++) {
320
+ const extended = candidate + " " + words[j];
321
+ try {
322
+ if (fs.existsSync(extended)) {
323
+ bestPath = extended;
324
+ candidate = extended;
325
+ }
326
+ else {
327
+ break;
328
+ }
329
+ }
330
+ catch {
331
+ break;
332
+ }
333
+ }
334
+ if (bestPath) {
335
+ paths.push(bestPath);
336
+ }
337
+ }
338
+ }
339
+ }
340
+ // Filter to actually existing paths, deduplicate
341
+ const seen = new Set();
342
+ return paths.filter((p) => {
343
+ if (seen.has(p))
344
+ return false;
345
+ seen.add(p);
346
+ try {
347
+ return fs.existsSync(p);
348
+ }
349
+ catch {
350
+ return false;
351
+ }
352
+ });
353
+ }
354
+ // ─── Helper Functions ───
355
+ /**
356
+ * Determine if a path is a single file (not a directory).
357
+ */
358
+ export function isSingleFile(p) {
359
+ try {
360
+ return fs.statSync(p).isFile();
361
+ }
362
+ catch {
363
+ return false;
364
+ }
365
+ }
366
+ /**
367
+ * Resolve the correct tool for a single file based on extension.
368
+ */
369
+ export function resolveFileAnalysisTool(filepath) {
370
+ const ext = filepath.substring(filepath.lastIndexOf(".")).toLowerCase();
371
+ const toolName = FILE_TOOL_MAP[ext];
372
+ if (!toolName)
373
+ return null;
374
+ // Some tools use 'filepath', others use 'path'
375
+ const argKey = ["python_check", "css_check", "html_check"].includes(toolName) ? "path" : "filepath";
376
+ return { tool: toolName, args: { [argKey]: filepath } };
377
+ }
378
+ // ─── Intent Detection & Execution ───
379
+ /**
380
+ * Detect user intent from the message and automatically execute the appropriate tool.
381
+ * Returns IntentResult with handled=true if a tool was executed, false otherwise.
382
+ */
383
+ export async function detectAndExecuteIntent(userMessage, onEvent) {
384
+ const msg = userMessage.toLowerCase();
385
+ const paths = extractPaths(userMessage);
386
+ // Pipeline intent: PDF → 규칙 생성 (no explicit path needed)
387
+ if (isPdfToRulesIntent(msg)) {
388
+ return await executePdfToRulesPipeline(userMessage, onEvent);
389
+ }
390
+ // Pipeline intent: 전체 보고서 (분석 + Excel + Markdown + 개선보고서)
391
+ if (isFullReportIntent(msg)) {
392
+ return await executeFullReportPipeline(userMessage, onEvent);
393
+ }
394
+ // Excel paste detection: tab-separated text with rule_id pattern (no file path needed)
395
+ if (paths.length === 0 && userMessage.includes("\t") && /[a-zA-Z]+-[a-zA-Z]+-\w+/.test(userMessage)) {
396
+ const improvementKeywords = ["개선", "코드개선", "개선보고서", "improvement", "fix report", "감리", "before after"];
397
+ if (improvementKeywords.some((kw) => msg.includes(kw)) && getTool("generate_improvement_report")) {
398
+ const pattern = INTENT_PATTERNS.find((p) => p.tool === "generate_improvement_report");
399
+ if (pattern) {
400
+ const args = pattern.buildArgs("", userMessage);
401
+ return await executeIntentTool("generate_improvement_report", args, onEvent);
402
+ }
403
+ }
404
+ }
405
+ // No path found → can't auto-route
406
+ if (paths.length === 0) {
407
+ return { handled: false };
408
+ }
409
+ const targetPath = paths[0];
410
+ // Check if path is a single file
411
+ if (isSingleFile(targetPath)) {
412
+ const fileInfo = resolveFileAnalysisTool(targetPath);
413
+ if (fileInfo) {
414
+ return await executeIntentTool(fileInfo.tool, fileInfo.args, onEvent);
415
+ }
416
+ // Unknown file type → fall back to LLM
417
+ return { handled: false };
418
+ }
419
+ // Path is a directory → match intent patterns
420
+ for (const pattern of INTENT_PATTERNS) {
421
+ // Skip the single-file marker for directories
422
+ if (pattern.tool === "_single_file")
423
+ continue;
424
+ if (pattern.keywords.some((kw) => msg.includes(kw))) {
425
+ // Skip if tool not available (e.g., MCP server not connected)
426
+ if (!getTool(pattern.tool))
427
+ continue;
428
+ const args = pattern.buildArgs(targetPath, userMessage);
429
+ return await executeIntentTool(pattern.tool, args, onEvent);
430
+ }
431
+ }
432
+ // Has a directory path but no matching keyword → default to analyze_all
433
+ // (user likely wants some kind of analysis if they provided a path)
434
+ const hasAnalysisHint = /분석|검사|확인|체크|check|analyze|review|scan|report/i.test(msg);
435
+ if (hasAnalysisHint) {
436
+ return await executeIntentTool("analyze_all", { path: targetPath }, onEvent);
437
+ }
438
+ return { handled: false };
439
+ }
440
+ /**
441
+ * Execute a tool by name and return an IntentResult with the summary prompt.
442
+ */
443
+ async function executeIntentTool(toolName, toolArgs, onEvent) {
444
+ const toolCall = {
445
+ id: `intent_${Date.now()}_${Math.random().toString(36).slice(2)}`,
446
+ name: toolName,
447
+ arguments: toolArgs,
448
+ };
449
+ // Emit tool_use start event
450
+ onEvent?.({
451
+ type: "tool_use",
452
+ tool: toolName,
453
+ status: "start",
454
+ args: toolArgs,
455
+ });
456
+ const result = await executeTool(toolCall);
457
+ // Emit tool_result event
458
+ onEvent?.({
459
+ type: "tool_result",
460
+ tool: toolName,
461
+ status: result.success ? "complete" : "error",
462
+ result,
463
+ });
464
+ if (!result.success) {
465
+ return {
466
+ handled: true,
467
+ toolName,
468
+ toolArgs,
469
+ toolResult: result,
470
+ summaryPrompt: `도구 "${toolName}" 실행 중 오류가 발생했습니다: ${result.error}\n사용자에게 오류 내용을 설명해주세요.`,
471
+ };
472
+ }
473
+ // Compress result to fit in context window
474
+ const compressed = compressAnalysisResult(result.content);
475
+ return {
476
+ handled: true,
477
+ toolName,
478
+ toolArgs,
479
+ toolResult: result,
480
+ summaryPrompt: `아래는 "${toolName}" 도구의 실행 결과입니다. 사용자에게 한국어로 핵심 내용을 요약해주세요.\n\n${compressed}`,
481
+ };
482
+ }
483
+ // ─── Result Compression ───
484
+ /**
485
+ * Compress analysis result JSON to fit within LLM context window.
486
+ * Extracts only key metrics, removing verbose raw data.
487
+ */
488
+ export function compressAnalysisResult(resultContent, maxChars = 2000) {
489
+ try {
490
+ const parsed = JSON.parse(resultContent);
491
+ // apex MCP result (has summary + profiles_used)
492
+ if (parsed.summary && parsed.profiles_used !== undefined) {
493
+ const compact = {
494
+ summary: parsed.summary,
495
+ profiles_used: parsed.profiles_used,
496
+ duration: parsed.duration,
497
+ total_issues: parsed.total_issues,
498
+ };
499
+ if (parsed.top_issues) {
500
+ compact.top_issues = parsed.top_issues.slice(0, 15);
501
+ }
502
+ const result = JSON.stringify(compact, null, 1);
503
+ return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
504
+ }
505
+ // analyze_all result
506
+ if (parsed.path && parsed.fileStats) {
507
+ const compact = {
508
+ path: parsed.path,
509
+ totalFiles: parsed.totalFiles,
510
+ fileStats: parsed.fileStats,
511
+ analysesRun: parsed.analysesRun,
512
+ successful: parsed.successful,
513
+ failed: parsed.failed,
514
+ };
515
+ // Extract issue summaries (compact)
516
+ if (parsed.issuesSummary?.length > 0) {
517
+ compact.issues = parsed.issuesSummary.map((is) => ({
518
+ tool: is.tool,
519
+ issues: is.issues.slice(0, 5),
520
+ }));
521
+ }
522
+ // Extract per-tool summaries (key metrics only)
523
+ if (parsed.details?.length > 0) {
524
+ compact.analyses = parsed.details.map((d) => {
525
+ const s = d.summary;
526
+ const brief = { tool: d.tool };
527
+ // Extract numeric/small fields only
528
+ for (const [k, v] of Object.entries(s)) {
529
+ if (typeof v === "number" || typeof v === "boolean") {
530
+ brief[k] = v;
531
+ }
532
+ else if (typeof v === "string" && v.length < 100) {
533
+ brief[k] = v;
534
+ }
535
+ // Skip arrays/objects (raw data) to save space
536
+ }
537
+ // Include issues from samples (java_analyze etc.)
538
+ if (Array.isArray(s.samples)) {
539
+ const allIssues = [];
540
+ for (const sample of s.samples) {
541
+ if (Array.isArray(sample.result?.issues)) {
542
+ allIssues.push(...sample.result.issues.slice(0, 3));
543
+ }
544
+ }
545
+ if (allIssues.length > 0) {
546
+ brief.issues = allIssues.slice(0, 10);
547
+ }
548
+ }
549
+ return brief;
550
+ });
551
+ }
552
+ if (parsed.errors?.length > 0) {
553
+ compact.errors = parsed.errors;
554
+ }
555
+ const result = JSON.stringify(compact, null, 1);
556
+ return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
557
+ }
558
+ // java_analyze or other single-file results
559
+ if (parsed.file || parsed.filepath || parsed.classes || parsed.functions) {
560
+ const result = JSON.stringify(parsed, null, 1);
561
+ return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
562
+ }
563
+ // Generic: just truncate
564
+ const result = JSON.stringify(parsed, null, 1);
565
+ return result.length > maxChars ? result.slice(0, maxChars) + "..." : result;
566
+ }
567
+ catch {
568
+ // Not valid JSON, return truncated raw text
569
+ return resultContent.length > maxChars ? resultContent.slice(0, maxChars) + "..." : resultContent;
570
+ }
571
+ }
572
+ // ─── Pipeline Intents ───
573
+ /**
574
+ * Detect if the message is a "PDF → 규칙 생성" workflow.
575
+ */
576
+ function isPdfToRulesIntent(msg) {
577
+ const hasPdf = /pdf/.test(msg);
578
+ const hasRuleKeyword = /규칙|룰|rule|만들어/.test(msg);
579
+ const hasStandardKeyword = /개발\s*표준|표준|standard/.test(msg);
580
+ return hasPdf && (hasRuleKeyword || hasStandardKeyword);
581
+ }
582
+ /**
583
+ * Execute the PDF → Rules pipeline:
584
+ * 1. Find PDF files in CWD
585
+ * 2. import_pdf_standards for each
586
+ * 3. generate_apex_rules from the converted standards
587
+ */
588
+ async function executePdfToRulesPipeline(userMessage, onEvent) {
589
+ const cwd = process.cwd();
590
+ const allResults = [];
591
+ // Step 1: Find PDF files
592
+ // Try to extract a glob pattern from the message (e.g., "FSS*.pdf")
593
+ const pdfPatternMatch = userMessage.match(/([A-Za-z가-힣0-9*?_-]+\.pdf)/i);
594
+ const pdfGlob = pdfPatternMatch ? pdfPatternMatch[1] : "*.pdf";
595
+ let pdfFiles = await glob(pdfGlob, { cwd, absolute: true });
596
+ // Fallback: if exact pattern doesn't match, try *.pdf
597
+ if (pdfFiles.length === 0 && pdfGlob !== "*.pdf") {
598
+ pdfFiles = await glob("*.pdf", { cwd, absolute: true });
599
+ }
600
+ if (pdfFiles.length === 0) {
601
+ return {
602
+ handled: true,
603
+ toolName: "import_pdf_standards",
604
+ toolArgs: { pattern: pdfGlob },
605
+ toolResult: { success: false, content: "", error: `PDF 파일을 찾을 수 없습니다: ${pdfGlob} (경로: ${cwd})` },
606
+ summaryPrompt: `현재 디렉토리(${cwd})에서 PDF 파일을 찾을 수 없습니다. 도구를 호출하지 말고, 사용자에게 PDF 파일 위치를 확인하라고 안내해주세요.`,
607
+ };
608
+ }
609
+ // Step 2: Import each PDF → MD
610
+ const standardsDir = path.resolve(cwd, ".activo/standards");
611
+ for (const pdfPath of pdfFiles) {
612
+ const importArgs = { pdfPath, outputDir: standardsDir };
613
+ onEvent?.({ type: "tool_use", tool: "import_pdf_standards", status: "start", args: importArgs });
614
+ const importCall = {
615
+ id: `pipeline_import_${Date.now()}_${Math.random().toString(36).slice(2)}`,
616
+ name: "import_pdf_standards",
617
+ arguments: importArgs,
618
+ };
619
+ const importResult = await executeTool(importCall);
620
+ onEvent?.({
621
+ type: "tool_result",
622
+ tool: "import_pdf_standards",
623
+ status: importResult.success ? "complete" : "error",
624
+ result: importResult,
625
+ });
626
+ if (importResult.success) {
627
+ allResults.push(`PDF 변환 완료: ${path.basename(pdfPath)}`);
628
+ }
629
+ else {
630
+ allResults.push(`PDF 변환 실패: ${path.basename(pdfPath)} — ${importResult.error}`);
631
+ }
632
+ }
633
+ // Step 3: Generate rules from converted standards
634
+ const ruleArgs = {
635
+ standards_dir: standardsDir,
636
+ schema_path: resolveSchemaPath(),
637
+ existing_rulesets_dir: resolveRulesetsDir(),
638
+ output_dir: path.resolve(cwd, ".activo/generated-rules"),
639
+ };
640
+ onEvent?.({ type: "tool_use", tool: "generate_apex_rules", status: "start", args: ruleArgs });
641
+ const ruleCall = {
642
+ id: `pipeline_rules_${Date.now()}_${Math.random().toString(36).slice(2)}`,
643
+ name: "generate_apex_rules",
644
+ arguments: ruleArgs,
645
+ };
646
+ const ruleResult = await executeTool(ruleCall);
647
+ onEvent?.({
648
+ type: "tool_result",
649
+ tool: "generate_apex_rules",
650
+ status: ruleResult.success ? "complete" : "error",
651
+ result: ruleResult,
652
+ });
653
+ if (ruleResult.success) {
654
+ allResults.push(`규칙 생성 완료`);
655
+ }
656
+ else {
657
+ allResults.push(`규칙 생성 실패: ${ruleResult.error}`);
658
+ }
659
+ // Build summary for LLM
660
+ const compressed = ruleResult.success
661
+ ? compressAnalysisResult(ruleResult.content, 4000)
662
+ : `오류: ${ruleResult.error}`;
663
+ return {
664
+ handled: true,
665
+ toolName: "generate_apex_rules",
666
+ toolArgs: ruleArgs,
667
+ toolResult: ruleResult,
668
+ summaryPrompt: `PDF 개발표준을 분석하여 YAML 규칙을 생성했습니다.\n\n처리된 PDF: ${pdfFiles.map(f => path.basename(f)).join(", ")}\n\n생성 결과:\n${compressed}\n\n사용자에게 한국어로 생성된 규칙의 내용과 수량을 요약해주세요. 생성된 규칙 파일 경로도 안내해주세요.`,
669
+ };
670
+ }
671
+ /**
672
+ * Detect if the message is a "전체 보고서" workflow intent.
673
+ * 키워드: 전체보고서, 종합보고서, 전체리포트, full report, complete report
674
+ */
675
+ function isFullReportIntent(msg) {
676
+ return /전체\s*보고서|종합\s*보고서|전체\s*리포트|full\s*report|complete\s*report/.test(msg);
677
+ }
678
+ /**
679
+ * Execute the Full Report pipeline:
680
+ * 1. mcp_apex_analyze_code (all profiles)
681
+ * 2. mcp_apex_export_excel → .xlsx
682
+ * 3. generate_report → .md
683
+ * 4. generate_improvement_report → 개선코드 .md
684
+ */
685
+ async function executeFullReportPipeline(userMessage, onEvent) {
686
+ const paths = extractPaths(userMessage);
687
+ if (paths.length === 0) {
688
+ return { handled: false };
689
+ }
690
+ // 분석 대상: 첫 번째 존재하는 디렉토리 (소스코드 경로)
691
+ // 출력 디렉토리: 두 번째 존재하는 디렉토리 (없으면 CWD)
692
+ const existingDirs = paths.filter((p) => {
693
+ try {
694
+ return fs.statSync(p).isDirectory();
695
+ }
696
+ catch {
697
+ return false;
698
+ }
699
+ });
700
+ const targetPath = existingDirs[0] || paths[0];
701
+ const outputDir = existingDirs[1] || ".";
702
+ const allSteps = [];
703
+ let analysisJson = "";
704
+ let excelPath = "";
705
+ let mdPath = "";
706
+ let improvementPath = "";
707
+ // Step 1: apex 분석
708
+ const analyzeArgs = { path: targetPath, profile: "all", max_issues: 100 };
709
+ onEvent?.({ type: "tool_use", tool: "mcp_apex_analyze_code", status: "start", args: analyzeArgs });
710
+ const analyzeCall = {
711
+ id: `fullreport_analyze_${Date.now()}`,
712
+ name: "mcp_apex_analyze_code",
713
+ arguments: analyzeArgs,
714
+ };
715
+ const analyzeResult = await executeTool(analyzeCall);
716
+ onEvent?.({ type: "tool_result", tool: "mcp_apex_analyze_code", status: analyzeResult.success ? "complete" : "error", result: analyzeResult });
717
+ if (!analyzeResult.success) {
718
+ return {
719
+ handled: true,
720
+ toolName: "mcp_apex_analyze_code",
721
+ toolArgs: analyzeArgs,
722
+ toolResult: analyzeResult,
723
+ summaryPrompt: `분석 중 오류가 발생했습니다: ${analyzeResult.error}\n사용자에게 오류를 설명해주세요.`,
724
+ };
725
+ }
726
+ analysisJson = analyzeResult.content;
727
+ allSteps.push("✓ 코드 분석 완료");
728
+ // Step 2: Excel 출력
729
+ const now = new Date();
730
+ const pad = (n) => n.toString().padStart(2, "0");
731
+ const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}`;
732
+ fs.mkdirSync(outputDir, { recursive: true });
733
+ const xlsxFile = path.join(outputDir, `${timestamp}.xlsx`);
734
+ const excelArgs = { output_file: xlsxFile };
735
+ onEvent?.({ type: "tool_use", tool: "mcp_apex_export_excel", status: "start", args: excelArgs });
736
+ const excelCall = {
737
+ id: `fullreport_excel_${Date.now()}`,
738
+ name: "mcp_apex_export_excel",
739
+ arguments: excelArgs,
740
+ };
741
+ const excelResult = await executeTool(excelCall);
742
+ onEvent?.({ type: "tool_result", tool: "mcp_apex_export_excel", status: excelResult.success ? "complete" : "error", result: excelResult });
743
+ if (excelResult.success) {
744
+ try {
745
+ excelPath = JSON.parse(excelResult.content).output_file || xlsxFile;
746
+ }
747
+ catch {
748
+ excelPath = xlsxFile;
749
+ }
750
+ allSteps.push(`✓ Excel 생성: ${excelPath}`);
751
+ }
752
+ else {
753
+ allSteps.push(`⚠ Excel 생성 실패: ${excelResult.error}`);
754
+ }
755
+ // Step 3: Markdown 보고서
756
+ const reportArgs = { report: analysisJson, output_dir: outputDir, with_excel: false };
757
+ onEvent?.({ type: "tool_use", tool: "generate_report", status: "start", args: reportArgs });
758
+ const reportCall = {
759
+ id: `fullreport_md_${Date.now()}`,
760
+ name: "generate_report",
761
+ arguments: reportArgs,
762
+ };
763
+ const reportResult = await executeTool(reportCall);
764
+ onEvent?.({ type: "tool_result", tool: "generate_report", status: reportResult.success ? "complete" : "error", result: reportResult });
765
+ if (reportResult.success) {
766
+ try {
767
+ mdPath = JSON.parse(reportResult.content).report_path || "";
768
+ }
769
+ catch { /* skip */ }
770
+ allSteps.push(`✓ 보고서 생성: ${mdPath}`);
771
+ }
772
+ else {
773
+ allSteps.push(`⚠ 보고서 생성 실패: ${reportResult.error}`);
774
+ }
775
+ // Step 4: 개선 보고서
776
+ const improvArgs = { report: analysisJson, output_dir: outputDir };
777
+ onEvent?.({ type: "tool_use", tool: "generate_improvement_report", status: "start", args: improvArgs });
778
+ const improvCall = {
779
+ id: `fullreport_improv_${Date.now()}`,
780
+ name: "generate_improvement_report",
781
+ arguments: improvArgs,
782
+ };
783
+ const improvResult = await executeTool(improvCall);
784
+ onEvent?.({ type: "tool_result", tool: "generate_improvement_report", status: improvResult.success ? "complete" : "error", result: improvResult });
785
+ if (improvResult.success) {
786
+ try {
787
+ improvementPath = JSON.parse(improvResult.content).report_path || "";
788
+ }
789
+ catch { /* skip */ }
790
+ allSteps.push(`✓ 개선 보고서 생성: ${improvementPath}`);
791
+ }
792
+ else {
793
+ allSteps.push(`⚠ 개선 보고서 생성 실패: ${improvResult.error}`);
794
+ }
795
+ const summaryData = compressAnalysisResult(analysisJson, 3000);
796
+ return {
797
+ handled: true,
798
+ toolName: "generate_report",
799
+ toolArgs: reportArgs,
800
+ toolResult: reportResult,
801
+ summaryPrompt: `전체 보고서 파이프라인이 완료되었습니다.\n\n진행 결과:\n${allSteps.join("\n")}\n\n분석 요약:\n${summaryData}\n\n사용자에게 한국어로 다음을 안내해주세요:\n1. 생성된 파일 목록과 경로 (Excel: ${excelPath}, 보고서: ${mdPath}, 개선보고서: ${improvementPath})\n2. 주요 이슈 통계 요약\n3. 우선 조치 권장사항`,
802
+ };
803
+ }
804
+ //# sourceMappingURL=intentRouter.js.map