skillrepo 4.8.1 → 4.8.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillrepo",
3
- "version": "4.8.1",
3
+ "version": "4.8.2",
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": {
@@ -27,7 +27,8 @@
27
27
  * - 404 not-found → clean error, exit 5
28
28
  * - 409 self-ownership → clean error, exit 5 ("you can't add your
29
29
  * own skill to your own library")
30
- * - 403 plan-limit → validationError (via http.mjs) with billing hint
30
+ * - 403 plan-limit → validationError (via http.mjs); the server
31
+ * message carries the per-cap remedy (no CLI-side hint, #2239)
31
32
  * - 403 scope → scopeError (via http.mjs) with write-key hint
32
33
  *
33
34
  * Flags: --global / --agent / --json / --key / --url
package/src/lib/http.mjs CHANGED
@@ -339,7 +339,7 @@ async function mapErrorResponse(res, url) {
339
339
  // `src/app/api/v1/library/route.ts:226-235`. The route comment
340
340
  // EXPLICITLY documents that the CLI must distinguish this from
341
341
  // scope errors. We map it to `validationError` (exit 5) with a
342
- // hint pointing at billing — it is not a key-permission issue
342
+ // no CLI-side hint — it is not a key-permission issue
343
343
  // so neither authError nor scopeError is the right fit, and
344
344
  // adding a dedicated planLimitError to the exit code matrix is
345
345
  // out of scope for v3.0.0.
@@ -350,9 +350,13 @@ async function mapErrorResponse(res, url) {
350
350
  // 3. Generic 403 — suspended account, blocked user, etc. Maps to
351
351
  // authError with a "contact support" hint.
352
352
  if (typeof code === "string" && code === "plan_limit") {
353
- return validationError(message, {
354
- hint: "Upgrade your plan at /app/settings/billing or remove an existing skill from your library.",
355
- });
353
+ // No CLI-side hint: the server's plan_limit message now states the
354
+ // limit AND the correct remedy for the specific cap that fired
355
+ // (skills "Delete one to add another.", library "Remove one to add
356
+ // another.", cap-0 "A personal library isn't included in your
357
+ // plan."). A generic hint here either duplicates it or — for the
358
+ // cap-0 tier — contradicts it (#2239 round-2 review).
359
+ return validationError(message);
356
360
  }
357
361
  if (typeof code === "string" && code.toLowerCase().includes("scope")) {
358
362
  return scopeError(message, {
@@ -1233,7 +1237,7 @@ async function setSkillVisibility(serverUrl, apiKey, owner, name, target, option
1233
1237
  // CANNOT fall through to `mapErrorResponse` (which re-reads the
1234
1238
  // body and would get an empty object — a `plan_limit` 403 would
1235
1239
  // mis-classify as authError exit 2 instead of validationError
1236
- // exit 5 with billing hint). Pre-parse once, then dispatch every
1240
+ // exit 5, message self-contained). Pre-parse once, then dispatch every
1237
1241
  // code path inline mirroring `mapErrorResponse`'s shape so the
1238
1242
  // CliError types stay consistent across endpoints.
1239
1243
  if (res.status === 403 || res.status === 422) {
@@ -1253,13 +1257,13 @@ async function setSkillVisibility(serverUrl, apiKey, owner, name, target, option
1253
1257
  }
1254
1258
  // Mirror `mapErrorResponse`'s 403 taxonomy so a 403 here behaves
1255
1259
  // identically to a 403 on any other endpoint:
1256
- // - `plan_limit` → validationError (exit 5) with billing hint
1260
+ // - `plan_limit` → validationError (exit 5), server message only
1257
1261
  // - any `*scope*` code → scopeError (exit 4)
1258
1262
  // - everything else → authError (exit 2, "suspended" hint)
1259
1263
  if (code === "plan_limit") {
1260
- throw validationError(message, {
1261
- hint: "Upgrade your plan at /app/settings/billing or remove an existing skill from your library.",
1262
- });
1264
+ // Same as mapErrorResponse: the server message is the complete,
1265
+ // cap-specific remedy no CLI hint (#2239 round-2 review).
1266
+ throw validationError(message);
1263
1267
  }
1264
1268
  if (code && code.toLowerCase().includes("scope")) {
1265
1269
  throw scopeError(message, {
@@ -235,7 +235,7 @@ describe("runAdd — error paths", () => {
235
235
  );
236
236
  });
237
237
 
238
- it("403 plan-limit maps to validationError with billing hint", async () => {
238
+ it("403 plan-limit maps to validationError (server message self-contained, no hint)", async () => {
239
239
  server.setAddResponse("alice", "limit-test", {
240
240
  status: 403,
241
241
  body: {
@@ -192,11 +192,11 @@ describe("runPublish — error paths", () => {
192
192
  });
193
193
  });
194
194
 
195
- it("403 plan_limit → CliError EXIT_VALIDATION (NOT authError), billing hint surfaces", async () => {
195
+ it("403 plan_limit → CliError EXIT_VALIDATION (NOT authError), no CLI-side hint", async () => {
196
196
  // Real-world regression scenario: an account over its skill
197
197
  // quota tries to publish. Server returns 403 with code
198
198
  // `plan_limit`. The CLI MUST classify this as exit 5
199
- // validation with the billing hint, NOT exit 2 authError
199
+ // validation with the self-contained server message, NOT exit 2 authError
200
200
  // ("contact support") — a pre-review version of the code
201
201
  // consumed the 403 body and let mapErrorResponse re-parse, but
202
202
  // the body was empty by then so plan_limit fell through to
@@ -211,7 +211,7 @@ describe("runPublish — error paths", () => {
211
211
  await assert.rejects(runPublish(baseArgv(), { stdout }), (err) => {
212
212
  assert.ok(err instanceof CliError);
213
213
  assert.equal(err.exitCode, EXIT_VALIDATION);
214
- assert.match(err.hint ?? "", /Upgrade your plan|billing/);
214
+ assert.equal(err.hint, undefined); // server plan_limit message is self-contained (#2239)
215
215
  return true;
216
216
  });
217
217
  });
@@ -514,10 +514,10 @@ describe("runUnpublish — error paths", () => {
514
514
  );
515
515
  });
516
516
 
517
- it("403 plan_limit → CliError EXIT_VALIDATION (NOT authError), billing hint surfaces", async () => {
517
+ it("403 plan_limit → CliError EXIT_VALIDATION (NOT authError), no CLI-side hint", async () => {
518
518
  // Same shared `setSkillVisibility` 403 dispatch as publish: a
519
519
  // plan_limit 403 must classify as exit 5 validation with the
520
- // billing hint, never exit 2 authError. Mirrors the publish-side
520
+ // self-contained server message, never exit 2 authError. Mirrors the publish-side
521
521
  // regression guard for the unpublish verb.
522
522
  server.setUnpublishResponse("alice", "my-skill", {
523
523
  status: 403,
@@ -529,7 +529,7 @@ describe("runUnpublish — error paths", () => {
529
529
  await assert.rejects(runUnpublish(baseArgv(), { stdout }), (err) => {
530
530
  assert.ok(err instanceof CliError);
531
531
  assert.equal(err.exitCode, EXIT_VALIDATION);
532
- assert.match(err.hint ?? "", /Upgrade your plan|billing/);
532
+ assert.equal(err.hint, undefined); // server plan_limit message is self-contained (#2239)
533
533
  return true;
534
534
  });
535
535
  });
@@ -401,7 +401,7 @@ describe("runPush — server error mapping", () => {
401
401
 
402
402
  it("maps server 403 plan_limit to EXIT_VALIDATION (NOT auth)", async () => {
403
403
  // mapErrorResponse explicitly disambiguates plan_limit from the
404
- // generic-403 authError path so the user sees a billing hint, not
404
+ // generic-403 authError path so the user sees the plan-limit message, not
405
405
  // a "contact support" hint. Mirrors the http.test.mjs assertion.
406
406
  file("SKILL.md", VALID_SKILL_MD);
407
407
  server.setPushResponse({
@@ -363,7 +363,7 @@ describe("CLI E2E — read commands", () => {
363
363
  assert.match(r.stderr, /write-scoped key|scope/);
364
364
  });
365
365
 
366
- it("add with 403 plan_limit exits 5 with billing hint", async () => {
366
+ it("add with 403 plan_limit exits 5 (validation, server message shown)", async () => {
367
367
  server.setAddResponseForAny({
368
368
  status: 403,
369
369
  body: { error: "Your free plan allows up to 5 library skills.", code: "plan_limit" },
@@ -1278,7 +1278,7 @@ describe("pushSkill", () => {
1278
1278
  it("maps 403 plan_limit through mapErrorResponse to validationError", async () => {
1279
1279
  const srv = await makePushServer((req, res) => {
1280
1280
  jsonRes(res, 403, {
1281
- error: "Plan limit reached",
1281
+ error: "Your developer plan allows up to 40 skills. Delete one to add another.",
1282
1282
  code: "plan_limit",
1283
1283
  });
1284
1284
  });
@@ -2119,9 +2119,9 @@ describe("publishLibrarySkill", () => {
2119
2119
  }
2120
2120
  });
2121
2121
 
2122
- it("throws validationError on 403 plan_limit (billing hint, not auth)", async () => {
2122
+ it("throws validationError on 403 plan_limit (validation exit, not auth)", async () => {
2123
2123
  const srv = await startServer((req, res) =>
2124
- jsonRes(res, 403, { error: "Plan limit reached", code: "plan_limit" }),
2124
+ jsonRes(res, 403, { error: "Your developer plan allows up to 40 skills. Delete one to add another.", code: "plan_limit" }),
2125
2125
  );
2126
2126
  try {
2127
2127
  await assert.rejects(