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
+802
-171
No files matched your search
@@ -88,10 +88,12 @@ elydora install \
|
|||||||
| Agent | Key |
|
| Agent | Key |
|
||||||
|-------|-----|
|
|-------|-----|
|
||||||
| Claude Code | `claudecode` |
|
| Claude Code | `claudecode` |
|
||||||
|
| Copilot CLI | `copilot` |
|
||||||
| Cursor | `cursor` |
|
| Cursor | `cursor` |
|
||||||
| Gemini CLI | `gemini` |
|
| Gemini CLI | `gemini` |
|
||||||
| Augment Code | `augment` |
|
| Kiro CLI | `kirocli` |
|
||||||
| Kiro | `kiro` |
|
| Kiro IDE | `kiroide` |
|
||||||
|
| Letta Code | `letta` |
|
||||||
| OpenCode | `opencode` |
|
| OpenCode | `opencode` |
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|||||||
+61
-11
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -91,12 +92,12 @@ func cmdInstall(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate guard script (PreToolUse — freeze enforcement)
|
// Generate guard script (PreToolUse — freeze enforcement)
|
||||||
guardScriptPath, err := guardScriptPathForAgent(*agent)
|
guardScriptPath, err := guardScriptPathForAgent(*agentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
guardScript := plugins.GenerateGuardScript(*agent)
|
guardScript := plugins.GenerateGuardScript(*agent, *agentID)
|
||||||
if err := os.MkdirAll(filepath.Dir(guardScriptPath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(guardScriptPath), 0755); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error creating guard script directory: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error creating guard script directory: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -131,6 +132,7 @@ func cmdInstall(args []string) {
|
|||||||
func cmdUninstall(args []string) {
|
func cmdUninstall(args []string) {
|
||||||
fs := flag.NewFlagSet("uninstall", flag.ExitOnError)
|
fs := flag.NewFlagSet("uninstall", flag.ExitOnError)
|
||||||
agent := fs.String("agent", "", "Agent name (required)")
|
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)
|
fs.Parse(args)
|
||||||
|
|
||||||
@@ -147,16 +149,64 @@ func cmdUninstall(args []string) {
|
|||||||
os.Exit(1)
|
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)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also remove the guard script
|
// Remove the per-agent directory
|
||||||
guardPath, _ := guardScriptPathForAgent(*agent)
|
if resolvedAgentID != "" {
|
||||||
if guardPath != "" {
|
home, err := os.UserHomeDir()
|
||||||
os.Remove(guardPath)
|
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 ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -219,7 +269,7 @@ func cmdAgents() {
|
|||||||
names := sortedAgentNames()
|
names := sortedAgentNames()
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
entry := plugins.SupportedAgents[name]
|
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)
|
fmt.Printf(" %-14s %-20s %s\n", name, entry.Name, configPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,11 +283,11 @@ func sortedAgentNames() []string {
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
// guardScriptPathForAgent returns the path to ~/.elydora/hooks/<agent>-guard.js.
|
// guardScriptPathForAgent returns the path to ~/.elydora/<agentId>/guard.js.
|
||||||
func guardScriptPathForAgent(agentName string) (string, error) {
|
func guardScriptPathForAgent(agentId string) (string, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
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.
|
// AgentPlugin is the interface that every agent integration must implement.
|
||||||
type AgentPlugin interface {
|
type AgentPlugin interface {
|
||||||
Install(config InstallConfig) error
|
Install(config InstallConfig) error
|
||||||
Uninstall() error
|
Uninstall(agentID string) error
|
||||||
Status() (PluginStatus, error)
|
Status() (PluginStatus, error)
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ package plugins
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
type ClaudeCodePlugin struct{}
|
type ClaudeCodePlugin struct{}
|
||||||
|
|
||||||
func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
|
func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
|
||||||
scriptPath, err := hookScriptPath("claudecode")
|
scriptPath, err := hookScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -25,7 +26,7 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("claudecode")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -100,12 +101,12 @@ func (p *ClaudeCodePlugin) Install(config InstallConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ClaudeCodePlugin) Uninstall() error {
|
func (p *ClaudeCodePlugin) Uninstall(agentID string) error {
|
||||||
configDir, err := expandHome("~/.claude")
|
configDir, err := expandHome("~/.claude")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -162,29 +163,26 @@ func (p *ClaudeCodePlugin) Uninstall() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptPath, _ := hookScriptPath("claudecode")
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
if scriptPath != "" {
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
}
|
}
|
||||||
gPath, _ := guardScriptPath("claudecode")
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fmt.Println("Uninstalled Elydora hook for Claude Code.")
|
fmt.Println("Uninstalled Elydora hook for Claude Code.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
|
func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("claudecode")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir, err := expandHome("~/.claude")
|
configDir, err := expandHome("~/.claude")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
status := PluginStatus{
|
status := PluginStatus{
|
||||||
AgentName: "claudecode",
|
AgentName: "claudecode",
|
||||||
@@ -192,12 +190,7 @@ func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
|
|||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if hook script exists
|
// Check if hook is configured in settings and extract hook script path
|
||||||
if _, err := os.Stat(scriptPath); err == nil {
|
|
||||||
status.HookScriptExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if hook is configured in settings
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, nil
|
return status, nil
|
||||||
@@ -208,12 +201,53 @@ func (p *ClaudeCodePlugin) Status() (PluginStatus, error) {
|
|||||||
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
|
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
|
||||||
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
|
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
|
||||||
status.HookConfigured = preConfigured && postConfigured
|
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
|
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||||
return status, nil
|
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.
|
// hasElydoraEntry checks if a hook array (interface{}) contains an Elydora entry.
|
||||||
func hasElydoraEntry(hookArray interface{}) bool {
|
func hasElydoraEntry(hookArray interface{}) bool {
|
||||||
arr, _ := hookArray.([]interface{})
|
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 {
|
func (p *CursorPlugin) Install(config InstallConfig) error {
|
||||||
scriptPath, err := hookScriptPath("cursor")
|
scriptPath, err := hookScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("cursor")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,12 +99,7 @@ func (p *CursorPlugin) Install(config InstallConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CursorPlugin) Uninstall() error {
|
func (p *CursorPlugin) Uninstall(agentID string) error {
|
||||||
scriptPath, err := hookScriptPath("cursor")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath, err := p.configPath()
|
configPath, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -165,21 +160,21 @@ func (p *CursorPlugin) Uninstall() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
gPath, _ := guardScriptPath("cursor")
|
}
|
||||||
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fmt.Println("Uninstalled Elydora hook for Cursor.")
|
fmt.Println("Uninstalled Elydora hook for Cursor.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *CursorPlugin) Status() (PluginStatus, error) {
|
func (p *CursorPlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("cursor")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configPath, err := p.configPath()
|
configPath, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
@@ -191,10 +186,6 @@ func (p *CursorPlugin) Status() (PluginStatus, error) {
|
|||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(scriptPath); err == nil {
|
|
||||||
status.HookScriptExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, nil
|
return status, nil
|
||||||
@@ -205,12 +196,33 @@ func (p *CursorPlugin) Status() (PluginStatus, error) {
|
|||||||
preConfigured := hasCursorElydoraEntry(hooks["preToolUse"])
|
preConfigured := hasCursorElydoraEntry(hooks["preToolUse"])
|
||||||
postConfigured := hasCursorElydoraEntry(hooks["postToolUse"])
|
postConfigured := hasCursorElydoraEntry(hooks["postToolUse"])
|
||||||
status.HookConfigured = preConfigured && postConfigured
|
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
|
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||||
return status, nil
|
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.
|
// hasCursorElydoraEntry checks if a Cursor hook array (flat format) contains an Elydora entry.
|
||||||
func hasCursorElydoraEntry(hookArray interface{}) bool {
|
func hasCursorElydoraEntry(hookArray interface{}) bool {
|
||||||
arr, _ := hookArray.([]interface{})
|
arr, _ := hookArray.([]interface{})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package plugins
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GeminiPlugin manages the Elydora audit hook for Gemini CLI.
|
// GeminiPlugin manages the Elydora audit hook for Gemini CLI.
|
||||||
@@ -10,7 +11,7 @@ import (
|
|||||||
type GeminiPlugin struct{}
|
type GeminiPlugin struct{}
|
||||||
|
|
||||||
func (p *GeminiPlugin) Install(config InstallConfig) error {
|
func (p *GeminiPlugin) Install(config InstallConfig) error {
|
||||||
scriptPath, err := hookScriptPath("gemini")
|
scriptPath, err := hookScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -24,7 +25,7 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("gemini")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -34,7 +35,7 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -99,12 +100,12 @@ func (p *GeminiPlugin) Install(config InstallConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GeminiPlugin) Uninstall() error {
|
func (p *GeminiPlugin) Uninstall(agentID string) error {
|
||||||
configDir, err := expandHome("~/.gemini")
|
configDir, err := expandHome("~/.gemini")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -161,29 +162,26 @@ func (p *GeminiPlugin) Uninstall() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptPath, _ := hookScriptPath("gemini")
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
if scriptPath != "" {
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
}
|
}
|
||||||
gPath, _ := guardScriptPath("gemini")
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fmt.Println("Uninstalled Elydora hook for Gemini CLI.")
|
fmt.Println("Uninstalled Elydora hook for Gemini CLI.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *GeminiPlugin) Status() (PluginStatus, error) {
|
func (p *GeminiPlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("gemini")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configDir, err := expandHome("~/.gemini")
|
configDir, err := expandHome("~/.gemini")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
configPath := filepath.Join(configDir, "settings.json")
|
||||||
|
|
||||||
status := PluginStatus{
|
status := PluginStatus{
|
||||||
AgentName: "gemini",
|
AgentName: "gemini",
|
||||||
@@ -191,10 +189,6 @@ func (p *GeminiPlugin) Status() (PluginStatus, error) {
|
|||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(scriptPath); err == nil {
|
|
||||||
status.HookScriptExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, nil
|
return status, nil
|
||||||
@@ -205,6 +199,14 @@ func (p *GeminiPlugin) Status() (PluginStatus, error) {
|
|||||||
preConfigured := hasElydoraEntry(hooks["BeforeTool"])
|
preConfigured := hasElydoraEntry(hooks["BeforeTool"])
|
||||||
postConfigured := hasElydoraEntry(hooks["AfterTool"])
|
postConfigured := hasElydoraEntry(hooks["AfterTool"])
|
||||||
status.HookConfigured = preConfigured && postConfigured
|
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
|
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// GenerateHookScript writes a self-contained Node.js hook script to disk.
|
// 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.
|
// chain state, and performs fire-and-forget EOR submission with inline Ed25519 crypto.
|
||||||
// It always exits 0 so it never blocks the host agent.
|
// It always exits 0 so it never blocks the host agent.
|
||||||
func GenerateHookScript(destPath string, config InstallConfig) error {
|
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)
|
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 {
|
if err := os.WriteFile(destPath, []byte(script), 0755); err != nil {
|
||||||
return fmt.Errorf("write hook script: %w", err)
|
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
|
// 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.
|
// the generated hook script can read them at runtime without embedding secrets.
|
||||||
func writeAgentConfig(config InstallConfig) error {
|
func writeAgentConfig(config InstallConfig) error {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resolve home directory: %w", err)
|
return fmt.Errorf("resolve home directory: %w", err)
|
||||||
}
|
}
|
||||||
agentsDir := filepath.Join(home, ".elydora", "agents")
|
agentDir := filepath.Join(home, ".elydora", config.AgentID)
|
||||||
if err := os.MkdirAll(agentsDir, 0755); err != nil {
|
if err := os.MkdirAll(agentDir, 0755); err != nil {
|
||||||
return fmt.Errorf("create agents directory: %w", err)
|
return fmt.Errorf("create agent directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseURL := config.BaseURL
|
baseURL := config.BaseURL
|
||||||
@@ -51,16 +51,19 @@ func writeAgentConfig(config InstallConfig) error {
|
|||||||
"agent_id": %q,
|
"agent_id": %q,
|
||||||
"kid": %q,
|
"kid": %q,
|
||||||
"base_url": %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 {
|
if err := os.WriteFile(configPath, []byte(configJSON), 0600); err != nil {
|
||||||
return fmt.Errorf("write agent config: %w", err)
|
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 {
|
if err := os.WriteFile(keyPath, []byte(config.PrivateKey+"\n"), 0600); err != nil {
|
||||||
return fmt.Errorf("write agent key: %w", err)
|
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.
|
// 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
|
// 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.
|
// 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
|
return fmt.Sprintf(`#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -86,9 +89,10 @@ const path = require('node:path');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
|
|
||||||
const AGENT_NAME = %q;
|
const AGENT_NAME = %q;
|
||||||
|
const AGENT_ID = %q;
|
||||||
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
||||||
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
|
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
|
||||||
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.status.json');
|
const STATUS_CACHE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'status-cache.json');
|
||||||
const CACHE_TTL_MS = 60000; // 60 seconds
|
const CACHE_TTL_MS = 60000; // 60 seconds
|
||||||
|
|
||||||
// Consume stdin so the parent process doesn't block on a full pipe
|
// Consume stdin so the parent process doesn't block on a full pipe
|
||||||
@@ -166,10 +170,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
return fmt.Sprintf(`#!/usr/bin/env node
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -184,11 +188,12 @@ const path = require('node:path');
|
|||||||
const os = require('node:os');
|
const os = require('node:os');
|
||||||
|
|
||||||
const AGENT_NAME = %q;
|
const AGENT_NAME = %q;
|
||||||
|
const AGENT_ID = %q;
|
||||||
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
const ELYDORA_DIR = path.join(os.homedir(), '.elydora');
|
||||||
const CONFIG_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.json');
|
const CONFIG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'config.json');
|
||||||
const KEY_PATH = path.join(ELYDORA_DIR, 'agents', AGENT_NAME + '.key');
|
const KEY_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'private.key');
|
||||||
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, 'chain-state.json');
|
const CHAIN_STATE_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'chain-state.json');
|
||||||
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, 'error.log');
|
const ERROR_LOG_PATH = path.join(ELYDORA_DIR, AGENT_ID, 'error.log');
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
|
// Embedded crypto (from @elydora/sdk src/crypto.ts + src/utils.ts)
|
||||||
@@ -461,5 +466,5 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
`, agentName, agentName)
|
`, agentName, agentName, agentId)
|
||||||
}
|
}
|
||||||
@@ -53,20 +53,20 @@ func writeJSONFile(path string, data map[string]interface{}) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// hookScriptPath returns the default path for the hook script inside ~/.elydora/hooks/.
|
// hookScriptPath returns the default path for the hook script inside ~/.elydora/<agentId>/.
|
||||||
func hookScriptPath(agentName string) (string, error) {
|
func hookScriptPath(agentId string) (string, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
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/.
|
// guardScriptPath returns the default path for the guard script inside ~/.elydora/<agentId>/.
|
||||||
func guardScriptPath(agentName string) (string, error) {
|
func guardScriptPath(agentId string) (string, error) {
|
||||||
home, err := os.UserHomeDir()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
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"
|
"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.
|
// 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()
|
home, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("resolve home directory: %w", err)
|
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
|
return filepath.Join(home, ".kiro", "hooks", "elydora-audit.kiro.hook"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *KiroPlugin) Install(config InstallConfig) error {
|
func (p *KiroIdePlugin) Install(config InstallConfig) error {
|
||||||
scriptPath, err := hookScriptPath("kiro")
|
scriptPath, err := hookScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ func (p *KiroPlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("kiro")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -49,16 +49,16 @@ func (p *KiroPlugin) Install(config InstallConfig) error {
|
|||||||
return fmt.Errorf("create directory %s: %w", dir, err)
|
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 {
|
if err := os.WriteFile(hookFile, []byte(content), 0644); err != nil {
|
||||||
return fmt.Errorf("write %s: %w", hookFile, err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKiroHookFile(scriptPath, guardPath string) string {
|
func buildKiroIdeHookFile(scriptPath, guardPath string) string {
|
||||||
hookConfig := map[string]interface{}{
|
hookConfig := map[string]interface{}{
|
||||||
"name": "Elydora Audit",
|
"name": "Elydora Audit",
|
||||||
"hooks": map[string]interface{}{
|
"hooks": map[string]interface{}{
|
||||||
@@ -76,7 +76,7 @@ func buildKiroHookFile(scriptPath, guardPath string) string {
|
|||||||
return string(encoded) + "\n"
|
return string(encoded) + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *KiroPlugin) Uninstall() error {
|
func (p *KiroIdePlugin) Uninstall(agentID string) error {
|
||||||
hookFile, err := p.configPath()
|
hookFile, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -85,40 +85,33 @@ func (p *KiroPlugin) Uninstall() error {
|
|||||||
return fmt.Errorf("remove %s: %w", hookFile, err)
|
return fmt.Errorf("remove %s: %w", hookFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptPath, _ := hookScriptPath("kiro")
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
if scriptPath != "" {
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
}
|
}
|
||||||
gPath, _ := guardScriptPath("kiro")
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Uninstalled Elydora hook for Kiro.")
|
fmt.Println("Uninstalled Elydora hook for Kiro IDE.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *KiroPlugin) Status() (PluginStatus, error) {
|
func (p *KiroIdePlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("kiro")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hookFile, err := p.configPath()
|
hookFile, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
status := PluginStatus{
|
status := PluginStatus{
|
||||||
AgentName: "kiro",
|
AgentName: "kiroide",
|
||||||
DisplayName: "Kiro",
|
DisplayName: "Kiro IDE",
|
||||||
ConfigPath: hookFile,
|
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
|
// Check if hook file exists and contains both pre_tool_use and post_tool_use
|
||||||
data, err := os.ReadFile(hookFile)
|
data, err := os.ReadFile(hookFile)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -129,6 +122,18 @@ func (p *KiroPlugin) Status() (PluginStatus, error) {
|
|||||||
_, hasPre := hooks["pre_tool_use"]
|
_, hasPre := hooks["pre_tool_use"]
|
||||||
_, hasPost := hooks["post_tool_use"]
|
_, hasPost := hooks["post_tool_use"]
|
||||||
status.HookConfigured = hasPre && hasPost
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AugmentPlugin manages the Elydora audit hook for Augment Code.
|
// LettaPlugin manages the Elydora audit hook for Letta Code.
|
||||||
// It merges a PostToolUse hook into ~/.augment/settings.json.
|
// It merges PreToolUse/PostToolUse hooks into ~/.letta/settings.json.
|
||||||
type AugmentPlugin struct{}
|
type LettaPlugin struct{}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Install(config InstallConfig) error {
|
func (p *LettaPlugin) configPath() (string, error) {
|
||||||
scriptPath, err := hookScriptPath("augment")
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -24,17 +33,16 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("augment")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir, err := expandHome("~/.augment")
|
configPath, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,6 +66,7 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
|
|||||||
preFiltered = append(preFiltered, entry)
|
preFiltered = append(preFiltered, entry)
|
||||||
}
|
}
|
||||||
guardEntry := map[string]interface{}{
|
guardEntry := map[string]interface{}{
|
||||||
|
"matcher": "*",
|
||||||
"hooks": []interface{}{
|
"hooks": []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
@@ -80,6 +89,7 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
|
|||||||
postFiltered = append(postFiltered, entry)
|
postFiltered = append(postFiltered, entry)
|
||||||
}
|
}
|
||||||
hookEntry := map[string]interface{}{
|
hookEntry := map[string]interface{}{
|
||||||
|
"matcher": "*",
|
||||||
"hooks": []interface{}{
|
"hooks": []interface{}{
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
@@ -95,16 +105,15 @@ func (p *AugmentPlugin) Install(config InstallConfig) error {
|
|||||||
if err := writeJSONFile(configPath, settings); err != nil {
|
if err := writeJSONFile(configPath, settings); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Uninstall() error {
|
func (p *LettaPlugin) Uninstall(agentID string) error {
|
||||||
configDir, err := expandHome("~/.augment")
|
configPath, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
configPath := configDir + "/settings.json"
|
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,7 +122,7 @@ func (p *AugmentPlugin) Uninstall() error {
|
|||||||
|
|
||||||
hooks, _ := settings["hooks"].(map[string]interface{})
|
hooks, _ := settings["hooks"].(map[string]interface{})
|
||||||
if hooks == nil {
|
if hooks == nil {
|
||||||
fmt.Println("No Augment Code hooks found.")
|
fmt.Println("No Letta Code hooks found.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,40 +170,32 @@ func (p *AugmentPlugin) Uninstall() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptPath, _ := hookScriptPath("augment")
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
if scriptPath != "" {
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
}
|
}
|
||||||
gPath, _ := guardScriptPath("augment")
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
fmt.Println("Uninstalled Elydora hook for Augment Code.")
|
}
|
||||||
|
fmt.Println("Uninstalled Elydora hook for Letta Code.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AugmentPlugin) Status() (PluginStatus, error) {
|
func (p *LettaPlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("augment")
|
configPath, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir, err := expandHome("~/.augment")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
configPath := configDir + "/settings.json"
|
|
||||||
|
|
||||||
status := PluginStatus{
|
status := PluginStatus{
|
||||||
AgentName: "augment",
|
AgentName: "letta",
|
||||||
DisplayName: "Augment Code",
|
DisplayName: "Letta Code",
|
||||||
ConfigPath: configPath,
|
ConfigPath: configPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(scriptPath); err == nil {
|
|
||||||
status.HookScriptExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := readJSONFile(configPath)
|
settings, err := readJSONFile(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status, nil
|
return status, nil
|
||||||
@@ -205,8 +206,17 @@ func (p *AugmentPlugin) Status() (PluginStatus, error) {
|
|||||||
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
|
preConfigured := hasElydoraEntry(hooks["PreToolUse"])
|
||||||
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
|
postConfigured := hasElydoraEntry(hooks["PostToolUse"])
|
||||||
status.HookConfigured = preConfigured && postConfigured
|
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
|
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// OpenCodePlugin manages the Elydora audit hook for OpenCode.
|
// 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 {
|
func (p *OpenCodePlugin) Install(config InstallConfig) error {
|
||||||
scriptPath, err := hookScriptPath("opencode")
|
scriptPath, err := hookScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -33,7 +34,7 @@ func (p *OpenCodePlugin) Install(config InstallConfig) error {
|
|||||||
|
|
||||||
guardPath := config.GuardScriptPath
|
guardPath := config.GuardScriptPath
|
||||||
if guardPath == "" {
|
if guardPath == "" {
|
||||||
guardPath, err = guardScriptPath("opencode")
|
guardPath, err = guardScriptPath(config.AgentID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -109,7 +110,7 @@ module.exports = {
|
|||||||
`, guardPath, scriptPath)
|
`, guardPath, scriptPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OpenCodePlugin) Uninstall() error {
|
func (p *OpenCodePlugin) Uninstall(agentID string) error {
|
||||||
pluginFile, err := p.configPath()
|
pluginFile, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -118,25 +119,22 @@ func (p *OpenCodePlugin) Uninstall() error {
|
|||||||
return fmt.Errorf("remove %s: %w", pluginFile, err)
|
return fmt.Errorf("remove %s: %w", pluginFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptPath, _ := hookScriptPath("opencode")
|
if agentID != "" {
|
||||||
|
scriptPath, _ := hookScriptPath(agentID)
|
||||||
if scriptPath != "" {
|
if scriptPath != "" {
|
||||||
os.Remove(scriptPath)
|
os.Remove(scriptPath)
|
||||||
}
|
}
|
||||||
gPath, _ := guardScriptPath("opencode")
|
gPath, _ := guardScriptPath(agentID)
|
||||||
if gPath != "" {
|
if gPath != "" {
|
||||||
os.Remove(gPath)
|
os.Remove(gPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Uninstalled Elydora hook for OpenCode.")
|
fmt.Println("Uninstalled Elydora hook for OpenCode.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OpenCodePlugin) Status() (PluginStatus, error) {
|
func (p *OpenCodePlugin) Status() (PluginStatus, error) {
|
||||||
scriptPath, err := hookScriptPath("opencode")
|
|
||||||
if err != nil {
|
|
||||||
return PluginStatus{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginFile, err := p.configPath()
|
pluginFile, err := p.configPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return PluginStatus{}, err
|
return PluginStatus{}, err
|
||||||
@@ -148,12 +146,34 @@ func (p *OpenCodePlugin) Status() (PluginStatus, error) {
|
|||||||
ConfigPath: pluginFile,
|
ConfigPath: pluginFile,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(scriptPath); err == nil {
|
|
||||||
status.HookScriptExists = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := os.Stat(pluginFile); err == nil {
|
if _, err := os.Stat(pluginFile); err == nil {
|
||||||
status.HookConfigured = true
|
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
|
status.Installed = status.HookConfigured && status.HookScriptExists
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ type AgentRegistryEntry struct {
|
|||||||
// SupportedAgents is the canonical registry of all supported agent integrations.
|
// SupportedAgents is the canonical registry of all supported agent integrations.
|
||||||
var SupportedAgents = map[string]AgentRegistryEntry{
|
var SupportedAgents = map[string]AgentRegistryEntry{
|
||||||
"claudecode": {Name: "Claude Code", ConfigDir: "~/.claude", ConfigFile: "settings.json"},
|
"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"},
|
"gemini": {Name: "Gemini CLI", ConfigDir: "~/.gemini", ConfigFile: "settings.json"},
|
||||||
"augment": {Name: "Augment Code", ConfigDir: "~/.augment", ConfigFile: "settings.json"},
|
"kirocli": {Name: "Kiro CLI", ConfigDir: "~/.kiro", ConfigFile: "settings.json"},
|
||||||
"kiro": {Name: "Kiro", ConfigDir: ".kiro/hooks", ConfigFile: "elydora-audit.kiro.hook"},
|
"kiroide": {Name: "Kiro IDE", ConfigDir: "~/.kiro/hooks", ConfigFile: "elydora-audit.kiro.hook"},
|
||||||
"opencode": {Name: "OpenCode", ConfigDir: ".opencode/plugins", ConfigFile: "elydora-audit.js"},
|
"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.
|
// NewPlugin creates a plugin instance for the given agent name.
|
||||||
@@ -27,12 +29,16 @@ func NewPlugin(agentName string) AgentPlugin {
|
|||||||
return &CursorPlugin{}
|
return &CursorPlugin{}
|
||||||
case "gemini":
|
case "gemini":
|
||||||
return &GeminiPlugin{}
|
return &GeminiPlugin{}
|
||||||
case "augment":
|
case "kirocli":
|
||||||
return &AugmentPlugin{}
|
return &KiroCliPlugin{}
|
||||||
case "kiro":
|
case "kiroide":
|
||||||
return &KiroPlugin{}
|
return &KiroIdePlugin{}
|
||||||
case "opencode":
|
case "opencode":
|
||||||
return &OpenCodePlugin{}
|
return &OpenCodePlugin{}
|
||||||
|
case "copilot":
|
||||||
|
return &CopilotPlugin{}
|
||||||
|
case "letta":
|
||||||
|
return &LettaPlugin{}
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user