chore: install openagent opencode

Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
2026-04-07 11:31:26 -04:00
parent b4c03ff25e
commit c2263602c4
204 changed files with 38010 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
<!-- Context: openagents-repo/agents | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
# Custom Agents in OpenCode
Plugins can register custom AI agents that have specific roles, instructions, and toolsets.
## Agent Definition
Custom agents are configured in the plugin's `config` function.
```typescript
export const registerCustomAgents = (config) => {
return {
...config,
agents: [
{
name: "my-helper",
description: "A friendly assistant for this project",
instructions: "You are a helpful assistant. Use your tools to help the user.",
model: "claude-3-5-sonnet-latest", // Specify the model
tools: ["say_hello", "read", "write"] // Reference built-in or custom tools
}
]
};
};
```
## Integrating into Plugin
The `config` method in the plugin return object is used to register agents.
```typescript
export const MyPlugin: Plugin = async (context) => {
return {
config: async (currentConfig) => {
return registerCustomAgents(currentConfig);
},
// ... other properties
};
};
```
## Agent Capabilities
- **Model Choice**: You can select specific models for different agents.
- **Scoped Tools**: Limit what tools an agent can use to ensure safety or focus.
- **System Instructions**: Define the "personality" and rules for the agent.

View File

@@ -0,0 +1,44 @@
<!-- Context: openagents-repo/events | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
# OpenCode Plugin Events
OpenCode fires over 25 events that you can hook into. These are categorized below:
## Command Events
- `command.executed`: Fired when a user or plugin runs a command.
## File Events
- `file.edited`: Fired when a file is modified via OpenCode tools.
- `file.watcher.updated`: Fired when the file watcher detects changes.
## Message Events (Read-Only)
- `message.updated`: Fired when a message in the session updates.
- `message.part.updated`: Fired when individual parts of a message update.
- `message.part.removed`: Fired when a part is removed.
- `message.removed`: Fired when entire message is removed.
## Session Events
- `session.created`: New session started.
- `session.updated`: Session state changed.
- `session.idle`: Session completed (no more activity expected).
- `session.status`: Session status changed.
- `session.error`: Error occurred in session.
- `session.compacted`: Session was compacted (context summarized).
## Tool Events (Interception)
- `tool.execute.before`: Fired before a tool runs. **Can block execution** by throwing an error.
- `tool.execute.after`: Fired after a tool completes with result.
## TUI Events
- `tui.prompt.append`: Text appended to prompt input.
- `tui.command.execute`: Command executed from TUI.
- `tui.toast.show`: Toast notification shown.
## Mapping from Claude Code Hooks
| Claude Hook | OpenCode Event |
|---|---|
| PreToolUse | tool.execute.before |
| PostToolUse | tool.execute.after |
| UserPromptSubmit | message.* events |
| SessionEnd | session.idle |

View File

@@ -0,0 +1,598 @@
<!-- Context: openagents-repo/events_skills | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
# OpenCode Events: Skills Plugin Implementation
## Overview
This document explains how the OpenCode Skills Plugin uses event hooks (`tool.execute.before` and `tool.execute.after`) to implement skill delivery and output enhancement. This is a practical example of the event system described in `events.md`.
---
## Event Hooks Used
### tool.execute.before
**Event Type:** Tool Execution Interception
**When it fires:** Before a tool function executes
**Purpose in Skills Plugin:** Inject skill content into the conversation
**Implementation:**
```typescript
const beforeHook = async (input: any, output: any) => {
// Check if this is a skill tool
if (input.tool.startsWith("skills_")) {
// Look up skill from map
const skill = skillMap.get(input.tool)
if (skill) {
// Inject skill content as silent prompt
await ctx.client.session.prompt({
path: { id: input.sessionID },
body: {
agent: input.agent,
noReply: true, // Don't trigger AI response
parts: [
{
type: "text",
text: `📚 Skill: ${skill.name}\nBase directory: ${skill.fullPath}\n\n${skill.content}`,
},
],
},
})
}
}
}
```
**Why use this hook?**
- Runs before tool execution, perfect for context injection
- Can access tool name and session ID
- Can inject content without triggering AI response
- Skill content persists in conversation history
**Input Parameters:**
- `input.tool` - Tool name (e.g., "skills_brand_guidelines")
- `input.sessionID` - Current session ID
- `input.agent` - Agent name that called the tool
- `output.args` - Tool arguments
**What you can do:**
- ✅ Inject context (skill content)
- ✅ Validate inputs
- ✅ Preprocess arguments
- ✅ Log tool calls
- ✅ Implement security checks
**What you can't do:**
- ❌ Modify tool output (tool hasn't run yet)
- ❌ Access tool results
---
### tool.execute.after
**Event Type:** Tool Execution Interception
**When it fires:** After a tool function completes
**Purpose in Skills Plugin:** Enhance output with visual feedback
**Implementation:**
```typescript
const afterHook = async (input: any, output: any) => {
// Check if this is a skill tool
if (input.tool.startsWith("skills_")) {
// Look up skill from map
const skill = skillMap.get(input.tool)
if (skill && output.output) {
// Add emoji title for visual feedback
output.title = `📚 ${skill.name}`
}
}
}
```
**Why use this hook?**
- Runs after tool execution, perfect for output enhancement
- Can modify output properties
- Can add visual feedback (emoji titles)
- Can implement logging/analytics
**Input Parameters:**
- `input.tool` - Tool name (e.g., "skills_brand_guidelines")
- `input.sessionID` - Current session ID
- `output.output` - Tool result/output
- `output.title` - Output title (can be modified)
**What you can do:**
- ✅ Modify output
- ✅ Add titles/formatting
- ✅ Log completion
- ✅ Add analytics
- ✅ Transform results
**What you can't do:**
- ❌ Modify tool arguments (already executed)
- ❌ Prevent tool execution (already happened)
---
## Event Lifecycle in Skills Plugin
```
┌─────────────────────────────────────────────────────────────────┐
│ AGENT CALLS SKILL TOOL │
│ (e.g., skills_brand_guidelines) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EVENT: tool.execute.before fires │
│ │
│ Hook Function: beforeHook(input, output) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Check: input.tool.startsWith("skills_") │ │
│ │ 2. Lookup: skillMap.get(input.tool) │ │
│ │ 3. Inject: ctx.client.session.prompt({ │ │
│ │ path: { id: input.sessionID }, │ │
│ │ body: { │ │
│ │ agent: input.agent, │ │
│ │ noReply: true, │ │
│ │ parts: [{ type: "text", text: skill.content }] │ │
│ │ } │ │
│ │ }) │ │
│ │ 4. Result: Skill content added to conversation │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Effect: Skill content persists in conversation history │
│ No AI response triggered (noReply: true) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ TOOL.EXECUTE() RUNS │
│ │
│ async execute(args, toolCtx) { │
│ return `Skill activated: ${skill.name}` │
│ } │
│ │
│ Effect: Minimal confirmation returned │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ EVENT: tool.execute.after fires │
│ │
│ Hook Function: afterHook(input, output) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Check: input.tool.startsWith("skills_") │ │
│ │ 2. Lookup: skillMap.get(input.tool) │ │
│ │ 3. Verify: output.output exists │ │
│ │ 4. Enhance: output.title = `📚 ${skill.name}` │ │
│ │ 5. Result: Output title modified with emoji │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ Effect: Visual feedback added to output │
│ Could add logging/analytics here │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ RESULT RETURNED TO AGENT │
│ │
│ - Tool confirmation message │
│ - Skill content in conversation history │
│ - Enhanced output with emoji title │
│ - Agent can now use skill content in reasoning │
└─────────────────────────────────────────────────────────────────┘
```
---
## Why Hooks Instead of Embedded Logic?
### Problem: Embedded Delivery (Anti-Pattern)
```typescript
// ❌ OLD: Skill delivery inside tool.execute()
async execute(args, toolCtx) {
const sendSilentPrompt = (text: string) =>
ctx.client.session.prompt({...})
await sendSilentPrompt(`The "${skill.name}" skill is loading...`)
await sendSilentPrompt(`Base directory: ${skill.fullPath}\n\n${skill.content}`)
return `Launching skill: ${skill.name}`
}
```
**Issues:**
1. **Tight Coupling**: Tool logic and delivery are inseparable
2. **Hard to Test**: Can't test tool without testing delivery
3. **Violates SOLID**: Single Responsibility Principle broken
4. **No Reusability**: Delivery logic can't be extracted
5. **Difficult to Monitor**: Can't track delivery separately
---
### Solution: Hook-Based Delivery (Best Practice)
```typescript
// ✅ NEW: Separated concerns using hooks
// Tool: Minimal and focused
async execute(args, toolCtx) {
return `Skill activated: ${skill.name}`
}
// Hook: Handles delivery
const beforeHook = async (input, output) => {
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get(input.tool)
if (skill) {
await ctx.client.session.prompt({...})
}
}
}
```
**Benefits:**
1.**Loose Coupling**: Tool and delivery are independent
2.**Easy to Test**: Each component tested separately
3.**SOLID Compliant**: Single Responsibility Principle
4.**Reusable**: Hooks can be composed with other plugins
5.**Monitorable**: Can add logging/analytics independently
---
## Skill Lookup Map: Performance Optimization
### Why a Map?
The skill lookup map enables O(1) access instead of O(n) search:
```typescript
// ✅ EFFICIENT: O(1) lookup
const skillMap = new Map<string, Skill>()
for (const skill of skills) {
skillMap.set(skill.toolName, skill)
}
const beforeHook = async (input, output) => {
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get(input.tool) // O(1) constant time
if (skill) {
// Use skill
}
}
}
```
### Performance Impact
| Number of Skills | Array Search (O(n)) | Map Lookup (O(1)) | Speedup |
|------------------|-------------------|------------------|---------|
| 10 | 10 comparisons | 1 lookup | 10x |
| 100 | 100 comparisons | 1 lookup | 100x |
| 1000 | 1000 comparisons | 1 lookup | 1000x |
**Conclusion:** Map lookup is essential for scalability
---
## Integration with OpenCode Event System
### Event Mapping
| OpenCode Event | Skills Plugin Hook | Purpose |
|---|---|---|
| `tool.execute.before` | `beforeHook` | Skill content injection |
| `tool.execute.after` | `afterHook` | Output enhancement |
### Plugin Return Object
```typescript
return {
// Custom tools
tool: tools,
// Hook: Runs before tool execution
"tool.execute.before": beforeHook,
// Hook: Runs after tool execution
"tool.execute.after": afterHook,
}
```
**Key Points:**
- Hooks apply to ALL tools (use `if` statements to filter)
- Multiple plugins can register hooks without conflict
- Hooks run in registration order
- Hooks can be async
---
## Comparison with Other Event Hooks
### Available Tool Execution Hooks
| Hook | When | Use Case |
|------|------|----------|
| `tool.execute.before` | Before tool runs | Input validation, context injection, preprocessing |
| `tool.execute.after` | After tool completes | Output formatting, logging, analytics |
### Other Event Hooks (Not Used in Skills Plugin)
| Hook | When | Use Case |
|------|------|----------|
| `session.created` | Session starts | Welcome messages, initialization |
| `message.updated` | Message changes | Monitoring, logging |
| `session.idle` | Session completes | Cleanup, background tasks |
| `session.error` | Error occurs | Error handling, logging |
---
## Real-World Example: Skill Delivery Flow
### Step 1: Agent Calls Skill Tool
```
Agent: "Use the brand-guidelines skill"
OpenCode: Calls skills_brand_guidelines tool
```
### Step 2: Before Hook Fires
```typescript
const beforeHook = async (input, output) => {
// input.tool = "skills_brand_guidelines"
// input.sessionID = "ses_abc123"
// input.agent = "my-helper"
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get("skills_brand_guidelines")
// skill = {
// name: "brand-guidelines",
// description: "Brand guidelines for the project",
// content: "# Brand Guidelines\n\n...",
// fullPath: "/path/to/skill"
// }
await ctx.client.session.prompt({
path: { id: "ses_abc123" },
body: {
agent: "my-helper",
noReply: true,
parts: [
{
type: "text",
text: "📚 Skill: brand-guidelines\nBase directory: /path/to/skill\n\n# Brand Guidelines\n\n..."
}
]
}
})
}
}
```
**Result:** Skill content added to conversation, no AI response
### Step 3: Tool Executes
```typescript
async execute(args, toolCtx) {
// Minimal logic
return `Skill activated: brand-guidelines`
}
```
**Result:** Simple confirmation returned
### Step 4: After Hook Fires
```typescript
const afterHook = async (input, output) => {
// input.tool = "skills_brand_guidelines"
// output.output = "Skill activated: brand-guidelines"
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get("skills_brand_guidelines")
if (skill && output.output) {
output.title = `📚 brand-guidelines`
}
}
}
```
**Result:** Output title enhanced with emoji
### Step 5: Agent Receives Result
```
Conversation History:
├─ User: "Use the brand-guidelines skill"
├─ Tool Call: skills_brand_guidelines
├─ Silent Message: "📚 Skill: brand-guidelines\n..."
├─ Tool Result: "Skill activated: brand-guidelines"
│ (with title: "📚 brand-guidelines")
└─ Agent: "I now have the brand guidelines. I can help with..."
```
---
## Testing Hooks
### Testing Before Hook
```typescript
describe("beforeHook", () => {
it("should inject skill content for skill tools", async () => {
const input = {
tool: "skills_brand_guidelines",
sessionID: "ses_test",
agent: "test-agent"
}
const output = { args: {} }
const mockPrompt = jest.fn()
ctx.client.session.prompt = mockPrompt
await beforeHook(input, output)
expect(mockPrompt).toHaveBeenCalledWith(
expect.objectContaining({
path: { id: "ses_test" },
body: expect.objectContaining({
agent: "test-agent",
noReply: true,
parts: expect.arrayContaining([
expect.objectContaining({
type: "text",
text: expect.stringContaining("brand-guidelines")
})
])
})
})
)
})
it("should skip non-skill tools", async () => {
const input = { tool: "read_file", sessionID: "ses_test" }
const output = { args: {} }
const mockPrompt = jest.fn()
ctx.client.session.prompt = mockPrompt
await beforeHook(input, output)
expect(mockPrompt).not.toHaveBeenCalled()
})
})
```
### Testing After Hook
```typescript
describe("afterHook", () => {
it("should add emoji title for skill tools", async () => {
const input = { tool: "skills_brand_guidelines" }
const output = { output: "Skill activated" }
await afterHook(input, output)
expect(output.title).toBe("📚 brand-guidelines")
})
it("should skip non-skill tools", async () => {
const input = { tool: "read_file" }
const output = { output: "File content" }
await afterHook(input, output)
expect(output.title).toBeUndefined()
})
it("should skip if output is missing", async () => {
const input = { tool: "skills_brand_guidelines" }
const output = { output: null }
await afterHook(input, output)
expect(output.title).toBeUndefined()
})
})
```
---
## Common Patterns
### Pattern 1: Tool-Specific Hooks
```typescript
const beforeHook = async (input, output) => {
switch (input.tool) {
case "skills_brand_guidelines":
// Handle brand guidelines
break
case "skills_api_reference":
// Handle API reference
break
default:
// Skip non-skill tools
}
}
```
### Pattern 2: Conditional Processing
```typescript
const beforeHook = async (input, output) => {
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get(input.tool)
if (skill && skill.allowedTools?.includes(input.agent)) {
// Process only if allowed
}
}
}
```
### Pattern 3: Logging & Monitoring
```typescript
const beforeHook = async (input, output) => {
if (input.tool.startsWith("skills_")) {
console.log(`[BEFORE] Skill tool called: ${input.tool}`)
console.log(`[BEFORE] Session: ${input.sessionID}`)
}
}
const afterHook = async (input, output) => {
if (input.tool.startsWith("skills_")) {
console.log(`[AFTER] Skill tool completed: ${input.tool}`)
console.log(`[AFTER] Output length: ${output.output?.length || 0}`)
}
}
```
### Pattern 4: Error Handling
```typescript
const beforeHook = async (input, output) => {
try {
if (input.tool.startsWith("skills_")) {
const skill = skillMap.get(input.tool)
if (!skill) {
throw new Error(`Skill not found: ${input.tool}`)
}
// Process skill
}
} catch (error) {
console.error(`Hook error:`, error)
// Don't rethrow - let tool execute anyway
}
}
```
---
## Key Takeaways
1. **Hooks are middleware**: They intercept tool execution at specific points
2. **Before hook**: For preprocessing, validation, context injection
3. **After hook**: For output enhancement, logging, analytics
4. **Lookup maps**: Enable O(1) access instead of O(n) search
5. **Separation of concerns**: Tools do one thing, hooks do another
6. **Composability**: Multiple plugins can register hooks without conflict
7. **Testability**: Each component can be tested independently
8. **Maintainability**: Changes are isolated to specific hooks
---
## References
- **OpenCode Events**: `context/capabilities/events.md`
- **Tool Definition**: `context/capabilities/tools.md`
- **Best Practices**: `context/reference/best-practices.md`
- **Skills Plugin Example**: `skills-plugin/example.ts`
- **Hook Lifecycle**: `skills-plugin/hook-lifecycle-and-patterns.md`
- **Implementation Pattern**: `skills-plugin/implementation-pattern.md`

View File

@@ -0,0 +1,53 @@
<!-- Context: openagents-repo/tools | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
# Building Custom Tools
Plugins can add custom tools that OpenCode agents can call autonomously.
## Tool Definition
Custom tools use Zod for schema definition and the `tool` helper from `@opencode-ai/plugin`.
```typescript
import { z } from 'zod';
import { tool } from '@opencode-ai/plugin';
export const MyCustomTool = tool(
z.object({
query: z.string().describe('Search query'),
limit: z.number().default(10).describe('Results limit')
}),
async (args, context) => {
const { query, limit } = args;
// Implementation logic
return { success: true, data: [] };
}
).describe('Search your database');
```
## Shell-based Tools
You can leverage Bun's shell API (`$`) to run commands in any language.
```typescript
export const PythonCalculatorTool = tool(
z.object({ expression: z.string() }),
async (args, context) => {
const { $ } = context;
const result = await $`python3 -c 'print(eval("${args.expression}"))'`;
return { result: result.stdout };
}
).describe('Calculate mathematical expressions');
```
## Integration
To register tools in your plugin:
```typescript
export const MyPlugin = async (context) => {
return {
tool: [MyCustomTool, PythonCalculatorTool]
};
};
```