core: cleaner error output and more flexible custom tool directories

- Removed debug console.log when dependency installation fails so users see clean warning messages instead of raw error dumps
- Fixed database connection cleanup to prevent resource leaks between sessions
- Added support for loading custom tools from both .opencode/tool (singular) and .opencode/tools (plural) directories, matching common naming conventions
This commit is contained in:
Dax Raad 2026-03-10 12:33:19 -04:00
commit 21e72cbf42
3 changed files with 112 additions and 42 deletions

View file

@ -288,7 +288,6 @@ export namespace Config {
// Install any additional dependencies defined in the package.json // Install any additional dependencies defined in the package.json
// This allows local plugins and custom tools to use external packages // This allows local plugins and custom tools to use external packages
await Npm.install(dir).catch((err: any) => { await Npm.install(dir).catch((err: any) => {
console.log(err)
log.warn("failed to install dependencies", { dir, error: err.message }) log.warn("failed to install dependencies", { dir, error: err.message })
}) })
} }

View file

@ -109,6 +109,7 @@ export namespace Database {
export function close() { export function close() {
Client().$client.close() Client().$client.close()
Client.reset()
} }
export type TxOrDb = Transaction | Client export type TxOrDb = Transaction | Client

View file

@ -5,48 +5,118 @@ import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
import { ToolRegistry } from "../../src/tool/registry" import { ToolRegistry } from "../../src/tool/registry"
test("loads tools with external dependencies without crashing", async () => { describe("tool.registry", () => {
await using tmp = await tmpdir({ test("loads tools from .opencode/tool (singular)", async () => {
init: async (dir) => { await using tmp = await tmpdir({
const opencodeDir = path.join(dir, ".opencode") init: async (dir) => {
await fs.mkdir(opencodeDir, { recursive: true }) const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const toolsDir = path.join(opencodeDir, "tools") const toolDir = path.join(opencodeDir, "tool")
await fs.mkdir(toolsDir, { recursive: true }) await fs.mkdir(toolDir, { recursive: true })
await Bun.write( await Bun.write(
path.join(opencodeDir, "package.json"), path.join(toolDir, "hello.ts"),
JSON.stringify({ [
name: "custom-tools", "export default {",
dependencies: { " description: 'hello tool',",
"@opencode-ai/plugin": "^0.0.0", " args: {},",
cowsay: "^1.6.0", " execute: async () => {",
}, " return 'hello world'",
}), " },",
) "}",
"",
].join("\n"),
)
},
})
await Bun.write( await Instance.provide({
path.join(toolsDir, "cowsay.ts"), directory: tmp.path,
[ fn: async () => {
"import { say } from 'cowsay'", const ids = await ToolRegistry.ids()
"export default {", expect(ids).toContain("hello")
" description: 'tool that imports cowsay at top level',", },
" args: { text: { type: 'string' } },", })
" execute: async ({ text }: { text: string }) => {",
" return say({ text })",
" },",
"}",
"",
].join("\n"),
)
},
}) })
await Instance.provide({ test("loads tools from .opencode/tools (plural)", async () => {
directory: tmp.path, await using tmp = await tmpdir({
fn: async () => { init: async (dir) => {
const ids = await ToolRegistry.ids() const opencodeDir = path.join(dir, ".opencode")
expect(ids).toContain("cowsay") await fs.mkdir(opencodeDir, { recursive: true })
},
const toolsDir = path.join(opencodeDir, "tools")
await fs.mkdir(toolsDir, { recursive: true })
await Bun.write(
path.join(toolsDir, "hello.ts"),
[
"export default {",
" description: 'hello tool',",
" args: {},",
" execute: async () => {",
" return 'hello world'",
" },",
"}",
"",
].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello")
},
})
})
test("loads tools with external dependencies without crashing", async () => {
await using tmp = await tmpdir({
init: async (dir) => {
const opencodeDir = path.join(dir, ".opencode")
await fs.mkdir(opencodeDir, { recursive: true })
const toolsDir = path.join(opencodeDir, "tools")
await fs.mkdir(toolsDir, { recursive: true })
await Bun.write(
path.join(opencodeDir, "package.json"),
JSON.stringify({
name: "custom-tools",
dependencies: {
"@opencode-ai/plugin": "^0.0.0",
cowsay: "^1.6.0",
},
}),
)
await Bun.write(
path.join(toolsDir, "cowsay.ts"),
[
"import { say } from 'cowsay'",
"export default {",
" description: 'tool that imports cowsay at top level',",
" args: { text: { type: 'string' } },",
" execute: async ({ text }: { text: string }) => {",
" return say({ text })",
" },",
"}",
"",
].join("\n"),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("cowsay")
},
})
}) })
}) })