diff --git a/cmd/elydora/plugins/augment_contract.go b/cmd/elydora/plugins/augment_contract.go index 39088d4..d38ef1f 100644 --- a/cmd/elydora/plugins/augment_contract.go +++ b/cmd/elydora/plugins/augment_contract.go @@ -67,14 +67,6 @@ type augmentRuntimeContract struct { auditWrapper string } -func cloneAugmentObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - func validateAugmentHandler( value any, event string, @@ -113,7 +105,7 @@ func validateAugmentHandler( return nil, fmt.Errorf("%s timeout must be a positive finite number", label) } } - return cloneAugmentObject(handler), nil + return cloneJSONObject(handler), nil } func validateAugmentMetadata(value any, label string) error { @@ -168,7 +160,7 @@ func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup } handlers = append(handlers, handler) } - return augmentGroup{object: cloneAugmentObject(object), handlers: handlers}, nil + return augmentGroup{object: cloneJSONObject(object), handlers: handlers}, nil } func readAugmentHooks(root map[string]any) (augmentHooks, error) { @@ -247,7 +239,7 @@ func renderAugmentDocument( if !document.exists && len(hooks) == 0 { return &augmentRenderedDocument{document: document}, nil } - root := cloneAugmentObject(document.root) + root := cloneJSONObject(document.root) if len(hooks) == 0 { delete(root, "hooks") } else { @@ -280,7 +272,7 @@ func renderAugmentHooks(hooks augmentHooks) map[string]any { for event, groups := range hooks { values := make([]any, 0, len(groups)) for _, group := range groups { - object := cloneAugmentObject(group.object) + object := cloneJSONObject(group.object) handlers := make([]any, 0, len(group.handlers)) for _, handler := range group.handlers { handlers = append(handlers, handler) diff --git a/cmd/elydora/plugins/claudecode_contract.go b/cmd/elydora/plugins/claudecode_contract.go index 687d68c..5aa454b 100644 --- a/cmd/elydora/plugins/claudecode_contract.go +++ b/cmd/elydora/plugins/claudecode_contract.go @@ -79,14 +79,6 @@ type claudeRenderedDocument struct { type claudeRuntimeContract = managedRuntimeContract -func cloneClaudeObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - func cloneClaudeHooks(source claudeHooks) claudeHooks { clone := make(claudeHooks, len(source)) for event, groups := range source { @@ -295,7 +287,7 @@ func validateClaudeHandler( } } } - return cloneClaudeObject(handler), nil + return cloneJSONObject(handler), nil } func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup, error) { @@ -328,7 +320,7 @@ func validateClaudeGroup(value any, event string, groupIndex int) (claudeGroup, } handlers = append(handlers, handler) } - return claudeGroup{object: cloneClaudeObject(object), handlers: handlers}, nil + return claudeGroup{object: cloneJSONObject(object), handlers: handlers}, nil } func readClaudeHooks(root map[string]any) (claudeHooks, error) { @@ -407,7 +399,7 @@ func renderClaudeHooks(hooks claudeHooks) map[string]any { for event, groups := range hooks { values := make([]any, 0, len(groups)) for _, group := range groups { - object := cloneClaudeObject(group.object) + object := cloneJSONObject(group.object) handlers := make([]any, 0, len(group.handlers)) for _, handler := range group.handlers { handlers = append(handlers, handler) @@ -435,7 +427,7 @@ func renderClaudeDocument( document: document, changed: true, remove: true, }, nil } - root := cloneClaudeObject(document.root) + root := cloneJSONObject(document.root) if len(hooks) == 0 { delete(root, "hooks") } else { diff --git a/cmd/elydora/plugins/claudecode_ownership.go b/cmd/elydora/plugins/claudecode_ownership.go index 3680cb7..ce4a21a 100644 --- a/cmd/elydora/plugins/claudecode_ownership.go +++ b/cmd/elydora/plugins/claudecode_ownership.go @@ -99,7 +99,7 @@ func removeManagedClaudeGroups( kept = append(kept, handler) } if len(kept) > 0 || !exactManagedClaudeGroup(group) { - object := cloneClaudeObject(group.object) + object := cloneJSONObject(group.object) object["hooks"] = group.object["hooks"] result = append(result, claudeGroup{object: object, handlers: kept}) } diff --git a/cmd/elydora/plugins/codex_contract.go b/cmd/elydora/plugins/codex_contract.go index 25983eb..b285527 100644 --- a/cmd/elydora/plugins/codex_contract.go +++ b/cmd/elydora/plugins/codex_contract.go @@ -39,14 +39,6 @@ type codexRenderedDocument struct { type codexRuntimeContract = managedRuntimeContract -func cloneCodexObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - func cloneCodexHooks(source codexHooks) codexHooks { clone := make(codexHooks, len(source)) for event, groups := range source { @@ -147,7 +139,7 @@ func removeCodexGroups( kept = append(kept, handler) } if len(kept) > 0 || !exactCodexMatcherGroup(group) { - next := cloneCodexObject(group) + next := cloneJSONObject(group) next["hooks"] = kept result = append(result, next) } @@ -223,7 +215,7 @@ func renderCodexDocument( if len(hooks) == 0 && entirelyManagedCodexDocument(document) { return &codexRenderedDocument{document: document, changed: true, remove: true}, nil } - root := cloneCodexObject(document.root) + root := cloneJSONObject(document.root) if len(hooks) == 0 { delete(root, "hooks") } else { diff --git a/cmd/elydora/plugins/copilot_contract.go b/cmd/elydora/plugins/copilot_contract.go index f8e30f9..d1e842e 100644 --- a/cmd/elydora/plugins/copilot_contract.go +++ b/cmd/elydora/plugins/copilot_contract.go @@ -74,14 +74,6 @@ func copilotManagedEntryForHandler( return resolveManagedScript(scriptPath, scriptName) } -func cloneCopilotObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - func cloneCopilotHooks(source copilotHooks) copilotHooks { clone := make(copilotHooks, len(source)) for event, handlers := range source { @@ -240,7 +232,7 @@ func renderCopilotDocument( document: document, changed: true, remove: true, }, nil } - root := cloneCopilotObject(document.root) + root := cloneJSONObject(document.root) root["version"] = float64(1) if len(hooks) == 0 { delete(root, "hooks") diff --git a/cmd/elydora/plugins/copilot_schema.go b/cmd/elydora/plugins/copilot_schema.go index 944d6fe..122db5b 100644 --- a/cmd/elydora/plugins/copilot_schema.go +++ b/cmd/elydora/plugins/copilot_schema.go @@ -274,7 +274,7 @@ func validateCopilotHooks(value any, label string) (copilotHooks, error) { if err := validateCopilotHandler(handler, event, itemLabel); err != nil { return nil, err } - handlers = append(handlers, cloneCopilotObject(handler)) + handlers = append(handlers, cloneJSONObject(handler)) } hooks[event] = handlers } diff --git a/cmd/elydora/plugins/grok_contract.go b/cmd/elydora/plugins/grok_contract.go index 7a58d3e..0cb35f1 100644 --- a/cmd/elydora/plugins/grok_contract.go +++ b/cmd/elydora/plugins/grok_contract.go @@ -46,14 +46,6 @@ type grokRenderedDocument struct { remove bool } -func cloneGrokObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - for key, item := range value { - clone[key] = item - } - return clone -} - func cloneGrokHooks(source grokHooks) grokHooks { clone := make(grokHooks, len(source)) for event, groups := range source { @@ -120,7 +112,7 @@ func validateGrokHandler( } } } - return cloneGrokObject(handler), nil + return cloneJSONObject(handler), nil } func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, error) { @@ -158,7 +150,7 @@ func validateGrokGroup(value any, event string, groupIndex int) (grokGroup, erro } handlers = append(handlers, handler) } - return grokGroup{object: cloneGrokObject(object), handlers: handlers}, nil + return grokGroup{object: cloneJSONObject(object), handlers: handlers}, nil } func readGrokHooks(root map[string]any) (grokHooks, error) { @@ -221,7 +213,7 @@ func renderGrokHooks(hooks grokHooks) map[string]any { for event, groups := range hooks { values := make([]any, 0, len(groups)) for _, group := range groups { - object := cloneGrokObject(group.object) + object := cloneJSONObject(group.object) handlers := make([]any, 0, len(group.handlers)) for _, handler := range group.handlers { handlers = append(handlers, handler) @@ -378,7 +370,7 @@ func renderGrokDocument( document: document, changed: true, remove: true, }, nil } - root := cloneGrokObject(document.root) + root := cloneJSONObject(document.root) if len(hooks) == 0 { delete(root, "hooks") } else { diff --git a/cmd/elydora/plugins/jsonc.go b/cmd/elydora/plugins/jsonc.go index 9dee1d2..931f9ca 100644 --- a/cmd/elydora/plugins/jsonc.go +++ b/cmd/elydora/plugins/jsonc.go @@ -293,3 +293,11 @@ func jsonPointer(path []any) string { } return pointer.String() } + +func cloneJSONObject(value map[string]any) map[string]any { + clone := make(map[string]any, len(value)) + for key, item := range value { + clone[key] = item + } + return clone +} diff --git a/cmd/elydora/plugins/jsonutil.go b/cmd/elydora/plugins/jsonutil.go index 1cbfccd..e83bde1 100644 --- a/cmd/elydora/plugins/jsonutil.go +++ b/cmd/elydora/plugins/jsonutil.go @@ -2,6 +2,7 @@ package plugins import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -49,6 +50,20 @@ func hookScriptPath(agentId string) (string, error) { return filepath.Join(agentDirectory, "hook.js"), nil } +// removeAgentScripts deletes hook.js and guard.js; missing files are fine. +func removeAgentScripts(agentID string) error { + for _, resolve := range []func(string) (string, error){hookScriptPath, guardScriptPath} { + path, err := resolve(agentID) + if err != nil { + return err + } + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("remove %s: %w", path, err) + } + } + return nil +} + // guardScriptPath returns ~/.elydora//guard.js. func guardScriptPath(agentId string) (string, error) { agentDirectory, err := ResolveAgentRuntimeDirectory(agentId) diff --git a/cmd/elydora/plugins/kirocli.go b/cmd/elydora/plugins/kirocli.go index d2c619e..2302ae3 100644 --- a/cmd/elydora/plugins/kirocli.go +++ b/cmd/elydora/plugins/kirocli.go @@ -79,7 +79,7 @@ func (p *KiroCliPlugin) Install(config InstallConfig) error { copyKiroObject(nextV2, v2Settings) nextV2["hooks"] = v2Hooks - nextV3 := cloneKiroObject(v3Settings) + nextV3 := cloneJSONObject(v3Settings) nextV3["version"] = "v1" v3Hooks := make([]any, 0, len(currentV3Hooks)+2) for _, hook := range currentV3Hooks { diff --git a/cmd/elydora/plugins/kirocli_contract.go b/cmd/elydora/plugins/kirocli_contract.go index 8dffe11..543b992 100644 --- a/cmd/elydora/plugins/kirocli_contract.go +++ b/cmd/elydora/plugins/kirocli_contract.go @@ -28,12 +28,6 @@ type kiroConfigMutation struct { remove bool } -func cloneKiroObject(value map[string]any) map[string]any { - clone := make(map[string]any, len(value)) - copyKiroObject(clone, value) - return clone -} - func copyKiroObject(destination, source map[string]any) { for key, value := range source { destination[key] = value @@ -49,7 +43,7 @@ func kiroHooksObject(settings map[string]any, label string) (map[string]any, err if !ok || hooks == nil { return nil, fmt.Errorf(`%s field "hooks" must be an object`, label) } - return cloneKiroObject(hooks), nil + return cloneJSONObject(hooks), nil } func kiroHookEntries(hooks map[string]any, event, label string) ([]map[string]any, error) { @@ -193,7 +187,7 @@ func prepareKiroV2Uninstall( } hooks["preToolUse"] = filteredPre hooks["postToolUse"] = filteredPost - next := cloneKiroObject(settings) + next := cloneJSONObject(settings) next["hooks"] = hooks mutation.value = next mutation.changed = true @@ -250,7 +244,7 @@ func prepareKiroV3Uninstall( if len(filtered) == len(hooks) { return mutation, nil } - next := cloneKiroObject(settings) + next := cloneJSONObject(settings) next["hooks"] = filtered owned := true for key := range settings { diff --git a/cmd/elydora/plugins/kiroide_command.go b/cmd/elydora/plugins/kiroide_command.go index e980395..a5acf15 100644 --- a/cmd/elydora/plugins/kiroide_command.go +++ b/cmd/elydora/plugins/kiroide_command.go @@ -1,101 +1,32 @@ package plugins import ( - "fmt" "path/filepath" - "runtime" "strings" ) -type kiroIdeRuntimeReference struct { - agentID string - scriptPath string -} - -func sameKiroIdePath(left, right string) bool { - left = filepath.Clean(left) - right = filepath.Clean(right) - if absolute, err := filepath.Abs(left); err == nil { - left = absolute - } - if absolute, err := filepath.Abs(right); err == nil { - right = absolute - } - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameKiroIdeAgentID(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func sameKiroIdeFileName(left, right string) bool { - if runtime.GOOS == "windows" { - return strings.EqualFold(left, right) - } - return left == right -} - -func buildKiroIdeCommand(runtimePath, scriptPath string) (string, error) { - if !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) { - return "", fmt.Errorf( - "Kiro IDE hook commands require absolute runtime and script paths", - ) - } - if runtime.GOOS == "windows" { - return encodedWindowsCommand(runtimePath, scriptPath), nil - } - return quotePOSIXArgument(runtimePath) + " " + quotePOSIXArgument(scriptPath), nil -} - -func parseKiroIdeCommand(command string) (string, string, bool) { - if runtime.GOOS == "windows" { - return parseEncodedWindowsCommand(command) - } - return parsePOSIXCommand(command) -} - func kiroIdeRuntimeReferenceForCommand( command string, scriptName string, -) (*kiroIdeRuntimeReference, error) { - runtimePath, scriptPath, ok := parseKiroIdeCommand(command) - if !ok || !filepath.IsAbs(runtimePath) || !filepath.IsAbs(scriptPath) || - !isNodeExecutable(runtimePath) || - !sameKiroIdeFileName(filepath.Base(scriptPath), scriptName) { +) (*managedScriptReference, error) { + runtimePath, scriptPath, ok := parseEncodedCommand(command) + if !ok || !filepath.IsAbs(runtimePath) || !isNodeExecutable(runtimePath) { return nil, nil } - agentDirectory := filepath.Dir(scriptPath) - runtimeRoot, err := AgentRuntimeRoot() - if err != nil { - return nil, err - } - if !sameKiroIdePath(filepath.Dir(agentDirectory), runtimeRoot) { - return nil, nil - } - agentID := filepath.Base(agentDirectory) - if agentID == "" || agentID == "." || agentID == ".." { - return nil, nil - } - return &kiroIdeRuntimeReference{agentID: agentID, scriptPath: scriptPath}, nil + return resolveManagedScript(scriptPath, scriptName) } func currentKiroIdeReference( hook map[string]any, nodePath string, -) (*kiroIdeRuntimeReference, error) { +) (*managedScriptReference, error) { reference, err := managedKiroIdeReference(hook) if err != nil || reference == nil { return reference, err } action := hook["action"].(map[string]any) command := action["command"].(string) - expected, err := buildKiroIdeCommand(nodePath, reference.scriptPath) + expected, err := buildEncodedCommand("Kiro IDE", nodePath, reference.scriptPath) if err != nil { return nil, err } @@ -105,30 +36,14 @@ func currentKiroIdeReference( return reference, nil } +// legacyKiroIdeGoReference reads the pre-2.1 `node