diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fd7f8aa72a5..2c512fa8407 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -378,6 +378,7 @@ export namespace SessionPrompt { ) let executionError: Error | undefined const taskAgent = await Agent.get(task.agent) + const toolLogger = Log.create({ service: "tool" }) const taskCtx: Tool.Context = { agent: task.agent, messageID: assistantMessage.id, @@ -400,6 +401,9 @@ export namespace SessionPrompt { ruleset: PermissionNext.merge(taskAgent.permission, session.permission ?? []), }) }, + log(level, message, extra) { + toolLogger[level](message, extra) + }, } const result = await taskTool.execute(taskArgs, taskCtx).catch((error) => { executionError = error @@ -640,6 +644,7 @@ export namespace SessionPrompt { }) { using _ = log.time("resolveTools") const tools: Record = {} + const toolLogger = Log.create({ service: "tool" }) const context = (args: any, options: ToolCallOptions): Tool.Context => ({ sessionID: input.session.id, @@ -673,6 +678,9 @@ export namespace SessionPrompt { ruleset: PermissionNext.merge(input.agent.permission, input.session.permission ?? []), }) }, + log(level, message, extra) { + toolLogger[level](message, extra) + }, }) for (const item of await ToolRegistry.tools(input.model.providerID)) { @@ -884,6 +892,7 @@ export namespace SessionPrompt { return pieces } const url = new URL(part.url) + const toolLogger = Log.create({ service: "tool" }) switch (url.protocol) { case "data:": if (part.mime === "text/plain") { @@ -983,6 +992,9 @@ export namespace SessionPrompt { extra: { bypassCwdCheck: true, model }, metadata: async () => {}, ask: async () => {}, + log(level, message, extra) { + toolLogger[level](message, extra) + }, } const result = await t.execute(args, readCtx) pieces.push({ @@ -1044,6 +1056,9 @@ export namespace SessionPrompt { extra: { bypassCwdCheck: true }, metadata: async () => {}, ask: async () => {}, + log(level, message, extra) { + toolLogger[level](message, extra) + }, } const result = await ListTool.init().then((t) => t.execute(args, listCtx)) return [ diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 434a3d42660..3695a3c92f7 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -21,6 +21,7 @@ export namespace Tool { extra?: { [key: string]: any } metadata(input: { title?: string; metadata?: M }): void ask(input: Omit): Promise + log(level: "debug" | "info" | "warn" | "error", message: string, extra?: Record): void } export interface Info { id: string diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 2eb17a9fc94..fc2b7da0e14 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -13,6 +13,7 @@ const ctx = { abort: AbortSignal.any([]), metadata: () => {}, ask: async () => {}, + log: () => {}, } const projectRoot = path.join(__dirname, "../..") diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index a79d931575c..efb8dbae01a 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -12,6 +12,7 @@ const ctx = { abort: AbortSignal.any([]), metadata: () => {}, ask: async () => {}, + log: () => {}, } const projectRoot = path.join(__dirname, "../..") diff --git a/packages/opencode/test/tool/patch.test.ts b/packages/opencode/test/tool/patch.test.ts index 3d3ec574e60..7e136cc2346 100644 --- a/packages/opencode/test/tool/patch.test.ts +++ b/packages/opencode/test/tool/patch.test.ts @@ -14,6 +14,7 @@ const ctx = { abort: AbortSignal.any([]), metadata: () => {}, ask: async () => {}, + log: () => {}, } const patchTool = await PatchTool.init() diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 826fa03f6ca..2d26cd32bf7 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -14,6 +14,7 @@ const ctx = { abort: AbortSignal.any([]), metadata: () => {}, ask: async () => {}, + log: () => {}, } describe("tool.read external_directory permission", () => { diff --git a/packages/plugin/src/tool.ts b/packages/plugin/src/tool.ts index 37e802ac408..5d29dd5d2a6 100644 --- a/packages/plugin/src/tool.ts +++ b/packages/plugin/src/tool.ts @@ -5,6 +5,7 @@ export type ToolContext = { messageID: string agent: string abort: AbortSignal + log(level: "debug" | "info" | "warn" | "error", message: string, extra?: Record): void } export function tool(input: { diff --git a/packages/web/src/content/docs/plugins.mdx b/packages/web/src/content/docs/plugins.mdx index bf26744f6c4..ac9344f028a 100644 --- a/packages/web/src/content/docs/plugins.mdx +++ b/packages/web/src/content/docs/plugins.mdx @@ -105,7 +105,12 @@ export const MyPlugin = async (ctx) => { ```js title=".opencode/plugin/example.js" export const MyPlugin = async ({ project, client, $, directory, worktree }) => { - console.log("Plugin initialized!") + // Use client.app.log() instead of console.log to avoid leaking output + await client.app.log({ + service: "my-plugin", + level: "info", + message: "Plugin initialized", + }) return { // Hook implementations go here @@ -290,7 +295,11 @@ Your custom tools will be available to opencode alongside built-in tools. ### Logging -Use `client.app.log()` instead of `console.log` for structured logging: +Use structured logging instead of `console.log` to avoid output leaking to stdout. + +#### In plugins + +Use `client.app.log()` for logging in plugin initialization and hooks: ```ts title=".opencode/plugin/my-plugin.ts" export const MyPlugin = async ({ client }) => { @@ -303,6 +312,29 @@ export const MyPlugin = async ({ client }) => { } ``` +#### In custom tools + +Use `ctx.log()` for logging in custom tool implementations: + +```ts title=".opencode/plugin/custom-tool.ts" +import { type Plugin, tool } from "@opencode-ai/plugin" + +export const CustomToolPlugin: Plugin = async (ctx) => { + return { + tool: { + mytool: tool({ + description: "A tool that logs", + args: { name: tool.schema.string() }, + async execute(args, ctx) { + ctx.log("info", "Tool executed", { name: args.name }) + return `Hello ${args.name}!` + }, + }), + }, + } +} +``` + Levels: `debug`, `info`, `warn`, `error`. See [SDK documentation](https://opencode.ai/docs/sdk) for details. ---