@ridit/lens 0.3.2 → 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.2",
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",
@@ -144,7 +144,7 @@ Rules:
144
144
  - Skip bullets that just restate the subject line or describe trivial version bumps
145
145
  - Be specific — mention file names, feature names, component names
146
146
  - No markdown, no backticks, no code blocks
147
- - Output ONLY the commit message, nothing else
147
+ - Output ONLY the commit message, nothing else — no preamble, no explanation, no thinking
148
148
 
149
149
  Examples of good short commits:
150
150
  chore: bump version to 0.1.6
@@ -158,6 +158,13 @@ feat(chat): add persistent memory across sessions
158
158
  - inject memory summary into system prompt on load
159
159
  - expose /memory commands for manual management`;
160
160
 
161
+ function stripThinking(raw: string): string {
162
+ return raw
163
+ .replace(/<thinking>[\s\S]*?<\/thinking>/g, "")
164
+ .replace(/^[\s\n]+/, "")
165
+ .trim();
166
+ }
167
+
161
168
  async function generateCommitMessage(
162
169
  provider: Provider,
163
170
  diff: string,
@@ -170,7 +177,8 @@ async function generateCommitMessage(
170
177
  },
171
178
  ];
172
179
  const raw = await callChat(provider, SYSTEM_PROMPT, msgs);
173
- return typeof raw === "string" ? raw.trim() : "chore: update files";
180
+ if (typeof raw !== "string") return "chore: update files";
181
+ return stripThinking(raw) || "chore: update files";
174
182
  }
175
183
 
176
184
  function trunc(s: string, n: number) {
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import figures from "figures";
4
+ import path from "path";
5
+ import { existsSync } from "fs";
6
+ import { WatchRunner } from "../components/watch/WatchRunner";
7
+ import { RED } from "../colors";
8
+
9
+ interface Props {
10
+ cmd: string;
11
+ path: string;
12
+ clean: boolean;
13
+ fixAll: boolean;
14
+ autoRestart: boolean;
15
+ prompt?: string;
16
+ }
17
+
18
+ export function WatchCommand({
19
+ cmd,
20
+ path: inputPath,
21
+ clean,
22
+ fixAll,
23
+ autoRestart,
24
+ prompt,
25
+ }: Props) {
26
+ const repoPath = path.resolve(inputPath);
27
+
28
+ if (!cmd.trim()) {
29
+ return (
30
+ <Box marginTop={1}>
31
+ <Text color={RED}>{figures.cross} Usage: lens watch "bun dev"</Text>
32
+ </Box>
33
+ );
34
+ }
35
+
36
+ if (!existsSync(repoPath)) {
37
+ return (
38
+ <Box marginTop={1}>
39
+ <Text color={RED}>
40
+ {figures.cross} Path not found: {repoPath}
41
+ </Text>
42
+ </Box>
43
+ );
44
+ }
45
+
46
+ return (
47
+ <WatchRunner
48
+ cmd={cmd}
49
+ repoPath={repoPath}
50
+ clean={clean}
51
+ fixAll={fixAll}
52
+ autoRestart={autoRestart}
53
+ extraPrompt={prompt}
54
+ />
55
+ );
56
+ }
@@ -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">
@@ -1179,7 +1179,6 @@ export function TimelineRunner({
1179
1179
 
1180
1180
  return (
1181
1181
  <Box flexDirection="column">
1182
- {/* header */}
1183
1182
  <Box gap={2} marginBottom={1}>
1184
1183
  <Text color={ACCENT} bold>
1185
1184
  ◈ TIMELINE
@@ -1195,7 +1194,6 @@ export function TimelineRunner({
1195
1194
  )}
1196
1195
  </Box>
1197
1196
 
1198
- {/* status messages */}
1199
1197
  <Static items={statusMsgs}>
1200
1198
  {(msg) => (
1201
1199
  <Box key={msg.id} paddingX={1} gap={1}>
@@ -1205,7 +1203,6 @@ export function TimelineRunner({
1205
1203
  )}
1206
1204
  </Static>
1207
1205
 
1208
- {/* search bar */}
1209
1206
  {isSearching && (
1210
1207
  <Box gap={1} marginBottom={1}>
1211
1208
  <Text color={ACCENT}>{"/"}</Text>
@@ -1218,7 +1215,6 @@ export function TimelineRunner({
1218
1215
  </Box>
1219
1216
  )}
1220
1217
 
1221
- {/* commit list */}
1222
1218
  {visible.map((commit, i) => {
1223
1219
  const absIdx = scrollOffset + i;
1224
1220
  const isSel = absIdx === selectedIdx;
@@ -1251,7 +1247,6 @@ export function TimelineRunner({
1251
1247
  </Box>
1252
1248
  )}
1253
1249
 
1254
- {/* revert overlay */}
1255
1250
  {isReverting && mode.type === "revert" && (
1256
1251
  <RevertConfirm
1257
1252
  commit={mode.commit}
@@ -1268,7 +1263,6 @@ export function TimelineRunner({
1268
1263
  />
1269
1264
  )}
1270
1265
 
1271
- {/* ask panel */}
1272
1266
  {isAsking && provider && (
1273
1267
  <AskPanel
1274
1268
  commits={commits}
@@ -1281,7 +1275,6 @@ export function TimelineRunner({
1281
1275
  />
1282
1276
  )}
1283
1277
 
1284
- {/* shortcut bar */}
1285
1278
  <Box marginTop={1}>
1286
1279
  <Text color="gray" dimColor>
1287
1280
  {shortcutHint}