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.
- package/README.md +79 -3
- package/dist/core/llm/ollama.d.ts +2 -0
- package/dist/core/llm/ollama.d.ts.map +1 -1
- package/dist/core/llm/ollama.js +26 -0
- package/dist/core/llm/ollama.js.map +1 -1
- package/dist/core/tools/ast.d.ts +81 -0
- package/dist/core/tools/ast.d.ts.map +1 -0
- package/dist/core/tools/ast.js +700 -0
- package/dist/core/tools/ast.js.map +1 -0
- package/dist/core/tools/cache.d.ts +19 -0
- package/dist/core/tools/cache.d.ts.map +1 -0
- package/dist/core/tools/cache.js +497 -0
- package/dist/core/tools/cache.js.map +1 -0
- package/dist/core/tools/cssAnalysis.d.ts +3 -0
- package/dist/core/tools/cssAnalysis.d.ts.map +1 -0
- package/dist/core/tools/cssAnalysis.js +270 -0
- package/dist/core/tools/cssAnalysis.js.map +1 -0
- package/dist/core/tools/embeddings.d.ts +8 -0
- package/dist/core/tools/embeddings.d.ts.map +1 -0
- package/dist/core/tools/embeddings.js +631 -0
- package/dist/core/tools/embeddings.js.map +1 -0
- package/dist/core/tools/frontendAst.d.ts +6 -0
- package/dist/core/tools/frontendAst.d.ts.map +1 -0
- package/dist/core/tools/frontendAst.js +680 -0
- package/dist/core/tools/frontendAst.js.map +1 -0
- package/dist/core/tools/htmlAnalysis.d.ts +3 -0
- package/dist/core/tools/htmlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/htmlAnalysis.js +398 -0
- package/dist/core/tools/htmlAnalysis.js.map +1 -0
- package/dist/core/tools/index.d.ts +10 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +21 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/javaAst.d.ts +6 -0
- package/dist/core/tools/javaAst.d.ts.map +1 -0
- package/dist/core/tools/javaAst.js +678 -0
- package/dist/core/tools/javaAst.js.map +1 -0
- package/dist/core/tools/memory.d.ts +11 -0
- package/dist/core/tools/memory.d.ts.map +1 -0
- package/dist/core/tools/memory.js +551 -0
- package/dist/core/tools/memory.js.map +1 -0
- package/dist/core/tools/mybatisAnalysis.d.ts +3 -0
- package/dist/core/tools/mybatisAnalysis.d.ts.map +1 -0
- package/dist/core/tools/mybatisAnalysis.js +251 -0
- package/dist/core/tools/mybatisAnalysis.js.map +1 -0
- package/dist/core/tools/sqlAnalysis.d.ts +3 -0
- package/dist/core/tools/sqlAnalysis.d.ts.map +1 -0
- package/dist/core/tools/sqlAnalysis.js +250 -0
- package/dist/core/tools/sqlAnalysis.js.map +1 -0
- package/package.json +2 -1
- package/src/core/llm/ollama.ts +30 -0
- package/src/core/tools/ast.ts +826 -0
- package/src/core/tools/cache.ts +570 -0
- package/src/core/tools/cssAnalysis.ts +324 -0
- package/src/core/tools/embeddings.ts +746 -0
- package/src/core/tools/frontendAst.ts +802 -0
- package/src/core/tools/htmlAnalysis.ts +466 -0
- package/src/core/tools/index.ts +21 -1
- package/src/core/tools/javaAst.ts +812 -0
- package/src/core/tools/memory.ts +655 -0
- package/src/core/tools/mybatisAnalysis.ts +322 -0
- package/src/core/tools/sqlAnalysis.ts +298 -0
- package/FINAL_SIMPLIFIED_SPEC.md +0 -456
- 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
|
+
];
|