summaryrefslogtreecommitdiffhomepage
path: root/packages/tool-write-file
diff options
context:
space:
mode:
Diffstat (limited to 'packages/tool-write-file')
-rw-r--r--packages/tool-write-file/package.json20
-rw-r--r--packages/tool-write-file/src/extension.ts32
-rw-r--r--packages/tool-write-file/src/index.ts6
-rw-r--r--packages/tool-write-file/src/write-file.test.ts420
-rw-r--r--packages/tool-write-file/src/write-file.ts258
-rw-r--r--packages/tool-write-file/tsconfig.json8
6 files changed, 372 insertions, 372 deletions
diff --git a/packages/tool-write-file/package.json b/packages/tool-write-file/package.json
index 63c2ccd..beb7c07 100644
--- a/packages/tool-write-file/package.json
+++ b/packages/tool-write-file/package.json
@@ -1,12 +1,12 @@
{
- "name": "@dispatch/tool-write-file",
- "version": "0.0.0",
- "type": "module",
- "private": true,
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "dependencies": {
- "@dispatch/kernel": "workspace:*",
- "@dispatch/exec-backend": "workspace:*"
- }
+ "name": "@dispatch/tool-write-file",
+ "version": "0.0.0",
+ "type": "module",
+ "private": true,
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "dependencies": {
+ "@dispatch/kernel": "workspace:*",
+ "@dispatch/exec-backend": "workspace:*"
+ }
}
diff --git a/packages/tool-write-file/src/extension.ts b/packages/tool-write-file/src/extension.ts
index 0a9a10f..36f7148 100644
--- a/packages/tool-write-file/src/extension.ts
+++ b/packages/tool-write-file/src/extension.ts
@@ -3,20 +3,20 @@ import type { Extension } from "@dispatch/kernel";
import { createWriteFileTool } from "./write-file.js";
export const extension: Extension = {
- manifest: {
- id: "tool-write-file",
- name: "Write File Tool",
- version: "0.0.0",
- apiVersion: "^0.1.0",
- trust: "bundled",
- activation: "eager",
- capabilities: { fs: true },
- contributes: { tools: ["write_file"] },
- // Host activates exec-backend first → host.getService at activation is safe.
- dependsOn: ["exec-backend"],
- },
- activate(host) {
- const resolveBackend = host.getService(execBackendHandle);
- host.defineTool(createWriteFileTool({ resolveBackend, workdir: process.cwd() }));
- },
+ manifest: {
+ id: "tool-write-file",
+ name: "Write File Tool",
+ version: "0.0.0",
+ apiVersion: "^0.1.0",
+ trust: "bundled",
+ activation: "eager",
+ capabilities: { fs: true },
+ contributes: { tools: ["write_file"] },
+ // Host activates exec-backend first → host.getService at activation is safe.
+ dependsOn: ["exec-backend"],
+ },
+ activate(host) {
+ const resolveBackend = host.getService(execBackendHandle);
+ host.defineTool(createWriteFileTool({ resolveBackend, workdir: process.cwd() }));
+ },
};
diff --git a/packages/tool-write-file/src/index.ts b/packages/tool-write-file/src/index.ts
index 8a2cb36..d81c90a 100644
--- a/packages/tool-write-file/src/index.ts
+++ b/packages/tool-write-file/src/index.ts
@@ -1,6 +1,6 @@
export { extension } from "./extension.js";
export {
- createWriteFileTool,
- decideOverwrite,
- validateArgs,
+ createWriteFileTool,
+ decideOverwrite,
+ validateArgs,
} from "./write-file.js";
diff --git a/packages/tool-write-file/src/write-file.test.ts b/packages/tool-write-file/src/write-file.test.ts
index d157eb2..5999291 100644
--- a/packages/tool-write-file/src/write-file.test.ts
+++ b/packages/tool-write-file/src/write-file.test.ts
@@ -7,17 +7,17 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createWriteFileTool, decideOverwrite, validateArgs } from "./write-file.js";
function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext {
- return {
- toolCallId: "test-call-1",
- onOutput: () => {},
- signal: AbortSignal.timeout(5000),
- log: createLogger(
- { extensionId: "test" },
- { emit: () => {} },
- { now: () => 0, newId: () => "id" },
- ),
- ...overrides,
- };
+ return {
+ toolCallId: "test-call-1",
+ onOutput: () => {},
+ signal: AbortSignal.timeout(5000),
+ log: createLogger(
+ { extensionId: "test" },
+ { emit: () => {} },
+ { now: () => 0, newId: () => "id" },
+ ),
+ ...overrides,
+ };
}
/**
@@ -26,220 +26,220 @@ function stubCtx(overrides?: Partial<ToolExecuteContext>): ToolExecuteContext {
* real fs edge is exercised, matching the constitution's strict-core rule.
*/
function makeTool(workdir: string) {
- return createWriteFileTool({ resolveBackend: () => localExecBackend, workdir });
+ return createWriteFileTool({ resolveBackend: () => localExecBackend, workdir });
}
let workdir: string;
beforeEach(async () => {
- workdir = await mkdtemp(join(tmpdir(), "tool-write-file-test-"));
+ workdir = await mkdtemp(join(tmpdir(), "tool-write-file-test-"));
});
afterEach(async () => {
- await rm(workdir, { recursive: true, force: true });
+ await rm(workdir, { recursive: true, force: true });
});
describe("decideOverwrite", () => {
- it("returns create when file absent and overwrite is false", () => {
- expect(decideOverwrite(false, false)).toBe("create");
- });
-
- it("returns create when file absent and overwrite is false (default)", () => {
- expect(decideOverwrite(false, false)).toBe("create");
- });
-
- it("returns error when file exists and overwrite is false", () => {
- const result = decideOverwrite(true, false);
- expect(typeof result).toBe("object");
- if (typeof result === "object") {
- expect(result.error).toContain("already exists");
- }
- });
-
- it("returns overwrite when file exists and overwrite is true", () => {
- expect(decideOverwrite(true, true)).toBe("overwrite");
- });
-
- it("returns error when file absent and overwrite is true", () => {
- const result = decideOverwrite(false, true);
- expect(typeof result).toBe("object");
- if (typeof result === "object") {
- expect(result.error).toContain("does not exist");
- }
- });
-
- it("covers all four rows of the truth table", () => {
- expect(decideOverwrite(false, false)).toBe("create");
- expect(decideOverwrite(true, false)).toEqual(
- expect.objectContaining({ error: expect.any(String) }),
- );
- expect(decideOverwrite(true, true)).toBe("overwrite");
- expect(decideOverwrite(false, true)).toEqual(
- expect.objectContaining({ error: expect.any(String) }),
- );
- });
+ it("returns create when file absent and overwrite is false", () => {
+ expect(decideOverwrite(false, false)).toBe("create");
+ });
+
+ it("returns create when file absent and overwrite is false (default)", () => {
+ expect(decideOverwrite(false, false)).toBe("create");
+ });
+
+ it("returns error when file exists and overwrite is false", () => {
+ const result = decideOverwrite(true, false);
+ expect(typeof result).toBe("object");
+ if (typeof result === "object") {
+ expect(result.error).toContain("already exists");
+ }
+ });
+
+ it("returns overwrite when file exists and overwrite is true", () => {
+ expect(decideOverwrite(true, true)).toBe("overwrite");
+ });
+
+ it("returns error when file absent and overwrite is true", () => {
+ const result = decideOverwrite(false, true);
+ expect(typeof result).toBe("object");
+ if (typeof result === "object") {
+ expect(result.error).toContain("does not exist");
+ }
+ });
+
+ it("covers all four rows of the truth table", () => {
+ expect(decideOverwrite(false, false)).toBe("create");
+ expect(decideOverwrite(true, false)).toEqual(
+ expect.objectContaining({ error: expect.any(String) }),
+ );
+ expect(decideOverwrite(true, true)).toBe("overwrite");
+ expect(decideOverwrite(false, true)).toEqual(
+ expect.objectContaining({ error: expect.any(String) }),
+ );
+ });
});
describe("validateArgs", () => {
- it("returns validated args for valid input", () => {
- const result = validateArgs({ path: "foo.txt", content: "hello" });
- expect(result).toEqual({ path: "foo.txt", content: "hello", overwrite: false });
- });
-
- it("parses overwrite as true", () => {
- const result = validateArgs({ path: "foo.txt", content: "x", overwrite: true });
- expect(result).toEqual({ path: "foo.txt", content: "x", overwrite: true });
- });
-
- it("defaults overwrite to false", () => {
- const result = validateArgs({ path: "foo.txt", content: "x" });
- expect(result).toEqual({ path: "foo.txt", content: "x", overwrite: false });
- });
-
- it("accepts empty string content", () => {
- const result = validateArgs({ path: "foo.txt", content: "" });
- expect(result).toEqual({ path: "foo.txt", content: "", overwrite: false });
- });
-
- it("returns error for null args", () => {
- expect(validateArgs(null)).toHaveProperty("error");
- });
-
- it("returns error for missing path", () => {
- expect(validateArgs({ content: "x" })).toHaveProperty("error");
- });
-
- it("returns error for missing content", () => {
- expect(validateArgs({ path: "foo.txt" })).toHaveProperty("error");
- });
-
- it("returns error for non-string content", () => {
- expect(validateArgs({ path: "foo.txt", content: 123 })).toHaveProperty("error");
- });
-
- it("returns error for non-boolean overwrite", () => {
- expect(validateArgs({ path: "foo.txt", content: "x", overwrite: "yes" })).toHaveProperty(
- "error",
- );
- });
+ it("returns validated args for valid input", () => {
+ const result = validateArgs({ path: "foo.txt", content: "hello" });
+ expect(result).toEqual({ path: "foo.txt", content: "hello", overwrite: false });
+ });
+
+ it("parses overwrite as true", () => {
+ const result = validateArgs({ path: "foo.txt", content: "x", overwrite: true });
+ expect(result).toEqual({ path: "foo.txt", content: "x", overwrite: true });
+ });
+
+ it("defaults overwrite to false", () => {
+ const result = validateArgs({ path: "foo.txt", content: "x" });
+ expect(result).toEqual({ path: "foo.txt", content: "x", overwrite: false });
+ });
+
+ it("accepts empty string content", () => {
+ const result = validateArgs({ path: "foo.txt", content: "" });
+ expect(result).toEqual({ path: "foo.txt", content: "", overwrite: false });
+ });
+
+ it("returns error for null args", () => {
+ expect(validateArgs(null)).toHaveProperty("error");
+ });
+
+ it("returns error for missing path", () => {
+ expect(validateArgs({ content: "x" })).toHaveProperty("error");
+ });
+
+ it("returns error for missing content", () => {
+ expect(validateArgs({ path: "foo.txt" })).toHaveProperty("error");
+ });
+
+ it("returns error for non-string content", () => {
+ expect(validateArgs({ path: "foo.txt", content: 123 })).toHaveProperty("error");
+ });
+
+ it("returns error for non-boolean overwrite", () => {
+ expect(validateArgs({ path: "foo.txt", content: "x", overwrite: "yes" })).toHaveProperty(
+ "error",
+ );
+ });
});
describe("createWriteFileTool", () => {
- it("creates a new file when overwrite is unset and the file is absent", async () => {
- const tool = makeTool(workdir);
- const result = await tool.execute({ path: "new-file.txt", content: "hello world" }, stubCtx());
-
- expect(result.isError).toBeUndefined();
- expect(result.content).toContain("Created");
- const written = await readFile(join(workdir, "new-file.txt"), "utf8");
- expect(written).toBe("hello world");
- });
-
- it("errors when the file exists and overwrite is unset", async () => {
- await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
-
- const tool = makeTool(workdir);
- const result = await tool.execute({ path: "existing.txt", content: "new content" }, stubCtx());
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("already exists");
- expect(result.content).toContain("overwrite");
- const unchanged = await readFile(join(workdir, "existing.txt"), "utf8");
- expect(unchanged).toBe("old content");
- });
-
- it("overwrites an existing file when overwrite is true", async () => {
- await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
-
- const tool = makeTool(workdir);
- const result = await tool.execute(
- { path: "existing.txt", content: "new content", overwrite: true },
- stubCtx(),
- );
-
- expect(result.isError).toBeUndefined();
- expect(result.content).toContain("Overwrote");
- const written = await readFile(join(workdir, "existing.txt"), "utf8");
- expect(written).toBe("new content");
- });
-
- it("errors when overwrite is true but the file is absent", async () => {
- const tool = makeTool(workdir);
- const result = await tool.execute(
- { path: "nonexistent.txt", content: "data", overwrite: true },
- stubCtx(),
- );
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("does not exist");
- });
-
- it("errors when the parent directory does not exist", async () => {
- const tool = makeTool(workdir);
- const result = await tool.execute({ path: "no/such/dir/file.txt", content: "data" }, stubCtx());
-
- expect(result.isError).toBe(true);
- expect(result.content).toContain("Error");
- });
-
- it("concurrencySafe is false", () => {
- const tool = makeTool(workdir);
- expect(tool.concurrencySafe).toBe(false);
- });
-
- it("has correct name and parameters shape", () => {
- const tool = makeTool(workdir);
- expect(tool.name).toBe("write_file");
- expect(tool.parameters.type).toBe("object");
- expect(tool.parameters.required).toEqual(["path", "content"]);
- expect(tool.parameters.properties?.path?.type).toBe("string");
- expect(tool.parameters.properties?.content?.type).toBe("string");
- expect(tool.parameters.properties?.overwrite?.type).toBe("boolean");
- });
-
- it("never throws on bad input (always returns ToolResult)", async () => {
- const tool = makeTool(workdir);
- const inputs = [null, undefined, 42, "string", {}, { path: "" }, { path: 123 }];
- for (const input of inputs) {
- const result = await tool.execute(input, stubCtx());
- expect(result).toHaveProperty("content");
- expect(typeof result.content).toBe("string");
- }
- });
-
- it("respects ctx.cwd over baked workdir", async () => {
- const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-"));
- try {
- const tool = makeTool(workdir);
- const result = await tool.execute(
- { path: "ctx-file.txt", content: "from ctx" },
- stubCtx({ cwd: ctxDir }),
- );
-
- expect(result.isError).toBeUndefined();
- const written = await readFile(join(ctxDir, "ctx-file.txt"), "utf8");
- expect(written).toBe("from ctx");
- } finally {
- await rm(ctxDir, { recursive: true, force: true });
- }
- });
-
- it("writes empty content", async () => {
- const tool = makeTool(workdir);
- const result = await tool.execute({ path: "empty.txt", content: "" }, stubCtx());
-
- expect(result.isError).toBeUndefined();
- const written = await readFile(join(workdir, "empty.txt"), "utf8");
- expect(written).toBe("");
- });
-
- it("writes content in subdirectory that exists", async () => {
- await mkdir(join(workdir, "sub"));
- const tool = makeTool(workdir);
- const result = await tool.execute({ path: "sub/file.txt", content: "nested" }, stubCtx());
-
- expect(result.isError).toBeUndefined();
- const written = await readFile(join(workdir, "sub", "file.txt"), "utf8");
- expect(written).toBe("nested");
- });
+ it("creates a new file when overwrite is unset and the file is absent", async () => {
+ const tool = makeTool(workdir);
+ const result = await tool.execute({ path: "new-file.txt", content: "hello world" }, stubCtx());
+
+ expect(result.isError).toBeUndefined();
+ expect(result.content).toContain("Created");
+ const written = await readFile(join(workdir, "new-file.txt"), "utf8");
+ expect(written).toBe("hello world");
+ });
+
+ it("errors when the file exists and overwrite is unset", async () => {
+ await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
+
+ const tool = makeTool(workdir);
+ const result = await tool.execute({ path: "existing.txt", content: "new content" }, stubCtx());
+
+ expect(result.isError).toBe(true);
+ expect(result.content).toContain("already exists");
+ expect(result.content).toContain("overwrite");
+ const unchanged = await readFile(join(workdir, "existing.txt"), "utf8");
+ expect(unchanged).toBe("old content");
+ });
+
+ it("overwrites an existing file when overwrite is true", async () => {
+ await writeFile(join(workdir, "existing.txt"), "old content", "utf8");
+
+ const tool = makeTool(workdir);
+ const result = await tool.execute(
+ { path: "existing.txt", content: "new content", overwrite: true },
+ stubCtx(),
+ );
+
+ expect(result.isError).toBeUndefined();
+ expect(result.content).toContain("Overwrote");
+ const written = await readFile(join(workdir, "existing.txt"), "utf8");
+ expect(written).toBe("new content");
+ });
+
+ it("errors when overwrite is true but the file is absent", async () => {
+ const tool = makeTool(workdir);
+ const result = await tool.execute(
+ { path: "nonexistent.txt", content: "data", overwrite: true },
+ stubCtx(),
+ );
+
+ expect(result.isError).toBe(true);
+ expect(result.content).toContain("does not exist");
+ });
+
+ it("errors when the parent directory does not exist", async () => {
+ const tool = makeTool(workdir);
+ const result = await tool.execute({ path: "no/such/dir/file.txt", content: "data" }, stubCtx());
+
+ expect(result.isError).toBe(true);
+ expect(result.content).toContain("Error");
+ });
+
+ it("concurrencySafe is false", () => {
+ const tool = makeTool(workdir);
+ expect(tool.concurrencySafe).toBe(false);
+ });
+
+ it("has correct name and parameters shape", () => {
+ const tool = makeTool(workdir);
+ expect(tool.name).toBe("write_file");
+ expect(tool.parameters.type).toBe("object");
+ expect(tool.parameters.required).toEqual(["path", "content"]);
+ expect(tool.parameters.properties?.path?.type).toBe("string");
+ expect(tool.parameters.properties?.content?.type).toBe("string");
+ expect(tool.parameters.properties?.overwrite?.type).toBe("boolean");
+ });
+
+ it("never throws on bad input (always returns ToolResult)", async () => {
+ const tool = makeTool(workdir);
+ const inputs = [null, undefined, 42, "string", {}, { path: "" }, { path: 123 }];
+ for (const input of inputs) {
+ const result = await tool.execute(input, stubCtx());
+ expect(result).toHaveProperty("content");
+ expect(typeof result.content).toBe("string");
+ }
+ });
+
+ it("respects ctx.cwd over baked workdir", async () => {
+ const ctxDir = await mkdtemp(join(tmpdir(), "ctx-cwd-test-"));
+ try {
+ const tool = makeTool(workdir);
+ const result = await tool.execute(
+ { path: "ctx-file.txt", content: "from ctx" },
+ stubCtx({ cwd: ctxDir }),
+ );
+
+ expect(result.isError).toBeUndefined();
+ const written = await readFile(join(ctxDir, "ctx-file.txt"), "utf8");
+ expect(written).toBe("from ctx");
+ } finally {
+ await rm(ctxDir, { recursive: true, force: true });
+ }
+ });
+
+ it("writes empty content", async () => {
+ const tool = makeTool(workdir);
+ const result = await tool.execute({ path: "empty.txt", content: "" }, stubCtx());
+
+ expect(result.isError).toBeUndefined();
+ const written = await readFile(join(workdir, "empty.txt"), "utf8");
+ expect(written).toBe("");
+ });
+
+ it("writes content in subdirectory that exists", async () => {
+ await mkdir(join(workdir, "sub"));
+ const tool = makeTool(workdir);
+ const result = await tool.execute({ path: "sub/file.txt", content: "nested" }, stubCtx());
+
+ expect(result.isError).toBeUndefined();
+ const written = await readFile(join(workdir, "sub", "file.txt"), "utf8");
+ expect(written).toBe("nested");
+ });
});
diff --git a/packages/tool-write-file/src/write-file.ts b/packages/tool-write-file/src/write-file.ts
index cf761b6..14a5ec4 100644
--- a/packages/tool-write-file/src/write-file.ts
+++ b/packages/tool-write-file/src/write-file.ts
@@ -3,51 +3,51 @@ import type { ExecBackend, ExecBackendResolver } from "@dispatch/exec-backend";
import type { ToolContract, ToolResult } from "@dispatch/kernel";
interface ValidatedArgs {
- readonly path: string;
- readonly content: string;
- readonly overwrite: boolean;
+ readonly path: string;
+ readonly content: string;
+ readonly overwrite: boolean;
}
export type OverwriteDecision = "create" | "overwrite" | { readonly error: string };
/** Pure: decide the action based on file existence and the overwrite flag. */
export function decideOverwrite(fileExists: boolean, overwrite: boolean): OverwriteDecision {
- if (!fileExists && !overwrite) return "create";
- if (fileExists && !overwrite) {
- return { error: "Error: File already exists; set overwrite: true to replace it." };
- }
- if (fileExists && overwrite) return "overwrite";
- return { error: "Error: overwrite: true but the file does not exist." };
+ if (!fileExists && !overwrite) return "create";
+ if (fileExists && !overwrite) {
+ return { error: "Error: File already exists; set overwrite: true to replace it." };
+ }
+ if (fileExists && overwrite) return "overwrite";
+ return { error: "Error: overwrite: true but the file does not exist." };
}
/** Pure: validate and coerce args from the model. */
export function validateArgs(args: unknown): ValidatedArgs | { readonly error: string } {
- if (args === null || args === undefined || typeof args !== "object") {
- return { error: "Error: Arguments must be an object." };
- }
- const obj = args as Record<string, unknown>;
-
- const rawPath = obj.path;
- if (typeof rawPath !== "string" || rawPath.length === 0) {
- return { error: 'Error: Missing or invalid "path" parameter (must be a non-empty string).' };
- }
-
- const rawContent = obj.content;
- if (typeof rawContent !== "string") {
- return {
- error: 'Error: Missing or invalid "content" parameter (must be a string).',
- };
- }
-
- let overwrite = false;
- if (obj.overwrite !== undefined) {
- if (typeof obj.overwrite !== "boolean") {
- return { error: 'Error: Invalid "overwrite" parameter (must be a boolean).' };
- }
- overwrite = obj.overwrite;
- }
-
- return { path: rawPath, content: rawContent, overwrite };
+ if (args === null || args === undefined || typeof args !== "object") {
+ return { error: "Error: Arguments must be an object." };
+ }
+ const obj = args as Record<string, unknown>;
+
+ const rawPath = obj.path;
+ if (typeof rawPath !== "string" || rawPath.length === 0) {
+ return { error: 'Error: Missing or invalid "path" parameter (must be a non-empty string).' };
+ }
+
+ const rawContent = obj.content;
+ if (typeof rawContent !== "string") {
+ return {
+ error: 'Error: Missing or invalid "content" parameter (must be a string).',
+ };
+ }
+
+ let overwrite = false;
+ if (obj.overwrite !== undefined) {
+ if (typeof obj.overwrite !== "boolean") {
+ return { error: 'Error: Invalid "overwrite" parameter (must be a boolean).' };
+ }
+ overwrite = obj.overwrite;
+ }
+
+ return { path: rawPath, content: rawContent, overwrite };
}
/**
@@ -62,99 +62,99 @@ export function validateArgs(args: unknown): ValidatedArgs | { readonly error: s
* injected so the tool is testable; `execute` prefers `ctx.cwd` when present.
*/
export function createWriteFileTool(deps: {
- readonly resolveBackend: ExecBackendResolver;
- readonly workdir?: string;
+ readonly resolveBackend: ExecBackendResolver;
+ readonly workdir?: string;
}): ToolContract {
- const workdir = deps.workdir !== undefined ? resolve(deps.workdir) : undefined;
-
- return {
- name: "write_file",
- description:
- "Write a whole file to disk. " +
- "By default, creates a new file; errors if it already exists. " +
- "Set overwrite: true to replace an existing file (errors if the file does not exist). " +
- "Parent directories are NOT auto-created — the parent must already exist.",
- parameters: {
- type: "object",
- properties: {
- path: {
- type: "string",
- description: "Path to the file, relative to the working directory.",
- },
- content: {
- type: "string",
- description: "The full content to write to the file.",
- },
- overwrite: {
- type: "boolean",
- description:
- "When false/unset: creates a new file (errors if it already exists). " +
- "When true: replaces an existing file (errors if it does not exist).",
- default: false,
- },
- },
- required: ["path", "content"],
- },
- concurrencySafe: false,
- async execute(args: unknown, ctx): Promise<ToolResult> {
- const validated = validateArgs(args);
- if ("error" in validated) {
- return { content: validated.error, isError: true };
- }
-
- const { path: relPath, content, overwrite } = validated;
-
- const effectiveBase = ctx.cwd ? resolve(ctx.cwd) : workdir;
- if (effectiveBase === undefined) {
- return {
- content:
- "Error: No working directory (neither ctx.cwd nor a baked workdir was provided).",
- isError: true,
- };
- }
- const resolvedPath = resolve(effectiveBase, relPath);
-
- const backend: ExecBackend = deps.resolveBackend(ctx.computerId);
-
- // Check existence. `backend.exists` never throws — it returns false
- // when the path is missing — so the old try/catch around `access`
- // collapses to a single boolean read.
- const fileExists = await backend.exists(resolvedPath);
-
- // Pure decision.
- const decision = decideOverwrite(fileExists, overwrite);
- if (typeof decision === "object") {
- return { content: decision.error, isError: true };
- }
-
- // Verify it's not a directory. `backend.stat` returns a
- // `{ isFile, isDirectory }` result; only reached when the file
- // exists, so an ENOENT here is a lost race left to propagate
- // (same as the prior uncaught `stat` call).
- if (fileExists) {
- const pathStat = await backend.stat(resolvedPath);
- if (pathStat.isDirectory) {
- return {
- content: `Error: "${relPath}" is a directory, not a file.`,
- isError: true,
- };
- }
- }
-
- // Write the file. LocalExecBackend throws node:fs-style errors
- // carrying a `.code` (e.g. ENOENT when the parent dir is missing);
- // the catch surfaces the message verbatim.
- try {
- await backend.writeFile(resolvedPath, content);
- } catch (err: unknown) {
- return {
- content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
- isError: true,
- };
- }
-
- const action = decision === "create" ? "Created" : "Overwrote";
- return { content: `${action} "${relPath}" (${content.length} bytes).` };
- },
- };
+ const workdir = deps.workdir !== undefined ? resolve(deps.workdir) : undefined;
+
+ return {
+ name: "write_file",
+ description:
+ "Write a whole file to disk. " +
+ "By default, creates a new file; errors if it already exists. " +
+ "Set overwrite: true to replace an existing file (errors if the file does not exist). " +
+ "Parent directories are NOT auto-created — the parent must already exist.",
+ parameters: {
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ description: "Path to the file, relative to the working directory.",
+ },
+ content: {
+ type: "string",
+ description: "The full content to write to the file.",
+ },
+ overwrite: {
+ type: "boolean",
+ description:
+ "When false/unset: creates a new file (errors if it already exists). " +
+ "When true: replaces an existing file (errors if it does not exist).",
+ default: false,
+ },
+ },
+ required: ["path", "content"],
+ },
+ concurrencySafe: false,
+ async execute(args: unknown, ctx): Promise<ToolResult> {
+ const validated = validateArgs(args);
+ if ("error" in validated) {
+ return { content: validated.error, isError: true };
+ }
+
+ const { path: relPath, content, overwrite } = validated;
+
+ const effectiveBase = ctx.cwd ? resolve(ctx.cwd) : workdir;
+ if (effectiveBase === undefined) {
+ return {
+ content:
+ "Error: No working directory (neither ctx.cwd nor a baked workdir was provided).",
+ isError: true,
+ };
+ }
+ const resolvedPath = resolve(effectiveBase, relPath);
+
+ const backend: ExecBackend = deps.resolveBackend(ctx.computerId);
+
+ // Check existence. `backend.exists` never throws — it returns false
+ // when the path is missing — so the old try/catch around `access`
+ // collapses to a single boolean read.
+ const fileExists = await backend.exists(resolvedPath);
+
+ // Pure decision.
+ const decision = decideOverwrite(fileExists, overwrite);
+ if (typeof decision === "object") {
+ return { content: decision.error, isError: true };
+ }
+
+ // Verify it's not a directory. `backend.stat` returns a
+ // `{ isFile, isDirectory }` result; only reached when the file
+ // exists, so an ENOENT here is a lost race left to propagate
+ // (same as the prior uncaught `stat` call).
+ if (fileExists) {
+ const pathStat = await backend.stat(resolvedPath);
+ if (pathStat.isDirectory) {
+ return {
+ content: `Error: "${relPath}" is a directory, not a file.`,
+ isError: true,
+ };
+ }
+ }
+
+ // Write the file. LocalExecBackend throws node:fs-style errors
+ // carrying a `.code` (e.g. ENOENT when the parent dir is missing);
+ // the catch surfaces the message verbatim.
+ try {
+ await backend.writeFile(resolvedPath, content);
+ } catch (err: unknown) {
+ return {
+ content: `Error writing file: ${err instanceof Error ? err.message : String(err)}`,
+ isError: true,
+ };
+ }
+
+ const action = decision === "create" ? "Created" : "Overwrote";
+ return { content: `${action} "${relPath}" (${content.length} bytes).` };
+ },
+ };
}
diff --git a/packages/tool-write-file/tsconfig.json b/packages/tool-write-file/tsconfig.json
index 30cdc4d..b790281 100644
--- a/packages/tool-write-file/tsconfig.json
+++ b/packages/tool-write-file/tsconfig.json
@@ -1,6 +1,6 @@
{
- "extends": "../../tsconfig.base.json",
- "compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true },
- "include": ["src/**/*.ts"],
- "references": [{ "path": "../kernel" }, { "path": "../exec-backend" }]
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true },
+ "include": ["src/**/*.ts"],
+ "references": [{ "path": "../kernel" }, { "path": "../exec-backend" }]
}