Dax Raad 9 months ago
parent
commit
91a9e455e2

+ 1 - 1
js/src/tool/bash.ts

@@ -171,7 +171,7 @@ Important:
 - Never update git config`;
 
 export const bash = Tool.define({
-  name: "bash",
+  name: "opencode.bash",
   description: DESCRIPTION,
   parameters: z.object({
     command: z.string(),

+ 1 - 1
js/src/tool/edit.ts

@@ -53,7 +53,7 @@ When making edits:
 Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`;
 
 export const edit = Tool.define({
-  name: "edit",
+  name: "opencode.edit",
   description: DESCRIPTION,
   parameters: z.object({
     filePath: z.string().describe("The absolute path to the file to modify"),

+ 1 - 1
js/src/tool/fetch.ts

@@ -38,7 +38,7 @@ TIPS:
 - Set appropriate timeouts for potentially slow websites`;
 
 export const Fetch = Tool.define({
-  name: "fetch",
+  name: "opencode.fetch",
   description: DESCRIPTION,
   parameters: z.object({
     url: z.string().describe("The URL to fetch content from"),

+ 1 - 1
js/src/tool/glob.ts

@@ -38,7 +38,7 @@ TIPS:
 - Always check if results are truncated and refine your search pattern if needed`;
 
 export const glob = Tool.define({
-  name: "glob",
+  name: "opencode.glob",
   description: DESCRIPTION,
   parameters: z.object({
     pattern: z.string().describe("The glob pattern to match files against"),

+ 1 - 1
js/src/tool/grep.ts

@@ -256,7 +256,7 @@ async function searchFiles(
 }
 
 export const grep = Tool.define({
-  name: "grep",
+  name: "opencode.grep",
   description: DESCRIPTION,
   parameters: z.object({
     pattern: z

+ 64 - 258
js/src/tool/ls.ts

@@ -2,289 +2,95 @@ import { z } from "zod";
 import { Tool } from "./tool";
 import { App } from "../app/app";
 import * as path from "path";
-import * as fs from "fs";
 
-const DESCRIPTION = `Directory listing tool that shows files and subdirectories in a tree structure, helping you explore and understand the project organization.
-
-WHEN TO USE THIS TOOL:
-- Use when you need to explore the structure of a directory
-- Helpful for understanding the organization of a project
-- Good first step when getting familiar with a new codebase
-
-HOW TO USE:
-- Provide a path to list (defaults to current working directory)
-- Optionally specify glob patterns to ignore
-- Results are displayed in a tree structure
-
-FEATURES:
-- Displays a hierarchical view of files and directories
-- Automatically skips hidden files/directories (starting with '.')
-- Skips common system directories like __pycache__
-- Can filter out files matching specific patterns
-
-LIMITATIONS:
-- Results are limited to 1000 files
-- Very large directories will be truncated
-- Does not show file sizes or permissions
-- Cannot recursively list all directories in a large project
-
-TIPS:
-- Use Glob tool for finding files by name patterns instead of browsing
-- Use Grep tool for searching file contents
-- Combine with other tools for more effective exploration`;
-
-const MAX_LS_FILES = 1000;
-
-interface TreeNode {
-  name: string;
-  path: string;
-  type: "file" | "directory";
-  children?: TreeNode[];
-}
+const IGNORE_PATTERNS = [
+  "node_modules/",
+  "__pycache__/",
+  ".git/",
+  "dist/",
+  "build/",
+  "target/",
+  "vendor/",
+  "bin/",
+  "obj/",
+  ".idea/",
+  ".vscode/",
+];
 
 export const ls = Tool.define({
-  name: "ls",
-  description: DESCRIPTION,
+  name: "opencode.ls",
+  description: "List directory contents",
   parameters: z.object({
-    path: z
-      .string()
-      .describe(
-        "The path to the directory to list (defaults to current working directory)",
-      )
-      .optional(),
-    ignore: z
-      .array(z.string())
-      .describe("List of glob patterns to ignore")
-      .optional(),
+    path: z.string().optional(),
+    ignore: z.array(z.string()).optional(),
   }),
   async execute(params) {
     const app = await App.use();
-    let searchPath = params.path || app.root;
-
-    if (!path.isAbsolute(searchPath)) {
-      searchPath = path.join(app.root, searchPath);
-    }
-
-    const stat = await fs.promises.stat(searchPath).catch(() => null);
-    if (!stat) {
-      return {
-        metadata: {},
-        output: `Path does not exist: ${searchPath}`,
-      };
-    }
-
-    const { files, truncated } = await listDirectory(
-      searchPath,
-      params.ignore || [],
-      MAX_LS_FILES,
-    );
-    const tree = createFileTree(files);
-    let output = printTree(tree, searchPath);
-
-    if (truncated) {
-      output = `There are more than ${MAX_LS_FILES} files in the directory. Use a more specific path or use the Glob tool to find specific files. The first ${MAX_LS_FILES} files and directories are included below:\n\n${output}`;
-    }
-
-    return {
-      metadata: {
-        count: files.length,
-        truncated,
-      },
-      output,
-    };
-  },
-});
-
-async function listDirectory(
-  initialPath: string,
-  ignorePatterns: string[],
-  limit: number,
-): Promise<{ files: string[]; truncated: boolean }> {
-  const results: string[] = [];
-  let truncated = false;
-
-  async function walk(dir: string): Promise<void> {
-    if (results.length >= limit) {
-      truncated = true;
-      return;
-    }
+    const searchPath = path.resolve(app.root, params.path || ".");
 
-    const entries = await fs.promises
-      .readdir(dir, { withFileTypes: true })
-      .catch(() => []);
+    const glob = new Bun.Glob("**/*");
+    const files = [];
 
-    for (const entry of entries) {
-      const fullPath = path.join(dir, entry.name);
-
-      if (shouldSkip(fullPath, ignorePatterns)) {
+    for await (const file of glob.scan({ cwd: searchPath })) {
+      if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p)))
         continue;
-      }
-
-      if (entry.isDirectory()) {
-        if (fullPath !== initialPath) {
-          results.push(fullPath + path.sep);
-        }
-
-        if (results.length >= limit) {
-          truncated = true;
-          return;
-        }
-        await walk(fullPath);
-      } else if (entry.isFile()) {
-        if (fullPath !== initialPath) {
-          results.push(fullPath);
-        }
-
-        if (results.length >= limit) {
-          truncated = true;
-          return;
-        }
-      }
+      if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
+        continue;
+      files.push(file);
+      if (files.length >= 1000) break;
     }
-  }
 
-  await walk(initialPath);
-  return { files: results, truncated };
-}
+    // Build directory structure
+    const dirs = new Set<string>();
+    const filesByDir = new Map<string, string[]>();
 
-function shouldSkip(filePath: string, ignorePatterns: string[]): boolean {
-  const base = path.basename(filePath);
+    for (const file of files) {
+      const dir = path.dirname(file);
+      const parts = dir === "." ? [] : dir.split("/");
 
-  if (base !== "." && base.startsWith(".")) {
-    return true;
-  }
-
-  const commonIgnored = [
-    "__pycache__",
-    "node_modules",
-    "dist",
-    "build",
-    "target",
-    "vendor",
-    "bin",
-    "obj",
-    ".git",
-    ".idea",
-    ".vscode",
-    ".DS_Store",
-    "*.pyc",
-    "*.pyo",
-    "*.pyd",
-    "*.so",
-    "*.dll",
-    "*.exe",
-  ];
-
-  if (filePath.includes(path.join("__pycache__", ""))) {
-    return true;
-  }
-
-  for (const ignored of commonIgnored) {
-    if (ignored.endsWith("/")) {
-      if (filePath.includes(path.join(ignored.slice(0, -1), ""))) {
-        return true;
-      }
-    } else if (ignored.startsWith("*.")) {
-      if (base.endsWith(ignored.slice(1))) {
-        return true;
+      // Add all parent directories
+      for (let i = 0; i <= parts.length; i++) {
+        const dirPath = i === 0 ? "." : parts.slice(0, i).join("/");
+        dirs.add(dirPath);
       }
-    } else {
-      if (base === ignored) {
-        return true;
-      }
-    }
-  }
 
-  for (const pattern of ignorePatterns) {
-    const glob = new Bun.Glob(pattern);
-    if (glob.match(base)) {
-      return true;
+      // Add file to its directory
+      if (!filesByDir.has(dir)) filesByDir.set(dir, []);
+      filesByDir.get(dir)!.push(path.basename(file));
     }
-  }
-
-  return false;
-}
-
-function createFileTree(sortedPaths: string[]): TreeNode[] {
-  const root: TreeNode[] = [];
-  const pathMap: Record<string, TreeNode> = {};
 
-  for (const filePath of sortedPaths) {
-    const parts = filePath.split(path.sep).filter((part) => part !== "");
-    let currentPath = "";
-    let parentPath = "";
+    function renderDir(dirPath: string, depth: number): string {
+      const indent = "  ".repeat(depth);
+      let output = "";
 
-    if (parts.length === 0) {
-      continue;
-    }
-
-    for (let i = 0; i < parts.length; i++) {
-      const part = parts[i];
-
-      if (currentPath === "") {
-        currentPath = part;
-      } else {
-        currentPath = path.join(currentPath, part);
+      if (depth > 0) {
+        output += `${indent}${path.basename(dirPath)}/\n`;
       }
 
-      if (pathMap[currentPath]) {
-        parentPath = currentPath;
-        continue;
-      }
-
-      const isLastPart = i === parts.length - 1;
-      const isDir = !isLastPart || filePath.endsWith(path.sep);
-      const nodeType = isDir ? "directory" : "file";
-
-      const newNode: TreeNode = {
-        name: part,
-        path: currentPath,
-        type: nodeType,
-        children: [],
-      };
+      const childIndent = "  ".repeat(depth + 1);
+      const children = Array.from(dirs)
+        .filter((d) => path.dirname(d) === dirPath && d !== dirPath)
+        .sort();
 
-      pathMap[currentPath] = newNode;
+      // Render subdirectories first
+      for (const child of children) {
+        output += renderDir(child, depth + 1);
+      }
 
-      if (i > 0 && parentPath !== "") {
-        if (pathMap[parentPath]) {
-          pathMap[parentPath].children?.push(newNode);
-        }
-      } else {
-        root.push(newNode);
+      // Render files
+      const files = filesByDir.get(dirPath) || [];
+      for (const file of files.sort()) {
+        output += `${childIndent}${file}\n`;
       }
 
-      parentPath = currentPath;
+      return output;
     }
-  }
-
-  return root;
-}
-
-function printTree(tree: TreeNode[], rootPath: string): string {
-  let result = `- ${rootPath}${path.sep}\n`;
-
-  for (const node of tree) {
-    result = printNode(node, 1, result);
-  }
-
-  return result;
-}
-
-function printNode(node: TreeNode, level: number, result: string): string {
-  const indent = "  ".repeat(level);
-
-  let nodeName = node.name;
-  if (node.type === "directory") {
-    nodeName += path.sep;
-  }
 
-  result += `${indent}- ${nodeName}\n`;
+    const output = `${searchPath}/\n` + renderDir(".", 0);
 
-  if (node.type === "directory" && node.children && node.children.length > 0) {
-    for (const child of node.children) {
-      result = printNode(child, level + 1, result);
-    }
-  }
-
-  return result;
-}
+    return {
+      metadata: { count: files.length, truncated: files.length >= 1000 },
+      output,
+    };
+  },
+});

+ 1 - 1
js/src/tool/lsp-diagnostics.ts

@@ -5,7 +5,7 @@ import { LSP } from "../lsp";
 import { App } from "../app/app";
 
 export const LspDiagnosticTool = Tool.define({
-  name: "diagnostics",
+  name: "opencode.lsp_diagnostic",
   description: `Get diagnostics for a file and/or project.
 
 WHEN TO USE THIS TOOL:

+ 1 - 1
js/src/tool/lsp-hover.ts

@@ -5,7 +5,7 @@ import { LSP } from "../lsp";
 import { App } from "../app/app";
 
 export const LspHoverTool = Tool.define({
-  name: "lsp.hover",
+  name: "opencode.lsp_hover",
   description: `
   Looks up hover information for a given position in a source file using the Language Server Protocol (LSP). 
   This includes type information, documentation, or symbol details at the specified line and character. 

+ 1 - 1
js/src/tool/patch.ts

@@ -266,7 +266,7 @@ async function applyCommit(
 }
 
 export const patch = Tool.define({
-  name: "patch",
+  name: "opencode.patch",
   description: DESCRIPTION,
   parameters: PatchParams,
   execute: async (params) => {

+ 1 - 1
js/src/tool/view.ts

@@ -41,7 +41,7 @@ TIPS:
 - When viewing large files, use the offset parameter to read specific sections`;
 
 export const view = Tool.define({
-  name: "view",
+  name: "opencode.view",
   description: DESCRIPTION,
   parameters: z.object({
     filePath: z.string().describe("The path to the file to read"),

+ 2 - 1
js/src/util/log.ts

@@ -38,9 +38,10 @@ export namespace Log {
         ...tags,
         ...extra,
       })
+        .filter(([_, value]) => value !== undefined && value !== null)
         .map(([key, value]) => `${key}=${value}`)
         .join(" ");
-      return [new Date().toISOString(), prefix, message].join(" ") + "\n";
+      return [new Date().toISOString(), prefix, message].filter(Boolean).join(" ") + "\n";
     }
     const result = {
       info(message?: any, extra?: Record<string, any>) {