diff --git a/modules/programs/opencode.nix b/modules/programs/opencode.nix index 02270b7ac..4cae5b72a 100644 --- a/modules/programs/opencode.nix +++ b/modules/programs/opencode.nix @@ -264,6 +264,49 @@ in See for the documentation. ''; }; + + tools = lib.mkOption { + type = lib.types.either (lib.types.attrsOf (lib.types.either lib.types.lines lib.types.path)) lib.types.path; + default = { }; + description = '' + Custom tools for opencode. + + This option can either be: + - An attribute set defining tools + - A path to a directory containing multiple tool files + + If an attribute set is used, the attribute name becomes the tool filename, + and the value is either: + - Inline content as a string (creates `opencode/tool/.ts`) + - A path to a file (creates `opencode/tool/.ts` or `opencode/tool/.js`) + + If a path is used, it is expected to contain tool files. + The directory is symlinked to {file}`$XDG_CONFIG_HOME/opencode/tool/`. + + See for the documentation. + ''; + example = lib.literalExpression '' + { + database-query = ''' + import { tool } from "@opencode-ai/plugin" + + export default tool({ + description: "Query the project database", + args: { + query: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args) { + // Your database logic here + return `Executed query: ''${args.query}` + }, + }) + '''; + + # Or reference an existing file + api-client = ./tools/api-client.ts; + } + ''; + }; }; config = mkIf cfg.enable { @@ -276,6 +319,10 @@ in assertion = !lib.isPath cfg.agents || lib.pathIsDirectory cfg.agents; message = "`programs.opencode.agents` must be a directory when set to a path"; } + { + assertion = !lib.isPath cfg.tools || lib.pathIsDirectory cfg.tools; + message = "`programs.opencode.tools` must be a directory when set to a path"; + } { assertion = !lib.isPath cfg.skills || lib.pathIsDirectory cfg.skills; message = "`programs.opencode.skills` must be a directory when set to a path"; @@ -325,6 +372,11 @@ in recursive = true; }; + "opencode/tool" = mkIf (lib.isPath cfg.tools) { + source = cfg.tools; + recursive = true; + }; + "opencode/skill" = mkIf (lib.isPath cfg.skills) { source = cfg.skills; recursive = true; @@ -351,6 +403,14 @@ in ) ) cfg.agents ) + // lib.optionalAttrs (builtins.isAttrs cfg.tools) ( + lib.mapAttrs' ( + name: content: + lib.nameValuePair "opencode/tool/${name}.ts" ( + if lib.isPath content then { source = content; } else { text = content; } + ) + ) cfg.tools + ) // lib.mapAttrs' ( name: content: if lib.isPath content && lib.pathIsDirectory content then diff --git a/tests/modules/programs/opencode/api-client-tool.ts b/tests/modules/programs/opencode/api-client-tool.ts new file mode 100644 index 000000000..6d2991d9a --- /dev/null +++ b/tests/modules/programs/opencode/api-client-tool.ts @@ -0,0 +1,12 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Make API requests to external services", + args: { + endpoint: tool.schema.string().describe("API endpoint to call"), + method: tool.schema.string().describe("HTTP method"), + }, + async execute(args) { + return `Called ${args.method} ${args.endpoint}` + }, +}) diff --git a/tests/modules/programs/opencode/database-query-tool.ts b/tests/modules/programs/opencode/database-query-tool.ts new file mode 100644 index 000000000..30069eb2b --- /dev/null +++ b/tests/modules/programs/opencode/database-query-tool.ts @@ -0,0 +1,11 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Query the project database", + args: { + query: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args) { + return `Executed query: ${args.query}` + }, +}) diff --git a/tests/modules/programs/opencode/default.nix b/tests/modules/programs/opencode/default.nix index c347ca1d3..cd88c35ef 100644 --- a/tests/modules/programs/opencode/default.nix +++ b/tests/modules/programs/opencode/default.nix @@ -10,6 +10,9 @@ opencode-commands-path = ./commands-path.nix; opencode-agents-bulk-directory = ./agents-bulk-directory.nix; opencode-commands-bulk-directory = ./commands-bulk-directory.nix; + opencode-tools-inline = ./tools-inline.nix; + opencode-tools-path = ./tools-path.nix; + opencode-tools-bulk-directory = ./tools-bulk-directory.nix; opencode-mixed-content = ./mixed-content.nix; opencode-skills-inline = ./skills-inline.nix; opencode-skills-path = ./skills-path.nix; diff --git a/tests/modules/programs/opencode/mixed-content.nix b/tests/modules/programs/opencode/mixed-content.nix index 8668b531c..fb6c36494 100644 --- a/tests/modules/programs/opencode/mixed-content.nix +++ b/tests/modules/programs/opencode/mixed-content.nix @@ -15,6 +15,22 @@ ''; path-agent = ./test-agent.md; }; + tools = { + inline-tool = '' + import { tool } from "@opencode-ai/plugin" + + export default tool({ + description: "Inline tool definition", + args: { + input: tool.schema.string().describe("Test input"), + }, + async execute(args) { + return `Processed: ''${args.input}` + }, + }) + ''; + path-tool = ./test-tool.ts; + }; skills = { inline-skill = '' --- @@ -54,6 +70,13 @@ assertFileContent home-files/.config/opencode/agent/path-agent.md \ ${./test-agent.md} + # Tools + assertFileExists home-files/.config/opencode/tool/inline-tool.ts + assertFileExists home-files/.config/opencode/tool/path-tool.ts + + assertFileContent home-files/.config/opencode/tool/path-tool.ts \ + ${./test-tool.ts} + # Skills assertFileExists home-files/.config/opencode/skill/inline-skill/SKILL.md assertFileExists home-files/.config/opencode/skill/path-skill/SKILL.md diff --git a/tests/modules/programs/opencode/test-tool.ts b/tests/modules/programs/opencode/test-tool.ts new file mode 100644 index 000000000..6b9a15ed2 --- /dev/null +++ b/tests/modules/programs/opencode/test-tool.ts @@ -0,0 +1,11 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Test tool for unit testing", + args: { + input: tool.schema.string().describe("Test input parameter"), + }, + async execute(args) { + return `Processed: ${args.input}` + }, +}) diff --git a/tests/modules/programs/opencode/tools-bulk-directory.nix b/tests/modules/programs/opencode/tools-bulk-directory.nix new file mode 100644 index 000000000..112debcb6 --- /dev/null +++ b/tests/modules/programs/opencode/tools-bulk-directory.nix @@ -0,0 +1,15 @@ +{ + programs.opencode = { + enable = true; + tools = ./tools-bulk; + }; + + nmt.script = '' + assertFileExists home-files/.config/opencode/tool/database-query.ts + assertFileExists home-files/.config/opencode/tool/api-client.ts + assertFileContent home-files/.config/opencode/tool/database-query.ts \ + ${./tools-bulk/database-query.ts} + assertFileContent home-files/.config/opencode/tool/api-client.ts \ + ${./tools-bulk/api-client.ts} + ''; +} diff --git a/tests/modules/programs/opencode/tools-bulk/api-client.ts b/tests/modules/programs/opencode/tools-bulk/api-client.ts new file mode 100644 index 000000000..6d2991d9a --- /dev/null +++ b/tests/modules/programs/opencode/tools-bulk/api-client.ts @@ -0,0 +1,12 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Make API requests to external services", + args: { + endpoint: tool.schema.string().describe("API endpoint to call"), + method: tool.schema.string().describe("HTTP method"), + }, + async execute(args) { + return `Called ${args.method} ${args.endpoint}` + }, +}) diff --git a/tests/modules/programs/opencode/tools-bulk/database-query.ts b/tests/modules/programs/opencode/tools-bulk/database-query.ts new file mode 100644 index 000000000..30069eb2b --- /dev/null +++ b/tests/modules/programs/opencode/tools-bulk/database-query.ts @@ -0,0 +1,11 @@ +import { tool } from "@opencode-ai/plugin" + +export default tool({ + description: "Query the project database", + args: { + query: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args) { + return `Executed query: ${args.query}` + }, +}) diff --git a/tests/modules/programs/opencode/tools-inline.nix b/tests/modules/programs/opencode/tools-inline.nix new file mode 100644 index 000000000..27df961fc --- /dev/null +++ b/tests/modules/programs/opencode/tools-inline.nix @@ -0,0 +1,42 @@ +{ + programs.opencode = { + enable = true; + tools = { + database-query = '' + import { tool } from "@opencode-ai/plugin" + + export default tool({ + description: "Query the project database", + args: { + query: tool.schema.string().describe("SQL query to execute"), + }, + async execute(args) { + return `Executed query: ''${args.query}` + }, + }) + ''; + api-client = '' + import { tool } from "@opencode-ai/plugin" + + export default tool({ + description: "Make API requests to external services", + args: { + endpoint: tool.schema.string().describe("API endpoint to call"), + method: tool.schema.string().describe("HTTP method"), + }, + async execute(args) { + return `Called ''${args.method} ''${args.endpoint}` + }, + }) + ''; + }; + }; + nmt.script = '' + assertFileExists home-files/.config/opencode/tool/database-query.ts + assertFileExists home-files/.config/opencode/tool/api-client.ts + assertFileContent home-files/.config/opencode/tool/database-query.ts \ + ${./database-query-tool.ts} + assertFileContent home-files/.config/opencode/tool/api-client.ts \ + ${./api-client-tool.ts} + ''; +} diff --git a/tests/modules/programs/opencode/tools-path.nix b/tests/modules/programs/opencode/tools-path.nix new file mode 100644 index 000000000..57d0bdee2 --- /dev/null +++ b/tests/modules/programs/opencode/tools-path.nix @@ -0,0 +1,13 @@ +{ + programs.opencode = { + enable = true; + tools = { + test-tool = ./test-tool.ts; + }; + }; + nmt.script = '' + assertFileExists home-files/.config/opencode/tool/test-tool.ts + assertFileContent home-files/.config/opencode/tool/test-tool.ts \ + ${./test-tool.ts} + ''; +}