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 }