diff --git a/.opencode/plugins/tools/git-worktree-create.ts b/.opencode/plugins/tools/git-worktree-create.ts new file mode 100644 index 0000000..b4c137f --- /dev/null +++ b/.opencode/plugins/tools/git-worktree-create.ts @@ -0,0 +1,22 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const gitWorktreeCreate: Info = { + name: "git-worktree-create", + description: "Create a git worktree for a branch or PR", + input: { + type: "object", + properties: { + branch: { type: "string", description: "Branch name or PR number" }, + }, + required: ["branch"], + additionalProperties: false, + }, + async execute(input) { + const { branch } = input as { branch: string } + const result = await $`git worktree add ../worktrees/${branch}`.text() + return { content: result.trim() } + }, +} + +export default gitWorktreeCreate diff --git a/.opencode/plugins/tools/index.ts b/.opencode/plugins/tools/index.ts new file mode 100644 index 0000000..f7f30ab --- /dev/null +++ b/.opencode/plugins/tools/index.ts @@ -0,0 +1,30 @@ +import { Plugin } from "@opencode/plugin" + +import gitWorktreeCreate from "./git-worktree-create.js" +import teaActionsRunsList from "./tea-actions-runs-list.js" +import teaActionsRunsLogs from "./tea-actions-runs-logs.js" +import teaComment from "./tea-comment.js" +import teaIssueCreate from "./tea-issue-create.js" +import teaIssueGet from "./tea-issue-get.js" +import teaIssues from "./tea-issues.js" +import teaPrCreate from "./tea-pr-create.js" +import teaPrGet from "./tea-pr-get.js" +import teaPulls from "./tea-pulls.js" + +export default Plugin.define({ + id: "tools", + async setup(ctx) { + await ctx.tool.transform((editor) => { + editor.add(gitWorktreeCreate) + editor.add(teaActionsRunsList) + editor.add(teaActionsRunsLogs) + editor.add(teaComment) + editor.add(teaIssueCreate) + editor.add(teaIssueGet) + editor.add(teaIssues) + editor.add(teaPrCreate) + editor.add(teaPrGet) + editor.add(teaPulls) + }) + }, +}) diff --git a/.opencode/plugins/tools/tea-actions-runs-list.ts b/.opencode/plugins/tools/tea-actions-runs-list.ts new file mode 100644 index 0000000..d3bc895 --- /dev/null +++ b/.opencode/plugins/tools/tea-actions-runs-list.ts @@ -0,0 +1,37 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaActionsRunsList: Info = { + name: "tea-actions-runs-list", + description: "List workflow runs for Gitea actions with optional filters", + input: { + type: "object", + properties: { + status: { type: "string", description: "Filter by status (success, failure, pending, queued, in_progress, skipped, canceled)" }, + branch: { type: "string", description: "Filter by branch name" }, + event: { type: "string", description: "Filter by event type (push, pull_request, etc.)" }, + limit: { type: "number", description: "Number of results to return (default: 30)" }, + since: { type: "string", description: "Show runs started after this time (e.g., '24h', '7d')" }, + }, + additionalProperties: false, + }, + async execute(input) { + const { status, branch, event, limit, since } = input as { + status?: string + branch?: string + event?: string + limit?: number + since?: string + } + let cmd = [`tea`, `actions`, `runs`, `list`, `-o`, `json`] + if (status) cmd = [...cmd, `--status`, status] + if (branch) cmd = [...cmd, `--branch`, branch] + if (event) cmd = [...cmd, `--event`, event] + if (limit) cmd = [...cmd, `--limit`, String(limit)] + if (since) cmd = [...cmd, `--since`, since] + const result = await $`${cmd}`.text() + return { content: result.trim() } + }, +} + +export default teaActionsRunsList diff --git a/.opencode/plugins/tools/tea-actions-runs-logs.ts b/.opencode/plugins/tools/tea-actions-runs-logs.ts new file mode 100644 index 0000000..7120681 --- /dev/null +++ b/.opencode/plugins/tools/tea-actions-runs-logs.ts @@ -0,0 +1,26 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaActionsRunsLogs: Info = { + name: "tea-actions-runs-logs", + description: "Get logs for a specific Gitea actions workflow run to diagnose failures", + input: { + type: "object", + properties: { + runId: { type: "number", description: "Workflow run ID to get logs for" }, + job: { type: "string", description: "Specific job ID to view (if omitted, shows all jobs)" }, + }, + required: ["runId"], + additionalProperties: false, + }, + async execute(input) { + const { runId, job } = input as { runId: number; job?: string } + let cmd = [`tea`, `actions`, `runs`, `logs`] + if (job) cmd = [...cmd, `--job`, job] + cmd = [...cmd, String(runId), `-o`, `simple`] + const result = await $`${cmd}`.text() + return { content: result.trim() } + }, +} + +export default teaActionsRunsLogs diff --git a/.opencode/plugins/tools/tea-comment.ts b/.opencode/plugins/tools/tea-comment.ts new file mode 100644 index 0000000..37db48f --- /dev/null +++ b/.opencode/plugins/tools/tea-comment.ts @@ -0,0 +1,23 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaComment: Info = { + name: "tea-comment", + description: "Add a comment to a specific issue using tea CLI", + input: { + type: "object", + properties: { + issueNumber: { type: "number", description: "Issue number/index" }, + body: { type: "string", description: "Comment body" }, + }, + required: ["issueNumber", "body"], + additionalProperties: false, + }, + async execute(input) { + const { issueNumber, body } = input as { issueNumber: number; body: string } + const result = await $`tea comment ${issueNumber} ${body}`.text() + return { content: result.trim() } + }, +} + +export default teaComment diff --git a/.opencode/plugins/tools/tea-issue-create.ts b/.opencode/plugins/tools/tea-issue-create.ts new file mode 100644 index 0000000..473ac39 --- /dev/null +++ b/.opencode/plugins/tools/tea-issue-create.ts @@ -0,0 +1,23 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaIssueCreate: Info = { + name: "tea-issue-create", + description: "Create a new issue in the GitHub repository using tea CLI", + input: { + type: "object", + properties: { + title: { type: "string", description: "Issue title" }, + description: { type: "string", description: "Issue description (supports markdown)" }, + }, + required: ["title"], + additionalProperties: false, + }, + async execute(input) { + const { title, description } = input as { title: string; description?: string } + const result = await $`tea issues create -t ${title} -d ${description}`.text() + return { content: result.trim() } + }, +} + +export default teaIssueCreate diff --git a/.opencode/plugins/tools/tea-issue-get.ts b/.opencode/plugins/tools/tea-issue-get.ts new file mode 100644 index 0000000..ad9bdd3 --- /dev/null +++ b/.opencode/plugins/tools/tea-issue-get.ts @@ -0,0 +1,22 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaIssueGet: Info = { + name: "tea-issue-get", + description: "Get a specific issue with all details and comments using tea CLI", + input: { + type: "object", + properties: { + issueNumber: { type: "number", description: "Issue number/index" }, + }, + required: ["issueNumber"], + additionalProperties: false, + }, + async execute(input) { + const { issueNumber } = input as { issueNumber: number } + const result = await $`tea issues ${issueNumber} --comments --output json`.text() + return { content: result.trim() } + }, +} + +export default teaIssueGet diff --git a/.opencode/plugins/tools/tea-issues.ts b/.opencode/plugins/tools/tea-issues.ts new file mode 100644 index 0000000..9bc3988 --- /dev/null +++ b/.opencode/plugins/tools/tea-issues.ts @@ -0,0 +1,18 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaIssues: Info = { + name: "tea-issues", + description: "List all issues in the project using tea CLI", + input: { + type: "object", + properties: {}, + additionalProperties: false, + }, + async execute() { + const result = await $`tea issues list`.text() + return { content: result.trim() } + }, +} + +export default teaIssues diff --git a/.opencode/plugins/tools/tea-pr-create.ts b/.opencode/plugins/tools/tea-pr-create.ts new file mode 100644 index 0000000..63abf06 --- /dev/null +++ b/.opencode/plugins/tools/tea-pr-create.ts @@ -0,0 +1,40 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaPrCreate: Info = { + name: "tea-pr-create", + description: "Create a pull request in the repository using tea CLI", + input: { + type: "object", + properties: { + title: { type: "string", description: "PR title" }, + head: { type: "string", description: "Branch name of the PR source (defaults to current branch)" }, + base: { type: "string", description: "Branch name of the PR target (defaults to repository default branch)" }, + description: { type: "string", description: "PR description (supports markdown)" }, + assignees: { type: "string", description: "Comma-separated list of usernames to assign" }, + labels: { type: "string", description: "Comma-separated list of labels to assign" }, + }, + required: ["title"], + additionalProperties: false, + }, + async execute(input) { + const { title, head, base, description, assignees, labels } = input as { + title: string + head?: string + base?: string + description?: string + assignees?: string + labels?: string + } + let cmd = [`tea`, `pulls`, `create`, `-t`, title] + if (head) cmd = [...cmd, `--head`, head] + if (base) cmd = [...cmd, `-b`, base] + if (description) cmd = [...cmd, `-d`, description] + if (assignees) cmd = [...cmd, `-a`, assignees] + if (labels) cmd = [...cmd, `-L`, labels] + const result = await $`${cmd}`.text() + return { content: result.trim() } + }, +} + +export default teaPrCreate diff --git a/.opencode/plugins/tools/tea-pr-get.ts b/.opencode/plugins/tools/tea-pr-get.ts new file mode 100644 index 0000000..a54014e --- /dev/null +++ b/.opencode/plugins/tools/tea-pr-get.ts @@ -0,0 +1,22 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaPrGet: Info = { + name: "tea-pr-get", + description: "Get a specific pull request with all details and comments using tea CLI", + input: { + type: "object", + properties: { + pullNumber: { type: "number", description: "Pull request number/index" }, + }, + required: ["pullNumber"], + additionalProperties: false, + }, + async execute(input) { + const { pullNumber } = input as { pullNumber: number } + const result = await $`tea pulls ${pullNumber} --comments --output json`.text() + return { content: result.trim() } + }, +} + +export default teaPrGet diff --git a/.opencode/plugins/tools/tea-pulls.ts b/.opencode/plugins/tools/tea-pulls.ts new file mode 100644 index 0000000..d4f27aa --- /dev/null +++ b/.opencode/plugins/tools/tea-pulls.ts @@ -0,0 +1,21 @@ +import { $ } from "bun" +import type { Info } from "@opencode/plugin/promise/tool" + +const teaPulls: Info = { + name: "tea-pulls", + description: "List pull requests in the repository using tea CLI", + input: { + type: "object", + properties: { + state: { type: "string", description: "Filter by state (all|open|closed)" }, + }, + additionalProperties: false, + }, + async execute(input) { + const state = (input as { state?: string }).state || "open" + const result = await $`tea pulls --state ${state} --output json`.text() + return { content: result.trim() } + }, +} + +export default teaPulls diff --git a/.opencode/tools/git-worktree-create.ts b/.opencode/tools/git-worktree-create.ts deleted file mode 100644 index d0a89a8..0000000 --- a/.opencode/tools/git-worktree-create.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Create a git worktree for a branch or PR", - args: { - branch: tool.schema.string().describe("Branch name or PR number"), - }, - async execute(args) { - const result = await $`git worktree add ../worktrees/${args.branch}`.text() - return result.trim() - }, -}) \ No newline at end of file diff --git a/.opencode/tools/tea-actions-runs-list.ts b/.opencode/tools/tea-actions-runs-list.ts deleted file mode 100644 index 1bd8784..0000000 --- a/.opencode/tools/tea-actions-runs-list.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "List workflow runs for Gitea actions with optional filters", - args: { - status: tool.schema.string().optional().describe("Filter by status (success, failure, pending, queued, in_progress, skipped, canceled)"), - branch: tool.schema.string().optional().describe("Filter by branch name"), - event: tool.schema.string().optional().describe("Filter by event type (push, pull_request, etc.)"), - limit: tool.schema.number().optional().describe("Number of results to return (default: 30)"), - since: tool.schema.string().optional().describe("Show runs started after this time (e.g., '24h', '7d')"), - }, - async execute(args) { - let cmd = [`tea`, `actions`, `runs`, `list`, `-o`, `json`] - if (args.status) cmd = [...cmd, `--status`, args.status] - if (args.branch) cmd = [...cmd, `--branch`, args.branch] - if (args.event) cmd = [...cmd, `--event`, args.event] - if (args.limit) cmd = [...cmd, `--limit`, String(args.limit)] - if (args.since) cmd = [...cmd, `--since`, args.since] - const result = await $`${cmd}`.text() - return result.trim() - }, -}) diff --git a/.opencode/tools/tea-actions-runs-logs.ts b/.opencode/tools/tea-actions-runs-logs.ts deleted file mode 100644 index 1f3354c..0000000 --- a/.opencode/tools/tea-actions-runs-logs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Get logs for a specific Gitea actions workflow run to diagnose failures", - args: { - runId: tool.schema.number().describe("Workflow run ID to get logs for"), - job: tool.schema.string().optional().describe("Specific job ID to view (if omitted, shows all jobs)"), - }, - async execute(args) { - let cmd = [`tea`, `actions`, `runs`, `logs`] - if (args.job) cmd = [...cmd, `--job`, args.job] - cmd = [...cmd, String(args.runId), `-o`, `simple`] - const result = await $`${cmd}`.text() - return result.trim() - }, -}) diff --git a/.opencode/tools/tea-comment.ts b/.opencode/tools/tea-comment.ts deleted file mode 100644 index c1e697c..0000000 --- a/.opencode/tools/tea-comment.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Add a comment to a specific issue using tea CLI", - args: { - issueNumber: tool.schema.number().describe("Issue number/index"), - body: tool.schema.string().describe("Comment body"), - }, - async execute(args) { - const result = await $`tea comment ${args.issueNumber} ${args.body}`.text() - return result.trim() - }, -}) \ No newline at end of file diff --git a/.opencode/tools/tea-issue-create.ts b/.opencode/tools/tea-issue-create.ts deleted file mode 100644 index e80bf62..0000000 --- a/.opencode/tools/tea-issue-create.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Create a new issue in the GitHub repository using tea CLI", - args: { - title: tool.schema.string().describe("Issue title"), - description: tool.schema.string().optional().describe("Issue description (supports markdown)"), - }, - async execute(args) { - const result = await $`tea issues create -t ${args.title} -d ${args.description}`.text() - return result.trim() - }, -}) \ No newline at end of file diff --git a/.opencode/tools/tea-issue-get.ts b/.opencode/tools/tea-issue-get.ts deleted file mode 100644 index c969496..0000000 --- a/.opencode/tools/tea-issue-get.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Get a specific issue with all details and comments using tea CLI", - args: { - issueNumber: tool.schema.number().describe("Issue number/index"), - }, - async execute(args) { - const result = await $`tea issues ${args.issueNumber} --comments --output json`.text() - return result.trim() - }, -}) \ No newline at end of file diff --git a/.opencode/tools/tea-issues.ts b/.opencode/tools/tea-issues.ts deleted file mode 100644 index 46a3c71..0000000 --- a/.opencode/tools/tea-issues.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "List all issues in the project using tea CLI", - args: {}, - async execute(args, context) { - const result = await $`tea issues list`.text() - return result.trim() - }, -}) \ No newline at end of file diff --git a/.opencode/tools/tea-pr-create.ts b/.opencode/tools/tea-pr-create.ts deleted file mode 100644 index 5f3dc3c..0000000 --- a/.opencode/tools/tea-pr-create.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Create a pull request in the repository using tea CLI", - args: { - title: tool.schema.string().describe("PR title"), - head: tool.schema.string().optional().describe("Branch name of the PR source (defaults to current branch)"), - base: tool.schema.string().optional().describe("Branch name of the PR target (defaults to repository default branch)"), - description: tool.schema.string().optional().describe("PR description (supports markdown)"), - assignees: tool.schema.string().optional().describe("Comma-separated list of usernames to assign"), - labels: tool.schema.string().optional().describe("Comma-separated list of labels to assign"), - }, - async execute(args) { - let cmd = [`tea`, `pulls`, `create`, `-t`, args.title] - if (args.head) cmd = [...cmd, `--head`, args.head] - if (args.base) cmd = [...cmd, `-b`, args.base] - if (args.description) cmd = [...cmd, `-d`, args.description] - if (args.assignees) cmd = [...cmd, `-a`, args.assignees] - if (args.labels) cmd = [...cmd, `-L`, args.labels] - const result = await $`${cmd}`.text() - return result.trim() - }, -}) diff --git a/.opencode/tools/tea-pr-get.ts b/.opencode/tools/tea-pr-get.ts deleted file mode 100644 index 4cd47a5..0000000 --- a/.opencode/tools/tea-pr-get.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "Get a specific pull request with all details and comments using tea CLI", - args: { - pullNumber: tool.schema.number().describe("Pull request number/index"), - }, - async execute(args) { - const result = await $`tea pulls ${args.pullNumber} --comments --output json`.text() - return result.trim() - }, -}) diff --git a/.opencode/tools/tea-pulls.ts b/.opencode/tools/tea-pulls.ts deleted file mode 100644 index 0f43194..0000000 --- a/.opencode/tools/tea-pulls.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { tool } from "@opencode-ai/plugin" -import { $ } from "bun" - -export default tool({ - description: "List pull requests in the repository using tea CLI", - args: { - state: tool.schema.string().optional().describe("Filter by state (all|open|closed)"), - }, - async execute(args) { - const state = args.state || "open" - const result = await $`tea pulls --state ${state} --output json`.text() - return result.trim() - }, -}) diff --git a/Makefile b/Makefile index 856e427..bf6bb54 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ OPENCODE_DIR := $(HOME)/.config/opencode REPO_DIR := $(shell pwd) # Items to symlink -ITEMS := skills tools agents AGENTS.md +ITEMS := skills plugins agents AGENTS.md # LLM services to manage LLM_SERVICES := qwen qwen-4bit diff --git a/README.md b/README.md index 39f01eb..5c0cfa9 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ OpenCode uses three component types: | Component | Location | Purpose | Example | |-----------|----------|---------|---------| | **Skills** | `.opencode/skills/` | Reference knowledge | `issue-writing` knows how to structure good issues | -| **Tools** | `.opencode/tools/` | Custom functions | `spawn_issues` implements multiple issues in parallel | +| **Tools** | `.opencode/plugins/` | Custom functions, provided by plugins | `tools` registers the `tea-*` Gitea CLI tools | | **Agents** | `.opencode/agents/` | Specialized subagents | `code-reviewer` handles PR reviews | ## Quick Start @@ -29,7 +29,7 @@ make install This creates symlinks at: - `~/.config/opencode/skills/` → `.opencode/skills/` -- `~/.config/opencode/tools/` → `.opencode/tools/` +- `~/.config/opencode/plugins/` → `.opencode/plugins/` - `~/.config/opencode/agents/` → `.opencode/agents/` ### Uninstallation @@ -67,7 +67,7 @@ architecture/ │ ├── .opencode/ # OpenCode configuration │ ├── skills/ # Reference knowledge (SKILL.md files) -│ ├── tools/ # Custom tools (TypeScript/JS) +│ ├── plugins/ # Plugins registering custom tools (TypeScript) │ └── agents/ # Specialized subagents (AGENT.md files) │ ├── Makefile # Symlink management @@ -96,24 +96,42 @@ Skills are auto-discovered by OpenCode and available via the `skill` tool. ### Tools -Create `.opencode/tools/.ts`: +Tools are registered by plugins under `.opencode/plugins/`. Create `.opencode/plugins//index.ts`: ```typescript -import { tool } from "@opencode-ai/plugin" +import { Plugin } from "@opencode/plugin" -export default tool({ +import type { Info } from "@opencode/plugin/promise/tool" + +const example: Info = { + name: "example", description: "What this tool does", - args: { - param: tool.schema.string().describe("Parameter description"), + input: { + type: "object", + properties: { + param: { type: "string", description: "Parameter description" }, + }, + required: ["param"], + additionalProperties: false, }, - async execute(args) { + async execute(input) { + const { param } = input as { param: string } // Your implementation - return "result" + return { content: `result for ${param}` } + }, +} + +export default Plugin.define({ + id: "example", + async setup(ctx) { + await ctx.tool.transform((editor) => { + editor.add(example) + }) }, }) ``` -Tools are auto-discovered and available to the LLM. +Tool input schemas are plain JSON Schema, and execution returns structured content. Plugins under `.opencode/plugins/` are auto-discovered and their tools are available to the LLM. ### Agents