@ridit/lens 0.1.5 → 0.1.7

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.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Know Your Codebase.",
5
5
  "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
6
  "license": "MIT",
@@ -8,17 +8,16 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/ridit-jangra/Lens"
10
10
  },
11
- "main": "dist/index.cjs",
11
+ "main": "dist/index.mjs",
12
12
  "bin": {
13
- "lens": "./dist/index.cjs"
13
+ "lens": "./dist/index.mjs"
14
14
  },
15
15
  "scripts": {
16
- "build": "bun build src/index.tsx --target node --outfile dist/index.cjs",
17
- "postbuild": "node -e \"const fs=require('fs');const f='dist/index.cjs';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'))\"",
16
+ "build": "bun build src/index.tsx --target node --outfile dist/index.mjs",
17
+ "postbuild": "node -e \"const fs=require('fs');const f='dist/index.mjs';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'))\"",
18
18
  "prepublishOnly": "npm run build"
19
19
  },
20
20
  "dependencies": {
21
- "@ridit/lens": "^0.1.4",
22
21
  "chalk": "^5.6.2",
23
22
  "commander": "^14.0.3",
24
23
  "figures": "^6.1.0",
package/skills.json ADDED
@@ -0,0 +1,7 @@
1
+ [
2
+ {
3
+ "id": "1773310757090-o6sri",
4
+ "name": "this is a skill",
5
+ "description": "this is a skill"
6
+ }
7
+ ]
@@ -116,6 +116,12 @@ export function StaticMessage({ msg }: { msg: Message }) {
116
116
  shell: "$",
117
117
  fetch: "~>",
118
118
  "read-file": "r",
119
+ "read-folder": "d",
120
+ grep: "/",
121
+ "delete-file": "x",
122
+ "delete-folder": "X",
123
+ "open-url": "↗",
124
+ "generate-pdf": "P",
119
125
  "write-file": "w",
120
126
  search: "?",
121
127
  };
@@ -48,6 +48,30 @@ export function PermissionPrompt({
48
48
  icon = "r";
49
49
  label = "read";
50
50
  value = tool.filePath;
51
+ } else if (tool.type === "read-folder") {
52
+ icon = "d";
53
+ label = "folder";
54
+ value = tool.folderPath;
55
+ } else if (tool.type === "grep") {
56
+ icon = "/";
57
+ label = "grep";
58
+ value = `${tool.pattern} ${tool.glob}`;
59
+ } else if (tool.type === "delete-file") {
60
+ icon = "x";
61
+ label = "delete";
62
+ value = tool.filePath;
63
+ } else if (tool.type === "delete-folder") {
64
+ icon = "X";
65
+ label = "delete folder";
66
+ value = tool.folderPath;
67
+ } else if (tool.type === "open-url") {
68
+ icon = "↗";
69
+ label = "open";
70
+ value = tool.url;
71
+ } else if (tool.type === "generate-pdf") {
72
+ icon = "P";
73
+ label = "pdf";
74
+ value = tool.filePath;
51
75
  } else if (tool.type === "write-file") {
52
76
  icon = "w";
53
77
  label = "write";
@@ -128,12 +152,15 @@ export function TypewriterText({
128
152
  return <Text color={color}>{displayed}</Text>;
129
153
  }
130
154
 
131
- export function ShortcutBar() {
155
+ export function ShortcutBar({ autoApprove }: { autoApprove?: boolean }) {
132
156
  return (
133
157
  <Box gap={3} marginTop={0}>
134
158
  <Text color="gray" dimColor>
135
159
  enter send · ^v paste · ^c exit
136
160
  </Text>
161
+ <Text color={autoApprove ? "green" : "gray"} dimColor={!autoApprove}>
162
+ {autoApprove ? "⚡ auto" : "/auto"}
163
+ </Text>
137
164
  </Box>
138
165
  );
139
166
  }
@@ -20,6 +20,12 @@ import {
20
20
  runShell,
21
21
  fetchUrl,
22
22
  readFile,
23
+ readFolder,
24
+ grepFiles,
25
+ deleteFile,
26
+ deleteFolder,
27
+ openUrl,
28
+ generatePdf,
23
29
  writeFile,
24
30
  buildSystemPrompt,
25
31
  parseResponse,
@@ -55,6 +61,7 @@ const COMMANDS = [
55
61
  { cmd: "/timeline", desc: "browse commit history" },
56
62
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
57
63
  { cmd: "/review", desc: "review current codebsae" },
64
+ { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
58
65
  ];
59
66
 
60
67
  function CommandPalette({
@@ -98,6 +105,11 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
98
105
  const [clonedUrls, setClonedUrls] = useState<Set<string>>(new Set());
99
106
  const [showTimeline, setShowTimeline] = useState(false);
100
107
  const [showReview, setShowReview] = useState(false);
108
+ const [autoApprove, setAutoApprove] = useState(false);
109
+
110
+ // Cache of tool results within a single conversation turn to prevent
111
+ // the model from re-calling tools it already ran with the same args
112
+ const toolResultCache = useRef<Map<string, string>>(new Map());
101
113
 
102
114
  const inputBuffer = useRef("");
103
115
  const flushTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -169,7 +181,13 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
169
181
  parsed.kind === "shell" ||
170
182
  parsed.kind === "fetch" ||
171
183
  parsed.kind === "read-file" ||
184
+ parsed.kind === "read-folder" ||
185
+ parsed.kind === "grep" ||
172
186
  parsed.kind === "write-file" ||
187
+ parsed.kind === "delete-file" ||
188
+ parsed.kind === "delete-folder" ||
189
+ parsed.kind === "open-url" ||
190
+ parsed.kind === "generate-pdf" ||
173
191
  parsed.kind === "search"
174
192
  ) {
175
193
  let tool: Parameters<typeof PermissionPrompt>[0]["tool"];
@@ -179,6 +197,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
179
197
  tool = { type: "fetch", url: parsed.url };
180
198
  } else if (parsed.kind === "read-file") {
181
199
  tool = { type: "read-file", filePath: parsed.filePath };
200
+ } else if (parsed.kind === "read-folder") {
201
+ tool = { type: "read-folder", folderPath: parsed.folderPath };
202
+ } else if (parsed.kind === "grep") {
203
+ tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
204
+ } else if (parsed.kind === "delete-file") {
205
+ tool = { type: "delete-file", filePath: parsed.filePath };
206
+ } else if (parsed.kind === "delete-folder") {
207
+ tool = { type: "delete-folder", folderPath: parsed.folderPath };
208
+ } else if (parsed.kind === "open-url") {
209
+ tool = { type: "open-url", url: parsed.url };
210
+ } else if (parsed.kind === "generate-pdf") {
211
+ tool = {
212
+ type: "generate-pdf",
213
+ filePath: parsed.filePath,
214
+ content: parsed.pdfContent,
215
+ };
182
216
  } else if (parsed.kind === "search") {
183
217
  tool = { type: "search", query: parsed.query };
184
218
  } else {
@@ -199,13 +233,34 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
199
233
  setCommitted((prev) => [...prev, preambleMsg]);
200
234
  }
201
235
 
202
- setStage({
203
- type: "permission",
204
- tool,
205
- pendingMessages: currentAll,
206
- resolve: async (approved: boolean) => {
207
- let result = "(denied by user)";
208
- if (approved) {
236
+ // Safe tools that can be auto-approved (no side effects)
237
+ const isSafeTool =
238
+ parsed.kind === "read-file" ||
239
+ parsed.kind === "read-folder" ||
240
+ parsed.kind === "grep" ||
241
+ parsed.kind === "fetch" ||
242
+ parsed.kind === "open-url" ||
243
+ parsed.kind === "search";
244
+
245
+ const executeAndContinue = async (approved: boolean) => {
246
+ let result = "(denied by user)";
247
+ if (approved) {
248
+ // Build a cache key for idempotent read-only tools
249
+ const cacheKey =
250
+ parsed.kind === "read-file"
251
+ ? `read-file:${parsed.filePath}`
252
+ : parsed.kind === "read-folder"
253
+ ? `read-folder:${parsed.folderPath}`
254
+ : parsed.kind === "grep"
255
+ ? `grep:${parsed.pattern}:${parsed.glob}`
256
+ : null;
257
+
258
+ if (cacheKey && toolResultCache.current.has(cacheKey)) {
259
+ // Return cached result with a note so the model stops retrying
260
+ result =
261
+ toolResultCache.current.get(cacheKey)! +
262
+ "\n\n[NOTE: This result was already retrieved earlier. Do not request it again.]";
263
+ } else {
209
264
  try {
210
265
  setStage({ type: "thinking" });
211
266
  if (parsed.kind === "shell") {
@@ -214,6 +269,22 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
214
269
  result = await fetchUrl(parsed.url);
215
270
  } else if (parsed.kind === "read-file") {
216
271
  result = readFile(parsed.filePath, repoPath);
272
+ } else if (parsed.kind === "read-folder") {
273
+ result = readFolder(parsed.folderPath, repoPath);
274
+ } else if (parsed.kind === "grep") {
275
+ result = grepFiles(parsed.pattern, parsed.glob, repoPath);
276
+ } else if (parsed.kind === "delete-file") {
277
+ result = deleteFile(parsed.filePath, repoPath);
278
+ } else if (parsed.kind === "delete-folder") {
279
+ result = deleteFolder(parsed.folderPath, repoPath);
280
+ } else if (parsed.kind === "open-url") {
281
+ result = openUrl(parsed.url);
282
+ } else if (parsed.kind === "generate-pdf") {
283
+ result = generatePdf(
284
+ parsed.filePath,
285
+ parsed.pdfContent,
286
+ repoPath,
287
+ );
217
288
  } else if (parsed.kind === "write-file") {
218
289
  result = writeFile(
219
290
  parsed.filePath,
@@ -223,72 +294,130 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
223
294
  } else if (parsed.kind === "search") {
224
295
  result = await searchWeb(parsed.query);
225
296
  }
297
+ // Store result in cache for cacheable tools
298
+ if (cacheKey) {
299
+ toolResultCache.current.set(cacheKey, result);
300
+ }
226
301
  } catch (err: unknown) {
227
302
  result = `Error: ${err instanceof Error ? err.message : "failed"}`;
228
303
  }
229
304
  }
305
+ }
230
306
 
231
- if (approved && !result.startsWith("Error:")) {
232
- const kindMap = {
233
- shell: "shell-run",
234
- fetch: "url-fetched",
235
- "read-file": "file-read",
236
- "write-file": "file-written",
237
- search: "url-fetched",
238
- } as const;
239
- appendHistory({
240
- kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
241
- detail:
242
- parsed.kind === "shell"
243
- ? parsed.command
244
- : parsed.kind === "fetch"
245
- ? parsed.url
246
- : parsed.kind === "search"
247
- ? parsed.query
248
- : parsed.filePath,
249
- summary: result.split("\n")[0]?.slice(0, 120) ?? "",
250
- repoPath,
251
- });
252
- }
253
-
254
- const toolName =
255
- parsed.kind === "shell"
256
- ? "shell"
257
- : parsed.kind === "fetch"
258
- ? "fetch"
259
- : parsed.kind === "read-file"
260
- ? "read-file"
307
+ if (approved && !result.startsWith("Error:")) {
308
+ const kindMap = {
309
+ shell: "shell-run",
310
+ fetch: "url-fetched",
311
+ "read-file": "file-read",
312
+ "read-folder": "file-read",
313
+ grep: "file-read",
314
+ "delete-file": "file-written",
315
+ "delete-folder": "file-written",
316
+ "open-url": "url-fetched",
317
+ "generate-pdf": "file-written",
318
+ "write-file": "file-written",
319
+ search: "url-fetched",
320
+ } as const;
321
+ appendHistory({
322
+ kind: kindMap[parsed.kind as keyof typeof kindMap] ?? "shell-run",
323
+ detail:
324
+ parsed.kind === "shell"
325
+ ? parsed.command
326
+ : parsed.kind === "fetch"
327
+ ? parsed.url
261
328
  : parsed.kind === "search"
262
- ? "search"
263
- : "write-file";
264
-
265
- const toolContent =
266
- parsed.kind === "shell"
267
- ? parsed.command
268
- : parsed.kind === "fetch"
269
- ? parsed.url
270
- : parsed.kind === "search"
271
- ? parsed.query
272
- : parsed.filePath;
273
-
274
- const toolMsg: Message = {
275
- role: "assistant",
276
- type: "tool",
277
- toolName,
278
- content: toolContent,
279
- result,
280
- approved,
281
- };
329
+ ? parsed.query
330
+ : parsed.kind === "read-folder"
331
+ ? parsed.folderPath
332
+ : parsed.kind === "grep"
333
+ ? `${parsed.pattern} ${parsed.glob}`
334
+ : parsed.kind === "delete-file"
335
+ ? parsed.filePath
336
+ : parsed.kind === "delete-folder"
337
+ ? parsed.folderPath
338
+ : parsed.kind === "open-url"
339
+ ? parsed.url
340
+ : parsed.kind === "generate-pdf"
341
+ ? parsed.filePath
342
+ : parsed.filePath,
343
+ summary: result.split("\n")[0]?.slice(0, 120) ?? "",
344
+ repoPath,
345
+ });
346
+ }
282
347
 
283
- const withTool = [...currentAll, toolMsg];
284
- setAllMessages(withTool);
285
- setCommitted((prev) => [...prev, toolMsg]);
348
+ const toolName =
349
+ parsed.kind === "shell"
350
+ ? "shell"
351
+ : parsed.kind === "fetch"
352
+ ? "fetch"
353
+ : parsed.kind === "read-file"
354
+ ? "read-file"
355
+ : parsed.kind === "read-folder"
356
+ ? "read-folder"
357
+ : parsed.kind === "grep"
358
+ ? "grep"
359
+ : parsed.kind === "delete-file"
360
+ ? "delete-file"
361
+ : parsed.kind === "delete-folder"
362
+ ? "delete-folder"
363
+ : parsed.kind === "open-url"
364
+ ? "open-url"
365
+ : parsed.kind === "generate-pdf"
366
+ ? "generate-pdf"
367
+ : parsed.kind === "search"
368
+ ? "search"
369
+ : "write-file";
370
+
371
+ const toolContent =
372
+ parsed.kind === "shell"
373
+ ? parsed.command
374
+ : parsed.kind === "fetch"
375
+ ? parsed.url
376
+ : parsed.kind === "search"
377
+ ? parsed.query
378
+ : parsed.kind === "read-folder"
379
+ ? parsed.folderPath
380
+ : parsed.kind === "grep"
381
+ ? `${parsed.pattern} — ${parsed.glob}`
382
+ : parsed.kind === "delete-file"
383
+ ? parsed.filePath
384
+ : parsed.kind === "delete-folder"
385
+ ? parsed.folderPath
386
+ : parsed.kind === "open-url"
387
+ ? parsed.url
388
+ : parsed.kind === "generate-pdf"
389
+ ? parsed.filePath
390
+ : parsed.filePath;
391
+
392
+ const toolMsg: Message = {
393
+ role: "assistant",
394
+ type: "tool",
395
+ toolName,
396
+ content: toolContent,
397
+ result,
398
+ approved,
399
+ };
400
+
401
+ const withTool = [...currentAll, toolMsg];
402
+ setAllMessages(withTool);
403
+ setCommitted((prev) => [...prev, toolMsg]);
404
+
405
+ setStage({ type: "thinking" });
406
+ callChat(provider!, systemPrompt, withTool)
407
+ .then((r: string) => processResponse(r, withTool))
408
+ .catch(handleError(withTool));
409
+ };
410
+
411
+ if (autoApprove && isSafeTool) {
412
+ executeAndContinue(true);
413
+ return;
414
+ }
286
415
 
287
- setStage({ type: "thinking" });
288
- callChat(provider!, systemPrompt, withTool)
289
- .then((r: string) => processResponse(r, withTool))
290
- .catch(handleError(withTool));
291
- },
416
+ setStage({
417
+ type: "permission",
418
+ tool,
419
+ pendingMessages: currentAll,
420
+ resolve: executeAndContinue,
292
421
  });
293
422
  return;
294
423
  }
@@ -349,6 +478,21 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
349
478
  return;
350
479
  }
351
480
 
481
+ if (text.trim().toLowerCase() === "/auto") {
482
+ const next = !autoApprove;
483
+ setAutoApprove(next);
484
+ const msg: Message = {
485
+ role: "assistant",
486
+ content: next
487
+ ? "Auto-approve ON — read, search, grep and folder tools will run without asking. Write and code changes still require approval."
488
+ : "Auto-approve OFF — all tools will ask for permission.",
489
+ type: "text",
490
+ };
491
+ setCommitted((prev) => [...prev, msg]);
492
+ setAllMessages((prev) => [...prev, msg]);
493
+ return;
494
+ }
495
+
352
496
  if (text.trim().toLowerCase() === "/clear history") {
353
497
  clearRepoHistory(repoPath);
354
498
  const clearedMsg: Message = {
@@ -365,6 +509,7 @@ export const ChatRunner = ({ repoPath }: { repoPath: string }) => {
365
509
  const nextAll = [...allMessages, userMsg];
366
510
  setCommitted((prev) => [...prev, userMsg]);
367
511
  setAllMessages(nextAll);
512
+ toolResultCache.current.clear();
368
513
  setStage({ type: "thinking" });
369
514
  callChat(provider, systemPrompt, nextAll)
370
515
  .then((raw: string) => processResponse(raw, nextAll))
@@ -724,7 +869,7 @@ Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}`
724
869
  setInputValue("");
725
870
  }}
726
871
  />
727
- <ShortcutBar />
872
+ <ShortcutBar autoApprove={autoApprove} />
728
873
  </Box>
729
874
  )}
730
875
  </Box>
package/src/index.tsx CHANGED
@@ -1,60 +1,60 @@
1
- import React from "react";
2
- import { render } from "ink";
3
- import { Command } from "commander";
4
- import { RepoCommand } from "./commands/repo";
5
- import { InitCommand } from "./commands/provider";
6
- import { ReviewCommand } from "./commands/review";
7
- import { TaskCommand } from "./commands/task";
8
- import { ChatCommand } from "./commands/chat";
9
- import { TimelineCommand } from "./commands/timeline";
10
-
11
- const program = new Command();
12
-
13
- program
14
- .command("repo <url>")
15
- .description("Analyze a remote repository")
16
- .action((url) => {
17
- render(<RepoCommand url={url} />);
18
- });
19
-
20
- program
21
- .command("provider")
22
- .description("Configure AI providers")
23
- .action(() => {
24
- render(<InitCommand />);
25
- });
26
-
27
- program
28
- .command("review [path]")
29
- .description("Review a local codebase")
30
- .action((inputPath) => {
31
- render(<ReviewCommand path={inputPath ?? "."} />);
32
- });
33
-
34
- program
35
- .command("task <text>")
36
- .description("Apply a natural language change to the codebase")
37
- .option("-p, --path <path>", "Path to the repo", ".")
38
- .action((text: string, opts: { path: string }) => {
39
- render(<TaskCommand prompt={text} path={opts.path} />);
40
- });
41
-
42
- program
43
- .command("chat")
44
- .description("Chat with your codebase — ask questions or make changes")
45
- .option("-p, --path <path>", "Path to the repo", ".")
46
- .action((opts: { path: string }) => {
47
- render(<ChatCommand path={opts.path} />);
48
- });
49
-
50
- program
51
- .command("timeline")
52
- .description(
53
- "Explore your code history — see commits, changes, and evolution",
54
- )
55
- .option("-p, --path <path>", "Path to the repo", ".")
56
- .action((opts: { path: string }) => {
57
- render(<TimelineCommand path={opts.path} />);
58
- });
59
-
60
- program.parse(process.argv);
1
+ import React from "react";
2
+ import { render } from "ink";
3
+ import { Command } from "commander";
4
+ import { RepoCommand } from "./commands/repo";
5
+ import { InitCommand } from "./commands/provider";
6
+ import { ReviewCommand } from "./commands/review";
7
+ import { TaskCommand } from "./commands/task";
8
+ import { ChatCommand } from "./commands/chat";
9
+ import { TimelineCommand } from "./commands/timeline";
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .command("repo <url>")
15
+ .description("Analyze a remote repository")
16
+ .action((url) => {
17
+ render(<RepoCommand url={url} />);
18
+ });
19
+
20
+ program
21
+ .command("provider")
22
+ .description("Configure AI providers")
23
+ .action(() => {
24
+ render(<InitCommand />);
25
+ });
26
+
27
+ program
28
+ .command("review [path]")
29
+ .description("Review a local codebase")
30
+ .action((inputPath) => {
31
+ render(<ReviewCommand path={inputPath ?? "."} />);
32
+ });
33
+
34
+ program
35
+ .command("task <text>")
36
+ .description("Apply a natural language change to the codebase")
37
+ .option("-p, --path <path>", "Path to the repo", ".")
38
+ .action((text: string, opts: { path: string }) => {
39
+ render(<TaskCommand prompt={text} path={opts.path} />);
40
+ });
41
+
42
+ program
43
+ .command("chat")
44
+ .description("Chat with your codebase — ask questions or make changes")
45
+ .option("-p, --path <path>", "Path to the repo", ".")
46
+ .action((opts: { path: string }) => {
47
+ render(<ChatCommand path={opts.path} />);
48
+ });
49
+
50
+ program
51
+ .command("timeline")
52
+ .description(
53
+ "Explore your code history — see commits, changes, and evolution",
54
+ )
55
+ .option("-p, --path <path>", "Path to the repo", ".")
56
+ .action((opts: { path: string }) => {
57
+ render(<TimelineCommand path={opts.path} />);
58
+ });
59
+
60
+ program.parse(process.argv);
package/src/types/chat.ts CHANGED
@@ -6,7 +6,13 @@ export type ToolCall =
6
6
  | { type: "shell"; command: string }
7
7
  | { type: "fetch"; url: string }
8
8
  | { type: "read-file"; filePath: string }
9
+ | { type: "read-folder"; folderPath: string }
10
+ | { type: "grep"; pattern: string; glob: string }
9
11
  | { type: "write-file"; filePath: string; fileContent: string }
12
+ | { type: "delete-file"; filePath: string }
13
+ | { type: "delete-folder"; folderPath: string }
14
+ | { type: "open-url"; url: string }
15
+ | { type: "generate-pdf"; filePath: string; content: string }
10
16
  | { type: "search"; query: string };
11
17
 
12
18
  // ── Messages ──────────────────────────────────────────────────────────────────
@@ -16,7 +22,18 @@ export type Message =
16
22
  | {
17
23
  role: "assistant";
18
24
  type: "tool";
19
- toolName: "shell" | "fetch" | "read-file" | "write-file" | "search";
25
+ toolName:
26
+ | "shell"
27
+ | "fetch"
28
+ | "read-file"
29
+ | "read-folder"
30
+ | "grep"
31
+ | "write-file"
32
+ | "delete-file"
33
+ | "delete-folder"
34
+ | "open-url"
35
+ | "generate-pdf"
36
+ | "search";
20
37
  content: string;
21
38
  result: string;
22
39
  approved: boolean;