@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.
@@ -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