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
@@ -1,746 +0,0 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import crypto from "crypto";
4
- import { glob } from "glob";
5
- import { Tool, ToolResult } from "./types.js";
6
- import { OllamaClient } from "../llm/ollama.js";
7
- import { loadConfig } from "../config.js";
8
-
9
- // Embeddings directory
10
- const EMBEDDINGS_DIR = ".activo/embeddings";
11
- const DEFAULT_EMBED_MODEL = "nomic-embed-text";
12
-
13
- // Code chunk for embedding
14
- interface CodeChunk {
15
- filepath: string;
16
- startLine: number;
17
- endLine: number;
18
- content: string;
19
- type: "function" | "class" | "block" | "file";
20
- name?: string;
21
- }
22
-
23
- // Embedding entry
24
- interface EmbeddingEntry {
25
- chunk: CodeChunk;
26
- embedding: number[];
27
- hash: string;
28
- }
29
-
30
- // Embeddings index
31
- interface EmbeddingsIndex {
32
- version: string;
33
- model: string;
34
- createdAt: string;
35
- updatedAt: string;
36
- files: Record<string, {
37
- hash: string;
38
- chunks: number; // count of chunks from this file
39
- }>;
40
- totalChunks: number;
41
- }
42
-
43
- // Get embeddings directory
44
- function getEmbeddingsDir(): string {
45
- return path.resolve(process.cwd(), EMBEDDINGS_DIR);
46
- }
47
-
48
- // Ensure embeddings directory exists
49
- function ensureEmbeddingsDir(): void {
50
- const dir = getEmbeddingsDir();
51
- if (!fs.existsSync(dir)) {
52
- fs.mkdirSync(dir, { recursive: true });
53
- }
54
- }
55
-
56
- // Get index path
57
- function getIndexPath(): string {
58
- return path.join(getEmbeddingsDir(), "index.json");
59
- }
60
-
61
- // Get embeddings data path
62
- function getDataPath(): string {
63
- return path.join(getEmbeddingsDir(), "data.json");
64
- }
65
-
66
- // Load index
67
- function loadIndex(): EmbeddingsIndex | null {
68
- const indexPath = getIndexPath();
69
- if (fs.existsSync(indexPath)) {
70
- try {
71
- return JSON.parse(fs.readFileSync(indexPath, "utf-8"));
72
- } catch {
73
- return null;
74
- }
75
- }
76
- return null;
77
- }
78
-
79
- // Save index
80
- function saveIndex(index: EmbeddingsIndex): void {
81
- ensureEmbeddingsDir();
82
- fs.writeFileSync(getIndexPath(), JSON.stringify(index, null, 2));
83
- }
84
-
85
- // Load embeddings data
86
- function loadData(): EmbeddingEntry[] {
87
- const dataPath = getDataPath();
88
- if (fs.existsSync(dataPath)) {
89
- try {
90
- return JSON.parse(fs.readFileSync(dataPath, "utf-8"));
91
- } catch {
92
- return [];
93
- }
94
- }
95
- return [];
96
- }
97
-
98
- // Save embeddings data
99
- function saveData(data: EmbeddingEntry[]): void {
100
- ensureEmbeddingsDir();
101
- fs.writeFileSync(getDataPath(), JSON.stringify(data));
102
- }
103
-
104
- // Calculate file hash
105
- function calculateHash(content: string): string {
106
- return crypto.createHash("md5").update(content).digest("hex");
107
- }
108
-
109
- // Split file into semantic chunks
110
- function splitIntoChunks(content: string, filepath: string): CodeChunk[] {
111
- const chunks: CodeChunk[] = [];
112
- const lines = content.split("\n");
113
- const ext = path.extname(filepath).toLowerCase();
114
-
115
- // For TypeScript/JavaScript, try to split by functions/classes
116
- if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
117
- let currentChunk: string[] = [];
118
- let chunkStart = 0;
119
- let braceCount = 0;
120
- let inFunction = false;
121
- let functionName = "";
122
- let chunkType: "function" | "class" | "block" = "block";
123
-
124
- for (let i = 0; i < lines.length; i++) {
125
- const line = lines[i];
126
- currentChunk.push(line);
127
-
128
- // Detect function/class start
129
- const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?(?:function|const|let|var)\s+(\w+)\s*[=:]?\s*(?:async\s*)?\(/);
130
- const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
131
- const methodMatch = line.match(/^\s*(?:async\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
132
-
133
- if (!inFunction && (funcMatch || classMatch)) {
134
- if (currentChunk.length > 1) {
135
- // Save previous chunk
136
- const prevContent = currentChunk.slice(0, -1).join("\n").trim();
137
- if (prevContent) {
138
- chunks.push({
139
- filepath,
140
- startLine: chunkStart + 1,
141
- endLine: i,
142
- content: prevContent,
143
- type: "block",
144
- });
145
- }
146
- }
147
- chunkStart = i;
148
- currentChunk = [line];
149
- inFunction = true;
150
- functionName = funcMatch?.[1] || classMatch?.[1] || "";
151
- chunkType = classMatch ? "class" : "function";
152
- }
153
-
154
- // Count braces
155
- braceCount += (line.match(/{/g) || []).length;
156
- braceCount -= (line.match(/}/g) || []).length;
157
-
158
- // End of function/class
159
- if (inFunction && braceCount === 0 && currentChunk.length > 1) {
160
- const chunkContent = currentChunk.join("\n").trim();
161
- if (chunkContent) {
162
- chunks.push({
163
- filepath,
164
- startLine: chunkStart + 1,
165
- endLine: i + 1,
166
- content: chunkContent,
167
- type: chunkType,
168
- name: functionName,
169
- });
170
- }
171
- currentChunk = [];
172
- chunkStart = i + 1;
173
- inFunction = false;
174
- functionName = "";
175
- chunkType = "block";
176
- }
177
- }
178
-
179
- // Remaining content
180
- if (currentChunk.length > 0) {
181
- const remaining = currentChunk.join("\n").trim();
182
- if (remaining) {
183
- chunks.push({
184
- filepath,
185
- startLine: chunkStart + 1,
186
- endLine: lines.length,
187
- content: remaining,
188
- type: inFunction ? chunkType : "block",
189
- name: functionName || undefined,
190
- });
191
- }
192
- }
193
- } else {
194
- // For other files, split by size (around 50 lines per chunk)
195
- const chunkSize = 50;
196
- for (let i = 0; i < lines.length; i += chunkSize) {
197
- const chunkLines = lines.slice(i, Math.min(i + chunkSize, lines.length));
198
- const chunkContent = chunkLines.join("\n").trim();
199
- if (chunkContent) {
200
- chunks.push({
201
- filepath,
202
- startLine: i + 1,
203
- endLine: Math.min(i + chunkSize, lines.length),
204
- content: chunkContent,
205
- type: "block",
206
- });
207
- }
208
- }
209
- }
210
-
211
- // If no chunks or only small chunks, treat whole file as one chunk
212
- if (chunks.length === 0 || (chunks.length === 1 && chunks[0].content.length < 100)) {
213
- return [{
214
- filepath,
215
- startLine: 1,
216
- endLine: lines.length,
217
- content: content.trim(),
218
- type: "file",
219
- }];
220
- }
221
-
222
- // Split large chunks into smaller ones (max 1500 chars per chunk)
223
- const maxChunkChars = 1500;
224
- const finalChunks: CodeChunk[] = [];
225
-
226
- for (const chunk of chunks) {
227
- if (chunk.content.length <= maxChunkChars) {
228
- finalChunks.push(chunk);
229
- } else {
230
- // Split by lines
231
- const chunkLines = chunk.content.split("\n");
232
- let subChunk: string[] = [];
233
- let subStart = chunk.startLine;
234
-
235
- for (let i = 0; i < chunkLines.length; i++) {
236
- subChunk.push(chunkLines[i]);
237
- const subContent = subChunk.join("\n");
238
-
239
- if (subContent.length >= maxChunkChars || i === chunkLines.length - 1) {
240
- if (subContent.trim()) {
241
- finalChunks.push({
242
- filepath: chunk.filepath,
243
- startLine: subStart,
244
- endLine: chunk.startLine + i,
245
- content: subContent.trim(),
246
- type: chunk.type,
247
- name: chunk.name ? `${chunk.name} (part)` : undefined,
248
- });
249
- }
250
- subChunk = [];
251
- subStart = chunk.startLine + i + 1;
252
- }
253
- }
254
- }
255
- }
256
-
257
- return finalChunks;
258
- }
259
-
260
- // Maximum characters for embedding (nomic-embed-text context limit)
261
- const MAX_EMBED_CHARS = 2000;
262
-
263
- // Prepare text for embedding (add context)
264
- function prepareForEmbedding(chunk: CodeChunk): string {
265
- const parts: string[] = [];
266
-
267
- // Add file context
268
- parts.push(`File: ${chunk.filepath}`);
269
-
270
- if (chunk.name) {
271
- parts.push(`${chunk.type}: ${chunk.name}`);
272
- }
273
-
274
- parts.push(`Lines: ${chunk.startLine}-${chunk.endLine}`);
275
- parts.push("");
276
-
277
- // Truncate content if too long
278
- let content = chunk.content;
279
- if (content.length > MAX_EMBED_CHARS) {
280
- content = content.slice(0, MAX_EMBED_CHARS) + "\n... (truncated)";
281
- }
282
- parts.push(content);
283
-
284
- return parts.join("\n");
285
- }
286
-
287
- // Calculate cosine similarity
288
- function cosineSimilarity(a: number[], b: number[]): number {
289
- if (a.length !== b.length) return 0;
290
-
291
- let dotProduct = 0;
292
- let normA = 0;
293
- let normB = 0;
294
-
295
- for (let i = 0; i < a.length; i++) {
296
- dotProduct += a[i] * b[i];
297
- normA += a[i] * a[i];
298
- normB += b[i] * b[i];
299
- }
300
-
301
- const magnitude = Math.sqrt(normA) * Math.sqrt(normB);
302
- return magnitude === 0 ? 0 : dotProduct / magnitude;
303
- }
304
-
305
- // Index Codebase Tool
306
- export const indexCodebaseTool: Tool = {
307
- name: "index_codebase",
308
- description: "Index codebase for semantic search (코드베이스 인덱싱). Creates embeddings for all code files. Run this before using semantic_search. Use when user asks: 'index', 'prepare search', '인덱싱', '검색 준비'.",
309
- parameters: {
310
- type: "object",
311
- properties: {
312
- pattern: {
313
- type: "string",
314
- description: "Glob pattern for files (default: **/*.{ts,js,tsx,jsx,py,go})",
315
- },
316
- force: {
317
- type: "boolean",
318
- description: "Force re-index all files (default: only changed files)",
319
- },
320
- model: {
321
- type: "string",
322
- description: "Embedding model (default: nomic-embed-text)",
323
- },
324
- },
325
- },
326
- handler: async (args): Promise<ToolResult> => {
327
- try {
328
- const pattern = (args.pattern as string) || "**/*.{ts,js,tsx,jsx,py,go,java,rs}";
329
- const force = args.force as boolean || false;
330
- const embedModel = (args.model as string) || DEFAULT_EMBED_MODEL;
331
-
332
- const config = loadConfig();
333
- const client = new OllamaClient(config.ollama);
334
-
335
- // Check if embedding model is available
336
- const models = await client.listModels();
337
- if (!models.some((m) => m.includes(embedModel.split(":")[0]))) {
338
- return {
339
- success: false,
340
- content: "",
341
- error: `임베딩 모델 '${embedModel}'을 찾을 수 없습니다. 'ollama pull ${embedModel}'로 설치하세요.`,
342
- };
343
- }
344
-
345
- // Find files
346
- const files = await glob(pattern, {
347
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/*.min.js"],
348
- });
349
-
350
- if (files.length === 0) {
351
- return { success: true, content: "인덱싱할 파일이 없습니다." };
352
- }
353
-
354
- // Load existing index and data
355
- const existingIndex = loadIndex();
356
- const existingData = force ? [] : loadData();
357
-
358
- const newIndex: EmbeddingsIndex = {
359
- version: "1.0",
360
- model: embedModel,
361
- createdAt: existingIndex?.createdAt || new Date().toISOString(),
362
- updatedAt: new Date().toISOString(),
363
- files: {},
364
- totalChunks: 0,
365
- };
366
-
367
- const newData: EmbeddingEntry[] = [];
368
- let processed = 0;
369
- let skipped = 0;
370
- let errors = 0;
371
-
372
- const results: string[] = [];
373
- results.push(`=== 코드베이스 인덱싱 ===`);
374
- results.push(`모델: ${embedModel}`);
375
- results.push(`파일: ${files.length}개`);
376
- results.push("");
377
-
378
- for (const file of files) {
379
- try {
380
- const content = fs.readFileSync(file, "utf-8");
381
- const hash = calculateHash(content);
382
- const relativePath = path.relative(process.cwd(), file);
383
-
384
- // Check if file unchanged
385
- if (!force && existingIndex?.files[relativePath]?.hash === hash) {
386
- // Keep existing embeddings
387
- const existing = existingData.filter((e) => e.chunk.filepath === relativePath);
388
- newData.push(...existing);
389
- newIndex.files[relativePath] = existingIndex.files[relativePath];
390
- skipped++;
391
- continue;
392
- }
393
-
394
- // Split into chunks
395
- const chunks = splitIntoChunks(content, relativePath);
396
-
397
- // Generate embeddings for each chunk
398
- for (const chunk of chunks) {
399
- const text = prepareForEmbedding(chunk);
400
- const embedding = await client.embed(text, embedModel);
401
-
402
- newData.push({
403
- chunk,
404
- embedding,
405
- hash: calculateHash(chunk.content),
406
- });
407
- }
408
-
409
- newIndex.files[relativePath] = { hash, chunks: chunks.length };
410
- processed++;
411
- results.push(`✅ ${relativePath} (${chunks.length} chunks)`);
412
- } catch (err) {
413
- errors++;
414
- results.push(`❌ ${file}: ${err}`);
415
- }
416
- }
417
-
418
- newIndex.totalChunks = newData.length;
419
-
420
- // Save index and data
421
- saveIndex(newIndex);
422
- saveData(newData);
423
-
424
- results.push("");
425
- results.push(`=== 완료 ===`);
426
- results.push(`처리됨: ${processed}개 파일`);
427
- results.push(`스킵됨: ${skipped}개 파일 (변경없음)`);
428
- results.push(`에러: ${errors}개`);
429
- results.push(`총 청크: ${newData.length}개`);
430
-
431
- return { success: true, content: results.join("\n") };
432
- } catch (error) {
433
- return { success: false, content: "", error: String(error) };
434
- }
435
- },
436
- };
437
-
438
- // Semantic Search Tool
439
- export const semanticSearchTool: Tool = {
440
- name: "semantic_search",
441
- description: "Search code by meaning/description (의미 기반 검색). Finds relevant code based on natural language query. Requires index_codebase first. Use when user asks: 'find code that', 'where is the code for', '관련 코드 찾아', '이런 코드 어디'.",
442
- parameters: {
443
- type: "object",
444
- required: ["query"],
445
- properties: {
446
- query: {
447
- type: "string",
448
- description: "Natural language query describing what you're looking for",
449
- },
450
- limit: {
451
- type: "number",
452
- description: "Maximum number of results (default: 5)",
453
- },
454
- threshold: {
455
- type: "number",
456
- description: "Minimum similarity score 0-1 (default: 0.3)",
457
- },
458
- },
459
- },
460
- handler: async (args): Promise<ToolResult> => {
461
- try {
462
- const query = args.query as string;
463
- const limit = (args.limit as number) || 5;
464
- const threshold = (args.threshold as number) || 0.3;
465
-
466
- // Load index and data
467
- const index = loadIndex();
468
- if (!index) {
469
- return {
470
- success: false,
471
- content: "",
472
- error: "인덱스가 없습니다. 먼저 'index_codebase'를 실행하세요.",
473
- };
474
- }
475
-
476
- const data = loadData();
477
- if (data.length === 0) {
478
- return {
479
- success: false,
480
- content: "",
481
- error: "임베딩 데이터가 없습니다. 먼저 'index_codebase'를 실행하세요.",
482
- };
483
- }
484
-
485
- const config = loadConfig();
486
- const client = new OllamaClient(config.ollama);
487
-
488
- // Generate query embedding
489
- const queryEmbedding = await client.embed(query, index.model);
490
-
491
- // Calculate similarities
492
- const results: Array<{
493
- entry: EmbeddingEntry;
494
- similarity: number;
495
- }> = [];
496
-
497
- for (const entry of data) {
498
- const similarity = cosineSimilarity(queryEmbedding, entry.embedding);
499
- if (similarity >= threshold) {
500
- results.push({ entry, similarity });
501
- }
502
- }
503
-
504
- // Sort by similarity
505
- results.sort((a, b) => b.similarity - a.similarity);
506
- const topResults = results.slice(0, limit);
507
-
508
- if (topResults.length === 0) {
509
- return {
510
- success: true,
511
- content: `"${query}"와 관련된 코드를 찾지 못했습니다. (threshold: ${threshold})`,
512
- };
513
- }
514
-
515
- const lines: string[] = [];
516
- lines.push(`=== 검색 결과: "${query}" ===`);
517
- lines.push(`(${topResults.length}개 결과, 유사도 >= ${threshold})`);
518
- lines.push("");
519
-
520
- for (let i = 0; i < topResults.length; i++) {
521
- const { entry, similarity } = topResults[i];
522
- const chunk = entry.chunk;
523
- const score = Math.round(similarity * 100);
524
-
525
- lines.push(`📍 #${i + 1} [${score}%] ${chunk.filepath}:${chunk.startLine}-${chunk.endLine}`);
526
- if (chunk.name) {
527
- lines.push(` ${chunk.type}: ${chunk.name}`);
528
- }
529
- lines.push(" ```");
530
- // Show first 10 lines of content
531
- const contentLines = chunk.content.split("\n").slice(0, 10);
532
- contentLines.forEach((l) => lines.push(` ${l}`));
533
- if (chunk.content.split("\n").length > 10) {
534
- lines.push(" ...");
535
- }
536
- lines.push(" ```");
537
- lines.push("");
538
- }
539
-
540
- return { success: true, content: lines.join("\n") };
541
- } catch (error) {
542
- return { success: false, content: "", error: String(error) };
543
- }
544
- },
545
- };
546
-
547
- // Find Similar Code Tool
548
- export const findSimilarCodeTool: Tool = {
549
- name: "find_similar_code",
550
- description: "Find code similar to a given snippet or file (유사 코드 찾기). Use when user asks: 'find similar', 'code like this', '비슷한 코드', '이런 패턴'.",
551
- parameters: {
552
- type: "object",
553
- properties: {
554
- code: {
555
- type: "string",
556
- description: "Code snippet to find similar code for",
557
- },
558
- filepath: {
559
- type: "string",
560
- description: "Or specify a file path to find similar files",
561
- },
562
- limit: {
563
- type: "number",
564
- description: "Maximum results (default: 5)",
565
- },
566
- },
567
- },
568
- handler: async (args): Promise<ToolResult> => {
569
- try {
570
- let codeToSearch = args.code as string | undefined;
571
- const filepath = args.filepath as string | undefined;
572
- const limit = (args.limit as number) || 5;
573
-
574
- if (!codeToSearch && !filepath) {
575
- return {
576
- success: false,
577
- content: "",
578
- error: "code 또는 filepath 중 하나를 지정하세요.",
579
- };
580
- }
581
-
582
- if (filepath) {
583
- const fullPath = path.resolve(filepath);
584
- if (!fs.existsSync(fullPath)) {
585
- return { success: false, content: "", error: `파일을 찾을 수 없음: ${filepath}` };
586
- }
587
- codeToSearch = fs.readFileSync(fullPath, "utf-8");
588
- }
589
-
590
- const index = loadIndex();
591
- if (!index) {
592
- return {
593
- success: false,
594
- content: "",
595
- error: "인덱스가 없습니다. 먼저 'index_codebase'를 실행하세요.",
596
- };
597
- }
598
-
599
- const data = loadData();
600
- const config = loadConfig();
601
- const client = new OllamaClient(config.ollama);
602
-
603
- // Generate embedding for the search code
604
- const searchEmbedding = await client.embed(codeToSearch!, index.model);
605
-
606
- // Find similar
607
- const results: Array<{
608
- entry: EmbeddingEntry;
609
- similarity: number;
610
- }> = [];
611
-
612
- const searchFilepath = filepath ? path.relative(process.cwd(), path.resolve(filepath)) : null;
613
-
614
- for (const entry of data) {
615
- // Skip the same file if searching by filepath
616
- if (searchFilepath && entry.chunk.filepath === searchFilepath) continue;
617
-
618
- const similarity = cosineSimilarity(searchEmbedding, entry.embedding);
619
- if (similarity > 0.5) { // Higher threshold for similarity search
620
- results.push({ entry, similarity });
621
- }
622
- }
623
-
624
- results.sort((a, b) => b.similarity - a.similarity);
625
- const topResults = results.slice(0, limit);
626
-
627
- if (topResults.length === 0) {
628
- return { success: true, content: "유사한 코드를 찾지 못했습니다." };
629
- }
630
-
631
- const lines: string[] = [];
632
- lines.push(`=== 유사 코드 검색 결과 ===`);
633
- lines.push("");
634
-
635
- for (let i = 0; i < topResults.length; i++) {
636
- const { entry, similarity } = topResults[i];
637
- const chunk = entry.chunk;
638
- const score = Math.round(similarity * 100);
639
-
640
- lines.push(`📍 #${i + 1} [${score}%] ${chunk.filepath}:${chunk.startLine}-${chunk.endLine}`);
641
- if (chunk.name) {
642
- lines.push(` ${chunk.type}: ${chunk.name}`);
643
- }
644
- lines.push(" ```");
645
- const contentLines = chunk.content.split("\n").slice(0, 8);
646
- contentLines.forEach((l) => lines.push(` ${l}`));
647
- if (chunk.content.split("\n").length > 8) {
648
- lines.push(" ...");
649
- }
650
- lines.push(" ```");
651
- lines.push("");
652
- }
653
-
654
- return { success: true, content: lines.join("\n") };
655
- } catch (error) {
656
- return { success: false, content: "", error: String(error) };
657
- }
658
- },
659
- };
660
-
661
- // Embeddings Status Tool
662
- export const embeddingsStatusTool: Tool = {
663
- name: "embeddings_status",
664
- description: "Show embeddings index status (임베딩 상태). Shows indexed files and statistics. Use when user asks: 'index status', 'embeddings info', '인덱스 상태'.",
665
- parameters: {
666
- type: "object",
667
- properties: {},
668
- },
669
- handler: async (): Promise<ToolResult> => {
670
- try {
671
- const index = loadIndex();
672
-
673
- if (!index) {
674
- return {
675
- success: true,
676
- content: "임베딩 인덱스가 없습니다. 'index_codebase'를 실행하여 생성하세요.",
677
- };
678
- }
679
-
680
- const files = Object.keys(index.files);
681
- const totalChunks = Object.values(index.files).reduce((sum, f) => sum + f.chunks, 0);
682
-
683
- const lines: string[] = [];
684
- lines.push(`=== 임베딩 인덱스 상태 ===`);
685
- lines.push("");
686
- lines.push(`📊 통계:`);
687
- lines.push(` 모델: ${index.model}`);
688
- lines.push(` 파일: ${files.length}개`);
689
- lines.push(` 청크: ${totalChunks}개`);
690
- lines.push(` 생성: ${index.createdAt.slice(0, 10)}`);
691
- lines.push(` 갱신: ${index.updatedAt.slice(0, 10)}`);
692
- lines.push("");
693
- lines.push(`📁 인덱싱된 파일:`);
694
-
695
- // Group by directory
696
- const byDir: Record<string, string[]> = {};
697
- for (const file of files) {
698
- const dir = path.dirname(file) || ".";
699
- if (!byDir[dir]) byDir[dir] = [];
700
- byDir[dir].push(path.basename(file));
701
- }
702
-
703
- for (const [dir, fileList] of Object.entries(byDir)) {
704
- lines.push(` ${dir}/`);
705
- fileList.slice(0, 10).forEach((f) => lines.push(` ${f}`));
706
- if (fileList.length > 10) {
707
- lines.push(` ... 외 ${fileList.length - 10}개`);
708
- }
709
- }
710
-
711
- return { success: true, content: lines.join("\n") };
712
- } catch (error) {
713
- return { success: false, content: "", error: String(error) };
714
- }
715
- },
716
- };
717
-
718
- // Clear Embeddings Tool
719
- export const clearEmbeddingsTool: Tool = {
720
- name: "clear_embeddings",
721
- description: "Clear embeddings index (임베딩 삭제). Removes all indexed data. Use when user asks: 'clear index', 'reset embeddings', '인덱스 삭제'.",
722
- parameters: {
723
- type: "object",
724
- properties: {},
725
- },
726
- handler: async (): Promise<ToolResult> => {
727
- try {
728
- const dir = getEmbeddingsDir();
729
- if (fs.existsSync(dir)) {
730
- fs.rmSync(dir, { recursive: true });
731
- }
732
- return { success: true, content: "임베딩 인덱스가 삭제되었습니다." };
733
- } catch (error) {
734
- return { success: false, content: "", error: String(error) };
735
- }
736
- },
737
- };
738
-
739
- // Export all embedding tools
740
- export const embeddingTools: Tool[] = [
741
- indexCodebaseTool,
742
- semanticSearchTool,
743
- findSimilarCodeTool,
744
- embeddingsStatusTool,
745
- clearEmbeddingsTool,
746
- ];