write.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import z from "zod"
  2. import * as path from "path"
  3. import { Tool } from "./tool"
  4. import { LSP } from "../lsp"
  5. import { Permission } from "../permission"
  6. import DESCRIPTION from "./write.txt"
  7. import { Bus } from "../bus"
  8. import { File } from "../file"
  9. import { FileTime } from "../file/time"
  10. import { Filesystem } from "../util/filesystem"
  11. import { Instance } from "../project/instance"
  12. import { Agent } from "../agent/agent"
  13. export const WriteTool = Tool.define("write", {
  14. description: DESCRIPTION,
  15. parameters: z.object({
  16. content: z.string().describe("The content to write to the file"),
  17. filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"),
  18. }),
  19. async execute(params, ctx) {
  20. const agent = await Agent.get(ctx.agent)
  21. const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath)
  22. if (!Filesystem.contains(Instance.directory, filepath)) {
  23. const parentDir = path.dirname(filepath)
  24. if (agent.permission.external_directory === "ask") {
  25. await Permission.ask({
  26. type: "external_directory",
  27. pattern: [parentDir, path.join(parentDir, "*")],
  28. sessionID: ctx.sessionID,
  29. messageID: ctx.messageID,
  30. callID: ctx.callID,
  31. title: `Write file outside working directory: ${filepath}`,
  32. metadata: {
  33. filepath,
  34. parentDir,
  35. },
  36. })
  37. } else if (agent.permission.external_directory === "deny") {
  38. throw new Permission.RejectedError(
  39. ctx.sessionID,
  40. "external_directory",
  41. ctx.callID,
  42. {
  43. filepath: filepath,
  44. parentDir,
  45. },
  46. `File ${filepath} is not in the current working directory`,
  47. )
  48. }
  49. }
  50. const file = Bun.file(filepath)
  51. const exists = await file.exists()
  52. if (exists) await FileTime.assert(ctx.sessionID, filepath)
  53. if (agent.permission.edit === "ask")
  54. await Permission.ask({
  55. type: "write",
  56. sessionID: ctx.sessionID,
  57. messageID: ctx.messageID,
  58. callID: ctx.callID,
  59. title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
  60. metadata: {
  61. filePath: filepath,
  62. content: params.content,
  63. exists,
  64. },
  65. })
  66. await Bun.write(filepath, params.content)
  67. await Bus.publish(File.Event.Edited, {
  68. file: filepath,
  69. })
  70. FileTime.read(ctx.sessionID, filepath)
  71. let output = ""
  72. await LSP.touchFile(filepath, true)
  73. const diagnostics = await LSP.diagnostics()
  74. for (const [file, issues] of Object.entries(diagnostics)) {
  75. if (issues.length === 0) continue
  76. if (file === filepath) {
  77. output += `\nThis file has errors, please fix\n<file_diagnostics>\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</file_diagnostics>\n`
  78. continue
  79. }
  80. output += `\n<project_diagnostics>\n${file}\n${issues.map(LSP.Diagnostic.pretty).join("\n")}\n</project_diagnostics>\n`
  81. }
  82. return {
  83. title: path.relative(Instance.worktree, filepath),
  84. metadata: {
  85. diagnostics,
  86. filepath,
  87. exists: exists,
  88. },
  89. output,
  90. }
  91. },
  92. })