2026-06-04 03:02:17 +00:00
/ * *
2026-07-27 00:08:55 +00:00
* Model - facing file - write leaf . Relative paths resolve within the active
2026-06-04 03:02:17 +00:00
* Location . Absolute paths inside that Location are accepted , while explicit
* absolute external paths retain mutation capability through a separate
2026-06-09 16:08:58 +00:00
* external_directory approval before edit approval .
2026-06-04 03:02:17 +00:00
* /
export * as WriteTool from "./write"
2026-07-27 00:08:55 +00:00
import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
2026-07-15 19:10:38 +00:00
import { ToolFailure } from "@opencode-ai/ai"
2026-07-03 13:03:53 +00:00
import { Effect , Schema } from "effect"
2026-07-29 21:23:04 +00:00
import { Bom } from "@opencode-ai/util/bom"
import { FSUtil } from "@opencode-ai/util/fs-util"
2026-07-27 00:08:55 +00:00
import { FileMutation } from "../../file-mutation"
2026-07-29 21:23:04 +00:00
import { Formatter } from "../../formatter"
2026-07-27 00:08:55 +00:00
import { LocationMutation } from "../../location-mutation"
import { Permission } from "../../permission"
2026-06-04 03:02:17 +00:00
export const name = "write"
// TODO: Revisit whether model-facing mutation schemas should prefer absolute `filePath` naming for trained-in compatibility after evaluating model behavior.
2026-06-07 01:55:34 +00:00
export const Input = Schema . Struct ( {
2026-06-04 03:02:17 +00:00
path : Schema.String.annotate ( {
description :
2026-06-09 16:08:58 +00:00
"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." ,
2026-06-04 03:02:17 +00:00
} ) ,
content : Schema.String.annotate ( { description : "Content to write to the file" } ) ,
} )
2026-06-07 01:55:34 +00:00
export const Output = Schema . Struct ( {
2026-06-04 03:02:17 +00:00
operation : Schema.Literal ( "write" ) ,
target : Schema.String ,
resource : Schema.String ,
existed : Schema.Boolean ,
} )
2026-06-07 01:55:34 +00:00
export type Output = typeof Output . Type
2026-06-04 03:02:17 +00:00
2026-06-07 01:55:34 +00:00
export const toModelOutput = ( output : Output ) = >
2026-06-04 03:02:17 +00:00
` ${ output . existed ? "Wrote" : "Created" } file successfully: ${ output . resource } `
2026-07-27 00:08:55 +00:00
/** Deferred write UX integrations remain visible at the model-facing seam. */
// TODO: Publish watcher/file-edit events after watcher integration exists.
2026-06-04 03:02:17 +00:00
// TODO: Add snapshots / undo after design exists.
2026-07-27 00:08:55 +00:00
// TODO: Add LSP notification and diagnostics after LSP runtime exists.
2026-06-04 03:02:17 +00:00
2026-07-03 13:03:53 +00:00
export const Plugin = {
2026-07-05 19:51:15 +00:00
id : "opencode.tool.write" ,
2026-07-03 13:03:53 +00:00
effect : Effect.fn ( "WriteTool.Plugin" ) ( function * ( ctx : PluginContext ) {
2026-06-04 03:02:17 +00:00
const mutation = yield * LocationMutation . Service
const files = yield * FileMutation . Service
2026-07-29 21:23:04 +00:00
const formatter = yield * Formatter . Service
const fs = yield * FSUtil . Service
2026-07-27 00:08:55 +00:00
const permission = yield * Permission . Service
2026-06-04 03:02:17 +00:00
2026-07-03 13:03:53 +00:00
yield * ctx . tool
2026-07-06 17:36:19 +00:00
. transform ( ( draft ) = >
draft . add (
2026-07-27 00:08:55 +00:00
( {
name ,
options : { codemode : false , permission : "edit" } ,
2026-07-06 17:36:19 +00:00
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." ,
input : Input ,
output : Output ,
execute : ( input , context ) = >
Effect . gen ( function * ( ) {
const source = {
type : "tool" as const ,
2026-07-14 19:57:16 +00:00
messageID : context.messageID ,
callID : context.callID ,
2026-07-06 17:36:19 +00:00
}
const target = yield * mutation . resolve ( { path : input.path , kind : "file" } )
const external = target . externalDirectory
if ( external )
yield * permission . assert ( {
. . . LocationMutation . externalDirectoryPermission ( external ) ,
sessionID : context.sessionID ,
agent : context.agent ,
source ,
} )
2026-06-07 00:49:12 +00:00
yield * permission . assert ( {
2026-07-06 17:36:19 +00:00
action : "edit" ,
resources : [ target . resource ] ,
save : [ "*" ] ,
2026-06-07 00:49:12 +00:00
sessionID : context.sessionID ,
agent : context.agent ,
source ,
} )
2026-07-29 21:23:04 +00:00
const result = yield * files . writeTextPreservingBom ( { target , content : input.content } )
const bom = ( yield * Bom . readFile ( fs , target . canonical ) ) . bom
if ( yield * formatter . file ( target . canonical ) ) yield * Bom . syncFile ( fs , target . canonical , bom )
return result
2026-07-06 22:14:06 +00:00
} ) . pipe (
2026-07-23 21:13:31 +00:00
Effect . map ( ( output ) = > ( { output , content : toModelOutput ( output ) } ) ) ,
2026-07-06 22:14:06 +00:00
Effect . mapError ( ( error ) = > new ToolFailure ( { message : ` Unable to write ${ input . path } ` , error } ) ) ,
) ,
2026-07-06 17:36:19 +00:00
} ) ,
2026-06-07 00:49:12 +00:00
) ,
2026-07-06 17:36:19 +00:00
)
2026-06-07 00:49:12 +00:00
. pipe ( Effect . orDie )
2026-06-04 03:02:17 +00:00
} ) ,
2026-07-03 13:03:53 +00:00
}