Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
286 changes: 51 additions & 235 deletions README.md

Large diffs are not rendered by default.

8 changes: 2 additions & 6 deletions cmd/dotagents/agents.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,21 +346,17 @@ func writeTOMLString(b *strings.Builder, key string, value string) {
}
b.WriteString(key)
b.WriteString(" = ")
b.WriteString(tomlQuote(value))
b.WriteString(strconv.Quote(value))
b.WriteString("\n")
}

func writeTOMLMultiline(b *strings.Builder, key string, value string) {
b.WriteString(key)
b.WriteString(" = ")
b.WriteString(tomlQuote(value))
b.WriteString(strconv.Quote(value))
b.WriteString("\n")
}

func tomlQuote(value string) string {
return strconv.Quote(value)
}

func codexModelFor(model string) string {
switch strings.ToLower(strings.TrimSpace(model)) {
case "haiku":
Expand Down
1 change: 1 addition & 0 deletions cmd/dotagents/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func mergeConfig(base *config, overlay config) {
base.MCPServers = mergeByKey(base.MCPServers, overlay.MCPServers, func(s mcpServerConfig) string { return strings.TrimSpace(s.Name) })
base.Hooks = mergeByKey(base.Hooks, overlay.Hooks, func(h hookConfig) string { return strings.TrimSpace(h.Name) })
base.Plugins = mergeByKey(base.Plugins, overlay.Plugins, func(p pluginConfig) string { return strings.TrimSpace(p.Name) })
base.Sources = mergeByKey(base.Sources, overlay.Sources, func(s sourceConfig) string { return strings.TrimSpace(s.Name) })
}

func mergeByKey[T any](base []T, overlay []T, key func(T) string) []T {
Expand Down
89 changes: 14 additions & 75 deletions cmd/dotagents/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,83 +271,14 @@ func claudeHooksConfigPath(home string) string {
}

func inspectClaudeHookMap(raw map[string]interface{}, hook hookConfig) string {
hooksRoot, ok := raw["hooks"].(map[string]interface{})
if !ok {
return stateMissing
}
groups, ok := hooksRoot[hook.Event].([]interface{})
if !ok {
return stateMissing
}
found := false
for _, groupRaw := range groups {
group, ok := groupRaw.(map[string]interface{})
if !ok {
continue
}
items, ok := group["hooks"].([]interface{})
if !ok {
continue
}
for _, itemRaw := range items {
item, ok := itemRaw.(map[string]interface{})
if !ok || !hookCommandMatches(item["command"], hook.Command) {
continue
}
found = true
if hookTimeoutMatches(item, hook.Timeout) {
return stateSynced
}
}
}
if found {
return stateDrifted
}
return stateMissing
return inspectGroupedHookMap(raw, hook, false)
}

func upsertClaudeHookMap(raw map[string]interface{}, hook hookConfig) {
hooksRoot, _ := raw["hooks"].(map[string]interface{})
if hooksRoot == nil {
hooksRoot = map[string]interface{}{}
raw["hooks"] = hooksRoot
}
groups, _ := hooksRoot[hook.Event].([]interface{})
if len(groups) == 0 {
groups = []interface{}{map[string]interface{}{"hooks": []interface{}{}}}
}
targetIndex := 0
for i, groupRaw := range groups {
group, ok := groupRaw.(map[string]interface{})
if !ok {
continue
}
items, _ := group["hooks"].([]interface{})
if containsHookCommand(items, hook.Command) {
targetIndex = i
break
}
}
for i, groupRaw := range groups {
group, _ := groupRaw.(map[string]interface{})
if group == nil {
if i != targetIndex {
continue
}
group = map[string]interface{}{}
groups[i] = group
}
items, _ := group["hooks"].([]interface{})
items = removeHookCommand(items, hook.Command)
if i == targetIndex {
items = append(items, renderHookEntry(hook))
}
group["hooks"] = items
}
hooksRoot[hook.Event] = groups
func inspectNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) string {
return inspectGroupedHookMap(raw, hook, true)
}

func inspectNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) string {
func inspectGroupedHookMap(raw map[string]interface{}, hook hookConfig, requireType bool) string {
hooksRoot, ok := raw["hooks"].(map[string]interface{})
if !ok {
return stateMissing
Expand All @@ -372,7 +303,7 @@ func inspectNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) strin
continue
}
found = true
if item["type"] == "command" && hookTimeoutMatches(item, hook.Timeout) {
if (!requireType || item["type"] == "command") && hookTimeoutMatches(item, hook.Timeout) {
return stateSynced
}
}
Expand All @@ -383,7 +314,15 @@ func inspectNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) strin
return stateMissing
}

func upsertClaudeHookMap(raw map[string]interface{}, hook hookConfig) {
upsertGroupedHookMap(raw, hook, renderHookEntry)
}

func upsertNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) {
upsertGroupedHookMap(raw, hook, renderNestedHookEntry)
}

func upsertGroupedHookMap(raw map[string]interface{}, hook hookConfig, render func(hookConfig) map[string]interface{}) {
hooksRoot, _ := raw["hooks"].(map[string]interface{})
if hooksRoot == nil {
hooksRoot = map[string]interface{}{}
Expand Down Expand Up @@ -417,7 +356,7 @@ func upsertNestedJSONHookMap(raw map[string]interface{}, hook hookConfig) {
items, _ := group["hooks"].([]interface{})
items = removeHookCommand(items, hook.Command)
if i == targetIndex {
items = append(items, renderNestedHookEntry(hook))
items = append(items, render(hook))
}
group["hooks"] = items
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/dotagents/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func inspectAmpAgent(agent agentConfig, expected map[string]string, cfg config,
}

sortReportLists(&report)
report.Synced = len(report.Missing) == 0 && len(report.Drifted) == 0 && len(report.Conflicts) == 0 && len(report.StaleManaged) == 0 && len(report.MissingMCP) == 0 && len(report.DriftedMCP) == 0 && len(report.MissingHook) == 0 && len(report.DriftedHook) == 0
report.Synced = isReportSynced(report)
return report, nil
}

Expand Down Expand Up @@ -408,7 +408,7 @@ func inspectHermesAgent(agent agentConfig, expected map[string]string, agentsSki
}

sortReportLists(&report)
report.Synced = len(report.Missing) == 0 && len(report.Drifted) == 0 && len(report.Conflicts) == 0 && len(report.StaleManaged) == 0 && len(report.MissingMCP) == 0 && len(report.DriftedMCP) == 0 && len(report.MissingHook) == 0 && len(report.DriftedHook) == 0
report.Synced = isReportSynced(report)
return report, nil
}

Expand Down
4 changes: 4 additions & 0 deletions cmd/dotagents/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type config struct {
ExternalSkills []externalSkillSource `yaml:"external_skills"`
Plugins []pluginConfig `yaml:"plugins,omitempty"`
Hooks []hookConfig `yaml:"hooks,omitempty"`
Sources []sourceConfig `yaml:"sources,omitempty"`
}

type pluginConfig struct {
Expand Down Expand Up @@ -171,6 +172,8 @@ func run(args []string) error {
return err
}
return runDoctor(opts)
case "sources":
return runSources(args[1:])
case "external":
return runExternal(args[1:])
case "promote":
Expand Down Expand Up @@ -245,6 +248,7 @@ func printUsage() {
fmt.Println(" dotagents mcp add <name> --command <cmd> Add/update canonical managed MCP")
fmt.Println(" dotagents mcp import <agent> <name> Import native MCP into canonical config")
fmt.Println(" dotagents mcp remove <name> Remove canonical managed MCP")
fmt.Println(" dotagents sources [--json|--compact] [name] Show external data source availability")
fmt.Println(" dotagents external list Show external skill sources and lock state")
fmt.Println(" dotagents external update [name ...] Move external sources to latest and rewrite the lock")
fmt.Println(" dotagents plugin add Install Claude Code plugin delivery for claude-code")
Expand Down
10 changes: 3 additions & 7 deletions cmd/dotagents/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,33 +103,29 @@ func applyAgentMCPSync(reports []agentReport, cfg config, home string) error {
}

func inspectMCPServer(agentName string, server mcpServerConfig, home string) (string, error) {
target, err := mcpTargetForAgent(agentName)
target, err := mcpTargetForHarness(agentName)
if err != nil {
return stateMissing, err
}
return target.inspect(target, server, home)
}

func patchMCPServer(agentName string, server mcpServerConfig, home string) error {
target, err := mcpTargetForAgent(agentName)
target, err := mcpTargetForHarness(agentName)
if err != nil {
return err
}
return target.patch(target, server, home)
}

func readNativeMCPServer(agentName string, name string, home string) (mcpServerConfig, error) {
target, err := mcpTargetForAgent(agentName)
target, err := mcpTargetForHarness(agentName)
if err != nil {
return mcpServerConfig{}, err
}
return target.read(target, name, home)
}

func mcpTargetForAgent(agentName string) (mcpTarget, error) {
return mcpTargetForHarness(agentName)
}

func inspectJSONMCPServer(target mcpTarget, server mcpServerConfig, home string) (string, error) {
configPath := target.configPath(home)
data, err := os.ReadFile(configPath)
Expand Down
Loading
Loading