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:
1 parent
c21e9fcfc6
commit
d80819093a
14 files changed
+830
-199
No files matched your search
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{})
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user