feat: sync Go Auggie hook integration

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 08:55:00 -04:00
1 parent e2f319914b
commit e334e84333
8 files changed
+1430 -2

No files matched your search

+2 -2
View File
@@ -30,8 +30,8 @@
"mechanism": "exit_code_2", "mechanism": "exit_code_2",
"failure_mode": "fail_open" "failure_mode": "fail_open"
}, },
"adapters": { "node": true, "python": true, "go": false }, "adapters": { "node": true, "python": true, "go": true },
"delivery_state": "partial", "delivery_state": "available",
"source_url": "https://docs.augmentcode.com/cli/hooks" "source_url": "https://docs.augmentcode.com/cli/hooks"
}, },
{ {
+3
View File
@@ -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. 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 ### Commands
| Command | Description | | Command | Description |
@@ -92,6 +94,7 @@ Grok Build installation writes native global hooks to `$GROK_HOME/hooks/elydora-
| Agent | Key | | Agent | Key |
|-------|-----| |-------|-----|
| Augment Code CLI | `augment` |
| Claude Code | `claudecode` | | Claude Code | `claudecode` |
| OpenAI Codex | `codex` | | OpenAI Codex | `codex` |
| Copilot CLI | `copilot` | | Copilot CLI | `copilot` |
+141
View File
@@ -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
}
@@ -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
}
+292
View File
@@ -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
}
+440
View File
@@ -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())
}
}
}
}
@@ -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()
}
+3
View File
@@ -9,6 +9,7 @@ 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{
"augment": {Name: "Augment Code CLI", ConfigDir: "~/.augment", ConfigFile: "settings.json"},
"claudecode": {Name: "Claude Code", ConfigDir: "~/.claude", ConfigFile: "settings.json"}, "claudecode": {Name: "Claude Code", ConfigDir: "~/.claude", ConfigFile: "settings.json"},
"codex": {Name: "OpenAI Codex", ConfigDir: "~/.codex", ConfigFile: "hooks.json"}, "codex": {Name: "OpenAI Codex", ConfigDir: "~/.codex", ConfigFile: "hooks.json"},
"cursor": {Name: "Cursor", ConfigDir: "~/.cursor", 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. // Returns nil if the agent is not supported.
func NewPlugin(agentName string) AgentPlugin { func NewPlugin(agentName string) AgentPlugin {
switch agentName { switch agentName {
case "augment":
return &AugmentPlugin{}
case "claudecode": case "claudecode":
return &ClaudeCodePlugin{} return &ClaudeCodePlugin{}
case "codex": case "codex":