registry.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { QuestionTool } from "./question"
  2. import { BashTool } from "./bash"
  3. import { EditTool } from "./edit"
  4. import { GlobTool } from "./glob"
  5. import { GrepTool } from "./grep"
  6. import { BatchTool } from "./batch"
  7. import { ReadTool } from "./read"
  8. import { TaskTool } from "./task"
  9. import { TodoWriteTool, TodoReadTool } from "./todo"
  10. import { WebFetchTool } from "./webfetch"
  11. import { WriteTool } from "./write"
  12. import { InvalidTool } from "./invalid"
  13. import { SkillTool } from "./skill"
  14. import type { Agent } from "../agent/agent"
  15. import { Tool } from "./tool"
  16. import { Instance } from "../project/instance"
  17. import { Config } from "../config/config"
  18. import path from "path"
  19. import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
  20. import z from "zod"
  21. import { Plugin } from "../plugin"
  22. import { WebSearchTool } from "./websearch"
  23. import { CodeSearchTool } from "./codesearch"
  24. import { Flag } from "@/flag/flag"
  25. import { Log } from "@/util/log"
  26. import { LspTool } from "./lsp"
  27. import { Truncate } from "./truncation"
  28. import { PlanExitTool, PlanEnterTool } from "./plan"
  29. import { ApplyPatchTool } from "./apply_patch"
  30. export namespace ToolRegistry {
  31. const log = Log.create({ service: "tool.registry" })
  32. export const state = Instance.state(async () => {
  33. const custom = [] as Tool.Info[]
  34. const glob = new Bun.Glob("{tool,tools}/*.{js,ts}")
  35. for (const dir of await Config.directories()) {
  36. for await (const match of glob.scan({
  37. cwd: dir,
  38. absolute: true,
  39. followSymlinks: true,
  40. dot: true,
  41. })) {
  42. const namespace = path.basename(match, path.extname(match))
  43. const mod = await import(match)
  44. for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
  45. custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
  46. }
  47. }
  48. }
  49. const plugins = await Plugin.list()
  50. for (const plugin of plugins) {
  51. for (const [id, def] of Object.entries(plugin.tool ?? {})) {
  52. custom.push(fromPlugin(id, def))
  53. }
  54. }
  55. return { custom }
  56. })
  57. function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
  58. return {
  59. id,
  60. init: async (initCtx) => ({
  61. parameters: z.object(def.args),
  62. description: def.description,
  63. execute: async (args, ctx) => {
  64. const pluginCtx = {
  65. ...ctx,
  66. directory: Instance.directory,
  67. worktree: Instance.worktree,
  68. } as unknown as PluginToolContext
  69. const result = await def.execute(args as any, pluginCtx)
  70. const out = await Truncate.output(result, {}, initCtx?.agent)
  71. return {
  72. title: "",
  73. output: out.truncated ? out.content : result,
  74. metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
  75. }
  76. },
  77. }),
  78. }
  79. }
  80. export async function register(tool: Tool.Info) {
  81. const { custom } = await state()
  82. const idx = custom.findIndex((t) => t.id === tool.id)
  83. if (idx >= 0) {
  84. custom.splice(idx, 1, tool)
  85. return
  86. }
  87. custom.push(tool)
  88. }
  89. async function all(): Promise<Tool.Info[]> {
  90. const custom = await state().then((x) => x.custom)
  91. const config = await Config.get()
  92. return [
  93. InvalidTool,
  94. ...(["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) ? [QuestionTool] : []),
  95. BashTool,
  96. ReadTool,
  97. GlobTool,
  98. GrepTool,
  99. EditTool,
  100. WriteTool,
  101. TaskTool,
  102. WebFetchTool,
  103. TodoWriteTool,
  104. TodoReadTool,
  105. WebSearchTool,
  106. CodeSearchTool,
  107. SkillTool,
  108. ApplyPatchTool,
  109. ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [LspTool] : []),
  110. ...(config.experimental?.batch_tool === true ? [BatchTool] : []),
  111. ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [PlanExitTool, PlanEnterTool] : []),
  112. ...custom,
  113. ]
  114. }
  115. export async function ids() {
  116. return all().then((x) => x.map((t) => t.id))
  117. }
  118. export async function tools(
  119. model: {
  120. providerID: string
  121. modelID: string
  122. },
  123. agent?: Agent.Info,
  124. ) {
  125. const tools = await all()
  126. const result = await Promise.all(
  127. tools
  128. .filter((t) => {
  129. // Enable websearch/codesearch for zen users OR via enable flag
  130. if (t.id === "codesearch" || t.id === "websearch") {
  131. return model.providerID === "opencode" || Flag.OPENCODE_ENABLE_EXA
  132. }
  133. // use apply tool in same format as codex
  134. const usePatch =
  135. model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4")
  136. if (t.id === "apply_patch") return usePatch
  137. if (t.id === "edit" || t.id === "write") return !usePatch
  138. return true
  139. })
  140. .map(async (t) => {
  141. using _ = log.time(t.id)
  142. return {
  143. id: t.id,
  144. ...(await t.init({ agent })),
  145. }
  146. }),
  147. )
  148. return result
  149. }
  150. }