Skip to content

Commit 31a8002

Browse files
committed
feat: use git clone for skill discovery to avoid API rate limits (v0.7.6)
1 parent 81422cc commit 31a8002

File tree

4 files changed

+111
-6
lines changed

4 files changed

+111
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.7.6] - 2026-01-19
9+
10+
### Changed
11+
- **Skill Discovery**: Now uses `git clone` for skill discovery from configured repositories, eliminating the need for GitHub tokens for public repositories and avoiding API rate limits.
12+
813
## [0.7.5] - 2026-01-19
914

1015
### Added

cmd/install.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,27 @@ If no agent is specified, skills are installed to .agent/skills/ by default.`,
119119

120120
if targetRepo != nil {
121121
fmt.Printf("Fetching skills from repo '%s'...\n", input)
122-
repos, err := repository.FetchSkills(*targetRepo)
123-
if err != nil {
124-
fmt.Printf("Failed to fetch skills from repo '%s': %v\n", input, err)
125-
failed = append(failed, input)
126-
continue
122+
123+
var repos []github.Repository
124+
var err error
125+
126+
// Try git-based discovery first for 'dir' type repos (avoids API rate limits)
127+
if targetRepo.Type == "dir" {
128+
repos, err = repository.FetchSkillsViaGit(*targetRepo)
129+
}
130+
131+
// If git discovery failed or wasn't applicable, fall back to API
132+
if err != nil || targetRepo.Type != "dir" {
133+
if err != nil && targetRepo.Type == "dir" {
134+
// Optional: log or print why git failed if verbose
135+
// fmt.Printf("Git discovery failed: %v. Falling back to API...\n", err)
136+
}
137+
repos, err = repository.FetchSkills(*targetRepo)
138+
if err != nil {
139+
fmt.Printf("Failed to fetch skills from repo '%s': %v\n", input, err)
140+
failed = append(failed, input)
141+
continue
142+
}
127143
}
128144

129145
if len(repos) == 0 {

cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ the Agent ecosystem.`,
4545
// Uncomment the following line if your bare application
4646
// has an action associated with it:
4747
// Run: func(cmd *cobra.Command, args []string) { },
48-
Version: "0.7.5",
48+
Version: "0.7.6",
4949
}
5050

5151
// Execute adds all child commands to the root command and sets flags appropriately.

internal/repository/fetch.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ package repository
22

33
import (
44
"fmt"
5+
"os"
6+
"path/filepath"
57
"strings"
68

79
"github.com/yeasy/ask/internal/config"
10+
"github.com/yeasy/ask/internal/git"
811
"github.com/yeasy/ask/internal/github"
12+
"github.com/yeasy/ask/internal/skill"
913
)
1014

1115
// FetchSkills returns a list of skills available in the given repository
@@ -28,4 +32,84 @@ func FetchSkills(repo config.Repo) ([]github.Repository, error) {
2832
default:
2933
return nil, fmt.Errorf("unknown repository type: %s", repo.Type)
3034
}
35+
}
36+
}
37+
38+
// FetchSkillsViaGit clones a repo and discovers skills locally (no API needed)
39+
func FetchSkillsViaGit(repo config.Repo) ([]github.Repository, error) {
40+
if repo.Type != "dir" {
41+
return nil, fmt.Errorf("git fetch only supports 'dir' type repos")
42+
}
43+
44+
parts := strings.Split(repo.URL, "/")
45+
if len(parts) < 2 {
46+
return nil, fmt.Errorf("invalid repository URL format: %s", repo.URL)
47+
}
48+
49+
owner := parts[0]
50+
repoName := parts[1]
51+
subPath := ""
52+
if len(parts) > 2 {
53+
subPath = strings.Join(parts[2:], "/")
54+
}
55+
56+
cloneURL := fmt.Sprintf("https://github.com/%s/%s.git", owner, repoName)
57+
58+
// Create temp dir
59+
tempDir, err := os.MkdirTemp("", "ask-discovery-*")
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to create temp dir: %w", err)
62+
}
63+
defer func() { _ = os.RemoveAll(tempDir) }()
64+
65+
// Clone repo
66+
// Using generic Clone (depth 1)
67+
if err := git.Clone(cloneURL, tempDir); err != nil {
68+
return nil, fmt.Errorf("failed to clone repo: %w", err)
69+
}
70+
71+
// Scan for skills
72+
searchPath := tempDir
73+
if subPath != "" {
74+
searchPath = filepath.Join(tempDir, subPath)
75+
}
76+
77+
entries, err := os.ReadDir(searchPath)
78+
if err != nil {
79+
return nil, fmt.Errorf("failed to read skills directory: %w", err)
80+
}
81+
82+
var skills []github.Repository
83+
84+
for _, entry := range entries {
85+
if !entry.IsDir() {
86+
continue
87+
}
88+
89+
skillDir := filepath.Join(searchPath, entry.Name())
90+
if skill.FindSkillMD(skillDir) {
91+
// Found a skill, extract metadata
92+
var desc string
93+
if meta, err := skill.ParseSkillMD(skillDir); err == nil && meta != nil {
94+
desc = meta.Description
95+
}
96+
97+
// Construct installation argument (owner/repo/path/to/skill)
98+
skillRelPath := subPath
99+
if skillRelPath != "" {
100+
skillRelPath = skillRelPath + "/" + entry.Name()
101+
} else {
102+
skillRelPath = entry.Name()
103+
}
104+
installArg := fmt.Sprintf("%s/%s/%s", owner, repoName, skillRelPath)
105+
106+
skills = append(skills, github.Repository{
107+
Name: entry.Name(),
108+
Description: desc,
109+
HTMLURL: installArg, // Passing the install arg string
110+
})
111+
}
112+
}
113+
114+
return skills, nil
31115
}

0 commit comments

Comments
 (0)