diff --git a/integrations/catalog.json b/integrations/catalog.json index b69cce1..9f17b82 100644 --- a/integrations/catalog.json +++ b/integrations/catalog.json @@ -30,8 +30,8 @@ "mechanism": "exit_code_2", "failure_mode": "fail_open" }, - "adapters": { "node": true, "python": true, "go": false }, - "delivery_state": "partial", + "adapters": { "node": true, "python": true, "go": true }, + "delivery_state": "available", "source_url": "https://docs.augmentcode.com/cli/hooks" }, { diff --git a/sdks/go/README.md b/sdks/go/README.md index 19a6928..77d43c6 100644 --- a/sdks/go/README.md +++ b/sdks/go/README.md @@ -79,6 +79,8 @@ Kimi installation follows detected runtime evidence. Kimi Code uses `$KIMI_CODE_ Grok Build installation writes native global hooks to `$GROK_HOME/hooks/elydora-audit.json` (default `~/.grok/hooks/elydora-audit.json`). Project hooks still follow Grok's `/hooks-trust` workflow; Elydora leaves project, Claude Code, and Cursor compatibility files unchanged. +Auggie installation writes user-level hooks to `~/.augment/settings.json` and creates the `.cmd` or `.sh` wrappers required by its command runner. System and workspace settings remain unchanged. Sessions started with `--augment-cache-dir` load settings from that alternate directory. + ### Commands | Command | Description | @@ -92,6 +94,7 @@ Grok Build installation writes native global hooks to `$GROK_HOME/hooks/elydora- | Agent | Key | |-------|-----| +| Augment Code CLI | `augment` | | Claude Code | `claudecode` | | OpenAI Codex | `codex` | | Copilot CLI | `copilot` | diff --git a/sdks/go/cmd/elydora/plugins/augment.go b/sdks/go/cmd/elydora/plugins/augment.go new file mode 100644 index 0000000..a7e4f39 --- /dev/null +++ b/sdks/go/cmd/elydora/plugins/augment.go @@ -0,0 +1,141 @@ +package plugins + +import "fmt" + +// AugmentPlugin manages Auggie native global user hooks. +type AugmentPlugin struct{} + +func (p *AugmentPlugin) Install(config InstallConfig) error { + if config.AgentID == "" { + return fmt.Errorf("agent ID is required") + } + document, err := readAugmentConfig() + if err != nil { + return err + } + if config.GuardScriptPath == "" { + return fmt.Errorf("guard script path is required") + } + guardExists, err := regularFileExists( + config.GuardScriptPath, "Elydora guard runtime", + ) + if err != nil { + return err + } + if !guardExists { + return fmt.Errorf( + "Elydora guard runtime is missing: %s", config.GuardScriptPath, + ) + } + hookPath, err := hookScriptPath(config.AgentID) + if err != nil { + return err + } + if config.HookScript != "" { + hookPath = config.HookScript + } + nodePath, err := resolveNodeRuntime() + if err != nil { + return err + } + if err := validateAugmentMatchers(document.hooks, nodePath); err != nil { + return err + } + runtimeRoot, err := augmentRuntimeRoot() + if err != nil { + return err + } + wrappers := resolveAugmentWrapperPaths(runtimeRoot, config.AgentID) + hooks, _ := removeManagedAugmentHooks(document.hooks, "", runtimeRoot) + hooks["PreToolUse"] = append( + hooks["PreToolUse"], + buildAugmentGroup(buildAugmentHandler(wrappers.guard)), + ) + hooks["PostToolUse"] = append( + hooks["PostToolUse"], + buildAugmentGroup(buildAugmentHandler(wrappers.audit)), + ) + next := cloneAugmentObject(document.root) + next["hooks"] = renderAugmentHooks(hooks) + + runtimeConfig := config + runtimeConfig.AgentName = augmentAgentKey + if err := GenerateHookScript(hookPath, runtimeConfig); err != nil { + return fmt.Errorf("generate hook script: %w", err) + } + if err := writeHookFileAtomic( + wrappers.guard, + buildAugmentWrapper(nodePath, config.GuardScriptPath), + 0700, + ); err != nil { + return fmt.Errorf("write Auggie guard wrapper: %w", err) + } + if err := writeHookFileAtomic( + wrappers.audit, + buildAugmentWrapper(nodePath, hookPath), + 0700, + ); err != nil { + return fmt.Errorf("write Auggie audit wrapper: %w", err) + } + if err := writeHookJSONObjectAtomic(document.configPath, next); err != nil { + return fmt.Errorf("write Auggie settings: %w", err) + } + fmt.Println("Auggie: user-level PreToolUse and PostToolUse hooks installed.") + return nil +} + +func (p *AugmentPlugin) Uninstall(agentID string) error { + document, err := readAugmentConfig() + if err != nil || !document.exists { + return err + } + runtimeRoot, err := augmentRuntimeRoot() + if err != nil { + return err + } + hooks, changed := removeManagedAugmentHooks( + document.hooks, agentID, runtimeRoot, + ) + if !changed { + return nil + } + next := cloneAugmentObject(document.root) + if len(hooks) == 0 { + delete(next, "hooks") + } else { + next["hooks"] = renderAugmentHooks(hooks) + } + if len(next) == 0 { + return removeHookFile(document.configPath, "Auggie settings") + } + return writeHookJSONObjectAtomic(document.configPath, next) +} + +func (p *AugmentPlugin) Status() (PluginStatus, error) { + document, err := readAugmentConfig() + status := PluginStatus{ + AgentName: augmentAgentKey, + DisplayName: "Augment Code CLI", + ConfigPath: document.configPath, + } + if err != nil { + return status, err + } + runtimeRoot, err := augmentRuntimeRoot() + if err != nil { + return status, err + } + contracts := augmentRuntimeContracts(document.hooks, runtimeRoot) + if len(contracts) == 0 { + return status, nil + } + status.HookConfigured = true + status.HookScriptExists, err = augmentRuntimeFilesExist( + contracts, runtimeRoot, + ) + if err != nil { + return status, err + } + status.Installed = status.HookScriptExists + return status, nil +} diff --git a/sdks/go/cmd/elydora/plugins/augment_contract.go b/sdks/go/cmd/elydora/plugins/augment_contract.go new file mode 100644 index 0000000..b157e59 --- /dev/null +++ b/sdks/go/cmd/elydora/plugins/augment_contract.go @@ -0,0 +1,376 @@ +package plugins + +import ( + "fmt" + "math" + "path/filepath" + "runtime" + "strings" +) + +const ( + augmentAgentKey = "augment" + augmentGuardScript = "guard.js" + augmentAuditScript = "hook.js" + augmentHookTimeout = float64(10_000) + augmentPOSIXApostrophe = `'"'"'` +) + +var ( + augmentToolEvents = map[string]struct{}{ + "PreToolUse": {}, + "PostToolUse": {}, + } + augmentSessionEvents = map[string]struct{}{ + "Stop": {}, + "SessionStart": {}, + "SessionEnd": {}, + "Notification": {}, + "PromptSubmit": {}, + } +) + +type augmentGroup struct { + object map[string]any + handlers []map[string]any +} + +type augmentHooks map[string][]augmentGroup + +type augmentDocument struct { + exists bool + configPath string + root map[string]any + hooks augmentHooks +} + +type augmentWrapperPaths struct { + guard string + audit string +} + +type augmentRuntimeContract struct { + agentID string + guardPath string + auditPath string + guardWrapper string + 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, + groupIndex, handlerIndex int, +) (map[string]any, error) { + label := fmt.Sprintf( + "Auggie settings handler hooks.%s[%d].hooks[%d]", + event, groupIndex, handlerIndex, + ) + handler, ok := value.(map[string]any) + if !ok || handler == nil { + return nil, fmt.Errorf("%s must be an object", label) + } + handlerType, ok := handler["type"].(string) + if !ok || handlerType != "command" { + return nil, fmt.Errorf(`%s type must be "command"`, label) + } + command, ok := handler["command"].(string) + if !ok || command == "" { + return nil, fmt.Errorf("%s requires a non-empty command", label) + } + if arguments, exists := handler["args"]; exists { + values, ok := arguments.([]any) + if !ok { + return nil, fmt.Errorf("%s args must be an array of strings", label) + } + for _, argument := range values { + if _, ok := argument.(string); !ok { + return nil, fmt.Errorf("%s args must be an array of strings", label) + } + } + } + if timeoutValue, exists := handler["timeout"]; exists { + timeout, ok := timeoutValue.(float64) + if !ok || math.IsNaN(timeout) || math.IsInf(timeout, 0) || timeout <= 0 { + return nil, fmt.Errorf("%s timeout must be a positive finite number", label) + } + } + return cloneAugmentObject(handler), nil +} + +func validateAugmentMetadata(value any, label string) error { + metadata, ok := value.(map[string]any) + if !ok || metadata == nil { + return fmt.Errorf("%s metadata must be an object", label) + } + for _, key := range []string{ + "includeConversationData", + "includeMCPMetadata", + "includeUserContext", + } { + if field, exists := metadata[key]; exists { + if _, ok := field.(bool); !ok { + return fmt.Errorf("%s metadata.%s must be a boolean", label, key) + } + } + } + return nil +} + +func validateAugmentGroup(value any, event string, groupIndex int) (augmentGroup, error) { + label := fmt.Sprintf("Auggie settings group hooks.%s[%d]", event, groupIndex) + object, ok := value.(map[string]any) + if !ok || object == nil { + return augmentGroup{}, fmt.Errorf("%s must be an object", label) + } + if _, sessionEvent := augmentSessionEvents[event]; sessionEvent { + if _, hasMatcher := object["matcher"]; hasMatcher { + return augmentGroup{}, fmt.Errorf("%s matcher is only supported for tool events", label) + } + } + if matcher, exists := object["matcher"]; exists { + if _, ok := matcher.(string); !ok { + return augmentGroup{}, fmt.Errorf("%s matcher must be a string", label) + } + } + if metadata, exists := object["metadata"]; exists { + if err := validateAugmentMetadata(metadata, label); err != nil { + return augmentGroup{}, err + } + } + values, ok := object["hooks"].([]any) + if !ok { + return augmentGroup{}, fmt.Errorf("%s must contain a hooks array", label) + } + handlers := make([]map[string]any, 0, len(values)) + for handlerIndex, value := range values { + handler, err := validateAugmentHandler(value, event, groupIndex, handlerIndex) + if err != nil { + return augmentGroup{}, err + } + handlers = append(handlers, handler) + } + return augmentGroup{object: cloneAugmentObject(object), handlers: handlers}, nil +} + +func readAugmentHooks(root map[string]any) (augmentHooks, error) { + value, exists := root["hooks"] + if !exists { + return augmentHooks{}, nil + } + object, ok := value.(map[string]any) + if !ok || object == nil { + return nil, fmt.Errorf(`Auggie settings field "hooks" must be an object`) + } + hooks := make(augmentHooks, len(object)) + for event, groupValue := range object { + if _, toolEvent := augmentToolEvents[event]; !toolEvent { + if _, sessionEvent := augmentSessionEvents[event]; !sessionEvent { + return nil, fmt.Errorf( + `Auggie settings field "hooks.%s" uses an unsupported event`, + event, + ) + } + } + values, ok := groupValue.([]any) + if !ok { + return nil, fmt.Errorf( + `Auggie settings field "hooks.%s" must be an array`, + event, + ) + } + groups := make([]augmentGroup, 0, len(values)) + for groupIndex, value := range values { + group, err := validateAugmentGroup(value, event, groupIndex) + if err != nil { + return nil, err + } + groups = append(groups, group) + } + hooks[event] = groups + } + return hooks, nil +} + +func renderAugmentHooks(hooks augmentHooks) map[string]any { + result := make(map[string]any, len(hooks)) + for event, groups := range hooks { + values := make([]any, 0, len(groups)) + for _, group := range groups { + object := cloneAugmentObject(group.object) + handlers := make([]any, 0, len(group.handlers)) + for _, handler := range group.handlers { + handlers = append(handlers, handler) + } + object["hooks"] = handlers + values = append(values, object) + } + result[event] = values + } + return result +} + +func readAugmentWindowsArgument(command string) (string, bool) { + if len(command) < 2 || command[0] != '"' { + return "", false + } + var value strings.Builder + for index := 1; index < len(command); index++ { + if command[index] == '\\' && index+1 < len(command) && command[index+1] == '"' { + value.WriteByte('"') + index++ + continue + } + if command[index] == '"' { + return value.String(), index == len(command)-1 && value.Len() > 0 + } + value.WriteByte(command[index]) + } + return "", false +} + +func readAugmentPOSIXArgument(command string) (string, bool) { + if len(command) < 2 || command[0] != '\'' { + return "", false + } + var value strings.Builder + for index := 1; index < len(command); { + if strings.HasPrefix(command[index:], augmentPOSIXApostrophe) { + value.WriteByte('\'') + index += len(augmentPOSIXApostrophe) + continue + } + if command[index] == '\'' { + return value.String(), index == len(command)-1 && value.Len() > 0 + } + value.WriteByte(command[index]) + index++ + } + return "", false +} + +func parseAugmentCommand(command string) (string, bool) { + if runtime.GOOS == "windows" { + return readAugmentWindowsArgument(command) + } + return readAugmentPOSIXArgument(command) +} + +func normalizeAugmentPath(value string) string { + absolute, err := filepath.Abs(value) + if err == nil { + value = absolute + } + value = filepath.ToSlash(filepath.Clean(value)) + if runtime.GOOS == "windows" { + return strings.ToLower(value) + } + return value +} + +func sameAugmentPath(left, right string) bool { + return normalizeAugmentPath(left) == normalizeAugmentPath(right) +} + +func sameAugmentAgentID(left, right string) bool { + if runtime.GOOS == "windows" { + return strings.EqualFold(left, right) + } + return left == right +} + +func sameAugmentFileName(left, right string) bool { + if runtime.GOOS == "windows" { + return strings.EqualFold(left, right) + } + return left == right +} + +func managedAugmentAgentID( + handler map[string]any, + wrapperName, runtimeRoot string, +) (string, bool) { + if handler["type"] != "command" || handler["timeout"] != augmentHookTimeout { + return "", false + } + if _, hasArguments := handler["args"]; hasArguments { + return "", false + } + command, ok := handler["command"].(string) + if !ok { + return "", false + } + wrapperPath, ok := parseAugmentCommand(command) + if !ok || !sameAugmentFileName(filepath.Base(wrapperPath), wrapperName) { + return "", false + } + agentDirectory := filepath.Dir(wrapperPath) + if !sameAugmentPath(filepath.Dir(agentDirectory), runtimeRoot) { + return "", false + } + agentID := filepath.Base(agentDirectory) + return agentID, agentID != "" && agentID != "." && agentID != ".." +} + +func removeManagedAugmentGroups( + groups []augmentGroup, + wrapperName, agentID, runtimeRoot string, +) ([]augmentGroup, bool) { + result := make([]augmentGroup, 0, len(groups)) + changed := false + for _, group := range groups { + handlers := make([]map[string]any, 0, len(group.handlers)) + groupChanged := false + for _, handler := range group.handlers { + managedID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) + remove := managed && (agentID == "" || sameAugmentAgentID(managedID, agentID)) + if remove { + changed = true + groupChanged = true + continue + } + handlers = append(handlers, handler) + } + if len(handlers) > 0 || !groupChanged { + result = append(result, augmentGroup{object: group.object, handlers: handlers}) + } + } + return result, changed +} + +func removeManagedAugmentHooks( + hooks augmentHooks, + agentID, runtimeRoot string, +) (augmentHooks, bool) { + result := make(augmentHooks, len(hooks)) + for event, groups := range hooks { + result[event] = groups + } + changed := false + for _, contract := range []struct{ event, wrapper string }{ + {"PreToolUse", augmentGuardWrapperName()}, + {"PostToolUse", augmentAuditWrapperName()}, + } { + groups, eventChanged := removeManagedAugmentGroups( + result[contract.event], contract.wrapper, agentID, runtimeRoot, + ) + if !eventChanged { + continue + } + changed = true + if len(groups) == 0 { + delete(result, contract.event) + } else { + result[contract.event] = groups + } + } + return result, changed +} diff --git a/sdks/go/cmd/elydora/plugins/augment_io.go b/sdks/go/cmd/elydora/plugins/augment_io.go new file mode 100644 index 0000000..dc731db --- /dev/null +++ b/sdks/go/cmd/elydora/plugins/augment_io.go @@ -0,0 +1,292 @@ +package plugins + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "sort" + "strings" +) + +const augmentRegexValidator = `import fs from "node:fs"; +const pattern = fs.readFileSync(0, "utf8"); +try { + new RegExp(pattern); +} catch (error) { + process.stderr.write(error instanceof Error ? error.message : String(error)); + process.exit(1); +}` + +func augmentGuardWrapperName() string { + if runtime.GOOS == "windows" { + return "augment-guard.cmd" + } + return "augment-guard.sh" +} + +func augmentAuditWrapperName() string { + if runtime.GOOS == "windows" { + return "augment-hook.cmd" + } + return "augment-hook.sh" +} + +func augmentHomeDirectory() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("resolve home directory: %w", err) + } + return home, nil +} + +func augmentRuntimeRoot() (string, error) { + home, err := augmentHomeDirectory() + if err != nil { + return "", err + } + return filepath.Join(home, ".elydora"), nil +} + +func augmentConfigPath() (string, error) { + home, err := augmentHomeDirectory() + if err != nil { + return "", err + } + return filepath.Join(home, ".augment", "settings.json"), nil +} + +func readAugmentConfig() (augmentDocument, error) { + configPath, err := augmentConfigPath() + if err != nil { + return augmentDocument{}, err + } + root, exists, err := readHookJSONObject(configPath, "Auggie settings") + if err != nil { + return augmentDocument{}, err + } + hooks, err := readAugmentHooks(root) + if err != nil { + return augmentDocument{}, err + } + return augmentDocument{ + exists: exists, + configPath: configPath, + root: root, + hooks: hooks, + }, nil +} + +func validateAugmentMatchers(hooks augmentHooks, nodePath string) error { + events := make([]string, 0, len(hooks)) + for event := range hooks { + events = append(events, event) + } + sort.Strings(events) + for _, event := range events { + for groupIndex, group := range hooks[event] { + matcher, exists := group.object["matcher"].(string) + if !exists { + continue + } + // #nosec G204 -- nodePath is resolved with exec.LookPath and matcher data uses stdin. + command := exec.Command( + nodePath, "--input-type=module", "--eval", augmentRegexValidator, + ) + command.Stdin = strings.NewReader(matcher) + output, err := command.CombinedOutput() + if err == nil { + continue + } + message := strings.TrimSpace(string(output)) + if message == "" { + message = err.Error() + } + return fmt.Errorf( + "Auggie settings group hooks.%s[%d] matcher "+ + "must be a valid JavaScript regular expression: %s", + event, groupIndex, message, + ) + } + } + return nil +} + +func quoteAugmentWindowsCommand(value string) string { + return `"` + strings.ReplaceAll(value, `"`, `\"`) + `"` +} + +func quoteAugmentBatchArgument(value string) string { + return `"` + strings.ReplaceAll(value, "%", "%%") + `"` +} + +func buildAugmentCommand(wrapperPath string) string { + if runtime.GOOS == "windows" { + return quoteAugmentWindowsCommand(wrapperPath) + } + return quotePOSIXArgument(wrapperPath) +} + +func buildAugmentWrapper(runtimePath, scriptPath string) []byte { + if runtime.GOOS == "windows" { + return []byte( + "@echo off\r\n" + + quoteAugmentBatchArgument(runtimePath) + " " + + quoteAugmentBatchArgument(scriptPath) + "\r\n" + + "exit /b %errorlevel%\r\n", + ) + } + return []byte( + "#!/bin/sh\nexec " + + quotePOSIXArgument(runtimePath) + " " + + quotePOSIXArgument(scriptPath) + "\n", + ) +} + +func buildAugmentHandler(wrapperPath string) map[string]any { + return map[string]any{ + "type": "command", + "command": buildAugmentCommand(wrapperPath), + "timeout": augmentHookTimeout, + } +} + +func buildAugmentGroup(handler map[string]any) augmentGroup { + return augmentGroup{ + object: map[string]any{"matcher": ".*"}, + handlers: []map[string]any{handler}, + } +} + +func resolveAugmentWrapperPaths(runtimeRoot, agentID string) augmentWrapperPaths { + agentDirectory := filepath.Join(runtimeRoot, agentID) + return augmentWrapperPaths{ + guard: filepath.Join(agentDirectory, augmentGuardWrapperName()), + audit: filepath.Join(agentDirectory, augmentAuditWrapperName()), + } +} + +func managedAugmentIDs( + groups []augmentGroup, + wrapperName, runtimeRoot string, +) map[string]string { + result := map[string]string{} + for _, group := range groups { + for _, handler := range group.handlers { + agentID, managed := managedAugmentAgentID(handler, wrapperName, runtimeRoot) + if managed { + key := agentID + if runtime.GOOS == "windows" { + key = strings.ToLower(key) + } + result[key] = agentID + } + } + } + return result +} + +func augmentRuntimeContracts( + hooks augmentHooks, + runtimeRoot string, +) []augmentRuntimeContract { + guards := managedAugmentIDs( + hooks["PreToolUse"], augmentGuardWrapperName(), runtimeRoot, + ) + audits := managedAugmentIDs( + hooks["PostToolUse"], augmentAuditWrapperName(), runtimeRoot, + ) + keys := make([]string, 0, len(guards)) + for key := range guards { + if _, exists := audits[key]; exists { + keys = append(keys, key) + } + } + sort.Strings(keys) + contracts := make([]augmentRuntimeContract, 0, len(keys)) + for _, key := range keys { + agentID := guards[key] + agentDirectory := filepath.Join(runtimeRoot, agentID) + contracts = append(contracts, augmentRuntimeContract{ + agentID: agentID, + guardPath: filepath.Join(agentDirectory, augmentGuardScript), + auditPath: filepath.Join(agentDirectory, augmentAuditScript), + guardWrapper: filepath.Join(agentDirectory, augmentGuardWrapperName()), + auditWrapper: filepath.Join(agentDirectory, augmentAuditWrapperName()), + }) + } + return contracts +} + +func augmentRuntimeFilesExist( + contracts []augmentRuntimeContract, + runtimeRoot string, +) (bool, error) { + entries, err := os.ReadDir(runtimeRoot) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf( + "read Elydora runtime directory at %s: %w", runtimeRoot, err, + ) + } + for _, contract := range contracts { + var entryName string + for _, entry := range entries { + if entry.IsDir() && sameAugmentAgentID(entry.Name(), contract.agentID) { + entryName = entry.Name() + break + } + } + if entryName == "" { + continue + } + agentDirectory := filepath.Join(runtimeRoot, entryName) + configPath := filepath.Join(agentDirectory, "config.json") + config, exists, err := readHookJSONObject( + configPath, "Elydora runtime config", + ) + if err != nil { + return false, err + } + if !exists { + continue + } + agentName, ok := config["agent_name"].(string) + if !ok { + return false, fmt.Errorf( + `Elydora runtime config at %s field "agent_name" must be a string`, + configPath, + ) + } + if agentName != augmentAgentKey { + continue + } + files := []struct { + path string + label string + }{ + {contract.guardPath, "Elydora guard runtime"}, + {contract.auditPath, "Elydora audit runtime"}, + {contract.guardWrapper, "Auggie guard wrapper"}, + {contract.auditWrapper, "Auggie audit wrapper"}, + } + complete := true + for _, file := range files { + exists, err := regularFileExists(file.path, file.label) + if err != nil { + return false, err + } + if !exists { + complete = false + break + } + } + if complete { + return true, nil + } + } + return false, nil +} diff --git a/sdks/go/cmd/elydora/plugins/augment_test.go b/sdks/go/cmd/elydora/plugins/augment_test.go new file mode 100644 index 0000000..dad7762 --- /dev/null +++ b/sdks/go/cmd/elydora/plugins/augment_test.go @@ -0,0 +1,440 @@ +package plugins + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" +) + +func TestAugmentRegistryAndFactory(t *testing.T) { + entry := SupportedAgents[augmentAgentKey] + if entry.Name != "Augment Code CLI" || + entry.ConfigDir != "~/.augment" || + entry.ConfigFile != "settings.json" { + t.Fatalf("Auggie registry entry = %#v", entry) + } + if _, ok := NewPlugin(augmentAgentKey).(*AugmentPlugin); !ok { + t.Fatalf("Auggie plugin factory returned %T", NewPlugin(augmentAgentKey)) + } +} + +func TestAugmentGeneratedArgumentParsers(t *testing.T) { + posixPath := "/tmp/home with spaces/agent's/augment-guard.sh" + posixCommand := quotePOSIXArgument(posixPath) + parsed, ok := readAugmentPOSIXArgument(posixCommand) + if !ok || parsed != posixPath { + t.Fatalf("parse POSIX wrapper = %q, %v", parsed, ok) + } + windowsPath := `C:\home with spaces\.elydora\agent-1\augment-guard.cmd` + windowsCommand := quoteAugmentWindowsCommand(windowsPath) + parsed, ok = readAugmentWindowsArgument(windowsCommand) + if !ok || parsed != windowsPath { + t.Fatalf("parse Windows wrapper = %q, %v", parsed, ok) + } +} + +func TestAugmentInstallPreservesSettingsAndIsIdempotent(t *testing.T) { + existing := `{ + "telemetryEnabled": false, + "hooks": { + "SessionStart": [{"hooks":[{"type":"command","command":"existing-command","args":["one"],"timeout":5000}],"metadata":{"includeUserContext":true},"label":"keep"}], + "PreToolUse": [{"matcher":"launch-process","hooks":[{"type":"command","command":"user-command"}]}] + } +}` + fixture := prepareAugmentFixture( + t, augmentFixtureOptions{existingRaw: &existing}, + ) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("repeat Auggie install: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + if settings["telemetryEnabled"] != false { + t.Fatalf("telemetry setting changed: %#v", settings) + } + hooks := requireObject(t, settings["hooks"]) + if len(requireArray(t, hooks["SessionStart"])) != 1 || + len(requireArray(t, hooks["PreToolUse"])) != 2 || + len(requireArray(t, hooks["PostToolUse"])) != 1 { + t.Fatalf("unexpected hooks after repeat install: %#v", hooks) + } + preGroups := requireArray(t, hooks["PreToolUse"]) + managedGroup := requireObject(t, preGroups[len(preGroups)-1]) + if managedGroup["matcher"] != ".*" { + t.Fatalf("managed matcher = %#v", managedGroup["matcher"]) + } + guard := augmentTestManagedHandler( + t, settings, "PreToolUse", fixture.guardWrapper, + ) + audit := augmentTestManagedHandler( + t, settings, "PostToolUse", fixture.auditWrapper, + ) + for _, handler := range []map[string]any{guard, audit} { + wantKeys := map[string]struct{}{"type": {}, "command": {}, "timeout": {}} + if len(handler) != len(wantKeys) { + t.Fatalf("handler has extra fields: %#v", handler) + } + for key := range handler { + if _, exists := wantKeys[key]; !exists { + t.Fatalf("handler has unexpected field %q", key) + } + } + if handler["type"] != "command" || handler["timeout"] != augmentHookTimeout { + t.Fatalf("unexpected handler: %#v", handler) + } + } + guardWrapper, err := os.ReadFile(fixture.guardWrapper) + if err != nil || !strings.Contains(string(guardWrapper), augmentGuardScript) { + t.Fatalf("guard wrapper = %q, %v", guardWrapper, err) + } + auditWrapper, err := os.ReadFile(fixture.auditWrapper) + if err != nil || !strings.Contains(string(auditWrapper), augmentAuditScript) { + t.Fatalf("audit wrapper = %q, %v", auditWrapper, err) + } + if runtime.GOOS == "windows" { + if !strings.HasPrefix(string(guardWrapper), "@echo off\r\n") || + !strings.Contains(string(guardWrapper), "exit /b %errorlevel%") { + t.Fatalf("invalid Windows wrapper: %q", guardWrapper) + } + } else { + info, statErr := os.Stat(fixture.guardWrapper) + if statErr != nil || info.Mode().Perm()&0100 == 0 || + !strings.HasPrefix(string(guardWrapper), "#!/bin/sh\nexec ") { + t.Fatalf("invalid POSIX wrapper: %q, %#v, %v", guardWrapper, info, statErr) + } + } + runtimeConfig := readAugmentTestObject(t, fixture.runtimeConfig) + if runtimeConfig["agent_name"] != augmentAgentKey { + t.Fatalf("runtime agent name = %#v", runtimeConfig["agent_name"]) + } + for _, path := range []string{ + filepath.Join(fixture.workspaceDir, ".augment", "settings.json"), + filepath.Join(fixture.workspaceDir, ".augment", "settings.local.json"), + } { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("workspace settings written at %s: %v", path, err) + } + } +} + +func TestAugmentWrappersBlockAndForwardPayloadByteForByte(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + capturePath := filepath.Join(t.TempDir(), "captured-event.json") + captureJSON, err := json.Marshal(capturePath) + if err != nil { + t.Fatalf("marshal capture path: %v", err) + } + captureScript := "const fs = require('node:fs'); const chunks = []; " + + "process.stdin.on('data', chunk => chunks.push(chunk)); " + + "process.stdin.on('end', () => fs.writeFileSync(" + + string(captureJSON) + ", Buffer.concat(chunks)));\n" + if err := os.WriteFile(fixture.hookPath, []byte(captureScript), 0700); err != nil { + t.Fatalf("write capture hook: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + prePayload := `{"hook_event_name":"PreToolUse","conversation_id":"conversation-1","workspace_roots":["workspace"],"tool_name":"launch-process","tool_input":{"command":"echo test"},"is_mcp_tool":false}` + guard := augmentTestManagedHandler( + t, settings, "PreToolUse", fixture.guardWrapper, + ) + exitCode, stderr := runAugmentCommand( + t, guard["command"].(string), fixture.homeDir, prePayload, + ) + if exitCode != 2 || !strings.Contains(stderr, "Agent is frozen by Elydora") { + t.Fatalf("guard exit = %d, stderr = %q", exitCode, stderr) + } + postPayload := `{"hook_event_name":"PostToolUse","conversation_id":"conversation-1","workspace_roots":["workspace"],"tool_name":"launch-process","tool_input":{"command":"echo test"},"tool_output":"test","is_mcp_tool":false}` + audit := augmentTestManagedHandler( + t, settings, "PostToolUse", fixture.auditWrapper, + ) + exitCode, stderr = runAugmentCommand( + t, audit["command"].(string), fixture.homeDir, postPayload, + ) + if exitCode != 0 { + t.Fatalf("audit exit = %d, stderr = %q", exitCode, stderr) + } + captured, err := os.ReadFile(capturePath) + if err != nil || string(captured) != postPayload { + t.Fatalf("captured payload = %q, %v", captured, err) + } +} + +func TestAugmentStatusRequiresPairCoreRuntimesAndWrappers(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + status, err := fixture.plugin.Status() + if err != nil || !status.Installed || !status.HookConfigured || + !status.HookScriptExists { + t.Fatalf("installed status = %#v, %v", status, err) + } + if err := os.Remove(fixture.guardWrapper); err != nil { + t.Fatalf("remove guard wrapper: %v", err) + } + status, err = fixture.plugin.Status() + if err != nil || status.Installed || !status.HookConfigured || + status.HookScriptExists { + t.Fatalf("missing-wrapper status = %#v, %v", status, err) + } + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("restore Auggie hooks: %v", err) + } + if err := os.Remove(fixture.hookPath); err != nil { + t.Fatalf("remove hook runtime: %v", err) + } + status, err = fixture.plugin.Status() + if err != nil || status.Installed || status.HookScriptExists { + t.Fatalf("missing-runtime status = %#v, %v", status, err) + } + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("restore Auggie hooks: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + delete(requireObject(t, settings["hooks"]), "PostToolUse") + writeAugmentTestObject(t, fixture.configPath, settings) + status, err = fixture.plugin.Status() + if err != nil || status.Installed || status.HookConfigured { + t.Fatalf("incomplete-pair status = %#v, %v", status, err) + } +} + +func TestAugmentStatusSkipsIncompleteEarlierContract(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + hooks := requireObject(t, settings["hooks"]) + for _, item := range []struct{ event, wrapper string }{ + {"PreToolUse", augmentGuardWrapperName()}, + {"PostToolUse", augmentAuditWrapperName()}, + } { + hooks[item.event] = append( + requireArray(t, hooks[item.event]), + map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join( + filepath.Dir(fixture.agentDir), "agent-0", item.wrapper, + ))}}, + ) + } + writeAugmentTestObject(t, fixture.configPath, settings) + status, err := fixture.plugin.Status() + if err != nil || !status.Installed { + t.Fatalf("status with earlier incomplete contract = %#v, %v", status, err) + } +} + +func TestAugmentUninstallPreservesExactUserOwnership(t *testing.T) { + existing := `{"owner":"user","hooks":{"Notification":[]}}` + fixture := prepareAugmentFixture( + t, augmentFixtureOptions{existingRaw: &existing}, + ) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + hooks := requireObject(t, settings["hooks"]) + preGroups := requireArray(t, hooks["PreToolUse"]) + preGroups = append([]any{map[string]any{ + "hooks": []any{}, "label": "keep empty group", + }}, preGroups...) + managedGroup := requireObject(t, preGroups[1]) + managedGroup["hooks"] = append( + requireArray(t, managedGroup["hooks"]), + map[string]any{"type": "command", "command": "user-command"}, + ) + preGroups = append(preGroups, + map[string]any{"hooks": []any{buildAugmentHandler( + fixture.guardWrapper + ".backup", + )}}, + map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join( + filepath.Dir(fixture.agentDir), "agent-10", augmentGuardWrapperName(), + ))}}, + ) + hooks["PreToolUse"] = preGroups + writeAugmentTestObject(t, fixture.configPath, settings) + uninstallID := augmentTestAgentID + if runtime.GOOS == "windows" { + uninstallID = "AGENT-1" + } + if err := fixture.plugin.Uninstall(uninstallID); err != nil { + t.Fatalf("uninstall Auggie hooks: %v", err) + } + remaining := readAugmentTestObject(t, fixture.configPath) + remainingHooks := requireObject(t, remaining["hooks"]) + if remaining["owner"] != "user" || + len(requireArray(t, remainingHooks["PreToolUse"])) != 4 { + t.Fatalf("user settings changed: %#v", remaining) + } + if !reflect.DeepEqual(requireArray(t, remainingHooks["Notification"]), []any{}) { + t.Fatalf("empty notification changed: %#v", remainingHooks["Notification"]) + } + raw, _ := os.ReadFile(fixture.configPath) + if !strings.Contains(string(raw), "backup") || + !strings.Contains(string(raw), "agent-10") { + t.Fatalf("similar handlers were removed: %s", raw) + } + if _, exists := remainingHooks["PostToolUse"]; exists { + t.Fatalf("managed PostToolUse remains: %#v", remainingHooks["PostToolUse"]) + } +} + +func TestAugmentInstallReplacesStaleAndPreservesEmptyGroups(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + settings := readAugmentTestObject(t, fixture.configPath) + hooks := requireObject(t, settings["hooks"]) + hooks["PreToolUse"] = append( + []any{map[string]any{"hooks": []any{}, "label": "keep empty group"}}, + requireArray(t, hooks["PreToolUse"])..., + ) + for _, item := range []struct{ event, wrapper string }{ + {"PreToolUse", augmentGuardWrapperName()}, + {"PostToolUse", augmentAuditWrapperName()}, + } { + hooks[item.event] = append( + requireArray(t, hooks[item.event]), + map[string]any{"hooks": []any{buildAugmentHandler(filepath.Join( + filepath.Dir(fixture.agentDir), "agent-old", item.wrapper, + ))}}, + ) + } + writeAugmentTestObject(t, fixture.configPath, settings) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("replace stale Auggie hooks: %v", err) + } + raw, _ := os.ReadFile(fixture.configPath) + current := readAugmentTestObject(t, fixture.configPath) + currentHooks := requireObject(t, current["hooks"]) + if strings.Contains(string(raw), "agent-old") || + len(requireArray(t, currentHooks["PreToolUse"])) != 2 || + len(requireArray(t, currentHooks["PostToolUse"])) != 1 { + t.Fatalf("stale hooks remain: %s", raw) + } +} + +func TestAugmentUninstallRemovesOwnedSettings(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + if err := fixture.plugin.Uninstall(augmentTestAgentID); err != nil { + t.Fatalf("uninstall Auggie hooks: %v", err) + } + if _, err := os.Stat(fixture.configPath); !os.IsNotExist(err) { + t.Fatalf("owned Auggie settings remain: %v", err) + } +} + +func TestAugmentMalformedSettingsPreventRuntimeWrites(t *testing.T) { + for _, testCase := range []struct{ name, raw, want string }{ + {"malformed", "{ malformed", "parse Auggie settings"}, + {"null-root", "null", "must contain a JSON object"}, + {"null-hooks", `{"hooks":null}`, `field "hooks" must be an object`}, + {"unknown-event", `{"hooks":{"UnknownEvent":[]}}`, "unsupported event"}, + {"null-event", `{"hooks":{"PreToolUse":null}}`, "must be an array"}, + {"null-group", `{"hooks":{"PreToolUse":[null]}}`, "must be an object"}, + {"session-matcher", `{"hooks":{"SessionStart":[{"matcher":".*","hooks":[]}]}}`, "only supported for tool events"}, + {"null-matcher", `{"hooks":{"PreToolUse":[{"matcher":null,"hooks":[]}]}}`, "matcher must be a string"}, + {"invalid-matcher", `{"hooks":{"PreToolUse":[{"matcher":"[","hooks":[]}]}}`, "valid JavaScript regular expression"}, + {"null-handlers", `{"hooks":{"PreToolUse":[{"hooks":null}]}}`, "must contain a hooks array"}, + {"http-handler", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"http","command":"x"}]}]}}`, `type must be "command"`}, + {"empty-command", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":""}]}]}}`, "non-empty command"}, + {"bad-args", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","args":[1]}]}]}}`, "array of strings"}, + {"zero-timeout", `{"hooks":{"PreToolUse":[{"hooks":[{"type":"command","command":"x","timeout":0}]}]}}`, "positive finite number"}, + {"bad-metadata", `{"hooks":{"PreToolUse":[{"hooks":[],"metadata":{"includeUserContext":"yes"}}]}}`, "must be a boolean"}, + } { + t.Run(testCase.name, func(t *testing.T) { + fixture := prepareAugmentFixture( + t, augmentFixtureOptions{existingRaw: &testCase.raw}, + ) + err := fixture.plugin.Install(fixture.config) + if err == nil || !strings.Contains(err.Error(), testCase.want) { + t.Fatalf("install error = %v, want %q", err, testCase.want) + } + raw, readErr := os.ReadFile(fixture.configPath) + if readErr != nil || string(raw) != testCase.raw { + t.Fatalf("original settings changed: %q, %v", raw, readErr) + } + for _, path := range []string{ + fixture.hookPath, + fixture.runtimeConfig, + fixture.privateKey, + fixture.guardWrapper, + fixture.auditWrapper, + } { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("runtime write occurred at %s: %v", path, err) + } + } + }) + } +} + +func TestAugmentMissingGuardPreventsWrites(t *testing.T) { + fixture := prepareAugmentFixture( + t, augmentFixtureOptions{withoutGuard: true}, + ) + err := fixture.plugin.Install(fixture.config) + if err == nil || !strings.Contains(err.Error(), "guard runtime is missing") { + t.Fatalf("install error = %v", err) + } + for _, path := range []string{ + fixture.configPath, + fixture.hookPath, + fixture.runtimeConfig, + fixture.privateKey, + fixture.guardWrapper, + fixture.auditWrapper, + } { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("write occurred at %s: %v", path, err) + } + } +} + +func TestAugmentStatusSurfacesMalformedRuntimeMetadata(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + if err := os.WriteFile( + fixture.runtimeConfig, []byte("{ malformed"), 0600, + ); err != nil { + t.Fatalf("corrupt runtime config: %v", err) + } + if _, err := fixture.plugin.Status(); err == nil || + !strings.Contains(err.Error(), "parse Elydora runtime config") { + t.Fatalf("status error = %v", err) + } +} + +func TestAugmentAtomicWritesLeaveNoTemporaryFiles(t *testing.T) { + fixture := prepareAugmentFixture(t, augmentFixtureOptions{}) + if err := fixture.plugin.Install(fixture.config); err != nil { + t.Fatalf("install Auggie hooks: %v", err) + } + for _, directory := range []string{ + fixture.agentDir, filepath.Dir(fixture.configPath), + } { + entries, err := os.ReadDir(directory) + if err != nil { + t.Fatalf("read directory %s: %v", directory, err) + } + for _, entry := range entries { + if strings.HasSuffix(entry.Name(), ".tmp") { + t.Fatalf("temporary file remains: %s", entry.Name()) + } + } + } +} diff --git a/sdks/go/cmd/elydora/plugins/augment_test_helpers_test.go b/sdks/go/cmd/elydora/plugins/augment_test_helpers_test.go new file mode 100644 index 0000000..d68c794 --- /dev/null +++ b/sdks/go/cmd/elydora/plugins/augment_test_helpers_test.go @@ -0,0 +1,173 @@ +package plugins + +import ( + "bytes" + "encoding/json" + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +const augmentTestAgentID = "agent-1" + +type augmentFixtureOptions struct { + existingRaw *string + withoutGuard bool +} + +type augmentFixture struct { + plugin *AugmentPlugin + config InstallConfig + homeDir string + workspaceDir string + agentDir string + configPath string + guardPath string + hookPath string + guardWrapper string + auditWrapper string + runtimeConfig string + privateKey string +} + +func augmentString(value string) *string { + return &value +} + +func prepareAugmentFixture( + t *testing.T, + options augmentFixtureOptions, +) *augmentFixture { + t.Helper() + homeDir := filepath.Join(t.TempDir(), "home with spaces and 'quote") + workspaceDir := filepath.Join(homeDir, "workspace") + agentDir := filepath.Join(homeDir, ".elydora", augmentTestAgentID) + configPath := filepath.Join(homeDir, ".augment", "settings.json") + guardPath := filepath.Join(agentDir, augmentGuardScript) + if err := os.MkdirAll(workspaceDir, 0755); err != nil { + t.Fatalf("create workspace: %v", err) + } + if err := os.MkdirAll(agentDir, 0755); err != nil { + t.Fatalf("create agent directory: %v", err) + } + if !options.withoutGuard { + source := "process.stdin.resume(); process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n" + if err := os.WriteFile(guardPath, []byte(source), 0700); err != nil { + t.Fatalf("write guard runtime: %v", err) + } + } + if options.existingRaw != nil { + if err := os.MkdirAll(filepath.Dir(configPath), 0755); err != nil { + t.Fatalf("create Auggie config directory: %v", err) + } + if err := os.WriteFile(configPath, []byte(*options.existingRaw), 0600); err != nil { + t.Fatalf("write Auggie settings: %v", err) + } + } + t.Setenv("HOME", homeDir) + t.Setenv("USERPROFILE", homeDir) + return &augmentFixture{ + plugin: &AugmentPlugin{}, + homeDir: homeDir, + workspaceDir: workspaceDir, + agentDir: agentDir, + config: InstallConfig{ + AgentName: augmentAgentKey, + OrgID: "org-1", + AgentID: augmentTestAgentID, + PrivateKey: "test-key", + KID: "kid-1", + BaseURL: "https://api.elydora.test", + GuardScriptPath: guardPath, + }, + configPath: configPath, + guardPath: guardPath, + hookPath: filepath.Join(agentDir, augmentAuditScript), + guardWrapper: filepath.Join(agentDir, augmentGuardWrapperName()), + auditWrapper: filepath.Join(agentDir, augmentAuditWrapperName()), + runtimeConfig: filepath.Join(agentDir, "config.json"), + privateKey: filepath.Join(agentDir, "private.key"), + } +} + +func readAugmentTestObject(t *testing.T, path string) map[string]any { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read %s: %v", path, err) + } + var value map[string]any + if err := json.Unmarshal(raw, &value); err != nil { + t.Fatalf("decode %s: %v", path, err) + } + return value +} + +func writeAugmentTestObject(t *testing.T, path string, value map[string]any) { + t.Helper() + encoded, err := json.Marshal(value) + if err != nil { + t.Fatalf("marshal Auggie settings: %v", err) + } + if err := os.WriteFile(path, encoded, 0600); err != nil { + t.Fatalf("write Auggie settings: %v", err) + } +} + +func augmentTestManagedHandler( + t *testing.T, + settings map[string]any, + event, wrapperPath string, +) map[string]any { + t.Helper() + expected := buildAugmentCommand(wrapperPath) + hooks := requireObject(t, settings["hooks"]) + for _, groupValue := range requireArray(t, hooks[event]) { + group := requireObject(t, groupValue) + for _, handlerValue := range requireArray(t, group["hooks"]) { + handler := requireObject(t, handlerValue) + if handler["command"] == expected { + return handler + } + } + } + t.Fatalf("%s handler for %s not found", event, wrapperPath) + return nil +} + +func runAugmentCommand( + t *testing.T, + command, homeDir, payload string, +) (int, string) { + t.Helper() + var process *exec.Cmd + if runtime.GOOS == "windows" { + commandFile := filepath.Join(t.TempDir(), "run-auggie-hook.cmd") + contents := []byte("@echo off\r\n" + command + "\r\n") + if err := os.WriteFile(commandFile, contents, 0600); err != nil { + t.Fatalf("write Auggie command file: %v", err) + } + process = exec.Command("cmd.exe", "/d", "/c", commandFile) + } else { + process = exec.Command("sh", "-c", command) + } + process.Env = append( + os.Environ(), "HOME="+homeDir, "USERPROFILE="+homeDir, + ) + process.Stdin = bytes.NewBufferString(payload) + var stderr bytes.Buffer + process.Stderr = &stderr + err := process.Run() + if err == nil { + return 0, stderr.String() + } + var exitError *exec.ExitError + if errors.As(err, &exitError) { + return exitError.ExitCode(), stderr.String() + } + t.Fatalf("run Auggie hook command: %v", err) + return -1, stderr.String() +} diff --git a/sdks/go/cmd/elydora/plugins/registry.go b/sdks/go/cmd/elydora/plugins/registry.go index c60b8b2..41962ba 100644 --- a/sdks/go/cmd/elydora/plugins/registry.go +++ b/sdks/go/cmd/elydora/plugins/registry.go @@ -9,6 +9,7 @@ type AgentRegistryEntry struct { // SupportedAgents is the canonical registry of all supported agent integrations. var SupportedAgents = map[string]AgentRegistryEntry{ + "augment": {Name: "Augment Code CLI", ConfigDir: "~/.augment", ConfigFile: "settings.json"}, "claudecode": {Name: "Claude Code", ConfigDir: "~/.claude", ConfigFile: "settings.json"}, "codex": {Name: "OpenAI Codex", ConfigDir: "~/.codex", ConfigFile: "hooks.json"}, "cursor": {Name: "Cursor", ConfigDir: "~/.cursor", ConfigFile: "hooks.json"}, @@ -26,6 +27,8 @@ var SupportedAgents = map[string]AgentRegistryEntry{ // Returns nil if the agent is not supported. func NewPlugin(agentName string) AgentPlugin { switch agentName { + case "augment": + return &AugmentPlugin{} case "claudecode": return &ClaudeCodePlugin{} case "codex":