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}
+ '';
+}