// // Copyright 2026 The InfiniFlow Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package source import ( "encoding/base64" "encoding/json" "fmt" "io" "net/http" "path" "strings" ) // GitHubSource handles GitHub repository skills type GitHubSource struct { client HTTPClientInterface } // NewGitHubSource creates a new GitHub source adapter func NewGitHubSource(client HTTPClientInterface) *GitHubSource { return &GitHubSource{client: client} } // SourceID returns the source identifier func (s *GitHubSource) SourceID() string { return "github" } // TrustLevel returns the trust level based on repository func (s *GitHubSource) TrustLevel(identifier string) string { owner, repo, _, err := parseGitHubURL(identifier) if err != nil { return "community" } if isTrustedGitHubRepo(owner, repo) { return "trusted" } return "community" } // Fetch retrieves a skill from GitHub func (s *GitHubSource) Fetch(identifier string) (*SkillBundle, error) { owner, repo, pathStr, err := parseGitHubURL(identifier) if err != nil { return nil, err } // Default to repo root if no path specified if pathStr == "" { pathStr = "." } // Try to get SKILL.md first to determine skill name skillName := repo meta := &SkillMetadata{Version: "1.0.0"} skillMdContent, err := s.fetchFileContent(owner, repo, path.Join(pathStr, "SKILL.md")) if err == nil { parsedMeta, parseErr := parseSkillFrontmatter(skillMdContent) if parseErr == nil { meta = parsedMeta if meta.Name != "" { skillName = meta.Name } } // If parsing fails, use default meta and skillName } // Fetch all files in the directory files, err := s.fetchDirectoryContents(owner, repo, pathStr) if err != nil { return nil, fmt.Errorf("failed to fetch directory contents: %w", err) } return &SkillBundle{ Name: skillName, Files: files, Source: "github", Identifier: identifier, TrustLevel: s.TrustLevel(identifier), Metadata: meta, }, nil } // Inspect retrieves metadata from GitHub func (s *GitHubSource) Inspect(identifier string) (*SkillMetadata, error) { owner, repo, pathStr, err := parseGitHubURL(identifier) if err != nil { return nil, err } skillMdPath := path.Join(pathStr, "SKILL.md") content, err := s.fetchFileContent(owner, repo, skillMdPath) if err != nil { // Return basic metadata if SKILL.md not found return &SkillMetadata{ Name: repo, Description: fmt.Sprintf("Skill from %s/%s", owner, repo), Version: "1.0.0", }, nil } meta, err := parseSkillFrontmatter(content) if err != nil { return nil, fmt.Errorf("invalid SKILL.md frontmatter in %s: %w", identifier, err) } return meta, nil } // fetchFileContent fetches a single file from GitHub func (s *GitHubSource) fetchFileContent(owner, repo, filePath string) (string, error) { var url string if filePath == "" || filePath == "." { url = fmt.Sprintf("https://api.github.com/repos/%s/%s/contents", owner, repo) } else { url = fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", owner, repo, filePath) } req, err := http.NewRequest("GET", url, nil) if err != nil { return "", err } req.Header.Set("Accept", "application/vnd.github.v3+json") req.Header.Set("User-Agent", "ragflow-cli") resp, err := s.client.Do(req) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("GitHub API returned %d", resp.StatusCode) } var result struct { Content string `json:"content"` Encoding string `json:"encoding"` } if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { return "", err } if result.Encoding == "base64" { decoded, err := base64.StdEncoding.DecodeString(result.Content) if err != nil { return "", err } return string(decoded), nil } return result.Content, nil } // fetchDirectoryContents recursively fetches directory contents from GitHub func (s *GitHubSource) fetchDirectoryContents(owner, repo, dirPath string) (map[string][]byte, error) { var url string if dirPath == "" || dirPath == "." { url = fmt.Sprintf("https://api.github.com/repos/%s/%s/contents", owner, repo) } else { url = fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", owner, repo, dirPath) } req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/vnd.github.v3+json") req.Header.Set("User-Agent", "ragflow-cli") resp, err := s.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("GitHub API returned %d", resp.StatusCode) } var items []struct { Name string `json:"name"` Path string `json:"path"` Type string `json:"type"` DownloadURL string `json:"download_url"` } if err := json.NewDecoder(resp.Body).Decode(&items); err != nil { return nil, err } files := make(map[string][]byte) for _, item := range items { // Skip hidden files and common ignore patterns if strings.HasPrefix(item.Name, ".") { continue } if item.Name == "node_modules" || item.Name == "__pycache__" { continue } if item.Type == "file" { // Calculate relative path relPath := item.Path if dirPath != "" && dirPath != "." { relPath = strings.TrimPrefix(item.Path, dirPath+"/") } content, err := s.downloadFile(item.DownloadURL) if err != nil { continue // Skip files we can't download } files[relPath] = content } else if item.Type == "dir" { // Recursively fetch subdirectory subFiles, err := s.fetchDirectoryContents(owner, repo, item.Path) if err != nil { continue } for subPath, content := range subFiles { relPath := subPath if dirPath != "" && dirPath != "." { relPath = strings.TrimPrefix(subPath, dirPath+"/") } files[relPath] = content } } } return files, nil } // downloadFile downloads a file from the given URL func (s *GitHubSource) downloadFile(url string) ([]byte, error) { resp, err := s.client.Get(url) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP %d", resp.StatusCode) } return io.ReadAll(resp.Body) }