opencode/packages/core/src/tool/write.ts
2026-06-04 23:12:17 -04:00

78 lines
3.5 KiB
TypeScript

/**
* Model-facing V2 file-write leaf. Relative paths resolve within the active
* Location. Absolute paths inside that Location are accepted, while explicit
* absolute external paths retain mutation capability through a separate
* external_directory approval before edit approval. Named project references
* are read-oriented and deliberately are not accepted by mutation tools.
*/
export * as WriteTool from "./write"
import { Tool, ToolFailure, toolText } from "@opencode-ai/llm"
import { Cause, Effect, Layer, Schema } from "effect"
import { FileMutation } from "../file-mutation"
import { LocationMutation } from "../location-mutation"
import { ToolRegistry } from "./registry"
export const name = "write"
// TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
export const Parameters = Schema.Struct({
path: Schema.String.annotate({
description:
"File path to write. Relative paths resolve within the active Location. Absolute paths inside that Location are accepted; external absolute paths require external_directory approval. Named project references are read-oriented and are not accepted.",
}),
content: Schema.String.annotate({ description: "Content to write to the file" }),
})
export const Success = Schema.Struct({
operation: Schema.Literal("write"),
target: Schema.String,
resource: Schema.String,
existed: Schema.Boolean,
})
export type Success = typeof Success.Type
export const toModelOutput = (output: Success) =>
`${output.existed ? "Wrote" : "Created"} file successfully: ${output.resource}`
const definition = Tool.make({
description:
"Write content to one file. Relative paths resolve within the active Location. Absolute paths inside the Location are accepted. Explicit external absolute paths require external_directory approval before edit approval. Named project references are read-oriented and are not accepted.",
parameters: Parameters,
success: Success,
toModelOutput: ({ output }) => [toolText({ type: "text", text: toModelOutput(output) })],
})
/** Deferred V2 write UX integrations remain visible at the model-facing seam. */
// TODO: Add formatter integration after V2 formatter runtime exists.
// TODO: Publish watcher/file-edit events after V2 watcher integration exists.
// TODO: Add snapshots / undo after design exists.
// TODO: Add LSP notification and diagnostics after V2 LSP runtime exists.
export const layer = Layer.effectDiscard(
Effect.gen(function* () {
const registry = yield* ToolRegistry.Service
const mutation = yield* LocationMutation.Service
const files = yield* FileMutation.Service
yield* registry.contribute((editor) =>
editor.set(name, {
tool: definition,
execute: ({ parameters, assertPermission }) =>
Effect.gen(function* () {
const plan = yield* mutation.resolve({ path: parameters.path, kind: "file" })
const external = plan.target.externalDirectory
if (external) yield* assertPermission(LocationMutation.externalDirectoryPermission(external))
yield* assertPermission({ action: "edit", resources: [plan.target.resource], save: ["*"] })
return yield* files.writeTextPreservingBom({ plan, content: parameters.content })
}).pipe(
Effect.catchCause((cause) =>
Effect.fail(
new ToolFailure({ message: `Unable to write ${parameters.path}`, error: Cause.squash(cause) }),
),
),
),
}),
)
}),
)