skillfish 1.0.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/LICENSE +17 -0
- package/README.md +192 -0
- package/dist/commands/add.d.ts +5 -0
- package/dist/commands/add.js +477 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.js +278 -0
- package/dist/commands/remove.d.ts +5 -0
- package/dist/commands/remove.js +336 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +56 -0
- package/dist/lib/agents.d.ts +40 -0
- package/dist/lib/agents.js +146 -0
- package/dist/lib/constants.d.ts +65 -0
- package/dist/lib/constants.js +68 -0
- package/dist/lib/github.d.ts +52 -0
- package/dist/lib/github.js +185 -0
- package/dist/lib/installer.d.ts +64 -0
- package/dist/lib/installer.js +163 -0
- package/dist/telemetry.d.ts +10 -0
- package/dist/telemetry.js +27 -0
- package/dist/utils.d.ts +130 -0
- package/dist/utils.js +163 -0
- package/package.json +68 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent configuration and detection logic.
|
|
3
|
+
* Supports all agents from the Agent Skills specification: https://agentskills.io
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Agent configuration - data-driven for easier maintenance.
|
|
7
|
+
* Detection checks home directory (agent installed globally) and cwd (local project).
|
|
8
|
+
*/
|
|
9
|
+
export type AgentConfig = {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly dir: string;
|
|
12
|
+
readonly homePaths: readonly string[];
|
|
13
|
+
readonly cwdPaths: readonly string[];
|
|
14
|
+
};
|
|
15
|
+
export declare const AGENT_CONFIGS: readonly AgentConfig[];
|
|
16
|
+
/**
|
|
17
|
+
* Check if an agent is detected on the system.
|
|
18
|
+
* Checks home directory paths first, then current working directory paths.
|
|
19
|
+
*/
|
|
20
|
+
export declare function detectAgent(config: AgentConfig, baseDir?: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Runtime agent type with detect function.
|
|
23
|
+
*/
|
|
24
|
+
export type Agent = {
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly dir: string;
|
|
27
|
+
readonly detect: () => boolean;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Build AGENTS array from config (preserves existing API).
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildAgents(baseDir?: string): readonly Agent[];
|
|
33
|
+
/**
|
|
34
|
+
* Get all detected agents.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getDetectedAgents(baseDir?: string): readonly Agent[];
|
|
37
|
+
/**
|
|
38
|
+
* Get the skill directory path for an agent.
|
|
39
|
+
*/
|
|
40
|
+
export declare function getAgentSkillDir(agent: Agent | AgentConfig, baseDir: string): string;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent configuration and detection logic.
|
|
3
|
+
* Supports all agents from the Agent Skills specification: https://agentskills.io
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
export const AGENT_CONFIGS = [
|
|
9
|
+
// === Primary Agents (widely used) ===
|
|
10
|
+
{
|
|
11
|
+
name: 'Claude Code',
|
|
12
|
+
dir: '.claude/skills',
|
|
13
|
+
homePaths: ['.claude/settings.json', '.claude/projects.json', '.claude/credentials.json'],
|
|
14
|
+
cwdPaths: ['.claude'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Cursor',
|
|
18
|
+
dir: '.cursor/skills',
|
|
19
|
+
homePaths: ['.cursor/extensions', '.cursor/argv.json'],
|
|
20
|
+
cwdPaths: ['.cursor'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'Windsurf',
|
|
24
|
+
dir: '.codeium/windsurf/skills',
|
|
25
|
+
homePaths: ['.codeium/windsurf/config.json', '.codeium/windsurf/argv.json'],
|
|
26
|
+
cwdPaths: ['.codeium/windsurf'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'Codex',
|
|
30
|
+
dir: '.codex/skills',
|
|
31
|
+
homePaths: ['.codex/config.json', '.codex/settings.json', '.codex'],
|
|
32
|
+
cwdPaths: ['.codex'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'GitHub Copilot',
|
|
36
|
+
dir: '.github/skills',
|
|
37
|
+
homePaths: ['.copilot/config.json', '.copilot'],
|
|
38
|
+
cwdPaths: ['.github/skills', '.github/copilot-instructions.md'],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'Gemini CLI',
|
|
42
|
+
dir: '.gemini/skills',
|
|
43
|
+
homePaths: ['.gemini'],
|
|
44
|
+
cwdPaths: ['.gemini'],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'OpenCode',
|
|
48
|
+
dir: '.opencode/skills',
|
|
49
|
+
homePaths: ['.config/opencode', '.opencode'],
|
|
50
|
+
cwdPaths: ['.opencode'],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Goose',
|
|
54
|
+
dir: '.goose/skills',
|
|
55
|
+
homePaths: ['.config/goose'],
|
|
56
|
+
cwdPaths: ['.goose'],
|
|
57
|
+
},
|
|
58
|
+
// === Secondary Agents ===
|
|
59
|
+
{
|
|
60
|
+
name: 'Amp',
|
|
61
|
+
dir: '.agents/skills',
|
|
62
|
+
homePaths: ['.config/amp'],
|
|
63
|
+
cwdPaths: ['.agents'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'Roo Code',
|
|
67
|
+
dir: '.roo/skills',
|
|
68
|
+
homePaths: ['.roo'],
|
|
69
|
+
cwdPaths: ['.roo'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'Kiro CLI',
|
|
73
|
+
dir: '.kiro/skills',
|
|
74
|
+
homePaths: ['.kiro'],
|
|
75
|
+
cwdPaths: ['.kiro'],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Kilo Code',
|
|
79
|
+
dir: '.kilocode/skills',
|
|
80
|
+
homePaths: ['.kilocode'],
|
|
81
|
+
cwdPaths: ['.kilocode'],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'Trae',
|
|
85
|
+
dir: '.trae/skills',
|
|
86
|
+
homePaths: ['.trae'],
|
|
87
|
+
cwdPaths: ['.trae'],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Cline',
|
|
91
|
+
dir: '.cline/skills',
|
|
92
|
+
homePaths: ['.cline/settings.json', '.cline'],
|
|
93
|
+
cwdPaths: ['.cline'],
|
|
94
|
+
},
|
|
95
|
+
// === Additional Agents ===
|
|
96
|
+
{
|
|
97
|
+
name: 'Antigravity',
|
|
98
|
+
dir: '.gemini/antigravity/skills',
|
|
99
|
+
homePaths: ['.gemini/antigravity'],
|
|
100
|
+
cwdPaths: ['.agent'],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Droid',
|
|
104
|
+
dir: '.factory/skills',
|
|
105
|
+
homePaths: ['.factory'],
|
|
106
|
+
cwdPaths: ['.factory'],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'Clawdbot',
|
|
110
|
+
dir: '.clawdbot/skills',
|
|
111
|
+
homePaths: ['.clawdbot'],
|
|
112
|
+
cwdPaths: ['.clawdbot'],
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
/**
|
|
116
|
+
* Check if an agent is detected on the system.
|
|
117
|
+
* Checks home directory paths first, then current working directory paths.
|
|
118
|
+
*/
|
|
119
|
+
export function detectAgent(config, baseDir) {
|
|
120
|
+
const home = homedir();
|
|
121
|
+
const cwd = baseDir ?? process.cwd();
|
|
122
|
+
return (config.homePaths.some((p) => existsSync(join(home, p))) ||
|
|
123
|
+
config.cwdPaths.some((p) => existsSync(join(cwd, p))));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build AGENTS array from config (preserves existing API).
|
|
127
|
+
*/
|
|
128
|
+
export function buildAgents(baseDir) {
|
|
129
|
+
return AGENT_CONFIGS.map((config) => ({
|
|
130
|
+
name: config.name,
|
|
131
|
+
dir: config.dir,
|
|
132
|
+
detect: () => detectAgent(config, baseDir),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get all detected agents.
|
|
137
|
+
*/
|
|
138
|
+
export function getDetectedAgents(baseDir) {
|
|
139
|
+
return buildAgents(baseDir).filter((a) => a.detect());
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the skill directory path for an agent.
|
|
143
|
+
*/
|
|
144
|
+
export function getAgentSkillDir(agent, baseDir) {
|
|
145
|
+
return join(baseDir, agent.dir);
|
|
146
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for skillfish CLI.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Exit codes for skillfish CLI commands.
|
|
6
|
+
*
|
|
7
|
+
* These follow POSIX conventions where 0 is success and non-zero indicates error.
|
|
8
|
+
* The specific codes help agents and scripts understand failure reasons without
|
|
9
|
+
* parsing error messages.
|
|
10
|
+
*
|
|
11
|
+
* | Code | Constant | Meaning |
|
|
12
|
+
* |------|-----------------|------------------------------------------------|
|
|
13
|
+
* | 0 | SUCCESS | Command completed successfully |
|
|
14
|
+
* | 1 | GENERAL_ERROR | Unspecified error (fallback) |
|
|
15
|
+
* | 2 | INVALID_ARGS | Invalid arguments or options provided |
|
|
16
|
+
* | 3 | NETWORK_ERROR | Network failure (timeout, rate limit, etc.) |
|
|
17
|
+
* | 4 | NOT_FOUND | Requested resource not found (skill, agent) |
|
|
18
|
+
*/
|
|
19
|
+
export declare const EXIT_CODES: {
|
|
20
|
+
/** Command completed successfully */
|
|
21
|
+
readonly SUCCESS: 0;
|
|
22
|
+
/** Unspecified error (fallback) */
|
|
23
|
+
readonly GENERAL_ERROR: 1;
|
|
24
|
+
/** Invalid arguments or options provided */
|
|
25
|
+
readonly INVALID_ARGS: 2;
|
|
26
|
+
/** Network failure (timeout, rate limit, etc.) */
|
|
27
|
+
readonly NETWORK_ERROR: 3;
|
|
28
|
+
/** Requested resource not found (skill, agent) */
|
|
29
|
+
readonly NOT_FOUND: 4;
|
|
30
|
+
};
|
|
31
|
+
export type ExitCode = (typeof EXIT_CODES)[keyof typeof EXIT_CODES];
|
|
32
|
+
/**
|
|
33
|
+
* Structured error codes for JSON output.
|
|
34
|
+
* These provide machine-readable error classification for automation.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ERROR_CODES: {
|
|
37
|
+
/** Invalid arguments or options */
|
|
38
|
+
readonly INVALID_ARGS: "INVALID_ARGS";
|
|
39
|
+
/** GitHub API rate limit exceeded */
|
|
40
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
41
|
+
/** Repository not found on GitHub */
|
|
42
|
+
readonly REPO_NOT_FOUND: "REPO_NOT_FOUND";
|
|
43
|
+
/** Network timeout or connection error */
|
|
44
|
+
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
45
|
+
/** SKILL.md not found in repository */
|
|
46
|
+
readonly SKILL_NOT_FOUND: "SKILL_NOT_FOUND";
|
|
47
|
+
/** No agents detected on system */
|
|
48
|
+
readonly NO_AGENTS: "NO_AGENTS";
|
|
49
|
+
/** User cancelled the operation */
|
|
50
|
+
readonly CANCELLED: "CANCELLED";
|
|
51
|
+
/** File system operation failed */
|
|
52
|
+
readonly FS_ERROR: "FS_ERROR";
|
|
53
|
+
/** General/unclassified error */
|
|
54
|
+
readonly UNKNOWN: "UNKNOWN";
|
|
55
|
+
};
|
|
56
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
57
|
+
/**
|
|
58
|
+
* Pattern for validating safe names (owner, repo, skill, agent names).
|
|
59
|
+
* Only allows alphanumeric characters, dots, hyphens, and underscores.
|
|
60
|
+
*/
|
|
61
|
+
export declare const SAFE_NAME_PATTERN: RegExp;
|
|
62
|
+
/**
|
|
63
|
+
* Validates a name against the safe name pattern.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isValidName(name: string): boolean;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants for skillfish CLI.
|
|
3
|
+
*/
|
|
4
|
+
// === Exit Codes ===
|
|
5
|
+
/**
|
|
6
|
+
* Exit codes for skillfish CLI commands.
|
|
7
|
+
*
|
|
8
|
+
* These follow POSIX conventions where 0 is success and non-zero indicates error.
|
|
9
|
+
* The specific codes help agents and scripts understand failure reasons without
|
|
10
|
+
* parsing error messages.
|
|
11
|
+
*
|
|
12
|
+
* | Code | Constant | Meaning |
|
|
13
|
+
* |------|-----------------|------------------------------------------------|
|
|
14
|
+
* | 0 | SUCCESS | Command completed successfully |
|
|
15
|
+
* | 1 | GENERAL_ERROR | Unspecified error (fallback) |
|
|
16
|
+
* | 2 | INVALID_ARGS | Invalid arguments or options provided |
|
|
17
|
+
* | 3 | NETWORK_ERROR | Network failure (timeout, rate limit, etc.) |
|
|
18
|
+
* | 4 | NOT_FOUND | Requested resource not found (skill, agent) |
|
|
19
|
+
*/
|
|
20
|
+
export const EXIT_CODES = {
|
|
21
|
+
/** Command completed successfully */
|
|
22
|
+
SUCCESS: 0,
|
|
23
|
+
/** Unspecified error (fallback) */
|
|
24
|
+
GENERAL_ERROR: 1,
|
|
25
|
+
/** Invalid arguments or options provided */
|
|
26
|
+
INVALID_ARGS: 2,
|
|
27
|
+
/** Network failure (timeout, rate limit, etc.) */
|
|
28
|
+
NETWORK_ERROR: 3,
|
|
29
|
+
/** Requested resource not found (skill, agent) */
|
|
30
|
+
NOT_FOUND: 4,
|
|
31
|
+
};
|
|
32
|
+
// === Error Codes (for JSON output) ===
|
|
33
|
+
/**
|
|
34
|
+
* Structured error codes for JSON output.
|
|
35
|
+
* These provide machine-readable error classification for automation.
|
|
36
|
+
*/
|
|
37
|
+
export const ERROR_CODES = {
|
|
38
|
+
/** Invalid arguments or options */
|
|
39
|
+
INVALID_ARGS: 'INVALID_ARGS',
|
|
40
|
+
/** GitHub API rate limit exceeded */
|
|
41
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
42
|
+
/** Repository not found on GitHub */
|
|
43
|
+
REPO_NOT_FOUND: 'REPO_NOT_FOUND',
|
|
44
|
+
/** Network timeout or connection error */
|
|
45
|
+
NETWORK_ERROR: 'NETWORK_ERROR',
|
|
46
|
+
/** SKILL.md not found in repository */
|
|
47
|
+
SKILL_NOT_FOUND: 'SKILL_NOT_FOUND',
|
|
48
|
+
/** No agents detected on system */
|
|
49
|
+
NO_AGENTS: 'NO_AGENTS',
|
|
50
|
+
/** User cancelled the operation */
|
|
51
|
+
CANCELLED: 'CANCELLED',
|
|
52
|
+
/** File system operation failed */
|
|
53
|
+
FS_ERROR: 'FS_ERROR',
|
|
54
|
+
/** General/unclassified error */
|
|
55
|
+
UNKNOWN: 'UNKNOWN',
|
|
56
|
+
};
|
|
57
|
+
// === Name Validation ===
|
|
58
|
+
/**
|
|
59
|
+
* Pattern for validating safe names (owner, repo, skill, agent names).
|
|
60
|
+
* Only allows alphanumeric characters, dots, hyphens, and underscores.
|
|
61
|
+
*/
|
|
62
|
+
export const SAFE_NAME_PATTERN = /^[\w.-]+$/;
|
|
63
|
+
/**
|
|
64
|
+
* Validates a name against the safe name pattern.
|
|
65
|
+
*/
|
|
66
|
+
export function isValidName(name) {
|
|
67
|
+
return SAFE_NAME_PATTERN.test(name);
|
|
68
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API functions for skill discovery and fetching.
|
|
3
|
+
*/
|
|
4
|
+
export declare const SKILL_FILENAME = "SKILL.md";
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when GitHub API rate limit is exceeded.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RateLimitError extends Error {
|
|
9
|
+
resetTime?: Date | undefined;
|
|
10
|
+
constructor(resetTime?: Date | undefined);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when the repository is not found.
|
|
14
|
+
*/
|
|
15
|
+
export declare class RepoNotFoundError extends Error {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
constructor(owner: string, repo: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Thrown on network errors (timeout, connection refused, etc.).
|
|
22
|
+
*/
|
|
23
|
+
export declare class NetworkError extends Error {
|
|
24
|
+
constructor(message: string);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Thrown when GitHub API returns unexpected response format.
|
|
28
|
+
*/
|
|
29
|
+
export declare class GitHubApiError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fetch with retry and exponential backoff.
|
|
34
|
+
* Retries on network errors and 5xx responses.
|
|
35
|
+
*/
|
|
36
|
+
export declare function fetchWithRetry(url: string, options: RequestInit, maxRetries?: number): Promise<Response>;
|
|
37
|
+
/**
|
|
38
|
+
* Fetch raw SKILL.md content from GitHub.
|
|
39
|
+
* Uses raw.githubusercontent.com which is not rate-limited like the API.
|
|
40
|
+
* Tries both main and master branches in parallel for better performance.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchSkillMdContent(owner: string, repo: string, path: string): Promise<string | null>;
|
|
43
|
+
/**
|
|
44
|
+
* Find all SKILL.md files in a GitHub repository.
|
|
45
|
+
* Uses sequential branch checking to conserve API rate limit (60/hr unauthenticated).
|
|
46
|
+
*
|
|
47
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
48
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
49
|
+
* @throws {NetworkError} On network errors (timeout, connection refused)
|
|
50
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
51
|
+
*/
|
|
52
|
+
export declare function findAllSkillMdFiles(owner: string, repo: string): Promise<string[]>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub API functions for skill discovery and fetching.
|
|
3
|
+
*/
|
|
4
|
+
import { isGitTreeResponse, extractSkillPaths, sleep } from '../utils.js';
|
|
5
|
+
// === Constants ===
|
|
6
|
+
const API_TIMEOUT_MS = 10000;
|
|
7
|
+
const MAX_RETRIES = 3;
|
|
8
|
+
const RETRY_DELAYS_MS = [1000, 2000, 4000]; // Exponential backoff
|
|
9
|
+
const DEFAULT_BRANCHES = ['main', 'master'];
|
|
10
|
+
export const SKILL_FILENAME = 'SKILL.md';
|
|
11
|
+
// === Error Types ===
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when GitHub API rate limit is exceeded.
|
|
14
|
+
*/
|
|
15
|
+
export class RateLimitError extends Error {
|
|
16
|
+
resetTime;
|
|
17
|
+
constructor(resetTime) {
|
|
18
|
+
super(`GitHub API rate limit exceeded${resetTime ? `. Resets at ${resetTime.toISOString()}` : '. Please try again later.'}`);
|
|
19
|
+
this.resetTime = resetTime;
|
|
20
|
+
this.name = 'RateLimitError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Thrown when the repository is not found.
|
|
25
|
+
*/
|
|
26
|
+
export class RepoNotFoundError extends Error {
|
|
27
|
+
owner;
|
|
28
|
+
repo;
|
|
29
|
+
constructor(owner, repo) {
|
|
30
|
+
super(`Repository not found: ${owner}/${repo}. Check the owner/repo name.`);
|
|
31
|
+
this.owner = owner;
|
|
32
|
+
this.repo = repo;
|
|
33
|
+
this.name = 'RepoNotFoundError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Thrown on network errors (timeout, connection refused, etc.).
|
|
38
|
+
*/
|
|
39
|
+
export class NetworkError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'NetworkError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Thrown when GitHub API returns unexpected response format.
|
|
47
|
+
*/
|
|
48
|
+
export class GitHubApiError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = 'GitHubApiError';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// === Functions ===
|
|
55
|
+
/**
|
|
56
|
+
* Fetch with retry and exponential backoff.
|
|
57
|
+
* Retries on network errors and 5xx responses.
|
|
58
|
+
*/
|
|
59
|
+
export async function fetchWithRetry(url, options, maxRetries = MAX_RETRIES) {
|
|
60
|
+
let lastError = null;
|
|
61
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url, options);
|
|
64
|
+
// Success or client error (4xx) - don't retry
|
|
65
|
+
if (res.ok || (res.status >= 400 && res.status < 500)) {
|
|
66
|
+
return res;
|
|
67
|
+
}
|
|
68
|
+
// Server error (5xx) - retry
|
|
69
|
+
if (res.status >= 500) {
|
|
70
|
+
lastError = new Error(`Server error: ${res.status}`);
|
|
71
|
+
if (attempt < maxRetries - 1) {
|
|
72
|
+
await sleep(RETRY_DELAYS_MS[attempt] || 4000);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return res;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
80
|
+
// Network error - retry
|
|
81
|
+
if (attempt < maxRetries - 1) {
|
|
82
|
+
await sleep(RETRY_DELAYS_MS[attempt] || 4000);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw lastError || new Error('Max retries exceeded');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Fetch raw SKILL.md content from GitHub.
|
|
91
|
+
* Uses raw.githubusercontent.com which is not rate-limited like the API.
|
|
92
|
+
* Tries both main and master branches in parallel for better performance.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchSkillMdContent(owner, repo, path) {
|
|
95
|
+
const headers = { 'User-Agent': 'skillfish' };
|
|
96
|
+
// Try both branches in parallel
|
|
97
|
+
const results = await Promise.allSettled(DEFAULT_BRANCHES.map(async (branch) => {
|
|
98
|
+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
|
|
99
|
+
const res = await fetchWithRetry(url, { headers }, 2);
|
|
100
|
+
if (!res.ok)
|
|
101
|
+
throw new Error(`HTTP ${res.status}`);
|
|
102
|
+
return res.text();
|
|
103
|
+
}));
|
|
104
|
+
// Return first successful result
|
|
105
|
+
for (const result of results) {
|
|
106
|
+
if (result.status === 'fulfilled') {
|
|
107
|
+
return result.value;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Find all SKILL.md files in a GitHub repository.
|
|
114
|
+
* Uses sequential branch checking to conserve API rate limit (60/hr unauthenticated).
|
|
115
|
+
*
|
|
116
|
+
* @throws {RateLimitError} When GitHub API rate limit is exceeded
|
|
117
|
+
* @throws {RepoNotFoundError} When the repository is not found
|
|
118
|
+
* @throws {NetworkError} On network errors (timeout, connection refused)
|
|
119
|
+
* @throws {GitHubApiError} When the API response format is unexpected
|
|
120
|
+
*/
|
|
121
|
+
export async function findAllSkillMdFiles(owner, repo) {
|
|
122
|
+
const headers = { 'User-Agent': 'skillfish' };
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
125
|
+
try {
|
|
126
|
+
// Try each branch sequentially to conserve rate limit
|
|
127
|
+
for (const branch of DEFAULT_BRANCHES) {
|
|
128
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
|
|
129
|
+
try {
|
|
130
|
+
const res = await fetchWithRetry(url, { headers, signal: controller.signal });
|
|
131
|
+
// Check for rate limiting
|
|
132
|
+
if (res.status === 403) {
|
|
133
|
+
const remaining = res.headers.get('X-RateLimit-Remaining');
|
|
134
|
+
if (remaining === '0') {
|
|
135
|
+
const resetHeader = res.headers.get('X-RateLimit-Reset');
|
|
136
|
+
const resetTime = resetHeader ? new Date(parseInt(resetHeader) * 1000) : undefined;
|
|
137
|
+
throw new RateLimitError(resetTime);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 404 means branch doesn't exist, try next
|
|
141
|
+
if (res.status === 404) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const rawData = await res.json();
|
|
148
|
+
if (!isGitTreeResponse(rawData)) {
|
|
149
|
+
throw new GitHubApiError('Unexpected response format from GitHub API.');
|
|
150
|
+
}
|
|
151
|
+
return extractSkillPaths(rawData, SKILL_FILENAME);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
// Re-throw typed errors
|
|
155
|
+
if (err instanceof RateLimitError ||
|
|
156
|
+
err instanceof GitHubApiError) {
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
// If this is the last branch, let the error propagate
|
|
160
|
+
if (branch === DEFAULT_BRANCHES[DEFAULT_BRANCHES.length - 1]) {
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
// Otherwise try next branch
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// No branch found
|
|
168
|
+
throw new RepoNotFoundError(owner, repo);
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
// Re-throw typed errors
|
|
172
|
+
if (err instanceof RateLimitError ||
|
|
173
|
+
err instanceof RepoNotFoundError ||
|
|
174
|
+
err instanceof GitHubApiError) {
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
178
|
+
throw new NetworkError('Request timed out. Check your network connection.');
|
|
179
|
+
}
|
|
180
|
+
throw new NetworkError(`Network error: ${err instanceof Error ? err.message : 'unknown error'}`);
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
clearTimeout(timeoutId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill installation logic.
|
|
3
|
+
* Handles downloading, validating, and installing skills to agent directories.
|
|
4
|
+
*/
|
|
5
|
+
import type { Agent } from './agents.js';
|
|
6
|
+
export interface InstallResult {
|
|
7
|
+
installed: Array<{
|
|
8
|
+
skill: string;
|
|
9
|
+
agent: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}>;
|
|
12
|
+
skipped: Array<{
|
|
13
|
+
skill: string;
|
|
14
|
+
agent: string;
|
|
15
|
+
reason: string;
|
|
16
|
+
}>;
|
|
17
|
+
warnings: string[];
|
|
18
|
+
failed: boolean;
|
|
19
|
+
failureReason?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface InstallOptions {
|
|
22
|
+
force: boolean;
|
|
23
|
+
baseDir: string;
|
|
24
|
+
}
|
|
25
|
+
export interface CopyResult {
|
|
26
|
+
warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Thrown when SKILL.md is not found in downloaded content.
|
|
30
|
+
*/
|
|
31
|
+
export declare class SkillMdNotFoundError extends Error {
|
|
32
|
+
skillPath: string;
|
|
33
|
+
constructor(skillPath: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively copies a directory while skipping symlinks for security.
|
|
37
|
+
* This prevents symlink attacks where malicious repos could link to sensitive files.
|
|
38
|
+
*
|
|
39
|
+
* SECURITY: Uses double-check pattern to minimize TOCTOU race window.
|
|
40
|
+
* The second lstatSync check immediately before cpSync reduces (but doesn't
|
|
41
|
+
* eliminate) the window for a race condition attack.
|
|
42
|
+
*
|
|
43
|
+
* @returns CopyResult with any warnings generated during copy
|
|
44
|
+
*/
|
|
45
|
+
export declare function safeCopyDir(src: string, dest: string): CopyResult;
|
|
46
|
+
/**
|
|
47
|
+
* Download and install a skill to multiple agent directories.
|
|
48
|
+
*
|
|
49
|
+
* @param owner - GitHub repository owner
|
|
50
|
+
* @param repo - GitHub repository name
|
|
51
|
+
* @param skillPath - Path to skill within repository (or SKILL.md for root)
|
|
52
|
+
* @param skillName - Name to use for the installed skill directory
|
|
53
|
+
* @param agents - List of agents to install to
|
|
54
|
+
* @param options - Installation options (force, baseDir)
|
|
55
|
+
* @returns InstallResult with details of what was installed/skipped
|
|
56
|
+
*/
|
|
57
|
+
export declare function installSkill(owner: string, repo: string, skillPath: string, skillName: string, agents: readonly Agent[], options: InstallOptions): Promise<InstallResult>;
|
|
58
|
+
/**
|
|
59
|
+
* List installed skills for a given agent directory.
|
|
60
|
+
*
|
|
61
|
+
* @param skillDir - Path to the agent's skills directory
|
|
62
|
+
* @returns Array of skill names that have a valid SKILL.md
|
|
63
|
+
*/
|
|
64
|
+
export declare function listInstalledSkillsInDir(skillDir: string): string[];
|