summaryrefslogtreecommitdiffhomepage
path: root/packages/core/src/tools
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/tools')
-rw-r--r--packages/core/src/tools/list-files.ts38
-rw-r--r--packages/core/src/tools/read-file.ts33
-rw-r--r--packages/core/src/tools/registry.ts35
-rw-r--r--packages/core/src/tools/write-file.ts33
4 files changed, 139 insertions, 0 deletions
diff --git a/packages/core/src/tools/list-files.ts b/packages/core/src/tools/list-files.ts
new file mode 100644
index 0000000..360ac98
--- /dev/null
+++ b/packages/core/src/tools/list-files.ts
@@ -0,0 +1,38 @@
+import { readdir } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { z } from "zod";
+import type { ToolDefinition } from "../types/index.js";
+
+export function createListFilesTool(workingDirectory: string): ToolDefinition {
+ return {
+ name: "list_files",
+ description: "List files and directories at a path relative to the working directory.",
+ parameters: z.object({
+ path: z
+ .string()
+ .optional()
+ .describe("Path to list, relative to the working directory. Defaults to '.'"),
+ }),
+ execute: async (args: Record<string, unknown>): Promise<string> => {
+ const relPath = (args.path as string | undefined) ?? ".";
+ const absolutePath = resolve(join(workingDirectory, relPath));
+ const absoluteWorkDir = resolve(workingDirectory);
+
+ if (!absolutePath.startsWith(`${absoluteWorkDir}/`) && absolutePath !== absoluteWorkDir) {
+ return `Error: Path "${relPath}" is outside the working directory.`;
+ }
+
+ try {
+ const entries = await readdir(absolutePath, { withFileTypes: true });
+ if (entries.length === 0) {
+ return "(empty directory)";
+ }
+ return entries
+ .map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
+ .join("\n");
+ } catch (err) {
+ return `Error listing files: ${err instanceof Error ? err.message : String(err)}`;
+ }
+ },
+ };
+}
diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts
new file mode 100644
index 0000000..476f243
--- /dev/null
+++ b/packages/core/src/tools/read-file.ts
@@ -0,0 +1,33 @@
+import { readFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import { z } from "zod";
+import type { ToolDefinition } from "../types/index.js";
+
+export function createReadFileTool(workingDirectory: string): ToolDefinition {
+ return {
+ name: "read_file",
+ description: "Read the contents of a file relative to the working directory.",
+ parameters: z.object({
+ path: z.string().describe("Path to the file, relative to the working directory"),
+ }),
+ execute: async (args: Record<string, unknown>): Promise<string> => {
+ const filePath = args.path as string;
+ const absolutePath = resolve(join(workingDirectory, filePath));
+ const absoluteWorkDir = resolve(workingDirectory);
+
+ if (!absolutePath.startsWith(`${absoluteWorkDir}/`) && absolutePath !== absoluteWorkDir) {
+ return `Error: Path "${filePath}" is outside the working directory.`;
+ }
+
+ try {
+ return await readFile(absolutePath, "utf8");
+ } catch (err) {
+ const code = (err as NodeJS.ErrnoException).code;
+ if (code === "ENOENT") {
+ return `Error: File "${filePath}" not found.`;
+ }
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
+ }
+ },
+ };
+}
diff --git a/packages/core/src/tools/registry.ts b/packages/core/src/tools/registry.ts
new file mode 100644
index 0000000..4699c93
--- /dev/null
+++ b/packages/core/src/tools/registry.ts
@@ -0,0 +1,35 @@
+import { tool } from "ai";
+import { z } from "zod";
+import type { ToolDefinition } from "../types/index.js";
+
+export function createToolRegistry(tools: ToolDefinition[]) {
+ const toolMap = new Map<string, ToolDefinition>(tools.map((t) => [t.name, t]));
+
+ return {
+ getTools(): ToolDefinition[] {
+ return [...toolMap.values()];
+ },
+
+ getTool(name: string): ToolDefinition | undefined {
+ return toolMap.get(name);
+ },
+
+ getAISDKTools() {
+ const result: Record<string, ReturnType<typeof tool>> = {};
+ for (const [name, def] of toolMap) {
+ const schema = def.parameters;
+ const t = tool({
+ description: def.description,
+ parameters: schema instanceof z.ZodObject ? schema : z.object({}),
+ execute: async (args) => {
+ return def.execute(args as Record<string, unknown>);
+ },
+ });
+ // The AI SDK tool() overloads cause type narrowing issues when
+ // execute is provided. The runtime value is correct.
+ result[name] = t as unknown as ReturnType<typeof tool>;
+ }
+ return result;
+ },
+ };
+}
diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts
new file mode 100644
index 0000000..23bc72a
--- /dev/null
+++ b/packages/core/src/tools/write-file.ts
@@ -0,0 +1,33 @@
+import { mkdir, writeFile } from "node:fs/promises";
+import { dirname, join, resolve } from "node:path";
+import { z } from "zod";
+import type { ToolDefinition } from "../types/index.js";
+
+export function createWriteFileTool(workingDirectory: string): ToolDefinition {
+ return {
+ name: "write_file",
+ description: "Write content to a file relative to the working directory.",
+ parameters: z.object({
+ path: z.string().describe("Path to the file, relative to the working directory"),
+ content: z.string().describe("Content to write to the file"),
+ }),
+ execute: async (args: Record<string, unknown>): Promise<string> => {
+ const filePath = args.path as string;
+ const content = args.content as string;
+ const absolutePath = resolve(join(workingDirectory, filePath));
+ const absoluteWorkDir = resolve(workingDirectory);
+
+ if (!absolutePath.startsWith(`${absoluteWorkDir}/`) && absolutePath !== absoluteWorkDir) {
+ return `Error: Path "${filePath}" is outside the working directory.`;
+ }
+
+ try {
+ await mkdir(dirname(absolutePath), { recursive: true });
+ await writeFile(absolutePath, content, "utf8");
+ return `Successfully wrote to "${filePath}".`;
+ } catch (err) {
+ return `Error writing file: ${err instanceof Error ? err.message : String(err)}`;
+ }
+ },
+ };
+}