@makeitvisible/cli 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/dist/bin/index.d.ts +1 -0
- package/dist/bin/index.js +1588 -0
- package/dist/bin/index.js.map +1 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +1329 -0
- package/dist/index.js.map +1 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import OpenAI from 'openai';
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import { join, relative, basename } from 'path';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
function git(command) {
|
|
14
|
+
try {
|
|
15
|
+
return execSync(`git ${command}`, {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
maxBuffer: 10 * 1024 * 1024
|
|
18
|
+
// 10MB buffer for large diffs
|
|
19
|
+
}).trim();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`Git command failed: git ${command}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function revisionExists(revision) {
|
|
25
|
+
try {
|
|
26
|
+
git(`rev-parse --verify ${revision}`);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function getDefaultRange() {
|
|
33
|
+
if (revisionExists("HEAD~1")) {
|
|
34
|
+
return "HEAD~1..HEAD";
|
|
35
|
+
}
|
|
36
|
+
if (revisionExists("HEAD")) {
|
|
37
|
+
const emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
38
|
+
return `${emptyTree}..HEAD`;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("No commits found in repository");
|
|
41
|
+
}
|
|
42
|
+
function getChangedFiles(range) {
|
|
43
|
+
const output = git(`diff --numstat ${range}`);
|
|
44
|
+
if (!output) return [];
|
|
45
|
+
return output.split("\n").map((line) => {
|
|
46
|
+
const [additions, deletions, path] = line.split(" ");
|
|
47
|
+
return {
|
|
48
|
+
path,
|
|
49
|
+
additions: parseInt(additions, 10) || 0,
|
|
50
|
+
deletions: parseInt(deletions, 10) || 0,
|
|
51
|
+
status: "modified"
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function getCommits(range) {
|
|
56
|
+
const output = git(`log --format="%H|%an|%ai|%s" ${range}`);
|
|
57
|
+
if (!output) return [];
|
|
58
|
+
return output.split("\n").map((line) => {
|
|
59
|
+
const [hash, author, date, message] = line.split("|");
|
|
60
|
+
return { hash, author, date, message };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function getDiffContent(range) {
|
|
64
|
+
return git(`diff ${range}`);
|
|
65
|
+
}
|
|
66
|
+
function detectBreakingChanges(commits, diffContent) {
|
|
67
|
+
const breakingPatterns = [
|
|
68
|
+
/BREAKING CHANGE/i,
|
|
69
|
+
/\bbreaking\b/i,
|
|
70
|
+
/\!:/,
|
|
71
|
+
/removed/i,
|
|
72
|
+
/deprecated/i
|
|
73
|
+
];
|
|
74
|
+
for (const commit of commits) {
|
|
75
|
+
if (breakingPatterns.some((pattern) => pattern.test(commit.message))) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const lines = diffContent.split("\n");
|
|
80
|
+
const removedExports = lines.filter(
|
|
81
|
+
(line) => line.startsWith("-") && /export\s+(default\s+)?(function|class|const|interface|type)/.test(line)
|
|
82
|
+
);
|
|
83
|
+
return removedExports.length > 0;
|
|
84
|
+
}
|
|
85
|
+
function extractKeywords(files, diffContent) {
|
|
86
|
+
const keywords = /* @__PURE__ */ new Set();
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
const parts = file.path.split("/");
|
|
89
|
+
for (const part of parts) {
|
|
90
|
+
if (!["src", "lib", "dist", "index", "node_modules"].includes(part)) {
|
|
91
|
+
const words = part.replace(/\.(ts|tsx|js|jsx|json|md|css|scss)$/, "").split(/[-_.]/).filter((w) => w.length > 2);
|
|
92
|
+
words.forEach((w) => keywords.add(w.toLowerCase()));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const codePatterns = [
|
|
97
|
+
/(?:function|class|interface|type|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
98
|
+
/export\s+(?:default\s+)?(?:function|class|const)\s+([A-Za-z_][A-Za-z0-9_]*)/g
|
|
99
|
+
];
|
|
100
|
+
for (const pattern of codePatterns) {
|
|
101
|
+
let match;
|
|
102
|
+
while ((match = pattern.exec(diffContent)) !== null) {
|
|
103
|
+
if (match[1] && match[1].length > 2) {
|
|
104
|
+
keywords.add(match[1].toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Array.from(keywords).slice(0, 20);
|
|
109
|
+
}
|
|
110
|
+
function generateTitle(commits, files) {
|
|
111
|
+
if (commits.length === 1) {
|
|
112
|
+
return commits[0].message;
|
|
113
|
+
}
|
|
114
|
+
if (commits.length > 1) {
|
|
115
|
+
const messages = commits.map((c) => c.message);
|
|
116
|
+
const firstWords = messages.map((m) => m.split(" ").slice(0, 3).join(" "));
|
|
117
|
+
return `Changes across ${commits.length} commits: ${firstWords[0]}...`;
|
|
118
|
+
}
|
|
119
|
+
const extensions = [...new Set(files.map((f) => f.path.split(".").pop()))];
|
|
120
|
+
return `Code changes in ${files.length} files (${extensions.join(", ")})`;
|
|
121
|
+
}
|
|
122
|
+
function generateDescription(commits, files, breakingChanges) {
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (commits.length > 0) {
|
|
125
|
+
parts.push(`This change includes ${commits.length} commit(s).`);
|
|
126
|
+
}
|
|
127
|
+
const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
|
|
128
|
+
const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
|
|
129
|
+
parts.push(`Modified ${files.length} file(s) with +${totalAdditions}/-${totalDeletions} lines.`);
|
|
130
|
+
if (breakingChanges) {
|
|
131
|
+
parts.push("\u26A0\uFE0F This change may contain breaking changes.");
|
|
132
|
+
}
|
|
133
|
+
return parts.join(" ");
|
|
134
|
+
}
|
|
135
|
+
function generateGuidelines(commits, files, breakingChanges) {
|
|
136
|
+
const guidelines = [];
|
|
137
|
+
const hasFeature = commits.some((c) => /feat|feature|add|new/i.test(c.message));
|
|
138
|
+
const hasFix = commits.some((c) => /fix|bug|patch|issue/i.test(c.message));
|
|
139
|
+
const hasRefactor = commits.some((c) => /refactor|clean|improve/i.test(c.message));
|
|
140
|
+
if (hasFeature) {
|
|
141
|
+
guidelines.push("Highlight the new capability and its user-facing benefits.");
|
|
142
|
+
}
|
|
143
|
+
if (hasFix) {
|
|
144
|
+
guidelines.push("Explain what was broken and how it's now fixed.");
|
|
145
|
+
}
|
|
146
|
+
if (hasRefactor) {
|
|
147
|
+
guidelines.push("Note the improvements without exposing internal complexity.");
|
|
148
|
+
}
|
|
149
|
+
if (breakingChanges) {
|
|
150
|
+
guidelines.push("Clearly communicate the breaking change and migration steps.");
|
|
151
|
+
guidelines.push("Provide a timeline for deprecation if applicable.");
|
|
152
|
+
}
|
|
153
|
+
const hasAPI = files.some((f) => /api|route|endpoint/i.test(f.path));
|
|
154
|
+
const hasUI = files.some((f) => /component|page|view|ui/i.test(f.path));
|
|
155
|
+
const hasConfig = files.some((f) => /config|env|setting/i.test(f.path));
|
|
156
|
+
if (hasAPI) {
|
|
157
|
+
guidelines.push("Document any API changes with request/response examples.");
|
|
158
|
+
}
|
|
159
|
+
if (hasUI) {
|
|
160
|
+
guidelines.push("Consider including screenshots or visual demos.");
|
|
161
|
+
}
|
|
162
|
+
if (hasConfig) {
|
|
163
|
+
guidelines.push("List any new configuration options or environment variables.");
|
|
164
|
+
}
|
|
165
|
+
return guidelines.length > 0 ? guidelines : ["Focus on the value delivered to users.", "Keep technical details accessible."];
|
|
166
|
+
}
|
|
167
|
+
async function analyzeDiff(range) {
|
|
168
|
+
try {
|
|
169
|
+
git("rev-parse --git-dir");
|
|
170
|
+
} catch {
|
|
171
|
+
throw new Error("Not in a git repository");
|
|
172
|
+
}
|
|
173
|
+
let effectiveRange = range;
|
|
174
|
+
if (range === "HEAD~1..HEAD") {
|
|
175
|
+
try {
|
|
176
|
+
effectiveRange = getDefaultRange();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Could not determine diff range: ${error instanceof Error ? error.message : error}`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
const [start] = range.split("..");
|
|
184
|
+
if (!revisionExists(start)) {
|
|
185
|
+
throw new Error(`Invalid revision: ${start}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const files = getChangedFiles(effectiveRange);
|
|
189
|
+
if (files.length === 0) {
|
|
190
|
+
throw new Error(`No changes found in range: ${effectiveRange}`);
|
|
191
|
+
}
|
|
192
|
+
const commits = getCommits(effectiveRange);
|
|
193
|
+
const diffContent = getDiffContent(effectiveRange);
|
|
194
|
+
const breakingChanges = detectBreakingChanges(commits, diffContent);
|
|
195
|
+
const keywords = extractKeywords(files, diffContent);
|
|
196
|
+
const title = generateTitle(commits, files);
|
|
197
|
+
const description = generateDescription(commits, files, breakingChanges);
|
|
198
|
+
const guidelines = generateGuidelines(commits, files, breakingChanges);
|
|
199
|
+
let repoUrl = "";
|
|
200
|
+
try {
|
|
201
|
+
repoUrl = git("config --get remote.origin.url").replace(/\.git$/, "");
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
const source = {
|
|
205
|
+
type: "commit",
|
|
206
|
+
reference: effectiveRange,
|
|
207
|
+
url: repoUrl ? `${repoUrl}/compare/${effectiveRange.replace("..", "...")}` : void 0
|
|
208
|
+
};
|
|
209
|
+
const context = {
|
|
210
|
+
summary: description,
|
|
211
|
+
technicalDetails: commits.map((c) => `${c.hash.slice(0, 7)}: ${c.message}`),
|
|
212
|
+
affectedComponents: files.map((f) => f.path),
|
|
213
|
+
breakingChanges,
|
|
214
|
+
keywords
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
title,
|
|
218
|
+
description,
|
|
219
|
+
source,
|
|
220
|
+
context,
|
|
221
|
+
guidelines
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function analyzePR(options) {
|
|
225
|
+
try {
|
|
226
|
+
git("rev-parse --git-dir");
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error("Not in a git repository");
|
|
229
|
+
}
|
|
230
|
+
let prTitle = "";
|
|
231
|
+
let prBody = "";
|
|
232
|
+
let prNumber = options.number;
|
|
233
|
+
let prUrl = options.url;
|
|
234
|
+
if (prUrl) {
|
|
235
|
+
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
236
|
+
if (match) {
|
|
237
|
+
prNumber = parseInt(match[3], 10);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (prNumber) {
|
|
241
|
+
try {
|
|
242
|
+
const prInfo = git(`gh pr view ${prNumber} --json title,body,headRefName,baseRefName`);
|
|
243
|
+
const parsed = JSON.parse(prInfo);
|
|
244
|
+
prTitle = parsed.title || "";
|
|
245
|
+
prBody = parsed.body || "";
|
|
246
|
+
const baseRef = parsed.baseRefName || "main";
|
|
247
|
+
const headRef = parsed.headRefName || "HEAD";
|
|
248
|
+
try {
|
|
249
|
+
git(`fetch origin ${baseRef}`);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
const range2 = `origin/${baseRef}...${headRef}`;
|
|
253
|
+
const result2 = await analyzeDiff(range2);
|
|
254
|
+
result2.title = prTitle || result2.title;
|
|
255
|
+
result2.description = prBody ? `${prBody.slice(0, 200)}${prBody.length > 200 ? "..." : ""}` : result2.description;
|
|
256
|
+
result2.source = {
|
|
257
|
+
type: "pr",
|
|
258
|
+
reference: `#${prNumber}`,
|
|
259
|
+
url: prUrl || `${git("config --get remote.origin.url").replace(/\.git$/, "")}/pull/${prNumber}`
|
|
260
|
+
};
|
|
261
|
+
return result2;
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const currentBranch = git("rev-parse --abbrev-ref HEAD");
|
|
266
|
+
let baseBranch = "main";
|
|
267
|
+
try {
|
|
268
|
+
git("rev-parse --verify main");
|
|
269
|
+
} catch {
|
|
270
|
+
try {
|
|
271
|
+
git("rev-parse --verify master");
|
|
272
|
+
baseBranch = "master";
|
|
273
|
+
} catch {
|
|
274
|
+
throw new Error("Could not find base branch (main or master)");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const range = `${baseBranch}...${currentBranch}`;
|
|
278
|
+
const result = await analyzeDiff(range);
|
|
279
|
+
result.source = {
|
|
280
|
+
type: "pr",
|
|
281
|
+
reference: prNumber ? `#${prNumber}` : currentBranch,
|
|
282
|
+
url: prUrl
|
|
283
|
+
};
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/analyzers/context/index.ts
|
|
288
|
+
function mergeContexts(...contexts) {
|
|
289
|
+
const merged = {
|
|
290
|
+
summary: "",
|
|
291
|
+
technicalDetails: [],
|
|
292
|
+
affectedComponents: [],
|
|
293
|
+
breakingChanges: false,
|
|
294
|
+
keywords: []
|
|
295
|
+
};
|
|
296
|
+
for (const ctx of contexts) {
|
|
297
|
+
if (ctx.summary) {
|
|
298
|
+
merged.summary = merged.summary ? `${merged.summary} ${ctx.summary}` : ctx.summary;
|
|
299
|
+
}
|
|
300
|
+
if (ctx.technicalDetails) {
|
|
301
|
+
merged.technicalDetails.push(...ctx.technicalDetails);
|
|
302
|
+
}
|
|
303
|
+
if (ctx.affectedComponents) {
|
|
304
|
+
merged.affectedComponents.push(...ctx.affectedComponents);
|
|
305
|
+
}
|
|
306
|
+
if (ctx.breakingChanges) {
|
|
307
|
+
merged.breakingChanges = true;
|
|
308
|
+
}
|
|
309
|
+
if (ctx.keywords) {
|
|
310
|
+
merged.keywords.push(...ctx.keywords);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
merged.technicalDetails = [...new Set(merged.technicalDetails)];
|
|
314
|
+
merged.affectedComponents = [...new Set(merged.affectedComponents)];
|
|
315
|
+
merged.keywords = [...new Set(merged.keywords)];
|
|
316
|
+
return merged;
|
|
317
|
+
}
|
|
318
|
+
function extractContextFromPaths(paths) {
|
|
319
|
+
const keywords = /* @__PURE__ */ new Set();
|
|
320
|
+
const components = /* @__PURE__ */ new Set();
|
|
321
|
+
for (const path of paths) {
|
|
322
|
+
components.add(path);
|
|
323
|
+
const segments = path.split("/").filter((s) => s && !["src", "lib", "dist", "index"].includes(s));
|
|
324
|
+
for (const segment of segments) {
|
|
325
|
+
const words = segment.replace(/\.(ts|tsx|js|jsx|vue|svelte|py|go|rs|java|rb)$/, "").split(/[-_.]/).filter((w) => w.length > 2);
|
|
326
|
+
words.forEach((w) => keywords.add(w.toLowerCase()));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
affectedComponents: Array.from(components),
|
|
331
|
+
keywords: Array.from(keywords)
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function detectBreakingChangesFromDiff(diff) {
|
|
335
|
+
const breakingPatterns = [
|
|
336
|
+
/^-export\s+/m,
|
|
337
|
+
// Removed exports
|
|
338
|
+
/^-\s*public\s+/m,
|
|
339
|
+
// Removed public methods
|
|
340
|
+
/BREAKING CHANGE/i,
|
|
341
|
+
/\bremoved\b.*\bapi\b/i,
|
|
342
|
+
/\bdeprecated\b/i
|
|
343
|
+
];
|
|
344
|
+
return breakingPatterns.some((pattern) => pattern.test(diff));
|
|
345
|
+
}
|
|
346
|
+
function generateTechnicalSummary(context) {
|
|
347
|
+
const parts = [];
|
|
348
|
+
if (context.affectedComponents.length > 0) {
|
|
349
|
+
parts.push(`Affects ${context.affectedComponents.length} component(s)`);
|
|
350
|
+
}
|
|
351
|
+
if (context.breakingChanges) {
|
|
352
|
+
parts.push("includes breaking changes");
|
|
353
|
+
}
|
|
354
|
+
if (context.keywords.length > 0) {
|
|
355
|
+
parts.push(`related to: ${context.keywords.slice(0, 5).join(", ")}`);
|
|
356
|
+
}
|
|
357
|
+
return parts.join("; ");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/generators/index.ts
|
|
361
|
+
var artifactGenerator = {};
|
|
362
|
+
|
|
363
|
+
// src/agent/tools/definitions.ts
|
|
364
|
+
var AGENT_TOOLS = [
|
|
365
|
+
{
|
|
366
|
+
type: "function",
|
|
367
|
+
function: {
|
|
368
|
+
name: "search_files",
|
|
369
|
+
description: "Search for files by name pattern or content. Use this to find files related to a feature, component, or concept. Returns file paths that match.",
|
|
370
|
+
parameters: {
|
|
371
|
+
type: "object",
|
|
372
|
+
properties: {
|
|
373
|
+
query: {
|
|
374
|
+
type: "string",
|
|
375
|
+
description: "Search query - can be a filename pattern (e.g., '*expense*', '*.schema.ts') or keywords to search in file content"
|
|
376
|
+
},
|
|
377
|
+
searchIn: {
|
|
378
|
+
type: "string",
|
|
379
|
+
enum: ["filename", "content", "both"],
|
|
380
|
+
description: "Where to search: filename only, file content only, or both"
|
|
381
|
+
},
|
|
382
|
+
fileTypes: {
|
|
383
|
+
type: "array",
|
|
384
|
+
items: { type: "string" },
|
|
385
|
+
description: "Optional file extensions to filter (e.g., ['.ts', '.tsx']). If not provided, searches all code files."
|
|
386
|
+
},
|
|
387
|
+
directory: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "Optional subdirectory to search in (relative to project root). If not provided, searches entire project."
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
required: ["query", "searchIn"]
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
type: "function",
|
|
398
|
+
function: {
|
|
399
|
+
name: "read_file",
|
|
400
|
+
description: "Read the contents of a specific file. Use this after finding a relevant file to understand its implementation, imports, exports, and logic.",
|
|
401
|
+
parameters: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
path: {
|
|
405
|
+
type: "string",
|
|
406
|
+
description: "Path to the file (relative to project root)"
|
|
407
|
+
},
|
|
408
|
+
startLine: {
|
|
409
|
+
type: "number",
|
|
410
|
+
description: "Optional start line number (1-indexed) to read from"
|
|
411
|
+
},
|
|
412
|
+
endLine: {
|
|
413
|
+
type: "number",
|
|
414
|
+
description: "Optional end line number (1-indexed) to read to"
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
required: ["path"]
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
type: "function",
|
|
423
|
+
function: {
|
|
424
|
+
name: "find_references",
|
|
425
|
+
description: "Find where a symbol (function, component, class, type, constant) is used/imported across the codebase. Critical for understanding how something is triggered or consumed.",
|
|
426
|
+
parameters: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
symbol: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "The symbol name to find references for (e.g., 'CreateExpenseModal', 'useAuth', 'ExpenseSchema')"
|
|
432
|
+
},
|
|
433
|
+
type: {
|
|
434
|
+
type: "string",
|
|
435
|
+
enum: ["import", "usage", "all"],
|
|
436
|
+
description: "Type of references: 'import' finds import statements, 'usage' finds actual usage, 'all' finds both"
|
|
437
|
+
}
|
|
438
|
+
},
|
|
439
|
+
required: ["symbol", "type"]
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
type: "function",
|
|
445
|
+
function: {
|
|
446
|
+
name: "find_definition",
|
|
447
|
+
description: "Find where a symbol is defined/exported. Use this to find the source definition of a type, function, or component.",
|
|
448
|
+
parameters: {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
symbol: {
|
|
452
|
+
type: "string",
|
|
453
|
+
description: "The symbol name to find the definition of"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
required: ["symbol"]
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
type: "function",
|
|
462
|
+
function: {
|
|
463
|
+
name: "list_directory",
|
|
464
|
+
description: "List files and subdirectories in a directory. Use this to explore the project structure and understand how code is organized.",
|
|
465
|
+
parameters: {
|
|
466
|
+
type: "object",
|
|
467
|
+
properties: {
|
|
468
|
+
path: {
|
|
469
|
+
type: "string",
|
|
470
|
+
description: "Directory path relative to project root. Use '.' or empty string for root."
|
|
471
|
+
},
|
|
472
|
+
recursive: {
|
|
473
|
+
type: "boolean",
|
|
474
|
+
description: "If true, lists all files recursively (up to 3 levels deep). If false, only immediate children."
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
required: ["path"]
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
type: "function",
|
|
483
|
+
function: {
|
|
484
|
+
name: "get_file_structure",
|
|
485
|
+
description: "Analyze a file and extract its structure: imports, exports, functions, classes, types, and their relationships. Use this to quickly understand what a file provides and depends on.",
|
|
486
|
+
parameters: {
|
|
487
|
+
type: "object",
|
|
488
|
+
properties: {
|
|
489
|
+
path: {
|
|
490
|
+
type: "string",
|
|
491
|
+
description: "Path to the file to analyze"
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
required: ["path"]
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
type: "function",
|
|
500
|
+
function: {
|
|
501
|
+
name: "search_types",
|
|
502
|
+
description: "Search for TypeScript/JavaScript type definitions, interfaces, schemas (Zod, Yup), or prop types. Useful for understanding data structures.",
|
|
503
|
+
parameters: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: {
|
|
506
|
+
query: {
|
|
507
|
+
type: "string",
|
|
508
|
+
description: "Search query for types (e.g., 'Expense', 'CreditCard', 'FormData')"
|
|
509
|
+
},
|
|
510
|
+
kind: {
|
|
511
|
+
type: "string",
|
|
512
|
+
enum: ["interface", "type", "schema", "all"],
|
|
513
|
+
description: "What kind of type definition to search for"
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
required: ["query", "kind"]
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
type: "function",
|
|
522
|
+
function: {
|
|
523
|
+
name: "complete_investigation",
|
|
524
|
+
description: "Call this when you have gathered enough information to create a comprehensive artifact. Provide your findings structured for documentation generation.",
|
|
525
|
+
parameters: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
title: {
|
|
529
|
+
type: "string",
|
|
530
|
+
description: "A clear, descriptive title for the feature/topic"
|
|
531
|
+
},
|
|
532
|
+
summary: {
|
|
533
|
+
type: "string",
|
|
534
|
+
description: "A 2-3 sentence summary explaining what this feature does from a user perspective"
|
|
535
|
+
},
|
|
536
|
+
entryPoints: {
|
|
537
|
+
type: "array",
|
|
538
|
+
items: {
|
|
539
|
+
type: "object",
|
|
540
|
+
properties: {
|
|
541
|
+
file: { type: "string" },
|
|
542
|
+
component: { type: "string" },
|
|
543
|
+
description: { type: "string" }
|
|
544
|
+
}
|
|
545
|
+
},
|
|
546
|
+
description: "The main entry points/components for this feature (e.g., the modal, page, or button that triggers it)"
|
|
547
|
+
},
|
|
548
|
+
dataFlow: {
|
|
549
|
+
type: "array",
|
|
550
|
+
items: {
|
|
551
|
+
type: "object",
|
|
552
|
+
properties: {
|
|
553
|
+
step: { type: "number" },
|
|
554
|
+
description: { type: "string" },
|
|
555
|
+
files: { type: "array", items: { type: "string" } }
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
description: "Step-by-step data flow explaining how the feature works"
|
|
559
|
+
},
|
|
560
|
+
keyFiles: {
|
|
561
|
+
type: "array",
|
|
562
|
+
items: {
|
|
563
|
+
type: "object",
|
|
564
|
+
properties: {
|
|
565
|
+
path: { type: "string" },
|
|
566
|
+
purpose: { type: "string" },
|
|
567
|
+
keyExports: { type: "array", items: { type: "string" } }
|
|
568
|
+
}
|
|
569
|
+
},
|
|
570
|
+
description: "The most important files for this feature"
|
|
571
|
+
},
|
|
572
|
+
dataStructures: {
|
|
573
|
+
type: "array",
|
|
574
|
+
items: {
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
name: { type: "string" },
|
|
578
|
+
file: { type: "string" },
|
|
579
|
+
fields: { type: "array", items: { type: "string" } },
|
|
580
|
+
description: { type: "string" }
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
description: "Key data structures (types, interfaces, schemas) used"
|
|
584
|
+
},
|
|
585
|
+
usageExamples: {
|
|
586
|
+
type: "array",
|
|
587
|
+
items: {
|
|
588
|
+
type: "object",
|
|
589
|
+
properties: {
|
|
590
|
+
description: { type: "string" },
|
|
591
|
+
file: { type: "string" },
|
|
592
|
+
codeSnippet: { type: "string" }
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
description: "Examples of how this feature is used in the codebase"
|
|
596
|
+
},
|
|
597
|
+
relatedFeatures: {
|
|
598
|
+
type: "array",
|
|
599
|
+
items: { type: "string" },
|
|
600
|
+
description: "Other features or components that relate to this one"
|
|
601
|
+
},
|
|
602
|
+
technicalNotes: {
|
|
603
|
+
type: "array",
|
|
604
|
+
items: { type: "string" },
|
|
605
|
+
description: "Important technical details, edge cases, or implementation notes"
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
required: [
|
|
609
|
+
"title",
|
|
610
|
+
"summary",
|
|
611
|
+
"entryPoints",
|
|
612
|
+
"dataFlow",
|
|
613
|
+
"keyFiles",
|
|
614
|
+
"dataStructures"
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
var CODE_EXTENSIONS = [
|
|
621
|
+
".ts",
|
|
622
|
+
".tsx",
|
|
623
|
+
".js",
|
|
624
|
+
".jsx",
|
|
625
|
+
".vue",
|
|
626
|
+
".svelte",
|
|
627
|
+
".py",
|
|
628
|
+
".go",
|
|
629
|
+
".rs",
|
|
630
|
+
".java",
|
|
631
|
+
".rb",
|
|
632
|
+
".json",
|
|
633
|
+
".yaml",
|
|
634
|
+
".yml"
|
|
635
|
+
];
|
|
636
|
+
var IGNORED_DIRS = [
|
|
637
|
+
"node_modules",
|
|
638
|
+
".git",
|
|
639
|
+
"dist",
|
|
640
|
+
"build",
|
|
641
|
+
".next",
|
|
642
|
+
".nuxt",
|
|
643
|
+
"coverage",
|
|
644
|
+
".turbo",
|
|
645
|
+
".cache",
|
|
646
|
+
"__pycache__",
|
|
647
|
+
".venv",
|
|
648
|
+
"venv"
|
|
649
|
+
];
|
|
650
|
+
async function searchFiles(ctx, args) {
|
|
651
|
+
const { query, searchIn, fileTypes, directory } = args;
|
|
652
|
+
const searchRoot = directory ? join(ctx.projectRoot, directory) : ctx.projectRoot;
|
|
653
|
+
if (!existsSync(searchRoot)) {
|
|
654
|
+
return { success: false, error: `Directory not found: ${directory}` };
|
|
655
|
+
}
|
|
656
|
+
const results = [];
|
|
657
|
+
const lowerQuery = query.toLowerCase();
|
|
658
|
+
const extensions = fileTypes?.length ? fileTypes : CODE_EXTENSIONS;
|
|
659
|
+
const patterns = extensions.map((ext) => `**/*${ext}`);
|
|
660
|
+
try {
|
|
661
|
+
for (const pattern of patterns) {
|
|
662
|
+
const files = await glob(pattern, {
|
|
663
|
+
cwd: searchRoot,
|
|
664
|
+
ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
665
|
+
nodir: true,
|
|
666
|
+
absolute: false
|
|
667
|
+
});
|
|
668
|
+
for (const file of files) {
|
|
669
|
+
const fullPath = join(searchRoot, file);
|
|
670
|
+
const relativePath = relative(ctx.projectRoot, fullPath);
|
|
671
|
+
if (searchIn === "filename" || searchIn === "both") {
|
|
672
|
+
const fileName = basename(file).toLowerCase();
|
|
673
|
+
if (fileName.includes(lowerQuery) || matchGlob(fileName, query)) {
|
|
674
|
+
if (!results.find((r) => r.path === relativePath)) {
|
|
675
|
+
results.push({ path: relativePath });
|
|
676
|
+
}
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (searchIn === "content" || searchIn === "both") {
|
|
681
|
+
try {
|
|
682
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
683
|
+
const lines = content.split("\n");
|
|
684
|
+
const matches = [];
|
|
685
|
+
for (let i = 0; i < lines.length; i++) {
|
|
686
|
+
if (lines[i].toLowerCase().includes(lowerQuery)) {
|
|
687
|
+
matches.push(`L${i + 1}: ${lines[i].trim().slice(0, 100)}`);
|
|
688
|
+
if (matches.length >= 5) break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (matches.length > 0) {
|
|
692
|
+
results.push({ path: relativePath, matches });
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
results.sort((a, b) => {
|
|
700
|
+
if (!a.matches && b.matches) return -1;
|
|
701
|
+
if (a.matches && !b.matches) return 1;
|
|
702
|
+
if (a.matches && b.matches) return b.matches.length - a.matches.length;
|
|
703
|
+
return 0;
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
success: true,
|
|
707
|
+
data: {
|
|
708
|
+
query,
|
|
709
|
+
totalResults: results.length,
|
|
710
|
+
results: results.slice(0, 20)
|
|
711
|
+
// Limit results
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
} catch (error) {
|
|
715
|
+
return {
|
|
716
|
+
success: false,
|
|
717
|
+
error: `Search failed: ${error instanceof Error ? error.message : error}`
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function matchGlob(filename, pattern) {
|
|
722
|
+
const regex = pattern.replace(/\./g, "\\.").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
723
|
+
return new RegExp(regex, "i").test(filename);
|
|
724
|
+
}
|
|
725
|
+
async function readFile(ctx, args) {
|
|
726
|
+
const { path: filePath, startLine, endLine } = args;
|
|
727
|
+
const fullPath = join(ctx.projectRoot, filePath);
|
|
728
|
+
if (!existsSync(fullPath)) {
|
|
729
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
733
|
+
const lines = content.split("\n");
|
|
734
|
+
const start = startLine ? Math.max(0, startLine - 1) : 0;
|
|
735
|
+
const end = endLine ? Math.min(lines.length, endLine) : lines.length;
|
|
736
|
+
const selectedLines = lines.slice(start, end);
|
|
737
|
+
const numberedContent = selectedLines.map((line, i) => `${start + i + 1}| ${line}`).join("\n");
|
|
738
|
+
return {
|
|
739
|
+
success: true,
|
|
740
|
+
data: {
|
|
741
|
+
path: filePath,
|
|
742
|
+
totalLines: lines.length,
|
|
743
|
+
linesShown: `${start + 1}-${end}`,
|
|
744
|
+
content: numberedContent
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
} catch (error) {
|
|
748
|
+
return {
|
|
749
|
+
success: false,
|
|
750
|
+
error: `Failed to read file: ${error instanceof Error ? error.message : error}`
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
async function findReferences(ctx, args) {
|
|
755
|
+
const { symbol, type } = args;
|
|
756
|
+
const results = [];
|
|
757
|
+
const patterns = [];
|
|
758
|
+
if (type === "import" || type === "all") {
|
|
759
|
+
patterns.push(
|
|
760
|
+
{ regex: new RegExp(`import\\s+.*\\b${symbol}\\b.*from`, "g"), type: "import" },
|
|
761
|
+
{ regex: new RegExp(`import\\s+${symbol}\\s+from`, "g"), type: "import" },
|
|
762
|
+
{ regex: new RegExp(`from\\s+['"][^'"]+['"].*\\b${symbol}\\b`, "g"), type: "import" },
|
|
763
|
+
{ regex: new RegExp(`require\\([^)]*${symbol}[^)]*\\)`, "g"), type: "import" }
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (type === "usage" || type === "all") {
|
|
767
|
+
patterns.push(
|
|
768
|
+
{ regex: new RegExp(`<${symbol}[\\s/>]`, "g"), type: "usage" },
|
|
769
|
+
// JSX component
|
|
770
|
+
{ regex: new RegExp(`\\b${symbol}\\s*\\(`, "g"), type: "usage" },
|
|
771
|
+
// Function call
|
|
772
|
+
{ regex: new RegExp(`:\\s*${symbol}[\\s,;>]`, "g"), type: "usage" },
|
|
773
|
+
// Type annotation
|
|
774
|
+
{ regex: new RegExp(`extends\\s+${symbol}\\b`, "g"), type: "usage" },
|
|
775
|
+
// Class extends
|
|
776
|
+
{ regex: new RegExp(`implements\\s+${symbol}\\b`, "g"), type: "usage" }
|
|
777
|
+
// Interface implements
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
try {
|
|
781
|
+
const files = await glob("**/*.{ts,tsx,js,jsx,vue,svelte}", {
|
|
782
|
+
cwd: ctx.projectRoot,
|
|
783
|
+
ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
784
|
+
nodir: true
|
|
785
|
+
});
|
|
786
|
+
for (const file of files) {
|
|
787
|
+
const fullPath = join(ctx.projectRoot, file);
|
|
788
|
+
try {
|
|
789
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
790
|
+
const lines = content.split("\n");
|
|
791
|
+
for (let i = 0; i < lines.length; i++) {
|
|
792
|
+
const line = lines[i];
|
|
793
|
+
for (const pattern of patterns) {
|
|
794
|
+
if (pattern.regex.test(line)) {
|
|
795
|
+
pattern.regex.lastIndex = 0;
|
|
796
|
+
results.push({
|
|
797
|
+
file,
|
|
798
|
+
line: i + 1,
|
|
799
|
+
type: pattern.type,
|
|
800
|
+
context: line.trim().slice(0, 150)
|
|
801
|
+
});
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch {
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
const groupedByFile = results.reduce(
|
|
810
|
+
(acc, ref) => {
|
|
811
|
+
if (!acc[ref.file]) acc[ref.file] = [];
|
|
812
|
+
acc[ref.file].push(ref);
|
|
813
|
+
return acc;
|
|
814
|
+
},
|
|
815
|
+
{}
|
|
816
|
+
);
|
|
817
|
+
return {
|
|
818
|
+
success: true,
|
|
819
|
+
data: {
|
|
820
|
+
symbol,
|
|
821
|
+
totalReferences: results.length,
|
|
822
|
+
fileCount: Object.keys(groupedByFile).length,
|
|
823
|
+
references: groupedByFile
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
} catch (error) {
|
|
827
|
+
return {
|
|
828
|
+
success: false,
|
|
829
|
+
error: `Failed to find references: ${error instanceof Error ? error.message : error}`
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function findDefinition(ctx, args) {
|
|
834
|
+
const { symbol } = args;
|
|
835
|
+
const definitionPatterns = [
|
|
836
|
+
new RegExp(`export\\s+(?:default\\s+)?(?:async\\s+)?function\\s+${symbol}\\b`),
|
|
837
|
+
new RegExp(`export\\s+(?:default\\s+)?class\\s+${symbol}\\b`),
|
|
838
|
+
new RegExp(`export\\s+(?:const|let|var)\\s+${symbol}\\s*=`),
|
|
839
|
+
new RegExp(`export\\s+(?:interface|type)\\s+${symbol}\\b`),
|
|
840
|
+
new RegExp(`(?:const|let|var)\\s+${symbol}\\s*=.*(?:function|=>|class)`),
|
|
841
|
+
new RegExp(`function\\s+${symbol}\\s*\\(`),
|
|
842
|
+
new RegExp(`class\\s+${symbol}\\s*(?:extends|implements|\\{)`),
|
|
843
|
+
new RegExp(`interface\\s+${symbol}\\s*(?:extends|\\{)`),
|
|
844
|
+
new RegExp(`type\\s+${symbol}\\s*=`),
|
|
845
|
+
new RegExp(`export\\s*\\{[^}]*\\b${symbol}\\b[^}]*\\}`)
|
|
846
|
+
// Named export
|
|
847
|
+
];
|
|
848
|
+
const results = [];
|
|
849
|
+
try {
|
|
850
|
+
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
851
|
+
cwd: ctx.projectRoot,
|
|
852
|
+
ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
853
|
+
nodir: true
|
|
854
|
+
});
|
|
855
|
+
for (const file of files) {
|
|
856
|
+
const fullPath = join(ctx.projectRoot, file);
|
|
857
|
+
try {
|
|
858
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
859
|
+
const lines = content.split("\n");
|
|
860
|
+
for (let i = 0; i < lines.length; i++) {
|
|
861
|
+
const line = lines[i];
|
|
862
|
+
for (const pattern of definitionPatterns) {
|
|
863
|
+
if (pattern.test(line)) {
|
|
864
|
+
let defType = "unknown";
|
|
865
|
+
if (/function/.test(line)) defType = "function";
|
|
866
|
+
else if (/class/.test(line)) defType = "class";
|
|
867
|
+
else if (/interface/.test(line)) defType = "interface";
|
|
868
|
+
else if (/type\s+\w+\s*=/.test(line)) defType = "type";
|
|
869
|
+
else if (/const|let|var/.test(line)) defType = "variable";
|
|
870
|
+
else if (/export\s*\{/.test(line)) defType = "re-export";
|
|
871
|
+
results.push({
|
|
872
|
+
file,
|
|
873
|
+
line: i + 1,
|
|
874
|
+
definitionType: defType,
|
|
875
|
+
context: lines.slice(i, Math.min(i + 5, lines.length)).join("\n").slice(0, 300)
|
|
876
|
+
});
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
return {
|
|
885
|
+
success: true,
|
|
886
|
+
data: {
|
|
887
|
+
symbol,
|
|
888
|
+
definitionsFound: results.length,
|
|
889
|
+
definitions: results
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
} catch (error) {
|
|
893
|
+
return {
|
|
894
|
+
success: false,
|
|
895
|
+
error: `Failed to find definition: ${error instanceof Error ? error.message : error}`
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function listDirectory(ctx, args) {
|
|
900
|
+
const { path: dirPath, recursive } = args;
|
|
901
|
+
const fullPath = dirPath ? join(ctx.projectRoot, dirPath) : ctx.projectRoot;
|
|
902
|
+
if (!existsSync(fullPath)) {
|
|
903
|
+
return { success: false, error: `Directory not found: ${dirPath}` };
|
|
904
|
+
}
|
|
905
|
+
try {
|
|
906
|
+
let listDir2 = function(dir, depth) {
|
|
907
|
+
if (depth > 3) return;
|
|
908
|
+
const entries = readdirSync(dir);
|
|
909
|
+
for (const entry of entries) {
|
|
910
|
+
if (IGNORED_DIRS.includes(entry) || entry.startsWith(".")) continue;
|
|
911
|
+
const entryPath = join(dir, entry);
|
|
912
|
+
const relativePath = relative(ctx.projectRoot, entryPath);
|
|
913
|
+
try {
|
|
914
|
+
const entryStat = statSync(entryPath);
|
|
915
|
+
if (entryStat.isDirectory()) {
|
|
916
|
+
items.push({ path: relativePath, type: "directory" });
|
|
917
|
+
if (recursive) {
|
|
918
|
+
listDir2(entryPath, depth + 1);
|
|
919
|
+
}
|
|
920
|
+
} else if (entryStat.isFile()) {
|
|
921
|
+
items.push({ path: relativePath, type: "file", size: entryStat.size });
|
|
922
|
+
}
|
|
923
|
+
} catch {
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
var listDir = listDir2;
|
|
928
|
+
const stat = statSync(fullPath);
|
|
929
|
+
if (!stat.isDirectory()) {
|
|
930
|
+
return { success: false, error: `Not a directory: ${dirPath}` };
|
|
931
|
+
}
|
|
932
|
+
const items = [];
|
|
933
|
+
listDir2(fullPath, 0);
|
|
934
|
+
items.sort((a, b) => {
|
|
935
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
936
|
+
return a.path.localeCompare(b.path);
|
|
937
|
+
});
|
|
938
|
+
return {
|
|
939
|
+
success: true,
|
|
940
|
+
data: {
|
|
941
|
+
path: dirPath || ".",
|
|
942
|
+
totalItems: items.length,
|
|
943
|
+
items: items.slice(0, 100)
|
|
944
|
+
// Limit results
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return {
|
|
949
|
+
success: false,
|
|
950
|
+
error: `Failed to list directory: ${error instanceof Error ? error.message : error}`
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function getFileStructure(ctx, args) {
|
|
955
|
+
const { path: filePath } = args;
|
|
956
|
+
const fullPath = join(ctx.projectRoot, filePath);
|
|
957
|
+
if (!existsSync(fullPath)) {
|
|
958
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
959
|
+
}
|
|
960
|
+
try {
|
|
961
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
962
|
+
const structure = {
|
|
963
|
+
imports: [],
|
|
964
|
+
exports: [],
|
|
965
|
+
functions: [],
|
|
966
|
+
classes: [],
|
|
967
|
+
types: [],
|
|
968
|
+
components: []
|
|
969
|
+
};
|
|
970
|
+
const lines = content.split("\n");
|
|
971
|
+
for (let i = 0; i < lines.length; i++) {
|
|
972
|
+
const line = lines[i];
|
|
973
|
+
const lineNum = i + 1;
|
|
974
|
+
const importMatch = line.match(
|
|
975
|
+
/import\s+(?:type\s+)?(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/
|
|
976
|
+
);
|
|
977
|
+
if (importMatch) {
|
|
978
|
+
const imports = importMatch[1] ? importMatch[1].split(",").map((s) => s.trim().split(" as ")[0]) : [importMatch[2]];
|
|
979
|
+
structure.imports.push({ from: importMatch[3], imports: imports.filter(Boolean) });
|
|
980
|
+
}
|
|
981
|
+
const exportMatch = line.match(
|
|
982
|
+
/export\s+(?:default\s+)?(?:async\s+)?(function|class|const|let|var|interface|type)\s+(\w+)/
|
|
983
|
+
);
|
|
984
|
+
if (exportMatch) {
|
|
985
|
+
structure.exports.push({ name: exportMatch[2], type: exportMatch[1], line: lineNum });
|
|
986
|
+
}
|
|
987
|
+
const funcMatch = line.match(
|
|
988
|
+
/(?:export\s+)?(?:default\s+)?(async\s+)?function\s+(\w+)/
|
|
989
|
+
);
|
|
990
|
+
if (funcMatch) {
|
|
991
|
+
structure.functions.push({
|
|
992
|
+
name: funcMatch[2],
|
|
993
|
+
line: lineNum,
|
|
994
|
+
async: !!funcMatch[1]
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const arrowMatch = line.match(
|
|
998
|
+
/(?:export\s+)?const\s+(\w+)\s*=\s*(async\s+)?(?:\([^)]*\)|[^=])\s*=>/
|
|
999
|
+
);
|
|
1000
|
+
if (arrowMatch) {
|
|
1001
|
+
structure.functions.push({
|
|
1002
|
+
name: arrowMatch[1],
|
|
1003
|
+
line: lineNum,
|
|
1004
|
+
async: !!arrowMatch[2]
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
const classMatch = line.match(
|
|
1008
|
+
/(?:export\s+)?(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?/
|
|
1009
|
+
);
|
|
1010
|
+
if (classMatch) {
|
|
1011
|
+
structure.classes.push({
|
|
1012
|
+
name: classMatch[1],
|
|
1013
|
+
line: lineNum,
|
|
1014
|
+
extends: classMatch[2]
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
const typeMatch = line.match(
|
|
1018
|
+
/(?:export\s+)?(interface|type)\s+(\w+)/
|
|
1019
|
+
);
|
|
1020
|
+
if (typeMatch) {
|
|
1021
|
+
structure.types.push({
|
|
1022
|
+
name: typeMatch[2],
|
|
1023
|
+
line: lineNum,
|
|
1024
|
+
kind: typeMatch[1]
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (line.match(/(?:export\s+)?(?:default\s+)?(?:const|function)\s+([A-Z]\w+)/) && (content.includes("React") || content.includes("jsx") || filePath.endsWith(".tsx"))) {
|
|
1028
|
+
const compMatch = line.match(/(?:const|function)\s+([A-Z]\w+)/);
|
|
1029
|
+
if (compMatch) {
|
|
1030
|
+
const propsMatch = line.match(/:\s*(?:React\.)?FC<(\w+)>|props:\s*(\w+)/);
|
|
1031
|
+
structure.components.push({
|
|
1032
|
+
name: compMatch[1],
|
|
1033
|
+
line: lineNum,
|
|
1034
|
+
props: propsMatch ? propsMatch[1] || propsMatch[2] : void 0
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return {
|
|
1040
|
+
success: true,
|
|
1041
|
+
data: {
|
|
1042
|
+
path: filePath,
|
|
1043
|
+
structure
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
} catch (error) {
|
|
1047
|
+
return {
|
|
1048
|
+
success: false,
|
|
1049
|
+
error: `Failed to analyze file: ${error instanceof Error ? error.message : error}`
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
async function searchTypes(ctx, args) {
|
|
1054
|
+
const { query, kind } = args;
|
|
1055
|
+
const results = [];
|
|
1056
|
+
const patterns = [];
|
|
1057
|
+
if (kind === "interface" || kind === "all") {
|
|
1058
|
+
patterns.push(new RegExp(`interface\\s+(\\w*${query}\\w*)`, "gi"));
|
|
1059
|
+
}
|
|
1060
|
+
if (kind === "type" || kind === "all") {
|
|
1061
|
+
patterns.push(new RegExp(`type\\s+(\\w*${query}\\w*)\\s*=`, "gi"));
|
|
1062
|
+
}
|
|
1063
|
+
if (kind === "schema" || kind === "all") {
|
|
1064
|
+
patterns.push(new RegExp(`(?:const|export\\s+const)\\s+(\\w*${query}\\w*Schema)\\s*=`, "gi"));
|
|
1065
|
+
patterns.push(new RegExp(`z\\.object.*${query}`, "gi"));
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
const files = await glob("**/*.{ts,tsx,js,jsx}", {
|
|
1069
|
+
cwd: ctx.projectRoot,
|
|
1070
|
+
ignore: IGNORED_DIRS.map((d) => `**/${d}/**`),
|
|
1071
|
+
nodir: true
|
|
1072
|
+
});
|
|
1073
|
+
for (const file of files) {
|
|
1074
|
+
const fullPath = join(ctx.projectRoot, file);
|
|
1075
|
+
try {
|
|
1076
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
1077
|
+
const lines = content.split("\n");
|
|
1078
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1079
|
+
const line = lines[i];
|
|
1080
|
+
for (const pattern of patterns) {
|
|
1081
|
+
const match = pattern.exec(line);
|
|
1082
|
+
if (match) {
|
|
1083
|
+
pattern.lastIndex = 0;
|
|
1084
|
+
const preview = lines.slice(i, Math.min(i + 10, lines.length)).join("\n").slice(0, 400);
|
|
1085
|
+
let detectedKind = "type";
|
|
1086
|
+
if (/interface/.test(line)) detectedKind = "interface";
|
|
1087
|
+
else if (/Schema/.test(line) || /z\./.test(line)) detectedKind = "schema";
|
|
1088
|
+
results.push({
|
|
1089
|
+
name: match[1] || query,
|
|
1090
|
+
file,
|
|
1091
|
+
line: i + 1,
|
|
1092
|
+
kind: detectedKind,
|
|
1093
|
+
preview
|
|
1094
|
+
});
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
} catch {
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
success: true,
|
|
1104
|
+
data: {
|
|
1105
|
+
query,
|
|
1106
|
+
kind,
|
|
1107
|
+
totalResults: results.length,
|
|
1108
|
+
results: results.slice(0, 15)
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
return {
|
|
1113
|
+
success: false,
|
|
1114
|
+
error: `Failed to search types: ${error instanceof Error ? error.message : error}`
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
async function executeTool(toolName, args, ctx) {
|
|
1119
|
+
switch (toolName) {
|
|
1120
|
+
case "search_files":
|
|
1121
|
+
return searchFiles(ctx, args);
|
|
1122
|
+
case "read_file":
|
|
1123
|
+
return readFile(ctx, args);
|
|
1124
|
+
case "find_references":
|
|
1125
|
+
return findReferences(ctx, args);
|
|
1126
|
+
case "find_definition":
|
|
1127
|
+
return findDefinition(ctx, args);
|
|
1128
|
+
case "list_directory":
|
|
1129
|
+
return listDirectory(ctx, args);
|
|
1130
|
+
case "get_file_structure":
|
|
1131
|
+
return getFileStructure(ctx, args);
|
|
1132
|
+
case "search_types":
|
|
1133
|
+
return searchTypes(ctx, args);
|
|
1134
|
+
case "complete_investigation":
|
|
1135
|
+
return { success: true, data: args };
|
|
1136
|
+
default:
|
|
1137
|
+
return { success: false, error: `Unknown tool: ${toolName}` };
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/agent/detective.ts
|
|
1142
|
+
var SYSTEM_PROMPT = `You are "The Detective" - an expert code analyst who explores codebases methodically to understand features and implementations.
|
|
1143
|
+
|
|
1144
|
+
Your mission is to investigate a codebase based on a user's query and produce a comprehensive understanding of how a feature works.
|
|
1145
|
+
|
|
1146
|
+
## Investigation Strategy
|
|
1147
|
+
|
|
1148
|
+
1. **Start Broad**: Begin by searching for files related to the query keywords. Look for obvious matches first.
|
|
1149
|
+
|
|
1150
|
+
2. **Follow the Trail**: When you find a relevant file:
|
|
1151
|
+
- Read it to understand what it does
|
|
1152
|
+
- Look at its imports to find dependencies
|
|
1153
|
+
- Find where it's exported/used to understand how it fits in
|
|
1154
|
+
|
|
1155
|
+
3. **Find Entry Points**: Always identify HOW a feature is triggered:
|
|
1156
|
+
- Is it a page/route?
|
|
1157
|
+
- Is it a component triggered by user action?
|
|
1158
|
+
- Is it an API endpoint?
|
|
1159
|
+
- Find the "beginning" of the user journey
|
|
1160
|
+
|
|
1161
|
+
4. **Trace Data Flow**: Understand:
|
|
1162
|
+
- What data does the feature work with? (types, schemas)
|
|
1163
|
+
- Where does data come from? (API, props, state)
|
|
1164
|
+
- Where does data go? (mutations, API calls, state updates)
|
|
1165
|
+
|
|
1166
|
+
5. **Find Usage Examples**: Search for where components/functions are actually used. This shows real-world integration.
|
|
1167
|
+
|
|
1168
|
+
6. **Document Technical Details**: Note any important patterns, validations, error handling, or edge cases.
|
|
1169
|
+
|
|
1170
|
+
## Tool Usage Tips
|
|
1171
|
+
|
|
1172
|
+
- Use \`search_files\` with searchIn="filename" first for broad discovery
|
|
1173
|
+
- Use \`search_files\` with searchIn="content" when looking for specific patterns
|
|
1174
|
+
- Use \`get_file_structure\` to quickly understand a file's purpose without reading everything
|
|
1175
|
+
- Use \`find_references\` to understand how something is used/triggered
|
|
1176
|
+
- Use \`find_definition\` when you see an import and need to find the source
|
|
1177
|
+
- Use \`search_types\` to find data structures and schemas
|
|
1178
|
+
- Use \`read_file\` to examine specific implementation details
|
|
1179
|
+
|
|
1180
|
+
## Investigation Rules
|
|
1181
|
+
|
|
1182
|
+
1. Be thorough but efficient - don't read every file, use structure analysis first
|
|
1183
|
+
2. Always find at least ONE usage example of the main component/function
|
|
1184
|
+
3. Always identify the data types/schemas involved
|
|
1185
|
+
4. Focus on answering: "If I were a new developer, what would I need to know to work on this?"
|
|
1186
|
+
5. When you have enough information, call \`complete_investigation\` with your structured findings
|
|
1187
|
+
|
|
1188
|
+
## Output Quality
|
|
1189
|
+
|
|
1190
|
+
Your final investigation should enable someone to:
|
|
1191
|
+
- Understand what the feature does (user perspective)
|
|
1192
|
+
- Know which files to look at
|
|
1193
|
+
- Understand the data flow
|
|
1194
|
+
- See real usage examples
|
|
1195
|
+
- Know about edge cases or important details`;
|
|
1196
|
+
var DetectiveAgent = class {
|
|
1197
|
+
openai;
|
|
1198
|
+
context;
|
|
1199
|
+
options;
|
|
1200
|
+
messages;
|
|
1201
|
+
toolCallCount = 0;
|
|
1202
|
+
constructor(options) {
|
|
1203
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
1204
|
+
if (!apiKey) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
"Missing OPENAI_API_KEY environment variable.\nSet it to use the Detective agent for code exploration."
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
this.openai = new OpenAI({ apiKey });
|
|
1210
|
+
this.context = { projectRoot: options.projectRoot };
|
|
1211
|
+
this.options = {
|
|
1212
|
+
maxIterations: 25,
|
|
1213
|
+
...options
|
|
1214
|
+
};
|
|
1215
|
+
this.messages = [{ role: "system", content: SYSTEM_PROMPT }];
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Run an investigation based on a user query
|
|
1219
|
+
*/
|
|
1220
|
+
async investigate(query) {
|
|
1221
|
+
this.messages.push({
|
|
1222
|
+
role: "user",
|
|
1223
|
+
content: `Investigate the following topic in the codebase:
|
|
1224
|
+
|
|
1225
|
+
"${query}"
|
|
1226
|
+
|
|
1227
|
+
Use the available tools to explore the codebase, understand the feature, and then call complete_investigation with your findings.`
|
|
1228
|
+
});
|
|
1229
|
+
let iterations = 0;
|
|
1230
|
+
const maxIterations = this.options.maxIterations;
|
|
1231
|
+
while (iterations < maxIterations) {
|
|
1232
|
+
iterations++;
|
|
1233
|
+
try {
|
|
1234
|
+
const response = await this.openai.chat.completions.create({
|
|
1235
|
+
model: "gpt-4o",
|
|
1236
|
+
messages: this.messages,
|
|
1237
|
+
tools: AGENT_TOOLS,
|
|
1238
|
+
tool_choice: "auto",
|
|
1239
|
+
temperature: 0.1
|
|
1240
|
+
});
|
|
1241
|
+
const message = response.choices[0].message;
|
|
1242
|
+
this.messages.push(message);
|
|
1243
|
+
if (message.content && this.options.onThinking) {
|
|
1244
|
+
this.options.onThinking(message.content);
|
|
1245
|
+
}
|
|
1246
|
+
if (!message.tool_calls || message.tool_calls.length === 0) {
|
|
1247
|
+
return {
|
|
1248
|
+
success: false,
|
|
1249
|
+
error: "Agent finished without completing investigation",
|
|
1250
|
+
toolCalls: this.toolCallCount
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
const toolResults = [];
|
|
1254
|
+
for (const toolCall of message.tool_calls) {
|
|
1255
|
+
const toolName = toolCall.function.name;
|
|
1256
|
+
let args;
|
|
1257
|
+
try {
|
|
1258
|
+
args = JSON.parse(toolCall.function.arguments);
|
|
1259
|
+
} catch {
|
|
1260
|
+
args = {};
|
|
1261
|
+
}
|
|
1262
|
+
this.toolCallCount++;
|
|
1263
|
+
if (this.options.onToolCall) {
|
|
1264
|
+
this.options.onToolCall(toolName, args);
|
|
1265
|
+
}
|
|
1266
|
+
if (toolName === "complete_investigation") {
|
|
1267
|
+
if (this.options.onToolResult) {
|
|
1268
|
+
this.options.onToolResult(toolName, { success: true, data: args });
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
success: true,
|
|
1272
|
+
title: args.title,
|
|
1273
|
+
summary: args.summary,
|
|
1274
|
+
entryPoints: args.entryPoints,
|
|
1275
|
+
dataFlow: args.dataFlow,
|
|
1276
|
+
keyFiles: args.keyFiles,
|
|
1277
|
+
dataStructures: args.dataStructures,
|
|
1278
|
+
usageExamples: args.usageExamples,
|
|
1279
|
+
relatedFeatures: args.relatedFeatures,
|
|
1280
|
+
technicalNotes: args.technicalNotes,
|
|
1281
|
+
toolCalls: this.toolCallCount
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const result = await executeTool(toolName, args, this.context);
|
|
1285
|
+
if (this.options.onToolResult) {
|
|
1286
|
+
this.options.onToolResult(toolName, result);
|
|
1287
|
+
}
|
|
1288
|
+
toolResults.push({
|
|
1289
|
+
role: "tool",
|
|
1290
|
+
tool_call_id: toolCall.id,
|
|
1291
|
+
content: JSON.stringify(result)
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
this.messages.push(...toolResults);
|
|
1295
|
+
} catch (error) {
|
|
1296
|
+
return {
|
|
1297
|
+
success: false,
|
|
1298
|
+
error: `Agent error: ${error instanceof Error ? error.message : error}`,
|
|
1299
|
+
toolCalls: this.toolCallCount
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return {
|
|
1304
|
+
success: false,
|
|
1305
|
+
error: `Investigation exceeded maximum iterations (${maxIterations})`,
|
|
1306
|
+
toolCalls: this.toolCallCount
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
async function investigateFeature(query, options) {
|
|
1311
|
+
const projectRoot = options.projectRoot || getProjectRoot();
|
|
1312
|
+
const agent = new DetectiveAgent({
|
|
1313
|
+
...options,
|
|
1314
|
+
projectRoot
|
|
1315
|
+
});
|
|
1316
|
+
return agent.investigate(query);
|
|
1317
|
+
}
|
|
1318
|
+
function getProjectRoot() {
|
|
1319
|
+
try {
|
|
1320
|
+
const { execSync: execSync2 } = __require("child_process");
|
|
1321
|
+
return execSync2("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
1322
|
+
} catch {
|
|
1323
|
+
return process.cwd();
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export { AGENT_TOOLS, DetectiveAgent, analyzeDiff, analyzePR, artifactGenerator, detectBreakingChangesFromDiff, executeTool, extractContextFromPaths, findDefinition, findReferences, generateTechnicalSummary, getFileStructure, investigateFeature, listDirectory, mergeContexts, readFile, searchFiles, searchTypes };
|
|
1328
|
+
//# sourceMappingURL=index.js.map
|
|
1329
|
+
//# sourceMappingURL=index.js.map
|