mirror of
https://github.com/aljazceru/opencode.git
synced 2025-12-24 19:24:22 +01:00
add tool tests
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { Tool, tool } from "./tool";
|
||||
import { Tool } from "./tool";
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000;
|
||||
const BANNED_COMMANDS = [
|
||||
@@ -170,7 +170,7 @@ Important:
|
||||
- Return an empty response - the user will see the gh output directly
|
||||
- Never update git config`;
|
||||
|
||||
export const BashTool = Tool.define({
|
||||
export const bash = Tool.define({
|
||||
name: "bash",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
@@ -182,7 +182,7 @@ export const BashTool = Tool.define({
|
||||
.describe("Optional timeout in milliseconds")
|
||||
.optional(),
|
||||
}),
|
||||
async execute(params, opts) {
|
||||
async execute(params) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
||||
if (BANNED_COMMANDS.some((item) => params.command.startsWith(item)))
|
||||
throw new Error(`Command '${params.command}' is not allowed`);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Log } from "../util/log";
|
||||
import { Tool } from "./tool";
|
||||
@@ -8,67 +7,6 @@ import { LSP } from "../lsp";
|
||||
|
||||
const log = Log.create({ service: "tool.edit" });
|
||||
|
||||
// Simple diff generation
|
||||
function generateDiff(
|
||||
oldContent: string,
|
||||
newContent: string,
|
||||
filePath: string,
|
||||
): {
|
||||
diff: string;
|
||||
additions: number;
|
||||
removals: number;
|
||||
} {
|
||||
const oldLines = oldContent.split("\n");
|
||||
const newLines = newContent.split("\n");
|
||||
|
||||
let diff = `--- ${filePath}\n+++ ${filePath}\n`;
|
||||
let additions = 0;
|
||||
let removals = 0;
|
||||
|
||||
// Very simple diff implementation - in a real implementation, you'd use a proper diff algorithm
|
||||
if (oldContent === "") {
|
||||
// New file
|
||||
diff += "@@ -0,0 +1," + newLines.length + " @@\n";
|
||||
for (const line of newLines) {
|
||||
diff += "+" + line + "\n";
|
||||
additions++;
|
||||
}
|
||||
} else if (newContent === "") {
|
||||
// Deleted content
|
||||
diff += "@@ -1," + oldLines.length + " +0,0 @@\n";
|
||||
for (const line of oldLines) {
|
||||
diff += "-" + line + "\n";
|
||||
removals++;
|
||||
}
|
||||
} else {
|
||||
// Modified content
|
||||
diff += "@@ -1," + oldLines.length + " +1," + newLines.length + " @@\n";
|
||||
|
||||
// This is a very simplified diff - a real implementation would use a proper diff algorithm
|
||||
const maxLines = Math.max(oldLines.length, newLines.length);
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
if (i < oldLines.length && i < newLines.length) {
|
||||
if (oldLines[i] !== newLines[i]) {
|
||||
diff += "-" + oldLines[i] + "\n";
|
||||
diff += "+" + newLines[i] + "\n";
|
||||
removals++;
|
||||
additions++;
|
||||
} else {
|
||||
diff += " " + oldLines[i] + "\n";
|
||||
}
|
||||
} else if (i < oldLines.length) {
|
||||
diff += "-" + oldLines[i] + "\n";
|
||||
removals++;
|
||||
} else if (i < newLines.length) {
|
||||
diff += "+" + newLines[i] + "\n";
|
||||
additions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { diff, additions, removals };
|
||||
}
|
||||
|
||||
const DESCRIPTION = `Edits files by replacing text, creating new files, or deleting content. For moving or renaming files, use the Bash tool with the 'mv' command instead. For larger file edits, use the FileWrite tool to overwrite files.
|
||||
|
||||
Before using this tool:
|
||||
@@ -117,7 +55,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 EditTool = Tool.define({
|
||||
export const edit = Tool.define({
|
||||
name: "edit",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
@@ -137,7 +75,7 @@ export const EditTool = Tool.define({
|
||||
|
||||
await (async () => {
|
||||
if (params.old_string === "") {
|
||||
await createNewFile(filePath, params.new_string);
|
||||
await Bun.write(filePath, params.new_string);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,7 +111,6 @@ export const EditTool = Tool.define({
|
||||
params.new_string +
|
||||
content.substring(index + params.old_string.length);
|
||||
|
||||
console.log(newContent);
|
||||
await file.write(newContent);
|
||||
})();
|
||||
|
||||
@@ -193,45 +130,9 @@ export const EditTool = Tool.define({
|
||||
output += `\n<project_diagnostics>\n${JSON.stringify(params)}\n</project_diagnostics>\n`;
|
||||
}
|
||||
}
|
||||
console.log(output);
|
||||
|
||||
return {
|
||||
output,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
async function createNewFile(
|
||||
filePath: string,
|
||||
content: string,
|
||||
): Promise<string> {
|
||||
try {
|
||||
try {
|
||||
const fileStats = fs.statSync(filePath);
|
||||
if (fileStats.isDirectory()) {
|
||||
throw new Error(`Path is a directory, not a file: ${filePath}`);
|
||||
}
|
||||
throw new Error(`File already exists: ${filePath}`);
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const dir = path.dirname(filePath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const { diff, additions, removals } = generateDiff("", content, filePath);
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
|
||||
FileTimes.write(filePath);
|
||||
FileTimes.read(filePath);
|
||||
|
||||
return `File created: ${filePath}`;
|
||||
} catch (err: any) {
|
||||
throw new Error(`Failed to create file: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getFile(filePath: string) {}
|
||||
|
||||
96
js/src/tool/glob.ts
Normal file
96
js/src/tool/glob.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { App } from "../app";
|
||||
|
||||
const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
|
||||
|
||||
WHEN TO USE THIS TOOL:
|
||||
- Use when you need to find files by name patterns or extensions
|
||||
- Great for finding specific file types across a directory structure
|
||||
- Useful for discovering files that match certain naming conventions
|
||||
|
||||
HOW TO USE:
|
||||
- Provide a glob pattern to match against file paths
|
||||
- Optionally specify a starting directory (defaults to current working directory)
|
||||
- Results are sorted with most recently modified files first
|
||||
|
||||
GLOB PATTERN SYNTAX:
|
||||
- '*' matches any sequence of non-separator characters
|
||||
- '**' matches any sequence of characters, including separators
|
||||
- '?' matches any single non-separator character
|
||||
- '[...]' matches any character in the brackets
|
||||
- '[!...]' matches any character not in the brackets
|
||||
|
||||
COMMON PATTERN EXAMPLES:
|
||||
- '*.js' - Find all JavaScript files in the current directory
|
||||
- '**/*.js' - Find all JavaScript files in any subdirectory
|
||||
- 'src/**/*.{ts,tsx}' - Find all TypeScript files in the src directory
|
||||
- '*.{html,css,js}' - Find all HTML, CSS, and JS files
|
||||
|
||||
LIMITATIONS:
|
||||
- Results are limited to 100 files (newest first)
|
||||
- Does not search file contents (use Grep tool for that)
|
||||
- Hidden files (starting with '.') are skipped
|
||||
|
||||
TIPS:
|
||||
- For the most useful results, combine with the Grep tool: first find files with Glob, then search their contents with Grep
|
||||
- When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
|
||||
- Always check if results are truncated and refine your search pattern if needed`;
|
||||
|
||||
export const glob = Tool.define({
|
||||
name: "glob",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
pattern: z.string().describe("The glob pattern to match files against"),
|
||||
path: z
|
||||
.string()
|
||||
.describe(
|
||||
"The directory to search in. Defaults to the current working directory.",
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const app = await App.use();
|
||||
const search = params.path || app.root;
|
||||
const limit = 100;
|
||||
const glob = new Bun.Glob(params.pattern);
|
||||
const files = [];
|
||||
let truncated = false;
|
||||
for await (const file of glob.scan({ cwd: search })) {
|
||||
if (files.length >= limit) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
const stats = await Bun.file(file)
|
||||
.stat()
|
||||
.then((x) => x.mtime.getTime())
|
||||
.catch(() => 0);
|
||||
files.push({
|
||||
path: file,
|
||||
mtime: stats,
|
||||
});
|
||||
}
|
||||
files.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
const output = [];
|
||||
if (files.length === 0) output.push("No files found");
|
||||
if (files.length > 0) {
|
||||
output.push(...files.map((f) => f.path));
|
||||
if (truncated) {
|
||||
output.push("");
|
||||
output.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
count: files.length,
|
||||
truncated,
|
||||
},
|
||||
output: output.join("\n"),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
4
js/src/tool/index.ts
Normal file
4
js/src/tool/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./bash";
|
||||
export * from "./edit";
|
||||
export * from "./view";
|
||||
export * from "./glob";
|
||||
@@ -4,49 +4,53 @@ import { Log } from "../util/log";
|
||||
const log = Log.create({ service: "tool" });
|
||||
|
||||
export namespace Tool {
|
||||
export interface Metadata {
|
||||
properties: Record<string, any>;
|
||||
export interface Metadata<
|
||||
Properties extends Record<string, any> = Record<string, any>,
|
||||
> {
|
||||
properties: Properties;
|
||||
time: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
}
|
||||
export function define<Params, Output>(
|
||||
input: AITool<Params, { metadata?: any; output: Output }> & {
|
||||
name: string;
|
||||
export function define<
|
||||
Params,
|
||||
Output extends { metadata?: any; output: any },
|
||||
Name extends string,
|
||||
>(
|
||||
input: AITool<Params, Output> & {
|
||||
name: Name;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
[input.name]: tool({
|
||||
...input,
|
||||
execute: async (params, opts) => {
|
||||
log.info("invoking", {
|
||||
id: opts.toolCallId,
|
||||
name: input.name,
|
||||
...params,
|
||||
return tool({
|
||||
...input,
|
||||
execute: async (params, opts) => {
|
||||
log.info("invoking", {
|
||||
id: opts.toolCallId,
|
||||
name: input.name,
|
||||
...params,
|
||||
});
|
||||
try {
|
||||
const start = Date.now();
|
||||
const result = await input.execute!(params, opts);
|
||||
const metadata: Metadata<Output["metadata"]> = {
|
||||
...result.metadata,
|
||||
time: {
|
||||
start,
|
||||
end: Date.now(),
|
||||
},
|
||||
};
|
||||
return {
|
||||
metadata,
|
||||
output: result.output,
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("error", {
|
||||
msg: e.toString(),
|
||||
});
|
||||
try {
|
||||
const start = Date.now();
|
||||
const result = await input.execute!(params, opts);
|
||||
const metadata: Metadata = {
|
||||
properties: result.metadata,
|
||||
time: {
|
||||
start,
|
||||
end: Date.now(),
|
||||
},
|
||||
};
|
||||
return {
|
||||
metadata,
|
||||
output: result.output,
|
||||
};
|
||||
} catch (e: any) {
|
||||
log.error("error", {
|
||||
msg: e.toString(),
|
||||
});
|
||||
return "An error occurred: " + e.toString();
|
||||
}
|
||||
},
|
||||
}),
|
||||
};
|
||||
return "An error occurred: " + e.toString();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ TIPS:
|
||||
- For code exploration, first use Grep to find relevant files, then View to examine them
|
||||
- When viewing large files, use the offset parameter to read specific sections`;
|
||||
|
||||
export const ViewTool = Tool.define({
|
||||
export const view = Tool.define({
|
||||
name: "view",
|
||||
description: DESCRIPTION,
|
||||
parameters: z.object({
|
||||
@@ -84,29 +84,25 @@ export const ViewTool = Tool.define({
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
}
|
||||
const stats = await file.stat();
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filePath}`);
|
||||
|
||||
if (stats.size > MAX_READ_SIZE) {
|
||||
if (stats.size > MAX_READ_SIZE)
|
||||
throw new Error(
|
||||
`File is too large (${stats.size} bytes). Maximum size is ${MAX_READ_SIZE} bytes`,
|
||||
);
|
||||
}
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT;
|
||||
const offset = params.offset || 0;
|
||||
const isImage = isImageFile(filePath);
|
||||
if (isImage) {
|
||||
if (isImage)
|
||||
throw new Error(
|
||||
`This is an image file of type: ${isImage}\nUse a different tool to process images`,
|
||||
);
|
||||
}
|
||||
const lines = await file.text().then((text) => text.split("\n"));
|
||||
const content = lines.slice(offset, offset + limit).map((line, index) => {
|
||||
line =
|
||||
line.length > MAX_LINE_LENGTH
|
||||
? line.substring(0, MAX_LINE_LENGTH) + "..."
|
||||
: line;
|
||||
return `${index + offset + 1}|${line}`;
|
||||
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`;
|
||||
});
|
||||
|
||||
let output = "<file>\n";
|
||||
|
||||
Reference in New Issue
Block a user