@ridit/lens 0.1.9 → 0.2.1

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
@@ -44302,6 +44302,8 @@ function fetchFileTree(repoPath) {
44302
44302
  });
44303
44303
  }
44304
44304
  function readImportantFiles(repoPath, files) {
44305
+ if (files.length > 100)
44306
+ return [];
44305
44307
  return files.filter(isImportantFile).flatMap((filePath) => {
44306
44308
  try {
44307
44309
  const content = readFileSync2(path2.join(repoPath, filePath), "utf-8");
@@ -48630,15 +48632,16 @@ var TaskCommand = ({
48630
48632
  };
48631
48633
 
48632
48634
  // src/commands/chat.tsx
48633
- import { existsSync as existsSync12 } from "fs";
48634
- import path17 from "path";
48635
+ import { existsSync as existsSync14 } from "fs";
48636
+ import path19 from "path";
48635
48637
 
48636
48638
  // src/components/chat/ChatRunner.tsx
48637
48639
  var import_react48 = __toESM(require_react(), 1);
48638
- import path16 from "path";
48639
- import os7 from "os";
48640
+ var import_react49 = __toESM(require_react(), 1);
48641
+ import path18 from "path";
48642
+ import os9 from "os";
48640
48643
 
48641
- // src/utils/chat.ts
48644
+ // src/tools/files.ts
48642
48645
  import path14 from "path";
48643
48646
  import {
48644
48647
  existsSync as existsSync10,
@@ -48648,443 +48651,912 @@ import {
48648
48651
  statSync as statSync4,
48649
48652
  writeFileSync as writeFileSync6
48650
48653
  } from "fs";
48651
- function buildSystemPrompt(files, historySummary = "") {
48652
- const fileList = files.map((f) => `### ${f.path}
48653
- \`\`\`
48654
- ${f.content.slice(0, 2000)}
48655
- \`\`\``).join(`
48656
-
48657
- `);
48658
- return `You are an expert software engineer assistant with access to the user's codebase and eleven tools.
48659
-
48660
- ## TOOLS
48661
-
48662
- 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.
48663
-
48664
- ### 1. fetch — load a URL
48665
- <fetch>https://example.com</fetch>
48666
-
48667
- ### 2. shell — run a terminal command
48668
- <shell>node -v</shell>
48669
-
48670
- ### 3. read-file read a file from the repo
48671
- <read-file>src/foo.ts</read-file>
48672
-
48673
- ### 4. read-folder — list contents of a folder (files + subfolders, one level deep)
48674
- <read-folder>src/components</read-folder>
48675
-
48676
- ### 5. grep search for a pattern across files in the repo (cross-platform, no shell needed)
48677
- <grep>
48678
- {"pattern": "ChatRunner", "glob": "src/**/*.tsx"}
48679
- </grep>
48680
-
48681
- ### 6. write-file — create or overwrite a file
48682
- <write-file>
48683
- {"path": "data/output.csv", "content": "col1,col2
48684
- val1,val2"}
48685
- </write-file>
48686
-
48687
- ### 7. delete-file — permanently delete a single file
48688
- <delete-file>src/old-component.tsx</delete-file>
48689
-
48690
- ### 8. delete-folder — permanently delete a folder and all its contents
48691
- <delete-folder>src/legacy</delete-folder>
48692
-
48693
- ### 9. open-url — open a URL in the user's default browser
48694
- <open-url>https://github.com/owner/repo</open-url>
48695
-
48696
- ### 10. generate-pdf — generate a PDF file from markdown-style content
48697
- <generate-pdf>
48698
- {"path": "output/report.pdf", "content": "# Title
48699
-
48700
- Some body text.
48701
-
48702
- ## Section
48703
-
48704
- More content."}
48705
- </generate-pdf>
48706
-
48707
- ### 11. search search the internet for anything you are unsure about
48708
- <search>how to use React useEffect cleanup function</search>
48654
+ var SKIP_DIRS4 = new Set([
48655
+ "node_modules",
48656
+ ".git",
48657
+ "dist",
48658
+ "build",
48659
+ ".next",
48660
+ "out",
48661
+ "coverage",
48662
+ "__pycache__",
48663
+ ".venv",
48664
+ "venv"
48665
+ ]);
48666
+ function walkDir3(dir, base2 = dir, results = []) {
48667
+ let entries;
48668
+ try {
48669
+ entries = readdirSync3(dir, { encoding: "utf-8" });
48670
+ } catch {
48671
+ return results;
48672
+ }
48673
+ for (const entry of entries) {
48674
+ if (results.length >= 100)
48675
+ return results;
48676
+ if (SKIP_DIRS4.has(entry))
48677
+ continue;
48678
+ const full = path14.join(dir, entry);
48679
+ const rel = path14.relative(base2, full).replace(/\\/g, "/");
48680
+ let isDir = false;
48681
+ try {
48682
+ isDir = statSync4(full).isDirectory();
48683
+ } catch {
48684
+ continue;
48685
+ }
48686
+ if (isDir)
48687
+ walkDir3(full, base2, results);
48688
+ else
48689
+ results.push(rel);
48690
+ }
48691
+ return results;
48692
+ }
48693
+ function applyPatches3(repoPath, patches) {
48694
+ for (const patch of patches) {
48695
+ const fullPath = path14.join(repoPath, patch.path);
48696
+ const dir = path14.dirname(fullPath);
48697
+ if (!existsSync10(dir))
48698
+ mkdirSync4(dir, { recursive: true });
48699
+ writeFileSync6(fullPath, patch.content, "utf-8");
48700
+ }
48701
+ }
48702
+ function readFile(filePath, repoPath) {
48703
+ const candidates = path14.isAbsolute(filePath) ? [filePath] : [filePath, path14.join(repoPath, filePath)];
48704
+ for (const candidate of candidates) {
48705
+ if (existsSync10(candidate)) {
48706
+ try {
48707
+ const content = readFileSync10(candidate, "utf-8");
48708
+ const lines = content.split(`
48709
+ `).length;
48710
+ return `File: ${candidate} (${lines} lines)
48709
48711
 
48710
- ### 12. clone clone a GitHub repo so you can explore and discuss it
48711
- <clone>https://github.com/owner/repo</clone>
48712
+ ${content.slice(0, 8000)}${content.length > 8000 ? `
48712
48713
 
48713
- ### 13. changes — propose code edits (shown as a diff for user approval)
48714
- <changes>
48715
- {"summary": "what changed and why", "patches": [{"path": "src/foo.ts", "content": "COMPLETE file content", "isNew": false}]}
48716
- </changes>
48714
+ (truncated)` : ""}`;
48715
+ } catch (err) {
48716
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
48717
+ }
48718
+ }
48719
+ }
48720
+ return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
48721
+ }
48722
+ function readFolder(folderPath, repoPath) {
48723
+ const sanitized = folderPath.replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "").trim();
48724
+ const candidates = path14.isAbsolute(sanitized) ? [sanitized] : [sanitized, path14.join(repoPath, sanitized)];
48725
+ for (const candidate of candidates) {
48726
+ if (!existsSync10(candidate))
48727
+ continue;
48728
+ let stat;
48729
+ try {
48730
+ stat = statSync4(candidate);
48731
+ } catch {
48732
+ continue;
48733
+ }
48734
+ if (!stat.isDirectory()) {
48735
+ return `Not a directory: ${candidate}. Use read-file to read a file.`;
48736
+ }
48737
+ let entries;
48738
+ try {
48739
+ entries = readdirSync3(candidate, { encoding: "utf-8" });
48740
+ } catch (err) {
48741
+ return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
48742
+ }
48743
+ const files = [];
48744
+ const subfolders = [];
48745
+ for (const entry of entries) {
48746
+ if (entry.startsWith(".") && entry !== ".env")
48747
+ continue;
48748
+ const full = path14.join(candidate, entry);
48749
+ try {
48750
+ if (statSync4(full).isDirectory())
48751
+ subfolders.push(`${entry}/`);
48752
+ else
48753
+ files.push(entry);
48754
+ } catch {}
48755
+ }
48756
+ const total = files.length + subfolders.length;
48757
+ const lines = [`Folder: ${candidate} (${total} entries)`, ""];
48758
+ if (files.length > 0) {
48759
+ lines.push("Files:");
48760
+ files.forEach((f) => lines.push(` ${f}`));
48761
+ }
48762
+ if (subfolders.length > 0) {
48763
+ if (files.length > 0)
48764
+ lines.push("");
48765
+ lines.push("Subfolders:");
48766
+ subfolders.forEach((d) => lines.push(` ${d}`));
48767
+ }
48768
+ if (total === 0)
48769
+ lines.push("(empty folder)");
48770
+ return lines.join(`
48771
+ `);
48772
+ }
48773
+ return `Folder not found: ${sanitized}`;
48774
+ }
48775
+ function grepFiles(pattern, glob, repoPath) {
48776
+ let regex2;
48777
+ try {
48778
+ regex2 = new RegExp(pattern, "i");
48779
+ } catch {
48780
+ regex2 = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
48781
+ }
48782
+ const globToFilter = (g) => {
48783
+ const cleaned = g.replace(/^\*\*\//, "");
48784
+ const parts = cleaned.split("/");
48785
+ const ext = parts[parts.length - 1];
48786
+ const prefix = parts.slice(0, -1).join("/");
48787
+ return (rel) => {
48788
+ if (ext?.startsWith("*.")) {
48789
+ if (!rel.endsWith(ext.slice(1)))
48790
+ return false;
48791
+ } else if (ext && !ext.includes("*")) {
48792
+ if (!rel.endsWith(ext))
48793
+ return false;
48794
+ }
48795
+ if (prefix && !prefix.includes("*")) {
48796
+ if (!rel.startsWith(prefix))
48797
+ return false;
48798
+ }
48799
+ return true;
48800
+ };
48801
+ };
48802
+ const filter2 = globToFilter(glob);
48803
+ const allFiles = walkDir3(repoPath);
48804
+ const matchedFiles = allFiles.filter(filter2);
48805
+ if (matchedFiles.length === 0)
48806
+ return `No files matched glob: ${glob}`;
48807
+ const results = [];
48808
+ let totalMatches = 0;
48809
+ for (const relPath of matchedFiles) {
48810
+ const fullPath = path14.join(repoPath, relPath);
48811
+ let content;
48812
+ try {
48813
+ content = readFileSync10(fullPath, "utf-8");
48814
+ } catch {
48815
+ continue;
48816
+ }
48817
+ const fileLines = content.split(`
48818
+ `);
48819
+ const fileMatches = [];
48820
+ fileLines.forEach((line, i) => {
48821
+ if (regex2.test(line)) {
48822
+ fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
48823
+ totalMatches++;
48824
+ }
48825
+ });
48826
+ if (fileMatches.length > 0)
48827
+ results.push(`${relPath}
48828
+ ${fileMatches.join(`
48829
+ `)}`);
48830
+ if (totalMatches >= 200) {
48831
+ results.push("(truncated — too many matches)");
48832
+ break;
48833
+ }
48834
+ }
48835
+ if (results.length === 0)
48836
+ return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
48837
+ return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)
48717
48838
 
48718
- ## RULES
48839
+ ${results.join(`
48719
48840
 
48720
- 1. When you need to use a tool, output ONLY the XML tag — nothing before or after it in that response
48721
- 2. ONE tool per response — emit the tag, then stop completely
48722
- 3. After the user approves and you get the result, continue your analysis in the next response
48723
- 4. NEVER print a URL, command, filename, or JSON blob as plain text when you should be using a tool
48724
- 5. NEVER say "I'll fetch" / "run this command" / "here's the write-file" — just emit the tag
48725
- 6. NEVER use shell to run git clone — always use the clone tag instead
48726
- 7. NEVER use shell to list files or folders (no ls, dir, find, git ls-files, tree) — ALWAYS use read-folder instead
48727
- 8. NEVER use shell to read a file (no cat, type, Get-Content) ALWAYS use read-file instead
48728
- 9. NEVER use shell grep, findstr, or Select-String to search file contents — ALWAYS use grep instead
48729
- 10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
48730
- 11. write-file content field must be the COMPLETE file content, never empty or placeholder
48731
- 12. After a write-file succeeds, do NOT repeat it — trust the result and move on
48732
- 13. After a write-file succeeds, tell the user it is done immediately — do NOT auto-read the file back to verify
48733
- 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
48734
- 15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
48735
- 16. NEVER second-guess yourself mid-response — commit to your answer
48736
- 17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
48737
- 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
48738
- 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\`
48739
- 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\`
48740
- 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\`
48741
- 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
48742
- 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
48743
- 24. NEVER chain tool calls unless the user's request explicitly requires multiple steps
48744
- 25. NEVER read files, list folders, or run tools that were not asked for in the current user message
48745
- 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.
48746
-
48747
- ## CRITICAL: READ BEFORE YOU WRITE
48748
-
48749
- These rules are mandatory whenever you plan to edit or create a file:
48750
-
48751
- ### Before modifying ANY existing file:
48752
- 1. ALWAYS use read-file on the exact file you plan to change FIRST
48753
- 2. Study the full current content — understand every import, every export, every type, every existing feature
48754
- 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
48755
- 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
48756
-
48757
- ### Before adding a feature that touches multiple files:
48758
- 1. Use read-folder on the relevant directory to see what files exist
48759
- 2. Use read-file on each file you plan to touch
48760
- 3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
48761
-
48762
- ### The golden rule for write-file and changes:
48763
- - The output file must contain EVERYTHING the original had, PLUS your new additions
48764
- - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
48765
- - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
48766
-
48767
- ## WHEN TO USE read-folder:
48768
- - Before editing files in an unfamiliar directory — list it first to understand the structure
48769
- - When a feature spans multiple files and you are not sure what exists
48770
- - When the user asks you to explore or explain a part of the codebase
48771
-
48772
- ## SCAFFOLDING A NEW PROJECT (follow this exactly)
48773
-
48774
- When the user asks to create a new CLI/app in a subfolder (e.g. "make a todo app called list"):
48775
- 1. Create all files first using write-file with paths like \`list/package.json\`, \`list/src/index.tsx\`
48776
- 2. Then run \`cd list && bun install\` (or npm/pnpm) in one shell command
48777
- 3. Then run the project with \`cd list && bun run index.ts\` or whatever the entry point is
48778
- 4. NEVER run \`bun init\` — it is interactive and will hang. Create package.json manually with write-file instead
48779
- 5. TSX files need either tsconfig.json with \`"jsx": "react-jsx"\` or \`/** @jsxImportSource react */\` at the top
48780
-
48781
- ## FETCH → WRITE FLOW (follow this exactly when saving fetched data)
48841
+ `)}`;
48842
+ }
48843
+ function writeFile(filePath, content, repoPath) {
48844
+ const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
48845
+ try {
48846
+ const dir = path14.dirname(fullPath);
48847
+ if (!existsSync10(dir))
48848
+ mkdirSync4(dir, { recursive: true });
48849
+ writeFileSync6(fullPath, content, "utf-8");
48850
+ const lines = content.split(`
48851
+ `).length;
48852
+ return `Written: ${fullPath} (${lines} lines, ${content.length} bytes)`;
48853
+ } catch (err) {
48854
+ return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
48855
+ }
48856
+ }
48857
+ function deleteFile(filePath, repoPath) {
48858
+ const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
48859
+ try {
48860
+ if (!existsSync10(fullPath))
48861
+ return `File not found: ${fullPath}`;
48862
+ const { unlinkSync } = __require("fs");
48863
+ unlinkSync(fullPath);
48864
+ return `Deleted: ${fullPath}`;
48865
+ } catch (err) {
48866
+ return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
48867
+ }
48868
+ }
48869
+ function deleteFolder(folderPath, repoPath) {
48870
+ const fullPath = path14.isAbsolute(folderPath) ? folderPath : path14.join(repoPath, folderPath);
48871
+ try {
48872
+ if (!existsSync10(fullPath))
48873
+ return `Folder not found: ${fullPath}`;
48874
+ const { rmSync } = __require("fs");
48875
+ rmSync(fullPath, { recursive: true, force: true });
48876
+ return `Deleted folder: ${fullPath}`;
48877
+ } catch (err) {
48878
+ return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
48879
+ }
48880
+ }
48881
+ // src/tools/shell.ts
48882
+ async function runShell(command, cwd2) {
48883
+ return new Promise((resolve) => {
48884
+ const { spawn: spawn2 } = __require("child_process");
48885
+ const isWin = process.platform === "win32";
48886
+ const shell = isWin ? "cmd.exe" : "/bin/sh";
48887
+ const shellFlag = isWin ? "/c" : "-c";
48888
+ const proc = spawn2(shell, [shellFlag, command], {
48889
+ cwd: cwd2,
48890
+ env: process.env,
48891
+ stdio: ["ignore", "pipe", "pipe"]
48892
+ });
48893
+ const chunks = [];
48894
+ const errChunks = [];
48895
+ proc.stdout.on("data", (d) => chunks.push(d));
48896
+ proc.stderr.on("data", (d) => errChunks.push(d));
48897
+ const killTimer = setTimeout(() => {
48898
+ proc.kill();
48899
+ resolve("(command timed out after 5 minutes)");
48900
+ }, 5 * 60 * 1000);
48901
+ proc.on("close", (code) => {
48902
+ clearTimeout(killTimer);
48903
+ const stdout = Buffer.concat(chunks).toString("utf-8").trim();
48904
+ const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
48905
+ const combined = [stdout, stderr].filter(Boolean).join(`
48906
+ `);
48907
+ resolve(combined || (code === 0 ? "(no output)" : `exit code ${code}`));
48908
+ });
48909
+ proc.on("error", (err) => {
48910
+ clearTimeout(killTimer);
48911
+ resolve(`Error: ${err.message}`);
48912
+ });
48913
+ });
48914
+ }
48915
+ function openUrl(url) {
48916
+ try {
48917
+ const { execSync: exec5 } = __require("child_process");
48918
+ const platform2 = process.platform;
48919
+ if (platform2 === "win32") {
48920
+ exec5(`start "" "${url}"`, { stdio: "ignore" });
48921
+ } else if (platform2 === "darwin") {
48922
+ exec5(`open "${url}"`, { stdio: "ignore" });
48923
+ } else {
48924
+ exec5(`xdg-open "${url}"`, { stdio: "ignore" });
48925
+ }
48926
+ return `Opened: ${url}`;
48927
+ } catch (err) {
48928
+ return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
48929
+ }
48930
+ }
48931
+ // src/tools/web.ts
48932
+ function stripTags(html) {
48933
+ return html.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#\d+;/g, " ").replace(/\s+/g, " ").trim();
48934
+ }
48935
+ function extractTables(html) {
48936
+ const tables = [];
48937
+ const tableRe = /<table[\s\S]*?<\/table>/gi;
48938
+ let tMatch;
48939
+ while ((tMatch = tableRe.exec(html)) !== null) {
48940
+ const tableHtml = tMatch[0];
48941
+ const rows = [];
48942
+ const rowRe = /<tr[\s\S]*?<\/tr>/gi;
48943
+ let rMatch;
48944
+ while ((rMatch = rowRe.exec(tableHtml)) !== null) {
48945
+ const cells = [];
48946
+ const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
48947
+ let cMatch;
48948
+ while ((cMatch = cellRe.exec(rMatch[0])) !== null) {
48949
+ cells.push(stripTags(cMatch[1] ?? ""));
48950
+ }
48951
+ if (cells.length > 0)
48952
+ rows.push(cells);
48953
+ }
48954
+ if (rows.length < 2)
48955
+ continue;
48956
+ const cols = Math.max(...rows.map((r) => r.length));
48957
+ const padded = rows.map((r) => {
48958
+ while (r.length < cols)
48959
+ r.push("");
48960
+ return r;
48961
+ });
48962
+ const widths = Array.from({ length: cols }, (_, ci) => Math.max(...padded.map((r) => (r[ci] ?? "").length), 3));
48963
+ const fmt = (r) => r.map((c, ci) => c.padEnd(widths[ci] ?? 0)).join(" | ");
48964
+ const header = fmt(padded[0]);
48965
+ const sep = widths.map((w) => "-".repeat(w)).join("-|-");
48966
+ const body = padded.slice(1).map(fmt).join(`
48967
+ `);
48968
+ tables.push(`${header}
48969
+ ${sep}
48970
+ ${body}`);
48971
+ }
48972
+ return tables.length > 0 ? `=== TABLES (${tables.length}) ===
48782
48973
 
48783
- 1. fetch the URL
48784
- 2. Analyze the result — count the rows, identify columns, check completeness
48785
- 3. Tell the user what you found: "Found X rows with columns: A, B, C. Writing now."
48786
- 4. emit write-file with correctly structured, complete content
48787
- 5. After write-file confirms success, emit read-file to verify
48788
- 6. Only after read-file confirms content is correct, tell the user it is done
48974
+ ${tables.join(`
48789
48975
 
48790
- ## WHEN TO USE TOOLS
48976
+ ---
48791
48977
 
48792
- - User shares any URL → fetch it immediately
48793
- - User asks to run anything → shell it immediately
48794
- - User asks to open a link, open a URL, or visit a website → open-url it immediately, do NOT use fetch
48795
- - User asks to delete a file → delete-file it immediately (requires approval)
48796
- - User asks to delete a folder or directory → delete-folder it immediately (requires approval)
48797
- - 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
48798
- - User asks to read a file → read-file it immediately, NEVER use shell cat/type
48799
- - 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
48800
- - User asks to explore a folder or directory → read-folder it immediately
48801
- - User asks to save/create/write a file → write-file it immediately, then read-file to verify
48802
- - User asks to modify/edit/add to an existing file → read-file it FIRST, then emit changes
48803
- - User shares a GitHub URL and wants to clone/explore/discuss it → use clone immediately, NEVER use shell git clone
48804
- - 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.
48805
- - You are unsure about an API, library, error, concept, or piece of code → search it immediately
48806
- - User asks about something recent or that you might not know → search it immediately
48807
- - You are about to say "I'm not sure" or "I don't know" → search instead of guessing
48978
+ `)}` : "";
48979
+ }
48980
+ function extractLists(html) {
48981
+ const lists = [];
48982
+ const listRe = /<[ou]l[\s\S]*?<\/[ou]l>/gi;
48983
+ let lMatch;
48984
+ while ((lMatch = listRe.exec(html)) !== null) {
48985
+ const items = [];
48986
+ const itemRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
48987
+ let iMatch;
48988
+ while ((iMatch = itemRe.exec(lMatch[0])) !== null) {
48989
+ const text = stripTags(iMatch[1] ?? "");
48990
+ if (text.length > 2)
48991
+ items.push(`• ${text}`);
48992
+ }
48993
+ if (items.length > 1)
48994
+ lists.push(items.join(`
48995
+ `));
48996
+ }
48997
+ return lists.length > 0 ? `=== LISTS ===
48808
48998
 
48809
- ## shell IS ONLY FOR:
48810
- - Running code: \`node script.js\`, \`bun run dev\`, \`python main.py\`
48811
- - Installing packages: \`npm install\`, \`pip install\`
48812
- - Building/testing: \`npm run build\`, \`bun test\`
48813
- - Git operations other than clone: \`git status\`, \`git log\`, \`git diff\`
48814
- - Anything that EXECUTES — not reads or lists
48999
+ ${lists.slice(0, 5).join(`
48815
49000
 
48816
- ## CODEBASE
49001
+ `)}` : "";
49002
+ }
49003
+ async function fetchUrl(url) {
49004
+ const res = await fetch(url, {
49005
+ headers: {
49006
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49007
+ Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
49008
+ "Accept-Language": "en-US,en;q=0.5"
49009
+ },
49010
+ signal: AbortSignal.timeout(15000)
49011
+ });
49012
+ if (!res.ok)
49013
+ throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49014
+ const contentType = res.headers.get("content-type") ?? "";
49015
+ if (contentType.includes("application/json")) {
49016
+ const json = await res.json();
49017
+ return JSON.stringify(json, null, 2).slice(0, 8000);
49018
+ }
49019
+ const html = await res.text();
49020
+ const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
49021
+ const title = titleMatch ? stripTags(titleMatch[1]) : "No title";
49022
+ const tables = extractTables(html);
49023
+ const lists = extractLists(html);
49024
+ const bodyText = stripTags(html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "")).replace(/\s{3,}/g, `
48817
49025
 
48818
- ${fileList.length > 0 ? fileList : "(no files indexed)"}
49026
+ `).slice(0, 3000);
49027
+ const parts = [`PAGE: ${title}`, `URL: ${url}`];
49028
+ if (tables)
49029
+ parts.push(tables);
49030
+ if (lists)
49031
+ parts.push(lists);
49032
+ parts.push(`=== TEXT ===
49033
+ ${bodyText}`);
49034
+ return parts.join(`
48819
49035
 
48820
- ${historySummary}`;
49036
+ `);
48821
49037
  }
48822
- var FEW_SHOT_MESSAGES = [
48823
- {
48824
- role: "user",
48825
- content: "delete src/old-component.tsx"
48826
- },
48827
- {
48828
- role: "assistant",
48829
- content: "<delete-file>src/old-component.tsx</delete-file>"
48830
- },
48831
- {
48832
- role: "user",
48833
- content: `Here is the output from delete-file of src/old-component.tsx:
49038
+ async function searchWeb(query) {
49039
+ const encoded = encodeURIComponent(query);
49040
+ const ddgUrl = `https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1&skip_disambig=1`;
49041
+ try {
49042
+ const res = await fetch(ddgUrl, {
49043
+ headers: { "User-Agent": "Lens/1.0" },
49044
+ signal: AbortSignal.timeout(8000)
49045
+ });
49046
+ if (res.ok) {
49047
+ const data = await res.json();
49048
+ const parts = [`Search: ${query}`];
49049
+ if (data.Answer)
49050
+ parts.push(`Answer: ${data.Answer}`);
49051
+ if (data.AbstractText) {
49052
+ parts.push(`Summary: ${data.AbstractText}`);
49053
+ if (data.AbstractURL)
49054
+ parts.push(`Source: ${data.AbstractURL}`);
49055
+ }
49056
+ if (data.Infobox?.content?.length) {
49057
+ const fields = data.Infobox.content.slice(0, 8).map((f) => ` ${f.label}: ${f.value}`).join(`
49058
+ `);
49059
+ parts.push(`Info:
49060
+ ${fields}`);
49061
+ }
49062
+ if (data.RelatedTopics?.length) {
49063
+ const topics = data.RelatedTopics.filter((t) => t.Text).slice(0, 5).map((t) => ` - ${t.Text}`).join(`
49064
+ `);
49065
+ if (topics)
49066
+ parts.push(`Related:
49067
+ ${topics}`);
49068
+ }
49069
+ const result2 = parts.join(`
48834
49070
 
48835
- Deleted: /repo/src/old-component.tsx
49071
+ `);
49072
+ if (result2.length > 60)
49073
+ return result2;
49074
+ }
49075
+ } catch {}
49076
+ try {
49077
+ const htmlUrl = `https://html.duckduckgo.com/html/?q=${encoded}`;
49078
+ const res = await fetch(htmlUrl, {
49079
+ headers: {
49080
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49081
+ Accept: "text/html"
49082
+ },
49083
+ signal: AbortSignal.timeout(1e4)
49084
+ });
49085
+ if (!res.ok)
49086
+ throw new Error(`HTTP ${res.status}`);
49087
+ const html = await res.text();
49088
+ const snippets = [];
49089
+ const snippetRe = /class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
49090
+ let m;
49091
+ while ((m = snippetRe.exec(html)) !== null && snippets.length < 6) {
49092
+ const text = m[1].replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
49093
+ if (text.length > 20)
49094
+ snippets.push(`- ${text}`);
49095
+ }
49096
+ const links = [];
49097
+ const linkRe = /class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
49098
+ while ((m = linkRe.exec(html)) !== null && links.length < 5) {
49099
+ const title = m[2].replace(/<[^>]+>/g, "").trim();
49100
+ const href = m[1];
49101
+ if (title && href)
49102
+ links.push(` ${title} — ${href}`);
49103
+ }
49104
+ if (snippets.length === 0 && links.length === 0)
49105
+ return `No results found for: ${query}`;
49106
+ const parts = [`Search results for: ${query}`];
49107
+ if (snippets.length > 0)
49108
+ parts.push(`Snippets:
49109
+ ${snippets.join(`
49110
+ `)}`);
49111
+ if (links.length > 0)
49112
+ parts.push(`Links:
49113
+ ${links.join(`
49114
+ `)}`);
49115
+ return parts.join(`
48836
49116
 
48837
- Please continue your response based on this output.`
48838
- },
48839
- {
48840
- role: "assistant",
48841
- content: "Done — `src/old-component.tsx` has been deleted."
48842
- },
48843
- {
48844
- role: "user",
48845
- content: "delete the legacy folder"
48846
- },
48847
- {
48848
- role: "assistant",
48849
- content: "<delete-folder>src/legacy</delete-folder>"
48850
- },
48851
- {
48852
- role: "user",
48853
- content: `Here is the output from delete-folder of src/legacy:
49117
+ `);
49118
+ } catch (err) {
49119
+ return `Search failed: ${err instanceof Error ? err.message : String(err)}`;
49120
+ }
49121
+ }
49122
+ // src/tools/pdf.ts
49123
+ import path15 from "path";
49124
+ import os6 from "os";
49125
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
49126
+ function generatePdf(filePath, content, repoPath) {
49127
+ const fullPath = path15.isAbsolute(filePath) ? filePath : path15.join(repoPath, filePath);
49128
+ try {
49129
+ const dir = path15.dirname(fullPath);
49130
+ if (!existsSync11(dir))
49131
+ mkdirSync5(dir, { recursive: true });
49132
+ const escaped = content.replace(/\\/g, "\\\\").replace(/"""/g, "\\\"\\\"\\\"").replace(/\r/g, "");
49133
+ const script = `
49134
+ import sys
49135
+ try:
49136
+ from reportlab.lib.pagesizes import letter
49137
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
49138
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
49139
+ from reportlab.lib.units import inch
49140
+ from reportlab.lib import colors
49141
+ except ImportError:
49142
+ import subprocess
49143
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
49144
+ from reportlab.lib.pagesizes import letter
49145
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
49146
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
49147
+ from reportlab.lib.units import inch
49148
+ from reportlab.lib import colors
48854
49149
 
48855
- Deleted folder: /repo/src/legacy
49150
+ doc = SimpleDocTemplate(
49151
+ r"""${fullPath}""",
49152
+ pagesize=letter,
49153
+ rightMargin=inch,
49154
+ leftMargin=inch,
49155
+ topMargin=inch,
49156
+ bottomMargin=inch,
49157
+ )
48856
49158
 
48857
- Please continue your response based on this output.`
48858
- },
48859
- {
48860
- role: "assistant",
48861
- content: "Done the `src/legacy` folder and all its contents have been deleted."
48862
- },
48863
- {
48864
- role: "user",
48865
- content: "open https://github.com/microsoft/typescript"
48866
- },
48867
- {
48868
- role: "assistant",
48869
- content: "<open-url>https://github.com/microsoft/typescript</open-url>"
48870
- },
48871
- {
48872
- role: "user",
48873
- content: `Here is the output from open-url https://github.com/microsoft/typescript:
49159
+ styles = getSampleStyleSheet()
49160
+ styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
49161
+ styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
49162
+ styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
49163
+ styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
49164
+ styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
48874
49165
 
48875
- Opened: https://github.com/microsoft/typescript
49166
+ raw = """${escaped}"""
48876
49167
 
48877
- Please continue your response based on this output.`
48878
- },
48879
- {
48880
- role: "assistant",
48881
- content: "Opened the TypeScript GitHub page in your browser."
48882
- },
48883
- {
48884
- role: "user",
48885
- content: "generate a PDF report about the project and save it to docs/report.pdf"
48886
- },
48887
- {
48888
- role: "assistant",
48889
- content: `<generate-pdf>
48890
- {"path": "docs/report.pdf", "content": "# Project Report\\n\\n## Overview\\n\\nThis document summarizes the project.\\n\\n## Details\\n\\nMore content here."}
48891
- </generate-pdf>`
48892
- },
48893
- {
48894
- role: "user",
48895
- content: `Here is the output from generate-pdf to docs/report.pdf:
49168
+ story = []
49169
+ for line in raw.split("\\n"):
49170
+ s = line.rstrip()
49171
+ if s.startswith("### "):
49172
+ story.append(Paragraph(s[4:], styles["H3"]))
49173
+ elif s.startswith("## "):
49174
+ story.append(Spacer(1, 6))
49175
+ story.append(Paragraph(s[3:], styles["H2"]))
49176
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
49177
+ elif s.startswith("# "):
49178
+ story.append(Paragraph(s[2:], styles["H1"]))
49179
+ story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
49180
+ elif s.startswith("- ") or s.startswith("* "):
49181
+ story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
49182
+ elif s.startswith("---"):
49183
+ story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
49184
+ elif s == "":
49185
+ story.append(Spacer(1, 6))
49186
+ else:
49187
+ import re
49188
+ s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
49189
+ s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
49190
+ s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
49191
+ story.append(Paragraph(s, styles["Body"]))
48896
49192
 
48897
- PDF generated: /repo/docs/report.pdf
49193
+ doc.build(story)
49194
+ print("OK")
49195
+ `.replace("${fullPath}", fullPath.replace(/\\/g, "/")).replace("${escaped}", escaped);
49196
+ const tmpFile = path15.join(os6.tmpdir(), `lens_pdf_${Date.now()}.py`);
49197
+ writeFileSync7(tmpFile, script, "utf-8");
49198
+ const { execSync: execSync2 } = __require("child_process");
49199
+ execSync2(`python "${tmpFile}"`, { stdio: "pipe" });
49200
+ try {
49201
+ __require("fs").unlinkSync(tmpFile);
49202
+ } catch {}
49203
+ return `PDF generated: ${fullPath}`;
49204
+ } catch (err) {
49205
+ return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
49206
+ }
49207
+ }
49208
+ // src/prompts/system.ts
49209
+ function buildSystemPrompt(files, memorySummary = "") {
49210
+ const fileList = files.map((f) => `### ${f.path}
49211
+ \`\`\`
49212
+ ${f.content.slice(0, 2000)}
49213
+ \`\`\``).join(`
48898
49214
 
48899
- Please continue your response based on this output.`
48900
- },
48901
- {
48902
- role: "assistant",
48903
- content: "Done — the PDF report has been saved to `docs/report.pdf`."
48904
- },
48905
- {
48906
- role: "user",
48907
- content: 'grep -R "ChatRunner" -n src'
48908
- },
48909
- {
48910
- role: "assistant",
48911
- content: `<grep>
48912
- {"pattern": "ChatRunner", "glob": "src/**/*"}
48913
- </grep>`
48914
- },
48915
- {
48916
- role: "user",
48917
- content: `Here is the output from grep for "ChatRunner":
49215
+ `);
49216
+ return `You are an expert software engineer assistant with access to the user's codebase and tools.
48918
49217
 
48919
- grep /ChatRunner/ src/**/* — 3 match(es) in 2 file(s)
49218
+ ## TOOLS
48920
49219
 
48921
- src/index.tsx
48922
- 12: import { ChatRunner } from "./components/chat/ChatRunner";
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.
48923
49221
 
48924
- src/components/chat/ChatRunner.tsx
48925
- 1: export const ChatRunner = ...
49222
+ ### 1. fetch — load a URL
49223
+ <fetch>https://example.com</fetch>
48926
49224
 
48927
- Please continue your response based on this output.`
48928
- },
48929
- {
48930
- role: "assistant",
48931
- content: "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`."
48932
- },
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>
49275
+
49276
+ ## MEMORY OPERATIONS
49277
+
49278
+ You can save and delete memories at any time by emitting these tags alongside your normal response.
49279
+ They are stripped before display — the user will not see the raw tags.
49280
+
49281
+ ### memory-add — save something important to long-term memory for this repo
49282
+ <memory-add>User prefers TypeScript strict mode in all new files</memory-add>
49283
+
49284
+ ### memory-delete — delete a memory by its ID (shown in brackets like [abc123])
49285
+ <memory-delete>abc123</memory-delete>
49286
+
49287
+ Use memory-add when:
49288
+ - The user explicitly asks you to remember something ("remember that...", "don't forget...")
49289
+ - You learn something project-specific that would be useful in future sessions
49290
+ (e.g. preferred patterns, architecture decisions, known gotchas, user preferences)
49291
+
49292
+ Use memory-delete when:
49293
+ - The user asks you to forget something
49294
+ - A memory is outdated or wrong and you are replacing it with a new one
49295
+
49296
+ You may emit multiple memory operations in a single response alongside normal content.
49297
+
49298
+ ## RULES
49299
+
49300
+ 1. When you need to use a tool, output ONLY the XML tag — nothing before or after it in that response
49301
+ 2. ONE tool per response — emit the tag, then stop completely
49302
+ 3. After the user approves and you get the result, continue your analysis in the next response
49303
+ 4. NEVER print a URL, command, filename, or JSON blob as plain text when you should be using a tool
49304
+ 5. NEVER say "I'll fetch" / "run this command" / "here's the write-file" — just emit the tag
49305
+ 6. NEVER use shell to run git clone — always use the clone tag instead
49306
+ 7. NEVER use shell to list files or folders (no ls, dir, find, git ls-files, tree) — ALWAYS use read-folder instead
49307
+ 8. NEVER use shell to read a file (no cat, type, Get-Content) — ALWAYS use read-file instead
49308
+ 9. NEVER use shell grep, findstr, or Select-String to search file contents — ALWAYS use grep instead
49309
+ 10. shell is ONLY for running code, installing packages, building, testing — not for filesystem inspection
49310
+ 11. write-file content field must be the COMPLETE file content, never empty or placeholder
49311
+ 12. After a write-file succeeds, do NOT repeat it — trust the result and move on
49312
+ 13. After a write-file succeeds, tell the user it is done immediately — do NOT auto-read the file back to verify
49313
+ 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
49314
+ 15. NEVER say "I made a mistake" and repeat the same tool — one attempt is enough, trust the output
49315
+ 16. NEVER second-guess yourself mid-response — commit to your answer
49316
+ 17. If a read-folder or read-file returns "not found", accept it and move on — do NOT retry the same path
49317
+ 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
49318
+ 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\`
49319
+ 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\`
49320
+ 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\`
49321
+ 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
+ 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
+ 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.
49327
+
49328
+ ## CRITICAL: READ BEFORE YOU WRITE
49329
+
49330
+ These rules are mandatory whenever you plan to edit or create a file:
49331
+
49332
+ ### Before modifying ANY existing file:
49333
+ 1. ALWAYS use read-file on the exact file you plan to change FIRST
49334
+ 2. Study the full current content — understand every import, every export, every type, every existing feature
49335
+ 3. Your changes patch MUST preserve ALL existing functionality — do not remove or rewrite things that were not part of the request
49336
+ 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
49337
+
49338
+ ### Before adding a feature that touches multiple files:
49339
+ 1. Use read-folder on the relevant directory to see what files exist
49340
+ 2. Use read-file on each file you plan to touch
49341
+ 3. Only then emit a changes tag — with patches that are surgical additions, not wholesale rewrites
49342
+
49343
+ ### The golden rule for write-file and changes:
49344
+ - The output file must contain EVERYTHING the original had, PLUS your new additions
49345
+ - NEVER produce a file that is shorter than the original unless you are explicitly asked to delete things
49346
+ - If you catch yourself rewriting a file from scratch, STOP — go back and read the original first
49347
+
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
+ ## CODEBASE
49398
+
49399
+ ${fileList.length > 0 ? fileList : "(no files indexed)"}
49400
+
49401
+ ${memorySummary}`;
49402
+ }
49403
+ // src/prompts/fewshot.ts
49404
+ var FEW_SHOT_MESSAGES = [
48933
49405
  {
48934
49406
  role: "user",
48935
- content: "find all usages of useInput in the codebase"
49407
+ content: "delete src/old-component.tsx"
48936
49408
  },
48937
49409
  {
48938
49410
  role: "assistant",
48939
- content: `<grep>
48940
- {"pattern": "useInput", "glob": "src/**/*.tsx"}
48941
- </grep>`
49411
+ content: "<delete-file>src/old-component.tsx</delete-file>"
48942
49412
  },
48943
49413
  {
48944
49414
  role: "user",
48945
- content: `Here is the output from grep for "useInput":
48946
-
48947
- grep /useInput/ src/**/*.tsx — 2 match(es) in 1 file(s)
49415
+ content: `Here is the output from delete-file of src/old-component.tsx:
48948
49416
 
48949
- src/components/chat/ChatRunner.tsx
48950
- 5: import { useInput } from "ink";
48951
- 210: useInput((input, key) => {
49417
+ Deleted: /repo/src/old-component.tsx
48952
49418
 
48953
49419
  Please continue your response based on this output.`
48954
49420
  },
48955
49421
  {
48956
49422
  role: "assistant",
48957
- content: "`useInput` is used in `src/components/chat/ChatRunner.tsx` imported on line 5 and called on line 210."
49423
+ content: "Done `src/old-component.tsx` has been deleted."
48958
49424
  },
48959
49425
  {
48960
49426
  role: "user",
48961
- content: "show me how to use the read-file tool"
49427
+ content: "delete the legacy folder"
48962
49428
  },
48963
49429
  {
48964
49430
  role: "assistant",
48965
- content: "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."
49431
+ content: "<delete-folder>src/legacy</delete-folder>"
48966
49432
  },
48967
49433
  {
48968
49434
  role: "user",
48969
- content: "show me an example of the shell tool"
49435
+ content: `Here is the output from delete-folder of src/legacy:
49436
+
49437
+ Deleted folder: /repo/src/legacy
49438
+
49439
+ Please continue your response based on this output.`
48970
49440
  },
48971
49441
  {
48972
49442
  role: "assistant",
48973
- content: "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."
49443
+ content: "Done the `src/legacy` folder and all its contents have been deleted."
48974
49444
  },
48975
49445
  {
48976
49446
  role: "user",
48977
- content: "read src folder"
49447
+ content: "open https://github.com/microsoft/typescript"
48978
49448
  },
48979
49449
  {
48980
49450
  role: "assistant",
48981
- content: "<read-folder>src</read-folder>"
49451
+ content: "<open-url>https://github.com/microsoft/typescript</open-url>"
48982
49452
  },
48983
49453
  {
48984
49454
  role: "user",
48985
- content: `Here is the output from read-folder of src:
48986
-
48987
- Folder: src (4 entries)
48988
-
48989
- Files:
48990
- index.ts
48991
- App.tsx
49455
+ content: `Here is the output from open-url https://github.com/microsoft/typescript:
48992
49456
 
48993
- Subfolders:
48994
- components/
48995
- utils/
49457
+ Opened: https://github.com/microsoft/typescript
48996
49458
 
48997
49459
  Please continue your response based on this output.`
48998
49460
  },
48999
49461
  {
49000
49462
  role: "assistant",
49001
- content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49463
+ content: "Opened the TypeScript GitHub page in your browser."
49002
49464
  },
49003
49465
  {
49004
49466
  role: "user",
49005
- content: "list src folder"
49467
+ content: "generate a PDF report about the project and save it to docs/report.pdf"
49006
49468
  },
49007
49469
  {
49008
49470
  role: "assistant",
49009
- content: "<read-folder>src</read-folder>"
49471
+ content: `<generate-pdf>
49472
+ {"path": "docs/report.pdf", "content": "# Project Report\\n\\n## Overview\\n\\nThis document summarizes the project.\\n\\n## Details\\n\\nMore content here."}
49473
+ </generate-pdf>`
49010
49474
  },
49011
49475
  {
49012
49476
  role: "user",
49013
- content: `Here is the output from read-folder of src:
49014
-
49015
- Folder: src (4 entries)
49016
-
49017
- Files:
49018
- index.ts
49019
- App.tsx
49477
+ content: `Here is the output from generate-pdf to docs/report.pdf:
49020
49478
 
49021
- Subfolders:
49022
- components/
49023
- utils/
49479
+ PDF generated: /repo/docs/report.pdf
49024
49480
 
49025
49481
  Please continue your response based on this output.`
49026
49482
  },
49027
49483
  {
49028
49484
  role: "assistant",
49029
- content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49485
+ content: "Done the PDF report has been saved to `docs/report.pdf`."
49030
49486
  },
49031
49487
  {
49032
49488
  role: "user",
49033
- content: "what files are in src/components?"
49489
+ content: 'grep -R "ChatRunner" -n src'
49034
49490
  },
49035
49491
  {
49036
49492
  role: "assistant",
49037
- content: "<read-folder>src/components</read-folder>"
49493
+ content: `<grep>
49494
+ {"pattern": "ChatRunner", "glob": "src/**/*"}
49495
+ </grep>`
49038
49496
  },
49039
49497
  {
49040
49498
  role: "user",
49041
- content: `Here is the output from read-folder of src/components:
49499
+ content: `Here is the output from grep for "ChatRunner":
49042
49500
 
49043
- Folder: src/components (5 entries)
49501
+ grep /ChatRunner/ src/**/* — 3 match(es) in 2 file(s)
49044
49502
 
49045
- Files:
49046
- Header.tsx
49047
- Footer.tsx
49048
- Button.tsx
49503
+ src/index.tsx
49504
+ 12: import { ChatRunner } from "./components/chat/ChatRunner";
49049
49505
 
49050
- Subfolders:
49051
- ui/
49052
- forms/
49506
+ src/components/chat/ChatRunner.tsx
49507
+ 1: export const ChatRunner = ...
49053
49508
 
49054
49509
  Please continue your response based on this output.`
49055
49510
  },
49056
49511
  {
49057
49512
  role: "assistant",
49058
- content: "The `src/components` folder has 3 files — `Header.tsx`, `Footer.tsx`, `Button.tsx` plus two subfolders: `ui/` and `forms/`."
49513
+ content: "`ChatRunner` is defined in `src/components/chat/ChatRunner.tsx` and imported in `src/index.tsx`."
49059
49514
  },
49060
49515
  {
49061
49516
  role: "user",
49062
- content: "list the files in src/utils"
49517
+ content: "find all usages of useInput in the codebase"
49063
49518
  },
49064
49519
  {
49065
49520
  role: "assistant",
49066
- content: "<read-folder>src/utils</read-folder>"
49521
+ content: `<grep>
49522
+ {"pattern": "useInput", "glob": "src/**/*.tsx"}
49523
+ </grep>`
49067
49524
  },
49068
49525
  {
49069
49526
  role: "user",
49070
- content: `Here is the output from read-folder of src/utils:
49071
-
49072
- Folder: src/utils (3 entries)
49527
+ content: `Here is the output from grep for "useInput":
49073
49528
 
49074
- Files:
49075
- api.ts
49076
- helpers.ts
49077
- format.ts
49529
+ grep /useInput/ src/**/*.tsx — 2 match(es) in 1 file(s)
49530
+
49531
+ src/components/chat/ChatRunner.tsx
49532
+ 5: import { useInput } from "ink";
49533
+ 210: useInput((input, key) => {
49078
49534
 
49079
49535
  Please continue your response based on this output.`
49080
49536
  },
49081
49537
  {
49082
49538
  role: "assistant",
49083
- content: "The `src/utils` folder contains 3 files: `api.ts`, `helpers.ts`, and `format.ts`."
49539
+ content: "`useInput` is used in `src/components/chat/ChatRunner.tsx` imported on line 5 and called on line 210."
49540
+ },
49541
+ {
49542
+ role: "user",
49543
+ content: "show me how to use the read-file tool"
49544
+ },
49545
+ {
49546
+ role: "assistant",
49547
+ content: "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."
49548
+ },
49549
+ {
49550
+ role: "user",
49551
+ content: "show me an example of the shell tool"
49552
+ },
49553
+ {
49554
+ role: "assistant",
49555
+ content: "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."
49084
49556
  },
49085
49557
  {
49086
49558
  role: "user",
49087
- content: "show me what's in the src directory"
49559
+ content: "read src folder"
49088
49560
  },
49089
49561
  {
49090
49562
  role: "assistant",
@@ -49108,40 +49580,40 @@ Please continue your response based on this output.`
49108
49580
  },
49109
49581
  {
49110
49582
  role: "assistant",
49111
- content: "The `src` directory has 2 files (`index.ts`, `App.tsx`) and 2 subfolders (`components/`, `utils/`)."
49583
+ content: "The `src` folder contains `index.ts`, `App.tsx`, plus subfolders `components/` and `utils/`."
49112
49584
  },
49113
49585
  {
49114
49586
  role: "user",
49115
- content: "show me the project structure"
49587
+ content: "what files are in src/components?"
49116
49588
  },
49117
49589
  {
49118
49590
  role: "assistant",
49119
- content: "<read-folder>.</read-folder>"
49591
+ content: "<read-folder>src/components</read-folder>"
49120
49592
  },
49121
49593
  {
49122
49594
  role: "user",
49123
- content: `Here is the output from read-folder of .:
49595
+ content: `Here is the output from read-folder of src/components:
49124
49596
 
49125
- Folder: . (5 entries)
49597
+ Folder: src/components (5 entries)
49126
49598
 
49127
49599
  Files:
49128
- package.json
49129
- tsconfig.json
49130
- README.md
49600
+ Header.tsx
49601
+ Footer.tsx
49602
+ Button.tsx
49131
49603
 
49132
49604
  Subfolders:
49133
- src/
49134
- node_modules/
49605
+ ui/
49606
+ forms/
49135
49607
 
49136
49608
  Please continue your response based on this output.`
49137
49609
  },
49138
49610
  {
49139
49611
  role: "assistant",
49140
- content: "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code."
49612
+ content: "The `src/components` folder has 3 files `Header.tsx`, `Footer.tsx`, `Button.tsx` plus two subfolders: `ui/` and `forms/`."
49141
49613
  },
49142
49614
  {
49143
49615
  role: "user",
49144
- content: "what does the project look like?"
49616
+ content: "show me the project structure"
49145
49617
  },
49146
49618
  {
49147
49619
  role: "assistant",
@@ -49151,21 +49623,22 @@ Please continue your response based on this output.`
49151
49623
  role: "user",
49152
49624
  content: `Here is the output from read-folder of .:
49153
49625
 
49154
- Folder: . (4 entries)
49626
+ Folder: . (5 entries)
49155
49627
 
49156
49628
  Files:
49157
49629
  package.json
49630
+ tsconfig.json
49158
49631
  README.md
49159
49632
 
49160
49633
  Subfolders:
49161
49634
  src/
49162
- dist/
49635
+ node_modules/
49163
49636
 
49164
49637
  Please continue your response based on this output.`
49165
49638
  },
49166
49639
  {
49167
49640
  role: "assistant",
49168
- content: "The repo root contains `package.json` and `README.md`, with source code in `src/` and compiled output in `dist/`."
49641
+ content: "The project root has 3 files (`package.json`, `tsconfig.json`, `README.md`) and the `src/` folder for source code."
49169
49642
  },
49170
49643
  {
49171
49644
  role: "user",
@@ -49314,13 +49787,62 @@ Please continue your response based on this output.`
49314
49787
  },
49315
49788
  {
49316
49789
  role: "user",
49317
- content: "what does this project do?"
49790
+ content: "scaffold a new project called myapp with package.json and src/index.ts"
49791
+ },
49792
+ {
49793
+ role: "assistant",
49794
+ content: `<write-file>
49795
+ {"path": "myapp/package.json", "content": "{"name":"myapp","version":"1.0.0"}"}
49796
+ </write-file>`
49797
+ },
49798
+ {
49799
+ role: "user",
49800
+ content: `Here is the output from write-file to myapp/package.json:
49801
+
49802
+ Written: /repo/myapp/package.json (1 lines, 38 bytes)
49803
+
49804
+ Please continue your response based on this output.`
49805
+ },
49806
+ {
49807
+ role: "assistant",
49808
+ content: `<write-file>
49809
+ {"path": "myapp/src/index.ts", "content": "console.log('hello')"}
49810
+ </write-file>`
49811
+ },
49812
+ {
49813
+ role: "user",
49814
+ content: `Here is the output from write-file to myapp/src/index.ts:
49815
+
49816
+ Written: /repo/myapp/src/index.ts (1 lines, 20 bytes)
49817
+
49818
+ Please continue your response based on this output.`
49819
+ },
49820
+ {
49821
+ role: "assistant",
49822
+ content: "Done — scaffolded `myapp/package.json` and `myapp/src/index.ts`. Run `cd myapp && npm install` to get started."
49823
+ },
49824
+ {
49825
+ role: "user",
49826
+ content: "remember that we always use bun in this project"
49827
+ },
49828
+ {
49829
+ role: "assistant",
49830
+ content: `<memory-add>Project uses bun as the package manager and runtime, not npm or node</memory-add>
49831
+
49832
+ Got it — I'll always use bun for this project.`
49833
+ },
49834
+ {
49835
+ role: "user",
49836
+ content: "forget the bun preference"
49318
49837
  },
49319
49838
  {
49320
49839
  role: "assistant",
49321
- content: "This project is a CLI coding assistant that lets you chat with an AI model about your codebase. It can read and write files, run shell commands, search the web, and propose diffs for your approval."
49840
+ content: `<memory-delete>MEMORY_ID</memory-delete>
49841
+
49842
+ Done — removed that memory.`
49322
49843
  }
49323
49844
  ];
49845
+ // src/utils/chat.ts
49324
49846
  function parseResponse(text) {
49325
49847
  const scanText = text.replace(/```[\s\S]*?```/g, (m) => " ".repeat(m.length));
49326
49848
  const candidates = [];
@@ -49357,728 +49879,265 @@ function parseResponse(text) {
49357
49879
  const originalMatch = originalRe.exec(text.slice(m.index));
49358
49880
  if (originalMatch) {
49359
49881
  const fakeMatch = Object.assign([
49360
- text.slice(m.index, m.index + originalMatch[0].length),
49361
- originalMatch[1]
49362
- ], { index: m.index, input: text, groups: undefined });
49363
- candidates.push({ index: m.index, kind: kind2, match: fakeMatch });
49364
- }
49365
- }
49366
- }
49367
- if (candidates.length === 0)
49368
- return { kind: "text", content: text.trim() };
49369
- candidates.sort((a, b) => a.index - b.index);
49370
- const { kind, match } = candidates[0];
49371
- const before2 = text.slice(0, match.index).replace(/<(fetch|shell|read-file|read-folder|write-file|search|clone|changes)[^>]*>[\s\S]*?<\/\1>/g, "").trim();
49372
- const body = (match[1] ?? "").trim();
49373
- if (kind === "changes") {
49374
- try {
49375
- const parsed = JSON.parse(body);
49376
- const display = [before2, parsed.summary].filter(Boolean).join(`
49377
-
49378
- `);
49379
- return { kind: "changes", content: display, patches: parsed.patches };
49380
- } catch {}
49381
- }
49382
- if (kind === "shell")
49383
- return { kind: "shell", content: before2, command: body };
49384
- if (kind === "fetch") {
49385
- const url = body.replace(/^<|>$/g, "").trim();
49386
- return { kind: "fetch", content: before2, url };
49387
- }
49388
- if (kind === "read-file")
49389
- return { kind: "read-file", content: before2, filePath: body };
49390
- if (kind === "read-folder")
49391
- return { kind: "read-folder", content: before2, folderPath: body };
49392
- if (kind === "delete-file")
49393
- return { kind: "delete-file", content: before2, filePath: body };
49394
- if (kind === "delete-folder")
49395
- return { kind: "delete-folder", content: before2, folderPath: body };
49396
- if (kind === "open-url") {
49397
- const url = body.replace(/^<|>$/g, "").trim();
49398
- return { kind: "open-url", content: before2, url };
49399
- }
49400
- if (kind === "generate-pdf") {
49401
- try {
49402
- const parsed = JSON.parse(body);
49403
- return {
49404
- kind: "generate-pdf",
49405
- content: before2,
49406
- filePath: parsed.path ?? parsed.filePath ?? "output.pdf",
49407
- pdfContent: parsed.content ?? ""
49408
- };
49409
- } catch {
49410
- return { kind: "text", content: text };
49411
- }
49412
- }
49413
- if (kind === "grep") {
49414
- try {
49415
- const parsed = JSON.parse(body);
49416
- return {
49417
- kind: "grep",
49418
- content: before2,
49419
- pattern: parsed.pattern,
49420
- glob: parsed.glob ?? "**/*"
49421
- };
49422
- } catch {
49423
- return { kind: "grep", content: before2, pattern: body, glob: "**/*" };
49424
- }
49425
- }
49426
- if (kind === "write-file") {
49427
- try {
49428
- const parsed = JSON.parse(body);
49429
- return {
49430
- kind: "write-file",
49431
- content: before2,
49432
- filePath: parsed.path,
49433
- fileContent: parsed.content
49434
- };
49435
- } catch {}
49436
- }
49437
- if (kind === "search")
49438
- return { kind: "search", content: before2, query: body };
49439
- if (kind === "clone") {
49440
- const url = body.replace(/^<|>$/g, "").trim();
49441
- return { kind: "clone", content: before2, repoUrl: url };
49442
- }
49443
- return { kind: "text", content: text.trim() };
49444
- }
49445
- function extractGithubUrl(text) {
49446
- const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
49447
- return match ? match[0] : null;
49448
- }
49449
- function toCloneUrl(url) {
49450
- const clean = url.replace(/\/+$/, "");
49451
- return clean.endsWith(".git") ? clean : `${clean}.git`;
49452
- }
49453
- function buildApiMessages(messages) {
49454
- return messages.map((m) => {
49455
- if (m.type === "tool") {
49456
- if (!m.approved) {
49457
- return {
49458
- role: "user",
49459
- content: "The tool call was denied by the user. Please respond without using that tool."
49460
- };
49461
- }
49462
- 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}`;
49463
- return {
49464
- role: "user",
49465
- content: `Here is the output from the ${label}:
49466
-
49467
- ${m.result}
49468
-
49469
- Please continue your response based on this output.`
49470
- };
49471
- }
49472
- return { role: m.role, content: m.content };
49473
- });
49474
- }
49475
- async function callChat(provider, systemPrompt, messages, abortSignal) {
49476
- const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
49477
- let url;
49478
- let headers;
49479
- let body;
49480
- if (provider.type === "anthropic") {
49481
- url = "https://api.anthropic.com/v1/messages";
49482
- headers = {
49483
- "Content-Type": "application/json",
49484
- "x-api-key": provider.apiKey ?? "",
49485
- "anthropic-version": "2023-06-01"
49486
- };
49487
- body = {
49488
- model: provider.model,
49489
- max_tokens: 4096,
49490
- system: systemPrompt,
49491
- messages: apiMessages
49492
- };
49493
- } else {
49494
- const base2 = provider.baseUrl ?? "https://api.openai.com/v1";
49495
- url = `${base2}/chat/completions`;
49496
- headers = {
49497
- "Content-Type": "application/json",
49498
- Authorization: `Bearer ${provider.apiKey}`
49499
- };
49500
- body = {
49501
- model: provider.model,
49502
- max_tokens: 4096,
49503
- messages: [{ role: "system", content: systemPrompt }, ...apiMessages]
49504
- };
49505
- }
49506
- const controller = new AbortController;
49507
- const timer = setTimeout(() => controller.abort(), 60000);
49508
- abortSignal?.addEventListener("abort", () => controller.abort());
49509
- const res = await fetch(url, {
49510
- method: "POST",
49511
- headers,
49512
- body: JSON.stringify(body),
49513
- signal: controller.signal
49514
- });
49515
- clearTimeout(timer);
49516
- if (!res.ok)
49517
- throw new Error(`API error ${res.status}: ${await res.text()}`);
49518
- const data = await res.json();
49519
- if (provider.type === "anthropic") {
49520
- const content = data.content;
49521
- return content.filter((b) => b.type === "text").map((b) => b.text).join("");
49522
- } else {
49523
- const choices = data.choices;
49524
- return choices[0]?.message.content ?? "";
49525
- }
49526
- }
49527
- var SKIP_DIRS4 = new Set([
49528
- "node_modules",
49529
- ".git",
49530
- "dist",
49531
- "build",
49532
- ".next",
49533
- "out",
49534
- "coverage",
49535
- "__pycache__",
49536
- ".venv",
49537
- "venv"
49538
- ]);
49539
- function walkDir3(dir, base2 = dir) {
49540
- const results = [];
49541
- let entries;
49542
- try {
49543
- entries = readdirSync3(dir, { encoding: "utf-8" });
49544
- } catch {
49545
- return results;
49546
- }
49547
- for (const entry of entries) {
49548
- if (SKIP_DIRS4.has(entry))
49549
- continue;
49550
- const full = path14.join(dir, entry);
49551
- const rel = path14.relative(base2, full).replace(/\\/g, "/");
49552
- let isDir = false;
49553
- try {
49554
- isDir = statSync4(full).isDirectory();
49555
- } catch {
49556
- continue;
49557
- }
49558
- if (isDir)
49559
- results.push(...walkDir3(full, base2));
49560
- else
49561
- results.push(rel);
49562
- }
49563
- return results;
49564
- }
49565
- function applyPatches3(repoPath, patches) {
49566
- for (const patch of patches) {
49567
- const fullPath = path14.join(repoPath, patch.path);
49568
- const dir = path14.dirname(fullPath);
49569
- if (!existsSync10(dir))
49570
- mkdirSync4(dir, { recursive: true });
49571
- writeFileSync6(fullPath, patch.content, "utf-8");
49572
- }
49573
- }
49574
- async function runShell(command, cwd2) {
49575
- return new Promise((resolve) => {
49576
- const { spawn: spawn2 } = __require("child_process");
49577
- const isWin = process.platform === "win32";
49578
- const shell = isWin ? "cmd.exe" : "/bin/sh";
49579
- const shellFlag = isWin ? "/c" : "-c";
49580
- const proc = spawn2(shell, [shellFlag, command], {
49581
- cwd: cwd2,
49582
- env: process.env,
49583
- stdio: ["ignore", "pipe", "pipe"]
49584
- });
49585
- const chunks = [];
49586
- const errChunks = [];
49587
- proc.stdout.on("data", (d) => chunks.push(d));
49588
- proc.stderr.on("data", (d) => errChunks.push(d));
49589
- const killTimer = setTimeout(() => {
49590
- proc.kill();
49591
- resolve("(command timed out after 5 minutes)");
49592
- }, 5 * 60 * 1000);
49593
- proc.on("close", (code) => {
49594
- clearTimeout(killTimer);
49595
- const stdout = Buffer.concat(chunks).toString("utf-8").trim();
49596
- const stderr = Buffer.concat(errChunks).toString("utf-8").trim();
49597
- const combined = [stdout, stderr].filter(Boolean).join(`
49598
- `);
49599
- resolve(combined || (code === 0 ? "(no output)" : `exit code ${code}`));
49600
- });
49601
- proc.on("error", (err) => {
49602
- clearTimeout(killTimer);
49603
- resolve(`Error: ${err.message}`);
49604
- });
49605
- });
49606
- }
49607
- function stripTags(html) {
49608
- return html.replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#\d+;/g, " ").replace(/\s+/g, " ").trim();
49609
- }
49610
- function extractTables(html) {
49611
- const tables = [];
49612
- const tableRe = /<table[\s\S]*?<\/table>/gi;
49613
- let tMatch;
49614
- while ((tMatch = tableRe.exec(html)) !== null) {
49615
- const tableHtml = tMatch[0];
49616
- const rows = [];
49617
- const rowRe = /<tr[\s\S]*?<\/tr>/gi;
49618
- let rMatch;
49619
- while ((rMatch = rowRe.exec(tableHtml)) !== null) {
49620
- const cells = [];
49621
- const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi;
49622
- let cMatch;
49623
- while ((cMatch = cellRe.exec(rMatch[0])) !== null) {
49624
- cells.push(stripTags(cMatch[1] ?? ""));
49625
- }
49626
- if (cells.length > 0)
49627
- rows.push(cells);
49628
- }
49629
- if (rows.length < 2)
49630
- continue;
49631
- const cols = Math.max(...rows.map((r) => r.length));
49632
- const padded = rows.map((r) => {
49633
- while (r.length < cols)
49634
- r.push("");
49635
- return r;
49636
- });
49637
- const widths = Array.from({ length: cols }, (_, ci) => Math.max(...padded.map((r) => (r[ci] ?? "").length), 3));
49638
- const fmt = (r) => r.map((c, ci) => c.padEnd(widths[ci] ?? 0)).join(" | ");
49639
- const header = fmt(padded[0]);
49640
- const sep = widths.map((w) => "-".repeat(w)).join("-|-");
49641
- const body = padded.slice(1).map(fmt).join(`
49642
- `);
49643
- tables.push(`${header}
49644
- ${sep}
49645
- ${body}`);
49646
- }
49647
- return tables.length > 0 ? `=== TABLES (${tables.length}) ===
49648
-
49649
- ${tables.join(`
49650
-
49651
- ---
49652
-
49653
- `)}` : "";
49654
- }
49655
- function extractLists(html) {
49656
- const lists = [];
49657
- const listRe = /<[ou]l[\s\S]*?<\/[ou]l>/gi;
49658
- let lMatch;
49659
- while ((lMatch = listRe.exec(html)) !== null) {
49660
- const items = [];
49661
- const itemRe = /<li[^>]*>([\s\S]*?)<\/li>/gi;
49662
- let iMatch;
49663
- while ((iMatch = itemRe.exec(lMatch[0])) !== null) {
49664
- const text = stripTags(iMatch[1] ?? "");
49665
- if (text.length > 2)
49666
- items.push(`• ${text}`);
49667
- }
49668
- if (items.length > 1)
49669
- lists.push(items.join(`
49670
- `));
49671
- }
49672
- return lists.length > 0 ? `=== LISTS ===
49673
-
49674
- ${lists.slice(0, 5).join(`
49675
-
49676
- `)}` : "";
49677
- }
49678
- async function fetchUrl(url) {
49679
- const res = await fetch(url, {
49680
- headers: {
49681
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49682
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
49683
- "Accept-Language": "en-US,en;q=0.5"
49684
- },
49685
- signal: AbortSignal.timeout(15000)
49686
- });
49687
- if (!res.ok)
49688
- throw new Error(`HTTP ${res.status}: ${res.statusText}`);
49689
- const contentType = res.headers.get("content-type") ?? "";
49690
- if (contentType.includes("application/json")) {
49691
- const json = await res.json();
49692
- return JSON.stringify(json, null, 2).slice(0, 8000);
49693
- }
49694
- const html = await res.text();
49695
- const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
49696
- const title = titleMatch ? stripTags(titleMatch[1]) : "No title";
49697
- const tables = extractTables(html);
49698
- const lists = extractLists(html);
49699
- const bodyText = stripTags(html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<nav[\s\S]*?<\/nav>/gi, "").replace(/<footer[\s\S]*?<\/footer>/gi, "").replace(/<header[\s\S]*?<\/header>/gi, "")).replace(/\s{3,}/g, `
49700
-
49701
- `).slice(0, 3000);
49702
- const parts = [`PAGE: ${title}`, `URL: ${url}`];
49703
- if (tables)
49704
- parts.push(tables);
49705
- if (lists)
49706
- parts.push(lists);
49707
- parts.push(`=== TEXT ===
49708
- ${bodyText}`);
49709
- return parts.join(`
49710
-
49711
- `);
49712
- }
49713
- async function searchWeb(query) {
49714
- const encoded = encodeURIComponent(query);
49715
- const ddgUrl = `https://api.duckduckgo.com/?q=${encoded}&format=json&no_html=1&skip_disambig=1`;
49716
- try {
49717
- const res = await fetch(ddgUrl, {
49718
- headers: { "User-Agent": "Lens/1.0" },
49719
- signal: AbortSignal.timeout(8000)
49720
- });
49721
- if (res.ok) {
49722
- const data = await res.json();
49723
- const parts = [`Search: ${query}`];
49724
- if (data.Answer)
49725
- parts.push(`Answer: ${data.Answer}`);
49726
- if (data.AbstractText) {
49727
- parts.push(`Summary: ${data.AbstractText}`);
49728
- if (data.AbstractURL)
49729
- parts.push(`Source: ${data.AbstractURL}`);
49730
- }
49731
- if (data.Infobox?.content?.length) {
49732
- const fields = data.Infobox.content.slice(0, 8).map((f) => ` ${f.label}: ${f.value}`).join(`
49733
- `);
49734
- parts.push(`Info:
49735
- ${fields}`);
49736
- }
49737
- if (data.RelatedTopics?.length) {
49738
- const topics = data.RelatedTopics.filter((t) => t.Text).slice(0, 5).map((t) => ` - ${t.Text}`).join(`
49739
- `);
49740
- if (topics)
49741
- parts.push(`Related:
49742
- ${topics}`);
49743
- }
49744
- const result2 = parts.join(`
49745
-
49746
- `);
49747
- if (result2.length > 60)
49748
- return result2;
49749
- }
49750
- } catch {}
49751
- try {
49752
- const htmlUrl = `https://html.duckduckgo.com/html/?q=${encoded}`;
49753
- const res = await fetch(htmlUrl, {
49754
- headers: {
49755
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
49756
- Accept: "text/html"
49757
- },
49758
- signal: AbortSignal.timeout(1e4)
49759
- });
49760
- if (!res.ok)
49761
- throw new Error(`HTTP ${res.status}`);
49762
- const html = await res.text();
49763
- const snippets = [];
49764
- const snippetRe = /class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;
49765
- let m;
49766
- while ((m = snippetRe.exec(html)) !== null && snippets.length < 6) {
49767
- const text = m[1].replace(/<[^>]+>/g, " ").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/\s+/g, " ").trim();
49768
- if (text.length > 20)
49769
- snippets.push(`- ${text}`);
49770
- }
49771
- const links = [];
49772
- const linkRe = /class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/g;
49773
- while ((m = linkRe.exec(html)) !== null && links.length < 5) {
49774
- const title = m[2].replace(/<[^>]+>/g, "").trim();
49775
- const href = m[1];
49776
- if (title && href)
49777
- links.push(` ${title} — ${href}`);
49778
- }
49779
- if (snippets.length === 0 && links.length === 0) {
49780
- return `No results found for: ${query}`;
49781
- }
49782
- const parts = [`Search results for: ${query}`];
49783
- if (snippets.length > 0)
49784
- parts.push(`Snippets:
49785
- ${snippets.join(`
49786
- `)}`);
49787
- if (links.length > 0)
49788
- parts.push(`Links:
49789
- ${links.join(`
49790
- `)}`);
49791
- return parts.join(`
49792
-
49793
- `);
49794
- } catch (err) {
49795
- return `Search failed: ${err instanceof Error ? err.message : String(err)}`;
49796
- }
49797
- }
49798
- function readFile(filePath, repoPath) {
49799
- const candidates = path14.isAbsolute(filePath) ? [filePath] : [filePath, path14.join(repoPath, filePath)];
49800
- for (const candidate of candidates) {
49801
- if (existsSync10(candidate)) {
49802
- try {
49803
- const content = readFileSync10(candidate, "utf-8");
49804
- const lines = content.split(`
49805
- `).length;
49806
- return `File: ${candidate} (${lines} lines)
49807
-
49808
- ${content.slice(0, 8000)}${content.length > 8000 ? `
49809
-
49810
- … (truncated)` : ""}`;
49811
- } catch (err) {
49812
- return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
49813
- }
49814
- }
49815
- }
49816
- return `File not found: ${filePath}. If reading from a cloned repo, use the full absolute path e.g. C:\\Users\\...\\repo\\file.ts`;
49817
- }
49818
- function readFolder(folderPath, repoPath) {
49819
- const sanitized = folderPath.replace(/^(ls|dir|find|tree|cat|read|ls -la?|ls -al?)\s+/i, "").trim();
49820
- const candidates = path14.isAbsolute(sanitized) ? [sanitized] : [sanitized, path14.join(repoPath, sanitized)];
49821
- for (const candidate of candidates) {
49822
- if (!existsSync10(candidate))
49823
- continue;
49824
- let stat;
49825
- try {
49826
- stat = statSync4(candidate);
49827
- } catch {
49828
- continue;
49829
- }
49830
- if (!stat.isDirectory()) {
49831
- return `Not a directory: ${candidate}. Use read-file to read a file.`;
49832
- }
49833
- let entries;
49834
- try {
49835
- entries = readdirSync3(candidate, { encoding: "utf-8" });
49836
- } catch (err) {
49837
- return `Error reading folder: ${err instanceof Error ? err.message : String(err)}`;
49838
- }
49839
- const files = [];
49840
- const subfolders = [];
49841
- for (const entry of entries) {
49842
- if (entry.startsWith(".") && entry !== ".env")
49843
- continue;
49844
- const full = path14.join(candidate, entry);
49845
- try {
49846
- if (statSync4(full).isDirectory()) {
49847
- subfolders.push(`${entry}/`);
49848
- } else {
49849
- files.push(entry);
49850
- }
49851
- } catch {}
49852
- }
49853
- const total = files.length + subfolders.length;
49854
- const lines = [`Folder: ${candidate} (${total} entries)`, ""];
49855
- if (files.length > 0) {
49856
- lines.push("Files:");
49857
- files.forEach((f) => lines.push(` ${f}`));
49858
- }
49859
- if (subfolders.length > 0) {
49860
- if (files.length > 0)
49861
- lines.push("");
49862
- lines.push("Subfolders:");
49863
- subfolders.forEach((d) => lines.push(` ${d}`));
49864
- }
49865
- if (total === 0) {
49866
- lines.push("(empty folder)");
49867
- }
49868
- return lines.join(`
49869
- `);
49870
- }
49871
- return `Folder not found: ${sanitized}`;
49872
- }
49873
- function grepFiles(pattern, glob, repoPath) {
49874
- let regex2;
49875
- try {
49876
- regex2 = new RegExp(pattern, "i");
49877
- } catch {
49878
- regex2 = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i");
49879
- }
49880
- const globToFilter = (g) => {
49881
- const cleaned = g.replace(/^\*\*\//, "");
49882
- const parts = cleaned.split("/");
49883
- const ext = parts[parts.length - 1];
49884
- const prefix = parts.slice(0, -1).join("/");
49885
- return (rel) => {
49886
- if (ext?.startsWith("*.")) {
49887
- const extSuffix = ext.slice(1);
49888
- if (!rel.endsWith(extSuffix))
49889
- return false;
49890
- } else if (ext && !ext.includes("*")) {
49891
- if (!rel.endsWith(ext))
49892
- return false;
49893
- }
49894
- if (prefix && !prefix.includes("*")) {
49895
- if (!rel.startsWith(prefix))
49896
- return false;
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 });
49897
49886
  }
49898
- return true;
49899
- };
49900
- };
49901
- const filter2 = globToFilter(glob);
49902
- const allFiles = walkDir3(repoPath);
49903
- const matchedFiles = allFiles.filter(filter2);
49904
- if (matchedFiles.length === 0) {
49905
- return `No files matched glob: ${glob}`;
49887
+ }
49906
49888
  }
49907
- const results = [];
49908
- let totalMatches = 0;
49909
- for (const relPath of matchedFiles) {
49910
- const fullPath = path14.join(repoPath, relPath);
49911
- let content;
49889
+ if (candidates.length === 0)
49890
+ return { kind: "text", content: text.trim() };
49891
+ 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();
49894
+ const body = (match[1] ?? "").trim();
49895
+ if (kind === "changes") {
49912
49896
  try {
49913
- content = readFileSync10(fullPath, "utf-8");
49914
- } catch {
49915
- continue;
49916
- }
49917
- const lines = content.split(`
49897
+ const parsed = JSON.parse(body);
49898
+ const display = [before2, parsed.summary].filter(Boolean).join(`
49899
+
49918
49900
  `);
49919
- const fileMatches = [];
49920
- lines.forEach((line, i) => {
49921
- if (regex2.test(line)) {
49922
- fileMatches.push(` ${i + 1}: ${line.trimEnd()}`);
49923
- totalMatches++;
49924
- }
49925
- });
49926
- if (fileMatches.length > 0) {
49927
- results.push(`${relPath}
49928
- ${fileMatches.join(`
49929
- `)}`);
49930
- }
49931
- if (totalMatches >= 200) {
49932
- results.push("(truncated — too many matches)");
49933
- break;
49934
- }
49901
+ return { kind: "changes", content: display, patches: parsed.patches };
49902
+ } catch {}
49935
49903
  }
49936
- if (results.length === 0) {
49937
- return `No matches for /${pattern}/ in ${matchedFiles.length} file(s) matching ${glob}`;
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
+ }
49938
49946
  }
49939
- return `grep /${pattern}/ ${glob} — ${totalMatches} match(es) in ${results.length} file(s)
49940
-
49941
- ${results.join(`
49942
-
49943
- `)}`;
49944
- }
49945
- function writeFile(filePath, content, repoPath) {
49946
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
49947
- try {
49948
- const dir = path14.dirname(fullPath);
49949
- if (!existsSync10(dir))
49950
- mkdirSync4(dir, { recursive: true });
49951
- writeFileSync6(fullPath, content, "utf-8");
49952
- const lines = content.split(`
49953
- `).length;
49954
- return `Written: ${fullPath} (${lines} lines, ${content.length} bytes)`;
49955
- } catch (err) {
49956
- return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
49947
+ if (kind === "grep") {
49948
+ try {
49949
+ const parsed = JSON.parse(body);
49950
+ return {
49951
+ kind: "grep",
49952
+ content: before2,
49953
+ pattern: parsed.pattern,
49954
+ glob: parsed.glob ?? "**/*"
49955
+ };
49956
+ } catch {
49957
+ return { kind: "grep", content: before2, pattern: body, glob: "**/*" };
49958
+ }
49957
49959
  }
49958
- }
49959
- function deleteFile(filePath, repoPath) {
49960
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
49961
- try {
49962
- if (!existsSync10(fullPath))
49963
- return `File not found: ${fullPath}`;
49964
- const { unlinkSync } = __require("fs");
49965
- unlinkSync(fullPath);
49966
- return `Deleted: ${fullPath}`;
49967
- } catch (err) {
49968
- return `Error deleting file: ${err instanceof Error ? err.message : String(err)}`;
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 {}
49969
49970
  }
49971
+ return { kind: "text", content: text.trim() };
49970
49972
  }
49971
- function deleteFolder(folderPath, repoPath) {
49972
- const fullPath = path14.isAbsolute(folderPath) ? folderPath : path14.join(repoPath, folderPath);
49973
- try {
49974
- if (!existsSync10(fullPath))
49975
- return `Folder not found: ${fullPath}`;
49976
- const { rmSync } = __require("fs");
49977
- rmSync(fullPath, { recursive: true, force: true });
49978
- return `Deleted folder: ${fullPath}`;
49979
- } catch (err) {
49980
- return `Error deleting folder: ${err instanceof Error ? err.message : String(err)}`;
49981
- }
49973
+ function extractGithubUrl(text) {
49974
+ const match = text.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/);
49975
+ return match ? match[0] : null;
49982
49976
  }
49983
- function openUrl(url) {
49984
- try {
49985
- const { execSync: execSync2 } = __require("child_process");
49986
- const platform2 = process.platform;
49987
- if (platform2 === "win32") {
49988
- execSync2(`start "" "${url}"`, { stdio: "ignore" });
49989
- } else if (platform2 === "darwin") {
49990
- execSync2(`open "${url}"`, { stdio: "ignore" });
49991
- } else {
49992
- execSync2(`xdg-open "${url}"`, { stdio: "ignore" });
49993
- }
49994
- return `Opened: ${url}`;
49995
- } catch (err) {
49996
- return `Error opening URL: ${err instanceof Error ? err.message : String(err)}`;
49997
- }
49977
+ function toCloneUrl(url) {
49978
+ const clean = url.replace(/\/+$/, "");
49979
+ return clean.endsWith(".git") ? clean : `${clean}.git`;
49998
49980
  }
49999
- function generatePdf(filePath, content, repoPath) {
50000
- const fullPath = path14.isAbsolute(filePath) ? filePath : path14.join(repoPath, filePath);
50001
- try {
50002
- const dir = path14.dirname(fullPath);
50003
- if (!existsSync10(dir))
50004
- mkdirSync4(dir, { recursive: true });
50005
- const escaped = content.replace(/\\/g, "\\\\").replace(/"""/g, "\\\"\\\"\\\"").replace(/\r/g, "");
50006
- const script = `
50007
- import sys
50008
- try:
50009
- from reportlab.lib.pagesizes import letter
50010
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
50011
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
50012
- from reportlab.lib.units import inch
50013
- from reportlab.lib import colors
50014
- except ImportError:
50015
- import subprocess
50016
- subprocess.check_call([sys.executable, "-m", "pip", "install", "reportlab", "--break-system-packages", "-q"])
50017
- from reportlab.lib.pagesizes import letter
50018
- from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
50019
- from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
50020
- from reportlab.lib.units import inch
50021
- from reportlab.lib import colors
50022
-
50023
- doc = SimpleDocTemplate(
50024
- r"""${fullPath}""",
50025
- pagesize=letter,
50026
- rightMargin=inch,
50027
- leftMargin=inch,
50028
- topMargin=inch,
50029
- bottomMargin=inch,
50030
- )
50031
-
50032
- styles = getSampleStyleSheet()
50033
- styles.add(ParagraphStyle(name="H1", parent=styles["Heading1"], fontSize=22, spaceAfter=10))
50034
- styles.add(ParagraphStyle(name="H2", parent=styles["Heading2"], fontSize=16, spaceAfter=8))
50035
- styles.add(ParagraphStyle(name="H3", parent=styles["Heading3"], fontSize=13, spaceAfter=6))
50036
- styles.add(ParagraphStyle(name="Body", parent=styles["Normal"], fontSize=11, leading=16, spaceAfter=8))
50037
- styles.add(ParagraphStyle(name="Bullet", parent=styles["Normal"], fontSize=11, leading=16, leftIndent=20, spaceAfter=4, bulletIndent=10))
49981
+ function buildApiMessages(messages) {
49982
+ return messages.map((m) => {
49983
+ if (m.type === "tool") {
49984
+ if (!m.approved) {
49985
+ return {
49986
+ role: "user",
49987
+ content: "The tool call was denied by the user. Please respond without using that tool."
49988
+ };
49989
+ }
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
+ return {
49992
+ role: "user",
49993
+ content: `Here is the output from the ${label}:
50038
49994
 
50039
- raw = """${escaped}"""
49995
+ ${m.result}
50040
49996
 
50041
- story = []
50042
- for line in raw.split("\\n"):
50043
- s = line.rstrip()
50044
- if s.startswith("### "):
50045
- story.append(Paragraph(s[4:], styles["H3"]))
50046
- elif s.startswith("## "):
50047
- story.append(Spacer(1, 6))
50048
- story.append(Paragraph(s[3:], styles["H2"]))
50049
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
50050
- elif s.startswith("# "):
50051
- story.append(Paragraph(s[2:], styles["H1"]))
50052
- story.append(HRFlowable(width="100%", thickness=1, color=colors.black, spaceAfter=6))
50053
- elif s.startswith("- ") or s.startswith("* "):
50054
- story.append(Paragraph(u"\\u2022 " + s[2:], styles["Bullet"]))
50055
- elif s.startswith("---"):
50056
- story.append(HRFlowable(width="100%", thickness=0.5, color=colors.grey, spaceAfter=4))
50057
- elif s == "":
50058
- story.append(Spacer(1, 6))
50059
- else:
50060
- import re
50061
- s = re.sub(r"\\*\\*(.+?)\\*\\*", r"<b>\\1</b>", s)
50062
- s = re.sub(r"\\*(.+?)\\*", r"<i>\\1</i>", s)
50063
- s = re.sub(r"\`(.+?)\`", r"<font name='Courier'>\\1</font>", s)
50064
- story.append(Paragraph(s, styles["Body"]))
49997
+ Please continue your response based on this output.`
49998
+ };
49999
+ }
50000
+ return { role: m.role, content: m.content };
50001
+ });
50002
+ }
50003
+ async function callChat(provider, systemPrompt, messages, abortSignal) {
50004
+ const apiMessages = [...FEW_SHOT_MESSAGES, ...buildApiMessages(messages)];
50005
+ let url;
50006
+ let headers;
50007
+ let body;
50008
+ if (provider.type === "anthropic") {
50009
+ url = "https://api.anthropic.com/v1/messages";
50010
+ headers = {
50011
+ "Content-Type": "application/json",
50012
+ "x-api-key": provider.apiKey ?? "",
50013
+ "anthropic-version": "2023-06-01"
50014
+ };
50015
+ body = {
50016
+ model: provider.model,
50017
+ max_tokens: 4096,
50018
+ system: systemPrompt,
50019
+ messages: apiMessages
50020
+ };
50021
+ } else {
50022
+ const base2 = provider.baseUrl ?? "https://api.openai.com/v1";
50023
+ url = `${base2}/chat/completions`;
50024
+ headers = {
50025
+ "Content-Type": "application/json",
50026
+ Authorization: `Bearer ${provider.apiKey}`
50027
+ };
50028
+ body = {
50029
+ model: provider.model,
50030
+ max_tokens: 4096,
50031
+ messages: [{ role: "system", content: systemPrompt }, ...apiMessages]
50032
+ };
50033
+ }
50034
+ const controller = new AbortController;
50035
+ const timer = setTimeout(() => controller.abort(), 60000);
50036
+ abortSignal?.addEventListener("abort", () => controller.abort());
50037
+ const res = await fetch(url, {
50038
+ method: "POST",
50039
+ headers,
50040
+ body: JSON.stringify(body),
50041
+ signal: controller.signal
50042
+ });
50043
+ clearTimeout(timer);
50044
+ if (!res.ok)
50045
+ throw new Error(`API error ${res.status}: ${await res.text()}`);
50046
+ const data = await res.json();
50047
+ if (provider.type === "anthropic") {
50048
+ const content = data.content;
50049
+ return content.filter((b) => b.type === "text").map((b) => b.text).join("");
50050
+ } else {
50051
+ const choices = data.choices;
50052
+ return choices[0]?.message.content ?? "";
50053
+ }
50054
+ }
50065
50055
 
50066
- doc.build(story)
50067
- print("OK")
50068
- `.replace("${fullPath}", fullPath.replace(/\\/g, "/")).replace("${escaped}", escaped);
50069
- const os6 = __require("os");
50070
- const tmpFile = path14.join(os6.tmpdir(), `lens_pdf_${Date.now()}.py`);
50071
- writeFileSync6(tmpFile, script, "utf-8");
50072
- const { execSync: execSync2 } = __require("child_process");
50073
- execSync2(`python "${tmpFile}"`, { stdio: "pipe" });
50056
+ // src/utils/chatHistory.ts
50057
+ import {
50058
+ existsSync as existsSync12,
50059
+ mkdirSync as mkdirSync6,
50060
+ readFileSync as readFileSync11,
50061
+ readdirSync as readdirSync4,
50062
+ writeFileSync as writeFileSync8,
50063
+ unlinkSync
50064
+ } from "fs";
50065
+ import path16 from "path";
50066
+ import os7 from "os";
50067
+ var LENS_DIR = path16.join(os7.homedir(), ".lens");
50068
+ var CHATS_DIR = path16.join(LENS_DIR, "chats");
50069
+ function ensureChatsDir() {
50070
+ if (!existsSync12(CHATS_DIR))
50071
+ mkdirSync6(CHATS_DIR, { recursive: true });
50072
+ }
50073
+ function chatFilePath(name) {
50074
+ const safe = name.replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
50075
+ return path16.join(CHATS_DIR, `${safe}.json`);
50076
+ }
50077
+ function saveChat(name, repoPath, messages) {
50078
+ ensureChatsDir();
50079
+ const data = {
50080
+ name,
50081
+ repoPath,
50082
+ messages,
50083
+ savedAt: new Date().toISOString(),
50084
+ userMessageCount: messages.filter((m) => m.role === "user").length
50085
+ };
50086
+ writeFileSync8(chatFilePath(name), JSON.stringify(data, null, 2), "utf-8");
50087
+ }
50088
+ function loadChat(name) {
50089
+ const filePath = chatFilePath(name);
50090
+ if (!existsSync12(filePath))
50091
+ return null;
50092
+ try {
50093
+ return JSON.parse(readFileSync11(filePath, "utf-8"));
50094
+ } catch {
50095
+ return null;
50096
+ }
50097
+ }
50098
+ function listChats(repoPath) {
50099
+ ensureChatsDir();
50100
+ const files = readdirSync4(CHATS_DIR).filter((f) => f.endsWith(".json"));
50101
+ const chats = [];
50102
+ for (const file of files) {
50074
50103
  try {
50075
- __require("fs").unlinkSync(tmpFile);
50104
+ const data = JSON.parse(readFileSync11(path16.join(CHATS_DIR, file), "utf-8"));
50105
+ if (!repoPath || data.repoPath === repoPath)
50106
+ chats.push(data);
50076
50107
  } catch {}
50077
- return `PDF generated: ${fullPath}`;
50078
- } catch (err) {
50079
- return `Error generating PDF: ${err instanceof Error ? err.message : String(err)}`;
50108
+ }
50109
+ return chats.sort((a, b) => new Date(b.savedAt).getTime() - new Date(a.savedAt).getTime());
50110
+ }
50111
+ function deleteChat(name) {
50112
+ const filePath = chatFilePath(name);
50113
+ if (!existsSync12(filePath))
50114
+ return false;
50115
+ try {
50116
+ unlinkSync(filePath);
50117
+ return true;
50118
+ } catch {
50119
+ return false;
50080
50120
  }
50081
50121
  }
50122
+ function getChatNameSuggestions(messages) {
50123
+ const userMsgs = messages.filter((m) => m.role === "user").map((m) => m.content.toLowerCase().trim());
50124
+ const date = new Date().toISOString().slice(0, 10);
50125
+ if (userMsgs.length === 0) {
50126
+ return [`chat-${date}`, `session-${date}`, `new-chat`];
50127
+ }
50128
+ const suggestions = [];
50129
+ const toSlug = (s) => s.replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean).slice(0, 4).join("-");
50130
+ const firstSlug = toSlug(userMsgs[0]);
50131
+ if (firstSlug)
50132
+ suggestions.push(firstSlug);
50133
+ if (userMsgs.length > 1) {
50134
+ const lastSlug = toSlug(userMsgs[userMsgs.length - 1]);
50135
+ if (lastSlug && lastSlug !== firstSlug)
50136
+ suggestions.push(lastSlug);
50137
+ }
50138
+ suggestions.push(`session-${date}`);
50139
+ return suggestions.slice(0, 3);
50140
+ }
50082
50141
 
50083
50142
  // src/components/chat/ChatMessage.tsx
50084
50143
  var jsx_dev_runtime19 = __toESM(require_jsx_dev_runtime(), 1);
@@ -50182,6 +50241,19 @@ function MessageBody({ content }) {
50182
50241
  })
50183
50242
  }, undefined, false, undefined, this);
50184
50243
  }
50244
+ function summarizeToolContent(toolName, content) {
50245
+ if (toolName === "write-file" || toolName === "read-file") {
50246
+ const pathMatch = content.match(/"path"\s*:\s*"([^"]+)"/);
50247
+ if (pathMatch)
50248
+ return pathMatch[1];
50249
+ }
50250
+ if (content.includes('"summary"')) {
50251
+ const summaryMatch = content.match(/"summary"\s*:\s*"([^"]+)"/);
50252
+ if (summaryMatch)
50253
+ return summaryMatch[1];
50254
+ }
50255
+ return content.length > 120 ? content.slice(0, 120) + "…" : content;
50256
+ }
50185
50257
  function StaticMessage({ msg }) {
50186
50258
  if (msg.role === "user") {
50187
50259
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
@@ -50212,7 +50284,7 @@ function StaticMessage({ msg }) {
50212
50284
  search: "?"
50213
50285
  };
50214
50286
  const icon = icons[msg.toolName] ?? "·";
50215
- const label = msg.toolName === "shell" ? msg.content : msg.toolName === "search" ? `"${msg.content}"` : msg.content;
50287
+ const label = msg.toolName === "shell" ? msg.content : msg.toolName === "search" ? `"${msg.content}"` : summarizeToolContent(msg.toolName, msg.content);
50216
50288
  return /* @__PURE__ */ jsx_dev_runtime19.jsxDEV(Box_default, {
50217
50289
  flexDirection: "column",
50218
50290
  marginBottom: 1,
@@ -50497,7 +50569,8 @@ function PermissionPrompt({
50497
50569
  function InputBox({
50498
50570
  value,
50499
50571
  onChange,
50500
- onSubmit
50572
+ onSubmit,
50573
+ inputKey
50501
50574
  }) {
50502
50575
  return /* @__PURE__ */ jsx_dev_runtime20.jsxDEV(Box_default, {
50503
50576
  marginTop: 1,
@@ -50518,7 +50591,7 @@ function InputBox({
50518
50591
  value,
50519
50592
  onChange,
50520
50593
  onSubmit
50521
- }, undefined, false, undefined, this)
50594
+ }, inputKey, false, undefined, this)
50522
50595
  ]
50523
50596
  }, undefined, true, undefined, this)
50524
50597
  }, undefined, false, undefined, this);
@@ -52283,53 +52356,96 @@ function TimelineRunner({
52283
52356
  }, undefined, true, undefined, this);
52284
52357
  }
52285
52358
 
52286
- // src/utils/history.ts
52287
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
52288
- import path15 from "path";
52289
- import os6 from "os";
52290
- var LENS_DIR = path15.join(os6.homedir(), ".lens");
52291
- var HISTORY_PATH = path15.join(LENS_DIR, "history.json");
52292
- function loadHistory() {
52293
- if (!existsSync11(HISTORY_PATH))
52294
- return { entries: [] };
52359
+ // src/utils/memory.ts
52360
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
52361
+ import path17 from "path";
52362
+ import os8 from "os";
52363
+ var LENS_DIR2 = path17.join(os8.homedir(), ".lens");
52364
+ var MEMORY_PATH = path17.join(LENS_DIR2, "memory.json");
52365
+ function loadMemoryFile() {
52366
+ if (!existsSync13(MEMORY_PATH))
52367
+ return { entries: [], memories: [] };
52295
52368
  try {
52296
- return JSON.parse(readFileSync11(HISTORY_PATH, "utf-8"));
52369
+ const data = JSON.parse(readFileSync12(MEMORY_PATH, "utf-8"));
52370
+ return {
52371
+ entries: data.entries ?? [],
52372
+ memories: data.memories ?? []
52373
+ };
52297
52374
  } catch {
52298
- return { entries: [] };
52375
+ return { entries: [], memories: [] };
52299
52376
  }
52300
52377
  }
52301
- function saveHistory(h) {
52302
- if (!existsSync11(LENS_DIR))
52303
- mkdirSync5(LENS_DIR, { recursive: true });
52304
- writeFileSync7(HISTORY_PATH, JSON.stringify(h, null, 2), "utf-8");
52378
+ function saveMemoryFile(m) {
52379
+ if (!existsSync13(LENS_DIR2))
52380
+ mkdirSync7(LENS_DIR2, { recursive: true });
52381
+ writeFileSync9(MEMORY_PATH, JSON.stringify(m, null, 2), "utf-8");
52305
52382
  }
52306
- function appendHistory(entry) {
52307
- const h = loadHistory();
52308
- h.entries.push({ ...entry, timestamp: new Date().toISOString() });
52309
- if (h.entries.length > 500)
52310
- h.entries = h.entries.slice(-500);
52311
- saveHistory(h);
52383
+ function appendMemory(entry) {
52384
+ const m = loadMemoryFile();
52385
+ m.entries.push({ ...entry, timestamp: new Date().toISOString() });
52386
+ if (m.entries.length > 500)
52387
+ m.entries = m.entries.slice(-500);
52388
+ saveMemoryFile(m);
52312
52389
  }
52313
- function buildHistorySummary(repoPath) {
52314
- const h = loadHistory();
52315
- const relevant = h.entries.filter((e) => e.repoPath === repoPath).slice(-50);
52316
- if (relevant.length === 0)
52317
- return "";
52318
- const lines = relevant.map((e) => {
52319
- const ts = new Date(e.timestamp).toLocaleString();
52320
- return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
52321
- });
52322
- return `## WHAT YOU HAVE ALREADY DONE IN THIS REPO
52390
+ function buildMemorySummary(repoPath) {
52391
+ const m = loadMemoryFile();
52392
+ const relevant = m.entries.filter((e) => e.repoPath === repoPath).slice(-50);
52393
+ const memories = m.memories.filter((mem) => mem.repoPath === repoPath);
52394
+ const parts = [];
52395
+ if (memories.length > 0) {
52396
+ parts.push(`## MEMORIES ABOUT THIS REPO
52397
+
52398
+ ${memories.map((mem) => `- [${mem.id}] ${mem.content}`).join(`
52399
+ `)}`);
52400
+ }
52401
+ if (relevant.length > 0) {
52402
+ const lines = relevant.map((e) => {
52403
+ const ts = new Date(e.timestamp).toLocaleString();
52404
+ return `[${ts}] ${e.kind}: ${e.detail} — ${e.summary}`;
52405
+ });
52406
+ parts.push(`## WHAT YOU HAVE ALREADY DONE IN THIS REPO
52323
52407
 
52324
52408
  The following actions have already been completed. Do NOT repeat them unless the user explicitly asks you to redo something:
52325
52409
 
52326
52410
  ${lines.join(`
52327
- `)}`;
52411
+ `)}`);
52412
+ }
52413
+ return parts.join(`
52414
+
52415
+ `);
52416
+ }
52417
+ function clearRepoMemory(repoPath) {
52418
+ const m = loadMemoryFile();
52419
+ m.entries = m.entries.filter((e) => e.repoPath !== repoPath);
52420
+ m.memories = m.memories.filter((mem) => mem.repoPath !== repoPath);
52421
+ saveMemoryFile(m);
52422
+ }
52423
+ function generateId() {
52424
+ return Math.random().toString(36).slice(2, 8);
52425
+ }
52426
+ function addMemory(content, repoPath) {
52427
+ const m = loadMemoryFile();
52428
+ const memory = {
52429
+ id: generateId(),
52430
+ content,
52431
+ timestamp: new Date().toISOString(),
52432
+ repoPath
52433
+ };
52434
+ m.memories.push(memory);
52435
+ saveMemoryFile(m);
52436
+ return memory;
52328
52437
  }
52329
- function clearRepoHistory(repoPath) {
52330
- const h = loadHistory();
52331
- h.entries = h.entries.filter((e) => e.repoPath !== repoPath);
52332
- saveHistory(h);
52438
+ function deleteMemory(id, repoPath) {
52439
+ const m = loadMemoryFile();
52440
+ const before2 = m.memories.length;
52441
+ m.memories = m.memories.filter((mem) => !(mem.id === id && mem.repoPath === repoPath));
52442
+ if (m.memories.length === before2)
52443
+ return false;
52444
+ saveMemoryFile(m);
52445
+ return true;
52446
+ }
52447
+ function listMemories(repoPath) {
52448
+ return loadMemoryFile().memories.filter((mem) => mem.repoPath === repoPath);
52333
52449
  }
52334
52450
 
52335
52451
  // src/components/chat/ChatRunner.tsx
@@ -52337,58 +52453,120 @@ var jsx_dev_runtime22 = __toESM(require_jsx_dev_runtime(), 1);
52337
52453
  var COMMANDS = [
52338
52454
  { cmd: "/timeline", desc: "browse commit history" },
52339
52455
  { cmd: "/clear history", desc: "wipe session memory for this repo" },
52340
- { cmd: "/review", desc: "review current codebsae" },
52341
- { cmd: "/auto", desc: "toggle auto-approve for read/search tools" }
52456
+ { cmd: "/review", desc: "review current codebase" },
52457
+ { cmd: "/auto", desc: "toggle auto-approve for read/search tools" },
52458
+ { cmd: "/chat", desc: "chat history commands" },
52459
+ { cmd: "/chat list", desc: "list saved chats for this repo" },
52460
+ { cmd: "/chat load", desc: "load a saved chat by name" },
52461
+ { cmd: "/chat rename", desc: "rename the current chat" },
52462
+ { cmd: "/chat delete", desc: "delete a saved chat by name" },
52463
+ { cmd: "/memory", desc: "memory commands" },
52464
+ { cmd: "/memory list", desc: "list all memories for this repo" },
52465
+ { cmd: "/memory add", desc: "add a memory" },
52466
+ { cmd: "/memory delete", desc: "delete a memory by id" },
52467
+ { cmd: "/memory clear", desc: "clear all memories for this repo" }
52342
52468
  ];
52343
52469
  function CommandPalette({
52344
52470
  query,
52345
- onSelect
52471
+ onSelect,
52472
+ recentChats
52346
52473
  }) {
52347
52474
  const q = query.toLowerCase();
52475
+ const isChatLoad = q.startsWith("/chat load") || q.startsWith("/chat delete");
52476
+ const chatFilter = isChatLoad ? q.startsWith("/chat load") ? q.slice("/chat load".length).trim() : q.slice("/chat delete".length).trim() : "";
52477
+ const filteredChats = chatFilter ? recentChats.filter((n) => n.toLowerCase().includes(chatFilter)) : recentChats;
52348
52478
  const matches2 = COMMANDS.filter((c) => c.cmd.startsWith(q));
52349
- if (!matches2.length)
52479
+ if (!matches2.length && !isChatLoad)
52480
+ return null;
52481
+ if (!matches2.length && isChatLoad && filteredChats.length === 0)
52350
52482
  return null;
52351
52483
  return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52352
52484
  flexDirection: "column",
52353
52485
  marginBottom: 1,
52354
52486
  marginLeft: 2,
52355
- children: matches2.map((c, i) => {
52356
- const isExact = c.cmd === query;
52357
- return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52358
- gap: 2,
52487
+ children: [
52488
+ matches2.map((c, i) => {
52489
+ const isExact = c.cmd === query;
52490
+ return /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52491
+ gap: 2,
52492
+ children: [
52493
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52494
+ color: isExact ? ACCENT : "white",
52495
+ bold: isExact,
52496
+ children: c.cmd
52497
+ }, undefined, false, undefined, this),
52498
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52499
+ color: "gray",
52500
+ dimColor: true,
52501
+ children: c.desc
52502
+ }, undefined, false, undefined, this)
52503
+ ]
52504
+ }, i, true, undefined, this);
52505
+ }),
52506
+ isChatLoad && filteredChats.length > 0 && /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52507
+ flexDirection: "column",
52508
+ marginTop: matches2.length ? 1 : 0,
52359
52509
  children: [
52360
- /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52361
- color: isExact ? ACCENT : "white",
52362
- bold: isExact,
52363
- children: c.cmd
52364
- }, undefined, false, undefined, this),
52365
52510
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52366
52511
  color: "gray",
52367
52512
  dimColor: true,
52368
- children: c.desc
52369
- }, undefined, false, undefined, this)
52513
+ children: chatFilter ? `matching "${chatFilter}":` : "recent chats:"
52514
+ }, undefined, false, undefined, this),
52515
+ filteredChats.map((name, i) => /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Box_default, {
52516
+ gap: 1,
52517
+ marginLeft: 2,
52518
+ children: [
52519
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52520
+ color: ACCENT,
52521
+ children: "·"
52522
+ }, undefined, false, undefined, this),
52523
+ /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(Text, {
52524
+ color: "white",
52525
+ children: name
52526
+ }, undefined, false, undefined, this)
52527
+ ]
52528
+ }, i, true, undefined, this))
52370
52529
  ]
52371
- }, i, true, undefined, this);
52372
- })
52373
- }, undefined, false, undefined, this);
52530
+ }, undefined, true, undefined, this)
52531
+ ]
52532
+ }, undefined, true, undefined, this);
52374
52533
  }
52375
52534
  var ChatRunner = ({ repoPath }) => {
52376
- const [stage, setStage] = import_react48.useState({ type: "picking-provider" });
52377
- const [committed, setCommitted] = import_react48.useState([]);
52378
- const [provider, setProvider] = import_react48.useState(null);
52379
- const [systemPrompt, setSystemPrompt] = import_react48.useState("");
52380
- const [inputValue, setInputValue] = import_react48.useState("");
52381
- const [pendingMsgIndex, setPendingMsgIndex] = import_react48.useState(null);
52382
- const [allMessages, setAllMessages] = import_react48.useState([]);
52383
- const [clonedUrls, setClonedUrls] = import_react48.useState(new Set);
52384
- const [showTimeline, setShowTimeline] = import_react48.useState(false);
52385
- const [showReview, setShowReview] = import_react48.useState(false);
52386
- const [autoApprove, setAutoApprove] = import_react48.useState(false);
52387
- const abortControllerRef = import_react48.useRef(null);
52388
- const toolResultCache = import_react48.useRef(new Map);
52389
- const inputBuffer = import_react48.useRef("");
52390
- const flushTimer = import_react48.useRef(null);
52535
+ const [stage, setStage] = import_react49.useState({ type: "picking-provider" });
52536
+ const [committed, setCommitted] = import_react49.useState([]);
52537
+ const [provider, setProvider] = import_react49.useState(null);
52538
+ const [systemPrompt, setSystemPrompt] = import_react49.useState("");
52539
+ const [inputValue, setInputValue] = import_react49.useState("");
52540
+ const [pendingMsgIndex, setPendingMsgIndex] = import_react49.useState(null);
52541
+ const [allMessages, setAllMessages] = import_react49.useState([]);
52542
+ const [clonedUrls, setClonedUrls] = import_react49.useState(new Set);
52543
+ const [showTimeline, setShowTimeline] = import_react49.useState(false);
52544
+ const [showReview, setShowReview] = import_react49.useState(false);
52545
+ const [autoApprove, setAutoApprove] = import_react49.useState(false);
52546
+ const [chatName, setChatName] = import_react49.useState(null);
52547
+ const chatNameRef = import_react49.useRef(null);
52548
+ const [recentChats, setRecentChats] = import_react49.useState([]);
52549
+ const inputHistoryRef = import_react49.useRef([]);
52550
+ const historyIndexRef = import_react49.useRef(-1);
52551
+ const [inputKey, setInputKey] = import_react49.useState(0);
52552
+ const updateChatName = (name) => {
52553
+ chatNameRef.current = name;
52554
+ setChatName(name);
52555
+ };
52556
+ const abortControllerRef = import_react49.useRef(null);
52557
+ const toolResultCache = import_react49.useRef(new Map);
52558
+ const inputBuffer = import_react49.useRef("");
52559
+ const flushTimer = import_react49.useRef(null);
52391
52560
  const thinkingPhrase = useThinkingPhrase(stage.type === "thinking");
52561
+ import_react48.default.useEffect(() => {
52562
+ const chats = listChats(repoPath);
52563
+ setRecentChats(chats.slice(0, 10).map((c) => c.name));
52564
+ }, [repoPath]);
52565
+ import_react48.default.useEffect(() => {
52566
+ if (chatNameRef.current && allMessages.length > 1) {
52567
+ saveChat(chatNameRef.current, repoPath, allMessages);
52568
+ }
52569
+ }, [allMessages]);
52392
52570
  const flushBuffer = () => {
52393
52571
  const buf = inputBuffer.current;
52394
52572
  if (!buf)
@@ -52423,7 +52601,24 @@ var ChatRunner = ({ repoPath }) => {
52423
52601
  setStage({ type: "idle" });
52424
52602
  return;
52425
52603
  }
52426
- const parsed = parseResponse(raw);
52604
+ const memAddMatches = [
52605
+ ...raw.matchAll(/<memory-add>([\s\S]*?)<\/memory-add>/g)
52606
+ ];
52607
+ const memDelMatches = [
52608
+ ...raw.matchAll(/<memory-delete>([\s\S]*?)<\/memory-delete>/g)
52609
+ ];
52610
+ for (const match of memAddMatches) {
52611
+ const content = match[1].trim();
52612
+ if (content)
52613
+ addMemory(content, repoPath);
52614
+ }
52615
+ for (const match of memDelMatches) {
52616
+ const id = match[1].trim();
52617
+ if (id)
52618
+ deleteMemory(id, repoPath);
52619
+ }
52620
+ const cleanRaw = raw.replace(/<memory-add>[\s\S]*?<\/memory-add>/g, "").replace(/<memory-delete>[\s\S]*?<\/memory-delete>/g, "").trim();
52621
+ const parsed = parseResponse(cleanRaw);
52427
52622
  if (parsed.kind === "changes") {
52428
52623
  if (parsed.patches.length === 0) {
52429
52624
  const msg2 = {
@@ -52555,7 +52750,7 @@ var ChatRunner = ({ repoPath }) => {
52555
52750
  "write-file": "file-written",
52556
52751
  search: "url-fetched"
52557
52752
  };
52558
- appendHistory({
52753
+ appendMemory({
52559
52754
  kind: kindMap2[parsed.kind] ?? "shell-run",
52560
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,
52561
52756
  summary: result2.split(`
@@ -52652,7 +52847,7 @@ var ChatRunner = ({ repoPath }) => {
52652
52847
  return;
52653
52848
  }
52654
52849
  if (text.trim().toLowerCase() === "/clear history") {
52655
- clearRepoHistory(repoPath);
52850
+ clearRepoMemory(repoPath);
52656
52851
  const clearedMsg = {
52657
52852
  role: "assistant",
52658
52853
  content: "History cleared for this repo.",
@@ -52662,11 +52857,212 @@ var ChatRunner = ({ repoPath }) => {
52662
52857
  setAllMessages((prev) => [...prev, clearedMsg]);
52663
52858
  return;
52664
52859
  }
52860
+ if (text.trim().toLowerCase() === "/chat") {
52861
+ const msg = {
52862
+ role: "assistant",
52863
+ content: "Chat commands: `/chat list` · `/chat load <n>` · `/chat rename <n>` · `/chat delete <n>`",
52864
+ type: "text"
52865
+ };
52866
+ setCommitted((prev) => [...prev, msg]);
52867
+ setAllMessages((prev) => [...prev, msg]);
52868
+ return;
52869
+ }
52870
+ if (text.trim().toLowerCase().startsWith("/chat rename")) {
52871
+ const parts = text.trim().split(/\s+/);
52872
+ const newName = parts.slice(2).join("-");
52873
+ if (!newName) {
52874
+ const msg2 = {
52875
+ role: "assistant",
52876
+ content: "Usage: `/chat rename <new-name>`",
52877
+ type: "text"
52878
+ };
52879
+ setCommitted((prev) => [...prev, msg2]);
52880
+ setAllMessages((prev) => [...prev, msg2]);
52881
+ return;
52882
+ }
52883
+ const oldName = chatNameRef.current;
52884
+ if (oldName)
52885
+ deleteChat(oldName);
52886
+ updateChatName(newName);
52887
+ saveChat(newName, repoPath, allMessages);
52888
+ setRecentChats((prev) => [newName, ...prev.filter((n) => n !== newName && n !== oldName)].slice(0, 10));
52889
+ const msg = {
52890
+ role: "assistant",
52891
+ content: `Chat renamed to **${newName}**.`,
52892
+ type: "text"
52893
+ };
52894
+ setCommitted((prev) => [...prev, msg]);
52895
+ setAllMessages((prev) => [...prev, msg]);
52896
+ return;
52897
+ }
52898
+ if (text.trim().toLowerCase().startsWith("/chat delete")) {
52899
+ const parts = text.trim().split(/\s+/);
52900
+ const name = parts.slice(2).join("-");
52901
+ if (!name) {
52902
+ const msg2 = {
52903
+ role: "assistant",
52904
+ content: "Usage: `/chat delete <name>`",
52905
+ type: "text"
52906
+ };
52907
+ setCommitted((prev) => [...prev, msg2]);
52908
+ setAllMessages((prev) => [...prev, msg2]);
52909
+ return;
52910
+ }
52911
+ const deleted = deleteChat(name);
52912
+ if (!deleted) {
52913
+ const msg2 = {
52914
+ role: "assistant",
52915
+ content: `Chat **${name}** not found.`,
52916
+ type: "text"
52917
+ };
52918
+ setCommitted((prev) => [...prev, msg2]);
52919
+ setAllMessages((prev) => [...prev, msg2]);
52920
+ return;
52921
+ }
52922
+ if (chatNameRef.current === name) {
52923
+ chatNameRef.current = null;
52924
+ setChatName(null);
52925
+ }
52926
+ setRecentChats((prev) => prev.filter((n) => n !== name));
52927
+ const msg = {
52928
+ role: "assistant",
52929
+ content: `Chat **${name}** deleted.`,
52930
+ type: "text"
52931
+ };
52932
+ setCommitted((prev) => [...prev, msg]);
52933
+ setAllMessages((prev) => [...prev, msg]);
52934
+ return;
52935
+ }
52936
+ if (text.trim().toLowerCase() === "/chat list") {
52937
+ const chats = listChats(repoPath);
52938
+ const content = chats.length === 0 ? "No saved chats for this repo yet." : `Saved chats:
52939
+
52940
+ ${chats.map((c) => `- **${c.name}** · ${c.userMessageCount} messages · ${new Date(c.savedAt).toLocaleString()}`).join(`
52941
+ `)}`;
52942
+ const msg = { role: "assistant", content, type: "text" };
52943
+ setCommitted((prev) => [...prev, msg]);
52944
+ setAllMessages((prev) => [...prev, msg]);
52945
+ return;
52946
+ }
52947
+ if (text.trim().toLowerCase().startsWith("/chat load")) {
52948
+ const parts = text.trim().split(/\s+/);
52949
+ const name = parts.slice(2).join("-");
52950
+ if (!name) {
52951
+ const chats = listChats(repoPath);
52952
+ const content = chats.length === 0 ? "No saved chats found." : `Specify a chat name. Recent chats:
52953
+
52954
+ ${chats.slice(0, 10).map((c) => `- **${c.name}**`).join(`
52955
+ `)}`;
52956
+ const msg = { role: "assistant", content, type: "text" };
52957
+ setCommitted((prev) => [...prev, msg]);
52958
+ setAllMessages((prev) => [...prev, msg]);
52959
+ return;
52960
+ }
52961
+ const saved = loadChat(name);
52962
+ if (!saved) {
52963
+ const msg = {
52964
+ role: "assistant",
52965
+ content: `Chat **${name}** not found. Use \`/chat list\` to see saved chats.`,
52966
+ type: "text"
52967
+ };
52968
+ setCommitted((prev) => [...prev, msg]);
52969
+ setAllMessages((prev) => [...prev, msg]);
52970
+ return;
52971
+ }
52972
+ updateChatName(name);
52973
+ setAllMessages(saved.messages);
52974
+ setCommitted(saved.messages);
52975
+ const notice = {
52976
+ role: "assistant",
52977
+ content: `Loaded chat **${name}** · ${saved.userMessageCount} messages · saved ${new Date(saved.savedAt).toLocaleString()}`,
52978
+ type: "text"
52979
+ };
52980
+ setCommitted((prev) => [...prev, notice]);
52981
+ setAllMessages((prev) => [...prev, notice]);
52982
+ return;
52983
+ }
52984
+ if (text.trim().toLowerCase() === "/memory list" || text.trim().toLowerCase() === "/memory") {
52985
+ const mems = listMemories(repoPath);
52986
+ const content = mems.length === 0 ? "No memories stored for this repo yet." : `Memories for this repo:
52987
+
52988
+ ${mems.map((m) => `- [${m.id}] ${m.content}`).join(`
52989
+ `)}`;
52990
+ const msg = { role: "assistant", content, type: "text" };
52991
+ setCommitted((prev) => [...prev, msg]);
52992
+ setAllMessages((prev) => [...prev, msg]);
52993
+ return;
52994
+ }
52995
+ if (text.trim().toLowerCase().startsWith("/memory add")) {
52996
+ const content = text.trim().slice("/memory add".length).trim();
52997
+ if (!content) {
52998
+ const msg2 = {
52999
+ role: "assistant",
53000
+ content: "Usage: `/memory add <content>`",
53001
+ type: "text"
53002
+ };
53003
+ setCommitted((prev) => [...prev, msg2]);
53004
+ setAllMessages((prev) => [...prev, msg2]);
53005
+ return;
53006
+ }
53007
+ const mem = addMemory(content, repoPath);
53008
+ const msg = {
53009
+ role: "assistant",
53010
+ content: `Memory saved **[${mem.id}]**: ${mem.content}`,
53011
+ type: "text"
53012
+ };
53013
+ setCommitted((prev) => [...prev, msg]);
53014
+ setAllMessages((prev) => [...prev, msg]);
53015
+ return;
53016
+ }
53017
+ if (text.trim().toLowerCase().startsWith("/memory delete")) {
53018
+ const id = text.trim().split(/\s+/)[2];
53019
+ if (!id) {
53020
+ const msg2 = {
53021
+ role: "assistant",
53022
+ content: "Usage: `/memory delete <id>`",
53023
+ type: "text"
53024
+ };
53025
+ setCommitted((prev) => [...prev, msg2]);
53026
+ setAllMessages((prev) => [...prev, msg2]);
53027
+ return;
53028
+ }
53029
+ const deleted = deleteMemory(id, repoPath);
53030
+ const msg = {
53031
+ role: "assistant",
53032
+ content: deleted ? `Memory **[${id}]** deleted.` : `Memory **[${id}]** not found.`,
53033
+ type: "text"
53034
+ };
53035
+ setCommitted((prev) => [...prev, msg]);
53036
+ setAllMessages((prev) => [...prev, msg]);
53037
+ return;
53038
+ }
53039
+ if (text.trim().toLowerCase() === "/memory clear") {
53040
+ clearRepoMemory(repoPath);
53041
+ const msg = {
53042
+ role: "assistant",
53043
+ content: "All memories cleared for this repo.",
53044
+ type: "text"
53045
+ };
53046
+ setCommitted((prev) => [...prev, msg]);
53047
+ setAllMessages((prev) => [...prev, msg]);
53048
+ return;
53049
+ }
52665
53050
  const userMsg = { role: "user", content: text, type: "text" };
52666
53051
  const nextAll = [...allMessages, userMsg];
52667
53052
  setCommitted((prev) => [...prev, userMsg]);
52668
53053
  setAllMessages(nextAll);
52669
53054
  toolResultCache.current.clear();
53055
+ inputHistoryRef.current = [
53056
+ text,
53057
+ ...inputHistoryRef.current.filter((m) => m !== text)
53058
+ ].slice(0, 50);
53059
+ historyIndexRef.current = -1;
53060
+ if (!chatName) {
53061
+ const name = getChatNameSuggestions(nextAll)[0] ?? `chat-${new Date().toISOString().slice(0, 10)}`;
53062
+ updateChatName(name);
53063
+ setRecentChats((prev) => [name, ...prev.filter((n) => n !== name)].slice(0, 10));
53064
+ saveChat(name, repoPath, nextAll);
53065
+ }
52670
53066
  const abort = new AbortController;
52671
53067
  abortControllerRef.current = abort;
52672
53068
  setStage({ type: "thinking" });
@@ -52686,6 +53082,21 @@ var ChatRunner = ({ repoPath }) => {
52686
53082
  process.exit(0);
52687
53083
  return;
52688
53084
  }
53085
+ if (key.upArrow && inputHistoryRef.current.length > 0) {
53086
+ const next = Math.min(historyIndexRef.current + 1, inputHistoryRef.current.length - 1);
53087
+ historyIndexRef.current = next;
53088
+ setInputValue(inputHistoryRef.current[next]);
53089
+ setInputKey((k) => k + 1);
53090
+ return;
53091
+ }
53092
+ if (key.downArrow) {
53093
+ const next = historyIndexRef.current - 1;
53094
+ historyIndexRef.current = next;
53095
+ const val = next < 0 ? "" : inputHistoryRef.current[next];
53096
+ setInputValue(val);
53097
+ setInputKey((k) => k + 1);
53098
+ return;
53099
+ }
52689
53100
  if (key.tab && inputValue.startsWith("/")) {
52690
53101
  const q = inputValue.toLowerCase();
52691
53102
  const match = COMMANDS.find((c) => c.cmd.startsWith(q));
@@ -52704,9 +53115,9 @@ var ChatRunner = ({ repoPath }) => {
52704
53115
  startCloneRepo(cloneUrl).then((result2) => {
52705
53116
  if (result2.done) {
52706
53117
  const repoName = cloneUrl.split("/").pop()?.replace(/\.git$/, "") ?? "repo";
52707
- const destPath = path16.join(os7.tmpdir(), repoName);
53118
+ const destPath = path18.join(os9.tmpdir(), repoName);
52708
53119
  const fileCount = walkDir3(destPath).length;
52709
- appendHistory({
53120
+ appendMemory({
52710
53121
  kind: "url-fetched",
52711
53122
  detail: repoUrl,
52712
53123
  summary: `Cloned ${repoName} — ${fileCount} files`,
@@ -52834,7 +53245,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52834
53245
  const msg = allMessages[pendingMsgIndex];
52835
53246
  if (msg?.type === "plan") {
52836
53247
  setCommitted((prev) => [...prev, { ...msg, applied: false }]);
52837
- appendHistory({
53248
+ appendMemory({
52838
53249
  kind: "code-skipped",
52839
53250
  detail: msg.patches.map((p) => p.path).join(", "),
52840
53251
  summary: `Skipped changes to ${msg.patches.length} file(s)`,
@@ -52849,7 +53260,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52849
53260
  if (key.return || input === "a" || input === "A") {
52850
53261
  try {
52851
53262
  applyPatches3(repoPath, stage.patches);
52852
- appendHistory({
53263
+ appendMemory({
52853
53264
  kind: "code-applied",
52854
53265
  detail: stage.patches.map((p) => p.path).join(", "),
52855
53266
  summary: `Applied changes to ${stage.patches.length} file(s)`,
@@ -52892,7 +53303,7 @@ Ask me anything about it — I can read files, explain how it works, or suggest
52892
53303
  setStage({ type: "loading" });
52893
53304
  fetchFileTree(repoPath).catch(() => walkDir3(repoPath)).then((fileTree) => {
52894
53305
  const importantFiles = readImportantFiles(repoPath, fileTree);
52895
- const historySummary = buildHistorySummary(repoPath);
53306
+ const historySummary = buildMemorySummary(repoPath);
52896
53307
  const lensFile = readLensFile(repoPath);
52897
53308
  const lensContext = lensFile ? `
52898
53309
 
@@ -53029,18 +53440,21 @@ Tip: type /timeline to browse commit history.`,
53029
53440
  children: [
53030
53441
  inputValue.startsWith("/") && /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(CommandPalette, {
53031
53442
  query: inputValue,
53032
- onSelect: (cmd) => {
53033
- setInputValue(cmd);
53034
- }
53443
+ onSelect: (cmd) => setInputValue(cmd),
53444
+ recentChats
53035
53445
  }, undefined, false, undefined, this),
53036
53446
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(InputBox, {
53037
53447
  value: inputValue,
53038
- onChange: setInputValue,
53448
+ onChange: (v) => {
53449
+ historyIndexRef.current = -1;
53450
+ setInputValue(v);
53451
+ },
53039
53452
  onSubmit: (val) => {
53040
53453
  if (val.trim())
53041
53454
  sendMessage(val.trim());
53042
53455
  setInputValue("");
53043
- }
53456
+ },
53457
+ inputKey
53044
53458
  }, undefined, false, undefined, this),
53045
53459
  /* @__PURE__ */ jsx_dev_runtime22.jsxDEV(ShortcutBar, {
53046
53460
  autoApprove
@@ -53054,8 +53468,8 @@ Tip: type /timeline to browse commit history.`,
53054
53468
  // src/commands/chat.tsx
53055
53469
  var jsx_dev_runtime23 = __toESM(require_jsx_dev_runtime(), 1);
53056
53470
  var ChatCommand = ({ path: inputPath }) => {
53057
- const resolvedPath = path17.resolve(inputPath);
53058
- if (!existsSync12(resolvedPath)) {
53471
+ const resolvedPath = path19.resolve(inputPath);
53472
+ if (!existsSync14(resolvedPath)) {
53059
53473
  return /* @__PURE__ */ jsx_dev_runtime23.jsxDEV(Box_default, {
53060
53474
  marginTop: 1,
53061
53475
  children: /* @__PURE__ */ jsx_dev_runtime23.jsxDEV(Text, {
@@ -53074,12 +53488,12 @@ var ChatCommand = ({ path: inputPath }) => {
53074
53488
  };
53075
53489
 
53076
53490
  // src/commands/timeline.tsx
53077
- import { existsSync as existsSync13 } from "fs";
53078
- import path18 from "path";
53491
+ import { existsSync as existsSync15 } from "fs";
53492
+ import path20 from "path";
53079
53493
  var jsx_dev_runtime24 = __toESM(require_jsx_dev_runtime(), 1);
53080
53494
  var TimelineCommand = ({ path: inputPath }) => {
53081
- const resolvedPath = path18.resolve(inputPath);
53082
- if (!existsSync13(resolvedPath)) {
53495
+ const resolvedPath = path20.resolve(inputPath);
53496
+ if (!existsSync15(resolvedPath)) {
53083
53497
  return /* @__PURE__ */ jsx_dev_runtime24.jsxDEV(Box_default, {
53084
53498
  marginTop: 1,
53085
53499
  children: /* @__PURE__ */ jsx_dev_runtime24.jsxDEV(Text, {