Files
Elydora-Go-SDK/cmd/elydora/plugins/opencode.go
T

174 lines
4.5 KiB
Go

package plugins
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// OpenCodePlugin manages the Elydora audit hook for OpenCode.
// It writes .config/opencode/plugins/elydora-audit.mjs in the home directory.
type OpenCodePlugin struct{}
func (p *OpenCodePlugin) configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".config", "opencode", "plugins", "elydora-audit.mjs"), nil
}
func (p *OpenCodePlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
if config.HookScript != "" {
scriptPath = config.HookScript
}
if err := GenerateHookScript(scriptPath, config); err != nil {
return fmt.Errorf("generate hook script: %w", err)
}
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
}
pluginFile, err := p.configPath()
if err != nil {
return err
}
dir := filepath.Dir(pluginFile)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("create directory %s: %w", dir, err)
}
content := buildOpenCodePlugin(scriptPath, guardPath)
if err := os.WriteFile(pluginFile, []byte(content), 0644); err != nil {
return fmt.Errorf("write %s: %w", pluginFile, err)
}
fmt.Printf("Installed Elydora hook for OpenCode at %s\n", pluginFile)
return nil
}
func buildOpenCodePlugin(scriptPath, guardPath string) string {
return fmt.Sprintf(`// Elydora Audit Plugin for OpenCode
// Generated by @elydora/sdk — DO NOT EDIT
import { spawn, spawnSync } from 'node:child_process';
const GUARD_SCRIPT_PATH = %q;
const HOOK_SCRIPT_PATH = %q;
export const ElydoraAuditPlugin = async () => ({
"tool.execute.before": async () => {
const result = spawnSync('node', [GUARD_SCRIPT_PATH], {
timeout: 5000,
stdio: ['pipe', 'ignore', 'pipe'],
});
if (result.error) {
throw new Error('Elydora guard failed: ' + result.error.message);
}
if (result.status !== 0) {
const message = result.stderr?.toString().trim() || 'Elydora guard blocked tool execution.';
throw new Error(message);
}
},
"tool.execute.after": async (input) => {
const event = JSON.stringify({
tool_name: input?.tool || 'unknown',
tool_input: input?.args || {},
session_id: input?.sessionID || 'unknown',
});
const child = spawn('node', [HOOK_SCRIPT_PATH], {
detached: true,
stdio: ['pipe', 'ignore', 'ignore'],
});
child.on('error', (error) => {
console.error('[elydora] audit hook failed:', error.message);
});
child.stdin.end(event);
child.unref();
},
});
`, guardPath, scriptPath)
}
func (p *OpenCodePlugin) Uninstall(agentID string) error {
pluginFile, err := p.configPath()
if err != nil {
return err
}
if err := os.Remove(pluginFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove %s: %w", pluginFile, err)
}
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for OpenCode.")
return nil
}
func (p *OpenCodePlugin) Status() (PluginStatus, error) {
pluginFile, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "opencode",
DisplayName: "OpenCode",
ConfigPath: pluginFile,
}
if _, err := os.Stat(pluginFile); err == nil {
status.HookConfigured = true
// Read the plugin file to extract the hook script path
data, readErr := os.ReadFile(pluginFile)
if readErr == nil {
content := string(data)
// Look for the elydora hook.js path in the plugin file content
if idx := strings.Index(content, ".elydora"); idx >= 0 {
// Extract path from the spawn('node', ['/path/to/hook.js']) pattern
// Pragmatic: scan ~/.elydora/*/hook.js for existence
home, homeErr := os.UserHomeDir()
if homeErr == nil {
entries, dirErr := os.ReadDir(filepath.Join(home, ".elydora"))
if dirErr == nil {
for _, entry := range entries {
if entry.IsDir() {
hookPath := filepath.Join(home, ".elydora", entry.Name(), "hook.js")
if _, statErr := os.Stat(hookPath); statErr == nil {
status.HookScriptExists = true
break
}
}
}
}
}
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}