@ridit/lens 0.3.3 → 0.3.5

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.
@@ -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">
@@ -354,7 +354,7 @@ type ActiveInvestigation = {
354
354
  startTime: number;
355
355
  };
356
356
 
357
- export function WatchRunner({
357
+ export function RunRunner({
358
358
  cmd,
359
359
  repoPath,
360
360
  clean,
@@ -460,7 +460,6 @@ export function WatchRunner({
460
460
  lensContext = `Overview: ${lensFile.overview}
461
461
 
462
462
  Important folders: ${lensFile.importantFolders.join(", ")}
463
- ${lensFile.securityIssues.length > 0 ? `\nKnown security issues:\n${lensFile.securityIssues.map((s) => `- ${s}`).join("\n")}` : ""}
464
463
  ${lensFile.suggestions.length > 0 ? `\nProject suggestions:\n${lensFile.suggestions.map((s) => `- ${s}`).join("\n")}` : ""}`;
465
464
  }
466
465
  }
package/src/index.tsx CHANGED
@@ -7,7 +7,7 @@ import { InitCommand } from "./commands/provider";
7
7
  import { ReviewCommand } from "./commands/review";
8
8
  import { TaskCommand } from "./commands/task";
9
9
  import { ChatCommand } from "./commands/chat";
10
- import { WatchCommand } from "./commands/watch";
10
+ import { RunCommand } from "./commands/run";
11
11
  import { TimelineCommand } from "./commands/timeline";
12
12
  import { CommitCommand } from "./commands/commit";
13
13
  import { registerBuiltins } from "./utils/tools/builtins";
@@ -19,8 +19,7 @@ await loadAddons();
19
19
  const program = new Command();
20
20
 
21
21
  program
22
- .command("stalk <url>")
23
- .alias("repo")
22
+ .command("repo <url>")
24
23
  .description("Analyze a remote repository")
25
24
  .action((url) => {
26
25
  render(<RepoCommand url={url} />);
@@ -34,16 +33,14 @@ program
34
33
  });
35
34
 
36
35
  program
37
- .command("judge [path]")
38
- .alias("review")
36
+ .command("review [path]")
39
37
  .description("Review a local codebase")
40
38
  .action((inputPath) => {
41
39
  render(<ReviewCommand path={inputPath ?? "."} />);
42
40
  });
43
41
 
44
42
  program
45
- .command("cook <text>")
46
- .alias("task")
43
+ .command("task <text>")
47
44
  .description("Apply a natural language change to the codebase")
48
45
  .option("-p, --path <path>", "Path to the repo", ".")
49
46
  .action((text: string, opts: { path: string }) => {
@@ -51,8 +48,7 @@ program
51
48
  });
52
49
 
53
50
  program
54
- .command("vibe")
55
- .alias("chat")
51
+ .command("chat")
56
52
  .description("Chat with your codebase — ask questions or make changes")
57
53
  .option("-p, --path <path>", "Path to the repo", ".")
58
54
  .action((opts: { path: string }) => {
@@ -60,8 +56,7 @@ program
60
56
  });
61
57
 
62
58
  program
63
- .command("history")
64
- .alias("timeline")
59
+ .command("timeline")
65
60
  .description(
66
61
  "Explore your code history — see commits, changes, and evolution",
67
62
  )
@@ -71,8 +66,7 @@ program
71
66
  });
72
67
 
73
68
  program
74
- .command("crimes [files...]")
75
- .alias("commit")
69
+ .command("commit [files...]")
76
70
  .description(
77
71
  "Generate a smart conventional commit message from staged changes or specific files",
78
72
  )
@@ -109,9 +103,10 @@ program
109
103
  );
110
104
 
111
105
  program
112
- .command("watch <cmd>")
113
- .alias("spy")
114
- .description("Watch a dev command and get AI suggestions for errors")
106
+ .command("run <cmd>")
107
+ .description(
108
+ "Run your dev server. Lens detects and fixes errors automatically",
109
+ )
115
110
  .option("-p, --path <path>", "Path to the repo", ".")
116
111
  .option("--clean", "Only show AI suggestions, hide raw logs")
117
112
  .option("--fix-all", "Auto-apply fixes as errors are detected")
@@ -129,7 +124,7 @@ program
129
124
  },
130
125
  ) => {
131
126
  render(
132
- <WatchCommand
127
+ <RunCommand
133
128
  cmd={cmd}
134
129
  path={opts.path}
135
130
  clean={opts.clean ?? false}
@@ -1,9 +1,3 @@
1
- //
2
-
3
- //
4
-
5
- //
6
-
7
1
  import type { Tool } from "@ridit/lens-sdk";
8
2
 
9
3
  type ChartType = "bar" | "line" | "sparkline";
@@ -13,9 +7,7 @@ interface ChartInput {
13
7
  title?: string;
14
8
  labels?: string[];
15
9
  values: number[];
16
- /** For line charts: height in rows. Default 10. */
17
10
  height?: number;
18
- /** Bar fill character. Default "█" */
19
11
  fill?: string;
20
12
  }
21
13
 
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;