activo 0.2.2 β†’ 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 (64) hide show
  1. package/README.md +79 -3
  2. package/dist/core/llm/ollama.d.ts +2 -0
  3. package/dist/core/llm/ollama.d.ts.map +1 -1
  4. package/dist/core/llm/ollama.js +26 -0
  5. package/dist/core/llm/ollama.js.map +1 -1
  6. package/dist/core/tools/ast.d.ts +81 -0
  7. package/dist/core/tools/ast.d.ts.map +1 -0
  8. package/dist/core/tools/ast.js +700 -0
  9. package/dist/core/tools/ast.js.map +1 -0
  10. package/dist/core/tools/cache.d.ts +19 -0
  11. package/dist/core/tools/cache.d.ts.map +1 -0
  12. package/dist/core/tools/cache.js +497 -0
  13. package/dist/core/tools/cache.js.map +1 -0
  14. package/dist/core/tools/cssAnalysis.d.ts +3 -0
  15. package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
  16. package/dist/core/tools/cssAnalysis.js +270 -0
  17. package/dist/core/tools/cssAnalysis.js.map +1 -0
  18. package/dist/core/tools/embeddings.d.ts +8 -0
  19. package/dist/core/tools/embeddings.d.ts.map +1 -0
  20. package/dist/core/tools/embeddings.js +631 -0
  21. package/dist/core/tools/embeddings.js.map +1 -0
  22. package/dist/core/tools/frontendAst.d.ts +6 -0
  23. package/dist/core/tools/frontendAst.d.ts.map +1 -0
  24. package/dist/core/tools/frontendAst.js +680 -0
  25. package/dist/core/tools/frontendAst.js.map +1 -0
  26. package/dist/core/tools/htmlAnalysis.d.ts +3 -0
  27. package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
  28. package/dist/core/tools/htmlAnalysis.js +398 -0
  29. package/dist/core/tools/htmlAnalysis.js.map +1 -0
  30. package/dist/core/tools/index.d.ts +10 -0
  31. package/dist/core/tools/index.d.ts.map +1 -1
  32. package/dist/core/tools/index.js +21 -1
  33. package/dist/core/tools/index.js.map +1 -1
  34. package/dist/core/tools/javaAst.d.ts +6 -0
  35. package/dist/core/tools/javaAst.d.ts.map +1 -0
  36. package/dist/core/tools/javaAst.js +678 -0
  37. package/dist/core/tools/javaAst.js.map +1 -0
  38. package/dist/core/tools/memory.d.ts +11 -0
  39. package/dist/core/tools/memory.d.ts.map +1 -0
  40. package/dist/core/tools/memory.js +551 -0
  41. package/dist/core/tools/memory.js.map +1 -0
  42. package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
  43. package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
  44. package/dist/core/tools/mybatisAnalysis.js +251 -0
  45. package/dist/core/tools/mybatisAnalysis.js.map +1 -0
  46. package/dist/core/tools/sqlAnalysis.d.ts +3 -0
  47. package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
  48. package/dist/core/tools/sqlAnalysis.js +250 -0
  49. package/dist/core/tools/sqlAnalysis.js.map +1 -0
  50. package/package.json +2 -1
  51. package/src/core/llm/ollama.ts +30 -0
  52. package/src/core/tools/ast.ts +826 -0
  53. package/src/core/tools/cache.ts +570 -0
  54. package/src/core/tools/cssAnalysis.ts +324 -0
  55. package/src/core/tools/embeddings.ts +746 -0
  56. package/src/core/tools/frontendAst.ts +802 -0
  57. package/src/core/tools/htmlAnalysis.ts +466 -0
  58. package/src/core/tools/index.ts +21 -1
  59. package/src/core/tools/javaAst.ts +812 -0
  60. package/src/core/tools/memory.ts +655 -0
  61. package/src/core/tools/mybatisAnalysis.ts +322 -0
  62. package/src/core/tools/sqlAnalysis.ts +298 -0
  63. package/FINAL_SIMPLIFIED_SPEC.md +0 -456
  64. package/TODO.md +0 -193
@@ -0,0 +1,570 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import crypto from "crypto";
4
+ import { Tool, ToolResult } from "./types.js";
5
+ import { OllamaClient } from "../llm/ollama.js";
6
+ import { loadConfig } from "../config.js";
7
+
8
+ // Cache directory (project-level)
9
+ const CACHE_DIR = ".activo/cache";
10
+
11
+ // Cache entry interface
12
+ export interface FileCacheEntry {
13
+ filepath: string;
14
+ hash: string; // File content hash for invalidation
15
+ summary: string;
16
+ outline: string;
17
+ exports: string[];
18
+ imports: string[];
19
+ lastUpdated: string;
20
+ model: string;
21
+ }
22
+
23
+ // Cache index interface
24
+ interface CacheIndex {
25
+ version: string;
26
+ entries: Record<string, FileCacheEntry>;
27
+ }
28
+
29
+ // Get cache directory path
30
+ function getCacheDir(): string {
31
+ return path.resolve(process.cwd(), CACHE_DIR);
32
+ }
33
+
34
+ // Get cache index path
35
+ function getCacheIndexPath(): string {
36
+ return path.join(getCacheDir(), "index.json");
37
+ }
38
+
39
+ // Ensure cache directory exists
40
+ function ensureCacheDir(): void {
41
+ const cacheDir = getCacheDir();
42
+ if (!fs.existsSync(cacheDir)) {
43
+ fs.mkdirSync(cacheDir, { recursive: true });
44
+ }
45
+ }
46
+
47
+ // Load cache index
48
+ function loadCacheIndex(): CacheIndex {
49
+ const indexPath = getCacheIndexPath();
50
+ if (fs.existsSync(indexPath)) {
51
+ try {
52
+ const data = fs.readFileSync(indexPath, "utf-8");
53
+ return JSON.parse(data);
54
+ } catch {
55
+ return { version: "1.0", entries: {} };
56
+ }
57
+ }
58
+ return { version: "1.0", entries: {} };
59
+ }
60
+
61
+ // Save cache index
62
+ function saveCacheIndex(index: CacheIndex): void {
63
+ ensureCacheDir();
64
+ const indexPath = getCacheIndexPath();
65
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2));
66
+ }
67
+
68
+ // Calculate file hash
69
+ function calculateFileHash(content: string): string {
70
+ return crypto.createHash("md5").update(content).digest("hex");
71
+ }
72
+
73
+ // Extract code outline (functions, classes, types) without LLM
74
+ function extractOutline(content: string, filepath: string): string {
75
+ const ext = path.extname(filepath).toLowerCase();
76
+ const lines = content.split("\n");
77
+ const outline: string[] = [];
78
+
79
+ // TypeScript/JavaScript patterns
80
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
81
+ const patterns = [
82
+ /^export\s+(default\s+)?(async\s+)?function\s+(\w+)/,
83
+ /^export\s+(default\s+)?class\s+(\w+)/,
84
+ /^export\s+(default\s+)?(const|let|var)\s+(\w+)/,
85
+ /^export\s+(type|interface)\s+(\w+)/,
86
+ /^(async\s+)?function\s+(\w+)/,
87
+ /^class\s+(\w+)/,
88
+ /^(const|let|var)\s+(\w+)\s*[:=]\s*(async\s+)?\(/,
89
+ /^(const|let|var)\s+(\w+)\s*[:=]\s*\{/,
90
+ /^(type|interface)\s+(\w+)/,
91
+ ];
92
+
93
+ lines.forEach((line, idx) => {
94
+ const trimmed = line.trim();
95
+ for (const pattern of patterns) {
96
+ if (pattern.test(trimmed)) {
97
+ outline.push(`L${idx + 1}: ${trimmed.slice(0, 80)}${trimmed.length > 80 ? "..." : ""}`);
98
+ break;
99
+ }
100
+ }
101
+ });
102
+ }
103
+ // Python patterns
104
+ else if ([".py"].includes(ext)) {
105
+ const patterns = [
106
+ /^(async\s+)?def\s+(\w+)/,
107
+ /^class\s+(\w+)/,
108
+ ];
109
+
110
+ lines.forEach((line, idx) => {
111
+ const trimmed = line.trimStart();
112
+ // Only top-level definitions (no indentation)
113
+ if (line === trimmed || line.startsWith(" ") && !line.startsWith(" ")) {
114
+ for (const pattern of patterns) {
115
+ if (pattern.test(trimmed)) {
116
+ outline.push(`L${idx + 1}: ${trimmed.slice(0, 80)}${trimmed.length > 80 ? "..." : ""}`);
117
+ break;
118
+ }
119
+ }
120
+ }
121
+ });
122
+ }
123
+ // Go patterns
124
+ else if ([".go"].includes(ext)) {
125
+ const patterns = [
126
+ /^func\s+(\w+|\(\w+\s+\*?\w+\)\s+\w+)/,
127
+ /^type\s+(\w+)\s+(struct|interface)/,
128
+ ];
129
+
130
+ lines.forEach((line, idx) => {
131
+ const trimmed = line.trim();
132
+ for (const pattern of patterns) {
133
+ if (pattern.test(trimmed)) {
134
+ outline.push(`L${idx + 1}: ${trimmed.slice(0, 80)}${trimmed.length > 80 ? "..." : ""}`);
135
+ break;
136
+ }
137
+ }
138
+ });
139
+ }
140
+
141
+ return outline.length > 0 ? outline.join("\n") : "(No outline extracted)";
142
+ }
143
+
144
+ // Extract imports
145
+ function extractImports(content: string, filepath: string): string[] {
146
+ const ext = path.extname(filepath).toLowerCase();
147
+ const imports: string[] = [];
148
+
149
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
150
+ const importRegex = /import\s+.*?from\s+["'](.+?)["']/g;
151
+ const requireRegex = /require\s*\(\s*["'](.+?)["']\s*\)/g;
152
+ let match;
153
+ while ((match = importRegex.exec(content)) !== null) {
154
+ imports.push(match[1]);
155
+ }
156
+ while ((match = requireRegex.exec(content)) !== null) {
157
+ imports.push(match[1]);
158
+ }
159
+ } else if ([".py"].includes(ext)) {
160
+ const importRegex = /^(?:from\s+(\S+)\s+import|import\s+(\S+))/gm;
161
+ let match;
162
+ while ((match = importRegex.exec(content)) !== null) {
163
+ imports.push(match[1] || match[2]);
164
+ }
165
+ }
166
+
167
+ return [...new Set(imports)];
168
+ }
169
+
170
+ // Extract exports
171
+ function extractExports(content: string, filepath: string): string[] {
172
+ const ext = path.extname(filepath).toLowerCase();
173
+ const exports: string[] = [];
174
+
175
+ if ([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
176
+ const exportRegex = /export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|type|interface)\s+(\w+)/g;
177
+ const exportFromRegex = /export\s+\{([^}]+)\}/g;
178
+ let match;
179
+ while ((match = exportRegex.exec(content)) !== null) {
180
+ exports.push(match[1]);
181
+ }
182
+ while ((match = exportFromRegex.exec(content)) !== null) {
183
+ const names = match[1].split(",").map((s) => s.trim().split(/\s+as\s+/)[0]);
184
+ exports.push(...names);
185
+ }
186
+ }
187
+
188
+ return [...new Set(exports)].filter((e) => e);
189
+ }
190
+
191
+ // Summarize file using Ollama
192
+ async function summarizeWithLLM(content: string, filepath: string): Promise<string> {
193
+ const config = loadConfig();
194
+ const client = new OllamaClient(config.ollama);
195
+
196
+ // Truncate very large files
197
+ const maxChars = 8000;
198
+ const truncated = content.length > maxChars
199
+ ? content.slice(0, maxChars) + "\n\n... (truncated)"
200
+ : content;
201
+
202
+ const prompt = `λ‹€μŒ μ½”λ“œ νŒŒμΌμ„ λΆ„μ„ν•˜κ³  κ°„κ²°ν•˜κ²Œ μš”μ•½ν•΄μ£Όμ„Έμš”.
203
+
204
+ 파일: ${filepath}
205
+
206
+ μ½”λ“œ:
207
+ \`\`\`
208
+ ${truncated}
209
+ \`\`\`
210
+
211
+ λ‹€μŒ ν˜•μ‹μœΌλ‘œ λ‹΅λ³€ν•΄μ£Όμ„Έμš”:
212
+ 1. λͺ©μ : (이 파일의 μ£Όμš” λͺ©μ , 1-2λ¬Έμž₯)
213
+ 2. μ£Όμš” κΈ°λŠ₯: (핡심 ν•¨μˆ˜/클래슀 μ„€λͺ…, 3-5개)
214
+ 3. μ˜μ‘΄μ„±: (μ£Όμš” μ™ΈλΆ€ μ˜μ‘΄μ„±)
215
+ 4. μ°Έκ³ : (νŠΉμ΄μ‚¬ν•­μ΄λ‚˜ 주의점)
216
+
217
+ κ°„κ²°ν•˜κ³  핡심적인 μ •λ³΄λ§Œ ν¬ν•¨ν•˜μ„Έμš”.`;
218
+
219
+ try {
220
+ const response = await client.chat([
221
+ { role: "user", content: prompt }
222
+ ]);
223
+ return response.content;
224
+ } catch (error) {
225
+ return `(μš”μ•½ 생성 μ‹€νŒ¨: ${error})`;
226
+ }
227
+ }
228
+
229
+ // Summarize File Tool
230
+ export const summarizeFileTool: Tool = {
231
+ name: "summarize_file",
232
+ description: "Summarize/analyze a code file (파일 μš”μ•½, 뢄석). Uses LLM to explain what the file does. Caches result for faster retrieval. Use when user asks: 'summarize', 'explain', 'what does this file do', 'μš”μ•½', '뢄석', 'μ„€λͺ…'.",
233
+ parameters: {
234
+ type: "object",
235
+ required: ["filepath"],
236
+ properties: {
237
+ filepath: {
238
+ type: "string",
239
+ description: "Path to the file to summarize",
240
+ },
241
+ force: {
242
+ type: "boolean",
243
+ description: "Force regenerate summary even if cached",
244
+ },
245
+ },
246
+ },
247
+ handler: async (args): Promise<ToolResult> => {
248
+ try {
249
+ const filepath = path.resolve(args.filepath as string);
250
+ const force = args.force as boolean || false;
251
+
252
+ if (!fs.existsSync(filepath)) {
253
+ return { success: false, content: "", error: `File not found: ${filepath}` };
254
+ }
255
+
256
+ const stat = fs.statSync(filepath);
257
+ if (stat.isDirectory()) {
258
+ return { success: false, content: "", error: "Path is a directory" };
259
+ }
260
+
261
+ const content = fs.readFileSync(filepath, "utf-8");
262
+ const hash = calculateFileHash(content);
263
+ const relativePath = path.relative(process.cwd(), filepath);
264
+
265
+ // Check cache
266
+ const index = loadCacheIndex();
267
+ const cached = index.entries[relativePath];
268
+
269
+ if (!force && cached && cached.hash === hash) {
270
+ return {
271
+ success: true,
272
+ content: `[μΊμ‹œλ¨ - ${cached.lastUpdated}]\n\n${cached.summary}\n\n--- 아웃라인 ---\n${cached.outline}`,
273
+ };
274
+ }
275
+
276
+ // Generate new summary
277
+ const summary = await summarizeWithLLM(content, relativePath);
278
+ const outline = extractOutline(content, filepath);
279
+ const imports = extractImports(content, filepath);
280
+ const exports = extractExports(content, filepath);
281
+ const config = loadConfig();
282
+
283
+ // Update cache
284
+ const entry: FileCacheEntry = {
285
+ filepath: relativePath,
286
+ hash,
287
+ summary,
288
+ outline,
289
+ imports,
290
+ exports,
291
+ lastUpdated: new Date().toISOString(),
292
+ model: config.ollama.model,
293
+ };
294
+
295
+ index.entries[relativePath] = entry;
296
+ saveCacheIndex(index);
297
+
298
+ return {
299
+ success: true,
300
+ content: `[μƒˆλ‘œ 생성됨]\n\n${summary}\n\n--- 아웃라인 ---\n${outline}`,
301
+ };
302
+ } catch (error) {
303
+ return { success: false, content: "", error: String(error) };
304
+ }
305
+ },
306
+ };
307
+
308
+ // Get File Outline Tool (no LLM, fast)
309
+ export const getFileOutlineTool: Tool = {
310
+ name: "get_file_outline",
311
+ description: "List functions, classes, imports, exports in a file (ν•¨μˆ˜ λͺ©λ‘, ꡬ쑰, 아웃라인). Fast - no LLM needed. Use when user asks: 'list functions', 'show structure', 'what functions', 'ν•¨μˆ˜ λͺ©λ‘', 'ꡬ쑰', '아웃라인'.",
312
+ parameters: {
313
+ type: "object",
314
+ required: ["filepath"],
315
+ properties: {
316
+ filepath: {
317
+ type: "string",
318
+ description: "Path to the file",
319
+ },
320
+ },
321
+ },
322
+ handler: async (args): Promise<ToolResult> => {
323
+ try {
324
+ const filepath = path.resolve(args.filepath as string);
325
+
326
+ if (!fs.existsSync(filepath)) {
327
+ return { success: false, content: "", error: `File not found: ${filepath}` };
328
+ }
329
+
330
+ const content = fs.readFileSync(filepath, "utf-8");
331
+ const outline = extractOutline(content, filepath);
332
+ const imports = extractImports(content, filepath);
333
+ const exports = extractExports(content, filepath);
334
+
335
+ const result = [
336
+ `=== ${path.basename(filepath)} ===`,
337
+ "",
338
+ "πŸ“€ Exports:",
339
+ exports.length > 0 ? exports.map((e) => ` - ${e}`).join("\n") : " (none)",
340
+ "",
341
+ "πŸ“₯ Imports:",
342
+ imports.length > 0 ? imports.map((i) => ` - ${i}`).join("\n") : " (none)",
343
+ "",
344
+ "πŸ“‹ Outline:",
345
+ outline,
346
+ ].join("\n");
347
+
348
+ return { success: true, content: result };
349
+ } catch (error) {
350
+ return { success: false, content: "", error: String(error) };
351
+ }
352
+ },
353
+ };
354
+
355
+ // Get Cached Summary Tool
356
+ export const getCachedSummaryTool: Tool = {
357
+ name: "get_cached_summary",
358
+ description: "Retrieve previously cached file summary (μΊμ‹œλœ μš”μ•½ 쑰회). Returns JSON with summary, outline, imports, exports. Use when checking if summary exists.",
359
+ parameters: {
360
+ type: "object",
361
+ required: ["filepath"],
362
+ properties: {
363
+ filepath: {
364
+ type: "string",
365
+ description: "Path to the file",
366
+ },
367
+ },
368
+ },
369
+ handler: async (args): Promise<ToolResult> => {
370
+ try {
371
+ const filepath = path.resolve(args.filepath as string);
372
+ const relativePath = path.relative(process.cwd(), filepath);
373
+
374
+ if (!fs.existsSync(filepath)) {
375
+ return { success: false, content: "", error: `File not found: ${filepath}` };
376
+ }
377
+
378
+ const content = fs.readFileSync(filepath, "utf-8");
379
+ const currentHash = calculateFileHash(content);
380
+
381
+ const index = loadCacheIndex();
382
+ const cached = index.entries[relativePath];
383
+
384
+ if (cached && cached.hash === currentHash) {
385
+ return {
386
+ success: true,
387
+ content: JSON.stringify(cached, null, 2),
388
+ };
389
+ }
390
+
391
+ return {
392
+ success: true,
393
+ content: "(μΊμ‹œ μ—†μŒ λ˜λŠ” 파일이 변경됨)",
394
+ };
395
+ } catch (error) {
396
+ return { success: false, content: "", error: String(error) };
397
+ }
398
+ },
399
+ };
400
+
401
+ // List Cached Files Tool
402
+ export const listCacheTool: Tool = {
403
+ name: "list_cache",
404
+ description: "Show all cached/summarized files (μΊμ‹œ λͺ©λ‘). Use when user asks: 'show cache', 'what files are cached', 'μΊμ‹œ λͺ©λ‘', 'μΊμ‹œλœ 파일'.",
405
+ parameters: {
406
+ type: "object",
407
+ properties: {},
408
+ },
409
+ handler: async (): Promise<ToolResult> => {
410
+ try {
411
+ const index = loadCacheIndex();
412
+ const entries = Object.values(index.entries);
413
+
414
+ if (entries.length === 0) {
415
+ return { success: true, content: "μΊμ‹œλœ 파일이 μ—†μŠ΅λ‹ˆλ‹€." };
416
+ }
417
+
418
+ const result = entries.map((e) => {
419
+ return `πŸ“„ ${e.filepath}\n ν•΄μ‹œ: ${e.hash.slice(0, 8)}... | λͺ¨λΈ: ${e.model} | κ°±μ‹ : ${e.lastUpdated.slice(0, 10)}`;
420
+ }).join("\n\n");
421
+
422
+ return {
423
+ success: true,
424
+ content: `=== μΊμ‹œλœ 파일 (${entries.length}개) ===\n\n${result}`,
425
+ };
426
+ } catch (error) {
427
+ return { success: false, content: "", error: String(error) };
428
+ }
429
+ },
430
+ };
431
+
432
+ // Clear Cache Tool
433
+ export const clearCacheTool: Tool = {
434
+ name: "clear_cache",
435
+ description: "Delete cached summaries (μΊμ‹œ μ‚­μ œ/μ΄ˆκΈ°ν™”). Can clear all or specific file. Use when user asks: 'clear cache', 'delete cache', 'μΊμ‹œ μ‚­μ œ', 'μΊμ‹œ μ΄ˆκΈ°ν™”'.",
436
+ parameters: {
437
+ type: "object",
438
+ properties: {
439
+ filepath: {
440
+ type: "string",
441
+ description: "Optional: clear only this file's cache",
442
+ },
443
+ },
444
+ },
445
+ handler: async (args): Promise<ToolResult> => {
446
+ try {
447
+ const specificFile = args.filepath as string | undefined;
448
+ const index = loadCacheIndex();
449
+
450
+ if (specificFile) {
451
+ const relativePath = path.relative(process.cwd(), path.resolve(specificFile));
452
+ if (index.entries[relativePath]) {
453
+ delete index.entries[relativePath];
454
+ saveCacheIndex(index);
455
+ return { success: true, content: `μΊμ‹œ μ‚­μ œλ¨: ${relativePath}` };
456
+ }
457
+ return { success: true, content: `μΊμ‹œμ— μ—†μŒ: ${relativePath}` };
458
+ }
459
+
460
+ const count = Object.keys(index.entries).length;
461
+ saveCacheIndex({ version: "1.0", entries: {} });
462
+ return { success: true, content: `전체 μΊμ‹œ μ‚­μ œλ¨ (${count}개 파일)` };
463
+ } catch (error) {
464
+ return { success: false, content: "", error: String(error) };
465
+ }
466
+ },
467
+ };
468
+
469
+ // Batch Summarize Tool
470
+ export const batchSummarizeTool: Tool = {
471
+ name: "batch_summarize",
472
+ description: "Summarize multiple files at once (μ—¬λŸ¬ 파일 일괄 μš”μ•½). Use glob pattern like 'src/**/*.ts'. Use when user asks: 'summarize all files', 'analyze folder', '전체 μš”μ•½', '폴더 뢄석', 'λͺ¨λ“  파일'.",
473
+ parameters: {
474
+ type: "object",
475
+ required: ["pattern"],
476
+ properties: {
477
+ pattern: {
478
+ type: "string",
479
+ description: "Glob pattern (e.g., src/**/*.ts)",
480
+ },
481
+ skipCached: {
482
+ type: "boolean",
483
+ description: "Skip files that already have valid cache",
484
+ },
485
+ },
486
+ },
487
+ handler: async (args): Promise<ToolResult> => {
488
+ try {
489
+ const { glob } = await import("glob");
490
+ const pattern = args.pattern as string;
491
+ const skipCached = args.skipCached !== false; // default true
492
+
493
+ const files = await glob(pattern, {
494
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"],
495
+ });
496
+
497
+ if (files.length === 0) {
498
+ return { success: true, content: "λ§€μΉ­λ˜λŠ” 파일이 μ—†μŠ΅λ‹ˆλ‹€." };
499
+ }
500
+
501
+ const index = loadCacheIndex();
502
+ const results: string[] = [];
503
+ let processed = 0;
504
+ let skipped = 0;
505
+
506
+ for (const file of files.slice(0, 20)) { // Limit to 20 files
507
+ const filepath = path.resolve(file);
508
+ const relativePath = path.relative(process.cwd(), filepath);
509
+
510
+ try {
511
+ const content = fs.readFileSync(filepath, "utf-8");
512
+ const hash = calculateFileHash(content);
513
+ const cached = index.entries[relativePath];
514
+
515
+ if (skipCached && cached && cached.hash === hash) {
516
+ skipped++;
517
+ continue;
518
+ }
519
+
520
+ const summary = await summarizeWithLLM(content, relativePath);
521
+ const outline = extractOutline(content, filepath);
522
+ const imports = extractImports(content, filepath);
523
+ const exports = extractExports(content, filepath);
524
+ const config = loadConfig();
525
+
526
+ index.entries[relativePath] = {
527
+ filepath: relativePath,
528
+ hash,
529
+ summary,
530
+ outline,
531
+ imports,
532
+ exports,
533
+ lastUpdated: new Date().toISOString(),
534
+ model: config.ollama.model,
535
+ };
536
+
537
+ results.push(`βœ… ${relativePath}`);
538
+ processed++;
539
+ } catch (err) {
540
+ results.push(`❌ ${relativePath}: ${err}`);
541
+ }
542
+ }
543
+
544
+ saveCacheIndex(index);
545
+
546
+ const summary = [
547
+ `=== 배치 μš”μ•½ μ™„λ£Œ ===`,
548
+ `처리됨: ${processed}개`,
549
+ `μŠ€ν‚΅λ¨ (μΊμ‹œ): ${skipped}개`,
550
+ `전체 파일: ${files.length}개${files.length > 20 ? " (μ΅œλŒ€ 20개 처리)" : ""}`,
551
+ "",
552
+ ...results,
553
+ ].join("\n");
554
+
555
+ return { success: true, content: summary };
556
+ } catch (error) {
557
+ return { success: false, content: "", error: String(error) };
558
+ }
559
+ },
560
+ };
561
+
562
+ // All cache tools
563
+ export const cacheTools: Tool[] = [
564
+ summarizeFileTool,
565
+ getFileOutlineTool,
566
+ getCachedSummaryTool,
567
+ listCacheTool,
568
+ clearCacheTool,
569
+ batchSummarizeTool,
570
+ ];