skillfish 1.0.32 → 1.0.34

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
@@ -290,6 +290,26 @@ Your submission will be reviewed and added to [skill.fish](https://skill.fish) a
290
290
 
291
291
  ---
292
292
 
293
+ ## Private Repositories
294
+
295
+ skillfish installs from private repositories when a GitHub token is available. Tokens are resolved in this order:
296
+
297
+ 1. `SKILLFISH_GITHUB_TOKEN` - skillfish-specific override
298
+ 2. `GITHUB_TOKEN` - standard
299
+ 3. `GH_TOKEN` - GitHub CLI standard
300
+ 4. `gh auth token` - falls back to the GitHub CLI if you're logged in
301
+
302
+ The simplest setup is `gh auth login`. Otherwise, export a token before running skillfish:
303
+
304
+ ```bash
305
+ export GITHUB_TOKEN=ghp_...
306
+ skillfish add owner/private-repo
307
+ ```
308
+
309
+ Public repos work with or without a token, but providing one raises the GitHub API rate limit from 60 to 5,000 requests per hour.
310
+
311
+ ---
312
+
293
313
  ## Non-Interactive Mode
294
314
 
295
315
  All commands work without prompts for use in scripts, CI pipelines, and automation. Non-interactive mode activates when:
@@ -21,9 +21,14 @@ export const AGENT_CONFIGS = [
21
21
  },
22
22
  {
23
23
  name: 'Windsurf',
24
- dir: '.codeium/windsurf/skills',
25
- homePaths: ['.codeium/windsurf/config.json', '.codeium/windsurf/argv.json'],
26
- cwdPaths: ['.codeium/windsurf'],
24
+ dir: '.windsurf/skills',
25
+ globalDir: '.codeium/windsurf/skills',
26
+ homePaths: [
27
+ '.codeium/windsurf/config.json',
28
+ '.codeium/windsurf/argv.json',
29
+ '.codeium/windsurf',
30
+ ],
31
+ cwdPaths: ['.windsurf'],
27
32
  },
28
33
  {
29
34
  name: 'Codex',
@@ -0,0 +1,20 @@
1
+ /**
2
+ * GitHub authentication helpers for skillfish.
3
+ */
4
+ /**
5
+ * Resolve a GitHub token from the environment or the local `gh` CLI.
6
+ *
7
+ * Priority:
8
+ * 1. SKILLFISH_GITHUB_TOKEN
9
+ * 2. GITHUB_TOKEN
10
+ * 3. GH_TOKEN
11
+ * 4. `gh auth token` (if gh is on PATH and the user is logged in)
12
+ *
13
+ * Resolution happens once per process and the result is cached. Set env vars
14
+ * before the first call, or call `resetGitHubTokenCache()` to re-resolve.
15
+ */
16
+ export declare function getGitHubToken(): string | undefined;
17
+ /** Returns true when a GitHub token is available. */
18
+ export declare function hasGitHubToken(): boolean;
19
+ /** Reset the per-process token cache. Intended for use in tests only. */
20
+ export declare function resetGitHubTokenCache(): void;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * GitHub authentication helpers for skillfish.
3
+ */
4
+ import { execFileSync } from 'child_process';
5
+ // Module-scoped cache: undefined = not yet resolved, null = resolved to no token
6
+ let cachedToken = undefined;
7
+ /**
8
+ * Resolve a GitHub token from the environment or the local `gh` CLI.
9
+ *
10
+ * Priority:
11
+ * 1. SKILLFISH_GITHUB_TOKEN
12
+ * 2. GITHUB_TOKEN
13
+ * 3. GH_TOKEN
14
+ * 4. `gh auth token` (if gh is on PATH and the user is logged in)
15
+ *
16
+ * Resolution happens once per process and the result is cached. Set env vars
17
+ * before the first call, or call `resetGitHubTokenCache()` to re-resolve.
18
+ */
19
+ export function getGitHubToken() {
20
+ if (cachedToken !== undefined) {
21
+ return cachedToken ?? undefined;
22
+ }
23
+ const fromEnv = process.env.SKILLFISH_GITHUB_TOKEN?.trim() ||
24
+ process.env.GITHUB_TOKEN?.trim() ||
25
+ process.env.GH_TOKEN?.trim();
26
+ if (fromEnv) {
27
+ cachedToken = fromEnv;
28
+ return cachedToken;
29
+ }
30
+ // Fallback: ask the gh CLI for the active token (~50 ms, silent on failure)
31
+ try {
32
+ const raw = execFileSync('gh', ['auth', 'token'], {
33
+ stdio: ['ignore', 'pipe', 'ignore'],
34
+ timeout: 2000,
35
+ })
36
+ .toString()
37
+ .trim();
38
+ cachedToken = raw || null;
39
+ }
40
+ catch {
41
+ cachedToken = null;
42
+ }
43
+ return cachedToken ?? undefined;
44
+ }
45
+ /** Returns true when a GitHub token is available. */
46
+ export function hasGitHubToken() {
47
+ return getGitHubToken() !== undefined;
48
+ }
49
+ /** Reset the per-process token cache. Intended for use in tests only. */
50
+ export function resetGitHubTokenCache() {
51
+ cachedToken = undefined;
52
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { isGitTreeResponse, extractSkillPaths } from '../utils.js';
5
5
  import { fetchWithRetry } from './http.js';
6
+ import { getGitHubToken, hasGitHubToken } from './auth.js';
6
7
  export const SKILL_FILENAME = 'SKILL.md';
7
8
  // === Helper Functions ===
8
9
  /**
@@ -43,7 +44,10 @@ export class RepoNotFoundError extends Error {
43
44
  owner;
44
45
  repo;
45
46
  constructor(owner, repo) {
46
- super(`Repository not found: ${owner}/${repo}. Check the owner/repo name.`);
47
+ const hint = hasGitHubToken()
48
+ ? ''
49
+ : ', or set GITHUB_TOKEN if this is a private repository (or run `gh auth login`)';
50
+ super(`Repository not found: ${owner}/${repo}. Check the owner/repo name${hint}.`);
47
51
  this.owner = owner;
48
52
  this.repo = repo;
49
53
  this.name = 'RepoNotFoundError';
@@ -68,6 +72,13 @@ export class GitHubApiError extends Error {
68
72
  }
69
73
  }
70
74
  // === Helper Functions ===
75
+ function githubHeaders() {
76
+ const headers = { 'User-Agent': 'skillfish' };
77
+ const token = getGitHubToken();
78
+ if (token)
79
+ headers['Authorization'] = `Bearer ${token}`;
80
+ return headers;
81
+ }
71
82
  /**
72
83
  * Check if a response indicates rate limiting and throw RateLimitError if so.
73
84
  * @throws {RateLimitError} When rate limit is exceeded
@@ -111,7 +122,7 @@ function wrapApiError(err) {
111
122
  * @throws {NetworkError} On network errors
112
123
  */
113
124
  export async function fetchDefaultBranch(owner, repo) {
114
- const headers = { 'User-Agent': 'skillfish' };
125
+ const headers = githubHeaders();
115
126
  const url = `https://api.github.com/repos/${owner}/${repo}`;
116
127
  try {
117
128
  const res = await fetchWithRetry(url, { headers });
@@ -137,7 +148,7 @@ export async function fetchDefaultBranch(owner, repo) {
137
148
  * Uses raw.githubusercontent.com which is not rate-limited like the API.
138
149
  */
139
150
  export async function fetchSkillMdContent(owner, repo, path, branch) {
140
- const headers = { 'User-Agent': 'skillfish' };
151
+ const headers = githubHeaders();
141
152
  const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
142
153
  try {
143
154
  const res = await fetchWithRetry(url, { headers }, 2);
@@ -159,7 +170,7 @@ export async function fetchSkillMdContent(owner, repo, path, branch) {
159
170
  * @throws {GitHubApiError} When the API response format is unexpected
160
171
  */
161
172
  export async function fetchTreeSha(owner, repo, branch) {
162
- const headers = { 'User-Agent': 'skillfish' };
173
+ const headers = githubHeaders();
163
174
  const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}`;
164
175
  try {
165
176
  const res = await fetchWithRetry(url, { headers });
@@ -190,7 +201,7 @@ export async function fetchTreeSha(owner, repo, branch) {
190
201
  * @throws {GitHubApiError} When the API response format is unexpected
191
202
  */
192
203
  export async function fetchRecursiveTree(owner, repo, branch) {
193
- const headers = { 'User-Agent': 'skillfish' };
204
+ const headers = githubHeaders();
194
205
  const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
195
206
  try {
196
207
  const res = await fetchWithRetry(url, { headers });
@@ -8,6 +8,7 @@ import { join } from 'path';
8
8
  import { randomUUID } from 'crypto';
9
9
  import { downloadTemplate } from 'giget';
10
10
  import { getAgentSkillDir } from './agents.js';
11
+ import { getGitHubToken, hasGitHubToken } from './auth.js';
11
12
  import { SKILL_FILENAME } from './github.js';
12
13
  import { writeManifest, MANIFEST_VERSION, } from './manifest.js';
13
14
  /**
@@ -118,9 +119,11 @@ export async function installSkill(owner, repo, skillPath, skillName, agents, op
118
119
  }
119
120
  gigetSource = `${gigetSource}#${branch}`;
120
121
  }
122
+ const githubToken = getGitHubToken();
121
123
  await downloadTemplate(gigetSource, {
122
124
  dir: tmpDir,
123
125
  forceClean: true,
126
+ ...(githubToken ? { auth: githubToken } : {}),
124
127
  });
125
128
  // Validate download
126
129
  const skillMdPath = join(tmpDir, SKILL_FILENAME);
@@ -242,7 +245,10 @@ export async function installSkill(owner, repo, skillPath, skillName, agents, op
242
245
  const errMsg = err instanceof Error ? err.message : String(err);
243
246
  // Provide more helpful error messages for common failures
244
247
  if (errMsg.includes('404') || errMsg.includes('Not Found')) {
245
- result.failureReason = `Repository or path not found: ${owner}/${repo}${skillPath !== SKILL_FILENAME ? `/${skillPath}` : ''}`;
248
+ const hint = hasGitHubToken()
249
+ ? ''
250
+ : ' (set GITHUB_TOKEN if this is a private repository, or run `gh auth login`)';
251
+ result.failureReason = `Repository or path not found: ${owner}/${repo}${skillPath !== SKILL_FILENAME ? `/${skillPath}` : ''}${hint}`;
246
252
  }
247
253
  else {
248
254
  result.failureReason = errMsg;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillfish",
3
- "version": "1.0.32",
3
+ "version": "1.0.34",
4
4
  "description": "All in one Skill manager for AI coding agents. Install, update, and sync Skills across Claude Code, Cursor, Copilot + more.",
5
5
  "type": "module",
6
6
  "bin": {