@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ridit/lens",
3
- "version": "0.3.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 || lf.missingConfigs.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 + lf.missingConfigs.length} issues found`,
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 { requestFileList, analyzeRepo } from "../../utils/ai";
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
- ## Missing Configs
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
- ## Security Issues
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 = [...OUTPUT_FILES, "Preview repo", "Fix issues"] as const;
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="yellow">
286
- {figures.warning} Missing Configs
484
+ <Text bold color="cyan">
485
+ {figures.pointerSmall} Key Files
287
486
  </Text>
288
- {result.missingConfigs.length > 0 ? (
289
- result.missingConfigs.map((f) => (
290
- <Text key={f} color="yellow">
291
- {" "}
292
- {figures.bullet} {f}
293
- </Text>
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="red">
302
- {figures.cross} Security Issues
496
+ <Text bold color="cyan">
497
+ {figures.pointerSmall} Patterns & Idioms
303
498
  </Text>
304
- {result.securityIssues.length > 0 ? (
305
- result.securityIssues.map((s) => (
306
- <Text key={s} color="red">
307
- {" "}
308
- {figures.bullet} {s}
309
- </Text>
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
- missingConfigs: string[];
31
- securityIssues: string[];
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 = "npm" | "yarn" | "pnpm" | "pip" | "unknown";
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 — these reveal what the project exports
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 analyzing a repository.
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 thoroughly using the actual file contents above.
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
- Important instructions:
49
- - Read the actual source code carefully to determine what the project really is
50
- - Look at every component, hook, utility and describe what it actually does
51
- - importantFolders must describe EVERY folder with specifics: what files are in it, what they do, and why they matter
52
- - suggestions must be specific to the actual code you read reference real file names, real function names, real patterns you saw
53
- - missingConfigs should only list things genuinely missing for THIS type of project
54
- - securityIssues must reference actual file names and line patterns found
55
- - overview must be specific: name the actual components/features/exports you saw, not just the tech stack
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) with this exact shape:
59
+ Respond ONLY with a JSON object (no markdown, no explanation):
58
60
  {
59
- "overview": "3-5 sentences. Name the actual components, features, or exports you found. Describe what the project does, who would use it, and what makes it distinctive. Be specific — mention actual file names or component names.",
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/components: contains X, Y, Z components. ButtonComponent uses CVA for variants. Each component is exported from index.ts."
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
- "missingConfigs": ["only configs genuinely missing and relevant — explain WHY each is missing for this specific project"],
64
- "securityIssues": ["reference actual file names and patterns found"],
65
- "suggestions": ["each suggestion must reference actual code — e.g. 'In src/components/Button.tsx, consider adding ...' not generic advice"]
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
- missingConfigs: parsed.missingConfigs ?? [],
91
- securityIssues: parsed.securityIssues ?? [],
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[],