@ridit/lens 0.2.1 → 0.2.2

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 CHANGED
@@ -49206,72 +49206,17 @@ print("OK")
49206
49206
  }
49207
49207
  }
49208
49208
  // src/prompts/system.ts
49209
- function buildSystemPrompt(files, memorySummary = "") {
49209
+ function buildSystemPrompt(files, memorySummary = "", toolsSection) {
49210
49210
  const fileList = files.map((f) => `### ${f.path}
49211
49211
  \`\`\`
49212
49212
  ${f.content.slice(0, 2000)}
49213
49213
  \`\`\``).join(`
49214
49214
 
49215
49215
  `);
49216
+ const tools = toolsSection ?? BUILTIN_TOOLS_SECTION;
49216
49217
  return `You are an expert software engineer assistant with access to the user's codebase and tools.
49217
49218
 
49218
- ## TOOLS
49219
-
49220
- You have exactly thirteen tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
49221
-
49222
- ### 1. fetch — load a URL
49223
- <fetch>https://example.com</fetch>
49224
-
49225
- ### 2. shell — run a terminal command
49226
- <shell>node -v</shell>
49227
-
49228
- ### 3. read-file — read a file from the repo
49229
- <read-file>src/foo.ts</read-file>
49230
-
49231
- ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
49232
- <read-folder>src/components</read-folder>
49233
-
49234
- ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
49235
- <grep>
49236
- {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
49237
- </grep>
49238
-
49239
- ### 6. write-file — create or overwrite a file
49240
- <write-file>
49241
- {"path": "data/output.csv", "content": "col1,col2
49242
- val1,val2"}
49243
- </write-file>
49244
-
49245
- ### 7. delete-file — permanently delete a single file
49246
- <delete-file>src/old-component.tsx</delete-file>
49247
-
49248
- ### 8. delete-folder — permanently delete a folder and all its contents
49249
- <delete-folder>src/legacy</delete-folder>
49250
-
49251
- ### 9. open-url — open a URL in the user's default browser
49252
- <open-url>https://github.com/owner/repo</open-url>
49253
-
49254
- ### 10. generate-pdf — generate a PDF file from markdown-style content
49255
- <generate-pdf>
49256
- {"path": "output/report.pdf", "content": "# Title
49257
-
49258
- Some body text.
49259
-
49260
- ## Section
49261
-
49262
- More content."}
49263
- </generate-pdf>
49264
-
49265
- ### 11. search — search the internet for anything you are unsure about
49266
- <search>how to use React useEffect cleanup function</search>
49267
-
49268
- ### 12. clone — clone a GitHub repo so you can explore and discuss it
49269
- <clone>https://github.com/owner/repo</clone>
49270
-
49271
- ### 13. changes — propose code edits (shown as a diff for user approval)
49272
- <changes>
49273
- {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
49274
- </changes>
49219
+ ${tools}
49275
49220
 
49276
49221
  ## MEMORY OPERATIONS
49277
49222
 
@@ -49321,9 +49266,26 @@ You may emit multiple memory operations in a single response alongside normal co
49321
49266
  22. When scaffolding a multi-file project, after each write-file succeeds, immediately proceed to writing the NEXT file — NEVER rewrite a file you already wrote in this session. Each file is written ONCE and ONLY ONCE.
49322
49267
  23. For JSX/TSX files always use \`.tsx\` extension and include \`/** @jsxImportSource react */\` or ensure tsconfig has jsx set — bun needs this to parse JSX
49323
49268
  24. 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
49324
- 25. NEVER chain tool calls unless the user's request explicitly requires multiple steps
49325
- 26. NEVER read files, list folders, or run tools that were not asked for in the current user message
49326
- 27. NEVER use markdown formatting in plain text responses — no **bold**, no *italics*, no # headings, no bullet points with -, *, or +, no numbered lists, no backtick inline code. Write in plain prose. Only use fenced \`\`\` code blocks when showing actual code.
49269
+ 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
49270
+ 26. NEVER use markdown formatting in plain text responses — no **bold**, no *italics*, no # headings, no bullet points with -, *, or +, no numbered lists, no backtick inline code. Write in plain prose. Only use fenced \`\`\` code blocks when showing actual code.
49271
+
49272
+ ## SCAFFOLDING — CHAINING WRITE-FILE CALLS
49273
+
49274
+ When creating multiple files (e.g. scaffolding a project or creating 10 files), emit ALL of them
49275
+ in a single response by chaining the tags back-to-back with no text between them:
49276
+
49277
+ <write-file>
49278
+ {"path": "test/file1.txt", "content": "File 1 content"}
49279
+ </write-file>
49280
+ <write-file>
49281
+ {"path": "test/file2.txt", "content": "File 2 content"}
49282
+ </write-file>
49283
+ <write-file>
49284
+ {"path": "test/file3.txt", "content": "File 3 content"}
49285
+ </write-file>
49286
+
49287
+ The system processes each tag sequentially and automatically continues to the next one.
49288
+ Do NOT wait for a user message between files — emit all tags at once.
49327
49289
 
49328
49290
  ## CRITICAL: READ BEFORE YOU WRITE
49329
49291
 
@@ -49345,61 +49307,62 @@ These rules are mandatory whenever you plan to edit or create a file:
49345
49307
  - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
49346
49308
  - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
49347
49309
 
49348
- ## WHEN TO USE read-folder:
49349
- - Before editing files in an unfamiliar directory — list it first to understand the structure
49350
- - When a feature spans multiple files and you are not sure what exists
49351
- - When the user asks you to explore or explain a part of the codebase
49352
-
49353
- ## SCAFFOLDING A NEW PROJECT (follow this exactly)
49354
-
49355
- When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app called list"):
49356
- 1. Create all files first using write-file with paths like \`list/package.json\`, \`list/src/index.tsx\`
49357
- 2. Then run \`cd list && bun install\` (or npm/pnpm) in one shell command
49358
- 3. Then run the project with \`cd list && bun run index.ts\` or whatever the entry point is
49359
- 4. NEVER run \`bun init\` — it is interactive and will hang. Create package.json manually with write-file instead
49360
- 5. TSX files need either tsconfig.json with \`"jsx": "react-jsx"\` or \`/** @jsxImportSource react */\` at the top
49361
-
49362
- ## FETCH → WRITE FLOW (follow this exactly when saving fetched data)
49363
-
49364
- 1. fetch the URL
49365
- 2. Analyze the result — count the rows, identify columns, check completeness
49366
- 3. Tell the user what you found: "Found X rows with columns: A, B, C. Writing now."
49367
- 4. emit write-file with correctly structured, complete content
49368
- 5. After write-file confirms success, emit read-file to verify
49369
- 6. Only after read-file confirms content is correct, tell the user it is done
49370
-
49371
- ## WHEN TO USE TOOLS
49372
-
49373
- - User shares any URL → fetch it immediately
49374
- - User asks to run anything → shell it immediately
49375
- - User asks to open a link, open a URL, or visit a website → open-url it immediately, do NOT use fetch
49376
- - User asks to delete a file → delete-file it immediately (requires approval)
49377
- - User asks to delete a folder or directory → delete-folder it immediately (requires approval)
49378
- - 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
49379
- - User asks to read a file → read-file it immediately, NEVER use shell cat/type
49380
- - 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
49381
- - User asks to explore a folder or directory → read-folder it immediately
49382
- - User asks to save/create/write a file → write-file it immediately, then read-file to verify
49383
- - User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
49384
- - User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
49385
- - 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.
49386
- - You are unsure about an API, library, error, concept, or piece of code → search it immediately
49387
- - User asks about something recent or that you might not know → search it immediately
49388
- - You are about to say "I'm not sure" or "I don't know" → search instead of guessing
49389
-
49390
- ## shell IS ONLY FOR:
49391
- - Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
49392
- - Installing packages: \`npm install\`, \`pip install\`
49393
- - Building/testing: \`npm run build\`, \`bun test\`
49394
- - Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
49395
- - Anything that EXECUTES — not reads or lists
49396
-
49397
49310
  ## CODEBASE
49398
49311
 
49399
49312
  ${fileList.length > 0 ? fileList : "(no files indexed)"}
49400
49313
 
49401
49314
  ${memorySummary}`;
49402
49315
  }
49316
+ var BUILTIN_TOOLS_SECTION = `## TOOLS
49317
+
49318
+ You have exactly thirteen tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
49319
+
49320
+ ### 1. fetch — load a URL
49321
+ <fetch>https://example.com</fetch>
49322
+
49323
+ ### 2. shell — run a terminal command
49324
+ <shell>node -v</shell>
49325
+
49326
+ ### 3. read-file — read a file from the repo
49327
+ <read-file>src/foo.ts</read-file>
49328
+
49329
+ ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
49330
+ <read-folder>src/components</read-folder>
49331
+
49332
+ ### 5. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
49333
+ <grep>
49334
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
49335
+ </grep>
49336
+
49337
+ ### 6. write-file — create or overwrite a file
49338
+ <write-file>
49339
+ {"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}
49340
+ </write-file>
49341
+
49342
+ ### 7. delete-file — permanently delete a single file
49343
+ <delete-file>src/old-component.tsx</delete-file>
49344
+
49345
+ ### 8. delete-folder — permanently delete a folder and all its contents
49346
+ <delete-folder>src/legacy</delete-folder>
49347
+
49348
+ ### 9. open-url — open a URL in the user's default browser
49349
+ <open-url>https://github.com/owner/repo</open-url>
49350
+
49351
+ ### 10. generate-pdf — generate a PDF file from markdown-style content
49352
+ <generate-pdf>
49353
+ {"path": "output/report.pdf", "content": "# Title\\n\\nSome body text.\\n\\n## Section\\n\\nMore content."}
49354
+ </generate-pdf>
49355
+
49356
+ ### 11. search — search the internet for anything you are unsure about
49357
+ <search>how to use React useEffect cleanup function</search>
49358
+
49359
+ ### 12. clone — clone a GitHub repo so you can explore and discuss it
49360
+ <clone>https://github.com/owner/repo</clone>
49361
+
49362
+ ### 13. changes — propose code edits (shown as a diff for user approval)
49363
+ <changes>
49364
+ {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
49365
+ </changes>`;
49403
49366
  // src/prompts/fewshot.ts
49404
49367
  var FEW_SHOT_MESSAGES = [
49405
49368
  {
@@ -49842,133 +49805,129 @@ Got it — I'll always use bun for this project.`
49842
49805
  Done — removed that memory.`
49843
49806
  }
49844
49807
  ];
49808
+ // src/utils/tools/registry.ts
49809
+ class ToolRegistry {
49810
+ tools = new Map;
49811
+ register(tool) {
49812
+ if (this.tools.has(tool.name)) {
49813
+ console.warn(`[ToolRegistry] Overwriting existing tool: "${tool.name}"`);
49814
+ }
49815
+ this.tools.set(tool.name, tool);
49816
+ }
49817
+ unregister(name) {
49818
+ this.tools.delete(name);
49819
+ }
49820
+ get(name) {
49821
+ return this.tools.get(name);
49822
+ }
49823
+ all() {
49824
+ return Array.from(this.tools.values());
49825
+ }
49826
+ names() {
49827
+ return Array.from(this.tools.keys());
49828
+ }
49829
+ buildSystemPromptSection() {
49830
+ const lines = [`## TOOLS
49831
+ `];
49832
+ lines.push("You have exactly " + this.tools.size + ` tools. To use a tool you MUST wrap it in the exact XML tags shown below — no other format will work.
49833
+ `);
49834
+ let i = 1;
49835
+ for (const tool of this.tools.values()) {
49836
+ lines.push(tool.systemPromptEntry(i++));
49837
+ }
49838
+ return lines.join(`
49839
+ `);
49840
+ }
49841
+ }
49842
+ var registry = new ToolRegistry;
49843
+
49845
49844
  // src/utils/chat.ts
49846
49845
  function parseResponse(text) {
49847
49846
  const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49848
49847
  const candidates = [];
49849
- const patterns = [
49850
- { kind: "fetch", re: /<fetch>([\s\S]*?)<\/fetch>/g },
49851
- { kind: "shell", re: /<shell>([\s\S]*?)<\/shell>/g },
49852
- { kind: "read-file", re: /<read-file>([\s\S]*?)<\/read-file>/g },
49853
- { kind: "read-folder", re: /<read-folder>([\s\S]*?)<\/read-folder>/g },
49854
- { kind: "grep", re: /<grep>([\s\S]*?)<\/grep>/g },
49855
- { kind: "delete-file", re: /<delete-file>([\s\S]*?)<\/delete-file>/g },
49856
- {
49857
- kind: "delete-folder",
49858
- re: /<delete-folder>([\s\S]*?)<\/delete-folder>/g
49859
- },
49860
- { kind: "open-url", re: /<open-url>([\s\S]*?)<\/open-url>/g },
49861
- { kind: "generate-pdf", re: /<generate-pdf>([\s\S]*?)<\/generate-pdf>/g },
49862
- { kind: "write-file", re: /<write-file>([\s\S]*?)<\/write-file>/g },
49863
- { kind: "search", re: /<search>([\s\S]*?)<\/search>/g },
49864
- { kind: "clone", re: /<clone>([\s\S]*?)<\/clone>/g },
49865
- { kind: "changes", re: /<changes>([\s\S]*?)<\/changes>/g },
49866
- { kind: "fetch", re: /```fetch\r?\n([\s\S]*?)\r?\n```/g },
49867
- { kind: "shell", re: /```shell\r?\n([\s\S]*?)\r?\n```/g },
49868
- { kind: "read-file", re: /```read-file\r?\n([\s\S]*?)\r?\n```/g },
49869
- { kind: "read-folder", re: /```read-folder\r?\n([\s\S]*?)\r?\n```/g },
49870
- { kind: "write-file", re: /```write-file\r?\n([\s\S]*?)\r?\n```/g },
49871
- { kind: "search", re: /```search\r?\n([\s\S]*?)\r?\n```/g },
49872
- { kind: "changes", re: /```changes\r?\n([\s\S]*?)\r?\n```/g }
49873
- ];
49874
- for (const { kind: kind2, re } of patterns) {
49875
- re.lastIndex = 0;
49876
- const m = re.exec(scanText);
49877
- if (m) {
49878
- const originalRe = new RegExp(re.source, re.flags.replace("g", ""));
49879
- const originalMatch = originalRe.exec(text.slice(m.index));
49880
- if (originalMatch) {
49881
- const fakeMatch = Object.assign([
49882
- text.slice(m.index, m.index + originalMatch[0].length),
49883
- originalMatch[1]
49884
- ], { index: m.index, input: text, groups: undefined });
49885
- candidates.push({ index: m.index, kind: kind2, match: fakeMatch });
49848
+ for (const toolName2 of registry.names()) {
49849
+ const escaped = toolName2.replace(/[-]/g, "\\-");
49850
+ const xmlRe = new RegExp(`<${escaped}>([\\s\\S]*?)<\\/${escaped}>`, "g");
49851
+ xmlRe.lastIndex = 0;
49852
+ const xmlM = xmlRe.exec(scanText);
49853
+ if (xmlM) {
49854
+ const orig = new RegExp(xmlRe.source);
49855
+ const origM = orig.exec(text.slice(xmlM.index));
49856
+ if (origM) {
49857
+ candidates.push({
49858
+ index: xmlM.index,
49859
+ toolName: toolName2,
49860
+ match: Object.assign([
49861
+ text.slice(xmlM.index, xmlM.index + origM[0].length),
49862
+ origM[1]
49863
+ ], { index: xmlM.index, input: text, groups: undefined })
49864
+ });
49865
+ }
49866
+ }
49867
+ const fencedRe = new RegExp(`\`\`\`${escaped}\\r?\\n([\\s\\S]*?)\\r?\\n\`\`\``, "g");
49868
+ fencedRe.lastIndex = 0;
49869
+ const fencedM = fencedRe.exec(scanText);
49870
+ if (fencedM) {
49871
+ const orig = new RegExp(fencedRe.source);
49872
+ const origM = orig.exec(text.slice(fencedM.index));
49873
+ if (origM) {
49874
+ candidates.push({
49875
+ index: fencedM.index,
49876
+ toolName: toolName2,
49877
+ match: Object.assign([
49878
+ text.slice(fencedM.index, fencedM.index + origM[0].length),
49879
+ origM[1]
49880
+ ], { index: fencedM.index, input: text, groups: undefined })
49881
+ });
49886
49882
  }
49887
49883
  }
49888
49884
  }
49889
49885
  if (candidates.length === 0)
49890
49886
  return { kind: "text", content: text.trim() };
49891
49887
  candidates.sort((a, b) => a.index - b.index);
49892
- const { kind, match } = candidates[0];
49893
- const before2 = text.slice(0, match.index).replace(/<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g, "").trim();
49888
+ const { toolName, match } = candidates[0];
49889
+ const before2 = text.slice(0, match.index).trim();
49894
49890
  const body = (match[1] ?? "").trim();
49895
- if (kind === "changes") {
49891
+ const afterMatch = text.slice(match.index + match[0].length).trim();
49892
+ const remainder = afterMatch.length > 0 ? afterMatch : undefined;
49893
+ if (toolName === "changes") {
49896
49894
  try {
49897
49895
  const parsed = JSON.parse(body);
49898
49896
  const display = [before2, parsed.summary].filter(Boolean).join(`
49899
49897
 
49900
49898
  `);
49901
- return { kind: "changes", content: display, patches: parsed.patches };
49902
- } catch {}
49903
- }
49904
- if (kind === "shell")
49905
- return { kind: "shell", content: before2, command: body };
49906
- if (kind === "fetch")
49907
- return {
49908
- kind: "fetch",
49909
- content: before2,
49910
- url: body.replace(/^<|>$/g, "").trim()
49911
- };
49912
- if (kind === "read-file")
49913
- return { kind: "read-file", content: before2, filePath: body };
49914
- if (kind === "read-folder")
49915
- return { kind: "read-folder", content: before2, folderPath: body };
49916
- if (kind === "delete-file")
49917
- return { kind: "delete-file", content: before2, filePath: body };
49918
- if (kind === "delete-folder")
49919
- return { kind: "delete-folder", content: before2, folderPath: body };
49920
- if (kind === "open-url")
49921
- return {
49922
- kind: "open-url",
49923
- content: before2,
49924
- url: body.replace(/^<|>$/g, "").trim()
49925
- };
49926
- if (kind === "search")
49927
- return { kind: "search", content: before2, query: body };
49928
- if (kind === "clone")
49929
- return {
49930
- kind: "clone",
49931
- content: before2,
49932
- repoUrl: body.replace(/^<|>$/g, "").trim()
49933
- };
49934
- if (kind === "generate-pdf") {
49935
- try {
49936
- const parsed = JSON.parse(body);
49937
- return {
49938
- kind: "generate-pdf",
49939
- content: before2,
49940
- filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
49941
- pdfContent: parsed.content ?? ""
49942
- };
49943
- } catch {
49944
- return { kind: "text", content: text };
49945
- }
49946
- }
49947
- if (kind === "grep") {
49948
- try {
49949
- const parsed = JSON.parse(body);
49950
49899
  return {
49951
- kind: "grep",
49952
- content: before2,
49953
- pattern: parsed.pattern,
49954
- glob: parsed.glob ?? "**/*"
49900
+ kind: "changes",
49901
+ content: display,
49902
+ patches: parsed.patches,
49903
+ remainder
49955
49904
  };
49956
49905
  } catch {
49957
- return { kind: "grep", content: before2, pattern: body, glob: "**/*" };
49906
+ return { kind: "text", content: text.trim() };
49958
49907
  }
49959
49908
  }
49960
- if (kind === "write-file") {
49961
- try {
49962
- const parsed = JSON.parse(body);
49963
- return {
49964
- kind: "write-file",
49965
- content: before2,
49966
- filePath: parsed.path,
49967
- fileContent: parsed.content
49968
- };
49969
- } catch {}
49909
+ if (toolName === "clone") {
49910
+ return {
49911
+ kind: "clone",
49912
+ content: before2,
49913
+ repoUrl: body.replace(/^<|>$/g, "").trim(),
49914
+ remainder
49915
+ };
49970
49916
  }
49971
- return { kind: "text", content: text.trim() };
49917
+ const tool = registry.get(toolName);
49918
+ if (!tool)
49919
+ return { kind: "text", content: text.trim() };
49920
+ const input = tool.parseInput(body);
49921
+ if (input === null)
49922
+ return { kind: "text", content: text.trim() };
49923
+ return {
49924
+ kind: "tool",
49925
+ toolName,
49926
+ input,
49927
+ rawInput: body,
49928
+ content: before2,
49929
+ remainder
49930
+ };
49972
49931
  }
49973
49932
  function extractGithubUrl(text) {
49974
49933
  const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
@@ -49987,10 +49946,9 @@ function buildApiMessages(messages) {
49987
49946
  content: "The tool call was denied by the user. Please respond without using that tool."
49988
49947
  };
49989
49948
  }
49990
- const label = m.toolName === "shell" ? `shell command \`${m.content}\`` : m.toolName === "fetch" ? `fetch of ${m.content}` : m.toolName === "read-file" ? `read-file of ${m.content}` : m.toolName === "read-folder" ? `read-folder of ${m.content}` : m.toolName === "grep" ? `grep for "${m.content}"` : m.toolName === "delete-file" ? `delete-file of ${m.content}` : m.toolName === "delete-folder" ? `delete-folder of ${m.content}` : m.toolName === "open-url" ? `open-url ${m.content}` : m.toolName === "generate-pdf" ? `generate-pdf to ${m.content}` : m.toolName === "search" ? `web search for "${m.content}"` : `write-file to ${m.content}`;
49991
49949
  return {
49992
49950
  role: "user",
49993
- content: `Here is the output from the ${label}:
49951
+ content: `Here is the output from the ${m.toolName} of ${m.content}:
49994
49952
 
49995
49953
  ${m.result}
49996
49954
 
@@ -50489,50 +50447,71 @@ function PermissionPrompt({
50489
50447
  let icon;
50490
50448
  let label;
50491
50449
  let value;
50492
- if (tool.type === "shell") {
50493
- icon = "$";
50494
- label = "run";
50495
- value = tool.command;
50496
- } else if (tool.type === "fetch") {
50497
- icon = "~>";
50498
- label = "fetch";
50499
- value = tool.url;
50500
- } else if (tool.type === "read-file") {
50501
- icon = "r";
50502
- label = "read";
50503
- value = tool.filePath;
50504
- } else if (tool.type === "read-folder") {
50505
- icon = "d";
50506
- label = "folder";
50507
- value = tool.folderPath;
50508
- } else if (tool.type === "grep") {
50509
- icon = "/";
50510
- label = "grep";
50511
- value = `${tool.pattern} ${tool.glob}`;
50512
- } else if (tool.type === "delete-file") {
50513
- icon = "x";
50514
- label = "delete";
50515
- value = tool.filePath;
50516
- } else if (tool.type === "delete-folder") {
50517
- icon = "X";
50518
- label = "delete folder";
50519
- value = tool.folderPath;
50520
- } else if (tool.type === "open-url") {
50521
- icon = "↗";
50522
- label = "open";
50523
- value = tool.url;
50524
- } else if (tool.type === "generate-pdf") {
50525
- icon = "P";
50526
- label = "pdf";
50527
- value = tool.filePath;
50528
- } else if (tool.type === "write-file") {
50529
- icon = "w";
50530
- label = "write";
50531
- value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50450
+ if ("_label" in tool) {
50451
+ const iconMap = {
50452
+ run: "$",
50453
+ fetch: "~>",
50454
+ read: "r",
50455
+ write: "w",
50456
+ delete: "x",
50457
+ "delete folder": "X",
50458
+ open: "",
50459
+ pdf: "P",
50460
+ search: "?",
50461
+ folder: "d",
50462
+ grep: "/",
50463
+ clone: "",
50464
+ query: ""
50465
+ };
50466
+ icon = iconMap[tool._label] ?? "·";
50467
+ label = tool._label;
50468
+ value = tool._display;
50532
50469
  } else {
50533
- icon = "?";
50534
- label = "search";
50535
- value = tool.query;
50470
+ if (tool.type === "shell") {
50471
+ icon = "$";
50472
+ label = "run";
50473
+ value = tool.command;
50474
+ } else if (tool.type === "fetch") {
50475
+ icon = "~>";
50476
+ label = "fetch";
50477
+ value = tool.url;
50478
+ } else if (tool.type === "read-file") {
50479
+ icon = "r";
50480
+ label = "read";
50481
+ value = tool.filePath;
50482
+ } else if (tool.type === "read-folder") {
50483
+ icon = "d";
50484
+ label = "folder";
50485
+ value = tool.folderPath;
50486
+ } else if (tool.type === "grep") {
50487
+ icon = "/";
50488
+ label = "grep";
50489
+ value = `${tool.pattern} ${tool.glob}`;
50490
+ } else if (tool.type === "delete-file") {
50491
+ icon = "x";
50492
+ label = "delete";
50493
+ value = tool.filePath;
50494
+ } else if (tool.type === "delete-folder") {
50495
+ icon = "X";
50496
+ label = "delete folder";
50497
+ value = tool.folderPath;
50498
+ } else if (tool.type === "open-url") {
50499
+ icon = "↗";
50500
+ label = "open";
50501
+ value = tool.url;
50502
+ } else if (tool.type === "generate-pdf") {
50503
+ icon = "P";
50504
+ label = "pdf";
50505
+ value = tool.filePath;
50506
+ } else if (tool.type === "write-file") {
50507
+ icon = "w";
50508
+ label = "write";
50509
+ value = `${tool.filePath} (${tool.fileContent.length} bytes)`;
50510
+ } else {
50511
+ icon = "?";
50512
+ label = "search";
50513
+ value = tool.query ?? "";
50514
+ }
50536
50515
  }
50537
50516
  return /* @__PURE__ */ jsx_dev_runtime20.jsxDEV(Box_default, {
50538
50517
  flexDirection: "column",
@@ -52555,8 +52534,7 @@ var ChatRunner = ({ repoPath }) => {
52555
52534
  };
52556
52535
  const abortControllerRef = import_react49.useRef(null);
52557
52536
  const toolResultCache = import_react49.useRef(new Map);
52558
- const inputBuffer = import_react49.useRef("");
52559
- const flushTimer = import_react49.useRef(null);
52537
+ const batchApprovedRef = import_react49.useRef(false);
52560
52538
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
52561
52539
  import_react48.default.useEffect(() => {
52562
52540
  const chats = listChats(repoPath);
@@ -52567,22 +52545,8 @@ var ChatRunner = ({ repoPath }) => {
52567
52545
  saveChat(chatNameRef.current, repoPath, allMessages);
52568
52546
  }
52569
52547
  }, [allMessages]);
52570
- const flushBuffer = () => {
52571
- const buf = inputBuffer.current;
52572
- if (!buf)
52573
- return;
52574
- inputBuffer.current = "";
52575
- setInputValue((v) => v + buf);
52576
- };
52577
- const scheduleFlush = () => {
52578
- if (flushTimer.current !== null)
52579
- return;
52580
- flushTimer.current = setTimeout(() => {
52581
- flushTimer.current = null;
52582
- flushBuffer();
52583
- }, 16);
52584
- };
52585
52548
  const handleError = (currentAll) => (err) => {
52549
+ batchApprovedRef.current = false;
52586
52550
  if (err instanceof Error && err.name === "AbortError") {
52587
52551
  setStage({ type: "idle" });
52588
52552
  return;
@@ -52598,6 +52562,7 @@ var ChatRunner = ({ repoPath }) => {
52598
52562
  };
52599
52563
  const processResponse = (raw, currentAll, signal) => {
52600
52564
  if (signal.aborted) {
52565
+ batchApprovedRef.current = false;
52601
52566
  setStage({ type: "idle" });
52602
52567
  return;
52603
52568
  }
@@ -52620,14 +52585,15 @@ var ChatRunner = ({ repoPath }) => {
52620
52585
  const cleanRaw = raw.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "").replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "").trim();
52621
52586
  const parsed = parseResponse(cleanRaw);
52622
52587
  if (parsed.kind === "changes") {
52588
+ batchApprovedRef.current = false;
52623
52589
  if (parsed.patches.length === 0) {
52624
- const msg2 = {
52590
+ const msg = {
52625
52591
  role: "assistant",
52626
52592
  content: parsed.content,
52627
52593
  type: "text"
52628
52594
  };
52629
- setAllMessages([...currentAll, msg2]);
52630
- setCommitted((prev) => [...prev, msg2]);
52595
+ setAllMessages([...currentAll, msg]);
52596
+ setCommitted((prev) => [...prev, msg]);
52631
52597
  setStage({ type: "idle" });
52632
52598
  return;
52633
52599
  }
@@ -52651,144 +52617,8 @@ var ChatRunner = ({ repoPath }) => {
52651
52617
  });
52652
52618
  return;
52653
52619
  }
52654
- if (parsed.kind === "shell" || parsed.kind === "fetch" || parsed.kind === "read-file" || parsed.kind === "read-folder" || parsed.kind === "grep" || parsed.kind === "write-file" || parsed.kind === "delete-file" || parsed.kind === "delete-folder" || parsed.kind === "open-url" || parsed.kind === "generate-pdf" || parsed.kind === "search") {
52655
- let tool;
52656
- if (parsed.kind === "shell") {
52657
- tool = { type: "shell", command: parsed.command };
52658
- } else if (parsed.kind === "fetch") {
52659
- tool = { type: "fetch", url: parsed.url };
52660
- } else if (parsed.kind === "read-file") {
52661
- tool = { type: "read-file", filePath: parsed.filePath };
52662
- } else if (parsed.kind === "read-folder") {
52663
- tool = { type: "read-folder", folderPath: parsed.folderPath };
52664
- } else if (parsed.kind === "grep") {
52665
- tool = { type: "grep", pattern: parsed.pattern, glob: parsed.glob };
52666
- } else if (parsed.kind === "delete-file") {
52667
- tool = { type: "delete-file", filePath: parsed.filePath };
52668
- } else if (parsed.kind === "delete-folder") {
52669
- tool = { type: "delete-folder", folderPath: parsed.folderPath };
52670
- } else if (parsed.kind === "open-url") {
52671
- tool = { type: "open-url", url: parsed.url };
52672
- } else if (parsed.kind === "generate-pdf") {
52673
- tool = {
52674
- type: "generate-pdf",
52675
- filePath: parsed.filePath,
52676
- content: parsed.pdfContent
52677
- };
52678
- } else if (parsed.kind === "search") {
52679
- tool = { type: "search", query: parsed.query };
52680
- } else {
52681
- tool = {
52682
- type: "write-file",
52683
- filePath: parsed.filePath,
52684
- fileContent: parsed.fileContent
52685
- };
52686
- }
52687
- if (parsed.content) {
52688
- const preambleMsg = {
52689
- role: "assistant",
52690
- content: parsed.content,
52691
- type: "text"
52692
- };
52693
- setAllMessages([...currentAll, preambleMsg]);
52694
- setCommitted((prev) => [...prev, preambleMsg]);
52695
- }
52696
- const isSafeTool = parsed.kind === "read-file" || parsed.kind === "read-folder" || parsed.kind === "grep" || parsed.kind === "fetch" || parsed.kind === "open-url" || parsed.kind === "search";
52697
- const executeAndContinue = async (approved) => {
52698
- let result2 = "(denied by user)";
52699
- if (approved) {
52700
- const cacheKey = parsed.kind === "read-file" ? `read-file:${parsed.filePath}` : parsed.kind === "read-folder" ? `read-folder:${parsed.folderPath}` : parsed.kind === "grep" ? `grep:${parsed.pattern}:${parsed.glob}` : null;
52701
- if (cacheKey && toolResultCache.current.has(cacheKey)) {
52702
- result2 = toolResultCache.current.get(cacheKey) + `
52703
-
52704
- [NOTE: This result was already retrieved earlier. Do not request it again.]`;
52705
- } else {
52706
- try {
52707
- setStage({ type: "thinking" });
52708
- if (parsed.kind === "shell") {
52709
- result2 = await runShell(parsed.command, repoPath);
52710
- } else if (parsed.kind === "fetch") {
52711
- result2 = await fetchUrl(parsed.url);
52712
- } else if (parsed.kind === "read-file") {
52713
- result2 = readFile(parsed.filePath, repoPath);
52714
- } else if (parsed.kind === "read-folder") {
52715
- result2 = readFolder(parsed.folderPath, repoPath);
52716
- } else if (parsed.kind === "grep") {
52717
- result2 = grepFiles(parsed.pattern, parsed.glob, repoPath);
52718
- } else if (parsed.kind === "delete-file") {
52719
- result2 = deleteFile(parsed.filePath, repoPath);
52720
- } else if (parsed.kind === "delete-folder") {
52721
- result2 = deleteFolder(parsed.folderPath, repoPath);
52722
- } else if (parsed.kind === "open-url") {
52723
- result2 = openUrl(parsed.url);
52724
- } else if (parsed.kind === "generate-pdf") {
52725
- result2 = generatePdf(parsed.filePath, parsed.pdfContent, repoPath);
52726
- } else if (parsed.kind === "write-file") {
52727
- result2 = writeFile(parsed.filePath, parsed.fileContent, repoPath);
52728
- } else if (parsed.kind === "search") {
52729
- result2 = await searchWeb(parsed.query);
52730
- }
52731
- if (cacheKey) {
52732
- toolResultCache.current.set(cacheKey, result2);
52733
- }
52734
- } catch (err) {
52735
- result2 = `Error: ${err instanceof Error ? err.message : "failed"}`;
52736
- }
52737
- }
52738
- }
52739
- if (approved && !result2.startsWith("Error:")) {
52740
- const kindMap2 = {
52741
- shell: "shell-run",
52742
- fetch: "url-fetched",
52743
- "read-file": "file-read",
52744
- "read-folder": "file-read",
52745
- grep: "file-read",
52746
- "delete-file": "file-written",
52747
- "delete-folder": "file-written",
52748
- "open-url": "url-fetched",
52749
- "generate-pdf": "file-written",
52750
- "write-file": "file-written",
52751
- search: "url-fetched"
52752
- };
52753
- appendMemory({
52754
- kind: kindMap2[parsed.kind] ?? "shell-run",
52755
- detail: parsed.kind === "shell" ? parsed.command : parsed.kind === "fetch" ? parsed.url : parsed.kind === "search" ? parsed.query : parsed.kind === "read-folder" ? parsed.folderPath : parsed.kind === "grep" ? `${parsed.pattern} ${parsed.glob}` : parsed.kind === "delete-file" ? parsed.filePath : parsed.kind === "delete-folder" ? parsed.folderPath : parsed.kind === "open-url" ? parsed.url : parsed.kind === "generate-pdf" ? parsed.filePath : parsed.filePath,
52756
- summary: result2.split(`
52757
- `)[0]?.slice(0, 120) ?? "",
52758
- repoPath
52759
- });
52760
- }
52761
- const toolName = parsed.kind === "shell" ? "shell" : parsed.kind === "fetch" ? "fetch" : parsed.kind === "read-file" ? "read-file" : parsed.kind === "read-folder" ? "read-folder" : parsed.kind === "grep" ? "grep" : parsed.kind === "delete-file" ? "delete-file" : parsed.kind === "delete-folder" ? "delete-folder" : parsed.kind === "open-url" ? "open-url" : parsed.kind === "generate-pdf" ? "generate-pdf" : parsed.kind === "search" ? "search" : "write-file";
52762
- const toolContent = parsed.kind === "shell" ? parsed.command : parsed.kind === "fetch" ? parsed.url : parsed.kind === "search" ? parsed.query : parsed.kind === "read-folder" ? parsed.folderPath : parsed.kind === "grep" ? `${parsed.pattern} — ${parsed.glob}` : parsed.kind === "delete-file" ? parsed.filePath : parsed.kind === "delete-folder" ? parsed.folderPath : parsed.kind === "open-url" ? parsed.url : parsed.kind === "generate-pdf" ? parsed.filePath : parsed.filePath;
52763
- const toolMsg = {
52764
- role: "assistant",
52765
- type: "tool",
52766
- toolName,
52767
- content: toolContent,
52768
- result: result2,
52769
- approved
52770
- };
52771
- const withTool = [...currentAll, toolMsg];
52772
- setAllMessages(withTool);
52773
- setCommitted((prev) => [...prev, toolMsg]);
52774
- const nextAbort = new AbortController;
52775
- abortControllerRef.current = nextAbort;
52776
- setStage({ type: "thinking" });
52777
- callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52778
- };
52779
- if (autoApprove && isSafeTool) {
52780
- executeAndContinue(true);
52781
- return;
52782
- }
52783
- setStage({
52784
- type: "permission",
52785
- tool,
52786
- pendingMessages: currentAll,
52787
- resolve: executeAndContinue
52788
- });
52789
- return;
52790
- }
52791
52620
  if (parsed.kind === "clone") {
52621
+ batchApprovedRef.current = false;
52792
52622
  if (parsed.content) {
52793
52623
  const preambleMsg = {
52794
52624
  role: "assistant",
@@ -52805,23 +52635,116 @@ var ChatRunner = ({ repoPath }) => {
52805
52635
  });
52806
52636
  return;
52807
52637
  }
52808
- const msg = {
52809
- role: "assistant",
52810
- content: parsed.content,
52811
- type: "text"
52812
- };
52813
- const withMsg = [...currentAll, msg];
52814
- setAllMessages(withMsg);
52815
- setCommitted((prev) => [...prev, msg]);
52816
- const lastUserMsg = [...currentAll].reverse().find((m) => m.role === "user");
52817
- const githubUrl = lastUserMsg ? extractGithubUrl(lastUserMsg.content) : null;
52818
- if (githubUrl && !clonedUrls.has(githubUrl)) {
52819
- setTimeout(() => {
52820
- setStage({ type: "clone-offer", repoUrl: githubUrl });
52821
- }, 80);
52822
- } else {
52638
+ if (parsed.kind === "text") {
52639
+ batchApprovedRef.current = false;
52640
+ const msg = {
52641
+ role: "assistant",
52642
+ content: parsed.content,
52643
+ type: "text"
52644
+ };
52645
+ const withMsg = [...currentAll, msg];
52646
+ setAllMessages(withMsg);
52647
+ setCommitted((prev) => [...prev, msg]);
52648
+ const lastUserMsg = [...currentAll].reverse().find((m) => m.role === "user");
52649
+ const githubUrl = lastUserMsg ? extractGithubUrl(lastUserMsg.content) : null;
52650
+ if (githubUrl && !clonedUrls.has(githubUrl)) {
52651
+ setTimeout(() => setStage({ type: "clone-offer", repoUrl: githubUrl }), 80);
52652
+ } else {
52653
+ setStage({ type: "idle" });
52654
+ }
52655
+ return;
52656
+ }
52657
+ const tool = registry.get(parsed.toolName);
52658
+ if (!tool) {
52659
+ batchApprovedRef.current = false;
52823
52660
  setStage({ type: "idle" });
52661
+ return;
52662
+ }
52663
+ if (parsed.content) {
52664
+ const preambleMsg = {
52665
+ role: "assistant",
52666
+ content: parsed.content,
52667
+ type: "text"
52668
+ };
52669
+ setAllMessages([...currentAll, preambleMsg]);
52670
+ setCommitted((prev) => [...prev, preambleMsg]);
52671
+ }
52672
+ const remainder = parsed.remainder;
52673
+ const isSafe = tool.safe ?? false;
52674
+ const executeAndContinue = async (approved) => {
52675
+ if (approved && remainder) {
52676
+ batchApprovedRef.current = true;
52677
+ }
52678
+ let result2 = "(denied by user)";
52679
+ if (approved) {
52680
+ const cacheKey = isSafe ? `${parsed.toolName}:${parsed.rawInput}` : null;
52681
+ if (cacheKey && toolResultCache.current.has(cacheKey)) {
52682
+ result2 = toolResultCache.current.get(cacheKey) + `
52683
+
52684
+ [NOTE: This result was already retrieved earlier. Do not request it again.]`;
52685
+ } else {
52686
+ try {
52687
+ setStage({ type: "thinking" });
52688
+ const toolResult = await tool.execute(parsed.input, {
52689
+ repoPath,
52690
+ messages: currentAll
52691
+ });
52692
+ result2 = toolResult.value;
52693
+ if (cacheKey && toolResult.kind === "text") {
52694
+ toolResultCache.current.set(cacheKey, result2);
52695
+ }
52696
+ } catch (err) {
52697
+ result2 = `Error: ${err instanceof Error ? err.message : "failed"}`;
52698
+ }
52699
+ }
52700
+ }
52701
+ if (approved && !result2.startsWith("Error:")) {
52702
+ appendMemory({
52703
+ kind: "shell-run",
52704
+ detail: tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput,
52705
+ summary: result2.split(`
52706
+ `)[0]?.slice(0, 120) ?? "",
52707
+ repoPath
52708
+ });
52709
+ }
52710
+ const displayContent = tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput;
52711
+ const toolMsg = {
52712
+ role: "assistant",
52713
+ type: "tool",
52714
+ toolName: parsed.toolName,
52715
+ content: displayContent,
52716
+ result: result2,
52717
+ approved
52718
+ };
52719
+ const withTool = [...currentAll, toolMsg];
52720
+ setAllMessages(withTool);
52721
+ setCommitted((prev) => [...prev, toolMsg]);
52722
+ if (approved && remainder && remainder.length > 0) {
52723
+ processResponse(remainder, withTool, signal);
52724
+ return;
52725
+ }
52726
+ batchApprovedRef.current = false;
52727
+ const nextAbort = new AbortController;
52728
+ abortControllerRef.current = nextAbort;
52729
+ setStage({ type: "thinking" });
52730
+ callChat(provider, systemPrompt, withTool, nextAbort.signal).then((r) => processResponse(r, withTool, nextAbort.signal)).catch(handleError(withTool));
52731
+ };
52732
+ if (autoApprove && isSafe || batchApprovedRef.current) {
52733
+ executeAndContinue(true);
52734
+ return;
52824
52735
  }
52736
+ const permLabel = tool.permissionLabel ?? tool.name;
52737
+ const permValue = tool.summariseInput ? String(tool.summariseInput(parsed.input)) : parsed.rawInput;
52738
+ setStage({
52739
+ type: "permission",
52740
+ tool: {
52741
+ type: parsed.toolName,
52742
+ _display: permValue,
52743
+ _label: permLabel
52744
+ },
52745
+ pendingMessages: currentAll,
52746
+ resolve: executeAndContinue
52747
+ });
52825
52748
  };
52826
52749
  const sendMessage = (text) => {
52827
52750
  if (!provider)
@@ -52839,7 +52762,7 @@ var ChatRunner = ({ repoPath }) => {
52839
52762
  setAutoApprove(next);
52840
52763
  const msg = {
52841
52764
  role: "assistant",
52842
- content: next ? "Auto-approve ON — read, search, grep and folder tools will run without asking. Write and code changes still require approval." : "Auto-approve OFF — all tools will ask for permission.",
52765
+ content: next ? "Auto-approve ON — safe tools (read, search, fetch) will run without asking." : "Auto-approve OFF — all tools will ask for permission.",
52843
52766
  type: "text"
52844
52767
  };
52845
52768
  setCommitted((prev) => [...prev, msg]);
@@ -52848,13 +52771,13 @@ var ChatRunner = ({ repoPath }) => {
52848
52771
  }
52849
52772
  if (text.trim().toLowerCase() === "/clear history") {
52850
52773
  clearRepoMemory(repoPath);
52851
- const clearedMsg = {
52774
+ const msg = {
52852
52775
  role: "assistant",
52853
52776
  content: "History cleared for this repo.",
52854
52777
  type: "text"
52855
52778
  };
52856
- setCommitted((prev) => [...prev, clearedMsg]);
52857
- setAllMessages((prev) => [...prev, clearedMsg]);
52779
+ setCommitted((prev) => [...prev, msg]);
52780
+ setAllMessages((prev) => [...prev, msg]);
52858
52781
  return;
52859
52782
  }
52860
52783
  if (text.trim().toLowerCase() === "/chat") {
@@ -52901,7 +52824,7 @@ var ChatRunner = ({ repoPath }) => {
52901
52824
  if (!name) {
52902
52825
  const msg2 = {
52903
52826
  role: "assistant",
52904
- content: "Usage: `/chat delete <name>`",
52827
+ content: "Usage: `/chat delete <n>`",
52905
52828
  type: "text"
52906
52829
  };
52907
52830
  setCommitted((prev) => [...prev, msg2]);
@@ -53052,6 +52975,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53052
52975
  setCommitted((prev) => [...prev, userMsg]);
53053
52976
  setAllMessages(nextAll);
53054
52977
  toolResultCache.current.clear();
52978
+ batchApprovedRef.current = false;
53055
52979
  inputHistoryRef.current = [
53056
52980
  text,
53057
52981
  ...inputHistoryRef.current.filter((m) => m !== text)
@@ -53074,6 +52998,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53074
52998
  if (stage.type === "thinking" && key.escape) {
53075
52999
  abortControllerRef.current?.abort();
53076
53000
  abortControllerRef.current = null;
53001
+ batchApprovedRef.current = false;
53077
53002
  setStage({ type: "idle" });
53078
53003
  return;
53079
53004
  }
@@ -53092,8 +53017,7 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53092
53017
  if (key.downArrow) {
53093
53018
  const next = historyIndexRef.current - 1;
53094
53019
  historyIndexRef.current = next;
53095
- const val = next < 0 ? "" : inputHistoryRef.current[next];
53096
- setInputValue(val);
53020
+ setInputValue(next < 0 ? "" : inputHistoryRef.current[next]);
53097
53021
  setInputKey((k) => k + 1);
53098
53022
  return;
53099
53023
  }
@@ -53153,16 +53077,14 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53153
53077
  if (stage.type === "clone-exists") {
53154
53078
  if (input === "y" || input === "Y") {
53155
53079
  const { repoUrl, repoPath: existingPath } = stage;
53156
- const cloneUrl = toCloneUrl(repoUrl);
53157
53080
  setStage({ type: "cloning", repoUrl });
53158
- startCloneRepo(cloneUrl, { forceReclone: true }).then((result2) => {
53081
+ startCloneRepo(toCloneUrl(repoUrl), { forceReclone: true }).then((result2) => {
53159
53082
  if (result2.done) {
53160
- const fileCount = walkDir3(existingPath).length;
53161
53083
  setStage({
53162
53084
  type: "clone-done",
53163
53085
  repoUrl,
53164
53086
  destPath: existingPath,
53165
- fileCount
53087
+ fileCount: walkDir3(existingPath).length
53166
53088
  });
53167
53089
  } else {
53168
53090
  setStage({
@@ -53175,12 +53097,11 @@ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
53175
53097
  }
53176
53098
  if (input === "n" || input === "N") {
53177
53099
  const { repoUrl, repoPath: existingPath } = stage;
53178
- const fileCount = walkDir3(existingPath).length;
53179
53100
  setStage({
53180
53101
  type: "clone-done",
53181
53102
  repoUrl,
53182
53103
  destPath: existingPath,
53183
- fileCount
53104
+ fileCount: walkDir3(existingPath).length
53184
53105
  });
53185
53106
  return;
53186
53107
  }
@@ -53202,7 +53123,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
53202
53123
  type: "tool",
53203
53124
  toolName: "fetch",
53204
53125
  content: stage.repoUrl,
53205
- result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files. Use read-file with full path e.g. read-file ${stage.destPath}/README.md`,
53126
+ result: `Clone complete. Repo: ${repoName}. Local path: ${stage.destPath}. ${stage.fileCount} files.`,
53206
53127
  approved: true
53207
53128
  };
53208
53129
  const withClone = [...allMessages, contextMsg, summaryMsg];
@@ -53223,6 +53144,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
53223
53144
  return;
53224
53145
  }
53225
53146
  if (input === "n" || input === "N" || key.escape) {
53147
+ batchApprovedRef.current = false;
53226
53148
  stage.resolve(false);
53227
53149
  return;
53228
53150
  }
@@ -53312,17 +53234,16 @@ ${lensFile.overview}
53312
53234
 
53313
53235
  Important folders: ${lensFile.importantFolders.join(", ")}
53314
53236
  Suggestions: ${lensFile.suggestions.slice(0, 3).join("; ")}` : "";
53315
- setSystemPrompt(buildSystemPrompt(importantFiles, historySummary) + lensContext);
53316
- const historyNote = historySummary ? `
53317
-
53318
- I have memory of previous actions in this repo.` : "";
53319
- const lensGreetNote = lensFile ? `
53320
-
53321
- Found LENS.md — I have context from a previous analysis of this repo.` : "";
53237
+ const toolsSection = registry.buildSystemPromptSection();
53238
+ setSystemPrompt(buildSystemPrompt(importantFiles, historySummary, toolsSection) + lensContext);
53322
53239
  const greeting = {
53323
53240
  role: "assistant",
53324
- content: `Welcome to Lens
53325
- Codebase loaded — ${importantFiles.length} files indexed.${historyNote}${lensGreetNote}
53241
+ content: `Welcome to Lens
53242
+ Codebase loaded — ${importantFiles.length} files indexed.${historySummary ? `
53243
+
53244
+ I have memory of previous actions in this repo.` : ""}${lensFile ? `
53245
+
53246
+ Found LENS.md — I have context from a previous analysis of this repo.` : ""}
53326
53247
  Ask me anything, tell me what to build, share a URL, or ask me to read/write files.
53327
53248
 
53328
53249
  Tip: type /timeline to browse commit history.`,
@@ -53337,7 +53258,7 @@ Tip: type /timeline to browse commit history.`,
53337
53258
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ProviderPicker, {
53338
53259
  onDone: handleProviderDone
53339
53260
  }, undefined, false, undefined, this);
53340
- if (stage.type === "loading") {
53261
+ if (stage.type === "loading")
53341
53262
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
53342
53263
  gap: 1,
53343
53264
  marginTop: 1,
@@ -53357,19 +53278,16 @@ Tip: type /timeline to browse commit history.`,
53357
53278
  }, undefined, false, undefined, this)
53358
53279
  ]
53359
53280
  }, undefined, true, undefined, this);
53360
- }
53361
- if (showTimeline) {
53281
+ if (showTimeline)
53362
53282
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(TimelineRunner, {
53363
53283
  repoPath,
53364
53284
  onExit: () => setShowTimeline(false)
53365
53285
  }, undefined, false, undefined, this);
53366
- }
53367
- if (showReview) {
53286
+ if (showReview)
53368
53287
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ReviewCommand, {
53369
53288
  path: repoPath,
53370
53289
  onExit: () => setShowReview(false)
53371
53290
  }, undefined, false, undefined, this);
53372
- }
53373
53291
  if (stage.type === "clone-offer")
53374
53292
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(CloneOfferView, {
53375
53293
  stage,
@@ -53510,9 +53428,259 @@ var TimelineCommand = ({ path: inputPath }) => {
53510
53428
  repoPath: resolvedPath
53511
53429
  }, undefined, false, undefined, this);
53512
53430
  };
53431
+ // src/utils/tools/builtins.ts
53432
+ var fetchTool = {
53433
+ name: "fetch",
53434
+ description: "load a URL",
53435
+ safe: true,
53436
+ permissionLabel: "fetch",
53437
+ systemPromptEntry: (i) => `### ${i}. fetch — load a URL
53438
+ <fetch>https://example.com</fetch>`,
53439
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53440
+ summariseInput: (url) => url,
53441
+ execute: async (url) => {
53442
+ try {
53443
+ const value = await fetchUrl(url);
53444
+ return { kind: "text", value };
53445
+ } catch (err) {
53446
+ return {
53447
+ kind: "error",
53448
+ value: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`
53449
+ };
53450
+ }
53451
+ }
53452
+ };
53453
+ var shellTool = {
53454
+ name: "shell",
53455
+ description: "run a terminal command",
53456
+ safe: false,
53457
+ permissionLabel: "run",
53458
+ systemPromptEntry: (i) => `### ${i}. shell — run a terminal command
53459
+ <shell>node -v</shell>`,
53460
+ parseInput: (body) => body || null,
53461
+ summariseInput: (cmd) => cmd,
53462
+ execute: async (cmd, ctx) => {
53463
+ const value = await runShell(cmd, ctx.repoPath);
53464
+ return { kind: "text", value };
53465
+ }
53466
+ };
53467
+ var readFileTool = {
53468
+ name: "read-file",
53469
+ description: "read a file from the repo",
53470
+ safe: true,
53471
+ permissionLabel: "read",
53472
+ systemPromptEntry: (i) => `### ${i}. read-file — read a file from the repo
53473
+ <read-file>src/foo.ts</read-file>`,
53474
+ parseInput: (body) => body || null,
53475
+ summariseInput: (p) => p,
53476
+ execute: (filePath, ctx) => ({
53477
+ kind: "text",
53478
+ value: readFile(filePath, ctx.repoPath)
53479
+ })
53480
+ };
53481
+ var readFolderTool = {
53482
+ name: "read-folder",
53483
+ description: "list contents of a folder (files + subfolders, one level deep)",
53484
+ safe: true,
53485
+ permissionLabel: "folder",
53486
+ systemPromptEntry: (i) => `### ${i}. read-folder — list contents of a folder (files + subfolders, one level deep)
53487
+ <read-folder>src/components</read-folder>`,
53488
+ parseInput: (body) => body || null,
53489
+ summariseInput: (p) => p,
53490
+ execute: (folderPath, ctx) => ({
53491
+ kind: "text",
53492
+ value: readFolder(folderPath, ctx.repoPath)
53493
+ })
53494
+ };
53495
+ var grepTool = {
53496
+ name: "grep",
53497
+ description: "search for a pattern across files in the repo",
53498
+ safe: true,
53499
+ permissionLabel: "grep",
53500
+ systemPromptEntry: (i) => `### ${i}. grep — search for a pattern across files in the repo (cross-platform, no shell needed)
53501
+ <grep>
53502
+ {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
53503
+ </grep>`,
53504
+ parseInput: (body) => {
53505
+ try {
53506
+ const parsed = JSON.parse(body);
53507
+ return { pattern: parsed.pattern, glob: parsed.glob ?? "**/*" };
53508
+ } catch {
53509
+ return { pattern: body, glob: "**/*" };
53510
+ }
53511
+ },
53512
+ summariseInput: ({ pattern, glob }) => `${pattern} — ${glob}`,
53513
+ execute: ({ pattern, glob }, ctx) => ({
53514
+ kind: "text",
53515
+ value: grepFiles(pattern, glob, ctx.repoPath)
53516
+ })
53517
+ };
53518
+ var writeFileTool = {
53519
+ name: "write-file",
53520
+ description: "create or overwrite a file",
53521
+ safe: false,
53522
+ permissionLabel: "write",
53523
+ systemPromptEntry: (i) => `### ${i}. write-file — create or overwrite a file
53524
+ <write-file>
53525
+ {"path": "data/output.csv", "content": "col1,col2\\nval1,val2"}
53526
+ </write-file>`,
53527
+ parseInput: (body) => {
53528
+ try {
53529
+ const parsed = JSON.parse(body);
53530
+ if (!parsed.path)
53531
+ return null;
53532
+ return parsed;
53533
+ } catch {
53534
+ return null;
53535
+ }
53536
+ },
53537
+ summariseInput: ({ path: path21, content }) => `${path21} (${content.length} bytes)`,
53538
+ execute: ({ path: filePath, content }, ctx) => ({
53539
+ kind: "text",
53540
+ value: writeFile(filePath, content, ctx.repoPath)
53541
+ })
53542
+ };
53543
+ var deleteFileTool = {
53544
+ name: "delete-file",
53545
+ description: "permanently delete a single file",
53546
+ safe: false,
53547
+ permissionLabel: "delete",
53548
+ systemPromptEntry: (i) => `### ${i}. delete-file — permanently delete a single file
53549
+ <delete-file>src/old-component.tsx</delete-file>`,
53550
+ parseInput: (body) => body || null,
53551
+ summariseInput: (p) => p,
53552
+ execute: (filePath, ctx) => ({
53553
+ kind: "text",
53554
+ value: deleteFile(filePath, ctx.repoPath)
53555
+ })
53556
+ };
53557
+ var deleteFolderTool = {
53558
+ name: "delete-folder",
53559
+ description: "permanently delete a folder and all its contents",
53560
+ safe: false,
53561
+ permissionLabel: "delete folder",
53562
+ systemPromptEntry: (i) => `### ${i}. delete-folder — permanently delete a folder and all its contents
53563
+ <delete-folder>src/legacy</delete-folder>`,
53564
+ parseInput: (body) => body || null,
53565
+ summariseInput: (p) => p,
53566
+ execute: (folderPath, ctx) => ({
53567
+ kind: "text",
53568
+ value: deleteFolder(folderPath, ctx.repoPath)
53569
+ })
53570
+ };
53571
+ var openUrlTool = {
53572
+ name: "open-url",
53573
+ description: "open a URL in the user's default browser",
53574
+ safe: true,
53575
+ permissionLabel: "open",
53576
+ systemPromptEntry: (i) => `### ${i}. open-url — open a URL in the user's default browser
53577
+ <open-url>https://github.com/owner/repo</open-url>`,
53578
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53579
+ summariseInput: (url) => url,
53580
+ execute: (url) => ({ kind: "text", value: openUrl(url) })
53581
+ };
53582
+ var generatePdfTool = {
53583
+ name: "generate-pdf",
53584
+ description: "generate a PDF file from markdown-style content",
53585
+ safe: false,
53586
+ permissionLabel: "pdf",
53587
+ systemPromptEntry: (i) => `### ${i}. generate-pdf — generate a PDF file from markdown-style content
53588
+ <generate-pdf>
53589
+ {"path": "output/report.pdf", "content": "# Title\\n\\nSome body text."}
53590
+ </generate-pdf>`,
53591
+ parseInput: (body) => {
53592
+ try {
53593
+ const parsed = JSON.parse(body);
53594
+ return {
53595
+ filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
53596
+ content: parsed.content ?? ""
53597
+ };
53598
+ } catch {
53599
+ return null;
53600
+ }
53601
+ },
53602
+ summariseInput: ({ filePath }) => filePath,
53603
+ execute: ({ filePath, content }, ctx) => ({
53604
+ kind: "text",
53605
+ value: generatePdf(filePath, content, ctx.repoPath)
53606
+ })
53607
+ };
53608
+ var searchTool = {
53609
+ name: "search",
53610
+ description: "search the internet for anything you are unsure about",
53611
+ safe: true,
53612
+ permissionLabel: "search",
53613
+ systemPromptEntry: (i) => `### ${i}. search — search the internet for anything you are unsure about
53614
+ <search>how to use React useEffect cleanup function</search>`,
53615
+ parseInput: (body) => body || null,
53616
+ summariseInput: (q) => `"${q}"`,
53617
+ execute: async (query) => {
53618
+ try {
53619
+ const value = await searchWeb(query);
53620
+ return { kind: "text", value };
53621
+ } catch (err) {
53622
+ return {
53623
+ kind: "error",
53624
+ value: `Search failed: ${err instanceof Error ? err.message : String(err)}`
53625
+ };
53626
+ }
53627
+ }
53628
+ };
53629
+ var cloneTool = {
53630
+ name: "clone",
53631
+ description: "clone a GitHub repo so you can explore and discuss it",
53632
+ safe: false,
53633
+ permissionLabel: "clone",
53634
+ systemPromptEntry: (i) => `### ${i}. clone — clone a GitHub repo so you can explore and discuss it
53635
+ <clone>https://github.com/owner/repo</clone>`,
53636
+ parseInput: (body) => body.replace(/^<|>$/g, "").trim() || null,
53637
+ summariseInput: (url) => url,
53638
+ execute: (repoUrl) => ({
53639
+ kind: "text",
53640
+ value: `Clone of ${repoUrl} was handled by the UI.`
53641
+ })
53642
+ };
53643
+ var changesTool = {
53644
+ name: "changes",
53645
+ description: "propose code edits (shown as a diff for user approval)",
53646
+ safe: false,
53647
+ permissionLabel: "changes",
53648
+ systemPromptEntry: (i) => `### ${i}. changes — propose code edits (shown as a diff for user approval)
53649
+ <changes>
53650
+ {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
53651
+ </changes>`,
53652
+ parseInput: (body) => {
53653
+ try {
53654
+ return JSON.parse(body);
53655
+ } catch {
53656
+ return null;
53657
+ }
53658
+ },
53659
+ summariseInput: ({ summary }) => summary,
53660
+ execute: ({ summary }) => ({
53661
+ kind: "text",
53662
+ value: `Changes proposed: ${summary}`
53663
+ })
53664
+ };
53665
+ function registerBuiltins() {
53666
+ registry.register(fetchTool);
53667
+ registry.register(shellTool);
53668
+ registry.register(readFileTool);
53669
+ registry.register(readFolderTool);
53670
+ registry.register(grepTool);
53671
+ registry.register(writeFileTool);
53672
+ registry.register(deleteFileTool);
53673
+ registry.register(deleteFolderTool);
53674
+ registry.register(openUrlTool);
53675
+ registry.register(generatePdfTool);
53676
+ registry.register(searchTool);
53677
+ registry.register(cloneTool);
53678
+ registry.register(changesTool);
53679
+ }
53513
53680
 
53514
53681
  // src/index.tsx
53515
53682
  var jsx_dev_runtime25 = __toESM(require_jsx_dev_runtime(), 1);
53683
+ registerBuiltins();
53516
53684
  var program2 = new Command;
53517
53685
  program2.command("repo <url>").description("Analyze a remote repository").action((url) => {
53518
53686
  render_default(/* @__PURE__ */ jsx_dev_runtime25.jsxDEV(RepoCommand, {