summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2025-05-30 16:39:45 -0400
committerDax Raad <[email protected]>2025-05-30 16:43:42 -0400
commit91a9e455e233e454df1cfc552f2ddbf7d9fbecb8 (patch)
tree160f3be01f5153b4c6ce5d2114aa814ec5b16192
parentc391c6d3f3ee4a041ab30f576aae0abfca8f7c9a (diff)
downloadopencode-91a9e455e233e454df1cfc552f2ddbf7d9fbecb8.tar.gz
opencode-91a9e455e233e454df1cfc552f2ddbf7d9fbecb8.zip
sync
-rw-r--r--js/src/tool/bash.ts2
-rw-r--r--js/src/tool/edit.ts2
-rw-r--r--js/src/tool/fetch.ts2
-rw-r--r--js/src/tool/glob.ts2
-rw-r--r--js/src/tool/grep.ts2
-rw-r--r--js/src/tool/ls.ts322
-rw-r--r--js/src/tool/lsp-diagnostics.ts2
-rw-r--r--js/src/tool/lsp-hover.ts2
-rw-r--r--js/src/tool/patch.ts2
-rw-r--r--js/src/tool/view.ts2
-rw-r--r--js/src/util/log.ts3
11 files changed, 75 insertions, 268 deletions
diff --git a/js/src/tool/bash.ts b/js/src/tool/bash.ts
index f6a9fa6e2..5ad92e3c8 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 0fa10475b..9ba99a3be 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 741e1897f..573e0eec7 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 f52c3f5d0..da47ee4ca 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 0c5848074..8b2375f63 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 5954355a3..46efb307a 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;
-
- 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,
+ };
+ },
+});
diff --git a/js/src/tool/lsp-diagnostics.ts b/js/src/tool/lsp-diagnostics.ts
index ecd54c1a7..736efc034 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 4ffa90a75..c7a132645 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 137877a7b..9f9192fda 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 3ba36470d..ee11881d3 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 abaddfa37..34707b136 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<string, any>) {