@botdocs/cli 0.12.1 → 0.12.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/dist/commands/edit.js +3 -1
- package/dist/commands/install.js +6 -55
- package/dist/commands/sync.js +2 -2
- package/dist/commands/team.js +1 -1
- package/dist/index.js +0 -7
- package/dist/lib/api.d.ts +16 -1
- package/dist/lib/api.js +31 -6
- package/package.json +1 -1
- package/dist/commands/search.d.ts +0 -3
- package/dist/commands/search.js +0 -58
package/dist/commands/edit.js
CHANGED
|
@@ -44,7 +44,9 @@ export async function edit(rawRef, options) {
|
|
|
44
44
|
let manifest;
|
|
45
45
|
const refStr = `@${ref.username}/${ref.slug}`;
|
|
46
46
|
try {
|
|
47
|
-
manifest = await apiFetch(`/api/skills/${ref.username}/${ref.slug}/manifest
|
|
47
|
+
manifest = await apiFetch(`/api/skills/${ref.username}/${ref.slug}/manifest`, {
|
|
48
|
+
auth: 'optional',
|
|
49
|
+
});
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
50
52
|
if (err instanceof ApiError && err.status === 404) {
|
package/dist/commands/install.js
CHANGED
|
@@ -382,51 +382,6 @@ function isInstallUpToDate(prior, manifest) {
|
|
|
382
382
|
}
|
|
383
383
|
return true;
|
|
384
384
|
}
|
|
385
|
-
/** Compute the Levenshtein edit distance between two short strings. Used by
|
|
386
|
-
* the "did you mean?" suggester to filter search hits that are too far from
|
|
387
|
-
* the user's typo to be useful. Implementation is the textbook two-row DP —
|
|
388
|
-
* O(n*m) time, O(min(n,m)) space. Both inputs are slug-sized (≤ ~64 chars),
|
|
389
|
-
* so the constant factor doesn't matter. */
|
|
390
|
-
function levenshtein(a, b) {
|
|
391
|
-
if (a === b)
|
|
392
|
-
return 0;
|
|
393
|
-
if (a.length === 0)
|
|
394
|
-
return b.length;
|
|
395
|
-
if (b.length === 0)
|
|
396
|
-
return a.length;
|
|
397
|
-
let prev = new Array(b.length + 1);
|
|
398
|
-
let curr = new Array(b.length + 1);
|
|
399
|
-
for (let j = 0; j <= b.length; j++)
|
|
400
|
-
prev[j] = j;
|
|
401
|
-
for (let i = 1; i <= a.length; i++) {
|
|
402
|
-
curr[0] = i;
|
|
403
|
-
for (let j = 1; j <= b.length; j++) {
|
|
404
|
-
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
405
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
406
|
-
}
|
|
407
|
-
[prev, curr] = [curr, prev];
|
|
408
|
-
}
|
|
409
|
-
return prev[b.length];
|
|
410
|
-
}
|
|
411
|
-
/** On a 404, query the search endpoint for the user's slug fragment and
|
|
412
|
-
* return the top result as a `@user/slug` string when its slug is within
|
|
413
|
-
* Levenshtein distance 2 of the requested slug. Returns null when the
|
|
414
|
-
* search returns no results, when the top result is too far, or when
|
|
415
|
-
* `requestedRef` already equals the top match (defensive). Never throws —
|
|
416
|
-
* callers wrap in catch and treat any failure as "no suggestion". */
|
|
417
|
-
async function suggestClosestRef(requestedSlug, requestedRef) {
|
|
418
|
-
const encoded = encodeURIComponent(requestedSlug);
|
|
419
|
-
const resp = await apiFetch(`/api/search?q=${encoded}`);
|
|
420
|
-
const top = resp.results[0];
|
|
421
|
-
if (!top)
|
|
422
|
-
return null;
|
|
423
|
-
if (levenshtein(top.slug.toLowerCase(), requestedSlug.toLowerCase()) > 2)
|
|
424
|
-
return null;
|
|
425
|
-
const candidate = `@${top.author}/${top.slug}`;
|
|
426
|
-
if (candidate === requestedRef)
|
|
427
|
-
return null;
|
|
428
|
-
return candidate;
|
|
429
|
-
}
|
|
430
385
|
export async function install(rawRef, options) {
|
|
431
386
|
let ref;
|
|
432
387
|
try {
|
|
@@ -465,19 +420,15 @@ export async function install(rawRef, options) {
|
|
|
465
420
|
}
|
|
466
421
|
let manifest;
|
|
467
422
|
try {
|
|
468
|
-
manifest = await apiFetch(`/api/skills/${ref.username}/${ref.slug}/manifest
|
|
423
|
+
manifest = await apiFetch(`/api/skills/${ref.username}/${ref.slug}/manifest`, { auth: 'optional' });
|
|
469
424
|
}
|
|
470
425
|
catch (err) {
|
|
471
426
|
if (err instanceof ApiError && err.status === 404) {
|
|
472
|
-
// 404
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
// suggestion call succeeding.
|
|
478
|
-
const suggestion = await suggestClosestRef(ref.slug, refStr).catch(() => null);
|
|
479
|
-
const suffix = suggestion ? `\n Did you mean: ${suggestion}?\n` : '\n';
|
|
480
|
-
console.error(`\n ✗ Skill or bundle not found: ${refStr}${suffix}`);
|
|
427
|
+
// A 404 here also covers "PRIVATE skill you can't read" — the server
|
|
428
|
+
// returns 404 (never 403) for private-no-access so existence never
|
|
429
|
+
// leaks. We don't try to suggest a close match: the discovery/search
|
|
430
|
+
// surface was removed (private-by-default), so there's nothing to query.
|
|
431
|
+
console.error(`\n ✗ Skill or bundle not found: ${refStr}\n`);
|
|
481
432
|
process.exit(1);
|
|
482
433
|
}
|
|
483
434
|
// 410 Gone = author soft-deleted the skill upstream. Distinct from 404
|
package/dist/commands/sync.js
CHANGED
|
@@ -269,7 +269,7 @@ async function installTeamSkill(teamSlug, skill, options, silent) {
|
|
|
269
269
|
// version locking.)
|
|
270
270
|
let manifest;
|
|
271
271
|
try {
|
|
272
|
-
manifest = await apiFetch(`/api/skills/${skill.username}/${skill.slug}/manifest
|
|
272
|
+
manifest = await apiFetch(`/api/skills/${skill.username}/${skill.slug}/manifest`, { auth: 'optional' });
|
|
273
273
|
}
|
|
274
274
|
catch (err) {
|
|
275
275
|
if (err instanceof ApiError) {
|
|
@@ -376,7 +376,7 @@ async function runSyncCore(rawRef, targets, options, deps) {
|
|
|
376
376
|
const { username, slug } = refToPath(entry.ref);
|
|
377
377
|
let manifest;
|
|
378
378
|
try {
|
|
379
|
-
manifest = await apiFetch(`/api/skills/${username}/${slug}/manifest
|
|
379
|
+
manifest = await apiFetch(`/api/skills/${username}/${slug}/manifest`, { auth: 'optional' });
|
|
380
380
|
}
|
|
381
381
|
catch (err) {
|
|
382
382
|
if (err instanceof ApiError) {
|
package/dist/commands/team.js
CHANGED
|
@@ -237,7 +237,7 @@ export function registerTeamCommands(program) {
|
|
|
237
237
|
});
|
|
238
238
|
team
|
|
239
239
|
.command('push <slug> <ref>')
|
|
240
|
-
.description('Pin
|
|
240
|
+
.description('Pin one of your own published skills to a team (WRITE+ only; you can only pin skills you own)')
|
|
241
241
|
.option('--version <v>', 'Pin to a specific version (omit to float to latest)')
|
|
242
242
|
.action(async (slug, ref, opts) => {
|
|
243
243
|
await teamPush(slug, ref, { ...opts, json: program.opts().json });
|
package/dist/index.js
CHANGED
|
@@ -110,7 +110,6 @@ function genericFriendlyMessage(reason) {
|
|
|
110
110
|
}
|
|
111
111
|
import { Command } from 'commander';
|
|
112
112
|
import { checkAuthFilePerms } from './lib/config.js';
|
|
113
|
-
import { search } from './commands/search.js';
|
|
114
113
|
import { publish } from './commands/publish.js';
|
|
115
114
|
import { unpublish } from './commands/unpublish.js';
|
|
116
115
|
import { delete_ as deleteCmd } from './commands/delete.js';
|
|
@@ -179,12 +178,6 @@ program
|
|
|
179
178
|
.action(async (source) => {
|
|
180
179
|
await validate(source, { json: program.opts().json });
|
|
181
180
|
});
|
|
182
|
-
program
|
|
183
|
-
.command('search <query>')
|
|
184
|
-
.description('Search for BotDocs')
|
|
185
|
-
.action(async (query) => {
|
|
186
|
-
await search(query, { json: program.opts().json });
|
|
187
|
-
});
|
|
188
181
|
program
|
|
189
182
|
.command('publish <source>')
|
|
190
183
|
.description('Publish a BotDoc — pass a local path to upload, or @user/slug to mark an existing draft live')
|
package/dist/lib/api.d.ts
CHANGED
|
@@ -40,7 +40,14 @@ export declare class WaitlistError extends ApiError {
|
|
|
40
40
|
interface FetchOptions {
|
|
41
41
|
method?: string;
|
|
42
42
|
body?: unknown;
|
|
43
|
-
|
|
43
|
+
/** Authorization behavior:
|
|
44
|
+
* - `false` (default): never attach a bearer token.
|
|
45
|
+
* - `true`: REQUIRE a saved token — throw `ApiError(401)` when absent.
|
|
46
|
+
* - `'optional'`: attach the bearer IF a token is saved, but don't throw
|
|
47
|
+
* when it's missing. Used by read paths (manifest/versions) that gate on
|
|
48
|
+
* the server side: a signed-in user reaches their PRIVATE skills, while an
|
|
49
|
+
* anonymous caller still fetches PUBLIC/bootstrap skills tokenlessly. */
|
|
50
|
+
auth?: boolean | 'optional';
|
|
44
51
|
/** Per-call override of the default request timeout (ms). When the timer
|
|
45
52
|
* fires we abort the underlying fetch and throw an ApiError(0, "timed out…").
|
|
46
53
|
* Defaults to {@link DEFAULT_TIMEOUT_MS}. Set higher for known-slow paths
|
|
@@ -71,6 +78,14 @@ export declare function apiFetch<T>(path: string, options?: FetchOptions): Promi
|
|
|
71
78
|
* network failures and timeouts both surface as `ApiError(0, …)` so the
|
|
72
79
|
* callers don't need to special-case fetchRawContent vs apiFetch in
|
|
73
80
|
* their `instanceof ApiError` catch arms.
|
|
81
|
+
*
|
|
82
|
+
* Attaches `Authorization: Bearer <token>` when one is saved so a signed-in
|
|
83
|
+
* user can read the raw bytes of their PRIVATE skills (the server gates
|
|
84
|
+
* `/api/raw` on the same predicate as the manifest). PUBLIC and platform
|
|
85
|
+
* bootstrap skills still resolve with no token, so first-run fetches (e.g.
|
|
86
|
+
* `@botdocs/cli-quickstart`) keep working unauthenticated. The TLS guard in
|
|
87
|
+
* getApiUrl() refuses to emit the bearer over plaintext to a non-loopback
|
|
88
|
+
* host — see {@link getApiUrl}.
|
|
74
89
|
*/
|
|
75
90
|
export declare function fetchRawContent(rawUrl: string, options?: {
|
|
76
91
|
timeoutMs?: number;
|
package/dist/lib/api.js
CHANGED
|
@@ -131,12 +131,24 @@ export async function apiFetch(path, options = {}) {
|
|
|
131
131
|
if (auth) {
|
|
132
132
|
const config = loadAuth();
|
|
133
133
|
if (!config?.token) {
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
134
|
+
// `auth: 'optional'` means "attach the bearer if we have one, otherwise
|
|
135
|
+
// proceed anonymously" — the server gates the read. Only the strict
|
|
136
|
+
// `auth: true` path throws when no token is saved. Shape that as an
|
|
137
|
+
// ApiError(401) so callers that already branch on auth failures (e.g.
|
|
138
|
+
// sync's optional team-skill path) handle it uniformly without a
|
|
139
|
+
// separate "no token saved" code path.
|
|
140
|
+
if (auth === 'optional') {
|
|
141
|
+
// No token — fall through and send the request without Authorization.
|
|
142
|
+
// The TLS guard in getApiUrl() above already ran, so the (absent)
|
|
143
|
+
// bearer can't leak over plaintext regardless.
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
throw new ApiError(401, 'Not authenticated. Run `botdocs login` first.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
headers['Authorization'] = `Bearer ${config.token}`;
|
|
138
151
|
}
|
|
139
|
-
headers['Authorization'] = `Bearer ${config.token}`;
|
|
140
152
|
}
|
|
141
153
|
// AbortController + setTimeout is the lowest-overhead way to enforce a hard
|
|
142
154
|
// per-request deadline on Node's fetch — there's no built-in `timeout`
|
|
@@ -216,18 +228,31 @@ export async function apiFetch(path, options = {}) {
|
|
|
216
228
|
* network failures and timeouts both surface as `ApiError(0, …)` so the
|
|
217
229
|
* callers don't need to special-case fetchRawContent vs apiFetch in
|
|
218
230
|
* their `instanceof ApiError` catch arms.
|
|
231
|
+
*
|
|
232
|
+
* Attaches `Authorization: Bearer <token>` when one is saved so a signed-in
|
|
233
|
+
* user can read the raw bytes of their PRIVATE skills (the server gates
|
|
234
|
+
* `/api/raw` on the same predicate as the manifest). PUBLIC and platform
|
|
235
|
+
* bootstrap skills still resolve with no token, so first-run fetches (e.g.
|
|
236
|
+
* `@botdocs/cli-quickstart`) keep working unauthenticated. The TLS guard in
|
|
237
|
+
* getApiUrl() refuses to emit the bearer over plaintext to a non-loopback
|
|
238
|
+
* host — see {@link getApiUrl}.
|
|
219
239
|
*/
|
|
220
240
|
export async function fetchRawContent(rawUrl, options = {}) {
|
|
221
241
|
const { timeoutMs = DEFAULT_TIMEOUT_MS } = options;
|
|
222
242
|
const baseUrl = getApiUrl();
|
|
223
243
|
const url = rawUrl.startsWith('http') ? rawUrl : `${baseUrl}${rawUrl}`;
|
|
244
|
+
const headers = {};
|
|
245
|
+
const config = loadAuth();
|
|
246
|
+
if (config?.token) {
|
|
247
|
+
headers['Authorization'] = `Bearer ${config.token}`;
|
|
248
|
+
}
|
|
224
249
|
// Same pattern as apiFetch — AbortController + clearTimeout in finally so
|
|
225
250
|
// the timer never outlives the request and blocks process exit.
|
|
226
251
|
const controller = new AbortController();
|
|
227
252
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
228
253
|
let response;
|
|
229
254
|
try {
|
|
230
|
-
response = await fetch(url, { signal: controller.signal });
|
|
255
|
+
response = await fetch(url, { headers, signal: controller.signal });
|
|
231
256
|
}
|
|
232
257
|
catch (err) {
|
|
233
258
|
if (isAbortError(err)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@botdocs/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "CLI for BotDocs — author, publish, install, and sync agent skills across Claude, Claude Code, Cursor, Codex, ChatGPT, Windsurf, Copilot, Gemini, Antigravity, and OpenCode.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"botdocs",
|
package/dist/commands/search.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { apiFetch } from '../lib/api.js';
|
|
2
|
-
export async function search(query, options = {}) {
|
|
3
|
-
if (!query.trim()) {
|
|
4
|
-
console.error('Please provide a search query.');
|
|
5
|
-
process.exit(1);
|
|
6
|
-
}
|
|
7
|
-
const encoded = encodeURIComponent(query);
|
|
8
|
-
const data = await apiFetch(`/api/search?q=${encoded}`);
|
|
9
|
-
if (options.json) {
|
|
10
|
-
console.log(JSON.stringify(data, null, 2));
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
if (data.results.length === 0) {
|
|
14
|
-
console.log(`No results for "${query}".`);
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
console.log(`${data.total} result(s) for "${query}":\n`);
|
|
18
|
-
// Print formatted table
|
|
19
|
-
const maxTitle = Math.min(40, Math.max(...data.results.map((r) => r.title.length)));
|
|
20
|
-
const maxAuthor = Math.max(...data.results.map((r) => r.author.length));
|
|
21
|
-
const header = [
|
|
22
|
-
'Title'.padEnd(maxTitle),
|
|
23
|
-
'Author'.padEnd(maxAuthor),
|
|
24
|
-
'Stars'.padStart(5),
|
|
25
|
-
'Clones'.padStart(6),
|
|
26
|
-
'Category',
|
|
27
|
-
].join(' ');
|
|
28
|
-
const separator = '-'.repeat(header.length);
|
|
29
|
-
console.log(header);
|
|
30
|
-
console.log(separator);
|
|
31
|
-
for (const result of data.results) {
|
|
32
|
-
const title = result.title.length > maxTitle
|
|
33
|
-
? result.title.slice(0, maxTitle - 1) + '…'
|
|
34
|
-
: result.title.padEnd(maxTitle);
|
|
35
|
-
const row = [
|
|
36
|
-
title,
|
|
37
|
-
result.author.padEnd(maxAuthor),
|
|
38
|
-
String(result.starCount).padStart(5),
|
|
39
|
-
String(result.cloneCount).padStart(6),
|
|
40
|
-
formatCategory(result.category),
|
|
41
|
-
].join(' ');
|
|
42
|
-
console.log(row);
|
|
43
|
-
}
|
|
44
|
-
if (data.totalPages > 1) {
|
|
45
|
-
console.log(`\nPage ${data.page}/${data.totalPages}`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function formatCategory(category) {
|
|
49
|
-
const labels = {
|
|
50
|
-
KNOWLEDGE_MANAGEMENT: 'Knowledge',
|
|
51
|
-
DEV_WORKFLOW: 'Dev Workflow',
|
|
52
|
-
AUTOMATION: 'Automation',
|
|
53
|
-
AGENT_CONFIG: 'Agent Config',
|
|
54
|
-
PROJECT_SCAFFOLD: 'Scaffold',
|
|
55
|
-
OTHER: 'Other',
|
|
56
|
-
};
|
|
57
|
-
return labels[category] ?? category;
|
|
58
|
-
}
|