mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-07 01:08:58 +00:00
run formatter
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 30000;
|
||||
const MAX_OUTPUT_LENGTH = 30000
|
||||
const BANNED_COMMANDS = [
|
||||
"alias",
|
||||
"curl",
|
||||
@@ -20,9 +20,9 @@ const BANNED_COMMANDS = [
|
||||
"chrome",
|
||||
"firefox",
|
||||
"safari",
|
||||
];
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000;
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000;
|
||||
]
|
||||
const DEFAULT_TIMEOUT = 1 * 60 * 1000
|
||||
const MAX_TIMEOUT = 10 * 60 * 1000
|
||||
|
||||
const DESCRIPTION = `Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.
|
||||
|
||||
@@ -168,7 +168,7 @@ EOF
|
||||
|
||||
Important:
|
||||
- Return an empty response - the user will see the gh output directly
|
||||
- Never update git config`;
|
||||
- Never update git config`
|
||||
|
||||
export const bash = Tool.define({
|
||||
name: "opencode.bash",
|
||||
@@ -183,17 +183,17 @@ export const bash = Tool.define({
|
||||
.optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
||||
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`);
|
||||
throw new Error(`Command '${params.command}' is not allowed`)
|
||||
|
||||
const process = Bun.spawnSync({
|
||||
cmd: ["bash", "-c", params.command],
|
||||
maxBuffer: MAX_OUTPUT_LENGTH,
|
||||
timeout: timeout,
|
||||
});
|
||||
})
|
||||
return {
|
||||
output: process.stdout.toString("utf-8"),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import * as path from "path";
|
||||
import { Tool } from "./tool";
|
||||
import { FileTimes } from "./util/file-times";
|
||||
import { LSP } from "../lsp";
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
import { LSP } from "../lsp"
|
||||
|
||||
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.
|
||||
|
||||
@@ -50,7 +50,7 @@ When making edits:
|
||||
- Do not leave the code in a broken state
|
||||
- Always use relative file paths
|
||||
|
||||
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({
|
||||
name: "opencode.edit",
|
||||
@@ -62,68 +62,68 @@ export const edit = Tool.define({
|
||||
}),
|
||||
async execute(params) {
|
||||
if (!params.filePath) {
|
||||
throw new Error("filePath is required");
|
||||
throw new Error("filePath is required")
|
||||
}
|
||||
|
||||
let filePath = params.filePath;
|
||||
let filePath = params.filePath
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(process.cwd(), filePath);
|
||||
filePath = path.join(process.cwd(), filePath)
|
||||
}
|
||||
|
||||
await (async () => {
|
||||
if (params.oldString === "") {
|
||||
await Bun.write(filePath, params.newString);
|
||||
return;
|
||||
await Bun.write(filePath, params.newString)
|
||||
return
|
||||
}
|
||||
|
||||
const read = FileTimes.get(filePath);
|
||||
const read = FileTimes.get(filePath)
|
||||
if (!read)
|
||||
throw new Error(
|
||||
`You must read the file ${filePath} before editing it. Use the View tool first`,
|
||||
);
|
||||
const file = Bun.file(filePath);
|
||||
if (!(await file.exists())) throw new Error(`File ${filePath} not found`);
|
||||
const stats = await file.stat();
|
||||
)
|
||||
const file = Bun.file(filePath)
|
||||
if (!(await file.exists())) throw new Error(`File ${filePath} not found`)
|
||||
const stats = await file.stat()
|
||||
if (stats.isDirectory())
|
||||
throw new Error(`Path is a directory, not a file: ${filePath}`);
|
||||
throw new Error(`Path is a directory, not a file: ${filePath}`)
|
||||
if (stats.mtime.getTime() > read.getTime())
|
||||
throw new Error(
|
||||
`File ${filePath} has been modified since it was last read.\nLast modification: ${read.toISOString()}\nLast read: ${stats.mtime.toISOString()}\n\nPlease read the file again before modifying it.`,
|
||||
);
|
||||
)
|
||||
|
||||
const content = await file.text();
|
||||
const index = content.indexOf(params.oldString);
|
||||
const content = await file.text()
|
||||
const index = content.indexOf(params.oldString)
|
||||
if (index === -1)
|
||||
throw new Error(
|
||||
`oldString not found in file. Make sure it matches exactly, including whitespace and line breaks`,
|
||||
);
|
||||
const lastIndex = content.lastIndexOf(params.oldString);
|
||||
)
|
||||
const lastIndex = content.lastIndexOf(params.oldString)
|
||||
if (index !== lastIndex)
|
||||
throw new Error(
|
||||
`oldString appears multiple times in the file. Please provide more context to ensure a unique match`,
|
||||
);
|
||||
)
|
||||
|
||||
const newContent =
|
||||
content.substring(0, index) +
|
||||
params.newString +
|
||||
content.substring(index + params.oldString.length);
|
||||
content.substring(index + params.oldString.length)
|
||||
|
||||
await file.write(newContent);
|
||||
})();
|
||||
await file.write(newContent)
|
||||
})()
|
||||
|
||||
FileTimes.write(filePath);
|
||||
FileTimes.read(filePath);
|
||||
FileTimes.write(filePath)
|
||||
FileTimes.read(filePath)
|
||||
|
||||
let output = "";
|
||||
await LSP.file(filePath);
|
||||
const diagnostics = await LSP.diagnostics();
|
||||
let output = ""
|
||||
await LSP.file(filePath)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
for (const [file, issues] of Object.entries(diagnostics)) {
|
||||
if (issues.length === 0) continue;
|
||||
if (issues.length === 0) continue
|
||||
if (file === filePath) {
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`;
|
||||
continue;
|
||||
output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
|
||||
continue
|
||||
}
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`;
|
||||
output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -131,6 +131,6 @@ export const edit = Tool.define({
|
||||
diagnostics,
|
||||
},
|
||||
output,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { JSDOM } from "jsdom";
|
||||
import TurndownService from "turndown";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import TurndownService from "turndown"
|
||||
|
||||
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const DEFAULT_TIMEOUT = 30 * 1000; // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000; // 2 minutes
|
||||
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const DEFAULT_TIMEOUT = 30 * 1000 // 30 seconds
|
||||
const MAX_TIMEOUT = 120 * 1000 // 2 minutes
|
||||
|
||||
const DESCRIPTION = `Fetches content from a URL and returns it in the specified format.
|
||||
|
||||
@@ -35,7 +34,7 @@ TIPS:
|
||||
- Use text format for plain text content or simple API responses
|
||||
- Use markdown format for content that should be rendered with formatting
|
||||
- Use html format when you need the raw HTML structure
|
||||
- Set appropriate timeouts for potentially slow websites`;
|
||||
- Set appropriate timeouts for potentially slow websites`
|
||||
|
||||
export const Fetch = Tool.define({
|
||||
name: "opencode.fetch",
|
||||
@@ -60,18 +59,18 @@ export const Fetch = Tool.define({
|
||||
!params.url.startsWith("http://") &&
|
||||
!params.url.startsWith("https://")
|
||||
) {
|
||||
throw new Error("URL must start with http:// or https://");
|
||||
throw new Error("URL must start with http:// or https://")
|
||||
}
|
||||
|
||||
const timeout = Math.min(
|
||||
(params.timeout ?? DEFAULT_TIMEOUT / 1000) * 1000,
|
||||
MAX_TIMEOUT,
|
||||
);
|
||||
)
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
||||
if (opts?.abortSignal) {
|
||||
opts.abortSignal.addEventListener("abort", () => controller.abort());
|
||||
opts.abortSignal.addEventListener("abort", () => controller.abort())
|
||||
}
|
||||
|
||||
const response = await fetch(params.url, {
|
||||
@@ -79,59 +78,59 @@ export const Fetch = Tool.define({
|
||||
headers: {
|
||||
"User-Agent": "opencode/1.0",
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed with status code: ${response.status}`);
|
||||
throw new Error(`Request failed with status code: ${response.status}`)
|
||||
}
|
||||
|
||||
// Check content length
|
||||
const contentLength = response.headers.get("content-length");
|
||||
const contentLength = response.headers.get("content-length")
|
||||
if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) {
|
||||
throw new Error("Response too large (exceeds 5MB limit)");
|
||||
throw new Error("Response too large (exceeds 5MB limit)")
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
if (arrayBuffer.byteLength > MAX_RESPONSE_SIZE) {
|
||||
throw new Error("Response too large (exceeds 5MB limit)");
|
||||
throw new Error("Response too large (exceeds 5MB limit)")
|
||||
}
|
||||
|
||||
const content = new TextDecoder().decode(arrayBuffer);
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const content = new TextDecoder().decode(arrayBuffer)
|
||||
const contentType = response.headers.get("content-type") || ""
|
||||
|
||||
switch (params.format) {
|
||||
case "text":
|
||||
if (contentType.includes("text/html")) {
|
||||
const text = extractTextFromHTML(content);
|
||||
return { output: text };
|
||||
const text = extractTextFromHTML(content)
|
||||
return { output: text }
|
||||
}
|
||||
return { output: content };
|
||||
return { output: content }
|
||||
|
||||
case "markdown":
|
||||
if (contentType.includes("text/html")) {
|
||||
const markdown = convertHTMLToMarkdown(content);
|
||||
return { output: markdown };
|
||||
const markdown = convertHTMLToMarkdown(content)
|
||||
return { output: markdown }
|
||||
}
|
||||
return { output: "```\n" + content + "\n```" };
|
||||
return { output: "```\n" + content + "\n```" }
|
||||
|
||||
case "html":
|
||||
return { output: content };
|
||||
return { output: content }
|
||||
|
||||
default:
|
||||
return { output: content };
|
||||
return { output: content }
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function extractTextFromHTML(html: string): string {
|
||||
const dom = new JSDOM(html);
|
||||
const text = dom.window.document.body?.textContent || "";
|
||||
return text.replace(/\s+/g, " ").trim();
|
||||
const doc = new DOMParser().parseFromString(html, "text/html")
|
||||
const text = doc.body.textContent || doc.body.innerText || ""
|
||||
return text.replace(/\s+/g, " ").trim()
|
||||
}
|
||||
|
||||
function convertHTMLToMarkdown(html: string): string {
|
||||
const turndownService = new TurndownService();
|
||||
return turndownService.turndown(html);
|
||||
const turndownService = new TurndownService()
|
||||
return turndownService.turndown(html)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { App } from "../app/app";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
|
||||
const DESCRIPTION = `Fast file pattern matching tool that finds files by name and pattern, returning matching paths sorted by modification time (newest first).
|
||||
|
||||
@@ -35,7 +35,7 @@ LIMITATIONS:
|
||||
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`;
|
||||
- Always check if results are truncated and refine your search pattern if needed`
|
||||
|
||||
export const glob = Tool.define({
|
||||
name: "opencode.glob",
|
||||
@@ -50,37 +50,37 @@ export const glob = Tool.define({
|
||||
.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;
|
||||
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;
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
const stats = await Bun.file(file)
|
||||
.stat()
|
||||
.then((x) => x.mtime.getTime())
|
||||
.catch(() => 0);
|
||||
.catch(() => 0)
|
||||
files.push({
|
||||
path: file,
|
||||
mtime: stats,
|
||||
});
|
||||
})
|
||||
}
|
||||
files.sort((a, b) => b.mtime - a.mtime);
|
||||
files.sort((a, b) => b.mtime - a.mtime)
|
||||
|
||||
const output = [];
|
||||
if (files.length === 0) output.push("No files found");
|
||||
const output = []
|
||||
if (files.length === 0) output.push("No files found")
|
||||
if (files.length > 0) {
|
||||
output.push(...files.map((f) => f.path));
|
||||
output.push(...files.map((f) => f.path))
|
||||
if (truncated) {
|
||||
output.push("");
|
||||
output.push("")
|
||||
output.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ export const glob = Tool.define({
|
||||
truncated,
|
||||
},
|
||||
output: output.join("\n"),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { App } from "../app/app";
|
||||
import { spawn } from "child_process";
|
||||
import { promises as fs } from "fs";
|
||||
import path from "path";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import { spawn } from "child_process"
|
||||
import { promises as fs } from "fs"
|
||||
import path from "path"
|
||||
|
||||
const DESCRIPTION = `Fast content search tool that finds files containing specific text or patterns, returning matching file paths sorted by modification time (newest first).
|
||||
|
||||
@@ -40,13 +40,13 @@ TIPS:
|
||||
- For faster, more targeted searches, first use Glob to find relevant files, then use 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
|
||||
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`;
|
||||
- Use literal_text=true when searching for exact text containing special characters like dots, parentheses, etc.`
|
||||
|
||||
interface GrepMatch {
|
||||
path: string;
|
||||
modTime: number;
|
||||
lineNum: number;
|
||||
lineText: string;
|
||||
path: string
|
||||
modTime: number
|
||||
lineNum: number
|
||||
lineText: string
|
||||
}
|
||||
|
||||
function escapeRegexPattern(pattern: string): string {
|
||||
@@ -65,27 +65,27 @@ function escapeRegexPattern(pattern: string): string {
|
||||
"^",
|
||||
"$",
|
||||
"|",
|
||||
];
|
||||
let escaped = pattern;
|
||||
]
|
||||
let escaped = pattern
|
||||
|
||||
for (const char of specialChars) {
|
||||
escaped = escaped.replaceAll(char, "\\" + char);
|
||||
escaped = escaped.replaceAll(char, "\\" + char)
|
||||
}
|
||||
|
||||
return escaped;
|
||||
return escaped
|
||||
}
|
||||
|
||||
function globToRegex(glob: string): string {
|
||||
let regexPattern = glob.replaceAll(".", "\\.");
|
||||
regexPattern = regexPattern.replaceAll("*", ".*");
|
||||
regexPattern = regexPattern.replaceAll("?", ".");
|
||||
let regexPattern = glob.replaceAll(".", "\\.")
|
||||
regexPattern = regexPattern.replaceAll("*", ".*")
|
||||
regexPattern = regexPattern.replaceAll("?", ".")
|
||||
|
||||
// Handle {a,b,c} patterns
|
||||
regexPattern = regexPattern.replace(/\{([^}]+)\}/g, (_, inner) => {
|
||||
return "(" + inner.replace(/,/g, "|") + ")";
|
||||
});
|
||||
return "(" + inner.replace(/,/g, "|") + ")"
|
||||
})
|
||||
|
||||
return regexPattern;
|
||||
return regexPattern
|
||||
}
|
||||
|
||||
async function searchWithRipgrep(
|
||||
@@ -94,71 +94,71 @@ async function searchWithRipgrep(
|
||||
include?: string,
|
||||
): Promise<GrepMatch[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const args = ["-n", pattern];
|
||||
const args = ["-n", pattern]
|
||||
if (include) {
|
||||
args.push("--glob", include);
|
||||
args.push("--glob", include)
|
||||
}
|
||||
args.push(searchPath);
|
||||
args.push(searchPath)
|
||||
|
||||
const rg = spawn("rg", args);
|
||||
let output = "";
|
||||
let errorOutput = "";
|
||||
const rg = spawn("rg", args)
|
||||
let output = ""
|
||||
let errorOutput = ""
|
||||
|
||||
rg.stdout.on("data", (data) => {
|
||||
output += data.toString();
|
||||
});
|
||||
output += data.toString()
|
||||
})
|
||||
|
||||
rg.stderr.on("data", (data) => {
|
||||
errorOutput += data.toString();
|
||||
});
|
||||
errorOutput += data.toString()
|
||||
})
|
||||
|
||||
rg.on("close", async (code) => {
|
||||
if (code === 1) {
|
||||
// No matches found
|
||||
resolve([]);
|
||||
return;
|
||||
resolve([])
|
||||
return
|
||||
}
|
||||
|
||||
if (code !== 0) {
|
||||
reject(new Error(`ripgrep failed: ${errorOutput}`));
|
||||
return;
|
||||
reject(new Error(`ripgrep failed: ${errorOutput}`))
|
||||
return
|
||||
}
|
||||
|
||||
const lines = output.trim().split("\n");
|
||||
const matches: GrepMatch[] = [];
|
||||
const lines = output.trim().split("\n")
|
||||
const matches: GrepMatch[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line) continue;
|
||||
if (!line) continue
|
||||
|
||||
// Parse ripgrep output format: file:line:content
|
||||
const parts = line.split(":", 3);
|
||||
if (parts.length < 3) continue;
|
||||
const parts = line.split(":", 3)
|
||||
if (parts.length < 3) continue
|
||||
|
||||
const filePath = parts[0];
|
||||
const lineNum = parseInt(parts[1], 10);
|
||||
const lineText = parts[2];
|
||||
const filePath = parts[0]
|
||||
const lineNum = parseInt(parts[1], 10)
|
||||
const lineText = parts[2]
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(filePath);
|
||||
const stats = await fs.stat(filePath)
|
||||
matches.push({
|
||||
path: filePath,
|
||||
modTime: stats.mtime.getTime(),
|
||||
lineNum,
|
||||
lineText,
|
||||
});
|
||||
})
|
||||
} catch {
|
||||
// Skip files we can't access
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
resolve(matches);
|
||||
});
|
||||
resolve(matches)
|
||||
})
|
||||
|
||||
rg.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function searchFilesWithRegex(
|
||||
@@ -166,68 +166,68 @@ async function searchFilesWithRegex(
|
||||
rootPath: string,
|
||||
include?: string,
|
||||
): Promise<GrepMatch[]> {
|
||||
const matches: GrepMatch[] = [];
|
||||
const regex = new RegExp(pattern);
|
||||
const matches: GrepMatch[] = []
|
||||
const regex = new RegExp(pattern)
|
||||
|
||||
let includePattern: RegExp | undefined;
|
||||
let includePattern: RegExp | undefined
|
||||
if (include) {
|
||||
const regexPattern = globToRegex(include);
|
||||
includePattern = new RegExp(regexPattern);
|
||||
const regexPattern = globToRegex(include)
|
||||
includePattern = new RegExp(regexPattern)
|
||||
}
|
||||
|
||||
async function walkDir(dir: string) {
|
||||
if (matches.length >= 200) return;
|
||||
if (matches.length >= 200) return
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true })
|
||||
|
||||
for (const entry of entries) {
|
||||
if (matches.length >= 200) break;
|
||||
if (matches.length >= 200) break
|
||||
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
const fullPath = path.join(dir, entry.name)
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip hidden directories
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
await walkDir(fullPath);
|
||||
if (entry.name.startsWith(".")) continue
|
||||
await walkDir(fullPath)
|
||||
} else if (entry.isFile()) {
|
||||
// Skip hidden files
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
if (entry.name.startsWith(".")) continue
|
||||
|
||||
if (includePattern && !includePattern.test(fullPath)) {
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(fullPath, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
const content = await fs.readFile(fullPath, "utf-8")
|
||||
const lines = content.split("\n")
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (regex.test(lines[i])) {
|
||||
const stats = await fs.stat(fullPath);
|
||||
const stats = await fs.stat(fullPath)
|
||||
matches.push({
|
||||
path: fullPath,
|
||||
modTime: stats.mtime.getTime(),
|
||||
lineNum: i + 1,
|
||||
lineText: lines[i],
|
||||
});
|
||||
break; // Only first match per file
|
||||
})
|
||||
break // Only first match per file
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
continue;
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Skip directories we can't read
|
||||
return;
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await walkDir(rootPath);
|
||||
return matches;
|
||||
await walkDir(rootPath)
|
||||
return matches
|
||||
}
|
||||
|
||||
async function searchFiles(
|
||||
@@ -236,23 +236,23 @@ async function searchFiles(
|
||||
include?: string,
|
||||
limit: number = 100,
|
||||
): Promise<{ matches: GrepMatch[]; truncated: boolean }> {
|
||||
let matches: GrepMatch[];
|
||||
let matches: GrepMatch[]
|
||||
|
||||
try {
|
||||
matches = await searchWithRipgrep(pattern, rootPath, include);
|
||||
matches = await searchWithRipgrep(pattern, rootPath, include)
|
||||
} catch {
|
||||
matches = await searchFilesWithRegex(pattern, rootPath, include);
|
||||
matches = await searchFilesWithRegex(pattern, rootPath, include)
|
||||
}
|
||||
|
||||
// Sort by modification time (newest first)
|
||||
matches.sort((a, b) => b.modTime - a.modTime);
|
||||
matches.sort((a, b) => b.modTime - a.modTime)
|
||||
|
||||
const truncated = matches.length > limit;
|
||||
const truncated = matches.length > limit
|
||||
if (truncated) {
|
||||
matches = matches.slice(0, limit);
|
||||
matches = matches.slice(0, limit)
|
||||
}
|
||||
|
||||
return { matches, truncated };
|
||||
return { matches, truncated }
|
||||
}
|
||||
|
||||
export const grep = Tool.define({
|
||||
@@ -283,54 +283,54 @@ export const grep = Tool.define({
|
||||
}),
|
||||
async execute(params) {
|
||||
if (!params.pattern) {
|
||||
throw new Error("pattern is required");
|
||||
throw new Error("pattern is required")
|
||||
}
|
||||
|
||||
const app = await App.use();
|
||||
const searchPath = params.path || app.root;
|
||||
const app = await App.use()
|
||||
const searchPath = params.path || app.root
|
||||
|
||||
// If literalText is true, escape the pattern
|
||||
const searchPattern = params.literalText
|
||||
? escapeRegexPattern(params.pattern)
|
||||
: params.pattern;
|
||||
: params.pattern
|
||||
|
||||
const { matches, truncated } = await searchFiles(
|
||||
searchPattern,
|
||||
searchPath,
|
||||
params.include,
|
||||
100,
|
||||
);
|
||||
)
|
||||
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
metadata: { matches: 0, truncated },
|
||||
output: "No files found"
|
||||
};
|
||||
output: "No files found",
|
||||
}
|
||||
}
|
||||
|
||||
const lines = [`Found ${matches.length} matches`];
|
||||
const lines = [`Found ${matches.length} matches`]
|
||||
|
||||
let currentFile = "";
|
||||
let currentFile = ""
|
||||
for (const match of matches) {
|
||||
if (currentFile !== match.path) {
|
||||
if (currentFile !== "") {
|
||||
lines.push("");
|
||||
lines.push("")
|
||||
}
|
||||
currentFile = match.path;
|
||||
lines.push(`${match.path}:`);
|
||||
currentFile = match.path
|
||||
lines.push(`${match.path}:`)
|
||||
}
|
||||
if (match.lineNum > 0) {
|
||||
lines.push(` Line ${match.lineNum}: ${match.lineText}`);
|
||||
lines.push(` Line ${match.lineNum}: ${match.lineText}`)
|
||||
} else {
|
||||
lines.push(` ${match.path}`);
|
||||
lines.push(` ${match.path}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
lines.push("");
|
||||
lines.push("")
|
||||
lines.push(
|
||||
"(Results are truncated. Consider using a more specific path or pattern.)",
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -339,7 +339,6 @@ export const grep = Tool.define({
|
||||
truncated,
|
||||
},
|
||||
output: lines.join("\n"),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export * from "./bash";
|
||||
export * from "./edit";
|
||||
export * from "./fetch";
|
||||
export * from "./glob";
|
||||
export * from "./grep";
|
||||
export * from "./view";
|
||||
export * from "./ls";
|
||||
export * from "./lsp-diagnostics";
|
||||
export * from "./lsp-hover";
|
||||
export * from "./bash"
|
||||
export * from "./edit"
|
||||
export * from "./fetch"
|
||||
export * from "./glob"
|
||||
export * from "./grep"
|
||||
export * from "./view"
|
||||
export * from "./ls"
|
||||
export * from "./lsp-diagnostics"
|
||||
export * from "./lsp-hover"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import { App } from "../app/app";
|
||||
import * as path from "path";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import { App } from "../app/app"
|
||||
import * as path from "path"
|
||||
|
||||
const IGNORE_PATTERNS = [
|
||||
"node_modules/",
|
||||
@@ -15,7 +15,7 @@ const IGNORE_PATTERNS = [
|
||||
"obj/",
|
||||
".idea/",
|
||||
".vscode/",
|
||||
];
|
||||
]
|
||||
|
||||
export const ls = Tool.define({
|
||||
name: "opencode.ls",
|
||||
@@ -25,72 +25,72 @@ export const ls = Tool.define({
|
||||
ignore: z.array(z.string()).optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
const app = await App.use();
|
||||
const searchPath = path.resolve(app.root, params.path || ".");
|
||||
const app = await App.use()
|
||||
const searchPath = path.resolve(app.root, params.path || ".")
|
||||
|
||||
const glob = new Bun.Glob("**/*");
|
||||
const files = [];
|
||||
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;
|
||||
continue
|
||||
if (params.ignore?.some((pattern) => new Bun.Glob(pattern).match(file)))
|
||||
continue;
|
||||
files.push(file);
|
||||
if (files.length >= 1000) break;
|
||||
continue
|
||||
files.push(file)
|
||||
if (files.length >= 1000) break
|
||||
}
|
||||
|
||||
// Build directory structure
|
||||
const dirs = new Set<string>();
|
||||
const filesByDir = new Map<string, string[]>();
|
||||
const dirs = new Set<string>()
|
||||
const filesByDir = new Map<string, string[]>()
|
||||
|
||||
for (const file of files) {
|
||||
const dir = path.dirname(file);
|
||||
const parts = dir === "." ? [] : dir.split("/");
|
||||
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);
|
||||
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));
|
||||
if (!filesByDir.has(dir)) filesByDir.set(dir, [])
|
||||
filesByDir.get(dir)!.push(path.basename(file))
|
||||
}
|
||||
|
||||
function renderDir(dirPath: string, depth: number): string {
|
||||
const indent = " ".repeat(depth);
|
||||
let output = "";
|
||||
const indent = " ".repeat(depth)
|
||||
let output = ""
|
||||
|
||||
if (depth > 0) {
|
||||
output += `${indent}${path.basename(dirPath)}/\n`;
|
||||
output += `${indent}${path.basename(dirPath)}/\n`
|
||||
}
|
||||
|
||||
const childIndent = " ".repeat(depth + 1);
|
||||
const childIndent = " ".repeat(depth + 1)
|
||||
const children = Array.from(dirs)
|
||||
.filter((d) => path.dirname(d) === dirPath && d !== dirPath)
|
||||
.sort();
|
||||
.sort()
|
||||
|
||||
// Render subdirectories first
|
||||
for (const child of children) {
|
||||
output += renderDir(child, depth + 1);
|
||||
output += renderDir(child, depth + 1)
|
||||
}
|
||||
|
||||
// Render files
|
||||
const files = filesByDir.get(dirPath) || [];
|
||||
const files = filesByDir.get(dirPath) || []
|
||||
for (const file of files.sort()) {
|
||||
output += `${childIndent}${file}\n`;
|
||||
output += `${childIndent}${file}\n`
|
||||
}
|
||||
|
||||
return output;
|
||||
return output
|
||||
}
|
||||
|
||||
const output = `${searchPath}/\n` + renderDir(".", 0);
|
||||
const output = `${searchPath}/\n` + renderDir(".", 0)
|
||||
|
||||
return {
|
||||
metadata: { count: files.length, truncated: files.length >= 1000 },
|
||||
output,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import path from "path";
|
||||
import { LSP } from "../lsp";
|
||||
import { App } from "../app/app";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import path from "path"
|
||||
import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const LspDiagnosticTool = Tool.define({
|
||||
name: "opencode.lsp_diagnostic",
|
||||
@@ -34,13 +34,13 @@ TIPS:
|
||||
path: z.string().describe("The path to the file to get diagnostics."),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
const app = await App.use();
|
||||
const app = await App.use()
|
||||
const normalized = path.isAbsolute(args.path)
|
||||
? args.path
|
||||
: path.join(app.root, args.path);
|
||||
await LSP.file(normalized);
|
||||
const diagnostics = await LSP.diagnostics();
|
||||
const file = diagnostics[normalized];
|
||||
: path.join(app.root, args.path)
|
||||
await LSP.file(normalized)
|
||||
const diagnostics = await LSP.diagnostics()
|
||||
const file = diagnostics[normalized]
|
||||
return {
|
||||
metadata: {
|
||||
diagnostics,
|
||||
@@ -48,6 +48,6 @@ TIPS:
|
||||
output: file?.length
|
||||
? file.map(LSP.Diagnostic.pretty).join("\n")
|
||||
: "No errors found",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "./tool";
|
||||
import path from "path";
|
||||
import { LSP } from "../lsp";
|
||||
import { App } from "../app/app";
|
||||
import { z } from "zod"
|
||||
import { Tool } from "./tool"
|
||||
import path from "path"
|
||||
import { LSP } from "../lsp"
|
||||
import { App } from "../app/app"
|
||||
|
||||
export const LspHoverTool = Tool.define({
|
||||
name: "opencode.lsp_hover",
|
||||
@@ -17,22 +17,22 @@ export const LspHoverTool = Tool.define({
|
||||
character: z.number().describe("The character number to get diagnostics."),
|
||||
}),
|
||||
execute: async (args) => {
|
||||
console.log(args);
|
||||
const app = await App.use();
|
||||
console.log(args)
|
||||
const app = await App.use()
|
||||
const file = path.isAbsolute(args.file)
|
||||
? args.file
|
||||
: path.join(app.root, args.file);
|
||||
await LSP.file(file);
|
||||
: path.join(app.root, args.file)
|
||||
await LSP.file(file)
|
||||
const result = await LSP.hover({
|
||||
...args,
|
||||
file,
|
||||
});
|
||||
console.log(result);
|
||||
})
|
||||
console.log(result)
|
||||
return {
|
||||
metadata: {
|
||||
result,
|
||||
},
|
||||
output: JSON.stringify(result, null, 2),
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { Tool } from "./tool";
|
||||
import { FileTimes } from "./util/file-times";
|
||||
import { z } from "zod"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { Tool } from "./tool"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
|
||||
const DESCRIPTION = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
|
||||
|
||||
@@ -31,198 +31,198 @@ CRITICAL REQUIREMENTS FOR USING THIS TOOL:
|
||||
3. VALIDATION: Ensure edits result in idiomatic, correct code
|
||||
4. PATHS: Always use absolute file paths (starting with /)
|
||||
|
||||
The tool will apply all changes in a single atomic operation.`;
|
||||
The tool will apply all changes in a single atomic operation.`
|
||||
|
||||
const PatchParams = z.object({
|
||||
patchText: z
|
||||
.string()
|
||||
.describe("The full patch text that describes all changes to be made"),
|
||||
});
|
||||
})
|
||||
|
||||
interface PatchResponseMetadata {
|
||||
changed: string[];
|
||||
additions: number;
|
||||
removals: number;
|
||||
changed: string[]
|
||||
additions: number
|
||||
removals: number
|
||||
}
|
||||
|
||||
interface Change {
|
||||
type: "add" | "update" | "delete";
|
||||
old_content?: string;
|
||||
new_content?: string;
|
||||
type: "add" | "update" | "delete"
|
||||
old_content?: string
|
||||
new_content?: string
|
||||
}
|
||||
|
||||
interface Commit {
|
||||
changes: Record<string, Change>;
|
||||
changes: Record<string, Change>
|
||||
}
|
||||
|
||||
interface PatchOperation {
|
||||
type: "update" | "add" | "delete";
|
||||
filePath: string;
|
||||
hunks?: PatchHunk[];
|
||||
content?: string;
|
||||
type: "update" | "add" | "delete"
|
||||
filePath: string
|
||||
hunks?: PatchHunk[]
|
||||
content?: string
|
||||
}
|
||||
|
||||
interface PatchHunk {
|
||||
contextLine: string;
|
||||
changes: PatchChange[];
|
||||
contextLine: string
|
||||
changes: PatchChange[]
|
||||
}
|
||||
|
||||
interface PatchChange {
|
||||
type: "keep" | "remove" | "add";
|
||||
content: string;
|
||||
type: "keep" | "remove" | "add"
|
||||
content: string
|
||||
}
|
||||
|
||||
function identifyFilesNeeded(patchText: string): string[] {
|
||||
const files: string[] = [];
|
||||
const lines = patchText.split("\n");
|
||||
const files: string[] = []
|
||||
const lines = patchText.split("\n")
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.startsWith("*** Update File:") ||
|
||||
line.startsWith("*** Delete File:")
|
||||
) {
|
||||
const filePath = line.split(":", 2)[1]?.trim();
|
||||
if (filePath) files.push(filePath);
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (filePath) files.push(filePath)
|
||||
}
|
||||
}
|
||||
return files;
|
||||
return files
|
||||
}
|
||||
|
||||
function identifyFilesAdded(patchText: string): string[] {
|
||||
const files: string[] = [];
|
||||
const lines = patchText.split("\n");
|
||||
const files: string[] = []
|
||||
const lines = patchText.split("\n")
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("*** Add File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim();
|
||||
if (filePath) files.push(filePath);
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (filePath) files.push(filePath)
|
||||
}
|
||||
}
|
||||
return files;
|
||||
return files
|
||||
}
|
||||
|
||||
function textToPatch(
|
||||
patchText: string,
|
||||
_currentFiles: Record<string, string>,
|
||||
): [PatchOperation[], number] {
|
||||
const operations: PatchOperation[] = [];
|
||||
const lines = patchText.split("\n");
|
||||
let i = 0;
|
||||
let fuzz = 0;
|
||||
const operations: PatchOperation[] = []
|
||||
const lines = patchText.split("\n")
|
||||
let i = 0
|
||||
let fuzz = 0
|
||||
|
||||
while (i < lines.length) {
|
||||
const line = lines[i];
|
||||
const line = lines[i]
|
||||
|
||||
if (line.startsWith("*** Update File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim();
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (!filePath) {
|
||||
i++;
|
||||
continue;
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
const hunks: PatchHunk[] = [];
|
||||
i++;
|
||||
const hunks: PatchHunk[] = []
|
||||
i++
|
||||
|
||||
while (i < lines.length && !lines[i].startsWith("***")) {
|
||||
if (lines[i].startsWith("@@")) {
|
||||
const contextLine = lines[i].substring(2).trim();
|
||||
const changes: PatchChange[] = [];
|
||||
i++;
|
||||
const contextLine = lines[i].substring(2).trim()
|
||||
const changes: PatchChange[] = []
|
||||
i++
|
||||
|
||||
while (
|
||||
i < lines.length &&
|
||||
!lines[i].startsWith("@@") &&
|
||||
!lines[i].startsWith("***")
|
||||
) {
|
||||
const changeLine = lines[i];
|
||||
const changeLine = lines[i]
|
||||
if (changeLine.startsWith(" ")) {
|
||||
changes.push({ type: "keep", content: changeLine.substring(1) });
|
||||
changes.push({ type: "keep", content: changeLine.substring(1) })
|
||||
} else if (changeLine.startsWith("-")) {
|
||||
changes.push({
|
||||
type: "remove",
|
||||
content: changeLine.substring(1),
|
||||
});
|
||||
})
|
||||
} else if (changeLine.startsWith("+")) {
|
||||
changes.push({ type: "add", content: changeLine.substring(1) });
|
||||
changes.push({ type: "add", content: changeLine.substring(1) })
|
||||
}
|
||||
i++;
|
||||
i++
|
||||
}
|
||||
|
||||
hunks.push({ contextLine, changes });
|
||||
hunks.push({ contextLine, changes })
|
||||
} else {
|
||||
i++;
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
operations.push({ type: "update", filePath, hunks });
|
||||
operations.push({ type: "update", filePath, hunks })
|
||||
} else if (line.startsWith("*** Add File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim();
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (!filePath) {
|
||||
i++;
|
||||
continue;
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
let content = "";
|
||||
i++;
|
||||
let content = ""
|
||||
i++
|
||||
|
||||
while (i < lines.length && !lines[i].startsWith("***")) {
|
||||
if (lines[i].startsWith("+")) {
|
||||
content += lines[i].substring(1) + "\n";
|
||||
content += lines[i].substring(1) + "\n"
|
||||
}
|
||||
i++;
|
||||
i++
|
||||
}
|
||||
|
||||
operations.push({ type: "add", filePath, content: content.slice(0, -1) });
|
||||
operations.push({ type: "add", filePath, content: content.slice(0, -1) })
|
||||
} else if (line.startsWith("*** Delete File:")) {
|
||||
const filePath = line.split(":", 2)[1]?.trim();
|
||||
const filePath = line.split(":", 2)[1]?.trim()
|
||||
if (filePath) {
|
||||
operations.push({ type: "delete", filePath });
|
||||
operations.push({ type: "delete", filePath })
|
||||
}
|
||||
i++;
|
||||
i++
|
||||
} else {
|
||||
i++;
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
return [operations, fuzz];
|
||||
return [operations, fuzz]
|
||||
}
|
||||
|
||||
function patchToCommit(
|
||||
operations: PatchOperation[],
|
||||
currentFiles: Record<string, string>,
|
||||
): Commit {
|
||||
const changes: Record<string, Change> = {};
|
||||
const changes: Record<string, Change> = {}
|
||||
|
||||
for (const op of operations) {
|
||||
if (op.type === "delete") {
|
||||
changes[op.filePath] = {
|
||||
type: "delete",
|
||||
old_content: currentFiles[op.filePath] || "",
|
||||
};
|
||||
}
|
||||
} else if (op.type === "add") {
|
||||
changes[op.filePath] = {
|
||||
type: "add",
|
||||
new_content: op.content || "",
|
||||
};
|
||||
}
|
||||
} else if (op.type === "update" && op.hunks) {
|
||||
const originalContent = currentFiles[op.filePath] || "";
|
||||
const lines = originalContent.split("\n");
|
||||
const originalContent = currentFiles[op.filePath] || ""
|
||||
const lines = originalContent.split("\n")
|
||||
|
||||
for (const hunk of op.hunks) {
|
||||
const contextIndex = lines.findIndex((line) =>
|
||||
line.includes(hunk.contextLine),
|
||||
);
|
||||
)
|
||||
if (contextIndex === -1) {
|
||||
throw new Error(`Context line not found: ${hunk.contextLine}`);
|
||||
throw new Error(`Context line not found: ${hunk.contextLine}`)
|
||||
}
|
||||
|
||||
let currentIndex = contextIndex;
|
||||
let currentIndex = contextIndex
|
||||
for (const change of hunk.changes) {
|
||||
if (change.type === "keep") {
|
||||
currentIndex++;
|
||||
currentIndex++
|
||||
} else if (change.type === "remove") {
|
||||
lines.splice(currentIndex, 1);
|
||||
lines.splice(currentIndex, 1)
|
||||
} else if (change.type === "add") {
|
||||
lines.splice(currentIndex, 0, change.content);
|
||||
currentIndex++;
|
||||
lines.splice(currentIndex, 0, change.content)
|
||||
currentIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,11 +231,11 @@ function patchToCommit(
|
||||
type: "update",
|
||||
old_content: originalContent,
|
||||
new_content: lines.join("\n"),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { changes };
|
||||
return { changes }
|
||||
}
|
||||
|
||||
function generateDiff(
|
||||
@@ -244,11 +244,11 @@ function generateDiff(
|
||||
filePath: string,
|
||||
): [string, number, number] {
|
||||
// Mock implementation - would need actual diff generation
|
||||
const lines1 = oldContent.split("\n");
|
||||
const lines2 = newContent.split("\n");
|
||||
const additions = Math.max(0, lines2.length - lines1.length);
|
||||
const removals = Math.max(0, lines1.length - lines2.length);
|
||||
return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals];
|
||||
const lines1 = oldContent.split("\n")
|
||||
const lines2 = newContent.split("\n")
|
||||
const additions = Math.max(0, lines2.length - lines1.length)
|
||||
const removals = Math.max(0, lines1.length - lines2.length)
|
||||
return [`--- ${filePath}\n+++ ${filePath}\n`, additions, removals]
|
||||
}
|
||||
|
||||
async function applyCommit(
|
||||
@@ -258,9 +258,9 @@ async function applyCommit(
|
||||
): Promise<void> {
|
||||
for (const [filePath, change] of Object.entries(commit.changes)) {
|
||||
if (change.type === "delete") {
|
||||
await deleteFile(filePath);
|
||||
await deleteFile(filePath)
|
||||
} else if (change.new_content !== undefined) {
|
||||
await writeFile(filePath, change.new_content);
|
||||
await writeFile(filePath, change.new_content)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -271,142 +271,142 @@ export const patch = Tool.define({
|
||||
parameters: PatchParams,
|
||||
execute: async (params) => {
|
||||
if (!params.patchText) {
|
||||
throw new Error("patchText is required");
|
||||
throw new Error("patchText is required")
|
||||
}
|
||||
|
||||
// Identify all files needed for the patch and verify they've been read
|
||||
const filesToRead = identifyFilesNeeded(params.patchText);
|
||||
const filesToRead = identifyFilesNeeded(params.patchText)
|
||||
for (const filePath of filesToRead) {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
|
||||
if (!FileTimes.get(absPath)) {
|
||||
throw new Error(
|
||||
`you must read the file ${filePath} before patching it. Use the FileRead tool first`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const stats = await fs.stat(absPath);
|
||||
const stats = await fs.stat(absPath)
|
||||
if (stats.isDirectory()) {
|
||||
throw new Error(`path is a directory, not a file: ${absPath}`);
|
||||
throw new Error(`path is a directory, not a file: ${absPath}`)
|
||||
}
|
||||
|
||||
const lastRead = FileTimes.get(absPath);
|
||||
const lastRead = FileTimes.get(absPath)
|
||||
if (lastRead && stats.mtime > lastRead) {
|
||||
throw new Error(
|
||||
`file ${absPath} has been modified since it was last read (mod time: ${stats.mtime.toISOString()}, last read: ${lastRead.toISOString()})`,
|
||||
);
|
||||
)
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.code === "ENOENT") {
|
||||
throw new Error(`file not found: ${absPath}`);
|
||||
throw new Error(`file not found: ${absPath}`)
|
||||
}
|
||||
throw new Error(`failed to access file: ${error.message}`);
|
||||
throw new Error(`failed to access file: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new files to ensure they don't already exist
|
||||
const filesToAdd = identifyFilesAdded(params.patchText);
|
||||
const filesToAdd = identifyFilesAdded(params.patchText)
|
||||
for (const filePath of filesToAdd) {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.stat(absPath);
|
||||
throw new Error(`file already exists and cannot be added: ${absPath}`);
|
||||
await fs.stat(absPath)
|
||||
throw new Error(`file already exists and cannot be added: ${absPath}`)
|
||||
} catch (error: any) {
|
||||
if (error.code !== "ENOENT") {
|
||||
throw new Error(`failed to check file: ${error.message}`);
|
||||
throw new Error(`failed to check file: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all required files
|
||||
const currentFiles: Record<string, string> = {};
|
||||
const currentFiles: Record<string, string> = {}
|
||||
for (const filePath of filesToRead) {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(absPath, "utf-8");
|
||||
currentFiles[filePath] = content;
|
||||
const content = await fs.readFile(absPath, "utf-8")
|
||||
currentFiles[filePath] = content
|
||||
} catch (error: any) {
|
||||
throw new Error(`failed to read file ${absPath}: ${error.message}`);
|
||||
throw new Error(`failed to read file ${absPath}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the patch
|
||||
const [patch, fuzz] = textToPatch(params.patchText, currentFiles);
|
||||
const [patch, fuzz] = textToPatch(params.patchText, currentFiles)
|
||||
if (fuzz > 3) {
|
||||
throw new Error(
|
||||
`patch contains fuzzy matches (fuzz level: ${fuzz}). Please make your context lines more precise`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Convert patch to commit
|
||||
const commit = patchToCommit(patch, currentFiles);
|
||||
const commit = patchToCommit(patch, currentFiles)
|
||||
|
||||
// Apply the changes to the filesystem
|
||||
await applyCommit(
|
||||
commit,
|
||||
async (filePath: string, content: string) => {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
|
||||
// Create parent directories if needed
|
||||
const dir = path.dirname(absPath);
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
await fs.writeFile(absPath, content, "utf-8");
|
||||
const dir = path.dirname(absPath)
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
await fs.writeFile(absPath, content, "utf-8")
|
||||
},
|
||||
async (filePath: string) => {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
await fs.unlink(absPath);
|
||||
await fs.unlink(absPath)
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
// Calculate statistics
|
||||
const changedFiles: string[] = [];
|
||||
let totalAdditions = 0;
|
||||
let totalRemovals = 0;
|
||||
const changedFiles: string[] = []
|
||||
let totalAdditions = 0
|
||||
let totalRemovals = 0
|
||||
|
||||
for (const [filePath, change] of Object.entries(commit.changes)) {
|
||||
let absPath = filePath;
|
||||
let absPath = filePath
|
||||
if (!path.isAbsolute(absPath)) {
|
||||
absPath = path.resolve(process.cwd(), absPath);
|
||||
absPath = path.resolve(process.cwd(), absPath)
|
||||
}
|
||||
changedFiles.push(absPath);
|
||||
changedFiles.push(absPath)
|
||||
|
||||
const oldContent = change.old_content || "";
|
||||
const newContent = change.new_content || "";
|
||||
const oldContent = change.old_content || ""
|
||||
const newContent = change.new_content || ""
|
||||
|
||||
// Calculate diff statistics
|
||||
const [, additions, removals] = generateDiff(
|
||||
oldContent,
|
||||
newContent,
|
||||
filePath,
|
||||
);
|
||||
totalAdditions += additions;
|
||||
totalRemovals += removals;
|
||||
)
|
||||
totalAdditions += additions
|
||||
totalRemovals += removals
|
||||
|
||||
// Record file operations
|
||||
FileTimes.write(absPath);
|
||||
FileTimes.read(absPath);
|
||||
FileTimes.write(absPath)
|
||||
FileTimes.read(absPath)
|
||||
}
|
||||
|
||||
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`;
|
||||
const output = result;
|
||||
const result = `Patch applied successfully. ${changedFiles.length} files changed, ${totalAdditions} additions, ${totalRemovals} removals`
|
||||
const output = result
|
||||
|
||||
return {
|
||||
metadata: {
|
||||
@@ -415,6 +415,6 @@ export const patch = Tool.define({
|
||||
removals: totalRemovals,
|
||||
} satisfies PatchResponseMetadata,
|
||||
output,
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { tool, type Tool as AITool } from "ai";
|
||||
import { Log } from "../util/log";
|
||||
import { tool, type Tool as AITool } from "ai"
|
||||
import { Log } from "../util/log"
|
||||
|
||||
const log = Log.create({ service: "tool" });
|
||||
const log = Log.create({ service: "tool" })
|
||||
|
||||
export namespace Tool {
|
||||
export interface Metadata<
|
||||
Properties extends Record<string, any> = Record<string, any>,
|
||||
> {
|
||||
properties: Properties;
|
||||
properties: Properties
|
||||
time: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
start: number
|
||||
end: number
|
||||
}
|
||||
}
|
||||
export function define<
|
||||
Params,
|
||||
@@ -19,7 +19,7 @@ export namespace Tool {
|
||||
Name extends string,
|
||||
>(
|
||||
input: AITool<Params, Output> & {
|
||||
name: Name;
|
||||
name: Name
|
||||
},
|
||||
) {
|
||||
return tool({
|
||||
@@ -29,33 +29,33 @@ export namespace Tool {
|
||||
id: opts.toolCallId,
|
||||
name: input.name,
|
||||
...params,
|
||||
});
|
||||
})
|
||||
try {
|
||||
const start = Date.now();
|
||||
const result = await input.execute!(params, opts);
|
||||
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(),
|
||||
});
|
||||
})
|
||||
return {
|
||||
metadata: {
|
||||
error: true,
|
||||
},
|
||||
output: "An error occurred: " + e.toString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { App } from "../../app/app";
|
||||
import { App } from "../../app/app"
|
||||
|
||||
export namespace FileTimes {
|
||||
export const state = App.state("tool.filetimes", () => ({
|
||||
read: new Map<string, Date>(),
|
||||
write: new Map<string, Date>(),
|
||||
}));
|
||||
}))
|
||||
|
||||
export function read(filePath: string) {
|
||||
state().read.set(filePath, new Date());
|
||||
state().read.set(filePath, new Date())
|
||||
}
|
||||
|
||||
export function write(filePath: string) {
|
||||
state().write.set(filePath, new Date());
|
||||
state().write.set(filePath, new Date())
|
||||
}
|
||||
|
||||
export function get(filePath: string): Date | null {
|
||||
return state().read.get(filePath) || null;
|
||||
return state().read.get(filePath) || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Tool } from "./tool";
|
||||
import { LSP } from "../lsp";
|
||||
import { FileTimes } from "./util/file-times";
|
||||
import { z } from "zod"
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { Tool } from "./tool"
|
||||
import { LSP } from "../lsp"
|
||||
import { FileTimes } from "./util/file-times"
|
||||
|
||||
const MAX_READ_SIZE = 250 * 1024;
|
||||
const DEFAULT_READ_LIMIT = 2000;
|
||||
const MAX_LINE_LENGTH = 2000;
|
||||
const MAX_READ_SIZE = 250 * 1024
|
||||
const DEFAULT_READ_LIMIT = 2000
|
||||
const MAX_LINE_LENGTH = 2000
|
||||
|
||||
const DESCRIPTION = `File viewing tool that reads and displays the contents of files with line numbers, allowing you to examine code, logs, or text data.
|
||||
|
||||
@@ -38,7 +38,7 @@ LIMITATIONS:
|
||||
TIPS:
|
||||
- Use with Glob tool to first find files you want to view
|
||||
- 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`;
|
||||
- When viewing large files, use the offset parameter to read specific sections`
|
||||
|
||||
export const view = Tool.define({
|
||||
name: "opencode.view",
|
||||
@@ -55,17 +55,17 @@ export const view = Tool.define({
|
||||
.optional(),
|
||||
}),
|
||||
async execute(params) {
|
||||
let filePath = params.filePath;
|
||||
let filePath = params.filePath
|
||||
if (!path.isAbsolute(filePath)) {
|
||||
filePath = path.join(process.cwd(), filePath);
|
||||
filePath = path.join(process.cwd(), filePath)
|
||||
}
|
||||
|
||||
const file = Bun.file(filePath);
|
||||
const file = Bun.file(filePath)
|
||||
if (!(await file.exists())) {
|
||||
const dir = path.dirname(filePath);
|
||||
const base = path.basename(filePath);
|
||||
const dir = path.dirname(filePath)
|
||||
const base = path.basename(filePath)
|
||||
|
||||
const dirEntries = fs.readdirSync(dir);
|
||||
const dirEntries = fs.readdirSync(dir)
|
||||
const suggestions = dirEntries
|
||||
.filter(
|
||||
(entry) =>
|
||||
@@ -73,80 +73,80 @@ export const view = Tool.define({
|
||||
base.toLowerCase().includes(entry.toLowerCase()),
|
||||
)
|
||||
.map((entry) => path.join(dir, entry))
|
||||
.slice(0, 3);
|
||||
.slice(0, 3)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
throw new Error(
|
||||
`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
throw new Error(`File not found: ${filePath}`);
|
||||
throw new Error(`File not found: ${filePath}`)
|
||||
}
|
||||
const stats = await file.stat();
|
||||
const stats = await file.stat()
|
||||
|
||||
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);
|
||||
)
|
||||
const limit = params.limit ?? DEFAULT_READ_LIMIT
|
||||
const offset = params.offset || 0
|
||||
const isImage = isImageFile(filePath)
|
||||
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 lines = await file.text().then((text) => text.split("\n"))
|
||||
const raw = lines.slice(offset, offset + limit).map((line) => {
|
||||
return line.length > MAX_LINE_LENGTH
|
||||
? line.substring(0, MAX_LINE_LENGTH) + "..."
|
||||
: line;
|
||||
});
|
||||
: line
|
||||
})
|
||||
const content = raw.map((line, index) => {
|
||||
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`;
|
||||
});
|
||||
const preview = raw.slice(0, 20).join("\n");
|
||||
return `${(index + offset + 1).toString().padStart(5, "0")}| ${line}`
|
||||
})
|
||||
const preview = raw.slice(0, 20).join("\n")
|
||||
|
||||
let output = "<file>\n";
|
||||
output += content.join("\n");
|
||||
let output = "<file>\n"
|
||||
output += content.join("\n")
|
||||
|
||||
if (lines.length > offset + content.length) {
|
||||
output += `\n\n(File has more lines. Use 'offset' parameter to read beyond line ${
|
||||
offset + content.length
|
||||
})`;
|
||||
})`
|
||||
}
|
||||
output += "\n</file>";
|
||||
output += "\n</file>"
|
||||
|
||||
// just warms the lsp client
|
||||
LSP.file(filePath);
|
||||
FileTimes.read(filePath);
|
||||
LSP.file(filePath)
|
||||
FileTimes.read(filePath)
|
||||
|
||||
return {
|
||||
output,
|
||||
metadata: {
|
||||
preview,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function isImageFile(filePath: string): string | false {
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const ext = path.extname(filePath).toLowerCase()
|
||||
switch (ext) {
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
return "JPEG";
|
||||
return "JPEG"
|
||||
case ".png":
|
||||
return "PNG";
|
||||
return "PNG"
|
||||
case ".gif":
|
||||
return "GIF";
|
||||
return "GIF"
|
||||
case ".bmp":
|
||||
return "BMP";
|
||||
return "BMP"
|
||||
case ".svg":
|
||||
return "SVG";
|
||||
return "SVG"
|
||||
case ".webp":
|
||||
return "WebP";
|
||||
return "WebP"
|
||||
default:
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user