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-29 23:04:13 +00:00
import { FileDiff } from "@opencode-ai/schema/file-diff"
2026-07-03 13:03:53 +00:00
import { Effect , Schema } from "effect"
2026-07-29 23:04:13 +00:00
import { createTwoFilesPatch , diffLines } from "diff"
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 ( {
2026-07-29 23:04:13 +00:00
description : "Path to the file to write to" ,
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 :
2026-07-29 23:04:13 +00:00
"Writes a file to the local filesystem, overwriting if one exists.\n\nMissing parent directories are created automatically.\n\nUse this tool to create new files or overwrite existing files. For partial changes, use the edit tool instead." ,
2026-07-06 17:36:19 +00:00
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-07-29 23:04:13 +00:00
const current = yield * Bom . readFile ( fs , target . canonical ) . pipe (
Effect . catchReason ( "PlatformError" , "NotFound" , ( ) = > Effect . succeed ( undefined ) ) ,
)
const next = Bom . split ( input . content )
const counts = diffLines ( current ? . text ? ? "" , next . text ) . reduce (
( result , item ) = > ( {
additions : result.additions + ( item . added ? ( item . count ? ? 0 ) : 0 ) ,
deletions : result.deletions + ( item . removed ? ( item . count ? ? 0 ) : 0 ) ,
} ) ,
{ additions : 0 , deletions : 0 } ,
)
const preview : typeof FileDiff . Info . Type = {
file : target.resource ,
patch : createTwoFilesPatch ( target . resource , target . resource , current ? . text ? ? "" , next . text ) ,
status : current ? "modified" : "added" ,
. . . counts ,
}
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-07-29 23:04:13 +00:00
metadata : { files : [ preview ] } ,
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
}