@ridit/lens 0.1.6 → 0.1.8
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/dist/index.mjs +2028 -207
- package/hello.py +51 -0
- package/package.json +3 -4
- package/src/colors.ts +9 -0
- package/src/components/chat/ChatMessage.tsx +573 -66
- package/src/components/chat/ChatOverlays.tsx +28 -1
- package/src/components/chat/ChatRunner.tsx +250 -69
- package/src/index.tsx +60 -60
- package/src/types/chat.ts +18 -1
- package/src/utils/chat.ts +805 -32
- package/src/utils/thinking.tsx +20 -1
- package/tsconfig.json +1 -1
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
|
|
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
|
|
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.
|
|
42
|
+
### 4. read-folder — list 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
|
-
###
|
|
55
|
+
### 7. delete-file — permanently 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
|
-
###
|
|
72
|
+
### 12. clone — clone a GitHub repo so you can explore and discuss it
|
|
51
73
|
<clone>https://github.com/owner/repo</clone>
|
|
52
74
|
|
|
53
|
-
###
|
|
75
|
+
### 13. 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,50 @@ 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.
|
|
67
|
-
8.
|
|
68
|
-
9.
|
|
69
|
-
10.
|
|
70
|
-
11.
|
|
71
|
-
12.
|
|
72
|
-
13.
|
|
73
|
-
14.
|
|
74
|
-
15.
|
|
75
|
-
16.
|
|
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, tell the user it is done immediately — do NOT auto-read the file back to verify
|
|
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-response — commit 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
|
+
19. 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
|
+
20. 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
|
+
21. 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
|
+
22. 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
|
+
23. When explaining how to use a tool in text, use [tag] bracket notation or a fenced code block — NEVER emit a real XML tool tag as part of an explanation or example
|
|
105
|
+
24. NEVER chain tool calls unless the user's request explicitly requires multiple steps
|
|
106
|
+
25. NEVER read files, list folders, or run tools that were not asked for in the current user message
|
|
107
|
+
|
|
108
|
+
## CRITICAL: READ BEFORE YOU WRITE
|
|
109
|
+
|
|
110
|
+
These rules are mandatory whenever you plan to edit or create a file:
|
|
111
|
+
|
|
112
|
+
### Before modifying ANY existing file:
|
|
113
|
+
1. ALWAYS use read-file on the exact file you plan to change FIRST
|
|
114
|
+
2. Study the full current content — understand every import, every export, every type, every existing feature
|
|
115
|
+
3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
|
|
116
|
+
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
|
|
117
|
+
|
|
118
|
+
### Before adding a feature that touches multiple files:
|
|
119
|
+
1. Use read-folder on the relevant directory to see what files exist
|
|
120
|
+
2. Use read-file on each file you plan to touch
|
|
121
|
+
3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
|
|
122
|
+
|
|
123
|
+
### The golden rule for write-file and changes:
|
|
124
|
+
- The output file must contain EVERYTHING the original had, PLUS your new additions
|
|
125
|
+
- NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
|
|
126
|
+
- If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
|
|
127
|
+
|
|
128
|
+
## WHEN TO USE read-folder:
|
|
129
|
+
- Before editing files in an unfamiliar directory — list it first to understand the structure
|
|
130
|
+
- When a feature spans multiple files and you are not sure what exists
|
|
131
|
+
- When the user asks you to explore or explain a part of the codebase
|
|
76
132
|
|
|
77
133
|
## SCAFFOLDING A NEW PROJECT (follow this exactly)
|
|
78
134
|
|
|
@@ -96,14 +152,28 @@ When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app
|
|
|
96
152
|
|
|
97
153
|
- User shares any URL → fetch it immediately
|
|
98
154
|
- User asks to run anything → shell it immediately
|
|
99
|
-
- User asks to
|
|
155
|
+
- User asks to open a link, open a URL, or visit a website → open-url it immediately, do NOT use fetch
|
|
156
|
+
- User asks to delete a file → delete-file it immediately (requires approval)
|
|
157
|
+
- User asks to delete a folder or directory → delete-folder it immediately (requires approval)
|
|
158
|
+
- 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
|
|
159
|
+
- User asks to read a file → read-file it immediately, NEVER use shell cat/type
|
|
160
|
+
- 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
|
|
161
|
+
- User asks to explore a folder or directory → read-folder it immediately
|
|
100
162
|
- User asks to save/create/write a file → write-file it immediately, then read-file to verify
|
|
163
|
+
- User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
|
|
101
164
|
- User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
|
|
102
165
|
- 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
166
|
- You are unsure about an API, library, error, concept, or piece of code → search it immediately
|
|
104
167
|
- User asks about something recent or that you might not know → search it immediately
|
|
105
168
|
- You are about to say "I'm not sure" or "I don't know" → search instead of guessing
|
|
106
169
|
|
|
170
|
+
## shell IS ONLY FOR:
|
|
171
|
+
- Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
|
|
172
|
+
- Installing packages: \`npm install\`, \`pip install\`
|
|
173
|
+
- Building/testing: \`npm run build\`, \`bun test\`
|
|
174
|
+
- Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
|
|
175
|
+
- Anything that EXECUTES — not reads or lists
|
|
176
|
+
|
|
107
177
|
## CODEBASE
|
|
108
178
|
|
|
109
179
|
${fileList.length > 0 ? fileList : "(no files indexed)"}
|
|
@@ -112,8 +182,267 @@ ${historySummary}`;
|
|
|
112
182
|
}
|
|
113
183
|
|
|
114
184
|
// ── Few-shot examples ─────────────────────────────────────────────────────────
|
|
115
|
-
|
|
116
185
|
export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
186
|
+
// ── delete / open / pdf ───────────────────────────────────────────────────
|
|
187
|
+
{
|
|
188
|
+
role: "user",
|
|
189
|
+
content: "delete src/old-component.tsx",
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
role: "assistant",
|
|
193
|
+
content: "<delete-file>src/old-component.tsx</delete-file>",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
role: "user",
|
|
197
|
+
content:
|
|
198
|
+
"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.",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
role: "assistant",
|
|
202
|
+
content: "Done — `src/old-component.tsx` has been deleted.",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
role: "user",
|
|
206
|
+
content: "delete the legacy folder",
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
role: "assistant",
|
|
210
|
+
content: "<delete-folder>src/legacy</delete-folder>",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
role: "user",
|
|
214
|
+
content:
|
|
215
|
+
"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.",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
role: "assistant",
|
|
219
|
+
content:
|
|
220
|
+
"Done — the `src/legacy` folder and all its contents have been deleted.",
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
role: "user",
|
|
224
|
+
content: "open https://github.com/microsoft/typescript",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
role: "assistant",
|
|
228
|
+
content: "<open-url>https://github.com/microsoft/typescript</open-url>",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
role: "user",
|
|
232
|
+
content:
|
|
233
|
+
"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.",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
role: "assistant",
|
|
237
|
+
content: "Opened the TypeScript GitHub page in your browser.",
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
role: "user",
|
|
241
|
+
content:
|
|
242
|
+
"generate a PDF report about the project and save it to docs/report.pdf",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
role: "assistant",
|
|
246
|
+
content:
|
|
247
|
+
'<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>',
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
role: "user",
|
|
251
|
+
content:
|
|
252
|
+
"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.",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
role: "assistant",
|
|
256
|
+
content: "Done — the PDF report has been saved to `docs/report.pdf`.",
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
// ── grep ──────────────────────────────────────────────────────────────────
|
|
260
|
+
{
|
|
261
|
+
role: "user",
|
|
262
|
+
content: 'grep -R "ChatRunner" -n src',
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
role: "assistant",
|
|
266
|
+
content: '<grep>\n{"pattern": "ChatRunner", "glob": "src/**/*"}\n</grep>',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
role: "user",
|
|
270
|
+
content:
|
|
271
|
+
'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.',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
role: "assistant",
|
|
275
|
+
content:
|
|
276
|
+
"`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`.",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
role: "user",
|
|
280
|
+
content: "find all usages of useInput in the codebase",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
role: "assistant",
|
|
284
|
+
content: '<grep>\n{"pattern": "useInput", "glob": "src/**/*.tsx"}\n</grep>',
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
role: "user",
|
|
288
|
+
content:
|
|
289
|
+
'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.',
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
role: "assistant",
|
|
293
|
+
content:
|
|
294
|
+
"`useInput` is used in `src/components/chat/ChatRunner.tsx` — imported on line 5 and called on line 210.",
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
// ── showing tool usage as an example (bracket notation, NOT real tags) ────
|
|
298
|
+
{
|
|
299
|
+
role: "user",
|
|
300
|
+
content: "show me how to use the read-file tool",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
role: "assistant",
|
|
304
|
+
content:
|
|
305
|
+
"To read a file, emit the tag alone in your response — nothing else on that turn:\n\n```text\n[read-file]src/components/Header.tsx[/read-file]\n```\n\nThe result comes back as the next user message and you can then analyze it.",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
role: "user",
|
|
309
|
+
content: "show me an example of the shell tool",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
role: "assistant",
|
|
313
|
+
content:
|
|
314
|
+
"To run a shell command, emit only the tag:\n\n```text\n[shell]node -v[/shell]\n```\n\nThe stdout/stderr is returned and you can read it in the next turn.",
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
// ── read-folder ───────────────────────────────────────────────────────────
|
|
318
|
+
{
|
|
319
|
+
role: "user",
|
|
320
|
+
content: "read src folder",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
role: "assistant",
|
|
324
|
+
content: "<read-folder>src</read-folder>",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
role: "user",
|
|
328
|
+
content:
|
|
329
|
+
"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.",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
role: "assistant",
|
|
333
|
+
content:
|
|
334
|
+
"The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`.",
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
role: "user",
|
|
338
|
+
content: "list src folder",
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
role: "assistant",
|
|
342
|
+
content: "<read-folder>src</read-folder>",
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
role: "user",
|
|
346
|
+
content:
|
|
347
|
+
"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.",
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
role: "assistant",
|
|
351
|
+
content:
|
|
352
|
+
"The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`.",
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
role: "user",
|
|
356
|
+
content: "what files are in src/components?",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
role: "assistant",
|
|
360
|
+
content: "<read-folder>src/components</read-folder>",
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
role: "user",
|
|
364
|
+
content:
|
|
365
|
+
"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.",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
role: "assistant",
|
|
369
|
+
content:
|
|
370
|
+
"The `src/components` folder has 3 files — `Header.tsx`, `Footer.tsx`, `Button.tsx` — plus two subfolders: `ui/` and `forms/`.",
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
role: "user",
|
|
374
|
+
content: "list the files in src/utils",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
role: "assistant",
|
|
378
|
+
content: "<read-folder>src/utils</read-folder>",
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
role: "user",
|
|
382
|
+
content:
|
|
383
|
+
"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.",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
role: "assistant",
|
|
387
|
+
content:
|
|
388
|
+
"The `src/utils` folder contains 3 files: `api.ts`, `helpers.ts`, and `format.ts`.",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
role: "user",
|
|
392
|
+
content: "show me what's in the src directory",
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
role: "assistant",
|
|
396
|
+
content: "<read-folder>src</read-folder>",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
role: "user",
|
|
400
|
+
content:
|
|
401
|
+
"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.",
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
role: "assistant",
|
|
405
|
+
content:
|
|
406
|
+
"The `src` directory has 2 files (`index.ts`, `App.tsx`) and 2 subfolders (`components/`, `utils/`).",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
role: "user",
|
|
410
|
+
content: "show me the project structure",
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
role: "assistant",
|
|
414
|
+
content: "<read-folder>.</read-folder>",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
role: "user",
|
|
418
|
+
content:
|
|
419
|
+
"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.",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
role: "assistant",
|
|
423
|
+
content:
|
|
424
|
+
"The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code.",
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
role: "user",
|
|
428
|
+
content: "what does the project look like?",
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
role: "assistant",
|
|
432
|
+
content: "<read-folder>.</read-folder>",
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
role: "user",
|
|
436
|
+
content:
|
|
437
|
+
"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.",
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
role: "assistant",
|
|
441
|
+
content:
|
|
442
|
+
"The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`.",
|
|
443
|
+
},
|
|
444
|
+
|
|
445
|
+
// ── fetch → write → done (no auto-verify) ────────────────────────────────
|
|
117
446
|
{
|
|
118
447
|
role: "user",
|
|
119
448
|
content: "fetch https://api.github.com/repos/microsoft/typescript",
|
|
@@ -148,17 +477,50 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
148
477
|
},
|
|
149
478
|
{
|
|
150
479
|
role: "assistant",
|
|
151
|
-
content: "
|
|
480
|
+
content: "Done — `ts-info.json` has been saved.",
|
|
152
481
|
},
|
|
482
|
+
|
|
483
|
+
// ── write it to a file → done, nothing else ───────────────────────────────
|
|
153
484
|
{
|
|
154
485
|
role: "user",
|
|
486
|
+
content: "write it to a file called hello.py",
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
role: "assistant",
|
|
155
490
|
content:
|
|
156
|
-
'
|
|
491
|
+
'<write-file>\n{"path": "hello.py", "content": "# hello.py\\nprint(\'hello\')"}\n</write-file>',
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
role: "user",
|
|
495
|
+
content:
|
|
496
|
+
"Here is the output from write-file to hello.py:\n\nWritten: /repo/hello.py (2 lines, 32 bytes)\n\nPlease continue your response based on this output.",
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
role: "assistant",
|
|
500
|
+
content: "Done — `hello.py` has been written.",
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// ── read before write ─────────────────────────────────────────────────────
|
|
504
|
+
{
|
|
505
|
+
role: "user",
|
|
506
|
+
content: "add a logout button to src/components/Header.tsx",
|
|
157
507
|
},
|
|
158
508
|
{
|
|
159
509
|
role: "assistant",
|
|
160
|
-
content: "
|
|
510
|
+
content: "<read-file>src/components/Header.tsx</read-file>",
|
|
161
511
|
},
|
|
512
|
+
{
|
|
513
|
+
role: "user",
|
|
514
|
+
content:
|
|
515
|
+
"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.",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
role: "assistant",
|
|
519
|
+
content:
|
|
520
|
+
'<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>',
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
// ── shell ─────────────────────────────────────────────────────────────────
|
|
162
524
|
{
|
|
163
525
|
role: "user",
|
|
164
526
|
content: "what node version am I on",
|
|
@@ -176,6 +538,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
176
538
|
role: "assistant",
|
|
177
539
|
content: "You're running Node.js v20.11.0.",
|
|
178
540
|
},
|
|
541
|
+
|
|
542
|
+
// ── clone ─────────────────────────────────────────────────────────────────
|
|
179
543
|
{
|
|
180
544
|
role: "user",
|
|
181
545
|
content: "clone https://github.com/facebook/react",
|
|
@@ -194,6 +558,8 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
194
558
|
content:
|
|
195
559
|
"Cloned! The React repo has 2847 files. I can read source files, explain how it works, or suggest improvements — just ask.",
|
|
196
560
|
},
|
|
561
|
+
|
|
562
|
+
// ── search ────────────────────────────────────────────────────────────────
|
|
197
563
|
{
|
|
198
564
|
role: "user",
|
|
199
565
|
content: "what does the ?? operator do in typescript",
|
|
@@ -213,7 +579,6 @@ export const FEW_SHOT_MESSAGES: { role: string; content: string }[] = [
|
|
|
213
579
|
"The `??` operator is the nullish coalescing operator. It returns the right side only when the left side is `null` or `undefined`.",
|
|
214
580
|
},
|
|
215
581
|
];
|
|
216
|
-
|
|
217
582
|
// ── Response parser ───────────────────────────────────────────────────────────
|
|
218
583
|
|
|
219
584
|
export type ParsedResponse =
|
|
@@ -222,6 +587,17 @@ export type ParsedResponse =
|
|
|
222
587
|
| { kind: "shell"; content: string; command: string }
|
|
223
588
|
| { kind: "fetch"; content: string; url: string }
|
|
224
589
|
| { kind: "read-file"; content: string; filePath: string }
|
|
590
|
+
| { kind: "read-folder"; content: string; folderPath: string }
|
|
591
|
+
| { kind: "grep"; content: string; pattern: string; glob: string }
|
|
592
|
+
| { kind: "delete-file"; content: string; filePath: string }
|
|
593
|
+
| { kind: "delete-folder"; content: string; folderPath: string }
|
|
594
|
+
| { kind: "open-url"; content: string; url: string }
|
|
595
|
+
| {
|
|
596
|
+
kind: "generate-pdf";
|
|
597
|
+
content: string;
|
|
598
|
+
filePath: string;
|
|
599
|
+
pdfContent: string;
|
|
600
|
+
}
|
|
225
601
|
| {
|
|
226
602
|
kind: "write-file";
|
|
227
603
|
content: string;
|
|
@@ -232,6 +608,10 @@ export type ParsedResponse =
|
|
|
232
608
|
| { kind: "clone"; content: string; repoUrl: string };
|
|
233
609
|
|
|
234
610
|
export function parseResponse(text: string): ParsedResponse {
|
|
611
|
+
// Strip fenced code blocks before scanning for tool tags so that tags shown
|
|
612
|
+
// as examples inside ``` ... ``` blocks are never executed.
|
|
613
|
+
const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
|
|
614
|
+
|
|
235
615
|
type Candidate = {
|
|
236
616
|
index: number;
|
|
237
617
|
kind:
|
|
@@ -239,6 +619,12 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
239
619
|
| "shell"
|
|
240
620
|
| "fetch"
|
|
241
621
|
| "read-file"
|
|
622
|
+
| "read-folder"
|
|
623
|
+
| "grep"
|
|
624
|
+
| "delete-file"
|
|
625
|
+
| "delete-folder"
|
|
626
|
+
| "open-url"
|
|
627
|
+
| "generate-pdf"
|
|
242
628
|
| "write-file"
|
|
243
629
|
| "search"
|
|
244
630
|
| "clone";
|
|
@@ -250,6 +636,15 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
250
636
|
{ kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
|
|
251
637
|
{ kind: "shell", re: /<shell>([\s\S]*?)<\/shell>/g },
|
|
252
638
|
{ kind: "read-file", re: /<read-file>([\s\S]*?)<\/read-file>/g },
|
|
639
|
+
{ kind: "read-folder", re: /<read-folder>([\s\S]*?)<\/read-folder>/g },
|
|
640
|
+
{ kind: "grep", re: /<grep>([\s\S]*?)<\/grep>/g },
|
|
641
|
+
{ kind: "delete-file", re: /<delete-file>([\s\S]*?)<\/delete-file>/g },
|
|
642
|
+
{
|
|
643
|
+
kind: "delete-folder",
|
|
644
|
+
re: /<delete-folder>([\s\S]*?)<\/delete-folder>/g,
|
|
645
|
+
},
|
|
646
|
+
{ kind: "open-url", re: /<open-url>([\s\S]*?)<\/open-url>/g },
|
|
647
|
+
{ kind: "generate-pdf", re: /<generate-pdf>([\s\S]*?)<\/generate-pdf>/g },
|
|
253
648
|
{ kind: "write-file", re: /<write-file>([\s\S]*?)<\/write-file>/g },
|
|
254
649
|
{ kind: "search", re: /<search>([\s\S]*?)<\/search>/g },
|
|
255
650
|
{ kind: "clone", re: /<clone>([\s\S]*?)<\/clone>/g },
|
|
@@ -257,6 +652,7 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
257
652
|
{ kind: "fetch", re: /```fetch\r?\n([\s\S]*?)\r?\n```/g },
|
|
258
653
|
{ kind: "shell", re: /```shell\r?\n([\s\S]*?)\r?\n```/g },
|
|
259
654
|
{ kind: "read-file", re: /```read-file\r?\n([\s\S]*?)\r?\n```/g },
|
|
655
|
+
{ kind: "read-folder", re: /```read-folder\r?\n([\s\S]*?)\r?\n```/g },
|
|
260
656
|
{ kind: "write-file", re: /```write-file\r?\n([\s\S]*?)\r?\n```/g },
|
|
261
657
|
{ kind: "search", re: /```search\r?\n([\s\S]*?)\r?\n```/g },
|
|
262
658
|
{ kind: "changes", re: /```changes\r?\n([\s\S]*?)\r?\n```/g },
|
|
@@ -264,16 +660,40 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
264
660
|
|
|
265
661
|
for (const { kind, re } of patterns) {
|
|
266
662
|
re.lastIndex = 0;
|
|
267
|
-
|
|
268
|
-
|
|
663
|
+
// Scan against the code-block-stripped text so tags inside ``` are ignored
|
|
664
|
+
const m = re.exec(scanText);
|
|
665
|
+
if (m) {
|
|
666
|
+
// Re-extract the match body from the original text at the same position
|
|
667
|
+
const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
|
|
668
|
+
const originalMatch = originalRe.exec(text.slice(m.index));
|
|
669
|
+
if (originalMatch) {
|
|
670
|
+
// Reconstruct a match-like object with the correct index
|
|
671
|
+
const fakeMatch = Object.assign(
|
|
672
|
+
[
|
|
673
|
+
text.slice(m.index, m.index + originalMatch[0].length),
|
|
674
|
+
originalMatch[1],
|
|
675
|
+
] as unknown as RegExpExecArray,
|
|
676
|
+
{ index: m.index, input: text, groups: undefined },
|
|
677
|
+
);
|
|
678
|
+
candidates.push({ index: m.index, kind, match: fakeMatch });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
269
681
|
}
|
|
270
682
|
|
|
271
683
|
if (candidates.length === 0) return { kind: "text", content: text.trim() };
|
|
272
684
|
|
|
273
685
|
candidates.sort((a, b) => a.index - b.index);
|
|
274
686
|
const { kind, match } = candidates[0]!;
|
|
275
|
-
|
|
276
|
-
|
|
687
|
+
|
|
688
|
+
// Strip any leaked tool tags from preamble text
|
|
689
|
+
const before = text
|
|
690
|
+
.slice(0, match.index)
|
|
691
|
+
.replace(
|
|
692
|
+
/<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g,
|
|
693
|
+
"",
|
|
694
|
+
)
|
|
695
|
+
.trim();
|
|
696
|
+
const body = (match[1] ?? "").trim();
|
|
277
697
|
|
|
278
698
|
if (kind === "changes") {
|
|
279
699
|
try {
|
|
@@ -299,6 +719,48 @@ export function parseResponse(text: string): ParsedResponse {
|
|
|
299
719
|
if (kind === "read-file")
|
|
300
720
|
return { kind: "read-file", content: before, filePath: body };
|
|
301
721
|
|
|
722
|
+
if (kind === "read-folder")
|
|
723
|
+
return { kind: "read-folder", content: before, folderPath: body };
|
|
724
|
+
|
|
725
|
+
if (kind === "delete-file")
|
|
726
|
+
return { kind: "delete-file", content: before, filePath: body };
|
|
727
|
+
|
|
728
|
+
if (kind === "delete-folder")
|
|
729
|
+
return { kind: "delete-folder", content: before, folderPath: body };
|
|
730
|
+
|
|
731
|
+
if (kind === "open-url") {
|
|
732
|
+
const url = body.replace(/^<|>$/g, "").trim();
|
|
733
|
+
return { kind: "open-url", content: before, url };
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (kind === "generate-pdf") {
|
|
737
|
+
try {
|
|
738
|
+
const parsed = JSON.parse(body);
|
|
739
|
+
return {
|
|
740
|
+
kind: "generate-pdf",
|
|
741
|
+
content: before,
|
|
742
|
+
filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
|
|
743
|
+
pdfContent: parsed.content ?? "",
|
|
744
|
+
};
|
|
745
|
+
} catch {
|
|
746
|
+
return { kind: "text", content: text };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (kind === "grep") {
|
|
751
|
+
try {
|
|
752
|
+
const parsed = JSON.parse(body) as { pattern: string; glob?: string };
|
|
753
|
+
return {
|
|
754
|
+
kind: "grep",
|
|
755
|
+
content: before,
|
|
756
|
+
pattern: parsed.pattern,
|
|
757
|
+
glob: parsed.glob ?? "**/*",
|
|
758
|
+
};
|
|
759
|
+
} catch {
|
|
760
|
+
return { kind: "grep", content: before, pattern: body, glob: "**/*" };
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
302
764
|
if (kind === "write-file") {
|
|
303
765
|
try {
|
|
304
766
|
const parsed = JSON.parse(body) as { path: string; content: string };
|
|
@@ -364,9 +826,21 @@ function buildApiMessages(
|
|
|
364
826
|
? `fetch of ${m.content}`
|
|
365
827
|
: m.toolName === "read-file"
|
|
366
828
|
? `read-file of ${m.content}`
|
|
367
|
-
: m.toolName === "
|
|
368
|
-
? `
|
|
369
|
-
:
|
|
829
|
+
: m.toolName === "read-folder"
|
|
830
|
+
? `read-folder of ${m.content}`
|
|
831
|
+
: m.toolName === "grep"
|
|
832
|
+
? `grep for "${m.content}"`
|
|
833
|
+
: m.toolName === "delete-file"
|
|
834
|
+
? `delete-file of ${m.content}`
|
|
835
|
+
: m.toolName === "delete-folder"
|
|
836
|
+
? `delete-folder of ${m.content}`
|
|
837
|
+
: m.toolName === "open-url"
|
|
838
|
+
? `open-url ${m.content}`
|
|
839
|
+
: m.toolName === "generate-pdf"
|
|
840
|
+
? `generate-pdf to ${m.content}`
|
|
841
|
+
: m.toolName === "search"
|
|
842
|
+
? `web search for "${m.content}"`
|
|
843
|
+
: `write-file to ${m.content}`;
|
|
370
844
|
return {
|
|
371
845
|
role: "user",
|
|
372
846
|
content: `Here is the output from the ${label}:\n\n${m.result}\n\nPlease continue your response based on this output.`,
|
|
@@ -380,6 +854,7 @@ export async function callChat(
|
|
|
380
854
|
provider: Provider,
|
|
381
855
|
systemPrompt: string,
|
|
382
856
|
messages: Message[],
|
|
857
|
+
abortSignal?: AbortSignal,
|
|
383
858
|
): Promise<string> {
|
|
384
859
|
const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
|
|
385
860
|
|
|
@@ -417,6 +892,8 @@ export async function callChat(
|
|
|
417
892
|
const controller = new AbortController();
|
|
418
893
|
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
419
894
|
|
|
895
|
+
abortSignal?.addEventListener("abort", () => controller.abort());
|
|
896
|
+
|
|
420
897
|
const res = await fetch(url, {
|
|
421
898
|
method: "POST",
|
|
422
899
|
headers,
|
|
@@ -527,9 +1004,6 @@ export function applyPatches(repoPath: string, patches: FilePatch[]): void {
|
|
|
527
1004
|
|
|
528
1005
|
export async function runShell(command: string, cwd: string): Promise<string> {
|
|
529
1006
|
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
1007
|
const { spawn } =
|
|
534
1008
|
require("child_process") as typeof import("child_process");
|
|
535
1009
|
const isWin = process.platform === "win32";
|
|
@@ -813,6 +1287,155 @@ export function readFile(filePath: string, repoPath: string): string {
|
|
|
813
1287
|
return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
|
|
814
1288
|
}
|
|
815
1289
|
|
|
1290
|
+
export function readFolder(folderPath: string, repoPath: string): string {
|
|
1291
|
+
// Strip any command prefixes the model may have included (e.g. "ls src" → "src", "read src" → "src")
|
|
1292
|
+
const sanitized = folderPath
|
|
1293
|
+
.replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "")
|
|
1294
|
+
.trim();
|
|
1295
|
+
|
|
1296
|
+
const candidates = path.isAbsolute(sanitized)
|
|
1297
|
+
? [sanitized]
|
|
1298
|
+
: [sanitized, path.join(repoPath, sanitized)];
|
|
1299
|
+
|
|
1300
|
+
for (const candidate of candidates) {
|
|
1301
|
+
if (!existsSync(candidate)) continue;
|
|
1302
|
+
let stat: ReturnType<typeof statSync>;
|
|
1303
|
+
try {
|
|
1304
|
+
stat = statSync(candidate);
|
|
1305
|
+
} catch {
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
if (!stat.isDirectory()) {
|
|
1309
|
+
return `Not a directory: ${candidate}. Use read-file to read a file.`;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
let entries: string[];
|
|
1313
|
+
try {
|
|
1314
|
+
entries = readdirSync(candidate, { encoding: "utf-8" });
|
|
1315
|
+
} catch (err) {
|
|
1316
|
+
return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const files: string[] = [];
|
|
1320
|
+
const subfolders: string[] = [];
|
|
1321
|
+
|
|
1322
|
+
for (const entry of entries) {
|
|
1323
|
+
if (entry.startsWith(".") && entry !== ".env") continue;
|
|
1324
|
+
const full = path.join(candidate, entry);
|
|
1325
|
+
try {
|
|
1326
|
+
if (statSync(full).isDirectory()) {
|
|
1327
|
+
subfolders.push(`${entry}/`);
|
|
1328
|
+
} else {
|
|
1329
|
+
files.push(entry);
|
|
1330
|
+
}
|
|
1331
|
+
} catch {
|
|
1332
|
+
// skip unreadable entries
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const total = files.length + subfolders.length;
|
|
1337
|
+
const lines: string[] = [`Folder: ${candidate} (${total} entries)`, ""];
|
|
1338
|
+
|
|
1339
|
+
if (files.length > 0) {
|
|
1340
|
+
lines.push("Files:");
|
|
1341
|
+
files.forEach((f) => lines.push(` ${f}`));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (subfolders.length > 0) {
|
|
1345
|
+
if (files.length > 0) lines.push("");
|
|
1346
|
+
lines.push("Subfolders:");
|
|
1347
|
+
subfolders.forEach((d) => lines.push(` ${d}`));
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
if (total === 0) {
|
|
1351
|
+
lines.push("(empty folder)");
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
return lines.join("\n");
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
return `Folder not found: ${sanitized}`;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
export function grepFiles(
|
|
1361
|
+
pattern: string,
|
|
1362
|
+
glob: string,
|
|
1363
|
+
repoPath: string,
|
|
1364
|
+
): string {
|
|
1365
|
+
let regex: RegExp;
|
|
1366
|
+
try {
|
|
1367
|
+
regex = new RegExp(pattern, "i");
|
|
1368
|
+
} catch {
|
|
1369
|
+
regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const globToFilter = (g: string): ((rel: string) => boolean) => {
|
|
1373
|
+
const cleaned = g.replace(/^\*\*\//, "");
|
|
1374
|
+
const parts = cleaned.split("/");
|
|
1375
|
+
const ext = parts[parts.length - 1];
|
|
1376
|
+
const prefix = parts.slice(0, -1).join("/");
|
|
1377
|
+
|
|
1378
|
+
return (rel: string) => {
|
|
1379
|
+
if (ext?.startsWith("*.")) {
|
|
1380
|
+
const extSuffix = ext.slice(1);
|
|
1381
|
+
if (!rel.endsWith(extSuffix)) return false;
|
|
1382
|
+
} else if (ext && !ext.includes("*")) {
|
|
1383
|
+
if (!rel.endsWith(ext)) return false;
|
|
1384
|
+
}
|
|
1385
|
+
if (prefix && !prefix.includes("*")) {
|
|
1386
|
+
if (!rel.startsWith(prefix)) return false;
|
|
1387
|
+
}
|
|
1388
|
+
return true;
|
|
1389
|
+
};
|
|
1390
|
+
};
|
|
1391
|
+
|
|
1392
|
+
const filter = globToFilter(glob);
|
|
1393
|
+
const allFiles = walkDir(repoPath);
|
|
1394
|
+
const matchedFiles = allFiles.filter(filter);
|
|
1395
|
+
|
|
1396
|
+
if (matchedFiles.length === 0) {
|
|
1397
|
+
return `No files matched glob: ${glob}`;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
const results: string[] = [];
|
|
1401
|
+
let totalMatches = 0;
|
|
1402
|
+
|
|
1403
|
+
for (const relPath of matchedFiles) {
|
|
1404
|
+
const fullPath = path.join(repoPath, relPath);
|
|
1405
|
+
let content: string;
|
|
1406
|
+
try {
|
|
1407
|
+
content = readFileSync(fullPath, "utf-8");
|
|
1408
|
+
} catch {
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
const lines = content.split("\n");
|
|
1413
|
+
const fileMatches: string[] = [];
|
|
1414
|
+
|
|
1415
|
+
lines.forEach((line, i) => {
|
|
1416
|
+
if (regex.test(line)) {
|
|
1417
|
+
fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
|
|
1418
|
+
totalMatches++;
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1422
|
+
if (fileMatches.length > 0) {
|
|
1423
|
+
results.push(`${relPath}\n${fileMatches.join("\n")}`);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
if (totalMatches >= 200) {
|
|
1427
|
+
results.push("(truncated — too many matches)");
|
|
1428
|
+
break;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (results.length === 0) {
|
|
1433
|
+
return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)\n\n${results.join("\n\n")}`;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
816
1439
|
export function writeFile(
|
|
817
1440
|
filePath: string,
|
|
818
1441
|
content: string,
|
|
@@ -831,3 +1454,153 @@ export function writeFile(
|
|
|
831
1454
|
return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
|
|
832
1455
|
}
|
|
833
1456
|
}
|
|
1457
|
+
|
|
1458
|
+
export function deleteFile(filePath: string, repoPath: string): string {
|
|
1459
|
+
const fullPath = path.isAbsolute(filePath)
|
|
1460
|
+
? filePath
|
|
1461
|
+
: path.join(repoPath, filePath);
|
|
1462
|
+
try {
|
|
1463
|
+
if (!existsSync(fullPath)) return `File not found: ${fullPath}`;
|
|
1464
|
+
const { unlinkSync } = require("fs") as typeof import("fs");
|
|
1465
|
+
unlinkSync(fullPath);
|
|
1466
|
+
return `Deleted: ${fullPath}`;
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
export function deleteFolder(folderPath: string, repoPath: string): string {
|
|
1473
|
+
const fullPath = path.isAbsolute(folderPath)
|
|
1474
|
+
? folderPath
|
|
1475
|
+
: path.join(repoPath, folderPath);
|
|
1476
|
+
try {
|
|
1477
|
+
if (!existsSync(fullPath)) return `Folder not found: ${fullPath}`;
|
|
1478
|
+
const { rmSync } = require("fs") as typeof import("fs");
|
|
1479
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
1480
|
+
return `Deleted folder: ${fullPath}`;
|
|
1481
|
+
} catch (err) {
|
|
1482
|
+
return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
export function openUrl(url: string): string {
|
|
1487
|
+
try {
|
|
1488
|
+
const { execSync } =
|
|
1489
|
+
require("child_process") as typeof import("child_process");
|
|
1490
|
+
const platform = process.platform;
|
|
1491
|
+
if (platform === "win32") {
|
|
1492
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
1493
|
+
} else if (platform === "darwin") {
|
|
1494
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
1495
|
+
} else {
|
|
1496
|
+
execSync(`xdg-open "${url}"`, { stdio: "ignore" });
|
|
1497
|
+
}
|
|
1498
|
+
return `Opened: ${url}`;
|
|
1499
|
+
} catch (err) {
|
|
1500
|
+
return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
export function generatePdf(
|
|
1505
|
+
filePath: string,
|
|
1506
|
+
content: string,
|
|
1507
|
+
repoPath: string,
|
|
1508
|
+
): string {
|
|
1509
|
+
const fullPath = path.isAbsolute(filePath)
|
|
1510
|
+
? filePath
|
|
1511
|
+
: path.join(repoPath, filePath);
|
|
1512
|
+
|
|
1513
|
+
try {
|
|
1514
|
+
const dir = path.dirname(fullPath);
|
|
1515
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1516
|
+
|
|
1517
|
+
const escaped = content
|
|
1518
|
+
.replace(/\\/g, "\\\\")
|
|
1519
|
+
.replace(/"""/g, '\\"\\"\\"')
|
|
1520
|
+
.replace(/\r/g, "");
|
|
1521
|
+
|
|
1522
|
+
const script = `
|
|
1523
|
+
import sys
|
|
1524
|
+
try:
|
|
1525
|
+
from reportlab.lib.pagesizes import letter
|
|
1526
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
|
|
1527
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
1528
|
+
from reportlab.lib.units import inch
|
|
1529
|
+
from reportlab.lib import colors
|
|
1530
|
+
except ImportError:
|
|
1531
|
+
import subprocess
|
|
1532
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
|
|
1533
|
+
from reportlab.lib.pagesizes import letter
|
|
1534
|
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
|
|
1535
|
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
1536
|
+
from reportlab.lib.units import inch
|
|
1537
|
+
from reportlab.lib import colors
|
|
1538
|
+
|
|
1539
|
+
doc = SimpleDocTemplate(
|
|
1540
|
+
r"""${fullPath}""",
|
|
1541
|
+
pagesize=letter,
|
|
1542
|
+
rightMargin=inch,
|
|
1543
|
+
leftMargin=inch,
|
|
1544
|
+
topMargin=inch,
|
|
1545
|
+
bottomMargin=inch,
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
styles = getSampleStyleSheet()
|
|
1549
|
+
styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
|
|
1550
|
+
styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
|
|
1551
|
+
styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
|
|
1552
|
+
styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
|
|
1553
|
+
styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
|
|
1554
|
+
|
|
1555
|
+
raw = """${escaped}"""
|
|
1556
|
+
|
|
1557
|
+
story = []
|
|
1558
|
+
for line in raw.split("\\n"):
|
|
1559
|
+
s = line.rstrip()
|
|
1560
|
+
if s.startswith("### "):
|
|
1561
|
+
story.append(Paragraph(s[4:], styles["H3"]))
|
|
1562
|
+
elif s.startswith("## "):
|
|
1563
|
+
story.append(Spacer(1, 6))
|
|
1564
|
+
story.append(Paragraph(s[3:], styles["H2"]))
|
|
1565
|
+
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
|
|
1566
|
+
elif s.startswith("# "):
|
|
1567
|
+
story.append(Paragraph(s[2:], styles["H1"]))
|
|
1568
|
+
story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
|
|
1569
|
+
elif s.startswith("- ") or s.startswith("* "):
|
|
1570
|
+
story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
|
|
1571
|
+
elif s.startswith("---"):
|
|
1572
|
+
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
|
|
1573
|
+
elif s == "":
|
|
1574
|
+
story.append(Spacer(1, 6))
|
|
1575
|
+
else:
|
|
1576
|
+
import re
|
|
1577
|
+
s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
|
|
1578
|
+
s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
|
|
1579
|
+
s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
|
|
1580
|
+
story.append(Paragraph(s, styles["Body"]))
|
|
1581
|
+
|
|
1582
|
+
doc.build(story)
|
|
1583
|
+
print("OK")
|
|
1584
|
+
`
|
|
1585
|
+
.replace("${fullPath}", fullPath.replace(/\\/g, "/"))
|
|
1586
|
+
.replace("${escaped}", escaped);
|
|
1587
|
+
|
|
1588
|
+
const os = require("os") as typeof import("os");
|
|
1589
|
+
const tmpFile = path.join(os.tmpdir(), `lens_pdf_${Date.now()}.py`);
|
|
1590
|
+
writeFileSync(tmpFile, script, "utf-8");
|
|
1591
|
+
|
|
1592
|
+
const { execSync } =
|
|
1593
|
+
require("child_process") as typeof import("child_process");
|
|
1594
|
+
execSync(`python "${tmpFile}"`, { stdio: "pipe" });
|
|
1595
|
+
|
|
1596
|
+
try {
|
|
1597
|
+
require("fs").unlinkSync(tmpFile);
|
|
1598
|
+
} catch {
|
|
1599
|
+
/* ignore cleanup errors */
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
return `PDF generated: ${fullPath}`;
|
|
1603
|
+
} catch (err) {
|
|
1604
|
+
return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
|
|
1605
|
+
}
|
|
1606
|
+
}
|