openskillmd 0.2.0 → 0.3.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 (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +268 -113
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -66,6 +66,10 @@ The API base URL is resolved in this order:
66
66
  OPENSKILL_API_URL=http://localhost:8080/api osm search testing
67
67
  ```
68
68
 
69
+ > Maintainers: see [docs/environments-and-releases.md](docs/environments-and-releases.md)
70
+ > for how environments (staging/prod) and release channels (`latest`/`beta`) work —
71
+ > **one CLI, never per-environment packages.**
72
+
69
73
  ## Coming soon
70
74
 
71
75
  Authenticated publishing (`osm login` / `osm publish`) is in development and not
package/dist/index.js CHANGED
@@ -7,6 +7,16 @@ import { Command } from "commander";
7
7
  import { spawn } from "child_process";
8
8
  import { createRequire } from "module";
9
9
  import chalk3 from "chalk";
10
+ import ora from "ora";
11
+
12
+ // src/lib/config.ts
13
+ import { chmod, mkdir, readFile, writeFile } from "fs/promises";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+ import { z as z2 } from "zod";
17
+
18
+ // src/lib/env.ts
19
+ import { z } from "zod";
10
20
 
11
21
  // src/lib/format.ts
12
22
  import chalk2 from "chalk";
@@ -89,6 +99,155 @@ function jsonResult(obj) {
89
99
  process.stdout.write(JSON.stringify(obj) + "\n");
90
100
  }
91
101
 
102
+ // src/lib/env.ts
103
+ var envSchema = z.object({
104
+ OPENSKILL_API_URL: z.preprocess(
105
+ // An empty string behaved like "unset" before validation existed
106
+ // (`process.env.X || fallback`); keep that contract.
107
+ (value) => value === "" ? void 0 : value,
108
+ z.url(
109
+ "OPENSKILL_API_URL must be a full URL, e.g. https://staging.openskill.md/api"
110
+ ).optional()
111
+ )
112
+ });
113
+ function parseEnv(raw) {
114
+ const result = envSchema.safeParse(raw);
115
+ if (!result.success) {
116
+ const issue = result.error.issues[0];
117
+ const where = issue?.path.join(".") ?? "environment";
118
+ return { ok: false, message: `${where}: ${issue?.message ?? "invalid"}` };
119
+ }
120
+ return { ok: true, env: result.data };
121
+ }
122
+ var cached;
123
+ function getEnv() {
124
+ if (cached) return cached;
125
+ const result = parseEnv(process.env);
126
+ if (!result.ok) {
127
+ console.error(
128
+ errorMessage(
129
+ result.message,
130
+ "Fix or unset the variable, then re-run the command."
131
+ )
132
+ );
133
+ process.exit(2);
134
+ }
135
+ cached = result.env;
136
+ return cached;
137
+ }
138
+
139
+ // src/lib/config.ts
140
+ var CONFIG_DIR = join(homedir(), ".openskill");
141
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
142
+ var configSchema = z2.object({
143
+ token: z2.string().min(1).optional(),
144
+ email: z2.email().optional(),
145
+ baseUrl: z2.url().optional()
146
+ });
147
+ async function loadConfig() {
148
+ let raw;
149
+ try {
150
+ raw = await readFile(CONFIG_FILE, "utf-8");
151
+ } catch {
152
+ return {};
153
+ }
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse(raw);
157
+ } catch {
158
+ warnInvalidConfig("not valid JSON");
159
+ return {};
160
+ }
161
+ const result = configSchema.safeParse(parsed);
162
+ if (!result.success) {
163
+ const issue = result.error.issues[0];
164
+ warnInvalidConfig(
165
+ `${issue?.path.join(".") ?? "config"}: ${issue?.message ?? "invalid"}`
166
+ );
167
+ return {};
168
+ }
169
+ return result.data;
170
+ }
171
+ function warnInvalidConfig(reason) {
172
+ process.stderr.write(
173
+ `Warning: ignoring invalid ${CONFIG_FILE} (${reason}) \u2014 re-run \`osm login\` or fix the file.
174
+ `
175
+ );
176
+ }
177
+ var DEFAULT_API_BASE_URL = "https://staging.openskill.md/api";
178
+ function getApiBaseUrl(config) {
179
+ const raw = getEnv().OPENSKILL_API_URL || config.baseUrl || DEFAULT_API_BASE_URL;
180
+ return raw.replace(/\/+$/, "");
181
+ }
182
+
183
+ // src/lib/api.ts
184
+ var _apiBase;
185
+ async function getApiBase() {
186
+ if (_apiBase) return _apiBase;
187
+ const config = await loadConfig();
188
+ _apiBase = getApiBaseUrl(config);
189
+ return _apiBase;
190
+ }
191
+ async function resolveApiBase() {
192
+ return getApiBase();
193
+ }
194
+ async function resolveWebOrigin() {
195
+ return (await getApiBase()).replace(/\/api\/?$/, "");
196
+ }
197
+ var ApiError = class extends Error {
198
+ statusCode;
199
+ constructor(statusCode, message) {
200
+ super(message);
201
+ this.name = "ApiError";
202
+ this.statusCode = statusCode;
203
+ }
204
+ };
205
+ async function fetchApi(path6) {
206
+ const base = await getApiBase();
207
+ const url = `${base}${path6}`;
208
+ const res = await fetch(url);
209
+ if (!res.ok) {
210
+ throw new ApiError(res.status, `API returned ${res.status} for ${path6}`);
211
+ }
212
+ return res.json();
213
+ }
214
+ async function searchApi(query) {
215
+ return fetchApi(`/search?q=${encodeURIComponent(query)}`);
216
+ }
217
+ async function getSkill(slug) {
218
+ return fetchApi(`/skills/${encodeURIComponent(slug)}`);
219
+ }
220
+ async function getBlueprint(slug) {
221
+ return fetchApi(`/blueprints/${encodeURIComponent(slug)}`);
222
+ }
223
+ async function getCollections(params) {
224
+ const qs = new URLSearchParams();
225
+ if (params?.search) qs.set("search", params.search);
226
+ qs.set("limit", String(params?.limit ?? 50));
227
+ const res = await fetchApi(
228
+ `/collections?${qs.toString()}`
229
+ );
230
+ return res.data;
231
+ }
232
+ async function getCollection(slug) {
233
+ return fetchApi(`/collections/${encodeURIComponent(slug)}`);
234
+ }
235
+ async function listMcpServers(params) {
236
+ const qs = new URLSearchParams();
237
+ if (params?.search) qs.set("search", params.search);
238
+ if (params?.category) qs.set("category", params.category);
239
+ qs.set("limit", String(params?.limit ?? 50));
240
+ qs.set("sort", "stars");
241
+ qs.set("order", "desc");
242
+ return fetchApi(`/mcp-servers?${qs.toString()}`);
243
+ }
244
+ async function getMcpCategories() {
245
+ return fetchApi("/mcp-servers/categories");
246
+ }
247
+ async function getMcpServer(slug) {
248
+ return fetchApi(`/mcp-servers/${encodeURIComponent(slug)}`);
249
+ }
250
+
92
251
  // src/commands/add.ts
93
252
  var require2 = createRequire(import.meta.url);
94
253
  function stripSkillsBanner(chunk) {
@@ -103,17 +262,64 @@ function validateSource(source) {
103
262
  };
104
263
  }
105
264
  if (!/[/@:.]/.test(source)) {
265
+ return { ok: false, code: "needs-resolution", slug: source };
266
+ }
267
+ return { ok: true };
268
+ }
269
+ async function resolveSlug(slug) {
270
+ let skill;
271
+ try {
272
+ skill = await getSkill(slug);
273
+ } catch (err) {
274
+ if (err instanceof ApiError && err.statusCode === 404) {
275
+ return {
276
+ ok: false,
277
+ message: `No skill named "${slug}" in the registry.`,
278
+ suggestion: "Run `osm search <query>` to find it, or pass owner/repo directly."
279
+ };
280
+ }
106
281
  return {
107
282
  ok: false,
108
- code: 2,
109
- message: `"${source}" looks like an openskill slug, not a source identifier.`,
110
- suggestion: "Use owner/repo@skill (e.g. anthropics/skills@frontend-design) or run `osm search <query>` to find it."
283
+ message: `Couldn't reach the registry to resolve "${slug}".`,
284
+ suggestion: "Check your connection (and OPENSKILL_API_URL, if set), or pass owner/repo directly."
111
285
  };
112
286
  }
113
- return { ok: true };
287
+ const skillName = skill.name || null;
288
+ if (skill.githubOwner && skill.githubRepo) {
289
+ const source = `${skill.githubOwner}/${skill.githubRepo}`;
290
+ return {
291
+ ok: true,
292
+ source,
293
+ skillName,
294
+ note: skillName ? `resolved ${slug} \u2192 ${source} (skill: ${skillName})` : `resolved ${slug} \u2192 ${source}`
295
+ };
296
+ }
297
+ if (skill.sourceUrl && /^https?:\/\//.test(skill.sourceUrl)) {
298
+ return {
299
+ ok: true,
300
+ source: skill.sourceUrl,
301
+ skillName,
302
+ note: skillName ? `resolved ${slug} \u2192 ${skill.sourceUrl} (skill: ${skillName})` : `resolved ${slug} \u2192 ${skill.sourceUrl}`
303
+ };
304
+ }
305
+ return {
306
+ ok: false,
307
+ message: `"${slug}" lives only in the OpenSkill registry \u2014 it has no GitHub source to install from yet.`,
308
+ suggestion: `Run \`osm info ${slug}\` to inspect it.`
309
+ };
310
+ }
311
+ function toSkillSlug(name) {
312
+ return name.toLowerCase().replace(/[\s_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
114
313
  }
115
- function buildSkillsArgs(source, opts = {}) {
314
+ function buildSkillsArgs(source, opts = {}, skillFilter = null) {
116
315
  const args = ["add", source, "-y"];
316
+ if (skillFilter) {
317
+ args.push("--skill", skillFilter);
318
+ const kebab = toSkillSlug(skillFilter);
319
+ if (kebab && kebab !== skillFilter.toLowerCase()) {
320
+ args.push("--skill", kebab);
321
+ }
322
+ }
117
323
  for (const a of opts.agent ?? []) {
118
324
  args.push("-a", a);
119
325
  }
@@ -121,11 +327,12 @@ function buildSkillsArgs(source, opts = {}) {
121
327
  if (opts.copy) args.push("--copy");
122
328
  return args;
123
329
  }
124
- function buildJsonResult(source, opts, code, capturedStderr) {
330
+ function buildJsonResult(source, opts, code, capturedStderr, resolvedFrom = null) {
125
331
  if (code === 0) {
126
332
  return {
127
333
  ok: true,
128
334
  source,
335
+ resolvedFrom,
129
336
  agent: opts.agent ?? null,
130
337
  scope: opts.global ? "user" : "project"
131
338
  };
@@ -133,6 +340,7 @@ function buildJsonResult(source, opts, code, capturedStderr) {
133
340
  return {
134
341
  ok: false,
135
342
  source,
343
+ resolvedFrom,
136
344
  error: {
137
345
  code: `INSTALL_EXIT_${code}`,
138
346
  message: capturedStderr.trim() || "install failed"
@@ -140,10 +348,40 @@ function buildJsonResult(source, opts, code, capturedStderr) {
140
348
  };
141
349
  }
142
350
  async function addCommand(source, opts = {}) {
351
+ let installSource = source;
352
+ let resolvedFrom = null;
353
+ let skillFilter = null;
143
354
  const validation = validateSource(source);
144
355
  if (!validation.ok) {
145
- console.error(errorMessage(validation.message, validation.suggestion));
146
- process.exit(validation.code);
356
+ if (validation.code !== "needs-resolution") {
357
+ console.error(errorMessage(validation.message, validation.suggestion));
358
+ process.exit(validation.code);
359
+ }
360
+ const spinner = opts.json ? null : ora({
361
+ text: `Resolving "${validation.slug}" in the registry...`,
362
+ color: "magenta"
363
+ }).start();
364
+ const resolved = await resolveSlug(validation.slug);
365
+ spinner?.stop();
366
+ if (!resolved.ok) {
367
+ if (opts.json) {
368
+ jsonResult({
369
+ ok: false,
370
+ source,
371
+ resolvedFrom: null,
372
+ error: { code: "SLUG_RESOLUTION_FAILED", message: resolved.message }
373
+ });
374
+ } else {
375
+ console.error(errorMessage(resolved.message, resolved.suggestion));
376
+ }
377
+ process.exit(2);
378
+ }
379
+ if (!opts.json) {
380
+ console.error(chalk3.gray(` \u2192 ${resolved.note}`));
381
+ }
382
+ installSource = resolved.source;
383
+ resolvedFrom = validation.slug;
384
+ skillFilter = resolved.skillName;
147
385
  }
148
386
  let binPath;
149
387
  try {
@@ -157,13 +395,16 @@ async function addCommand(source, opts = {}) {
157
395
  );
158
396
  process.exit(3);
159
397
  }
160
- const args = buildSkillsArgs(source, opts);
398
+ const args = buildSkillsArgs(installSource, opts, skillFilter);
161
399
  if (!opts.json) {
162
400
  console.error(chalk3.gray(" \u2192 fetching skill files\u2026"));
163
401
  console.error("");
164
402
  }
165
403
  const child = spawn(process.execPath, [binPath, ...args], {
166
404
  stdio: ["inherit", "pipe", "pipe"],
405
+ // env.ts carve-out: this is a *write* into the child installer's env
406
+ // (opting it out of telemetry), not a read of our own config — so it
407
+ // doesn't go through getEnv().
167
408
  env: { ...process.env, DISABLE_TELEMETRY: "1" }
168
409
  });
169
410
  let capturedStderr = "";
@@ -189,7 +430,9 @@ async function addCommand(source, opts = {}) {
189
430
  child.once("error", () => resolve(3));
190
431
  });
191
432
  if (opts.json) {
192
- jsonResult(buildJsonResult(source, opts, code, capturedStderr));
433
+ jsonResult(
434
+ buildJsonResult(installSource, opts, code, capturedStderr, resolvedFrom)
435
+ );
193
436
  }
194
437
  process.exit(code);
195
438
  }
@@ -197,97 +440,7 @@ async function addCommand(source, opts = {}) {
197
440
  // src/commands/browse.ts
198
441
  import chalk4 from "chalk";
199
442
  import inquirer from "inquirer";
200
- import ora from "ora";
201
-
202
- // src/lib/config.ts
203
- import { readFile, writeFile, mkdir, chmod } from "fs/promises";
204
- import { join } from "path";
205
- import { homedir } from "os";
206
- var CONFIG_DIR = join(homedir(), ".openskill");
207
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
208
- async function loadConfig() {
209
- try {
210
- const raw = await readFile(CONFIG_FILE, "utf-8");
211
- return JSON.parse(raw);
212
- } catch {
213
- return {};
214
- }
215
- }
216
- var DEFAULT_API_BASE_URL = "https://staging.openskill.md/api";
217
- function getApiBaseUrl(config) {
218
- const raw = process.env.OPENSKILL_API_URL || config.baseUrl || DEFAULT_API_BASE_URL;
219
- return raw.replace(/\/+$/, "");
220
- }
221
-
222
- // src/lib/api.ts
223
- var _apiBase;
224
- async function getApiBase() {
225
- if (_apiBase) return _apiBase;
226
- const config = await loadConfig();
227
- _apiBase = getApiBaseUrl(config);
228
- return _apiBase;
229
- }
230
- async function resolveApiBase() {
231
- return getApiBase();
232
- }
233
- async function resolveWebOrigin() {
234
- return (await getApiBase()).replace(/\/api\/?$/, "");
235
- }
236
- var ApiError = class extends Error {
237
- statusCode;
238
- constructor(statusCode, message) {
239
- super(message);
240
- this.name = "ApiError";
241
- this.statusCode = statusCode;
242
- }
243
- };
244
- async function fetchApi(path6) {
245
- const base = await getApiBase();
246
- const url = `${base}${path6}`;
247
- const res = await fetch(url);
248
- if (!res.ok) {
249
- throw new ApiError(res.status, `API returned ${res.status} for ${path6}`);
250
- }
251
- return res.json();
252
- }
253
- async function searchApi(query) {
254
- return fetchApi(`/search?q=${encodeURIComponent(query)}`);
255
- }
256
- async function getSkill(slug) {
257
- return fetchApi(`/skills/${encodeURIComponent(slug)}`);
258
- }
259
- async function getBlueprint(slug) {
260
- return fetchApi(`/blueprints/${encodeURIComponent(slug)}`);
261
- }
262
- async function getCollections(params) {
263
- const qs = new URLSearchParams();
264
- if (params?.search) qs.set("search", params.search);
265
- qs.set("limit", String(params?.limit ?? 50));
266
- const res = await fetchApi(
267
- `/collections?${qs.toString()}`
268
- );
269
- return res.data;
270
- }
271
- async function getCollection(slug) {
272
- return fetchApi(`/collections/${encodeURIComponent(slug)}`);
273
- }
274
- async function listMcpServers(params) {
275
- const qs = new URLSearchParams();
276
- if (params?.search) qs.set("search", params.search);
277
- if (params?.category) qs.set("category", params.category);
278
- qs.set("limit", String(params?.limit ?? 50));
279
- qs.set("sort", "stars");
280
- qs.set("order", "desc");
281
- return fetchApi(`/mcp-servers?${qs.toString()}`);
282
- }
283
- async function getMcpCategories() {
284
- return fetchApi("/mcp-servers/categories");
285
- }
286
- async function getMcpServer(slug) {
287
- return fetchApi(`/mcp-servers/${encodeURIComponent(slug)}`);
288
- }
289
-
290
- // src/commands/browse.ts
443
+ import ora2 from "ora";
291
444
  var EXIT_SENTINEL = "__exit__";
292
445
  async function browseCommand() {
293
446
  try {
@@ -316,7 +469,7 @@ async function browseCommand() {
316
469
  }
317
470
  }
318
471
  async function browseCollections() {
319
- const spinner = ora({ text: "Loading collections...", color: "magenta" }).start();
472
+ const spinner = ora2({ text: "Loading collections...", color: "magenta" }).start();
320
473
  const collections = await getCollections({ limit: 50 });
321
474
  spinner.stop();
322
475
  if (collections.length === 0) {
@@ -338,7 +491,7 @@ async function browseCollections() {
338
491
  }
339
492
  ]);
340
493
  if (slug === EXIT_SENTINEL) return;
341
- const detailSpinner = ora({ text: "Loading collection...", color: "magenta" }).start();
494
+ const detailSpinner = ora2({ text: "Loading collection...", color: "magenta" }).start();
342
495
  const collection = await getCollection(slug);
343
496
  detailSpinner.stop();
344
497
  renderCollection(collection);
@@ -416,7 +569,7 @@ function renderCollection(collection) {
416
569
  );
417
570
  }
418
571
  async function browseMcpServers() {
419
- const spinner = ora({ text: "Loading MCP categories...", color: "magenta" }).start();
572
+ const spinner = ora2({ text: "Loading MCP categories...", color: "magenta" }).start();
420
573
  const categories = await getMcpCategories();
421
574
  spinner.stop();
422
575
  if (categories.length === 0) {
@@ -438,7 +591,7 @@ async function browseMcpServers() {
438
591
  }
439
592
  ]);
440
593
  if (category === EXIT_SENTINEL) return;
441
- const listSpinner = ora({ text: "Loading MCP servers...", color: "magenta" }).start();
594
+ const listSpinner = ora2({ text: "Loading MCP servers...", color: "magenta" }).start();
442
595
  const { data: servers } = await listMcpServers({ category, limit: 50 });
443
596
  listSpinner.stop();
444
597
  process.stderr.write(sectionHeader(`MCP Servers \xB7 ${category}`) + "\n");
@@ -467,9 +620,9 @@ async function browseMcpServers() {
467
620
 
468
621
  // src/commands/info.ts
469
622
  import chalk5 from "chalk";
470
- import ora2 from "ora";
623
+ import ora3 from "ora";
471
624
  async function infoCommand(slug) {
472
- const spinner = ora2({ text: `Fetching info for "${slug}"...`, color: "magenta" }).start();
625
+ const spinner = ora3({ text: `Fetching info for "${slug}"...`, color: "magenta" }).start();
473
626
  try {
474
627
  const skill = await getSkill(slug);
475
628
  spinner.stop();
@@ -633,7 +786,9 @@ import path from "path";
633
786
  var SAFE_SLUG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
634
787
  function sanitizeSlug(slug) {
635
788
  if (!SAFE_SLUG_PATTERN.test(slug)) {
636
- throw new Error(`Invalid slug "${slug}". Slugs must contain only lowercase letters, numbers, hyphens, dots, and underscores.`);
789
+ throw new Error(
790
+ `Invalid slug "${slug}". Slugs must contain only lowercase letters, numbers, hyphens, dots, and underscores.`
791
+ );
637
792
  }
638
793
  if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
639
794
  throw new Error(`Invalid slug "${slug}". Path separators are not allowed.`);
@@ -989,9 +1144,9 @@ async function removeCommand(slug) {
989
1144
  import fs4 from "fs";
990
1145
  import path5 from "path";
991
1146
  import chalk10 from "chalk";
992
- import ora3 from "ora";
1147
+ import ora4 from "ora";
993
1148
  async function routerInstallCommand() {
994
- const spinner = ora3({ text: "Downloading OpenSkill router...", color: "magenta" }).start();
1149
+ const spinner = ora4({ text: "Downloading OpenSkill router...", color: "magenta" }).start();
995
1150
  try {
996
1151
  const origin = await resolveWebOrigin();
997
1152
  const res = await fetch(`${origin}/skills/router/SKILL.md`);
@@ -1186,9 +1341,9 @@ async function scoreCommand(filepath) {
1186
1341
 
1187
1342
  // src/commands/search.ts
1188
1343
  import chalk12 from "chalk";
1189
- import ora4 from "ora";
1344
+ import ora5 from "ora";
1190
1345
  async function searchCommand(query) {
1191
- const spinner = ora4({ text: `Searching for "${query}"...`, color: "magenta" }).start();
1346
+ const spinner = ora5({ text: `Searching for "${query}"...`, color: "magenta" }).start();
1192
1347
  try {
1193
1348
  const results = await searchApi(query);
1194
1349
  spinner.stop();
@@ -1273,7 +1428,7 @@ async function searchCommand(query) {
1273
1428
  }
1274
1429
 
1275
1430
  // src/program.ts
1276
- var VERSION = "0.2.0";
1431
+ var VERSION = "0.3.0";
1277
1432
  var BrandedCommand = class _BrandedCommand extends Command {
1278
1433
  createCommand(name) {
1279
1434
  return new _BrandedCommand(name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openskillmd",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "The OpenSkill CLI (osm) — search, add, score, and explore AI agent skills from your terminal",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,7 +54,8 @@
54
54
  "inquirer": "^12.6.0",
55
55
  "open": "^10.2.0",
56
56
  "ora": "^8.2.0",
57
- "skills": "^1.4.6"
57
+ "skills": "^1.4.6",
58
+ "zod": "^4.4.3"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@biomejs/biome": "^2.0.0",