@botdocs/cli 0.6.0 → 0.8.1
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 +80 -8
- package/dist/commands/delete.d.ts +23 -0
- package/dist/commands/delete.js +106 -0
- package/dist/commands/ingest.d.ts +58 -0
- package/dist/commands/ingest.js +344 -27
- package/dist/commands/install.js +11 -0
- package/dist/commands/publish.d.ts +29 -1
- package/dist/commands/publish.js +85 -1
- package/dist/commands/unpublish.d.ts +16 -0
- package/dist/commands/unpublish.js +53 -0
- package/dist/commands/views/ingest-discover-app.d.ts +6 -1
- package/dist/commands/views/ingest-discover-app.js +56 -14
- package/dist/index.js +18 -1
- package/dist/lib/auto-detect.js +19 -0
- package/dist/lib/ingest-discover.d.ts +42 -2
- package/dist/lib/ingest-discover.js +72 -6
- package/dist/lib/ref.d.ts +42 -0
- package/dist/lib/ref.js +60 -0
- package/package.json +1 -1
- package/templates/agents.md +2 -1
package/README.md
CHANGED
|
@@ -55,7 +55,9 @@ botdocs publish my-skill/
|
|
|
55
55
|
| `edit <ref>` | LLM-assisted revision of a published skill ecosystem file (BYOK). |
|
|
56
56
|
| `validate <source>` | Pre-publish structural check on a directory or file. |
|
|
57
57
|
| `search <query>` | Search the public registry. |
|
|
58
|
-
| `publish <source>` | Publish from a file, directory, or zip archive. |
|
|
58
|
+
| `publish <source>` | Publish from a file, directory, or zip archive — or pass `@user/slug` to mark an existing draft live. |
|
|
59
|
+
| `unpublish <ref>` | Hide a published BotDoc from `/explore` (sets the `draft` flag back). |
|
|
60
|
+
| `delete <ref>` | Delete a BotDoc. Drafts are hard-deleted (row + all children); published BotDocs are soft-deleted (hidden, version history preserved). |
|
|
59
61
|
| `install <ref>` | Install a skill or bundle (auto-detects destinations). |
|
|
60
62
|
| `sync [ref]` | Check installed skills/bundles for updates and apply. |
|
|
61
63
|
| `uninstall <ref>` | Remove an installed skill or bundle. |
|
|
@@ -290,10 +292,28 @@ ambiguously (e.g. paths containing `_`).
|
|
|
290
292
|
|
|
291
293
|
Authors who want to share their existing collection of skills run
|
|
292
294
|
`botdocs ingest <path>` — the CLI walks the directory, detects each
|
|
293
|
-
skill across all
|
|
294
|
-
chatgpt, codex, copilot, windsurf, gemini,
|
|
295
|
-
uploads them as drafts in your BotDocs
|
|
296
|
-
publishing.
|
|
295
|
+
skill across all 11 supported ecosystems (claude, claude-code,
|
|
296
|
+
claude-code-agents, cursor, chatgpt, codex, copilot, windsurf, gemini,
|
|
297
|
+
antigravity, opencode), and uploads them as drafts in your BotDocs
|
|
298
|
+
account for review before publishing.
|
|
299
|
+
|
|
300
|
+
For nested ecosystems (claude SKILL.md, claude-code-agents AGENT.md),
|
|
301
|
+
ingest also sweeps adjacent files inside the skill directory —
|
|
302
|
+
`scripts/*.sh`, `templates/*`, reference docs — and uploads them with
|
|
303
|
+
their original POSIX file mode so the executable bit on helper scripts
|
|
304
|
+
survives the round-trip. `botdocs install` restores the mode on disk.
|
|
305
|
+
|
|
306
|
+
Per-skill size limits (enforced both client- and server-side):
|
|
307
|
+
|
|
308
|
+
- 64 KB per file
|
|
309
|
+
- 512 KB total per skill
|
|
310
|
+
- 25 files per skill
|
|
311
|
+
|
|
312
|
+
Binary files are detected by a null-byte sniff on the first 8 KB and
|
|
313
|
+
skipped with an inline warning. Cap violations warn and skip without
|
|
314
|
+
failing the ingest. Skipped dirs: `node_modules/`, `.git/`, `dist/`,
|
|
315
|
+
`build/`, `.next/`, `.turbo/`, `__pycache__/`, `venv/`, `.venv/`, plus
|
|
316
|
+
all dotfiles, `.DS_Store`, `*.log`, `*.lock`.
|
|
297
317
|
|
|
298
318
|
By default `ingest` expects the canonical BotDocs layout (e.g.
|
|
299
319
|
`claude-code/commands/<slug>.md`, `cursor/rules/<slug>.mdc`). If your
|
|
@@ -316,12 +336,63 @@ The upload always uses the canonical BotDocs filename, so when someone
|
|
|
316
336
|
else `botdocs install`s the resulting skill it lands in the right
|
|
317
337
|
on-disk location.
|
|
318
338
|
|
|
339
|
+
### Publishing drafts (and taking them back down)
|
|
340
|
+
|
|
341
|
+
`botdocs ingest` and `botdocs edit` both create drafts — hidden from
|
|
342
|
+
`/explore`, 404 for everyone but the author. To flip a draft live:
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
botdocs publish @me/my-skill # → ✓ Published @me/my-skill
|
|
346
|
+
botdocs publish @me/my-skill --json
|
|
347
|
+
# → {"ok":true,"ref":"@me/my-skill","status":"published"}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`botdocs publish` is overloaded by argument shape: a local path
|
|
351
|
+
(`./my-skill/`) runs the existing upload flow unchanged; a
|
|
352
|
+
`@user/slug` ref toggles the publish flag via the API.
|
|
353
|
+
|
|
354
|
+
To hide a published BotDoc again (e.g. you want to revise it without
|
|
355
|
+
the in-progress version visible publicly):
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
botdocs unpublish @me/my-skill # prompts to confirm
|
|
359
|
+
botdocs unpublish @me/my-skill --yes # skip the prompt (scripts/CI)
|
|
360
|
+
botdocs unpublish @me/my-skill --json # implies --yes, emits JSON
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Unpublishing sets the `draft` flag back; the URL 404s for non-authors
|
|
364
|
+
and the BotDoc disappears from `/explore`. It's idempotent — calling
|
|
365
|
+
it on something already a draft is a no-op.
|
|
366
|
+
|
|
367
|
+
To delete a BotDoc entirely:
|
|
368
|
+
|
|
369
|
+
```bash
|
|
370
|
+
botdocs delete @me/my-skill # state-aware confirm prompt
|
|
371
|
+
botdocs delete @me/my-skill --yes # skip the prompt (scripts/CI)
|
|
372
|
+
botdocs delete @me/my-skill --json # implies --yes, emits JSON
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
`delete` behaves differently depending on what's there:
|
|
376
|
+
|
|
377
|
+
- **Draft** → *hard delete*. The row and all children (files, version
|
|
378
|
+
history, bundle pins, team pins) are removed. The confirm prompt is a
|
|
379
|
+
single yes/no.
|
|
380
|
+
- **Published** → *soft delete*. Sets `deletedAt`; the BotDoc disappears
|
|
381
|
+
from `/explore` and the public URL 404s, but version history is kept.
|
|
382
|
+
The confirm prompt requires typing the full ref (`@user/slug`) so a
|
|
383
|
+
fat-finger doesn't strand bookmarks at a 404.
|
|
384
|
+
|
|
385
|
+
JSON output is `{ok, ref, mode}` where `mode` is `"hard"` or `"soft"`,
|
|
386
|
+
mirroring the server's decision.
|
|
387
|
+
|
|
319
388
|
### Zero-argument discovery mode
|
|
320
389
|
|
|
321
390
|
Run `botdocs ingest` (no path) and the CLI scans your machine across
|
|
322
|
-
every known on-disk location for the
|
|
391
|
+
every known on-disk location for the ten supported ecosystems:
|
|
323
392
|
|
|
324
393
|
- `~/.claude/commands/` and `<repo>/.claude/commands/` (Claude Code)
|
|
394
|
+
- `~/.claude/agents/**/AGENT.md` and `<repo>/.claude/agents/**/AGENT.md`
|
|
395
|
+
(Claude Code agents, multi-file)
|
|
325
396
|
- `~/.claude/skills/**/SKILL.md` (Claude skills, nested by scope)
|
|
326
397
|
- `<repo>/.cursor/rules/` (Cursor)
|
|
327
398
|
- `<repo>/.codex/skills/` (Codex)
|
|
@@ -335,7 +406,8 @@ Project-scoped scans (Cursor, Codex, Copilot, Windsurf, the project
|
|
|
335
406
|
flavor of Claude Code) only run when the current directory is inside
|
|
336
407
|
a git repo.
|
|
337
408
|
|
|
338
|
-
A scan opens an interactive Ink TUI sectioned by ecosystem
|
|
409
|
+
A scan opens an interactive Ink TUI sectioned by ecosystem. Multi-file
|
|
410
|
+
skills show their aggregate size + file count instead of per-file lines:
|
|
339
411
|
|
|
340
412
|
```text
|
|
341
413
|
BotDocs ingest
|
|
@@ -347,7 +419,7 @@ Found 7 skills across 4 tools:
|
|
|
347
419
|
[ ] generic-test-cmd 0.1 KB · 4 lines ← unchecked (< 100 bytes)
|
|
348
420
|
|
|
349
421
|
Claude skills:
|
|
350
|
-
[x] code-review 4
|
|
422
|
+
[x] code-review 12.4 KB · 4 files ← SKILL.md + 3 adjacent
|
|
351
423
|
|
|
352
424
|
Cursor rules:
|
|
353
425
|
[x] typescript-strict 0.8 KB · 25 lines
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
interface DeleteOptions {
|
|
2
|
+
yes?: boolean;
|
|
3
|
+
json?: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Delete a BotDoc by ref. Behavior split (decided server-side):
|
|
7
|
+
*
|
|
8
|
+
* - Draft → hard delete with cascade. The row and all children (files,
|
|
9
|
+
* versions, version files, bundle pins, team pins) go away.
|
|
10
|
+
* - Published → soft delete (sets `deletedAt`). Hidden from /explore;
|
|
11
|
+
* the public URL 404s; version history is preserved.
|
|
12
|
+
*
|
|
13
|
+
* Prompts unless `--yes` (or `--json`, which implies `--yes`). Draft
|
|
14
|
+
* deletes get a single yes/no confirm; published deletes require typing
|
|
15
|
+
* the ref exactly because the effect is visible to anyone who had the
|
|
16
|
+
* URL bookmarked. The CLI does a preflight GET to know which prompt to
|
|
17
|
+
* show before the user commits.
|
|
18
|
+
*
|
|
19
|
+
* Errors flow through the shared `handlePublishToggleError` so 401/403/
|
|
20
|
+
* 404 messages stay in sync with publish/unpublish.
|
|
21
|
+
*/
|
|
22
|
+
export declare function delete_(rawRef: string, options: DeleteOptions): Promise<void>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { apiFetch } from '../lib/api.js';
|
|
3
|
+
import { parseRef } from '../lib/ref.js';
|
|
4
|
+
import { handlePublishToggleError } from './publish.js';
|
|
5
|
+
/**
|
|
6
|
+
* Delete a BotDoc by ref. Behavior split (decided server-side):
|
|
7
|
+
*
|
|
8
|
+
* - Draft → hard delete with cascade. The row and all children (files,
|
|
9
|
+
* versions, version files, bundle pins, team pins) go away.
|
|
10
|
+
* - Published → soft delete (sets `deletedAt`). Hidden from /explore;
|
|
11
|
+
* the public URL 404s; version history is preserved.
|
|
12
|
+
*
|
|
13
|
+
* Prompts unless `--yes` (or `--json`, which implies `--yes`). Draft
|
|
14
|
+
* deletes get a single yes/no confirm; published deletes require typing
|
|
15
|
+
* the ref exactly because the effect is visible to anyone who had the
|
|
16
|
+
* URL bookmarked. The CLI does a preflight GET to know which prompt to
|
|
17
|
+
* show before the user commits.
|
|
18
|
+
*
|
|
19
|
+
* Errors flow through the shared `handlePublishToggleError` so 401/403/
|
|
20
|
+
* 404 messages stay in sync with publish/unpublish.
|
|
21
|
+
*/
|
|
22
|
+
export async function delete_(rawRef, options) {
|
|
23
|
+
let parsed;
|
|
24
|
+
try {
|
|
25
|
+
parsed = parseRef(rawRef);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const { username, slug } = parsed;
|
|
32
|
+
const refLabel = `@${username}/${slug}`;
|
|
33
|
+
// Preflight to determine state — we need to know draft vs published so
|
|
34
|
+
// we can show the right confirm prompt. Skip the network round-trip
|
|
35
|
+
// when prompts are disabled (`--yes`/`--json`): the server returns the
|
|
36
|
+
// resolved mode in the DELETE response anyway, and skipping the GET
|
|
37
|
+
// avoids a double-fail if the user has a stale token.
|
|
38
|
+
if (!options.yes && !options.json) {
|
|
39
|
+
let info;
|
|
40
|
+
try {
|
|
41
|
+
info = await apiFetch(`/api/botdocs/${username}/${slug}`, {
|
|
42
|
+
method: 'GET',
|
|
43
|
+
auth: true,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
handlePublishToggleError(err, refLabel, options);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const confirmed = await promptConfirm(refLabel, info.isDraft);
|
|
51
|
+
if (!confirmed) {
|
|
52
|
+
console.log(' Cancelled.\n');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let result;
|
|
57
|
+
try {
|
|
58
|
+
result = await apiFetch(`/api/botdocs/${username}/${slug}`, {
|
|
59
|
+
method: 'DELETE',
|
|
60
|
+
auth: true,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
handlePublishToggleError(err, refLabel, options);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (options.json) {
|
|
68
|
+
console.log(JSON.stringify({ ok: true, ref: refLabel, mode: result.mode }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (result.mode === 'hard') {
|
|
72
|
+
console.log(`✓ Deleted draft ${refLabel}.`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
console.log(`✓ Deleted ${refLabel} — hidden from /explore (version history preserved).`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Show the appropriate confirm prompt for the BotDoc's state.
|
|
80
|
+
*
|
|
81
|
+
* Draft → simple yes/no confirm.
|
|
82
|
+
* Published → type-the-ref. We don't accept anything else — a fat-finger
|
|
83
|
+
* on a soft delete still strands public bookmarks at a 404.
|
|
84
|
+
*
|
|
85
|
+
* Returns `false` for any non-positive outcome (ctrl-C, no, wrong text).
|
|
86
|
+
*/
|
|
87
|
+
async function promptConfirm(refLabel, isDraft) {
|
|
88
|
+
if (isDraft) {
|
|
89
|
+
const confirmed = await p.confirm({
|
|
90
|
+
message: `Delete draft ${refLabel}? This can't be undone.`,
|
|
91
|
+
initialValue: false,
|
|
92
|
+
});
|
|
93
|
+
if (p.isCancel(confirmed))
|
|
94
|
+
return false;
|
|
95
|
+
return confirmed === true;
|
|
96
|
+
}
|
|
97
|
+
const typed = await p.text({
|
|
98
|
+
message: `Delete ${refLabel}? This hides it from /explore (version history is kept). ` +
|
|
99
|
+
`Bookmarks will 404. Confirm by typing the ref:`,
|
|
100
|
+
placeholder: refLabel,
|
|
101
|
+
validate: (v) => (v === refLabel ? undefined : `Must match ${refLabel} exactly`),
|
|
102
|
+
});
|
|
103
|
+
if (p.isCancel(typed))
|
|
104
|
+
return false;
|
|
105
|
+
return typed === refLabel;
|
|
106
|
+
}
|
|
@@ -12,6 +12,43 @@ interface IngestOptions {
|
|
|
12
12
|
* `--no-ink` flag on `login` and `sync`. */
|
|
13
13
|
noInk?: boolean;
|
|
14
14
|
}
|
|
15
|
+
/** Per-file size cap. Any file larger than this is skipped with a warning. */
|
|
16
|
+
export declare const PER_FILE_BYTE_CAP: number;
|
|
17
|
+
/** Total bytes across all files in a single skill. */
|
|
18
|
+
export declare const PER_SKILL_BYTE_CAP: number;
|
|
19
|
+
/** Total file count in a single skill. */
|
|
20
|
+
export declare const PER_SKILL_FILE_CAP = 25;
|
|
21
|
+
export interface AdjacentSweepFile {
|
|
22
|
+
/** Absolute path on disk. */
|
|
23
|
+
absPath: string;
|
|
24
|
+
/** Path relative to the skill root, forward-slashed (`scripts/helper.sh`). */
|
|
25
|
+
relPath: string;
|
|
26
|
+
/** File contents (text). Binary files are skipped before this is read. */
|
|
27
|
+
content: string;
|
|
28
|
+
/** Low 9 bits of st_mode, captured before reading. */
|
|
29
|
+
mode: number;
|
|
30
|
+
/** Size in bytes. */
|
|
31
|
+
sizeBytes: number;
|
|
32
|
+
}
|
|
33
|
+
export interface AdjacentSweepWarning {
|
|
34
|
+
relPath: string;
|
|
35
|
+
reason: 'binary' | 'oversize' | 'cap-total-size' | 'cap-file-count';
|
|
36
|
+
}
|
|
37
|
+
export interface AdjacentSweepResult {
|
|
38
|
+
files: AdjacentSweepFile[];
|
|
39
|
+
warnings: AdjacentSweepWarning[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Walk a skill root recursively and return every non-skipped file the caller
|
|
43
|
+
* should ingest as an adjacent file. Applies dir-skip, dotfile, binary, and
|
|
44
|
+
* cap rules. Stops collecting once total caps are exceeded — remaining files
|
|
45
|
+
* land in `warnings`. The root file (e.g. SKILL.md) is excluded so the caller
|
|
46
|
+
* can handle it separately.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sweepSkillRoot(skillRoot: string, rootFileAbs: string): AdjacentSweepResult;
|
|
49
|
+
/** Format a sweep warning summary line for a single skill. Empty string when
|
|
50
|
+
* there are no warnings — caller can use that to suppress the line. */
|
|
51
|
+
export declare function formatSweepWarnings(slug: string, warnings: AdjacentSweepWarning[]): string;
|
|
15
52
|
export interface EcosystemDetector {
|
|
16
53
|
/** Path prefix the file's root-relative path must start with for auto-detect mode. */
|
|
17
54
|
pathPrefix: string;
|
|
@@ -35,6 +72,27 @@ export interface EcosystemDetector {
|
|
|
35
72
|
* on-disk location (e.g. chatgpt — manual-paste only).
|
|
36
73
|
*/
|
|
37
74
|
scanPaths: (homeDir: string, projectRoot: string, isGitRepo: boolean) => string[];
|
|
75
|
+
/**
|
|
76
|
+
* When true, ingest sweeps the skill directory and uploads adjacent files
|
|
77
|
+
* (scripts/, templates/, etc.) alongside the root file. Today this is only
|
|
78
|
+
* meaningful for `claude` and `claude-code-agents` — flat-file ecosystems
|
|
79
|
+
* leave it false. `claude-code` (commands) keeps it as `true` for forward-
|
|
80
|
+
* compat but is a no-op since commands have no enclosing directory.
|
|
81
|
+
*/
|
|
82
|
+
includeAdjacent?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Given the absolute path to a matched root file, return the directory that
|
|
85
|
+
* counts as the "skill root" — everything below it is eligible for the
|
|
86
|
+
* adjacent-file sweep. For nested ecosystems this is the parent dir of the
|
|
87
|
+
* root file. Only invoked when `includeAdjacent` is true.
|
|
88
|
+
*/
|
|
89
|
+
skillRoot?: (absPath: string) => string;
|
|
90
|
+
/**
|
|
91
|
+
* Build the canonical filename for an adjacent file given its skill slug
|
|
92
|
+
* and the path relative to the skill root (e.g. `scripts/helper.sh`). Only
|
|
93
|
+
* invoked when `includeAdjacent` is true.
|
|
94
|
+
*/
|
|
95
|
+
canonicalAdjacentFilename?: (slug: string, relPath: string) => string;
|
|
38
96
|
}
|
|
39
97
|
/**
|
|
40
98
|
* Single source of truth for ecosystem detection. New ecosystems should be
|