@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.
Files changed (2) hide show
  1. package/bin/cli.mjs +314 -0
  2. 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
+ }