@botdocs/cli 0.6.0 → 0.8.0

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
@@ -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 10 supported ecosystems (claude, claude-code, cursor,
294
- chatgpt, codex, copilot, windsurf, gemini, antigravity, opencode), and
295
- uploads them as drafts in your BotDocs account for review before
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 nine supported ecosystems:
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.2 KB · 142 lines
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