@gathers/skills 0.1.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/bin/cli.mjs +314 -0
- package/package.json +28 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync, cpSync, rmSync } from "fs";
|
|
5
|
+
import { join, basename, sep } from "path";
|
|
6
|
+
import { tmpdir, homedir } from "os";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
|
|
9
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
const API = "https://skills.gather.is";
|
|
12
|
+
const GATHER_SKILL_REPO = "https://github.com/nicholasgriffintn/reskill.git";
|
|
13
|
+
const GATHER_SKILL_PATH = "skills/gather";
|
|
14
|
+
|
|
15
|
+
// Agent install paths — where each agent looks for skills
|
|
16
|
+
const AGENTS = {
|
|
17
|
+
"claude-code": { dir: ".claude/skills", global: join(homedir(), ".claude", "skills") },
|
|
18
|
+
"cursor": { dir: ".cursor/skills", global: join(homedir(), ".cursor", "skills") },
|
|
19
|
+
"windsurf": { dir: ".windsurf/skills", global: join(homedir(), ".windsurf", "skills") },
|
|
20
|
+
"cline": { dir: ".cline/skills", global: null },
|
|
21
|
+
"roo": { dir: ".roo/skills", global: join(homedir(), ".roo", "skills") },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const DIM = "\x1b[2m";
|
|
27
|
+
const BOLD = "\x1b[1m";
|
|
28
|
+
const GREEN = "\x1b[32m";
|
|
29
|
+
const CYAN = "\x1b[36m";
|
|
30
|
+
const YELLOW = "\x1b[33m";
|
|
31
|
+
const RED = "\x1b[31m";
|
|
32
|
+
const RESET = "\x1b[0m";
|
|
33
|
+
|
|
34
|
+
function log(msg) { console.log(msg); }
|
|
35
|
+
function dim(msg) { return `${DIM}${msg}${RESET}`; }
|
|
36
|
+
function bold(msg) { return `${BOLD}${msg}${RESET}`; }
|
|
37
|
+
function green(msg) { return `${GREEN}${msg}${RESET}`; }
|
|
38
|
+
function cyan(msg) { return `${CYAN}${msg}${RESET}`; }
|
|
39
|
+
function yellow(msg) { return `${YELLOW}${msg}${RESET}`; }
|
|
40
|
+
function red(msg) { return `${RED}${msg}${RESET}`; }
|
|
41
|
+
|
|
42
|
+
function makeTempDir() {
|
|
43
|
+
const dir = join(tmpdir(), `gather-skills-${randomBytes(4).toString("hex")}`);
|
|
44
|
+
mkdirSync(dir, { recursive: true });
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function cleanupTemp(dir) {
|
|
49
|
+
try { rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Detect which agents are present on this machine / in this project */
|
|
53
|
+
function detectAgents(isGlobal) {
|
|
54
|
+
const found = [];
|
|
55
|
+
for (const [name, paths] of Object.entries(AGENTS)) {
|
|
56
|
+
if (isGlobal) {
|
|
57
|
+
if (paths.global) found.push(name);
|
|
58
|
+
} else {
|
|
59
|
+
// Check if agent config dir exists in project or home
|
|
60
|
+
const projectDir = join(process.cwd(), paths.dir.split("/")[0]);
|
|
61
|
+
const homeDir = join(homedir(), paths.dir.split("/")[0]);
|
|
62
|
+
if (existsSync(projectDir) || existsSync(homeDir)) {
|
|
63
|
+
found.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Default to claude-code if nothing detected
|
|
68
|
+
if (found.length === 0) found.push("claude-code");
|
|
69
|
+
return found;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Copy a skill directory, excluding .git, README.md, metadata.json, _-prefixed */
|
|
73
|
+
function copySkillDir(src, dest) {
|
|
74
|
+
mkdirSync(dest, { recursive: true });
|
|
75
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (entry.name === ".git" || entry.name === "README.md" || entry.name === "metadata.json") continue;
|
|
78
|
+
if (entry.name.startsWith("_")) continue;
|
|
79
|
+
const srcPath = join(src, entry.name);
|
|
80
|
+
const destPath = join(dest, entry.name);
|
|
81
|
+
if (entry.isDirectory()) {
|
|
82
|
+
copySkillDir(srcPath, destPath);
|
|
83
|
+
} else {
|
|
84
|
+
cpSync(srcPath, destPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Track install on our API (fire-and-forget) */
|
|
90
|
+
function trackInstall(skillId) {
|
|
91
|
+
try {
|
|
92
|
+
fetch(`${API}/api/skills/${skillId}/installed`, { method: "POST" }).catch(() => {});
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Parse skill ID: "owner/repo/skill" → { owner, repo, skill } */
|
|
97
|
+
function parseSkillId(id) {
|
|
98
|
+
const parts = id.split("/");
|
|
99
|
+
if (parts.length === 3) return { owner: parts[0], repo: parts[1], skill: parts[2] };
|
|
100
|
+
if (parts.length === 2) return { owner: parts[0], repo: parts[1], skill: null };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Commands ────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
async function cmdInstall(flags) {
|
|
107
|
+
log("");
|
|
108
|
+
log(`${bold("skills.gather")} ${dim("— installing the Gather skill")}`);
|
|
109
|
+
log("");
|
|
110
|
+
|
|
111
|
+
const isGlobal = flags.global;
|
|
112
|
+
const agents = detectAgents(isGlobal);
|
|
113
|
+
|
|
114
|
+
// Fetch our SKILL.md directly from the API
|
|
115
|
+
log(` ${dim("→")} Fetching Gather skill...`);
|
|
116
|
+
let skillContent;
|
|
117
|
+
try {
|
|
118
|
+
const res = await fetch(`${API}/api/skill`);
|
|
119
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
120
|
+
skillContent = await res.text();
|
|
121
|
+
} catch (err) {
|
|
122
|
+
log(` ${red("✗")} Failed to fetch Gather skill: ${err.message}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Install to each detected agent
|
|
127
|
+
for (const agent of agents) {
|
|
128
|
+
const paths = AGENTS[agent];
|
|
129
|
+
const base = isGlobal ? paths.global : join(process.cwd(), paths.dir);
|
|
130
|
+
if (!base) continue;
|
|
131
|
+
|
|
132
|
+
const skillDir = join(base, "gather");
|
|
133
|
+
mkdirSync(skillDir, { recursive: true });
|
|
134
|
+
writeFileSync(join(skillDir, "SKILL.md"), skillContent, "utf-8");
|
|
135
|
+
log(` ${green("✓")} Installed to ${dim(skillDir)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
log("");
|
|
139
|
+
log(` ${green("Done!")} The Gather skill is now active.`);
|
|
140
|
+
log(` ${dim("It will guide you through browsing, installing, and reviewing skills.")}`);
|
|
141
|
+
log("");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function cmdAdd(skillId, flags) {
|
|
145
|
+
log("");
|
|
146
|
+
log(`${bold("skills.gather")} ${dim("— installing")} ${cyan(skillId)}`);
|
|
147
|
+
log("");
|
|
148
|
+
|
|
149
|
+
const parsed = parseSkillId(skillId);
|
|
150
|
+
if (!parsed) {
|
|
151
|
+
log(` ${red("✗")} Invalid skill ID. Use: ${yellow("owner/repo/skill-name")}`);
|
|
152
|
+
log(` ${dim("Example:")} npx @gathers/skills add anthropics/skills/pdf`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const isGlobal = flags.global;
|
|
157
|
+
const agents = detectAgents(isGlobal);
|
|
158
|
+
|
|
159
|
+
// Git clone the repo (shallow)
|
|
160
|
+
const tmp = makeTempDir();
|
|
161
|
+
const repoUrl = `https://github.com/${parsed.owner}/${parsed.repo}.git`;
|
|
162
|
+
log(` ${dim("→")} Cloning ${dim(parsed.owner + "/" + parsed.repo)}...`);
|
|
163
|
+
try {
|
|
164
|
+
execSync(`git clone --depth 1 --quiet "${repoUrl}" "${tmp}/repo"`, { stdio: "pipe" });
|
|
165
|
+
} catch (err) {
|
|
166
|
+
log(` ${red("✗")} Failed to clone: ${err.message}`);
|
|
167
|
+
cleanupTemp(tmp);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Find the skill directory — search multiple locations since repos
|
|
172
|
+
// nest skills in various ways (root, skills/, skill-name/, skills/skill-name/)
|
|
173
|
+
const repoDir = join(tmp, "repo");
|
|
174
|
+
let skillDir = null;
|
|
175
|
+
|
|
176
|
+
if (parsed.skill) {
|
|
177
|
+
// 3-part ID: search for skill-name/SKILL.md at various depths
|
|
178
|
+
const candidates = [
|
|
179
|
+
join(repoDir, parsed.skill), // repo/pdf/
|
|
180
|
+
join(repoDir, "skills", parsed.skill), // repo/skills/pdf/
|
|
181
|
+
join(repoDir, "src", parsed.skill), // repo/src/pdf/
|
|
182
|
+
repoDir, // repo/ (root, single-skill repo)
|
|
183
|
+
];
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
if (existsSync(join(candidate, "SKILL.md"))) {
|
|
186
|
+
skillDir = candidate;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// 2-part ID: look for SKILL.md at root
|
|
192
|
+
if (existsSync(join(repoDir, "SKILL.md"))) {
|
|
193
|
+
skillDir = repoDir;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!skillDir) {
|
|
198
|
+
log(` ${red("✗")} No SKILL.md found for "${parsed.skill || parsed.repo}"`);
|
|
199
|
+
log(` ${dim("Try checking the repo structure or skill name.")}`);
|
|
200
|
+
cleanupTemp(tmp);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check what files are in the skill
|
|
205
|
+
const files = readdirSync(skillDir).filter(f => !f.startsWith(".") && !f.startsWith("_") && f !== "README.md" && f !== "metadata.json");
|
|
206
|
+
log(` ${green("✓")} Found skill: ${files.length} file(s)`);
|
|
207
|
+
|
|
208
|
+
// Install to each detected agent
|
|
209
|
+
const skillName = parsed.skill || parsed.repo;
|
|
210
|
+
for (const agent of agents) {
|
|
211
|
+
const paths = AGENTS[agent];
|
|
212
|
+
const base = isGlobal ? paths.global : join(process.cwd(), paths.dir);
|
|
213
|
+
if (!base) continue;
|
|
214
|
+
|
|
215
|
+
const destDir = join(base, skillName);
|
|
216
|
+
copySkillDir(skillDir, destDir);
|
|
217
|
+
log(` ${green("✓")} Installed to ${dim(agent)}: ${dim(destDir)}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Track the install
|
|
221
|
+
trackInstall(skillId);
|
|
222
|
+
|
|
223
|
+
cleanupTemp(tmp);
|
|
224
|
+
|
|
225
|
+
log("");
|
|
226
|
+
log(` ${green("Done!")} ${cyan(skillName)} is now installed.`);
|
|
227
|
+
log(` ${dim("After using it, review it:")}`);
|
|
228
|
+
log(` ${dim("→")} POST ${API}/api/reviews/submit { "skill_id": "${skillId}", ... }`);
|
|
229
|
+
log("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function cmdBrowse(category) {
|
|
233
|
+
log("");
|
|
234
|
+
log(`${bold("skills.gather")} ${dim("— browsing skills")}`);
|
|
235
|
+
log("");
|
|
236
|
+
|
|
237
|
+
const url = category ? `${API}/c/${category}` : `${API}/llms.txt`;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const res = await fetch(url);
|
|
241
|
+
if (!res.ok) {
|
|
242
|
+
if (res.status === 404) {
|
|
243
|
+
log(` ${red("✗")} Category "${category}" not found.`);
|
|
244
|
+
log(` ${dim("Try:")} npx @gathers/skills browse frontend`);
|
|
245
|
+
} else {
|
|
246
|
+
log(` ${red("✗")} HTTP ${res.status}`);
|
|
247
|
+
}
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const text = await res.text();
|
|
251
|
+
log(text);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
log(` ${red("✗")} Failed to fetch: ${err.message}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function cmdHelp() {
|
|
259
|
+
log("");
|
|
260
|
+
log(`${bold("skills.gather")} ${dim("— browse, install, and review AI agent skills")}`);
|
|
261
|
+
log("");
|
|
262
|
+
log(` ${cyan("npx @gathers/skills install")} Install the Gather skill (do this first)`);
|
|
263
|
+
log(` ${cyan("npx @gathers/skills add")} ${yellow("<owner/repo/skill>")} Install a skill from the catalog`);
|
|
264
|
+
log(` ${cyan("npx @gathers/skills browse")} ${dim("[category]")} Browse skills (shows /llms.txt or category)`);
|
|
265
|
+
log(` ${cyan("npx @gathers/skills help")} Show this help`);
|
|
266
|
+
log("");
|
|
267
|
+
log(` ${dim("Flags:")}`);
|
|
268
|
+
log(` ${dim("-g, --global")} Install globally (all projects)`);
|
|
269
|
+
log("");
|
|
270
|
+
log(` ${dim("Examples:")}`);
|
|
271
|
+
log(` npx @gathers/skills install`);
|
|
272
|
+
log(` npx @gathers/skills add anthropics/skills/pdf`);
|
|
273
|
+
log(` npx @gathers/skills add vercel-labs/agent-skills/vercel-react-best-practices -g`);
|
|
274
|
+
log(` npx @gathers/skills browse frontend`);
|
|
275
|
+
log(` npx @gathers/skills browse security`);
|
|
276
|
+
log("");
|
|
277
|
+
log(` ${dim(`${API}/llms.txt`)}`);
|
|
278
|
+
log("");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
const args = process.argv.slice(2);
|
|
284
|
+
const command = args[0];
|
|
285
|
+
const flags = {
|
|
286
|
+
global: args.includes("-g") || args.includes("--global"),
|
|
287
|
+
};
|
|
288
|
+
// Filter out flags from positional args
|
|
289
|
+
const positional = args.filter(a => !a.startsWith("-"));
|
|
290
|
+
|
|
291
|
+
switch (command) {
|
|
292
|
+
case "install":
|
|
293
|
+
await cmdInstall(flags);
|
|
294
|
+
break;
|
|
295
|
+
case "add":
|
|
296
|
+
if (!positional[1]) {
|
|
297
|
+
log(`\n ${red("✗")} Missing skill ID.`);
|
|
298
|
+
log(` ${dim("Usage:")} npx @gathers/skills add ${yellow("owner/repo/skill-name")}\n`);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
}
|
|
301
|
+
await cmdAdd(positional[1], flags);
|
|
302
|
+
break;
|
|
303
|
+
case "browse":
|
|
304
|
+
await cmdBrowse(positional[1]);
|
|
305
|
+
break;
|
|
306
|
+
case "help":
|
|
307
|
+
case "--help":
|
|
308
|
+
case "-h":
|
|
309
|
+
cmdHelp();
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
cmdHelp();
|
|
313
|
+
break;
|
|
314
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gathers/skills",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Browse, install, and review AI agent skills — ranked by the community",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gather-skills": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"skills",
|
|
14
|
+
"ai-agents",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"cursor",
|
|
17
|
+
"agent-skills",
|
|
18
|
+
"code-review",
|
|
19
|
+
"gather"
|
|
20
|
+
],
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
}
|
|
28
|
+
}
|