From 91a9e455e233e454df1cfc552f2ddbf7d9fbecb8 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 May 2025 16:39:45 -0400 Subject: [PATCH] sync --- js/src/tool/bash.ts | 2 +- js/src/tool/edit.ts | 2 +- js/src/tool/fetch.ts | 2 +- js/src/tool/glob.ts | 2 +- js/src/tool/grep.ts | 2 +- js/src/tool/ls.ts | 338 +++++++-------------------------- js/src/tool/lsp-diagnostics.ts | 2 +- js/src/tool/lsp-hover.ts | 2 +- js/src/tool/patch.ts | 2 +- js/src/tool/view.ts | 2 +- js/src/util/log.ts | 3 +- 11 files changed, 83 insertions(+), 276 deletions(-) diff --git a/js/src/tool/bash.ts b/js/src/tool/bash.ts index f6a9fa6e..5ad92e3c 100644 --- a/js/src/tool/bash.ts +++ b/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(), diff --git a/js/src/tool/edit.ts b/js/src/tool/edit.ts index 0fa10475..9ba99a3b 100644 --- a/js/src/tool/edit.ts +++ b/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"), diff --git a/js/src/tool/fetch.ts b/js/src/tool/fetch.ts index 741e1897..573e0eec 100644 --- a/js/src/tool/fetch.ts +++ b/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"), diff --git a/js/src/tool/glob.ts b/js/src/tool/glob.ts index f52c3f5d..da47ee4c 100644 --- a/js/src/tool/glob.ts +++ b/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"), diff --git a/js/src/tool/grep.ts b/js/src/tool/grep.ts index 0c584807..8b2375f6 100644 --- a/js/src/tool/grep.ts +++ b/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 diff --git a/js/src/tool/ls.ts b/js/src/tool/ls.ts index 5954355a..46efb307 100644 --- a/js/src/tool/ls.ts +++ b/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; + const searchPath = path.resolve(app.root, params.path || "."); - if (!path.isAbsolute(searchPath)) { - searchPath = path.join(app.root, searchPath); + const glob = new Bun.Glob("**/*"); + const files = []; + + for await (const file of glob.scan({ cwd: searchPath })) { + if (file.startsWith(".") || IGNORE_PATTERNS.some((p) => file.includes(p))) + continue; + if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file))) + continue; + files.push(file); + if (files.length >= 1000) break; } - const stat = await fs.promises.stat(searchPath).catch(() => null); - if (!stat) { - return { - metadata: {}, - output: `Path does not exist: ${searchPath}`, - }; + // Build directory structure + const dirs = new Set(); + const filesByDir = new Map(); + + for (const file of files) { + const dir = path.dirname(file); + const parts = dir === "." ? [] : dir.split("/"); + + // Add all parent directories + for (let i = 0; i <= parts.length; i++) { + const dirPath = i === 0 ? "." : parts.slice(0, i).join("/"); + dirs.add(dirPath); + } + + // Add file to its directory + if (!filesByDir.has(dir)) filesByDir.set(dir, []); + filesByDir.get(dir)!.push(path.basename(file)); } - const { files, truncated } = await listDirectory( - searchPath, - params.ignore || [], - MAX_LS_FILES, - ); - const tree = createFileTree(files); - let output = printTree(tree, searchPath); + function renderDir(dirPath: string, depth: number): string { + const indent = " ".repeat(depth); + let output = ""; - 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}`; + if (depth > 0) { + output += `${indent}${path.basename(dirPath)}/\n`; + } + + const childIndent = " ".repeat(depth + 1); + const children = Array.from(dirs) + .filter((d) => path.dirname(d) === dirPath && d !== dirPath) + .sort(); + + // Render subdirectories first + for (const child of children) { + output += renderDir(child, depth + 1); + } + + // Render files + const files = filesByDir.get(dirPath) || []; + for (const file of files.sort()) { + output += `${childIndent}${file}\n`; + } + + return output; } + const output = `${searchPath}/\n` + renderDir(".", 0); + return { - metadata: { - count: files.length, - truncated, - }, + metadata: { count: files.length, truncated: files.length >= 1000 }, 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 { - if (results.length >= limit) { - truncated = true; - return; - } - - const entries = await fs.promises - .readdir(dir, { withFileTypes: true }) - .catch(() => []); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (shouldSkip(fullPath, ignorePatterns)) { - 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; - } - } - } - } - - await walk(initialPath); - return { files: results, truncated }; -} - -function shouldSkip(filePath: string, ignorePatterns: string[]): boolean { - const base = path.basename(filePath); - - 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; - } - } else { - if (base === ignored) { - return true; - } - } - } - - for (const pattern of ignorePatterns) { - const glob = new Bun.Glob(pattern); - if (glob.match(base)) { - return true; - } - } - - return false; -} - -function createFileTree(sortedPaths: string[]): TreeNode[] { - const root: TreeNode[] = []; - const pathMap: Record = {}; - - for (const filePath of sortedPaths) { - const parts = filePath.split(path.sep).filter((part) => part !== ""); - let currentPath = ""; - let parentPath = ""; - - 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 (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: [], - }; - - pathMap[currentPath] = newNode; - - if (i > 0 && parentPath !== "") { - if (pathMap[parentPath]) { - pathMap[parentPath].children?.push(newNode); - } - } else { - root.push(newNode); - } - - parentPath = currentPath; - } - } - - 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`; - - if (node.type === "directory" && node.children && node.children.length > 0) { - for (const child of node.children) { - result = printNode(child, level + 1, result); - } - } - - return result; -} diff --git a/js/src/tool/lsp-diagnostics.ts b/js/src/tool/lsp-diagnostics.ts index ecd54c1a..736efc03 100644 --- a/js/src/tool/lsp-diagnostics.ts +++ b/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: diff --git a/js/src/tool/lsp-hover.ts b/js/src/tool/lsp-hover.ts index 4ffa90a7..c7a13264 100644 --- a/js/src/tool/lsp-hover.ts +++ b/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. diff --git a/js/src/tool/patch.ts b/js/src/tool/patch.ts index 137877a7..9f9192fd 100644 --- a/js/src/tool/patch.ts +++ b/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) => { diff --git a/js/src/tool/view.ts b/js/src/tool/view.ts index 3ba36470..ee11881d 100644 --- a/js/src/tool/view.ts +++ b/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"), diff --git a/js/src/util/log.ts b/js/src/util/log.ts index abaddfa3..34707b13 100644 --- a/js/src/util/log.ts +++ b/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) {