@getskillmd/cli 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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/cli.js +430 -0
  4. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tekcify
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @getskillmd/cli
2
+
3
+ Install `skill.md` files into Claude Code, Cursor, and Windsurf with one command.
4
+
5
+ ## Install
6
+
7
+ No install needed — use `npx`:
8
+
9
+ ```bash
10
+ npx @getskillmd/cli add stripe
11
+ ```
12
+
13
+ Or install globally:
14
+
15
+ ```bash
16
+ npm i -g @getskillmd/cli
17
+ getskillmd add stripe
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Add a skill by slug
23
+
24
+ ```bash
25
+ npx @getskillmd/cli add stripe
26
+ npx @getskillmd/cli add tailwind --ide cursor
27
+ npx @getskillmd/cli add nextjs --ide all
28
+ ```
29
+
30
+ ### Generate from a URL
31
+
32
+ ```bash
33
+ npx @getskillmd/cli add https://stripe.com/docs/api
34
+ npx @getskillmd/cli add https://nextjs.org/docs --mode library
35
+ ```
36
+
37
+ Modes: `design` (default), `api`, `library`, `generic`.
38
+
39
+ ### List installed skills
40
+
41
+ ```bash
42
+ npx @getskillmd/cli list
43
+ ```
44
+
45
+ ## IDE behavior
46
+
47
+ | IDE | Path written | Notes |
48
+ | ------------ | ------------------------------------- | ---------------------------------- |
49
+ | Claude Code | `.claude/skills/<slug>/SKILL.md` | Directory created if missing |
50
+ | Cursor | `.cursor/rules/<slug>.mdc` | Wrapped with Cursor frontmatter |
51
+ | Windsurf | `.windsurfrules` | Appended in a labelled section |
52
+
53
+ If `--ide` is not provided, the CLI detects which config dirs already exist
54
+ in your project. If multiple are found (or none), you are prompted.
55
+
56
+ ## Environment
57
+
58
+ - `GETSKILLMD_API_URL` — override the API base URL. Defaults to `https://getskillmd.com`.
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+ import kleur from "kleur";
6
+ import prompts from "prompts";
7
+
8
+ // src/constants.ts
9
+ var DEFAULT_API_URL = "https://getskillmd.com";
10
+ var POLL_INTERVAL_MS = 2e3;
11
+ var POLL_TIMEOUT_MS = 24e4;
12
+ var REQUEST_TIMEOUT_MS = 3e4;
13
+ var IDE_LABELS = {
14
+ claude: "Claude Code",
15
+ cursor: "Cursor",
16
+ windsurf: "Windsurf"
17
+ };
18
+ var IDE_PATHS = {
19
+ claude: ".claude/skills",
20
+ cursor: ".cursor/rules",
21
+ windsurf: ".windsurfrules"
22
+ };
23
+ var WINDSURF_SECTION_HEADER_PREFIX = "# getskillmd:";
24
+ var WINDSURF_SECTION_FOOTER_PREFIX = "# end getskillmd:";
25
+ var VALID_MODES = ["design", "api", "library", "generic"];
26
+ var VALID_IDES = ["claude", "cursor", "windsurf", "all"];
27
+
28
+ // src/api.ts
29
+ function getBaseUrl() {
30
+ const fromEnv = process.env.GETSKILLMD_API_URL;
31
+ if (fromEnv && fromEnv.trim().length > 0) {
32
+ return fromEnv.replace(/\/$/, "");
33
+ }
34
+ return DEFAULT_API_URL;
35
+ }
36
+ async function request(path, init = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
37
+ const controller = new AbortController();
38
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
39
+ try {
40
+ const response = await fetch(`${getBaseUrl()}${path}`, {
41
+ ...init,
42
+ signal: controller.signal,
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ Accept: "application/json",
46
+ "User-Agent": "getskillmd-cli",
47
+ ...init.headers ?? {}
48
+ }
49
+ });
50
+ if (!response.ok) {
51
+ const text = await response.text().catch(() => "");
52
+ let message = `Request failed (${response.status})`;
53
+ try {
54
+ const parsed = JSON.parse(text);
55
+ message = parsed.error ?? parsed.message ?? message;
56
+ } catch {
57
+ if (text) message = text.slice(0, 200);
58
+ }
59
+ throw new ApiError(message, response.status);
60
+ }
61
+ return await response.json();
62
+ } finally {
63
+ clearTimeout(timer);
64
+ }
65
+ }
66
+ var ApiError = class extends Error {
67
+ status;
68
+ constructor(message, status) {
69
+ super(message);
70
+ this.name = "ApiError";
71
+ this.status = status;
72
+ }
73
+ };
74
+ async function fetchSkillBySlug(slug) {
75
+ return request(
76
+ `/api/cli/skills/${encodeURIComponent(slug)}`,
77
+ { method: "GET" }
78
+ );
79
+ }
80
+ async function startGenerationFromUrl(url, mode) {
81
+ return request("/api/cli/generate", {
82
+ method: "POST",
83
+ body: JSON.stringify({ url, mode })
84
+ });
85
+ }
86
+ async function pollGeneration(id) {
87
+ return request(`/api/cli/poll/${encodeURIComponent(id)}`, {
88
+ method: "GET"
89
+ });
90
+ }
91
+ async function waitForGeneration(id, onTick) {
92
+ const start = Date.now();
93
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
94
+ const result = await pollGeneration(id);
95
+ onTick(result.status);
96
+ if (result.status === "done" || result.status === "failed") {
97
+ return result;
98
+ }
99
+ await sleep(POLL_INTERVAL_MS);
100
+ }
101
+ throw new Error("Timed out waiting for generation");
102
+ }
103
+ function sleep(ms) {
104
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
105
+ }
106
+ function isUrlInput(input) {
107
+ if (input.startsWith("http://") || input.startsWith("https://")) return true;
108
+ if (input.includes("://")) return true;
109
+ return false;
110
+ }
111
+
112
+ // src/install.ts
113
+ import { existsSync } from "fs";
114
+ import { mkdir, readFile, writeFile } from "fs/promises";
115
+ import { dirname, join, resolve } from "path";
116
+ async function installSkill(input) {
117
+ const results = [];
118
+ for (const ide of input.ides) {
119
+ if (ide === "claude") {
120
+ results.push(await writeClaudeSkill(input.cwd, input.slug, input.skillMd));
121
+ } else if (ide === "cursor") {
122
+ results.push(await writeCursorRule(input.cwd, input.slug, input.skillMd));
123
+ } else if (ide === "windsurf") {
124
+ results.push(await writeWindsurfSection(input.cwd, input.slug, input.skillMd));
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+ async function writeClaudeSkill(cwd, slug, skillMd) {
130
+ const dir = resolve(cwd, IDE_PATHS.claude, slug);
131
+ const path = join(dir, "SKILL.md");
132
+ await mkdir(dir, { recursive: true });
133
+ await writeFile(path, skillMd, "utf8");
134
+ return { ide: "claude", path, written: true };
135
+ }
136
+ async function writeCursorRule(cwd, slug, skillMd) {
137
+ const dir = resolve(cwd, IDE_PATHS.cursor);
138
+ const path = join(dir, `${slug}.mdc`);
139
+ await mkdir(dir, { recursive: true });
140
+ const body = stripFrontmatter(skillMd);
141
+ const description = extractDescription(skillMd) ?? slug;
142
+ const content = [
143
+ "---",
144
+ `description: ${escapeYaml(description)}`,
145
+ "globs:",
146
+ ' - "**/*"',
147
+ "alwaysApply: false",
148
+ "---",
149
+ "",
150
+ body.trimStart()
151
+ ].join("\n");
152
+ await writeFile(path, content, "utf8");
153
+ return { ide: "cursor", path, written: true };
154
+ }
155
+ async function writeWindsurfSection(cwd, slug, skillMd) {
156
+ const path = resolve(cwd, IDE_PATHS.windsurf);
157
+ await mkdir(dirname(path), { recursive: true });
158
+ let existing = "";
159
+ if (existsSync(path)) {
160
+ existing = await readFile(path, "utf8");
161
+ }
162
+ const header = `${WINDSURF_SECTION_HEADER_PREFIX}${slug}`;
163
+ const footer = `${WINDSURF_SECTION_FOOTER_PREFIX}${slug}`;
164
+ const block = [header, "", skillMd.trimEnd(), "", footer, ""].join("\n");
165
+ const sectionRegex = new RegExp(
166
+ `(^|\\n)${escapeRegex(header)}[\\s\\S]*?${escapeRegex(footer)}\\n?`,
167
+ "g"
168
+ );
169
+ const stripped = existing.replace(sectionRegex, "$1");
170
+ const next = (stripped.trimEnd() + "\n\n" + block).trimStart();
171
+ await writeFile(path, next, "utf8");
172
+ return { ide: "windsurf", path, written: true };
173
+ }
174
+ function detectIdes(cwd) {
175
+ const detected = [];
176
+ if (existsSync(resolve(cwd, ".claude"))) detected.push("claude");
177
+ if (existsSync(resolve(cwd, ".cursor"))) detected.push("cursor");
178
+ if (existsSync(resolve(cwd, ".windsurfrules"))) detected.push("windsurf");
179
+ return detected;
180
+ }
181
+ function expandIdeSelection(selection) {
182
+ if (selection === "all") return ["claude", "cursor", "windsurf"];
183
+ return [selection];
184
+ }
185
+ function stripFrontmatter(raw) {
186
+ return raw.replace(/^---\n[\s\S]*?\n---\n*/, "");
187
+ }
188
+ function extractDescription(raw) {
189
+ const match = raw.match(/^description:\s*(.+)$/m);
190
+ if (!match) return null;
191
+ return match[1].trim().replace(/^"(.*)"$/, "$1");
192
+ }
193
+ function escapeYaml(value) {
194
+ if (/[":\n]/.test(value)) {
195
+ return `"${value.replace(/"/g, '\\"').replace(/\n/g, " ")}"`;
196
+ }
197
+ return value;
198
+ }
199
+ function escapeRegex(value) {
200
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
201
+ }
202
+
203
+ // src/list.ts
204
+ import { existsSync as existsSync2 } from "fs";
205
+ import { readdir, readFile as readFile2, stat } from "fs/promises";
206
+ import { join as join2, resolve as resolve2 } from "path";
207
+ async function listInstalledSkills(cwd) {
208
+ const skills = [];
209
+ skills.push(...await listClaudeSkills(cwd));
210
+ skills.push(...await listCursorSkills(cwd));
211
+ skills.push(...await listWindsurfSkills(cwd));
212
+ return skills;
213
+ }
214
+ async function listClaudeSkills(cwd) {
215
+ const root = resolve2(cwd, IDE_PATHS.claude);
216
+ if (!existsSync2(root)) return [];
217
+ const entries = await readdir(root);
218
+ const out = [];
219
+ for (const entry of entries) {
220
+ const dir = join2(root, entry);
221
+ const info = await stat(dir).catch(() => null);
222
+ if (!info?.isDirectory()) continue;
223
+ const path = join2(dir, "SKILL.md");
224
+ if (existsSync2(path)) {
225
+ out.push({ ide: "claude", slug: entry, path });
226
+ }
227
+ }
228
+ return out;
229
+ }
230
+ async function listCursorSkills(cwd) {
231
+ const root = resolve2(cwd, IDE_PATHS.cursor);
232
+ if (!existsSync2(root)) return [];
233
+ const entries = await readdir(root);
234
+ return entries.filter((entry) => entry.endsWith(".mdc")).map((entry) => ({
235
+ ide: "cursor",
236
+ slug: entry.replace(/\.mdc$/, ""),
237
+ path: join2(root, entry)
238
+ }));
239
+ }
240
+ async function listWindsurfSkills(cwd) {
241
+ const path = resolve2(cwd, IDE_PATHS.windsurf);
242
+ if (!existsSync2(path)) return [];
243
+ const content = await readFile2(path, "utf8");
244
+ const matches = content.matchAll(
245
+ new RegExp(`^${escapeRegex2(WINDSURF_SECTION_HEADER_PREFIX)}(.+)$`, "gm")
246
+ );
247
+ const out = [];
248
+ for (const match of matches) {
249
+ out.push({ ide: "windsurf", slug: match[1].trim(), path });
250
+ }
251
+ return out;
252
+ }
253
+ function escapeRegex2(value) {
254
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
255
+ }
256
+
257
+ // src/cli.ts
258
+ var VERSION = "0.1.0";
259
+ var program = new Command();
260
+ program.name("getskillmd").description("Install skill.md files into Claude Code, Cursor, and Windsurf").version(VERSION, "-v, --version", "Print the version");
261
+ program.command("add").argument("<slug-or-url>", "skill slug or source URL").option(
262
+ "--ide <ide>",
263
+ `target IDE: ${VALID_IDES.join(" | ")}`
264
+ ).option(
265
+ "--mode <mode>",
266
+ `generation mode (URL only): ${VALID_MODES.join(" | ")}`,
267
+ "design"
268
+ ).option("--cwd <path>", "working directory", process.cwd()).description("Install a skill into your IDE").action(async (input, options) => {
269
+ try {
270
+ await runAdd(input, options);
271
+ } catch (error) {
272
+ handleError(error);
273
+ }
274
+ });
275
+ program.command("list").option("--cwd <path>", "working directory", process.cwd()).description("List skills installed in this project").action(async (options) => {
276
+ try {
277
+ await runList(options);
278
+ } catch (error) {
279
+ handleError(error);
280
+ }
281
+ });
282
+ program.parseAsync(process.argv).catch(handleError);
283
+ async function runAdd(input, options) {
284
+ const cwd = options.cwd;
285
+ const ideSelection = await resolveIdeSelection(options.ide, cwd);
286
+ const ides = expandIdeSelection(ideSelection);
287
+ const mode = parseMode(options.mode);
288
+ const skill = isUrlInput(input) ? await generateFromUrl(input, mode) : await loadFromSlug(input);
289
+ console.log(
290
+ kleur.dim("Installing"),
291
+ kleur.cyan(skill.slug),
292
+ kleur.dim("into"),
293
+ kleur.bold(ides.map((i) => IDE_LABELS[i]).join(", "))
294
+ );
295
+ const results = await installSkill({
296
+ cwd,
297
+ slug: skill.slug,
298
+ skillMd: skill.skillMd,
299
+ ides
300
+ });
301
+ for (const result of results) {
302
+ console.log(
303
+ kleur.green(" +"),
304
+ kleur.bold(IDE_LABELS[result.ide]),
305
+ kleur.dim(`(${result.path.replace(cwd, ".")})`)
306
+ );
307
+ }
308
+ console.log();
309
+ console.log(kleur.green("Done."), kleur.dim(`${skill.slug} ready to use.`));
310
+ }
311
+ async function runList(options) {
312
+ const skills = await listInstalledSkills(options.cwd);
313
+ if (skills.length === 0) {
314
+ console.log(kleur.dim("No skills installed in this directory."));
315
+ console.log(
316
+ kleur.dim("Looked in:"),
317
+ Object.values(IDE_PATHS).join(", ")
318
+ );
319
+ return;
320
+ }
321
+ const grouped = /* @__PURE__ */ new Map();
322
+ for (const skill of skills) {
323
+ const list = grouped.get(skill.ide) ?? [];
324
+ list.push(skill);
325
+ grouped.set(skill.ide, list);
326
+ }
327
+ for (const [ide, list] of grouped) {
328
+ console.log(kleur.bold(IDE_LABELS[ide]));
329
+ for (const skill of list) {
330
+ console.log(" ", kleur.cyan(skill.slug), kleur.dim(skill.path));
331
+ }
332
+ }
333
+ }
334
+ async function loadFromSlug(slug) {
335
+ console.log(kleur.dim("Fetching"), kleur.cyan(slug), kleur.dim("..."));
336
+ const response = await fetchSkillBySlug(slug);
337
+ return { slug: response.slug, skillMd: response.skill_md };
338
+ }
339
+ async function generateFromUrl(url, mode) {
340
+ console.log(
341
+ kleur.dim("Generating skill from"),
342
+ kleur.cyan(url),
343
+ kleur.dim(`(${mode})`)
344
+ );
345
+ const start = await startGenerationFromUrl(url, mode);
346
+ let lastStatus = null;
347
+ const result = await waitForGeneration(start.id, (status) => {
348
+ if (status !== lastStatus) {
349
+ lastStatus = status;
350
+ console.log(kleur.dim(" status:"), status);
351
+ }
352
+ });
353
+ if (result.status === "failed") {
354
+ throw new Error(result.error ?? "Generation failed");
355
+ }
356
+ if (!result.skill_md || !result.slug) {
357
+ throw new Error("Generation completed without a skill payload");
358
+ }
359
+ return { slug: result.slug, skillMd: result.skill_md };
360
+ }
361
+ async function resolveIdeSelection(raw, cwd) {
362
+ if (raw) {
363
+ if (!isValidIde(raw)) {
364
+ throw new Error(
365
+ `Invalid --ide value "${raw}". Use one of: ${VALID_IDES.join(", ")}`
366
+ );
367
+ }
368
+ return raw;
369
+ }
370
+ const detected = detectIdes(cwd);
371
+ if (detected.length === 1) return detected[0];
372
+ if (detected.length === 0) {
373
+ const response2 = await prompts({
374
+ type: "select",
375
+ name: "ide",
376
+ message: "Which IDE?",
377
+ choices: [
378
+ { title: IDE_LABELS.claude, value: "claude" },
379
+ { title: IDE_LABELS.cursor, value: "cursor" },
380
+ { title: IDE_LABELS.windsurf, value: "windsurf" },
381
+ { title: "All of them", value: "all" }
382
+ ],
383
+ initial: 0
384
+ });
385
+ if (!response2.ide) throw new Error("Aborted");
386
+ return response2.ide;
387
+ }
388
+ const response = await prompts({
389
+ type: "select",
390
+ name: "ide",
391
+ message: "Multiple IDE configs detected. Which one?",
392
+ choices: [
393
+ ...detected.map((ide) => ({ title: IDE_LABELS[ide], value: ide })),
394
+ { title: "All detected", value: "all" }
395
+ ],
396
+ initial: 0
397
+ });
398
+ if (!response.ide) throw new Error("Aborted");
399
+ return response.ide;
400
+ }
401
+ function parseMode(raw) {
402
+ if (!raw) return "design";
403
+ if (!isValidMode(raw)) {
404
+ throw new Error(
405
+ `Invalid --mode value "${raw}". Use one of: ${VALID_MODES.join(", ")}`
406
+ );
407
+ }
408
+ return raw;
409
+ }
410
+ function isValidIde(value) {
411
+ return VALID_IDES.includes(value);
412
+ }
413
+ function isValidMode(value) {
414
+ return VALID_MODES.includes(value);
415
+ }
416
+ function handleError(error) {
417
+ if (error instanceof ApiError) {
418
+ console.error(kleur.red("Error:"), error.message);
419
+ if (error.status === 404) {
420
+ console.error(
421
+ kleur.dim("Hint: pass a full URL to generate a new skill from scratch.")
422
+ );
423
+ }
424
+ } else if (error instanceof Error) {
425
+ console.error(kleur.red("Error:"), error.message);
426
+ } else {
427
+ console.error(kleur.red("Error:"), String(error));
428
+ }
429
+ process.exit(1);
430
+ }
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@getskillmd/cli",
3
+ "version": "0.1.0",
4
+ "description": "CLI to install skill.md files into Claude Code, Cursor, and Windsurf",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "tekcify",
9
+ "url": "https://tekcify.com"
10
+ },
11
+ "homepage": "https://getskillmd.com",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/tekcify/getskillmd.git",
15
+ "directory": "packages/cli"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/tekcify/getskillmd/issues"
19
+ },
20
+ "keywords": [
21
+ "ai",
22
+ "claude",
23
+ "cursor",
24
+ "windsurf",
25
+ "skills",
26
+ "skill.md",
27
+ "agent"
28
+ ],
29
+ "bin": {
30
+ "getskillmd": "./dist/cli.js"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "dependencies": {
44
+ "commander": "^12.1.0",
45
+ "kleur": "^4.1.5",
46
+ "nanoid": "^5.1.11",
47
+ "prompts": "^2.4.2"
48
+ },
49
+ "devDependencies": {
50
+ "@types/node": "^20",
51
+ "@types/prompts": "^2.4.9",
52
+ "tsup": "^8.3.5",
53
+ "typescript": "^5"
54
+ },
55
+ "scripts": {
56
+ "build": "tsup",
57
+ "dev": "tsup --watch",
58
+ "type-check": "tsc --noEmit"
59
+ }
60
+ }