antikit 1.10.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,184 +10,191 @@ npm install -g antikit
10
10
 
11
11
  ## Features
12
12
 
13
- - **Multi-source Support**: Fetch skills from any GitHub repository.
14
- - **Sub-directory Support**: Skills can reside in sub-folders (e.g. `.claude/skills`).
15
- - **Interactive UI**: Browse, select, and update skills with a rich terminal UI.
16
- - **Dependency Management**: Automatically resolves and installs skill dependencies defined in `SKILL.md`.
17
- - **Smart Upgrades**: Detects version changes and allows easy upgrades.
18
- - **Autocomplete**: Full Zsh/Bash comparison support.
13
+ - **Multi-source Support**: Fetch skills from any GitHub repository (Public or Private).
14
+ - **Sub-directory Support**: Skills can reside in sub-folders (e.g. `.claude/skills`) within a repo.
15
+ - **Interactive UI**: Browse, select, multi-install, and update skills with a rich terminal interface.
16
+ - **Private Repo Access**: Seamless integration with private repositories via GitHub Token.
17
+ - **Smart Upgrades**: Detects version changes, manages dependencies, and streamlines updates.
18
+ - **Autocomplete**: Full Zsh/Bash/Fish tab-completion support.
19
+ - **Performance**: Optimized fetching using GitHub GraphQL API.
19
20
 
20
- ## Usage
21
+ ---
22
+
23
+ ## šŸš€ Quick Start
21
24
 
22
- ### ļæ½ Quick Start
25
+ ### 1. Setup Autocomplete (Recommended)
23
26
 
24
- **1. Setup Autocomplete (Recommended)**
27
+ This enables tab completion for commands and skill names.
25
28
 
26
29
  ```bash
27
30
  antikit completion
28
- # Follow instructions to add to ~/.zshrc or ~/.bashrc
31
+ # Follow the instructions to add the script to your ~/.zshrc or ~/.bashrc
29
32
  ```
30
33
 
31
- **2. Browse & Install Skills**
34
+ ### 2. Configure Authentication (Optional but Recommended)
32
35
 
33
- ```bash
34
- antikit list
35
- # or simply
36
- antikit ls
37
- ```
36
+ To avoid API rate limits (60 requests/hr) and access **Private Repositories**, configure a GitHub Token.
38
37
 
39
- _Shows an interactive menu to search, select, and install/update skills._
38
+ 1. **Create Token:** [Generate New Token](https://github.com/settings/tokens/new?description=antikit-cli&scopes=repo)
39
+ - Select scope `public_repo` (for public access) or `repo` (for private access).
40
+ 2. **Set Token:**
41
+ ```bash
42
+ antikit config set-token ghp_YOUR_TOKEN_HERE
43
+ ```
40
44
 
41
45
  ---
42
46
 
43
- ### ļæ½šŸ“¦ Manage Skills
47
+ ## šŸ“š Usage Guide
48
+
49
+ ### šŸ” Discovery & Listing
44
50
 
45
- #### List available skills
51
+ **Interactive Mode (Default)**
52
+ Browse skills, see installed status, updates available, and descriptions. Use Space to select multiple skills and Enter to install.
46
53
 
47
54
  ```bash
48
- # Interactive mode (Default) - Browse, Multi-select, Update
49
- antikit ls
55
+ antikit list
56
+ # Alias: antikit ls
57
+ ```
50
58
 
51
- # Search skills by name
52
- antikit ls -s <query>
59
+ **Search & Filters**
53
60
 
54
- # Text mode (Non-interactive list)
55
- antikit ls --text
61
+ ```bash
62
+ # Search by skill name
63
+ antikit ls -s <keyword>
56
64
 
57
65
  # Filter by source
58
66
  antikit ls --source official
67
+ antikit ls --source claudekit
68
+
69
+ # Text Mode (Non-interactive, good for scripting)
70
+ antikit ls --text
59
71
  ```
60
72
 
61
- #### Install a skill
73
+ ### ā¬‡ļø Installation & Updates
62
74
 
63
- Automatically installs dependencies defined in `SKILL.md`.
75
+ **Install Skills**
76
+ Automatically resolves and installs dependencies defined in `SKILL.md`.
64
77
 
65
78
  ```bash
66
79
  antikit install <skill-name>
67
- # or
68
- antikit i <skill-name>
80
+ # Alias: antikit i <skill-name>
69
81
 
70
- # Force overwrite if exists
82
+ # Force re-install
71
83
  antikit install <skill-name> --force
72
84
  ```
73
85
 
74
- #### Upgrade installed skills
75
-
76
- Update your local skills to the latest version from their sources.
86
+ **Upgrade Skills**
87
+ Keep your skills up-to-date with one command.
77
88
 
78
89
  ```bash
79
- # Upgrade all installed skills
90
+ # Upgrade all installed skills (Interactive confirmation)
80
91
  antikit upgrade
81
- # or
82
- antikit ug
92
+ # Alias: antikit ug
83
93
 
84
94
  # Upgrade a specific skill
85
95
  antikit upgrade <skill-name>
86
96
 
87
- # Upgrade without confirmation (good for scripts)
97
+ # Upgrade all without confirmation (CI/Script mode)
88
98
  antikit upgrade --yes
89
99
  ```
90
100
 
91
- #### List installed local skills
101
+ **Manage Local Skills**
92
102
 
93
103
  ```bash
104
+ # List locally installed skills
94
105
  antikit local
95
- # or
96
- antikit l
97
- ```
106
+ # Alias: antikit l
98
107
 
99
- #### Remove a skill
100
-
101
- ```bash
108
+ # Remove a skill
102
109
  antikit remove <skill-name>
103
- # or
104
- antikit rm <skill-name>
110
+ # Alias: antikit rm <skill-name>
105
111
  ```
106
112
 
107
- ---
113
+ ### šŸ“” Source Management
108
114
 
109
- ### šŸ“” Manage Sources
115
+ You can fetch skills from multiple repositories, including monorepos with sub-directories.
110
116
 
111
- You can fetch skills from multiple GitHub repositories, even from sub-directories.
117
+ **List Sources**
112
118
 
113
119
  ```bash
114
- # List configured sources
115
120
  antikit source list
121
+ ```
122
+
123
+ **Add Sources**
116
124
 
117
- # Add a standard Repo source (GitHub owner/repo)
118
- antikit source add vunamhung/antiskills
125
+ ```bash
126
+ # Add a Public/Private GitHub Repo
127
+ antikit source add owner/repo-name
119
128
 
120
- # Add a source from a SUB-DIRECTORY (e.g. monorepo)
129
+ # Add from a specific Sub-directory (e.g. monorepo)
121
130
  antikit source add mrgoonie/claudekit-skills --path .claude/skills --name claudekit
122
131
 
123
- # Add with a custom name
124
- antikit source add vunamhung/private-skills --name private
132
+ # Add with a custom alias
133
+ antikit source add my-org/private-skills --name work
134
+ ```
135
+
136
+ **Manage Sources**
125
137
 
126
- # Set a default source
127
- antikit source default private
138
+ ```bash
139
+ # Set a default source for basic installs
140
+ antikit source default work
128
141
 
129
142
  # Remove a source
130
- antikit source remove private
143
+ antikit source remove work
131
144
  ```
132
145
 
133
- ---
146
+ ### āš™ļø Configuration
147
+
148
+ Manage your local configuration and credentials.
134
149
 
135
- ### šŸ”„ Self Update
150
+ ```bash
151
+ # View current config (masked token)
152
+ antikit config list
136
153
 
137
- Update the `antikit` CLI tool itself to the latest version.
154
+ # Update GitHub Token
155
+ antikit config set-token <new_token>
156
+
157
+ # Remove Token
158
+ antikit config remove-token
159
+ ```
160
+
161
+ ### šŸ”„ Tool Maintenance
162
+
163
+ **Update CLI**
164
+ Update `antikit` itself to the latest version.
138
165
 
139
166
  ```bash
140
167
  antikit update
168
+ # Alias: antikit up
141
169
  ```
142
170
 
143
- _Note: You will also be notified automatically if a new version is available when running any command._
144
-
145
171
  ---
146
172
 
147
- ## Skill Development
173
+ ## šŸ›  Skill Development
148
174
 
149
175
  ### Skill Structure
150
176
 
151
- A skill is a directory containing a `SKILL.md` file.
177
+ A skill is simply a directory containing a `SKILL.md` (metadata & instructions) and any associated files.
152
178
 
153
179
  ### Defining Version & Dependencies
154
180
 
155
- You can specify version and dependencies in the `SKILL.md` frontmatter.
181
+ Add YAML frontmatter to your `SKILL.md` to enable versioning and automatic dependency installation.
156
182
 
157
183
  ```yaml
158
184
  ---
159
- name: my-skill
160
- description: A powerful skill that needs helpers
161
- version: 1.0.1
185
+ name: my-complex-skill
186
+ description: Performs magic operations
187
+ version: 1.2.0
162
188
  dependencies:
163
- - sql-helper
164
- - python-runner
189
+ - simple-helper-skill
190
+ - another-dependency
165
191
  ---
166
- # My Skill Content
192
+ # Skill Instructions
167
193
  ...
168
194
  ```
169
195
 
170
196
  ---
171
197
 
172
- ### šŸ” Authentication (Optional)
173
-
174
- To increase GitHub API rate limits (avoiding "API rate limit exceeded" errors), you can configure a Personal Access Token.
175
-
176
- ```bash
177
- # Set token
178
- antikit config set-token ghp_xxxxxxxxxxxx
179
-
180
- # Check config
181
- antikit config list
182
- # or
183
- antikit config ls
184
-
185
- # Remove token
186
- antikit config remove-token
187
- ```
188
-
189
- ---
190
-
191
198
  ## Requirements
192
199
 
193
200
  - Node.js >= 18.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antikit",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "CLI tool to manage AI agent skills from Anti Gravity skills repository",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "scripts": {
11
11
  "test": "echo \"Error: no test specified\" && exit 1",
12
- "semantic-release": "semantic-release",
12
+ "release": "commit-and-tag-version",
13
13
  "prepare": "husky",
14
14
  "format": "prettier --write ."
15
15
  },
@@ -47,16 +47,18 @@
47
47
  "@inquirer/prompts": "^8.2.0",
48
48
  "chalk": "^5.3.0",
49
49
  "commander": "^12.1.0",
50
+ "marked": "^15.0.12",
51
+ "marked-terminal": "^7.3.0",
50
52
  "omelette": "^0.4.17",
51
53
  "ora": "^8.1.1",
52
54
  "simple-git": "^3.27.0"
53
55
  },
54
56
  "devDependencies": {
55
- "@semantic-release/changelog": "^6.0.3",
56
- "@semantic-release/git": "^10.0.1",
57
+ "@commitlint/cli": "^20.3.1",
58
+ "@commitlint/config-conventional": "^20.3.1",
59
+ "commit-and-tag-version": "^12.6.1",
57
60
  "husky": "^9.1.7",
58
61
  "lint-staged": "^16.2.7",
59
- "prettier": "^3.7.4",
60
- "semantic-release": "^24.2.0"
62
+ "prettier": "^3.7.4"
61
63
  }
62
64
  }
@@ -0,0 +1,48 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { marked } from 'marked';
5
+ import TerminalRenderer from 'marked-terminal';
6
+ import { getSkillsDir, skillExists } from '../utils/local.js';
7
+ import { fetchSkillInfo } from '../utils/github.js';
8
+
9
+ // Setup marked to render to terminal
10
+ marked.setOptions({
11
+ renderer: new TerminalRenderer()
12
+ });
13
+
14
+ export async function showSkillInfo(skillName, options) {
15
+ if (!skillName) {
16
+ console.error(chalk.red('Please provide a skill name.'));
17
+ process.exit(1);
18
+ }
19
+
20
+ const skillsDir = getSkillsDir();
21
+ const localSkillPath = path.join(skillsDir, skillName, 'SKILL.md');
22
+
23
+ // 1. Try Local First
24
+ if (skillExists(skillName) && fs.existsSync(localSkillPath)) {
25
+ console.log(chalk.bold.green(`\nšŸ“– Viewing local docs for: ${skillName}\n`));
26
+ const content = fs.readFileSync(localSkillPath, 'utf8');
27
+ console.log(marked(content));
28
+ return;
29
+ }
30
+
31
+ // 2. Try Remote if not found locally
32
+ console.log(chalk.yellow(`Skill "${skillName}" not found locally. Searching remote...`));
33
+ const info = await fetchSkillInfo(skillName);
34
+
35
+ if (info && info.content) {
36
+ // Note: fetchSkillInfo currently returns parsed metadata (desc, version).
37
+ // I need to update fetchSkillInfo or create a new internal function to get THE RAW CONTENT for rendering.
38
+ // Wait, fetchSkillInfo in github.js calculates metadata from content but currently DOES NOT return the full content string.
39
+ // I should update fetchSkillInfo to return 'raw' content as well.
40
+
41
+ // Let's rely on the updated logic I'm about to add to github.js to return 'content'
42
+ console.log(chalk.bold.blue(`\nšŸ“– Viewing remote docs for: ${skillName}\n`));
43
+ console.log(marked(info.content));
44
+ } else {
45
+ // Fallback if I haven't updated github.js yet or skill not found
46
+ console.error(chalk.red(`\nāŒ Skill "${skillName}" not found in any configured source.`));
47
+ }
48
+ }
@@ -111,7 +111,14 @@ function displaySkillsList(skills) {
111
111
  return acc;
112
112
  }, {});
113
113
 
114
- for (const [sourceName, sourceSkills] of Object.entries(bySource)) {
114
+ const sortedSourceNames = Object.keys(bySource).sort((a, b) => {
115
+ if (a === 'official') return -1;
116
+ if (b === 'official') return 1;
117
+ return a.localeCompare(b);
118
+ });
119
+
120
+ for (const sourceName of sortedSourceNames) {
121
+ const sourceSkills = bySource[sourceName];
115
122
  console.log(chalk.magenta.bold(`\nšŸ“¦ ${sourceName}`));
116
123
  for (const skill of sourceSkills) {
117
124
  let status = chalk.dim(' ');
@@ -133,8 +140,10 @@ function displaySkillsList(skills) {
133
140
  }
134
141
 
135
142
  async function interactiveInstall(skills) {
136
- // Sort skills by Source then Name
143
+ // Sort skills by Source (Official first) then Name
137
144
  skills.sort((a, b) => {
145
+ if (a.source === 'official' && b.source !== 'official') return -1;
146
+ if (b.source === 'official' && a.source !== 'official') return 1;
138
147
  if (a.source !== b.source) return a.source.localeCompare(b.source);
139
148
  return a.name.localeCompare(b.name);
140
149
  });
@@ -0,0 +1,67 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ export async function validateSkill(targetPath = '.') {
6
+ const absolutePath = path.resolve(targetPath);
7
+
8
+ // If targetPath is a file (SKILL.md), use it. Iterate up if needed
9
+ let skillMdPath = absolutePath;
10
+ if (!skillMdPath.endsWith('SKILL.md')) {
11
+ skillMdPath = path.join(absolutePath, 'SKILL.md');
12
+ }
13
+
14
+ console.log(chalk.bold(`Inspecting: ${skillMdPath}\n`));
15
+
16
+ if (!fs.existsSync(skillMdPath)) {
17
+ console.error(chalk.red('āŒ SKILL.md not found!'));
18
+ console.log(
19
+ chalk.dim('Make sure you are in the root directory of the skill or provide a path.')
20
+ );
21
+ process.exit(1);
22
+ }
23
+
24
+ const content = fs.readFileSync(skillMdPath, 'utf8');
25
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
26
+
27
+ if (!match) {
28
+ console.error(chalk.red('āŒ Missing or invalid YAML Frontmatter.'));
29
+ console.log('File must start with:\n---\nkey: value\n---');
30
+ process.exit(1);
31
+ }
32
+
33
+ const frontmatter = match[1];
34
+
35
+ // Simple parser
36
+ const parse = key => {
37
+ const m = frontmatter.match(new RegExp(`${key}:\\s*(.+)`));
38
+ return m ? m[1].trim() : null;
39
+ };
40
+
41
+ const errors = [];
42
+ const name = parse('name');
43
+ const desc = parse('description');
44
+ const version = parse('version');
45
+
46
+ if (!name) errors.push('Missing "name" field');
47
+ if (!desc) errors.push('Missing "description" field');
48
+ if (!version) errors.push('Missing "version" field');
49
+
50
+ if (errors.length > 0) {
51
+ console.error(chalk.red('āŒ Validation Failed:'));
52
+ errors.forEach(e => console.error(chalk.red(` - ${e}`)));
53
+ process.exit(1);
54
+ }
55
+
56
+ // Validate dependencies format if exists
57
+ if (frontmatter.includes('dependencies:')) {
58
+ // Check indentation vaguely
59
+ // const deps = frontmatter.match(/dependencies:\s*\n(\s+-\s+.+\n)+/);
60
+ // Regex validation for list is hard, let's skip deep validation for now.
61
+ }
62
+
63
+ console.log(chalk.green('āœ… Skill Metadata is Valid!'));
64
+ console.log(chalk.dim('Name: ') + chalk.cyan(name));
65
+ console.log(chalk.dim('Version: ') + chalk.cyan(version));
66
+ console.log(chalk.dim('Description:') + desc);
67
+ }
package/src/index.js CHANGED
@@ -7,6 +7,8 @@ import { listRemoteSkills } from './commands/list.js';
7
7
  import { listLocalSkills } from './commands/local.js';
8
8
  import { installSkill } from './commands/install.js';
9
9
  import { removeSkill } from './commands/remove.js';
10
+ import { showSkillInfo } from './commands/info.js';
11
+ import { validateSkill } from './commands/validate.js';
10
12
  import { updateCli } from './commands/update.js';
11
13
  import { upgradeSkills } from './commands/upgrade.js';
12
14
  import { listSources, addNewSource, removeExistingSource, setDefault } from './commands/source.js';
@@ -59,6 +61,17 @@ program
59
61
  .description('Remove an installed skill')
60
62
  .action(removeSkill);
61
63
 
64
+ program
65
+ .command('info <skill>')
66
+ .alias('doc')
67
+ .description('Show skill documentation (SKILL.md)')
68
+ .action(showSkillInfo);
69
+
70
+ program
71
+ .command('validate [path]')
72
+ .description('Validate SKILL.md structure and metadata')
73
+ .action(validateSkill);
74
+
62
75
  program
63
76
  .command('update')
64
77
  .alias('up')
@@ -1,7 +1,11 @@
1
+ import chalk from 'chalk';
1
2
  import { getSources, getToken } from './configManager.js';
2
3
 
3
4
  const GITHUB_API = 'https://api.github.com';
4
5
 
6
+ // Global flag to prevent duplicate rate limit logs
7
+ let hasLoggedRateLimit = false;
8
+
5
9
  function getHeaders() {
6
10
  const headers = {
7
11
  Accept: 'application/vnd.github.v3+json',
@@ -14,6 +18,22 @@ function getHeaders() {
14
18
  return headers;
15
19
  }
16
20
 
21
+ function logRateLimitError() {
22
+ if (hasLoggedRateLimit) return;
23
+ hasLoggedRateLimit = true;
24
+
25
+ console.error(chalk.yellow('\nāš ļø GitHub API rate limit exceeded.'));
26
+ console.error(
27
+ chalk.dim('You are seeing this because unauthenticated requests are limited to 60/hr.')
28
+ );
29
+ console.error('\nTo fix this:');
30
+ console.error(
31
+ `1. Create a token: ${chalk.underline('https://github.com/settings/tokens/new?description=antikit-cli&scopes=repo')}`
32
+ );
33
+ console.error(`2. Run command: ${chalk.cyan('antikit config set-token <your_token>')}`);
34
+ console.error();
35
+ }
36
+
17
37
  /**
18
38
  * Fetch skills using GraphQL (Optimized: 1 request per source)
19
39
  */
@@ -44,8 +64,7 @@ async function fetchSkillsViaGraphQL(source, token) {
44
64
  }
45
65
  `;
46
66
 
47
- const branch = source.branch || 'main'; // This logic might need verifying branch exists, but usually main/master
48
- // Correct expression for path. If path is provided, it's "branch:path", else just "branch:"
67
+ const branch = source.branch || 'main';
49
68
  const expression = source.path ? `${branch}:${source.path}` : `${branch}:`;
50
69
 
51
70
  try {
@@ -79,7 +98,6 @@ async function fetchSkillsViaGraphQL(source, token) {
79
98
  let description = null;
80
99
  let version = '0.0.0';
81
100
 
82
- // Attempt to parse SKILL.md content if it exists
83
101
  const skillFile = item.object.file && item.object.file[0];
84
102
  if (skillFile && skillFile.object && skillFile.object.text) {
85
103
  const content = skillFile.object.text;
@@ -100,9 +118,10 @@ async function fetchSkillsViaGraphQL(source, token) {
100
118
  source: source.name,
101
119
  owner: source.owner,
102
120
  repo: source.repo,
121
+ branch: source.branch || 'main',
103
122
  basePath: source.path,
104
- description, // Pre-fetched!
105
- version // Pre-fetched!
123
+ description,
124
+ version
106
125
  };
107
126
  });
108
127
  } catch (e) {
@@ -114,7 +133,7 @@ async function fetchSkillsViaGraphQL(source, token) {
114
133
  * Fetch list of skills from a specific source
115
134
  */
116
135
  async function fetchSkillsFromSource(source) {
117
- // Try GraphQL first if token exists (Much faster)
136
+ // Try GraphQL first if token exists
118
137
  const token = getToken() || process.env.ANTIKIT_GITHUB_TOKEN || process.env.GITHUB_TOKEN;
119
138
  if (token) {
120
139
  const gqlResult = await fetchSkillsViaGraphQL(source, token);
@@ -126,7 +145,6 @@ async function fetchSkillsFromSource(source) {
126
145
  if (source.path) {
127
146
  url += `/${source.path}`;
128
147
  }
129
- // ... rest of function
130
148
 
131
149
  const response = await fetch(url, {
132
150
  headers: getHeaders()
@@ -137,11 +155,9 @@ async function fetchSkillsFromSource(source) {
137
155
 
138
156
  // Check for rate limit
139
157
  if (response.status === 403 && data.message.includes('rate limit')) {
140
- console.error('\nāš ļø GitHub API rate limit exceeded.');
141
- console.error(
142
- 'Please set GITHUB_TOKEN or ANTIKIT_GITHUB_TOKEN environment variable to increase limit.\n'
143
- );
158
+ logRateLimitError();
144
159
  }
160
+
145
161
  // Handle empty repository
146
162
  if (data.message === 'This repository is empty.') {
147
163
  return [];
@@ -153,7 +169,7 @@ async function fetchSkillsFromSource(source) {
153
169
  const contents = await response.json();
154
170
 
155
171
  if (!Array.isArray(contents)) {
156
- return []; // Handle case where path points to file, not dir
172
+ return [];
157
173
  }
158
174
 
159
175
  // Filter only directories (skills)
@@ -162,24 +178,39 @@ async function fetchSkillsFromSource(source) {
162
178
  .map(item => ({
163
179
  name: item.name,
164
180
  url: item.html_url,
165
- path: item.path, // Full path in repo (e.g. .claude/skills/foo)
181
+ path: item.path,
166
182
  source: source.name,
167
183
  owner: source.owner,
168
184
  repo: source.repo,
169
185
  branch: source.branch || 'main',
170
- basePath: source.path // Keep track of base path
186
+ basePath: source.path
171
187
  }));
172
188
 
173
189
  return skills;
174
190
  }
175
191
 
176
- // ... (fetchRemoteSkills remains same)
192
+ /**
193
+ * Fetch list of skills from all configured sources
194
+ */
195
+ export async function fetchRemoteSkills(sourceName = null) {
196
+ const sources = getSources();
197
+ const targetSources = sourceName ? sources.filter(s => s.name === sourceName) : sources;
198
+
199
+ if (targetSources.length === 0) {
200
+ throw new Error(`Source "${sourceName}" not found.`);
201
+ }
202
+
203
+ // Reset rate limit flag before new fetch
204
+ hasLoggedRateLimit = false;
205
+
206
+ const results = await Promise.all(targetSources.map(source => fetchSkillsFromSource(source)));
207
+ return results.flat();
208
+ }
177
209
 
178
210
  /**
179
211
  * Fetch SKILL.md content for a specific skill
180
212
  */
181
213
  export async function fetchSkillInfo(skillName, owner, repo, path = null, branch = null) {
182
- // If owner/repo not provided, search in all sources
183
214
  if (!owner || !repo) {
184
215
  const skills = await fetchRemoteSkills();
185
216
  const skill = skills.find(s => s.name === skillName);
@@ -192,23 +223,23 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
192
223
 
193
224
  let content = null;
194
225
 
195
- // Optimized: Use Raw URL if branch is known (avoids API rate limit)
226
+ // Optimized: Use Raw URL if branch is known
196
227
  if (branch) {
197
228
  let rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
198
229
  if (path) rawUrl += `/${path}`;
199
230
  rawUrl += `/${skillName}/SKILL.md`;
200
231
 
201
232
  try {
202
- const res = await fetch(rawUrl);
233
+ const res = await fetch(rawUrl, {
234
+ headers: getHeaders()
235
+ });
203
236
  if (res.ok) {
204
237
  content = await res.text();
205
238
  }
206
- } catch (e) {
207
- // Ignore fetch error, fallback to API
208
- }
239
+ } catch (e) {}
209
240
  }
210
241
 
211
- // Fallback: Use API (Counts against rate limit, but works if branch is wrong/private repo needs Auth)
242
+ // Fallback: Use API
212
243
  if (!content) {
213
244
  let url = `${GITHUB_API}/repos/${owner}/${repo}/contents`;
214
245
  if (path) {
@@ -221,6 +252,15 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
221
252
  });
222
253
 
223
254
  if (!response.ok) {
255
+ // Check for rate limit also here
256
+ if (response.status === 403) {
257
+ // We can check body/headers but usually 403 here means rate limit if 404 is handled
258
+ // But simpler just to ignore or log if we strictly check msg
259
+ try {
260
+ const d = await response.json();
261
+ if (d.message.includes('rate limit')) logRateLimitError();
262
+ } catch {}
263
+ }
224
264
  return null;
225
265
  }
226
266
 
@@ -228,7 +268,6 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
228
268
  content = Buffer.from(data.content, 'base64').toString('utf-8');
229
269
  }
230
270
 
231
- // Extract info from YAML frontmatter
232
271
  const match = content.match(/^---\n([\s\S]*?)\n---/);
233
272
  if (match) {
234
273
  const frontmatter = match[1];
@@ -237,11 +276,12 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
237
276
 
238
277
  return {
239
278
  description: descMatch ? descMatch[1].trim() : null,
240
- version: versionMatch ? versionMatch[1].trim() : '0.0.0'
279
+ version: versionMatch ? versionMatch[1].trim() : '0.0.0',
280
+ content // Return raw content
241
281
  };
242
282
  }
243
283
 
244
- return { description: null, version: '0.0.0' };
284
+ return { description: null, version: '0.0.0', content };
245
285
  }
246
286
 
247
287
  /**