@ridit/lens 0.3.3 → 0.3.4
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/LENS.md +41 -0
- package/README.md +2 -2
- package/dist/index.mjs +3919 -3587
- package/package.json +1 -2
- package/src/components/repo/LensFileMenu.tsx +2 -9
- package/src/components/repo/RepoAnalysis.tsx +241 -50
- package/src/types/repo.ts +15 -3
- package/src/utils/ai.ts +108 -20
- package/src/utils/lensfile.ts +83 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ridit/lens",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Know Your Codebase.",
|
|
5
5
|
"author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ridit/lens-sdk": "0.1.6",
|
|
23
|
-
"add": "^2.0.6",
|
|
24
23
|
"asciichart": "^1.5.25",
|
|
25
24
|
"bun": "^1.3.11",
|
|
26
25
|
"commander": "^14.0.3",
|
|
@@ -31,18 +31,11 @@ const buildOptions = (lf: LensFile): MenuOption[] => {
|
|
|
31
31
|
description: "Run a fresh AI analysis",
|
|
32
32
|
},
|
|
33
33
|
];
|
|
34
|
-
if (lf.suggestions.length > 0
|
|
34
|
+
if (lf.suggestions.length > 0) {
|
|
35
35
|
opts.push({
|
|
36
36
|
id: "fix-issues",
|
|
37
37
|
label: "Fix issues",
|
|
38
|
-
description: `${lf.suggestions.length
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
if (lf.securityIssues.length > 0) {
|
|
42
|
-
opts.push({
|
|
43
|
-
id: "security",
|
|
44
|
-
label: "Review security issues",
|
|
45
|
-
description: `${lf.securityIssues.length} issue(s) found`,
|
|
38
|
+
description: `${lf.suggestions.length} issues found`,
|
|
46
39
|
});
|
|
47
40
|
}
|
|
48
41
|
opts.push({
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import { Box, Text, Static, useInput } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
4
|
import figures from "figures";
|
|
5
|
-
import { useState } from "react";
|
|
5
|
+
import { useState, useRef } from "react";
|
|
6
6
|
import { writeFileSync } from "fs";
|
|
7
7
|
import path from "path";
|
|
8
8
|
import { ACCENT } from "../../colors";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
requestFileList,
|
|
11
|
+
analyzeRepo,
|
|
12
|
+
extractToolingPatch,
|
|
13
|
+
} from "../../utils/ai";
|
|
10
14
|
import { ProviderPicker } from "../provider/ProviderPicker";
|
|
11
15
|
import { PreviewRunner } from "./PreviewRunner";
|
|
12
16
|
import { IssueFixer } from "./IssueFixer";
|
|
13
|
-
import { writeLensFile } from "../../utils/lensfile";
|
|
17
|
+
import { writeLensFile, patchLensFile } from "../../utils/lensfile";
|
|
18
|
+
import { callChat } from "../../utils/chat";
|
|
19
|
+
import { StaticMessage } from "../chat/ChatMessage";
|
|
20
|
+
import { InputBox, TypewriterText, ShortcutBar } from "../chat/ChatOverlays";
|
|
14
21
|
import type { Provider } from "../../types/config";
|
|
15
22
|
import type { AnalysisResult, ImportantFile } from "../../types/repo";
|
|
23
|
+
import type { Message } from "../../types/chat";
|
|
16
24
|
import { useThinkingPhrase } from "../../utils/thinking";
|
|
17
25
|
|
|
18
26
|
type AnalysisStage =
|
|
@@ -24,12 +32,17 @@ type AnalysisStage =
|
|
|
24
32
|
| { type: "written"; filePath: string }
|
|
25
33
|
| { type: "previewing" }
|
|
26
34
|
| { type: "fixing"; result: AnalysisResult }
|
|
35
|
+
| { type: "asking"; result: AnalysisResult }
|
|
27
36
|
| { type: "error"; message: string };
|
|
28
37
|
|
|
29
38
|
const OUTPUT_FILES = ["CLAUDE.md", "copilot-instructions.md"] as const;
|
|
30
39
|
type OutputFile = (typeof OUTPUT_FILES)[number];
|
|
31
40
|
|
|
32
41
|
function buildMarkdown(repoUrl: string, result: AnalysisResult): string {
|
|
42
|
+
const toolingLines = Object.entries(result.tooling ?? {})
|
|
43
|
+
.map(([k, v]) => `- **${k}**: ${v}`)
|
|
44
|
+
.join("\n");
|
|
45
|
+
|
|
33
46
|
return `# Repository Analysis
|
|
34
47
|
|
|
35
48
|
> ${repoUrl}
|
|
@@ -37,28 +50,56 @@ function buildMarkdown(repoUrl: string, result: AnalysisResult): string {
|
|
|
37
50
|
## Overview
|
|
38
51
|
${result.overview}
|
|
39
52
|
|
|
53
|
+
## Architecture
|
|
54
|
+
${result.architecture ?? ""}
|
|
55
|
+
|
|
56
|
+
## Tooling
|
|
57
|
+
${toolingLines || "- Not determined"}
|
|
58
|
+
|
|
40
59
|
## Important Folders
|
|
41
60
|
${result.importantFolders.map((f) => `- ${f}`).join("\n")}
|
|
42
61
|
|
|
43
|
-
##
|
|
44
|
-
${
|
|
45
|
-
result.missingConfigs.length > 0
|
|
46
|
-
? result.missingConfigs.map((f) => `- ${f}`).join("\n")
|
|
47
|
-
: "- None detected"
|
|
48
|
-
}
|
|
62
|
+
## Key Files
|
|
63
|
+
${(result.keyFiles ?? []).map((f) => `- ${f}`).join("\n")}
|
|
49
64
|
|
|
50
|
-
##
|
|
51
|
-
${
|
|
52
|
-
result.securityIssues.length > 0
|
|
53
|
-
? result.securityIssues.map((s) => `- ⚠️ ${s}`).join("\n")
|
|
54
|
-
: "- None detected"
|
|
55
|
-
}
|
|
65
|
+
## Patterns & Idioms
|
|
66
|
+
${(result.patterns ?? []).map((p) => `- ${p}`).join("\n")}
|
|
56
67
|
|
|
57
68
|
## Suggestions
|
|
58
69
|
${result.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
59
70
|
`;
|
|
60
71
|
}
|
|
61
72
|
|
|
73
|
+
function buildQASystemPrompt(repoUrl: string, result: AnalysisResult): string {
|
|
74
|
+
const toolingLines = Object.entries(result.tooling ?? {})
|
|
75
|
+
.map(([k, v]) => `- ${k}: ${v}`)
|
|
76
|
+
.join("\n");
|
|
77
|
+
|
|
78
|
+
return `You are a codebase assistant for the repository at ${repoUrl}.
|
|
79
|
+
|
|
80
|
+
Here is what you know about this codebase:
|
|
81
|
+
|
|
82
|
+
Overview:
|
|
83
|
+
${result.overview}
|
|
84
|
+
|
|
85
|
+
Architecture:
|
|
86
|
+
${result.architecture ?? "Not determined"}
|
|
87
|
+
|
|
88
|
+
Tooling:
|
|
89
|
+
${toolingLines || "Not determined"}
|
|
90
|
+
|
|
91
|
+
Important Folders:
|
|
92
|
+
${result.importantFolders.map((f) => `- ${f}`).join("\n")}
|
|
93
|
+
|
|
94
|
+
Key Files:
|
|
95
|
+
${(result.keyFiles ?? []).map((f) => `- ${f}`).join("\n")}
|
|
96
|
+
|
|
97
|
+
Patterns & Idioms:
|
|
98
|
+
${(result.patterns ?? []).map((p) => `- ${p}`).join("\n")}
|
|
99
|
+
|
|
100
|
+
Answer questions about this codebase concisely and accurately. If you're unsure about something not covered in the analysis, say so clearly rather than guessing.`;
|
|
101
|
+
}
|
|
102
|
+
|
|
62
103
|
function AskingFilesStep() {
|
|
63
104
|
const phrase = useThinkingPhrase(true, "model");
|
|
64
105
|
return (
|
|
@@ -83,6 +124,121 @@ function AnalyzingStep() {
|
|
|
83
124
|
);
|
|
84
125
|
}
|
|
85
126
|
|
|
127
|
+
// ─── CodebaseQA ──────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
type QAStage = "idle" | "thinking";
|
|
130
|
+
|
|
131
|
+
function CodebaseQA({
|
|
132
|
+
repoUrl,
|
|
133
|
+
result,
|
|
134
|
+
provider,
|
|
135
|
+
onExit,
|
|
136
|
+
}: {
|
|
137
|
+
repoUrl: string;
|
|
138
|
+
result: AnalysisResult;
|
|
139
|
+
provider: Provider;
|
|
140
|
+
onExit: () => void;
|
|
141
|
+
}) {
|
|
142
|
+
const [committed, setCommitted] = useState<Message[]>([]);
|
|
143
|
+
const [allMessages, setAllMessages] = useState<Message[]>([]);
|
|
144
|
+
const [inputValue, setInputValue] = useState("");
|
|
145
|
+
const [inputKey, setInputKey] = useState(0);
|
|
146
|
+
const [qaStage, setQaStage] = useState<QAStage>("idle");
|
|
147
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
148
|
+
const systemPrompt = buildQASystemPrompt(repoUrl, result);
|
|
149
|
+
const thinkingPhrase = useThinkingPhrase(qaStage === "thinking");
|
|
150
|
+
|
|
151
|
+
useInput((_, key) => {
|
|
152
|
+
if (key.escape) {
|
|
153
|
+
if (qaStage === "thinking") {
|
|
154
|
+
abortRef.current?.abort();
|
|
155
|
+
abortRef.current = null;
|
|
156
|
+
setQaStage("idle");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
onExit();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const sendQuestion = (text: string) => {
|
|
164
|
+
const trimmed = text.trim();
|
|
165
|
+
if (!trimmed) return;
|
|
166
|
+
|
|
167
|
+
const userMsg: Message = { role: "user", type: "text", content: trimmed };
|
|
168
|
+
const nextAll = [...allMessages, userMsg];
|
|
169
|
+
setCommitted((prev) => [...prev, userMsg]);
|
|
170
|
+
setAllMessages(nextAll);
|
|
171
|
+
setQaStage("thinking");
|
|
172
|
+
|
|
173
|
+
const abort = new AbortController();
|
|
174
|
+
abortRef.current = abort;
|
|
175
|
+
|
|
176
|
+
callChat(provider, systemPrompt, nextAll, abort.signal)
|
|
177
|
+
.then((answer) => {
|
|
178
|
+
const assistantMsg: Message = {
|
|
179
|
+
role: "assistant",
|
|
180
|
+
type: "text",
|
|
181
|
+
content: answer,
|
|
182
|
+
};
|
|
183
|
+
setCommitted((prev) => [...prev, assistantMsg]);
|
|
184
|
+
setAllMessages([...nextAll, assistantMsg]);
|
|
185
|
+
setQaStage("idle");
|
|
186
|
+
})
|
|
187
|
+
.catch((err: unknown) => {
|
|
188
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
189
|
+
setQaStage("idle");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
const errMsg: Message = {
|
|
193
|
+
role: "assistant",
|
|
194
|
+
type: "text",
|
|
195
|
+
content: `Error: ${err instanceof Error ? err.message : "Request failed"}`,
|
|
196
|
+
};
|
|
197
|
+
setCommitted((prev) => [...prev, errMsg]);
|
|
198
|
+
setAllMessages([...nextAll, errMsg]);
|
|
199
|
+
setQaStage("idle");
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Box flexDirection="column">
|
|
205
|
+
<Static items={committed}>
|
|
206
|
+
{(msg, i) => <StaticMessage key={i} msg={msg} />}
|
|
207
|
+
</Static>
|
|
208
|
+
|
|
209
|
+
{qaStage === "thinking" && (
|
|
210
|
+
<Box gap={1}>
|
|
211
|
+
<Text color={ACCENT}>●</Text>
|
|
212
|
+
<TypewriterText text={thinkingPhrase} />
|
|
213
|
+
<Text color="gray" dimColor>
|
|
214
|
+
· esc cancel
|
|
215
|
+
</Text>
|
|
216
|
+
</Box>
|
|
217
|
+
)}
|
|
218
|
+
|
|
219
|
+
{qaStage === "idle" && (
|
|
220
|
+
<Box flexDirection="column">
|
|
221
|
+
<InputBox
|
|
222
|
+
value={inputValue}
|
|
223
|
+
onChange={setInputValue}
|
|
224
|
+
onSubmit={(val) => {
|
|
225
|
+
if (val.trim()) sendQuestion(val.trim());
|
|
226
|
+
setInputValue("");
|
|
227
|
+
setInputKey((k) => k + 1);
|
|
228
|
+
}}
|
|
229
|
+
inputKey={inputKey}
|
|
230
|
+
/>
|
|
231
|
+
<Text color="gray" dimColor>
|
|
232
|
+
enter send · esc back
|
|
233
|
+
</Text>
|
|
234
|
+
</Box>
|
|
235
|
+
)}
|
|
236
|
+
</Box>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── RepoAnalysis ─────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
86
242
|
export const RepoAnalysis = ({
|
|
87
243
|
repoUrl,
|
|
88
244
|
repoPath,
|
|
@@ -103,18 +259,31 @@ export const RepoAnalysis = ({
|
|
|
103
259
|
? { type: "done", result: preloadedResult }
|
|
104
260
|
: { type: "picking-provider" },
|
|
105
261
|
);
|
|
106
|
-
const [selectedOutput, setSelectedOutput] = useState<0 | 1 | 2 | 3>(0);
|
|
262
|
+
const [selectedOutput, setSelectedOutput] = useState<0 | 1 | 2 | 3 | 4>(0);
|
|
107
263
|
const [requestedFiles, setRequestedFiles] = useState<ImportantFile[]>([]);
|
|
108
264
|
const [provider, setProvider] = useState<Provider | null>(null);
|
|
109
265
|
|
|
110
|
-
const OPTIONS = [
|
|
266
|
+
const OPTIONS = [
|
|
267
|
+
...OUTPUT_FILES,
|
|
268
|
+
"Preview repo",
|
|
269
|
+
"Fix issues",
|
|
270
|
+
"Ask questions",
|
|
271
|
+
] as const;
|
|
111
272
|
|
|
112
273
|
const handleProviderDone = (p: Provider) => {
|
|
113
274
|
setProvider(p);
|
|
114
275
|
setStage({ type: "requesting-files" });
|
|
276
|
+
|
|
115
277
|
requestFileList(repoUrl, repoPath, fileTree, p)
|
|
116
278
|
.then((files) => {
|
|
117
279
|
setRequestedFiles(files);
|
|
280
|
+
|
|
281
|
+
extractToolingPatch(repoUrl, files.length > 0 ? files : initialFiles, p)
|
|
282
|
+
.then((patch) => {
|
|
283
|
+
if (patch) patchLensFile(repoPath, patch);
|
|
284
|
+
})
|
|
285
|
+
.catch(() => {});
|
|
286
|
+
|
|
118
287
|
setStage({ type: "analyzing" });
|
|
119
288
|
return analyzeRepo(repoUrl, files.length > 0 ? files : initialFiles, p);
|
|
120
289
|
})
|
|
@@ -133,10 +302,10 @@ export const RepoAnalysis = ({
|
|
|
133
302
|
useInput((_, key) => {
|
|
134
303
|
if (stage.type !== "done") return;
|
|
135
304
|
if (key.leftArrow)
|
|
136
|
-
setSelectedOutput((i) => Math.max(0, i - 1) as 0 | 1 | 2 | 3);
|
|
305
|
+
setSelectedOutput((i) => Math.max(0, i - 1) as 0 | 1 | 2 | 3 | 4);
|
|
137
306
|
if (key.rightArrow)
|
|
138
307
|
setSelectedOutput(
|
|
139
|
-
(i) => Math.min(OPTIONS.length - 1, i + 1) as 0 | 1 | 2 | 3,
|
|
308
|
+
(i) => Math.min(OPTIONS.length - 1, i + 1) as 0 | 1 | 2 | 3 | 4,
|
|
140
309
|
);
|
|
141
310
|
if (key.return) {
|
|
142
311
|
if (selectedOutput === 2) {
|
|
@@ -147,6 +316,10 @@ export const RepoAnalysis = ({
|
|
|
147
316
|
setStage({ type: "fixing", result: stage.result });
|
|
148
317
|
return;
|
|
149
318
|
}
|
|
319
|
+
if (selectedOutput === 4) {
|
|
320
|
+
setStage({ type: "asking", result: stage.result });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
150
323
|
const fileName = OUTPUT_FILES[selectedOutput] as OutputFile;
|
|
151
324
|
setStage({ type: "writing" });
|
|
152
325
|
try {
|
|
@@ -205,9 +378,7 @@ export const RepoAnalysis = ({
|
|
|
205
378
|
if (stage.type === "written") {
|
|
206
379
|
setTimeout(() => {
|
|
207
380
|
if (onExit) onExit();
|
|
208
|
-
else
|
|
209
|
-
process.exit(0);
|
|
210
|
-
}
|
|
381
|
+
else process.exit(0);
|
|
211
382
|
}, 100);
|
|
212
383
|
return (
|
|
213
384
|
<Text color="green">
|
|
@@ -228,9 +399,7 @@ export const RepoAnalysis = ({
|
|
|
228
399
|
onExit={() => {
|
|
229
400
|
setTimeout(() => {
|
|
230
401
|
if (onExit) onExit();
|
|
231
|
-
else
|
|
232
|
-
process.exit(0);
|
|
233
|
-
}
|
|
402
|
+
else process.exit(0);
|
|
234
403
|
}, 100);
|
|
235
404
|
}}
|
|
236
405
|
/>
|
|
@@ -250,6 +419,17 @@ export const RepoAnalysis = ({
|
|
|
250
419
|
);
|
|
251
420
|
}
|
|
252
421
|
|
|
422
|
+
if (stage.type === "asking") {
|
|
423
|
+
return (
|
|
424
|
+
<CodebaseQA
|
|
425
|
+
repoUrl={repoUrl}
|
|
426
|
+
result={stage.result}
|
|
427
|
+
provider={provider!}
|
|
428
|
+
onExit={() => setStage({ type: "done", result: stage.result })}
|
|
429
|
+
/>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
|
|
253
433
|
if (stage.type === "error") {
|
|
254
434
|
return (
|
|
255
435
|
<Text color="red">
|
|
@@ -269,6 +449,25 @@ export const RepoAnalysis = ({
|
|
|
269
449
|
<Text color="white">{result.overview}</Text>
|
|
270
450
|
</Box>
|
|
271
451
|
|
|
452
|
+
<Box flexDirection="column">
|
|
453
|
+
<Text bold color="cyan">
|
|
454
|
+
{figures.pointerSmall} Architecture
|
|
455
|
+
</Text>
|
|
456
|
+
<Text color="white">{result.architecture}</Text>
|
|
457
|
+
</Box>
|
|
458
|
+
|
|
459
|
+
<Box flexDirection="column">
|
|
460
|
+
<Text bold color="cyan">
|
|
461
|
+
{figures.pointerSmall} Tooling
|
|
462
|
+
</Text>
|
|
463
|
+
{Object.entries(result.tooling ?? {}).map(([k, v]) => (
|
|
464
|
+
<Text key={k} color="white">
|
|
465
|
+
{" "}
|
|
466
|
+
{figures.bullet} <Text bold>{k}</Text>: {v}
|
|
467
|
+
</Text>
|
|
468
|
+
))}
|
|
469
|
+
</Box>
|
|
470
|
+
|
|
272
471
|
<Box flexDirection="column">
|
|
273
472
|
<Text bold color="cyan">
|
|
274
473
|
{figures.pointerSmall} Important Folders
|
|
@@ -282,35 +481,27 @@ export const RepoAnalysis = ({
|
|
|
282
481
|
</Box>
|
|
283
482
|
|
|
284
483
|
<Box flexDirection="column">
|
|
285
|
-
<Text bold color="
|
|
286
|
-
{figures.
|
|
484
|
+
<Text bold color="cyan">
|
|
485
|
+
{figures.pointerSmall} Key Files
|
|
287
486
|
</Text>
|
|
288
|
-
{result.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
))
|
|
295
|
-
) : (
|
|
296
|
-
<Text color="gray"> None detected</Text>
|
|
297
|
-
)}
|
|
487
|
+
{(result.keyFiles ?? []).map((f) => (
|
|
488
|
+
<Text key={f} color="white">
|
|
489
|
+
{" "}
|
|
490
|
+
{figures.bullet} {f}
|
|
491
|
+
</Text>
|
|
492
|
+
))}
|
|
298
493
|
</Box>
|
|
299
494
|
|
|
300
495
|
<Box flexDirection="column">
|
|
301
|
-
<Text bold color="
|
|
302
|
-
{figures.
|
|
496
|
+
<Text bold color="cyan">
|
|
497
|
+
{figures.pointerSmall} Patterns & Idioms
|
|
303
498
|
</Text>
|
|
304
|
-
{result.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
))
|
|
311
|
-
) : (
|
|
312
|
-
<Text color="gray"> None detected</Text>
|
|
313
|
-
)}
|
|
499
|
+
{(result.patterns ?? []).map((p) => (
|
|
500
|
+
<Text key={p} color="white">
|
|
501
|
+
{" "}
|
|
502
|
+
{figures.bullet} {p}
|
|
503
|
+
</Text>
|
|
504
|
+
))}
|
|
314
505
|
</Box>
|
|
315
506
|
|
|
316
507
|
<Box flexDirection="column">
|
package/src/types/repo.ts
CHANGED
|
@@ -27,12 +27,24 @@ export type AIProvider =
|
|
|
27
27
|
export type AnalysisResult = {
|
|
28
28
|
overview: string;
|
|
29
29
|
importantFolders: string[];
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
|
|
31
|
+
tooling: Record<string, string>;
|
|
32
|
+
|
|
33
|
+
keyFiles: string[];
|
|
34
|
+
|
|
35
|
+
patterns: string[];
|
|
36
|
+
|
|
37
|
+
architecture: string;
|
|
32
38
|
suggestions: string[];
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
export type PackageManager =
|
|
41
|
+
export type PackageManager =
|
|
42
|
+
| "npm"
|
|
43
|
+
| "yarn"
|
|
44
|
+
| "pnpm"
|
|
45
|
+
| "bun"
|
|
46
|
+
| "pip"
|
|
47
|
+
| "unknown";
|
|
36
48
|
|
|
37
49
|
export type PreviewInfo = {
|
|
38
50
|
packageManager: PackageManager;
|
package/src/utils/ai.ts
CHANGED
|
@@ -19,8 +19,8 @@ Your job is to select the files you need to read to fully understand what this p
|
|
|
19
19
|
Rules:
|
|
20
20
|
- ALWAYS include package.json, tsconfig.json, README.md if they exist
|
|
21
21
|
- ALWAYS include ALL files inside src/ — especially index files, main entry points, and any files that reveal the project's purpose (components, hooks, utilities, exports)
|
|
22
|
-
- Include config files: vite.config, eslint.config, tailwind.config, etc.
|
|
23
|
-
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
22
|
+
- Include config files: vite.config, eslint.config, tailwind.config, bun.lockb, .nvmrc, etc.
|
|
23
|
+
- If there is a src/index.ts or src/main.ts or src/lib/index.ts, ALWAYS include it
|
|
24
24
|
- Do NOT skip source files just because there are many — pick up to 30 files
|
|
25
25
|
- Prefer breadth: pick at least one file from every folder under src/
|
|
26
26
|
|
|
@@ -36,36 +36,96 @@ export function buildAnalysisPrompt(
|
|
|
36
36
|
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 3000)}\n\`\`\``)
|
|
37
37
|
.join("\n\n");
|
|
38
38
|
|
|
39
|
-
return `You are a senior software engineer
|
|
39
|
+
return `You are a senior software engineer building a persistent knowledge base about a codebase. Your output will be stored and incrementally updated over time — it must be durable, structural knowledge, not ephemeral warnings.
|
|
40
|
+
|
|
40
41
|
Repository URL: ${repoUrl}
|
|
41
42
|
|
|
42
43
|
Here are the file contents:
|
|
43
44
|
|
|
44
45
|
${fileList}
|
|
45
46
|
|
|
46
|
-
Analyze this repository
|
|
47
|
+
Analyze this repository and extract permanent, structural understanding. Focus on WHAT the codebase IS and HOW it works — not linting issues or missing configs.
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
- Read
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
49
|
+
Rules:
|
|
50
|
+
- Read source code carefully. Reference real file names, real function names, real patterns.
|
|
51
|
+
- tooling: detect from package.json, lockfiles, config files. Keys: packageManager (npm/yarn/pnpm/bun), language, runtime, bundler, framework, testRunner, linter, formatter — only include what you actually found evidence of.
|
|
52
|
+
- keyFiles: list the most important files with a one-line description of what they do. Format: "src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai"
|
|
53
|
+
- patterns: list recurring idioms, design patterns, or conventions actually used in the code. E.g. "Discriminated union state machines for multi-stage UI flows", "React + Ink for terminal rendering"
|
|
54
|
+
- architecture: 2-3 sentences describing the high-level structure and how data flows through the system.
|
|
55
|
+
- importantFolders: describe EVERY folder with specifics — what files are in it and what they do.
|
|
56
|
+
- suggestions: specific, actionable improvements referencing real file names and real patterns you saw. No generic advice.
|
|
57
|
+
- overview: 3-5 sentences naming actual components, features, exports. Be specific.
|
|
56
58
|
|
|
57
|
-
Respond ONLY with a JSON object (no markdown, no explanation)
|
|
59
|
+
Respond ONLY with a JSON object (no markdown, no explanation):
|
|
58
60
|
{
|
|
59
|
-
"overview": "
|
|
61
|
+
"overview": "...",
|
|
62
|
+
"architecture": "...",
|
|
63
|
+
"tooling": {
|
|
64
|
+
"packageManager": "bun",
|
|
65
|
+
"language": "TypeScript",
|
|
66
|
+
"runtime": "Node.js",
|
|
67
|
+
"bundler": "tsup",
|
|
68
|
+
"framework": "Ink"
|
|
69
|
+
},
|
|
60
70
|
"importantFolders": [
|
|
61
|
-
"src/
|
|
71
|
+
"src/commands: contains chat.tsx, commit.tsx, review.tsx — each exports an Ink component that is the top-level renderer for that CLI command"
|
|
72
|
+
],
|
|
73
|
+
"keyFiles": [
|
|
74
|
+
"src/utils/ai.ts: callModel abstraction supporting anthropic/gemini/ollama/openai providers via a unified Provider type"
|
|
75
|
+
],
|
|
76
|
+
"patterns": [
|
|
77
|
+
"Discriminated union state machines (type + stage fields) for multi-step UI flows in every command component"
|
|
62
78
|
],
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
79
|
+
"suggestions": [
|
|
80
|
+
"In src/utils/ai.ts, callModel has no retry logic — adding exponential backoff would improve reliability for ollama which can be slow to start"
|
|
81
|
+
]
|
|
66
82
|
}`;
|
|
67
83
|
}
|
|
68
84
|
|
|
85
|
+
export function buildToolingPatchPrompt(
|
|
86
|
+
repoUrl: string,
|
|
87
|
+
files: ImportantFile[],
|
|
88
|
+
): string {
|
|
89
|
+
const relevant = files.filter((f) =>
|
|
90
|
+
[
|
|
91
|
+
"package.json",
|
|
92
|
+
"bun.lockb",
|
|
93
|
+
"yarn.lock",
|
|
94
|
+
"pnpm-lock.yaml",
|
|
95
|
+
"package-lock.json",
|
|
96
|
+
"tsconfig.json",
|
|
97
|
+
".nvmrc",
|
|
98
|
+
".node-version",
|
|
99
|
+
].includes(path.basename(f.path)),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (relevant.length === 0) return "";
|
|
103
|
+
|
|
104
|
+
const fileList = relevant
|
|
105
|
+
.map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 2000)}\n\`\`\``)
|
|
106
|
+
.join("\n\n");
|
|
107
|
+
|
|
108
|
+
return `You are analyzing a repository's tooling configuration.
|
|
109
|
+
Repository: ${repoUrl}
|
|
110
|
+
|
|
111
|
+
${fileList}
|
|
112
|
+
|
|
113
|
+
Extract only tooling information. Respond ONLY with a JSON object:
|
|
114
|
+
{
|
|
115
|
+
"tooling": {
|
|
116
|
+
"packageManager": "bun | npm | yarn | pnpm",
|
|
117
|
+
"language": "TypeScript | JavaScript | ...",
|
|
118
|
+
"runtime": "Node.js | Bun | Deno | ...",
|
|
119
|
+
"bundler": "tsup | esbuild | vite | webpack | ...",
|
|
120
|
+
"framework": "React | Ink | Next.js | ...",
|
|
121
|
+
"testRunner": "vitest | jest | ...",
|
|
122
|
+
"linter": "eslint | biome | ...",
|
|
123
|
+
"formatter": "prettier | biome | ..."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
Only include keys where you found actual evidence. No markdown, no explanation.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
69
129
|
function parseStringArray(text: string): string[] {
|
|
70
130
|
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
71
131
|
const match = cleaned.match(/\[[\s\S]*\]/);
|
|
@@ -87,12 +147,25 @@ function parseResult(text: string): AnalysisResult {
|
|
|
87
147
|
return {
|
|
88
148
|
overview: parsed.overview ?? "No overview provided",
|
|
89
149
|
importantFolders: parsed.importantFolders ?? [],
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
tooling: parsed.tooling ?? {},
|
|
151
|
+
keyFiles: parsed.keyFiles ?? [],
|
|
152
|
+
patterns: parsed.patterns ?? [],
|
|
153
|
+
architecture: parsed.architecture ?? "",
|
|
92
154
|
suggestions: parsed.suggestions ?? [],
|
|
93
155
|
};
|
|
94
156
|
}
|
|
95
157
|
|
|
158
|
+
function parseToolingPatch(text: string): Partial<AnalysisResult> | null {
|
|
159
|
+
try {
|
|
160
|
+
const cleaned = text.replace(/```json|```/g, "").trim();
|
|
161
|
+
const match = cleaned.match(/\{[\s\S]*\}/);
|
|
162
|
+
if (!match) return null;
|
|
163
|
+
return JSON.parse(match[0]) as Partial<AnalysisResult>;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
96
169
|
export function checkOllamaInstalled(): Promise<boolean> {
|
|
97
170
|
return new Promise((resolve) => {
|
|
98
171
|
exec("ollama --version", (err) => resolve(!err));
|
|
@@ -220,6 +293,21 @@ export async function requestFileList(
|
|
|
220
293
|
return files;
|
|
221
294
|
}
|
|
222
295
|
|
|
296
|
+
export async function extractToolingPatch(
|
|
297
|
+
repoUrl: string,
|
|
298
|
+
files: ImportantFile[],
|
|
299
|
+
provider: Provider,
|
|
300
|
+
): Promise<Partial<AnalysisResult> | null> {
|
|
301
|
+
const prompt = buildToolingPatchPrompt(repoUrl, files);
|
|
302
|
+
if (!prompt) return null;
|
|
303
|
+
try {
|
|
304
|
+
const text = await callModel(provider, prompt);
|
|
305
|
+
return parseToolingPatch(text);
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
223
311
|
export async function analyzeRepo(
|
|
224
312
|
repoUrl: string,
|
|
225
313
|
files: ImportantFile[],
|