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 +6 -5
- package/package.json +1 -1
- package/src/lib/http.mjs +19 -0
- package/src/test/commands/publish.test.mjs +2 -2
- package/src/test/commands/push.test.mjs +24 -0
- package/src/test/lib/http.test.mjs +87 -0
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": "
|
|
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:
|
|
283
|
-
|
|
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
|
|
322
|
-
auto-generated
|
|
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
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
|
|
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
|
-
/
|
|
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, {
|