chore: install openagent opencode
Signed-off-by: Dmytro Stanchiev <git@dmytros.dev>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<!-- Context: openagents-repo/lifecycle | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
|
||||
|
||||
# Plugin Lifecycle & Packaging
|
||||
|
||||
## File Structure for Complex Plugins
|
||||
|
||||
For larger plugins, follow this recommended structure:
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── .claude-plugin/
|
||||
│ └── plugin.json # Manifest (required for packaging)
|
||||
├── commands/ # Custom slash commands
|
||||
├── agents/ # Custom agents
|
||||
├── hooks/ # Event handlers
|
||||
└── README.md # Documentation
|
||||
```
|
||||
|
||||
## The Manifest (`plugin.json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "my-plugin",
|
||||
"description": "A custom plugin",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Your Name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `name` becomes the namespace prefix for commands: `/my-plugin:command`.
|
||||
|
||||
## SDK Access
|
||||
|
||||
Plugins have full access to the OpenCode SDK via `context.client`. This allows:
|
||||
- Sending prompts programmatically: `client.session.prompt()`
|
||||
- Managing sessions: `client.session.list()`, `client.session.get()`
|
||||
- Showing UI elements: `client.tui.showToast()`
|
||||
- Appending to prompt: `client.tui.appendPrompt()`
|
||||
@@ -0,0 +1,60 @@
|
||||
<!-- Context: openagents-repo/overview | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
|
||||
|
||||
# OpenCode Plugins Overview
|
||||
|
||||
OpenCode plugins are JavaScript or TypeScript modules that hook into **25+ events** across the entire OpenCode lifecycle—from when you type a prompt, to when tools execute, to when sessions complete.
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Zero-Config**: No build step or compilation required. Just drop `.ts` or `.js` files into the plugin folder.
|
||||
- **Middleware Pattern**: Plugins subscribe to events and execute logic, similar to Express.js middleware.
|
||||
- **Access**: Plugins receive a `context` object with:
|
||||
- `project`: Current project metadata.
|
||||
- `client`: OpenCode SDK client for programmatic control.
|
||||
- `$`: Bun's shell API for running commands.
|
||||
- `directory`: Current working directory.
|
||||
- `worktree`: Git worktree path.
|
||||
|
||||
## Plugin Registration
|
||||
|
||||
OpenCode looks for plugins in:
|
||||
1. **Project-level**: `.opencode/plugin/` (project root)
|
||||
2. **Global**: `~/.config/opencode/plugin/` (home directory)
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```typescript
|
||||
export const MyPlugin = async (context) => {
|
||||
const { project, client, $, directory, worktree } = context;
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
// Handle events here
|
||||
}
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Each exported function becomes a separate plugin instance. The name of the export is used as the plugin name.
|
||||
|
||||
## Build and Development
|
||||
|
||||
OpenCode plugins are typically written in TypeScript and bundled into a single JavaScript file for execution.
|
||||
|
||||
### Build Command
|
||||
Use Bun to bundle the plugin into the `dist` directory:
|
||||
|
||||
```bash
|
||||
bun build src/index.ts --outdir dist --target bun --format esm
|
||||
```
|
||||
|
||||
The output will be a single file (e.g., `./index.js`) containing all dependencies.
|
||||
|
||||
### Development Workflow
|
||||
1. **Source Code**: Write your plugin in `src/index.ts`.
|
||||
2. **Bundle**: Run the build command to generate `dist/index.js`.
|
||||
3. **Load**: Point OpenCode to the bundled file or the directory containing the manifest.
|
||||
4. **Watch Mode**: For rapid development, use the `--watch` flag with Bun build:
|
||||
```bash
|
||||
bun build src/index.ts --outdir dist --target bun --format esm --watch
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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 |
|
||||
@@ -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`
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
};
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
<!-- Context: openagents-repo/context-overview | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
|
||||
|
||||
# OpenCode Plugin Context Library
|
||||
|
||||
This library provides structured context for AI coding assistants to understand, build, and extend OpenCode plugins. Depending on your task, you can load specific parts of this library.
|
||||
|
||||
## 📚 Library Map
|
||||
|
||||
### 🏗️ Architecture
|
||||
Foundational concepts of how plugins are registered and executed.
|
||||
- [Overview](./architecture/overview.md): Basic structure, registration, and context object.
|
||||
- [Lifecycle](./architecture/lifecycle.md): Packaging, manifest, and session lifecycle.
|
||||
|
||||
### 🛠️ Capabilities
|
||||
Deep dives into specific plugin features.
|
||||
- [Events](./capabilities/events.md): Detailed list of all 25+ hookable events.
|
||||
- [Events: Skills Plugin](./capabilities/events_skills.md): Practical example of event hooks in the Skills Plugin.
|
||||
- [Tools](./capabilities/tools.md): How to build and register custom tools using Zod.
|
||||
- [Agents](./capabilities/agents.md): Creating and configuring custom AI agents.
|
||||
|
||||
### 📖 Reference
|
||||
Guidelines and troubleshooting.
|
||||
- [Best Practices](./reference/best-practices.md): Message injection workarounds, security, and performance.
|
||||
|
||||
### 🧩 Claude Code Plugins (External)
|
||||
Claude Code plugin system documentation (harvested from external docs).
|
||||
- [Concepts: Plugin Architecture](./concepts/plugin-architecture.md): Core concepts and structure
|
||||
- [Guides: Creating Plugins](./guides/creating-plugins.md): Step-by-step creation
|
||||
- [Guides: Migrating to Plugins](./guides/migrating-to-plugins.md): Convert standalone to plugin
|
||||
- [Lookup: Plugin Structure](./lookup/plugin-structure.md): Directory reference
|
||||
|
||||
## 🚀 How to use this library
|
||||
If you are asking an AI to build a new feature:
|
||||
1. **For a new tool**: Provide `architecture/overview.md` and `capabilities/tools.md`.
|
||||
2. **For reacting to events**: Provide `capabilities/events.md`.
|
||||
3. **For overall plugin architecture**: Provide `architecture/overview.md` and `architecture/lifecycle.md`.
|
||||
@@ -0,0 +1,28 @@
|
||||
<!-- Context: openagents-repo/best-practices | Priority: low | Version: 1.0 | Updated: 2026-02-15 -->
|
||||
|
||||
# Best Practices & Limitations
|
||||
|
||||
## Message Injection Workarounds
|
||||
|
||||
**The Reality**: The message system is largely read-only. You cannot mutate messages mid-stream or inject text directly into an existing message part.
|
||||
|
||||
### What Doesn't Work
|
||||
- Modifying `event.data.content` in `message.updated`.
|
||||
- Retroactively changing AI responses.
|
||||
|
||||
### What Works
|
||||
1. **Initial Context**: Use `session.created` to inject a starting message using `client.session.prompt()`.
|
||||
2. **Prompt Decoration**: Use `client.tui.appendPrompt()` to add text to the user's input box before they hit enter.
|
||||
3. **Tool Interception**: Use `tool.execute.before` to modify arguments *before* the tool runs.
|
||||
4. **On-Demand Context**: Provide custom tools that the AI can call when it needs more information.
|
||||
|
||||
## Security
|
||||
|
||||
- Always validate tool inputs in `tool.execute.before`.
|
||||
- Use environment variables for sensitive data; do not hardcode API keys.
|
||||
- Be careful with the `$` shell API to prevent command injection.
|
||||
|
||||
## Performance
|
||||
|
||||
- Avoid heavy synchronous operations in event handlers as they can block the TUI.
|
||||
- Use the `session.idle` event for cleanup or background sync tasks.
|
||||
42
.opencode/context/openagents-repo/plugins/navigation.md
Normal file
42
.opencode/context/openagents-repo/plugins/navigation.md
Normal file
@@ -0,0 +1,42 @@
|
||||
<!-- Context: openagents-repo/navigation | Priority: critical | Version: 1.0 | Updated: 2026-02-15 -->
|
||||
|
||||
# OpenAgents Plugins
|
||||
|
||||
**Purpose**: Plugin architecture and documentation for OpenAgents Control
|
||||
|
||||
---
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
openagents-repo/plugins/
|
||||
├── navigation.md (this file)
|
||||
├── context/
|
||||
│ └── [context plugin files]
|
||||
└── [plugin files]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Routes
|
||||
|
||||
| Task | Path |
|
||||
|------|------|
|
||||
| **Context plugin** | `./context/` |
|
||||
| **View plugins** | `./` |
|
||||
| **Guides** | `../guides/navigation.md` |
|
||||
|
||||
---
|
||||
|
||||
## By Type
|
||||
|
||||
**Context Plugin** → Context system plugin documentation
|
||||
**Plugin Architecture** → How plugins work in OpenAgents
|
||||
|
||||
---
|
||||
|
||||
## Related Context
|
||||
|
||||
- **OpenAgents Navigation** → `../navigation.md`
|
||||
- **Guides** → `../guides/navigation.md`
|
||||
- **Core Concepts** → `../core-concepts/navigation.md`
|
||||
Reference in New Issue
Block a user