package plugins import ( "errors" "fmt" "os" "path/filepath" "regexp" "strconv" ) var openCodeScriptPathPattern = regexp.MustCompile(`(?m)^const (GUARD_SCRIPT_PATH|HOOK_SCRIPT_PATH) = (".*");$`) // OpenCodePlugin writes ~/.config/opencode/plugins/elydora-audit.mjs. 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 the Elydora CLI; 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 != "" { if err := removeAgentScripts(agentID); err != nil { return err } } fmt.Println("Uninstalled Elydora hook for OpenCode.") return nil } func openCodeScriptPaths(data []byte, pluginFile string) (string, string, error) { paths := map[string]string{} for _, match := range openCodeScriptPathPattern.FindAllSubmatch(data, -1) { value, err := strconv.Unquote(string(match[2])) if err != nil { return "", "", fmt.Errorf( "OpenCode plugin at %s has an invalid %s: %w", pluginFile, match[1], err, ) } paths[string(match[1])] = value } guardPath, hasGuard := paths["GUARD_SCRIPT_PATH"] hookPath, hasHook := paths["HOOK_SCRIPT_PATH"] if !hasGuard || !hasHook { return "", "", fmt.Errorf("OpenCode plugin at %s has no Elydora script paths", pluginFile) } return guardPath, hookPath, 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, } data, err := os.ReadFile(pluginFile) if errors.Is(err, os.ErrNotExist) { return status, nil } if err != nil { return status, fmt.Errorf("read %s: %w", pluginFile, err) } status.HookConfigured = true guardPath, hookPath, err := openCodeScriptPaths(data, pluginFile) if err != nil { return status, err } guard, err := resolveManagedScript(guardPath, "guard.js") if err != nil { return status, err } audit, err := resolveManagedScript(hookPath, "hook.js") if err != nil { return status, err } if guard == nil || audit == nil || !sameManagedAgentID(guard.agentID, audit.agentID) { return status, nil } status.HookScriptExists, err = managedRuntimePresent(managedRuntimeContract{ agentID: guard.agentID, guardPath: guard.scriptPath, auditPath: audit.scriptPath, }, "opencode") if err != nil { return status, err } status.Installed = status.HookScriptExists return status, nil }