migrated custom tools to V2 plugin

This commit is contained in:
2026-10-22 00:53:02 +02:00
parent 5a1a3b3d13
commit 0f2e4330b8
23 changed files with 314 additions and 168 deletions
@@ -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
+30
View File
@@ -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)
})
},
})
@@ -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
@@ -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
+23
View File
@@ -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
@@ -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
+22
View File
@@ -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
+18
View File
@@ -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
+40
View File
@@ -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
+22
View File
@@ -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
+21
View File
@@ -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
-13
View File
@@ -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()
},
})
-23
View File
@@ -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()
},
})
-17
View File
@@ -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()
},
})
-14
View File
@@ -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()
},
})
-14
View File
@@ -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()
},
})
-13
View File
@@ -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()
},
})
-11
View File
@@ -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()
},
})
-24
View File
@@ -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()
},
})
-13
View File
@@ -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()
},
})
-14
View File
@@ -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()
},
})
+1 -1
View File
@@ -4,7 +4,7 @@ OPENCODE_DIR := $(HOME)/.config/opencode
REPO_DIR := $(shell pwd) REPO_DIR := $(shell pwd)
# Items to symlink # Items to symlink
ITEMS := skills tools agents AGENTS.md ITEMS := skills plugins agents AGENTS.md
# LLM services to manage # LLM services to manage
LLM_SERVICES := qwen qwen-4bit LLM_SERVICES := qwen qwen-4bit
+29 -11
View File
@@ -11,7 +11,7 @@ OpenCode uses three component types:
| Component | Location | Purpose | Example | | Component | Location | Purpose | Example |
|-----------|----------|---------|---------| |-----------|----------|---------|---------|
| **Skills** | `.opencode/skills/` | Reference knowledge | `issue-writing` knows how to structure good issues | | **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 | | **Agents** | `.opencode/agents/` | Specialized subagents | `code-reviewer` handles PR reviews |
## Quick Start ## Quick Start
@@ -29,7 +29,7 @@ make install
This creates symlinks at: This creates symlinks at:
- `~/.config/opencode/skills/` → `.opencode/skills/` - `~/.config/opencode/skills/` → `.opencode/skills/`
- `~/.config/opencode/tools/` → `.opencode/tools/` - `~/.config/opencode/plugins/` → `.opencode/plugins/`
- `~/.config/opencode/agents/` → `.opencode/agents/` - `~/.config/opencode/agents/` → `.opencode/agents/`
### Uninstallation ### Uninstallation
@@ -67,7 +67,7 @@ architecture/
│ │
├── .opencode/ # OpenCode configuration ├── .opencode/ # OpenCode configuration
│ ├── skills/ # Reference knowledge (SKILL.md files) │ ├── skills/ # Reference knowledge (SKILL.md files)
│ ├── tools/ # Custom tools (TypeScript/JS) │ ├── plugins/ # Plugins registering custom tools (TypeScript)
│ └── agents/ # Specialized subagents (AGENT.md files) │ └── agents/ # Specialized subagents (AGENT.md files)
│ │
├── Makefile # Symlink management ├── Makefile # Symlink management
@@ -96,24 +96,42 @@ Skills are auto-discovered by OpenCode and available via the `skill` tool.
### Tools ### Tools
Create `.opencode/tools/<name>.ts`: Tools are registered by plugins under `.opencode/plugins/`. Create `.opencode/plugins/<name>/index.ts`:
```typescript ```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", description: "What this tool does",
args: { input: {
param: tool.schema.string().describe("Parameter description"), type: "object",
properties: {
param: { type: "string", description: "Parameter description" },
}, },
async execute(args) { required: ["param"],
additionalProperties: false,
},
async execute(input) {
const { param } = input as { param: string }
// Your implementation // 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 ### Agents