skillrepo 4.8.2 → 4.8.4

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/README.md CHANGED
@@ -92,7 +92,7 @@ and just write the config + gitignore.
92
92
  ```json
93
93
  {
94
94
  "action": "initialized",
95
- "account": { "slug": "...", "id": "...", "tier": "free|pro|enterprise" },
95
+ "account": { "slug": "...", "id": "...", "tier": "publisher|developer|team" },
96
96
  "config": { "action": "created|updated|unchanged" },
97
97
  "vendors": ["claudeCode", "cursor", "..."],
98
98
  "mcp": {
@@ -279,8 +279,9 @@ from the server to your local library.
279
279
 
280
280
  Limits: total multipart body ≤ 4.5 MB, per-file path depth ≤ 5
281
281
  segments, executable/archive extensions blocked (full list in
282
- `src/lib/skills/constants.ts`). Anti-abuse rate limit: 5/min and
283
- 30/hr on Publisher; 30/min and 500/hr on Team.
282
+ `src/lib/skills/constants.ts`). Anti-abuse rate limit: 6/min and
283
+ 100/hr on Developer; 30/min and 500/hr on Team. (`push` needs a
284
+ write-scoped access key, which starts on the Developer plan.)
284
285
 
285
286
  Flags:
286
287
 
@@ -318,8 +319,8 @@ the CLI exits `4 / scope` with `code: publish_not_permitted`. The same
318
319
 
319
320
  Publish-only preconditions that surface as exit `5 / validation`:
320
321
 
321
- - `namespace_unset` — the account's name still equals its
322
- auto-generated slug; customize the namespace first.
322
+ - `namespace_unset` — the account's handle is still the
323
+ auto-generated placeholder; choose an Author ID in Settings first.
323
324
  - `analysis_pending` — safety analysis hasn't completed (only fires
324
325
  where analysis is enabled).
325
326
  - `safety_grade_too_low` — the skill's safety grade is `F`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "4.8.2",
3
+ "version": "4.8.4",
4
4
  "description": "Pull-based CLI for agent skills — init, sync, search, add, remove your library from any IDE",
5
5
  "type": "module",
6
6
  "bin": {
package/src/lib/http.mjs CHANGED
@@ -370,6 +370,25 @@ async function mapErrorResponse(res, url) {
370
370
  if (res.status === 404) {
371
371
  return null; // Caller decides
372
372
  }
373
+ if (res.status === 422 && code === "handle_required") {
374
+ // #2310: the push would CREATE a skill while the account's Author ID
375
+ // is still an unchosen placeholder. Line 1 is the server's message
376
+ // ("Set your Author ID before adding skills."); the hint points at
377
+ // the Settings surface that unblocks it. Derive the base URL from
378
+ // the request URL's origin (same posture as the 401 auth hint above)
379
+ // so staging users get a staging link, with the prod host as the
380
+ // parse-failure fallback.
381
+ let settingsBase = "https://skillrepo.dev";
382
+ try {
383
+ settingsBase = new URL(url).origin;
384
+ } catch {
385
+ // URL parse failure shouldn't happen — `safeFetch` already
386
+ // validated. Fall back to the prod default.
387
+ }
388
+ return validationError(message, {
389
+ hint: `Set it in Settings → Account Profile: ${settingsBase}/app/settings`,
390
+ });
391
+ }
373
392
  if (res.status === 429) {
374
393
  // 429 hits mapErrorResponse in two cases: (1) a read-only
375
394
  // endpoint's retry budget was exhausted (safeFetch with
@@ -220,7 +220,7 @@ describe("runPublish — error paths", () => {
220
220
  server.setPublishResponse("alice", "my-skill", {
221
221
  status: 422,
222
222
  body: {
223
- error: "Set up your account namespace before publishing.",
223
+ error: "Set your Author ID before publishing.",
224
224
  code: "namespace_unset",
225
225
  },
226
226
  });
@@ -229,7 +229,7 @@ describe("runPublish — error paths", () => {
229
229
  (err) =>
230
230
  err instanceof CliError &&
231
231
  err.exitCode === EXIT_VALIDATION &&
232
- /account namespace/.test(err.message),
232
+ /Author ID/.test(err.message),
233
233
  );
234
234
  });
235
235
 
@@ -453,6 +453,30 @@ describe("runPush — server error mapping", () => {
453
453
  );
454
454
  });
455
455
 
456
+ it("maps server 422 handle_required to EXIT_VALIDATION with the Settings hint (#2310, #2345)", async () => {
457
+ // The dedicated #2310 branch in mapErrorResponse: friendly message
458
+ // plus the Settings pointer. Previously pinned only at the
459
+ // http.test.mjs layer against hand-built JSON — this runs the whole
460
+ // runPush chain against the same wire shape the server emits
461
+ // (route.ts:724: `{ error, code: "handle_required" }`).
462
+ file("SKILL.md", VALID_SKILL_MD);
463
+ server.setPushResponse({
464
+ status: 422,
465
+ body: {
466
+ error: "Set your Author ID before adding skills.",
467
+ code: "handle_required",
468
+ },
469
+ });
470
+ await assert.rejects(
471
+ runPush(["--key", VALID_KEY, "--url", serverUrl, "skill"], { stdout }),
472
+ (err) =>
473
+ err instanceof CliError &&
474
+ err.exitCode === EXIT_VALIDATION &&
475
+ /author id/i.test(err.message) &&
476
+ /settings/i.test(err.hint ?? ""),
477
+ );
478
+ });
479
+
456
480
  it("maps server 413 payload_too_large to EXIT_VALIDATION (generic 4xx)", async () => {
457
481
  // 413 has NO explicit branch in mapErrorResponse (only 401/403/404/
458
482
  // 429/5xx are special-cased). It therefore falls through to the
@@ -1296,6 +1296,93 @@ describe("pushSkill", () => {
1296
1296
  }
1297
1297
  });
1298
1298
 
1299
+ it("maps 422 handle_required to validationError with a Settings hint on the server's own host (#2310)", async () => {
1300
+ const SERVER_MESSAGE = "Set your Author ID before adding skills.";
1301
+ const srv = await makePushServer((req, res) => {
1302
+ jsonRes(res, 422, {
1303
+ error: SERVER_MESSAGE,
1304
+ code: "handle_required",
1305
+ });
1306
+ });
1307
+ try {
1308
+ await assert.rejects(
1309
+ () =>
1310
+ pushSkill(srv.url, VALID_KEY, {
1311
+ files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
1312
+ }),
1313
+ (err) =>
1314
+ err instanceof CliError &&
1315
+ err.exitCode === EXIT_VALIDATION &&
1316
+ // Line 1: the server's message verbatim; line 2 (hint): the
1317
+ // Settings path on the REQUEST host (staging users get a
1318
+ // staging link), not hardcoded prod.
1319
+ err.message === SERVER_MESSAGE &&
1320
+ err.hint === `Set it in Settings → Account Profile: ${srv.url}/app/settings`,
1321
+ );
1322
+ } finally {
1323
+ await srv.close();
1324
+ }
1325
+ });
1326
+
1327
+ it("handle_required hint falls back to the prod host when the request URL is unparseable (#2310)", async () => {
1328
+ // The `new URL(url)` origin-derivation inside the handle_required
1329
+ // mapping can only throw if the URL that reached fetch was itself
1330
+ // unparseable — impossible over a real socket (undici validates the
1331
+ // URL before connecting), so stub fetch to hand back the 422
1332
+ // directly. `normalizeUrl` passes any non-empty string through, so
1333
+ // the raw serverUrl below reaches mapErrorResponse verbatim.
1334
+ const SERVER_MESSAGE = "Set your Author ID before adding skills.";
1335
+ const realFetch = globalThis.fetch;
1336
+ globalThis.fetch = async () =>
1337
+ new Response(
1338
+ JSON.stringify({ error: SERVER_MESSAGE, code: "handle_required" }),
1339
+ { status: 422, headers: { "Content-Type": "application/json" } },
1340
+ );
1341
+ try {
1342
+ await assert.rejects(
1343
+ () =>
1344
+ pushSkill("not a url", VALID_KEY, {
1345
+ files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
1346
+ }),
1347
+ (err) =>
1348
+ err instanceof CliError &&
1349
+ err.exitCode === EXIT_VALIDATION &&
1350
+ err.message === SERVER_MESSAGE &&
1351
+ // Parse-failure fallback: the hint pins the prod host instead
1352
+ // of the (unusable) request origin.
1353
+ err.hint ===
1354
+ "Set it in Settings → Account Profile: https://skillrepo.dev/app/settings",
1355
+ );
1356
+ } finally {
1357
+ globalThis.fetch = realFetch;
1358
+ }
1359
+ });
1360
+
1361
+ it("does NOT special-case a 422 without the handle_required code (generic validation)", async () => {
1362
+ // A 422 carrying a different code (e.g. idempotency_key_reused) must
1363
+ // keep the pre-#2310 generic mapping: validationError, no Settings hint.
1364
+ const srv = await makePushServer((req, res) => {
1365
+ jsonRes(res, 422, {
1366
+ error: "Idempotency-Key was reused with a different request body.",
1367
+ code: "idempotency_key_reused",
1368
+ });
1369
+ });
1370
+ try {
1371
+ await assert.rejects(
1372
+ () =>
1373
+ pushSkill(srv.url, VALID_KEY, {
1374
+ files: [{ relativePath: "SKILL.md", content: SKILL_MD }],
1375
+ }),
1376
+ (err) =>
1377
+ err instanceof CliError &&
1378
+ err.exitCode === EXIT_VALIDATION &&
1379
+ err.hint === undefined,
1380
+ );
1381
+ } finally {
1382
+ await srv.close();
1383
+ }
1384
+ });
1385
+
1299
1386
  it("maps 413 payload_too_large through mapErrorResponse", async () => {
1300
1387
  const srv = await makePushServer((req, res) => {
1301
1388
  jsonRes(res, 413, {