Migrate to per-agent subdirectories, uniform 8-agent support, cross-platform fixes

- All paths use ~/.elydora/{agentId}/ per-agent isolation
- Add copilot, letta plugins; split kiro into kirocli + kiroide
- Remove augment (SDK-only agent, not one-click)
- Fix filepath.Join usage (replace string concatenation)
- Fix stale comment referencing old ~/.elydora/agents/ path
- Uniform 8 agents: claudecode, cursor, gemini, kirocli, kiroide, opencode, copilot, letta

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-01 19:58:20 -05:00
1 parent c21e9fcfc6
commit d80819093a
14 files changed
+830 -199

No files matched your search

+4 -2
View File
@@ -88,10 +88,12 @@ elydora install \
| Agent | Key |
|-------|-----|
| Claude Code | `claudecode` |
| Copilot CLI | `copilot` |
| Cursor | `cursor` |
| Gemini CLI | `gemini` |
| Augment Code | `augment` |
| Kiro | `kiro` |
| Kiro CLI | `kirocli` |
| Kiro IDE | `kiroide` |
| Letta Code | `letta` |
| OpenCode | `opencode` |
## API Reference
+61 -11
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
@@ -91,12 +92,12 @@ func cmdInstall(args []string) {
}
// Generate guard script (PreToolUse — freeze enforcement)
guardScriptPath, err := guardScriptPathForAgent(*agent)
guardScriptPath, err := guardScriptPathForAgent(*agentID)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
guardScript := plugins.GenerateGuardScript(*agent)
guardScript := plugins.GenerateGuardScript(*agent, *agentID)
if err := os.MkdirAll(filepath.Dir(guardScriptPath), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Error creating guard script directory: %v\n", err)
os.Exit(1)
@@ -131,6 +132,7 @@ func cmdInstall(args []string) {
func cmdUninstall(args []string) {
fs := flag.NewFlagSet("uninstall", flag.ExitOnError)
agent := fs.String("agent", "", "Agent name (required)")
agentID := fs.String("agent-id", "", "Agent ID (if omitted, scans ~/.elydora/*/config.json for matching agent_name)")
fs.Parse(args)
@@ -147,18 +149,66 @@ func cmdUninstall(args []string) {
os.Exit(1)
}
if err := plugin.Uninstall(); err != nil {
// Resolve agentID: if not given, scan ~/.elydora/*/config.json for matching agent_name
resolvedAgentID := *agentID
if resolvedAgentID == "" {
resolvedAgentID = findAgentIDByName(*agent)
if resolvedAgentID == "" {
fmt.Fprintf(os.Stderr, "Warning: could not find agent ID for %q in ~/.elydora/*/config.json\n", *agent)
}
}
if err := plugin.Uninstall(resolvedAgentID); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Also remove the guard script
guardPath, _ := guardScriptPathForAgent(*agent)
if guardPath != "" {
os.Remove(guardPath)
// Remove the per-agent directory
if resolvedAgentID != "" {
home, err := os.UserHomeDir()
if err == nil {
agentDir := filepath.Join(home, ".elydora", resolvedAgentID)
if err := os.RemoveAll(agentDir); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to remove agent directory %s: %v\n", agentDir, err)
} else {
fmt.Printf(" Removed agent directory: %s\n", agentDir)
}
}
}
}
// findAgentIDByName scans ~/.elydora/*/config.json for a config whose agent_name
// matches the given name and returns the directory name (agent ID).
func findAgentIDByName(agentName string) string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
elydoraDir := filepath.Join(home, ".elydora")
entries, err := os.ReadDir(elydoraDir)
if err != nil {
return ""
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
configPath := filepath.Join(elydoraDir, entry.Name(), "config.json")
data, err := os.ReadFile(configPath)
if err != nil {
continue
}
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
continue
}
if name, _ := config["agent_name"].(string); name == agentName {
return entry.Name()
}
}
return ""
}
// ---------------------------------------------------------------------------
// status
// ---------------------------------------------------------------------------
@@ -219,7 +269,7 @@ func cmdAgents() {
names := sortedAgentNames()
for _, name := range names {
entry := plugins.SupportedAgents[name]
configPath := entry.ConfigDir + "/" + entry.ConfigFile
configPath := entry.ConfigDir + "/" + entry.ConfigFile // display only, not a filesystem path
fmt.Printf(" %-14s %-20s %s\n", name, entry.Name, configPath)
}
}
@@ -233,11 +283,11 @@ func sortedAgentNames() []string {
return names
}
// guardScriptPathForAgent returns the path to ~/.elydora/hooks/<agent>-guard.js.
func guardScriptPathForAgent(agentName string) (string, error) {
// guardScriptPathForAgent returns the path to ~/.elydora/<agentId>/guard.js.
func guardScriptPathForAgent(agentId string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".elydora", "hooks", agentName+"-guard.js"), nil
return filepath.Join(home, ".elydora", agentId, "guard.js"), nil
}
+1 -1
View File
@@ -26,6 +26,6 @@ type PluginStatus struct {
// AgentPlugin is the interface that every agent integration must implement.
type AgentPlugin interface {
Install(config InstallConfig) error
Uninstall() error
Uninstall(agentID string) error
Status() (PluginStatus, error)
}
+58 -24
View File
@@ -3,6 +3,7 @@ package plugins
import (
"fmt"
"os"
"path/filepath"
"strings"
)
@@ -11,7 +12,7 @@ import (
type ClaudeCodePlugin struct{}
func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("claudecode")
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -25,7 +26,7 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("claudecode")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -35,7 +36,7 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
settings, err := readJSONFile(configPath)
if err != nil {
@@ -100,12 +101,12 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
return nil
}
func (p *ClaudeCodePlugin) Uninstall() error {
func (p *ClaudeCodePlugin) Uninstall(agentID string) error {
configDir, err := expandHome("~/.claude")
if err != nil {
return err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
settings, err := readJSONFile(configPath)
if err != nil {
@@ -162,29 +163,26 @@ func (p *ClaudeCodePlugin) Uninstall() error {
return err
}
scriptPath, _ := hookScriptPath("claudecode")
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath("claudecode")
if gPath != "" {
os.Remove(gPath)
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for Claude Code.")
return nil
}
func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("claudecode")
if err != nil {
return PluginStatus{}, err
}
configDir, err := expandHome("~/.claude")
if err != nil {
return PluginStatus{}, err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
status := PluginStatus{
AgentName: "claudecode",
@@ -192,12 +190,7 @@ func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
ConfigPath: configPath,
}
// Check if hook script exists
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
// Check if hook is configured in settings
// Check if hook is configured in settings and extract hook script path
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
@@ -208,12 +201,53 @@ func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractElydoraScriptPath(hooks["PostToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
// extractElydoraScriptPath extracts the script path from a hook array's Elydora command entry.
func extractElydoraScriptPath(hookArray interface{}) string {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
// New format: { "hooks": [{ "type": "command", "command": "node /path/to/hook.js" }] }
if innerHooks, ok := m["hooks"].([]interface{}); ok {
for _, h := range innerHooks {
if hm, ok := h.(map[string]interface{}); ok {
if cmd, _ := hm["command"].(string); strings.Contains(cmd, "elydora") {
return extractPathFromNodeCommand(cmd)
}
}
}
}
// Old format: { "command": "node /path/to/hook.js" }
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
return extractPathFromNodeCommand(cmd)
}
}
}
return ""
}
// extractPathFromNodeCommand extracts the file path from a "node /path/to/script.js" command.
func extractPathFromNodeCommand(cmd string) string {
cmd = strings.TrimSpace(cmd)
if strings.HasPrefix(cmd, "node ") {
return strings.TrimSpace(cmd[5:])
}
return ""
}
// hasElydoraEntry checks if a hook array (interface{}) contains an Elydora entry.
func hasElydoraEntry(hookArray interface{}) bool {
arr, _ := hookArray.([]interface{})
+261
View File
@@ -0,0 +1,261 @@
package plugins
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// CopilotPlugin manages the Elydora audit hook for GitHub Copilot CLI.
// It writes/merges into .github/hooks/hooks.json (project-relative) using
// hooks.preToolUse[]/postToolUse[] with bash/powershell command fields.
type CopilotPlugin struct{}
func (p *CopilotPlugin) configPath() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("resolve working directory: %w", err)
}
return filepath.Join(cwd, ".github", "hooks", "hooks.json"), nil
}
func (p *CopilotPlugin) 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
}
}
configPath, err := p.configPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
// Ensure version field is set
settings["version"] = float64(1)
// Ensure hooks object exists
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
hooks = make(map[string]interface{})
}
// --- preToolUse (guard — freeze enforcement) ---
preToolUse, _ := hooks["preToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isCopilotElydoraEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
guardEntry := map[string]interface{}{
"type": "command",
"bash": "node " + guardPath,
"powershell": "node " + guardPath,
"timeoutSec": float64(5),
}
preFiltered = append(preFiltered, guardEntry)
hooks["preToolUse"] = preFiltered
// --- postToolUse (audit logging) ---
postToolUse, _ := hooks["postToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isCopilotElydoraEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
hookEntry := map[string]interface{}{
"type": "command",
"bash": "node " + scriptPath,
"powershell": "node " + scriptPath,
"timeoutSec": float64(5),
}
postFiltered = append(postFiltered, hookEntry)
hooks["postToolUse"] = postFiltered
settings["hooks"] = hooks
if err := writeJSONFile(configPath, settings); err != nil {
return err
}
fmt.Printf("Installed Elydora hook for Copilot CLI at %s\n", configPath)
return nil
}
func (p *CopilotPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
fmt.Println("No Copilot CLI hooks found.")
return nil
}
// Remove preToolUse Elydora entries
preToolUse, _ := hooks["preToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isCopilotElydoraEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
if len(preFiltered) == 0 {
delete(hooks, "preToolUse")
} else {
hooks["preToolUse"] = preFiltered
}
// Remove postToolUse Elydora entries
postToolUse, _ := hooks["postToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isCopilotElydoraEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
if len(postFiltered) == 0 {
delete(hooks, "postToolUse")
} else {
hooks["postToolUse"] = postFiltered
}
if len(hooks) == 0 {
delete(settings, "hooks")
} else {
settings["hooks"] = hooks
}
if err := writeJSONFile(configPath, settings); err != nil {
return 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 Copilot CLI.")
return nil
}
func (p *CopilotPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "copilot",
DisplayName: "Copilot CLI",
ConfigPath: configPath,
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks != nil {
preConfigured := hasCopilotElydoraEntry(hooks["preToolUse"])
postConfigured := hasCopilotElydoraEntry(hooks["postToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractCopilotElydoraScriptPath(hooks["postToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
// isCopilotElydoraEntry checks if a Copilot hook entry (bash/powershell fields) is an Elydora hook.
func isCopilotElydoraEntry(m map[string]interface{}) bool {
if bash, _ := m["bash"].(string); strings.Contains(bash, "elydora") {
return true
}
if ps, _ := m["powershell"].(string); strings.Contains(ps, "elydora") {
return true
}
return false
}
// hasCopilotElydoraEntry checks if a Copilot hook array contains an Elydora entry.
func hasCopilotElydoraEntry(hookArray interface{}) bool {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
if isCopilotElydoraEntry(m) {
return true
}
}
}
return false
}
// extractCopilotElydoraScriptPath extracts the script path from a Copilot hook array's Elydora command.
func extractCopilotElydoraScriptPath(hookArray interface{}) string {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
if bash, _ := m["bash"].(string); strings.Contains(bash, "elydora") {
return extractPathFromNodeCommand(bash)
}
if ps, _ := m["powershell"].(string); strings.Contains(ps, "elydora") {
return extractPathFromNodeCommand(ps)
}
}
}
return ""
}
+33 -21
View File
@@ -20,7 +20,7 @@ func (p *CursorPlugin) configPath() (string, error) {
}
func (p *CursorPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("cursor")
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -34,7 +34,7 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("cursor")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -99,12 +99,7 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
return nil
}
func (p *CursorPlugin) Uninstall() error {
scriptPath, err := hookScriptPath("cursor")
if err != nil {
return err
}
func (p *CursorPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
if err != nil {
return err
@@ -165,21 +160,21 @@ func (p *CursorPlugin) Uninstall() error {
return err
}
os.Remove(scriptPath)
gPath, _ := guardScriptPath("cursor")
if gPath != "" {
os.Remove(gPath)
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for Cursor.")
return nil
}
func (p *CursorPlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("cursor")
if err != nil {
return PluginStatus{}, err
}
configPath, err := p.configPath()
if err != nil {
return PluginStatus{}, err
@@ -191,10 +186,6 @@ func (p *CursorPlugin) Status() (PluginStatus, error) {
ConfigPath: configPath,
}
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
@@ -205,12 +196,33 @@ func (p *CursorPlugin) Status() (PluginStatus, error) {
preConfigured := hasCursorElydoraEntry(hooks["preToolUse"])
postConfigured := hasCursorElydoraEntry(hooks["postToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractCursorElydoraScriptPath(hooks["postToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
// extractCursorElydoraScriptPath extracts the script path from a Cursor hook array's Elydora command entry.
func extractCursorElydoraScriptPath(hookArray interface{}) string {
arr, _ := hookArray.([]interface{})
for _, entry := range arr {
if m, ok := entry.(map[string]interface{}); ok {
if cmd, _ := m["command"].(string); strings.Contains(cmd, "elydora") {
return extractPathFromNodeCommand(cmd)
}
}
}
return ""
}
// hasCursorElydoraEntry checks if a Cursor hook array (flat format) contains an Elydora entry.
func hasCursorElydoraEntry(hookArray interface{}) bool {
arr, _ := hookArray.([]interface{})
+24 -22
View File
@@ -3,6 +3,7 @@ package plugins
import (
"fmt"
"os"
"path/filepath"
)
// GeminiPlugin manages the Elydora audit hook for Gemini CLI.
@@ -10,7 +11,7 @@ import (
type GeminiPlugin struct{}
func (p *GeminiPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("gemini")
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -24,7 +25,7 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("gemini")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -34,7 +35,7 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
if err != nil {
return err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
settings, err := readJSONFile(configPath)
if err != nil {
@@ -99,12 +100,12 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
return nil
}
func (p *GeminiPlugin) Uninstall() error {
func (p *GeminiPlugin) Uninstall(agentID string) error {
configDir, err := expandHome("~/.gemini")
if err != nil {
return err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
settings, err := readJSONFile(configPath)
if err != nil {
@@ -161,29 +162,26 @@ func (p *GeminiPlugin) Uninstall() error {
return err
}
scriptPath, _ := hookScriptPath("gemini")
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath("gemini")
if gPath != "" {
os.Remove(gPath)
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for Gemini CLI.")
return nil
}
func (p *GeminiPlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("gemini")
if err != nil {
return PluginStatus{}, err
}
configDir, err := expandHome("~/.gemini")
if err != nil {
return PluginStatus{}, err
}
configPath := configDir + "/settings.json"
configPath := filepath.Join(configDir, "settings.json")
status := PluginStatus{
AgentName: "gemini",
@@ -191,10 +189,6 @@ func (p *GeminiPlugin) Status() (PluginStatus, error) {
ConfigPath: configPath,
}
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
@@ -205,6 +199,14 @@ func (p *GeminiPlugin) Status() (PluginStatus, error) {
preConfigured := hasElydoraEntry(hooks["BeforeTool"])
postConfigured := hasElydoraEntry(hooks["AfterTool"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractElydoraScriptPath(hooks["AfterTool"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
+25 -20
View File
@@ -7,7 +7,7 @@ import (
)
// GenerateHookScript writes a self-contained Node.js hook script to disk.
// The script reads config/key from ~/.elydora/agents/ at runtime, persists
// The script reads config/key from ~/.elydora/<agentId>/ at runtime, persists
// chain state, and performs fire-and-forget EOR submission with inline Ed25519 crypto.
// It always exits 0 so it never blocks the host agent.
func GenerateHookScript(destPath string, config InstallConfig) error {
@@ -21,7 +21,7 @@ func GenerateHookScript(destPath string, config InstallConfig) error {
return fmt.Errorf("write agent config: %w", err)
}
script := buildHookScript(config.AgentName)
script := buildHookScript(config.AgentName, config.AgentID)
if err := os.WriteFile(destPath, []byte(script), 0755); err != nil {
return fmt.Errorf("write hook script: %w", err)
}
@@ -29,16 +29,16 @@ func GenerateHookScript(destPath string, config InstallConfig) error {
}
// writeAgentConfig persists the agent configuration and private key to
// ~/.elydora/agents/<name>.json and ~/.elydora/agents/<name>.key so that
// ~/.elydora/<agentId>/config.json and ~/.elydora/<agentId>/private.key so that
// the generated hook script can read them at runtime without embedding secrets.
func writeAgentConfig(config InstallConfig) error {
home, err := os.UserHomeDir()
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
}
agentsDir := filepath.Join(home, ".elydora", "agents")
if err := os.MkdirAll(agentsDir, 0755); err != nil {
return fmt.Errorf("create agents directory: %w", err)
agentDir := filepath.Join(home, ".elydora", config.AgentID)
if err := os.MkdirAll(agentDir, 0755); err != nil {
return fmt.Errorf("create agent directory: %w", err)
}
baseURL := config.BaseURL
@@ -51,16 +51,19 @@ func writeAgentConfig(config InstallConfig) error {
"agent_id": %q,
"kid": %q,
"base_url": %q,
"token": %q
"token": %q,
"agent_name": %q
}
`, config.OrgID, config.AgentID, config.KID, baseURL, config.Token)
`, config.OrgID, config.AgentID, config.KID, baseURL, config.Token, config.AgentName)
configPath := filepath.Join(agentsDir, config.AgentName+".json")
configPath := filepath.Join(agentDir, "config.json")
// Note: 0600 permissions are a no-op on Windows; Go's os.Chmod only handles the read-only bit on Windows.
if err := os.WriteFile(configPath, []byte(configJSON), 0600); err != nil {
return fmt.Errorf("write agent config: %w", err)
}
keyPath := filepath.Join(agentsDir, config.AgentName+".key")
keyPath := filepath.Join(agentDir, "private.key")
// Note: 0600 permissions are a no-op on Windows; Go's os.Chmod only handles the read-only bit on Windows.
if err := os.WriteFile(keyPath, []byte(config.PrivateKey+"\n"), 0600); err != nil {
return fmt.Errorf("write agent key: %w", err)
}
@@ -72,7 +75,7 @@ func writeAgentConfig(config InstallConfig) error {
// whether the agent is frozen and blocks tool execution if so.
// It caches the agent status locally (60s TTL) to avoid hitting the API on every
// tool call, and fails open if the API is unreachable or config is missing.
func GenerateGuardScript(agentName string) string {
func GenerateGuardScript(agentName string, agentId string) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
@@ -86,9 +89,10 @@ const path = require('node:path');
const os = require('node:os');
const AGENT_NAME = %q;
const AGENT_ID = %q;
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.status.json');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
const CACHE_TTL_MS = 60000; // 60 seconds
// Consume stdin so the parent process doesn't block on a full pipe
@@ -166,10 +170,10 @@ async function main() {
}
main();
`, agentName, agentName)
`, agentName, agentName, agentId)
}
func buildHookScript(agentName string) string {
func buildHookScript(agentName string, agentId string) string {
return fmt.Sprintf(`#!/usr/bin/env node
'use strict';
@@ -184,11 +188,12 @@ const path = require('node:path');
const os = require('node:os');
const AGENT_NAME = %q;
const AGENT_ID = %q;
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
const KEY_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.key');
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, 'chain-state.json');
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, 'error.log');
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'chain-state.json');
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'error.log');
// ---------------------------------------------------------------------------
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
@@ -461,5 +466,5 @@ async function main() {
}
main();
`, agentName, agentName)
`, agentName, agentName, agentId)
}
+6 -6
View File
@@ -53,20 +53,20 @@ func writeJSONFile(path string, data map[string]interface{}) error {
return nil
}
// hookScriptPath returns the default path for the hook script inside ~/.elydora/hooks/.
func hookScriptPath(agentName string) (string, error) {
// hookScriptPath returns the default path for the hook script inside ~/.elydora/<agentId>/.
func hookScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".elydora", "hooks", agentName+"-hook.js"), nil
return filepath.Join(home, ".elydora", agentId, "hook.js"), nil
}
// guardScriptPath returns the default path for the guard script inside ~/.elydora/hooks/.
func guardScriptPath(agentName string) (string, error) {
// guardScriptPath returns the default path for the guard script inside ~/.elydora/<agentId>/.
func guardScriptPath(agentId string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".elydora", "hooks", agentName+"-guard.js"), nil
return filepath.Join(home, ".elydora", agentId, "guard.js"), nil
}
+224
View File
@@ -0,0 +1,224 @@
package plugins
import (
"fmt"
"os"
"path/filepath"
)
// KiroCliPlugin manages the Elydora audit hook for Kiro CLI.
// It merges PreToolUse/PostToolUse hooks into ~/.kiro/settings.json.
type KiroCliPlugin struct{}
func (p *KiroCliPlugin) configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".kiro", "settings.json"), nil
}
func (p *KiroCliPlugin) 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
}
}
configPath, err := p.configPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
hooks = make(map[string]interface{})
}
// --- PreToolUse (guard — freeze enforcement) ---
preToolUse, _ := hooks["PreToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
guardEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
"command": "node " + guardPath,
"timeout_ms": float64(5000),
},
},
}
preFiltered = append(preFiltered, guardEntry)
hooks["PreToolUse"] = preFiltered
// --- PostToolUse (audit logging) ---
postToolUse, _ := hooks["PostToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
hookEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
"command": "node " + scriptPath,
"timeout_ms": float64(5000),
},
},
}
postFiltered = append(postFiltered, hookEntry)
hooks["PostToolUse"] = postFiltered
settings["hooks"] = hooks
if err := writeJSONFile(configPath, settings); err != nil {
return err
}
fmt.Printf("Installed Elydora hook for Kiro CLI at %s\n", configPath)
return nil
}
func (p *KiroCliPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
if err != nil {
return err
}
settings, err := readJSONFile(configPath)
if err != nil {
return err
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
fmt.Println("No Kiro CLI hooks found.")
return nil
}
// Remove PreToolUse Elydora entries
preToolUse, _ := hooks["PreToolUse"].([]interface{})
var preFiltered []interface{}
for _, entry := range preToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
preFiltered = append(preFiltered, entry)
}
if len(preFiltered) == 0 {
delete(hooks, "PreToolUse")
} else {
hooks["PreToolUse"] = preFiltered
}
// Remove PostToolUse Elydora entries
postToolUse, _ := hooks["PostToolUse"].([]interface{})
var postFiltered []interface{}
for _, entry := range postToolUse {
if m, ok := entry.(map[string]interface{}); ok {
if isElydoraHookEntry(m) {
continue
}
}
postFiltered = append(postFiltered, entry)
}
if len(postFiltered) == 0 {
delete(hooks, "PostToolUse")
} else {
hooks["PostToolUse"] = postFiltered
}
if len(hooks) == 0 {
delete(settings, "hooks")
} else {
settings["hooks"] = hooks
}
if err := writeJSONFile(configPath, settings); err != nil {
return 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 Kiro CLI.")
return nil
}
func (p *KiroCliPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "kirocli",
DisplayName: "Kiro CLI",
ConfigPath: configPath,
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
}
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks != nil {
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractElydoraScriptPath(hooks["PostToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
@@ -7,11 +7,11 @@ import (
"path/filepath"
)
// KiroPlugin manages the Elydora audit hook for Kiro.
// KiroIdePlugin manages the Elydora audit hook for Kiro IDE.
// It writes a .kiro/hooks/elydora-audit.kiro.hook JSON file in the home directory.
type KiroPlugin struct{}
type KiroIdePlugin struct{}
func (p *KiroPlugin) configPath() (string, error) {
func (p *KiroIdePlugin) configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
@@ -19,8 +19,8 @@ func (p *KiroPlugin) configPath() (string, error) {
return filepath.Join(home, ".kiro", "hooks", "elydora-audit.kiro.hook"), nil
}
func (p *KiroPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("kiro")
func (p *KiroIdePlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -34,7 +34,7 @@ func (p *KiroPlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("kiro")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -49,16 +49,16 @@ func (p *KiroPlugin) Install(config InstallConfig) error {
return fmt.Errorf("create directory %s: %w", dir, err)
}
content := buildKiroHookFile(scriptPath, guardPath)
content := buildKiroIdeHookFile(scriptPath, guardPath)
if err := os.WriteFile(hookFile, []byte(content), 0644); err != nil {
return fmt.Errorf("write %s: %w", hookFile, err)
}
fmt.Printf("Installed Elydora hook for Kiro at %s\n", hookFile)
fmt.Printf("Installed Elydora hook for Kiro IDE at %s\n", hookFile)
return nil
}
func buildKiroHookFile(scriptPath, guardPath string) string {
func buildKiroIdeHookFile(scriptPath, guardPath string) string {
hookConfig := map[string]interface{}{
"name": "Elydora Audit",
"hooks": map[string]interface{}{
@@ -76,7 +76,7 @@ func buildKiroHookFile(scriptPath, guardPath string) string {
return string(encoded) + "\n"
}
func (p *KiroPlugin) Uninstall() error {
func (p *KiroIdePlugin) Uninstall(agentID string) error {
hookFile, err := p.configPath()
if err != nil {
return err
@@ -85,40 +85,33 @@ func (p *KiroPlugin) Uninstall() error {
return fmt.Errorf("remove %s: %w", hookFile, err)
}
scriptPath, _ := hookScriptPath("kiro")
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath("kiro")
if gPath != "" {
os.Remove(gPath)
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
fmt.Println("Uninstalled Elydora hook for Kiro.")
fmt.Println("Uninstalled Elydora hook for Kiro IDE.")
return nil
}
func (p *KiroPlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("kiro")
if err != nil {
return PluginStatus{}, err
}
func (p *KiroIdePlugin) Status() (PluginStatus, error) {
hookFile, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
status := PluginStatus{
AgentName: "kiro",
DisplayName: "Kiro",
AgentName: "kiroide",
DisplayName: "Kiro IDE",
ConfigPath: hookFile,
}
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
// Check if hook file exists and contains both pre_tool_use and post_tool_use
data, err := os.ReadFile(hookFile)
if err == nil {
@@ -129,6 +122,18 @@ func (p *KiroPlugin) Status() (PluginStatus, error) {
_, hasPre := hooks["pre_tool_use"]
_, hasPost := hooks["post_tool_use"]
status.HookConfigured = hasPre && hasPost
// Extract hook script path from the post_tool_use command
if postHook, ok := hooks["post_tool_use"].(map[string]interface{}); ok {
if cmd, _ := postHook["command"].(string); cmd != "" {
scriptPath := extractPathFromNodeCommand(cmd)
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
}
}
}
}
@@ -3,14 +3,23 @@ package plugins
import (
"fmt"
"os"
"path/filepath"
)
// AugmentPlugin manages the Elydora audit hook for Augment Code.
// It merges a PostToolUse hook into ~/.augment/settings.json.
type AugmentPlugin struct{}
// LettaPlugin manages the Elydora audit hook for Letta Code.
// It merges PreToolUse/PostToolUse hooks into ~/.letta/settings.json.
type LettaPlugin struct{}
func (p *AugmentPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("augment")
func (p *LettaPlugin) configPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("resolve home directory: %w", err)
}
return filepath.Join(home, ".letta", "settings.json"), nil
}
func (p *LettaPlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -24,17 +33,16 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("augment")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
}
configDir, err := expandHome("~/.augment")
configPath, err := p.configPath()
if err != nil {
return err
}
configPath := configDir + "/settings.json"
settings, err := readJSONFile(configPath)
if err != nil {
@@ -58,6 +66,7 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
preFiltered = append(preFiltered, entry)
}
guardEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
@@ -80,6 +89,7 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
postFiltered = append(postFiltered, entry)
}
hookEntry := map[string]interface{}{
"matcher": "*",
"hooks": []interface{}{
map[string]interface{}{
"type": "command",
@@ -95,16 +105,15 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
if err := writeJSONFile(configPath, settings); err != nil {
return err
}
fmt.Printf("Installed Elydora hook for Augment Code at %s\n", configPath)
fmt.Printf("Installed Elydora hook for Letta Code at %s\n", configPath)
return nil
}
func (p *AugmentPlugin) Uninstall() error {
configDir, err := expandHome("~/.augment")
func (p *LettaPlugin) Uninstall(agentID string) error {
configPath, err := p.configPath()
if err != nil {
return err
}
configPath := configDir + "/settings.json"
settings, err := readJSONFile(configPath)
if err != nil {
@@ -113,7 +122,7 @@ func (p *AugmentPlugin) Uninstall() error {
hooks, _ := settings["hooks"].(map[string]interface{})
if hooks == nil {
fmt.Println("No Augment Code hooks found.")
fmt.Println("No Letta Code hooks found.")
return nil
}
@@ -161,40 +170,32 @@ func (p *AugmentPlugin) Uninstall() error {
return err
}
scriptPath, _ := hookScriptPath("augment")
if scriptPath != "" {
os.Remove(scriptPath)
if agentID != "" {
scriptPath, _ := hookScriptPath(agentID)
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath(agentID)
if gPath != "" {
os.Remove(gPath)
}
}
gPath, _ := guardScriptPath("augment")
if gPath != "" {
os.Remove(gPath)
}
fmt.Println("Uninstalled Elydora hook for Augment Code.")
fmt.Println("Uninstalled Elydora hook for Letta Code.")
return nil
}
func (p *AugmentPlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("augment")
func (p *LettaPlugin) Status() (PluginStatus, error) {
configPath, err := p.configPath()
if err != nil {
return PluginStatus{}, err
}
configDir, err := expandHome("~/.augment")
if err != nil {
return PluginStatus{}, err
}
configPath := configDir + "/settings.json"
status := PluginStatus{
AgentName: "augment",
DisplayName: "Augment Code",
AgentName: "letta",
DisplayName: "Letta Code",
ConfigPath: configPath,
}
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
settings, err := readJSONFile(configPath)
if err != nil {
return status, nil
@@ -205,8 +206,17 @@ func (p *AugmentPlugin) Status() (PluginStatus, error) {
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
status.HookConfigured = preConfigured && postConfigured
// Extract hook script path from the configured command
scriptPath := extractElydoraScriptPath(hooks["PostToolUse"])
if scriptPath != "" {
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
}
}
status.Installed = status.HookConfigured && status.HookScriptExists
return status, nil
}
+39 -19
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
)
// OpenCodePlugin manages the Elydora audit hook for OpenCode.
@@ -19,7 +20,7 @@ func (p *OpenCodePlugin) configPath() (string, error) {
}
func (p *OpenCodePlugin) Install(config InstallConfig) error {
scriptPath, err := hookScriptPath("opencode")
scriptPath, err := hookScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -33,7 +34,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
guardPath := config.GuardScriptPath
if guardPath == "" {
guardPath, err = guardScriptPath("opencode")
guardPath, err = guardScriptPath(config.AgentID)
if err != nil {
return err
}
@@ -109,7 +110,7 @@ module.exports = {
`, guardPath, scriptPath)
}
func (p *OpenCodePlugin) Uninstall() error {
func (p *OpenCodePlugin) Uninstall(agentID string) error {
pluginFile, err := p.configPath()
if err != nil {
return err
@@ -118,13 +119,15 @@ func (p *OpenCodePlugin) Uninstall() error {
return fmt.Errorf("remove %s: %w", pluginFile, err)
}
scriptPath, _ := hookScriptPath("opencode")
if scriptPath != "" {
os.Remove(scriptPath)
}
gPath, _ := guardScriptPath("opencode")
if gPath != "" {
os.Remove(gPath)
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.")
@@ -132,11 +135,6 @@ func (p *OpenCodePlugin) Uninstall() error {
}
func (p *OpenCodePlugin) Status() (PluginStatus, error) {
scriptPath, err := hookScriptPath("opencode")
if err != nil {
return PluginStatus{}, err
}
pluginFile, err := p.configPath()
if err != nil {
return PluginStatus{}, err
@@ -148,12 +146,34 @@ func (p *OpenCodePlugin) Status() (PluginStatus, error) {
ConfigPath: pluginFile,
}
if _, err := os.Stat(scriptPath); err == nil {
status.HookScriptExists = true
}
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
+14 -8
View File
@@ -10,11 +10,13 @@ type AgentRegistryEntry struct {
// SupportedAgents is the canonical registry of all supported agent integrations.
var SupportedAgents = map[string]AgentRegistryEntry{
"claudecode": {Name: "Claude Code", ConfigDir: "~/.claude", ConfigFile: "settings.json"},
"cursor": {Name: "Cursor", ConfigDir: ".cursor", ConfigFile: "hooks.json"},
"cursor": {Name: "Cursor", ConfigDir: "~/.cursor", ConfigFile: "hooks.json"},
"gemini": {Name: "Gemini CLI", ConfigDir: "~/.gemini", ConfigFile: "settings.json"},
"augment": {Name: "Augment Code", ConfigDir: "~/.augment", ConfigFile: "settings.json"},
"kiro": {Name: "Kiro", ConfigDir: ".kiro/hooks", ConfigFile: "elydora-audit.kiro.hook"},
"opencode": {Name: "OpenCode", ConfigDir: ".opencode/plugins", ConfigFile: "elydora-audit.js"},
"kirocli": {Name: "Kiro CLI", ConfigDir: "~/.kiro", ConfigFile: "settings.json"},
"kiroide": {Name: "Kiro IDE", ConfigDir: "~/.kiro/hooks", ConfigFile: "elydora-audit.kiro.hook"},
"opencode": {Name: "OpenCode", ConfigDir: "~/.opencode/plugins", ConfigFile: "elydora-audit.js"},
"copilot": {Name: "Copilot CLI", ConfigDir: ".github/hooks", ConfigFile: "hooks.json"},
"letta": {Name: "Letta Code", ConfigDir: "~/.letta", ConfigFile: "settings.json"},
}
// NewPlugin creates a plugin instance for the given agent name.
@@ -27,12 +29,16 @@ func NewPlugin(agentName string) AgentPlugin {
return &CursorPlugin{}
case "gemini":
return &GeminiPlugin{}
case "augment":
return &AugmentPlugin{}
case "kiro":
return &KiroPlugin{}
case "kirocli":
return &KiroCliPlugin{}
case "kiroide":
return &KiroIdePlugin{}
case "opencode":
return &OpenCodePlugin{}
case "copilot":
return &CopilotPlugin{}
case "letta":
return &LettaPlugin{}
default:
return nil
}