package main import ( "encoding/json" "os" "path/filepath" "strings" ) // teamConfig is the shape of ~/.claude/teams//config.json type teamConfig struct { Name string `json:"name"` Members []teamMember `json:"members"` } type teamMember struct { AgentID string `json:"agentId"` // e.g. "backend-agent@lazy-tree" Name string `json:"name"` // e.g. "backend-agent" } // loadTeamAgentNames scans all team config files and returns a map // of agentID prefix -> name. The agentId in team configs has the format // "name@team-name", but the agentId in JSONL files is a short hex ID. // We can't directly match these, so instead we return all names and // try to match by looking at the JSONL filename pattern. // // Actually, the reliable approach: scan team configs and also try to // match via the session. For now, we build a map that can be matched // by the short agent ID from JSONL files. func loadTeamAgentNames() map[string]string { home, err := os.UserHomeDir() if err != nil { return nil } result := make(map[string]string) // Scan all team configs teamsDir := filepath.Join(home, ".claude", "teams") teamDirs, err := os.ReadDir(teamsDir) if err != nil { return result } for _, td := range teamDirs { if !td.IsDir() { continue } configPath := filepath.Join(teamsDir, td.Name(), "config.json") data, err := os.ReadFile(configPath) if err != nil { continue } var cfg teamConfig if err := json.Unmarshal(data, &cfg); err != nil { continue } for _, member := range cfg.Members { // The agentId in config is "name@team" format // We store by the name part — will try to match later name := member.Name if name == "" { continue } // Store the full config agent ID -> name mapping result[member.AgentID] = name } } return result } // resolveAgentName tries to find an agent's team name by checking if // any team config's member agentId contains this short agent ID as a prefix, // or by scanning the initial user event for context clues. func resolveAgentName(agentID string, agentNames map[string]string, events []Event) string { // Strategy 1: Check the initial user event for this agent. // The Task tool's "name" parameter appears in the parent's JSONL, not the agent's own. // But the first user event may contain "You are " or the agent name in context. for i := range events { evt := &events[i] if evt.AgentID != agentID || evt.Type != "user" { continue } // Check for Task tool spawn message with agent name in content var msg messageEnvelope if err := json.Unmarshal(evt.Message, &msg); err != nil { continue } var contentStr string if err := json.Unmarshal(msg.Content, &contentStr); err != nil { continue } // Look for teammate_id in the message — this is who SENT it, // and the agent often addresses itself by a role name // Check for "You are " or "You are Agent " if idx := strings.Index(contentStr, "You are "); idx >= 0 { rest := contentStr[idx+8:] // Take the next word or words until a period/newline end := strings.IndexAny(rest, ".\n,") if end > 0 && end < 40 { candidate := strings.TrimSpace(rest[:end]) // Clean up common patterns candidate = strings.TrimPrefix(candidate, "the ") if candidate != "" && len(candidate) < 30 { return candidate } } } // Only check the first user event break } return "" }