@ridit/lens 0.1.6 → 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/src/utils/chat.ts CHANGED
@@ -24,11 +24,11 @@ export function buildSystemPrompt(
24
24
  .map((f) => `### ${f.path}\n\`\`\`\n${f.content.slice(0, 2000)}\n\`\`\``)
25
25
  .join("\n\n");
26
26
 
27
- return `You are an expert software engineer assistant with access to the user's codebase and six tools.
27
+ return `You are an expert software engineer assistant with access to the user's codebase and eleven tools.
28
28
 
29
29
  ## TOOLS
30
30
 
31
- You have exactly six tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
31
+ You have exactly eleven tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
32
32
 
33
33
  ### 1. fetch — load a URL
34
34
  <fetch>https://example.com</fetch>
@@ -39,18 +39,40 @@ You have exactly six tools. To use a tool you MUST wrap it in the exact XML tags
39
39
  ### 3. read-file — read a file from the repo
40
40
  <read-file>src/foo.ts</read-file>
41
41
 
42
- ### 4. write-filecreate or overwrite a file
42
+ ### 4. read-folderlist contents of a folder (files + subfolders, one level deep)
43
+ <read-folder>src/components</read-folder>
44
+
45
+ ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
46
+ <grep>
47
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
48
+ </grep>
49
+
50
+ ### 6. write-file — create or overwrite a file
43
51
  <write-file>
44
52
  {"path": "data/output.csv", "content": "col1,col2\nval1,val2"}
45
53
  </write-file>
46
54
 
47
- ### 5. searchsearch the internet for anything you are unsure about
55
+ ### 7. delete-filepermanently delete a single file
56
+ <delete-file>src/old-component.tsx</delete-file>
57
+
58
+ ### 8. delete-folder — permanently delete a folder and all its contents
59
+ <delete-folder>src/legacy</delete-folder>
60
+
61
+ ### 9. open-url — open a URL in the user's default browser
62
+ <open-url>https://github.com/owner/repo</open-url>
63
+
64
+ ### 10. generate-pdf — generate a PDF file from markdown-style content
65
+ <generate-pdf>
66
+ {"path": "output/report.pdf", "content": "# Title\n\nSome body text.\n\n## Section\n\nMore content."}
67
+ </generate-pdf>
68
+
69
+ ### 11. search — search the internet for anything you are unsure about
48
70
  <search>how to use React useEffect cleanup function</search>
49
71
 
50
- ### 6. clone — clone a GitHub repo so you can explore and discuss it
72
+ ### 11. clone — clone a GitHub repo so you can explore and discuss it
51
73
  <clone>https://github.com/owner/repo</clone>
52
74
 
53
- ### 7. changes — propose code edits (shown as a diff for user approval)
75
+ ### 12. changes — propose code edits (shown as a diff for user approval)
54
76
  <changes>
55
77
  {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
56
78
  </changes>
@@ -63,16 +85,47 @@ You have exactly six tools. To use a tool you MUST wrap it in the exact XML tags
63
85
  4. NEVER print a URL, command, filename, or JSON blob as plain text when you should be using a tool
64
86
  5. NEVER say "I'll fetch" / "run this command" / "here's the write-file" — just emit the tag
65
87
  6. NEVER use shell to run git clone — always use the clone tag instead
66
- 7. write-file content field must be the COMPLETE file content, never empty or placeholder
67
- 8. After a write-file succeeds, do NOT repeat it trust the result and move on
68
- 9. After a write-file succeeds, use read-file to verify the content before telling the user it is done
69
- 10. NEVER apologize and redo a tool call you already madeif write-file or shell ran and returned a result, it worked, do not run it again
70
- 11. NEVER say "I made a mistake" and repeat the same tool one attempt is enough, trust the output
71
- 12. NEVER second-guess yourself mid-responsecommit to your answer
72
- 13. Every shell command runs from the repo root — \`cd\` has NO persistent effect. NEVER use \`cd\` alone. Use full paths or combine with && e.g. \`cd list && bun run index.ts\`
73
- 14. write-file paths are relative to the repo root — if creating files in a subfolder write the full relative path e.g. \`list/src/index.tsx\` NOT \`src/index.tsx\`
74
- 15. When scaffolding a new project in a subfolder, ALL write-file paths must start with that subfolder name e.g. \`list/package.json\`, \`list/src/index.tsx\`
75
- 16. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set bun needs this to parse JSX
88
+ 7. NEVER use shell to list files or folders (no ls, dir, find, git ls-files, tree) — ALWAYS use read-folder instead
89
+ 8. NEVER use shell to read a file (no cat, type, Get-Content)ALWAYS use read-file instead
90
+ 9. NEVER use shell grep, findstr, or Select-String to search file contents ALWAYS use grep instead
91
+ 10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
92
+ 11. write-file content field must be the COMPLETE file content, never empty or placeholder
93
+ 12. After a write-file succeeds, do NOT repeat it trust the result and move on
94
+ 13. After a write-file succeeds, use read-file to verify the content before telling the user it is done
95
+ 14. NEVER apologize and redo a tool call you already made — if write-file or shell ran and returned a result, it worked, do not run it again
96
+ 15. NEVER say "I made a mistake" and repeat the same tool one attempt is enough, trust the output
97
+ 16. NEVER second-guess yourself mid-responsecommit to your answer
98
+ 17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
99
+ 18. If you have already retrieved a result for a path in this conversation, do NOT request it again — use the result you already have
100
+ 17. Every shell command runs from the repo root — \`cd\` has NO persistent effect. NEVER use \`cd\` alone. Use full paths or combine with && e.g. \`cd list && bun run index.ts\`
101
+ 18. write-file paths are relative to the repo root — if creating files in a subfolder write the full relative path e.g. \`list/src/index.tsx\` NOT \`src/index.tsx\`
102
+ 19. When scaffolding a new project in a subfolder, ALL write-file paths must start with that subfolder name e.g. \`list/package.json\`, \`list/src/index.tsx\`
103
+ 20. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
104
+
105
+ ## CRITICAL: READ BEFORE YOU WRITE
106
+
107
+ These rules are mandatory whenever you plan to edit or create a file:
108
+
109
+ ### Before modifying ANY existing file:
110
+ 1. ALWAYS use read-file on the exact file you plan to change FIRST
111
+ 2. Study the full current content — understand every import, every export, every type, every existing feature
112
+ 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
113
+ 4. If you are unsure what other files import from the file you are editing, use read-folder on the parent directory first to see what exists nearby, then read-file the relevant ones
114
+
115
+ ### Before adding a feature that touches multiple files:
116
+ 1. Use read-folder on the relevant directory to see what files exist
117
+ 2. Use read-file on each file you plan to touch
118
+ 3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
119
+
120
+ ### The golden rule for write-file and changes:
121
+ - The output file must contain EVERYTHING the original had, PLUS your new additions
122
+ - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
123
+ - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
124
+
125
+ ## WHEN TO USE read-folder:
126
+ - Before editing files in an unfamiliar directory — list it first to understand the structure
127
+ - When a feature spans multiple files and you are not sure what exists
128
+ - When the user asks you to explore or explain a part of the codebase
76
129
 
77
130
  ## SCAFFOLDING A NEW PROJECT (follow this exactly)
78
131
 
@@ -96,14 +149,28 @@ When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app
96
149
 
97
150
  - User shares any URL → fetch it immediately
98
151
  - User asks to run anything → shell it immediately
99
- - User asks to read a fileread-file it immediately
152
+ - User asks to open a link, open a URL, or visit a website open-url it immediately, do NOT use fetch
153
+ - User asks to delete a file → delete-file it immediately (requires approval)
154
+ - User asks to delete a folder or directory → delete-folder it immediately (requires approval)
155
+ - User asks to search for a pattern in files, find usages, find where something is defined → grep it immediately, NEVER use shell grep/findstr/Select-String
156
+ - User asks to read a file → read-file it immediately, NEVER use shell cat/type
157
+ - User asks what files are in a folder, or to explore/list a directory → read-folder it immediately, NEVER use shell ls/dir/find/git ls-files
158
+ - User asks to explore a folder or directory → read-folder it immediately
100
159
  - User asks to save/create/write a file → write-file it immediately, then read-file to verify
160
+ - User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
101
161
  - User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
102
162
  - After clone succeeds, you will see context about the clone in the conversation. Wait for the user to ask a specific question before using any tools. Do NOT auto-read files, do NOT emit any tool tags until the user asks.
103
163
  - You are unsure about an API, library, error, concept, or piece of code → search it immediately
104
164
  - User asks about something recent or that you might not know → search it immediately
105
165
  - You are about to say "I'm not sure" or "I don't know" → search instead of guessing
106
166
 
167
+ ## shell IS ONLY FOR:
168
+ - Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
169
+ - Installing packages: \`npm install\`, \`pip install\`
170
+ - Building/testing: \`npm run build\`, \`bun test\`
171
+ - Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
172
+ - Anything that EXECUTES — not reads or lists
173
+
107
174
  ## CODEBASE
108
175
 
109
176
  ${fileList.length > 0 ? fileList : "(no files indexed)"}
@@ -114,6 +181,240 @@ ${historySummary}`;
114
181
  // ── Few-shot examples ─────────────────────────────────────────────────────────
115
182
 
116
183
  export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
184
+ // read-folder examples FIRST — highest priority pattern to establish
185
+ {
186
+ role: "user",
187
+ content: "delete src/old-component.tsx",
188
+ },
189
+ {
190
+ role: "assistant",
191
+ content: "<delete-file>src/old-component.tsx</delete-file>",
192
+ },
193
+ {
194
+ role: "user",
195
+ content:
196
+ "Here is the output from delete-file of src/old-component.tsx:\n\nDeleted: /repo/src/old-component.tsx\n\nPlease continue your response based on this output.",
197
+ },
198
+ {
199
+ role: "assistant",
200
+ content: "Done — `src/old-component.tsx` has been deleted.",
201
+ },
202
+ {
203
+ role: "user",
204
+ content: "delete the legacy folder",
205
+ },
206
+ {
207
+ role: "assistant",
208
+ content: "<delete-folder>src/legacy</delete-folder>",
209
+ },
210
+ {
211
+ role: "user",
212
+ content:
213
+ "Here is the output from delete-folder of src/legacy:\n\nDeleted folder: /repo/src/legacy\n\nPlease continue your response based on this output.",
214
+ },
215
+ {
216
+ role: "assistant",
217
+ content:
218
+ "Done — the `src/legacy` folder and all its contents have been deleted.",
219
+ },
220
+ {
221
+ role: "user",
222
+ content: "open https://github.com/microsoft/typescript",
223
+ },
224
+ {
225
+ role: "assistant",
226
+ content: "<open-url>https://github.com/microsoft/typescript</open-url>",
227
+ },
228
+ {
229
+ role: "user",
230
+ content:
231
+ "Here is the output from open-url https://github.com/microsoft/typescript:\n\nOpened: https://github.com/microsoft/typescript\n\nPlease continue your response based on this output.",
232
+ },
233
+ {
234
+ role: "assistant",
235
+ content: "Opened the TypeScript GitHub page in your browser.",
236
+ },
237
+ {
238
+ role: "user",
239
+ content:
240
+ "generate a PDF report about the project and save it to docs/report.pdf",
241
+ },
242
+ {
243
+ role: "assistant",
244
+ content:
245
+ '<generate-pdf>\n{"path": "docs/report.pdf", "content": "# Project Report\\n\\n## Overview\\n\\nThis document summarizes the project.\\n\\n## Details\\n\\nMore content here."}\n</generate-pdf>',
246
+ },
247
+ {
248
+ role: "user",
249
+ content:
250
+ "Here is the output from generate-pdf to docs/report.pdf:\n\nPDF generated: /repo/docs/report.pdf\n\nPlease continue your response based on this output.",
251
+ },
252
+ {
253
+ role: "assistant",
254
+ content: "Done — the PDF report has been saved to `docs/report.pdf`.",
255
+ },
256
+ {
257
+ role: "user",
258
+ content: 'grep -R "ChatRunner" -n src',
259
+ },
260
+ {
261
+ role: "assistant",
262
+ content: '<grep>\n{"pattern": "ChatRunner", "glob": "src/**/*"}\n</grep>',
263
+ },
264
+ {
265
+ role: "user",
266
+ content:
267
+ 'Here is the output from grep for "ChatRunner":\n\ngrep /ChatRunner/ src/**/* — 3 match(es) in 2 file(s)\n\nsrc/index.tsx\n 12: import { ChatRunner } from "./components/chat/ChatRunner";\n\nsrc/components/chat/ChatRunner.tsx\n 1: export const ChatRunner = ...\n\nPlease continue your response based on this output.',
268
+ },
269
+ {
270
+ role: "assistant",
271
+ content:
272
+ "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`.",
273
+ },
274
+ {
275
+ role: "user",
276
+ content: "find all usages of useInput in the codebase",
277
+ },
278
+ {
279
+ role: "assistant",
280
+ content: '<grep>\n{"pattern": "useInput", "glob": "src/**/*.tsx"}\n</grep>',
281
+ },
282
+ {
283
+ role: "user",
284
+ content:
285
+ 'Here is the output from grep for "useInput":\n\ngrep /useInput/ src/**/*.tsx — 2 match(es) in 1 file(s)\n\nsrc/components/chat/ChatRunner.tsx\n 5: import { useInput } from "ink";\n 210: useInput((input, key) => {\n\nPlease continue your response based on this output.',
286
+ },
287
+ {
288
+ role: "assistant",
289
+ content:
290
+ "`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210.",
291
+ },
292
+ {
293
+ role: "user",
294
+ content: "read src folder",
295
+ },
296
+ {
297
+ role: "assistant",
298
+ content: "<read-folder>src</read-folder>",
299
+ },
300
+ {
301
+ role: "user",
302
+ content:
303
+ "Here is the output from read-folder of src:\n\nFolder: src (4 entries)\n\nFiles:\n index.ts\n App.tsx\n\nSubfolders:\n components/\n utils/\n\nPlease continue your response based on this output.",
304
+ },
305
+ {
306
+ role: "assistant",
307
+ content:
308
+ "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`.",
309
+ },
310
+ {
311
+ role: "user",
312
+ content: "list src folder",
313
+ },
314
+ {
315
+ role: "assistant",
316
+ content: "<read-folder>src</read-folder>",
317
+ },
318
+ {
319
+ role: "user",
320
+ content:
321
+ "Here is the output from read-folder of src:\n\nFolder: src (4 entries)\n\nFiles:\n index.ts\n App.tsx\n\nSubfolders:\n components/\n utils/\n\nPlease continue your response based on this output.",
322
+ },
323
+ {
324
+ role: "assistant",
325
+ content:
326
+ "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`.",
327
+ },
328
+ {
329
+ role: "user",
330
+ content: "what files are in src/components?",
331
+ },
332
+ {
333
+ role: "assistant",
334
+ content: "<read-folder>src/components</read-folder>",
335
+ },
336
+ {
337
+ role: "user",
338
+ content:
339
+ "Here is the output from read-folder of src/components:\n\nFolder: src/components (5 entries)\n\nFiles:\n Header.tsx\n Footer.tsx\n Button.tsx\n\nSubfolders:\n ui/\n forms/\n\nPlease continue your response based on this output.",
340
+ },
341
+ {
342
+ role: "assistant",
343
+ content:
344
+ "The `src/components` folder has 3 files — `Header.tsx`, `Footer.tsx`, `Button.tsx` — plus two subfolders: `ui/` and `forms/`.",
345
+ },
346
+ {
347
+ role: "user",
348
+ content: "list the files in src/utils",
349
+ },
350
+ {
351
+ role: "assistant",
352
+ content: "<read-folder>src/utils</read-folder>",
353
+ },
354
+ {
355
+ role: "user",
356
+ content:
357
+ "Here is the output from read-folder of src/utils:\n\nFolder: src/utils (3 entries)\n\nFiles:\n api.ts\n helpers.ts\n format.ts\n\nPlease continue your response based on this output.",
358
+ },
359
+ {
360
+ role: "assistant",
361
+ content:
362
+ "The `src/utils` folder contains 3 files: `api.ts`, `helpers.ts`, and `format.ts`.",
363
+ },
364
+ {
365
+ role: "user",
366
+ content: "show me what's in the src directory",
367
+ },
368
+ {
369
+ role: "assistant",
370
+ content: "<read-folder>src</read-folder>",
371
+ },
372
+ {
373
+ role: "user",
374
+ content:
375
+ "Here is the output from read-folder of src:\n\nFolder: src (4 entries)\n\nFiles:\n index.ts\n App.tsx\n\nSubfolders:\n components/\n utils/\n\nPlease continue your response based on this output.",
376
+ },
377
+ {
378
+ role: "assistant",
379
+ content:
380
+ "The `src` directory has 2 files (`index.ts`, `App.tsx`) and 2 subfolders (`components/`, `utils/`).",
381
+ },
382
+ {
383
+ role: "user",
384
+ content: "show me the project structure",
385
+ },
386
+ {
387
+ role: "assistant",
388
+ content: "<read-folder>.</read-folder>",
389
+ },
390
+ {
391
+ role: "user",
392
+ content:
393
+ "Here is the output from read-folder of .:\n\nFolder: . (5 entries)\n\nFiles:\n package.json\n tsconfig.json\n README.md\n\nSubfolders:\n src/\n node_modules/\n\nPlease continue your response based on this output.",
394
+ },
395
+ {
396
+ role: "assistant",
397
+ content:
398
+ "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code.",
399
+ },
400
+ {
401
+ role: "user",
402
+ content: "what does the project look like?",
403
+ },
404
+ {
405
+ role: "assistant",
406
+ content: "<read-folder>.</read-folder>",
407
+ },
408
+ {
409
+ role: "user",
410
+ content:
411
+ "Here is the output from read-folder of .:\n\nFolder: . (4 entries)\n\nFiles:\n package.json\n README.md\n\nSubfolders:\n src/\n dist/\n\nPlease continue your response based on this output.",
412
+ },
413
+ {
414
+ role: "assistant",
415
+ content:
416
+ "The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`.",
417
+ },
117
418
  {
118
419
  role: "user",
119
420
  content: "fetch https://api.github.com/repos/microsoft/typescript",
@@ -159,6 +460,24 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
159
460
  role: "assistant",
160
461
  content: "Done — saved and verified `ts-info.json`. Data looks correct.",
161
462
  },
463
+ {
464
+ role: "user",
465
+ content: "add a logout button to src/components/Header.tsx",
466
+ },
467
+ {
468
+ role: "assistant",
469
+ content: "<read-file>src/components/Header.tsx</read-file>",
470
+ },
471
+ {
472
+ role: "user",
473
+ content:
474
+ "Here is the output from read-file of src/components/Header.tsx:\n\nFile: src/components/Header.tsx (42 lines)\n\nimport React from 'react';\n// ... full file content ...\n\nPlease continue your response based on this output.",
475
+ },
476
+ {
477
+ role: "assistant",
478
+ content:
479
+ '<changes>\n{"summary": "Add logout button to Header — preserves all existing nav items and imports", "patches": [{"path": "src/components/Header.tsx", "content": "// complete file with logout button added", "isNew": false}]}\n</changes>',
480
+ },
162
481
  {
163
482
  role: "user",
164
483
  content: "what node version am I on",
@@ -222,6 +541,17 @@ export type ParsedResponse =
222
541
  | { kind: "shell"; content: string; command: string }
223
542
  | { kind: "fetch"; content: string; url: string }
224
543
  | { kind: "read-file"; content: string; filePath: string }
544
+ | { kind: "read-folder"; content: string; folderPath: string }
545
+ | { kind: "grep"; content: string; pattern: string; glob: string }
546
+ | { kind: "delete-file"; content: string; filePath: string }
547
+ | { kind: "delete-folder"; content: string; folderPath: string }
548
+ | { kind: "open-url"; content: string; url: string }
549
+ | {
550
+ kind: "generate-pdf";
551
+ content: string;
552
+ filePath: string;
553
+ pdfContent: string;
554
+ }
225
555
  | {
226
556
  kind: "write-file";
227
557
  content: string;
@@ -239,6 +569,12 @@ export function parseResponse(text: string): ParsedResponse {
239
569
  | "shell"
240
570
  | "fetch"
241
571
  | "read-file"
572
+ | "read-folder"
573
+ | "grep"
574
+ | "delete-file"
575
+ | "delete-folder"
576
+ | "open-url"
577
+ | "generate-pdf"
242
578
  | "write-file"
243
579
  | "search"
244
580
  | "clone";
@@ -250,6 +586,15 @@ export function parseResponse(text: string): ParsedResponse {
250
586
  { kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
251
587
  { kind: "shell", re: /<shell>([\s\S]*?)<\/shell>/g },
252
588
  { kind: "read-file", re: /<read-file>([\s\S]*?)<\/read-file>/g },
589
+ { kind: "read-folder", re: /<read-folder>([\s\S]*?)<\/read-folder>/g },
590
+ { kind: "grep", re: /<grep>([\s\S]*?)<\/grep>/g },
591
+ { kind: "delete-file", re: /<delete-file>([\s\S]*?)<\/delete-file>/g },
592
+ {
593
+ kind: "delete-folder",
594
+ re: /<delete-folder>([\s\S]*?)<\/delete-folder>/g,
595
+ },
596
+ { kind: "open-url", re: /<open-url>([\s\S]*?)<\/open-url>/g },
597
+ { kind: "generate-pdf", re: /<generate-pdf>([\s\S]*?)<\/generate-pdf>/g },
253
598
  { kind: "write-file", re: /<write-file>([\s\S]*?)<\/write-file>/g },
254
599
  { kind: "search", re: /<search>([\s\S]*?)<\/search>/g },
255
600
  { kind: "clone", re: /<clone>([\s\S]*?)<\/clone>/g },
@@ -257,6 +602,7 @@ export function parseResponse(text: string): ParsedResponse {
257
602
  { kind: "fetch", re: /```fetch\r?\n([\s\S]*?)\r?\n```/g },
258
603
  { kind: "shell", re: /```shell\r?\n([\s\S]*?)\r?\n```/g },
259
604
  { kind: "read-file", re: /```read-file\r?\n([\s\S]*?)\r?\n```/g },
605
+ { kind: "read-folder", re: /```read-folder\r?\n([\s\S]*?)\r?\n```/g },
260
606
  { kind: "write-file", re: /```write-file\r?\n([\s\S]*?)\r?\n```/g },
261
607
  { kind: "search", re: /```search\r?\n([\s\S]*?)\r?\n```/g },
262
608
  { kind: "changes", re: /```changes\r?\n([\s\S]*?)\r?\n```/g },
@@ -272,7 +618,14 @@ export function parseResponse(text: string): ParsedResponse {
272
618
 
273
619
  candidates.sort((a, b) => a.index - b.index);
274
620
  const { kind, match } = candidates[0]!;
275
- const before = text.slice(0, match.index).trim();
621
+ // Strip any leaked tool tags from preamble (e.g. model emits tag twice or mid-sentence)
622
+ const before = text
623
+ .slice(0, match.index)
624
+ .replace(
625
+ /<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g,
626
+ "",
627
+ )
628
+ .trim();
276
629
  const body = match[1]!.trim();
277
630
 
278
631
  if (kind === "changes") {
@@ -299,6 +652,49 @@ export function parseResponse(text: string): ParsedResponse {
299
652
  if (kind === "read-file")
300
653
  return { kind: "read-file", content: before, filePath: body };
301
654
 
655
+ if (kind === "read-folder")
656
+ return { kind: "read-folder", content: before, folderPath: body };
657
+
658
+ if (kind === "delete-file")
659
+ return { kind: "delete-file", content: before, filePath: body };
660
+
661
+ if (kind === "delete-folder")
662
+ return { kind: "delete-folder", content: before, folderPath: body };
663
+
664
+ if (kind === "open-url") {
665
+ const url = body.replace(/^<|>$/g, "").trim();
666
+ return { kind: "open-url", content: before, url };
667
+ }
668
+
669
+ if (kind === "generate-pdf") {
670
+ try {
671
+ const parsed = JSON.parse(body);
672
+ return {
673
+ kind: "generate-pdf",
674
+ content: before,
675
+ filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
676
+ pdfContent: parsed.content ?? "",
677
+ };
678
+ } catch {
679
+ return { kind: "text", content: text };
680
+ }
681
+ }
682
+
683
+ if (kind === "grep") {
684
+ try {
685
+ const parsed = JSON.parse(body) as { pattern: string; glob?: string };
686
+ return {
687
+ kind: "grep",
688
+ content: before,
689
+ pattern: parsed.pattern,
690
+ glob: parsed.glob ?? "**/*",
691
+ };
692
+ } catch {
693
+ // treat body as plain pattern with no glob
694
+ return { kind: "grep", content: before, pattern: body, glob: "**/*" };
695
+ }
696
+ }
697
+
302
698
  if (kind === "write-file") {
303
699
  try {
304
700
  const parsed = JSON.parse(body) as { path: string; content: string };
@@ -364,9 +760,21 @@ function buildApiMessages(
364
760
  ? `fetch of ${m.content}`
365
761
  : m.toolName === "read-file"
366
762
  ? `read-file of ${m.content}`
367
- : m.toolName === "search"
368
- ? `web search for "${m.content}"`
369
- : `write-file to ${m.content}`;
763
+ : m.toolName === "read-folder"
764
+ ? `read-folder of ${m.content}`
765
+ : m.toolName === "grep"
766
+ ? `grep for "${m.content}"`
767
+ : m.toolName === "delete-file"
768
+ ? `delete-file of ${m.content}`
769
+ : m.toolName === "delete-folder"
770
+ ? `delete-folder of ${m.content}`
771
+ : m.toolName === "open-url"
772
+ ? `open-url ${m.content}`
773
+ : m.toolName === "generate-pdf"
774
+ ? `generate-pdf to ${m.content}`
775
+ : m.toolName === "search"
776
+ ? `web search for "${m.content}"`
777
+ : `write-file to ${m.content}`;
370
778
  return {
371
779
  role: "user",
372
780
  content: `Here is the output from the ${label}:\n\n${m.result}\n\nPlease continue your response based on this output.`,
@@ -527,9 +935,6 @@ export function applyPatches(repoPath: string, patches: FilePatch[]): void {
527
935
 
528
936
  export async function runShell(command: string, cwd: string): Promise<string> {
529
937
  return new Promise((resolve) => {
530
- // Use spawn so we can stream stdout+stderr and impose no hard cap on
531
- // long-running commands (pip install, python scripts, git commit, etc.)
532
- // We still cap at 5 minutes to avoid hanging forever.
533
938
  const { spawn } =
534
939
  require("child_process") as typeof import("child_process");
535
940
  const isWin = process.platform === "win32";
@@ -813,6 +1218,162 @@ export function readFile(filePath: string, repoPath: string): string {
813
1218
  return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
814
1219
  }
815
1220
 
1221
+ export function readFolder(folderPath: string, repoPath: string): string {
1222
+ // Strip any command prefixes the model may have included (e.g. "ls src" → "src", "read src" → "src")
1223
+ const sanitized = folderPath
1224
+ .replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "")
1225
+ .trim();
1226
+
1227
+ const candidates = path.isAbsolute(sanitized)
1228
+ ? [sanitized]
1229
+ : [sanitized, path.join(repoPath, sanitized)];
1230
+
1231
+ for (const candidate of candidates) {
1232
+ if (!existsSync(candidate)) continue;
1233
+ let stat: ReturnType<typeof statSync>;
1234
+ try {
1235
+ stat = statSync(candidate);
1236
+ } catch {
1237
+ continue;
1238
+ }
1239
+ if (!stat.isDirectory()) {
1240
+ return `Not a directory: ${candidate}. Use read-file to read a file.`;
1241
+ }
1242
+
1243
+ let entries: string[];
1244
+ try {
1245
+ entries = readdirSync(candidate, { encoding: "utf-8" });
1246
+ } catch (err) {
1247
+ return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
1248
+ }
1249
+
1250
+ const files: string[] = [];
1251
+ const subfolders: string[] = [];
1252
+
1253
+ for (const entry of entries) {
1254
+ if (entry.startsWith(".") && entry !== ".env") continue; // skip hidden except .env hint
1255
+ const full = path.join(candidate, entry);
1256
+ try {
1257
+ if (statSync(full).isDirectory()) {
1258
+ subfolders.push(`${entry}/`);
1259
+ } else {
1260
+ files.push(entry);
1261
+ }
1262
+ } catch {
1263
+ // skip unreadable entries
1264
+ }
1265
+ }
1266
+
1267
+ const total = files.length + subfolders.length;
1268
+ const lines: string[] = [`Folder: ${candidate} (${total} entries)`, ""];
1269
+
1270
+ if (files.length > 0) {
1271
+ lines.push("Files:");
1272
+ files.forEach((f) => lines.push(` ${f}`));
1273
+ }
1274
+
1275
+ if (subfolders.length > 0) {
1276
+ if (files.length > 0) lines.push("");
1277
+ lines.push("Subfolders:");
1278
+ subfolders.forEach((d) => lines.push(` ${d}`));
1279
+ }
1280
+
1281
+ if (total === 0) {
1282
+ lines.push("(empty folder)");
1283
+ }
1284
+
1285
+ return lines.join("\n");
1286
+ }
1287
+
1288
+ return `Folder not found: ${sanitized}`;
1289
+ }
1290
+
1291
+ export function grepFiles(
1292
+ pattern: string,
1293
+ glob: string,
1294
+ repoPath: string,
1295
+ ): string {
1296
+ let regex: RegExp;
1297
+ try {
1298
+ regex = new RegExp(pattern, "i");
1299
+ } catch {
1300
+ // fall back to literal string match if pattern is not valid regex
1301
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
1302
+ }
1303
+
1304
+ // Convert glob to a simple path prefix/suffix filter
1305
+ // Supports patterns like: src/**/*.tsx, **/*.ts, src/utils/*
1306
+ const globToFilter = (g: string): ((rel: string) => boolean) => {
1307
+ // strip leading **/
1308
+ const cleaned = g.replace(/^\*\*\//, "");
1309
+ const parts = cleaned.split("/");
1310
+ const ext = parts[parts.length - 1];
1311
+ const prefix = parts.slice(0, -1).join("/");
1312
+
1313
+ return (rel: string) => {
1314
+ // extension match (e.g. *.tsx)
1315
+ if (ext?.startsWith("*.")) {
1316
+ const extSuffix = ext.slice(1); // e.g. .tsx
1317
+ if (!rel.endsWith(extSuffix)) return false;
1318
+ } else if (ext && !ext.includes("*")) {
1319
+ // exact filename
1320
+ if (!rel.endsWith(ext)) return false;
1321
+ }
1322
+ // prefix match
1323
+ if (prefix && !prefix.includes("*")) {
1324
+ if (!rel.startsWith(prefix)) return false;
1325
+ }
1326
+ return true;
1327
+ };
1328
+ };
1329
+
1330
+ const filter = globToFilter(glob);
1331
+ const allFiles = walkDir(repoPath);
1332
+ const matchedFiles = allFiles.filter(filter);
1333
+
1334
+ if (matchedFiles.length === 0) {
1335
+ return `No files matched glob: ${glob}`;
1336
+ }
1337
+
1338
+ const results: string[] = [];
1339
+ let totalMatches = 0;
1340
+
1341
+ for (const relPath of matchedFiles) {
1342
+ const fullPath = path.join(repoPath, relPath);
1343
+ let content: string;
1344
+ try {
1345
+ content = readFileSync(fullPath, "utf-8");
1346
+ } catch {
1347
+ continue;
1348
+ }
1349
+
1350
+ const lines = content.split("\n");
1351
+ const fileMatches: string[] = [];
1352
+
1353
+ lines.forEach((line, i) => {
1354
+ if (regex.test(line)) {
1355
+ fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
1356
+ totalMatches++;
1357
+ }
1358
+ });
1359
+
1360
+ if (fileMatches.length > 0) {
1361
+ results.push(`${relPath}\n${fileMatches.join("\n")}`);
1362
+ }
1363
+
1364
+ if (totalMatches >= 200) {
1365
+ results.push("(truncated — too many matches)");
1366
+ break;
1367
+ }
1368
+ }
1369
+
1370
+ if (results.length === 0) {
1371
+ return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
1372
+ }
1373
+
1374
+ return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)\n\n${results.join("\n\n")}`;
1375
+ }
1376
+
816
1377
  export function writeFile(
817
1378
  filePath: string,
818
1379
  content: string,
@@ -831,3 +1392,155 @@ export function writeFile(
831
1392
  return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
832
1393
  }
833
1394
  }
1395
+
1396
+ export function deleteFile(filePath: string, repoPath: string): string {
1397
+ const fullPath = path.isAbsolute(filePath)
1398
+ ? filePath
1399
+ : path.join(repoPath, filePath);
1400
+ try {
1401
+ if (!existsSync(fullPath)) return `File not found: ${fullPath}`;
1402
+ const { unlinkSync } = require("fs") as typeof import("fs");
1403
+ unlinkSync(fullPath);
1404
+ return `Deleted: ${fullPath}`;
1405
+ } catch (err) {
1406
+ return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
1407
+ }
1408
+ }
1409
+
1410
+ export function deleteFolder(folderPath: string, repoPath: string): string {
1411
+ const fullPath = path.isAbsolute(folderPath)
1412
+ ? folderPath
1413
+ : path.join(repoPath, folderPath);
1414
+ try {
1415
+ if (!existsSync(fullPath)) return `Folder not found: ${fullPath}`;
1416
+ const { rmSync } = require("fs") as typeof import("fs");
1417
+ rmSync(fullPath, { recursive: true, force: true });
1418
+ return `Deleted folder: ${fullPath}`;
1419
+ } catch (err) {
1420
+ return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
1421
+ }
1422
+ }
1423
+
1424
+ export function openUrl(url: string): string {
1425
+ try {
1426
+ const { execSync } =
1427
+ require("child_process") as typeof import("child_process");
1428
+ const platform = process.platform;
1429
+ if (platform === "win32") {
1430
+ execSync(`start "" "${url}"`, { stdio: "ignore" });
1431
+ } else if (platform === "darwin") {
1432
+ execSync(`open "${url}"`, { stdio: "ignore" });
1433
+ } else {
1434
+ execSync(`xdg-open "${url}"`, { stdio: "ignore" });
1435
+ }
1436
+ return `Opened: ${url}`;
1437
+ } catch (err) {
1438
+ return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
1439
+ }
1440
+ }
1441
+
1442
+ export function generatePdf(
1443
+ filePath: string,
1444
+ content: string,
1445
+ repoPath: string,
1446
+ ): string {
1447
+ const fullPath = path.isAbsolute(filePath)
1448
+ ? filePath
1449
+ : path.join(repoPath, filePath);
1450
+
1451
+ try {
1452
+ const dir = path.dirname(fullPath);
1453
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
1454
+
1455
+ // Escape content for embedding in a Python string literal
1456
+ const escaped = content
1457
+ .replace(/\\/g, "\\\\")
1458
+ .replace(/"""/g, '\\"\\"\\"')
1459
+ .replace(/\r/g, "");
1460
+
1461
+ const script = `
1462
+ import sys
1463
+ try:
1464
+ from reportlab.lib.pagesizes import letter
1465
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
1466
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1467
+ from reportlab.lib.units import inch
1468
+ from reportlab.lib import colors
1469
+ except ImportError:
1470
+ import subprocess
1471
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
1472
+ from reportlab.lib.pagesizes import letter
1473
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
1474
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1475
+ from reportlab.lib.units import inch
1476
+ from reportlab.lib import colors
1477
+
1478
+ doc = SimpleDocTemplate(
1479
+ r"""${fullPath}""",
1480
+ pagesize=letter,
1481
+ rightMargin=inch,
1482
+ leftMargin=inch,
1483
+ topMargin=inch,
1484
+ bottomMargin=inch,
1485
+ )
1486
+
1487
+ styles = getSampleStyleSheet()
1488
+ styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
1489
+ styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
1490
+ styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
1491
+ styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
1492
+ styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
1493
+
1494
+ raw = """${escaped}"""
1495
+
1496
+ story = []
1497
+ for line in raw.split("\\n"):
1498
+ s = line.rstrip()
1499
+ if s.startswith("### "):
1500
+ story.append(Paragraph(s[4:], styles["H3"]))
1501
+ elif s.startswith("## "):
1502
+ story.append(Spacer(1, 6))
1503
+ story.append(Paragraph(s[3:], styles["H2"]))
1504
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
1505
+ elif s.startswith("# "):
1506
+ story.append(Paragraph(s[2:], styles["H1"]))
1507
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
1508
+ elif s.startswith("- ") or s.startswith("* "):
1509
+ story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
1510
+ elif s.startswith("---"):
1511
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
1512
+ elif s == "":
1513
+ story.append(Spacer(1, 6))
1514
+ else:
1515
+ # handle **bold** inline
1516
+ import re
1517
+ s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
1518
+ s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
1519
+ s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
1520
+ story.append(Paragraph(s, styles["Body"]))
1521
+
1522
+ doc.build(story)
1523
+ print("OK")
1524
+ `
1525
+ .replace("${fullPath}", fullPath.replace(/\\/g, "/"))
1526
+ .replace("${escaped}", escaped);
1527
+
1528
+ const os = require("os") as typeof import("os");
1529
+ const tmpFile = path.join(os.tmpdir(), `lens_pdf_${Date.now()}.py`);
1530
+ writeFileSync(tmpFile, script, "utf-8");
1531
+
1532
+ const { execSync } =
1533
+ require("child_process") as typeof import("child_process");
1534
+ execSync(`python "${tmpFile}"`, { stdio: "pipe" });
1535
+
1536
+ try {
1537
+ require("fs").unlinkSync(tmpFile);
1538
+ } catch {
1539
+ /* ignore cleanup errors */
1540
+ }
1541
+
1542
+ return `PDF generated: ${fullPath}`;
1543
+ } catch (err) {
1544
+ return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
1545
+ }
1546
+ }