package agents import ( "bufio" "fmt" "os" "path/filepath" "sort" "strconv" "strings" "codex-agent-manager/internal/codexhome" ) type Store struct { CodexHome string } func (s Store) List() ([]AgentDefinition, error) { agentsPath := filepath.Join(s.CodexHome, "agents") if info, err := os.Lstat(agentsPath); err != nil { if os.IsNotExist(err) { return []AgentDefinition{}, nil } return nil, err } else if info.Mode()&os.ModeSymlink != 0 { return nil, codexhome.ErrForbiddenPath } agentsDir, err := codexhome.ResolveInside(s.CodexHome, "agents") if err != nil { return nil, err } entries, err := os.ReadDir(agentsDir) if err != nil { if os.IsNotExist(err) { return []AgentDefinition{}, nil } return nil, err } sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) result := make([]AgentDefinition, 0, len(entries)) for _, entry := range entries { if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".toml") { continue } result = append(result, s.readOne(entry.Name())) } return result, nil } func (s Store) readOne(fileName string) AgentDefinition { path := filepath.Join(s.CodexHome, "agents", fileName) def := AgentDefinition{ ID: strings.TrimSuffix(fileName, filepath.Ext(fileName)), FilePath: path, FileName: fileName, ParseStatus: "valid", DraftStatus: "clean", ExtraFields: map[string]string{}, } safePath, err := codexhome.ResolveAgentTOML(s.CodexHome, fileName) if err != nil { def.ParseStatus = "invalid" def.ParseError = err.Error() return def } if info, err := os.Lstat(safePath); err != nil { def.ParseStatus = "invalid" def.ParseError = err.Error() return def } else if info.Mode()&os.ModeSymlink != 0 { def.ParseStatus = "invalid" def.ParseError = codexhome.ErrForbiddenPath.Error() return def } info, statErr := os.Stat(safePath) if statErr == nil { def.ModifiedAt = info.ModTime() } data, err := os.ReadFile(safePath) if err != nil { def.ParseStatus = "invalid" def.ParseError = err.Error() return def } def.Content = string(data) values, err := parseSimpleTOML(string(data)) if err != nil { def.ParseStatus = "invalid" def.ParseError = err.Error() return def } def.Name = values["name"] def.Description = values["description"] def.DeveloperInstructions = values["developer_instructions"] for key, value := range values { if key == "name" || key == "description" || key == "developer_instructions" { continue } def.ExtraFields[key] = value } return def } func parseSimpleTOML(input string) (map[string]string, error) { values := map[string]string{} scanner := bufio.NewScanner(strings.NewReader(input)) lineNumber := 0 for scanner.Scan() { lineNumber++ line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { return values, fmt.Errorf("第 %d 行不是有效的键值字段", lineNumber) } key := strings.TrimSpace(parts[0]) raw := strings.TrimSpace(parts[1]) if key == "" { return values, fmt.Errorf("第 %d 行缺少字段名", lineNumber) } if !isValidBareKey(key) { return values, fmt.Errorf("第 %d 行包含无效字段名", lineNumber) } if _, exists := values[key]; exists { return values, fmt.Errorf("第 %d 行重复字段名 %q", lineNumber, key) } value, err := parseTOMLString(raw, scanner, lineNumber) if err != nil { return values, err } values[key] = value } if err := scanner.Err(); err != nil { return values, err } return values, nil } func isValidBareKey(key string) bool { for _, char := range key { if char >= 'a' && char <= 'z' { continue } if char >= 'A' && char <= 'Z' { continue } if char >= '0' && char <= '9' { continue } if char == '_' || char == '-' { continue } return false } return true } func parseTOMLString(raw string, scanner *bufio.Scanner, lineNumber int) (string, error) { if strings.HasPrefix(raw, `"""`) { block := strings.TrimPrefix(raw, `"""`) for !strings.Contains(block, `"""`) && scanner.Scan() { block += "\n" + scanner.Text() } if !strings.Contains(block, `"""`) { return "", fmt.Errorf("第 %d 行未闭合的多行字符串", lineNumber) } value, trailing, _ := strings.Cut(block, `"""`) if strings.TrimSpace(trailing) != "" { return "", fmt.Errorf("第 %d 行多行字符串后存在不支持的内容", lineNumber) } return value, nil } if !strings.HasPrefix(raw, `"`) { return "", fmt.Errorf("第 %d 行仅支持字符串字段", lineNumber) } value, err := strconv.Unquote(raw) if err != nil { return "", fmt.Errorf("第 %d 行字符串字段语法无效", lineNumber) } return value, nil }