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