This commit is contained in:
Dax Raad
2025-05-30 16:39:45 -04:00
parent c391c6d3f3
commit 91a9e455e2
11 changed files with 83 additions and 276 deletions

View File

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

View File

@@ -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.`; 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({ export const edit = Tool.define({
name: "edit", name: "opencode.edit",
description: DESCRIPTION, description: DESCRIPTION,
parameters: z.object({ parameters: z.object({
filePath: z.string().describe("The absolute path to the file to modify"), filePath: z.string().describe("The absolute path to the file to modify"),

View File

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

View File

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

View File

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

View File

@@ -2,289 +2,95 @@ import { z } from "zod";
import { Tool } from "./tool"; import { Tool } from "./tool";
import { App } from "../app/app"; import { App } from "../app/app";
import * as path from "path"; 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. const IGNORE_PATTERNS = [
"node_modules/",
WHEN TO USE THIS TOOL: "__pycache__/",
- Use when you need to explore the structure of a directory ".git/",
- Helpful for understanding the organization of a project "dist/",
- Good first step when getting familiar with a new codebase "build/",
"target/",
HOW TO USE: "vendor/",
- Provide a path to list (defaults to current working directory) "bin/",
- Optionally specify glob patterns to ignore "obj/",
- Results are displayed in a tree structure ".idea/",
".vscode/",
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[];
}
export const ls = Tool.define({ export const ls = Tool.define({
name: "ls", name: "opencode.ls",
description: DESCRIPTION, description: "List directory contents",
parameters: z.object({ parameters: z.object({
path: z path: z.string().optional(),
.string() ignore: z.array(z.string()).optional(),
.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(),
}), }),
async execute(params) { async execute(params) {
const app = await App.use(); const app = await App.use();
let searchPath = params.path || app.root; const searchPath = path.resolve(app.root, params.path || ".");
if (!path.isAbsolute(searchPath)) { const glob = new Bun.Glob("**/*");
searchPath = path.join(app.root, searchPath); 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); // Build directory structure
if (!stat) { const dirs = new Set<string>();
return { const filesByDir = new Map<string, string[]>();
metadata: {},
output: `Path does not exist: ${searchPath}`, 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( function renderDir(dirPath: string, depth: number): string {
searchPath, const indent = " ".repeat(depth);
params.ignore || [], let output = "";
MAX_LS_FILES,
);
const tree = createFileTree(files);
let output = printTree(tree, searchPath);
if (truncated) { if (depth > 0) {
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}`; 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 { return {
metadata: { metadata: { count: files.length, truncated: files.length >= 1000 },
count: files.length,
truncated,
},
output, 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 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<string, TreeNode> = {};
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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