@botdocs/cli 0.5.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,12 +55,14 @@ 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. |
62
64
  | `list` | Show installed skills and bundles. |
63
- | `ingest <path>` | Walk a directory, detect existing skills, upload as drafts. |
65
+ | `ingest [path]` | Scan your system (or a directory), detect existing skills, upload as drafts. |
64
66
  | `team list` / `show` / `create` / `add` / `remove` / `push` / `unpush` | Manage teams: shared skill libraries for your org. |
65
67
  | `undo` | Restore the most recent backup run (reversible). |
66
68
  | `backups list` / `restore` / `diff` / `clear` | Browse, restore, diff, and prune backup runs. |
@@ -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,6 +336,114 @@ 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
+
388
+ ### Zero-argument discovery mode
389
+
390
+ Run `botdocs ingest` (no path) and the CLI scans your machine across
391
+ every known on-disk location for the ten supported ecosystems:
392
+
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)
396
+ - `~/.claude/skills/**/SKILL.md` (Claude skills, nested by scope)
397
+ - `<repo>/.cursor/rules/` (Cursor)
398
+ - `<repo>/.codex/skills/` (Codex)
399
+ - `<repo>/.github/instructions/` (GitHub Copilot)
400
+ - `<repo>/.codeium/windsurf-rules/` (Windsurf)
401
+ - `~/.gemini/instructions/` (Gemini CLI)
402
+ - `~/.gemini/antigravity/skills/` (Antigravity)
403
+ - `~/.config/opencode/instructions/` (OpenCode)
404
+
405
+ Project-scoped scans (Cursor, Codex, Copilot, Windsurf, the project
406
+ flavor of Claude Code) only run when the current directory is inside
407
+ a git repo.
408
+
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:
411
+
412
+ ```text
413
+ BotDocs ingest
414
+ Found 7 skills across 4 tools:
415
+
416
+ Claude Code:
417
+ [x] scx-pr-craft 2.1 KB · 67 lines
418
+ [x] pr-review-craft 1.8 KB · 54 lines
419
+ [ ] generic-test-cmd 0.1 KB · 4 lines ← unchecked (< 100 bytes)
420
+
421
+ Claude skills:
422
+ [x] code-review 12.4 KB · 4 files ← SKILL.md + 3 adjacent
423
+
424
+ Cursor rules:
425
+ [x] typescript-strict 0.8 KB · 25 lines
426
+
427
+ Gemini CLI:
428
+ [x] research-protocol 2.4 KB · 81 lines
429
+
430
+ ↑/↓ navigate · space toggle · a select all · n select none · enter confirm · q cancel
431
+ ```
432
+
433
+ Useful flags:
434
+
435
+ - `--auto` — skip the TUI and upload everything discovery finds
436
+ (with the < 100-byte stub filter applied).
437
+ - `--dry-run` — list what discovery found in plain text; don't upload.
438
+ - `--json` — emit the discovery as JSON; don't upload.
439
+ - `--no-ink` — disable the TUI on TTYs (for screen readers or simple
440
+ terminals). Falls back to plain-text listing without uploading;
441
+ combine with `--auto` if you want one-shot ingestion.
442
+
443
+ When the discovery returns nothing the CLI prints a hint pointing at
444
+ `botdocs init` (to scaffold a new skill) or `botdocs ingest <dir>`
445
+ (to ingest from a specific path).
446
+
319
447
  ## Development
320
448
 
321
449
  ```bash
@@ -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
+ }
@@ -2,8 +2,104 @@ interface IngestOptions {
2
2
  bundle?: string;
3
3
  dryRun?: boolean;
4
4
  json?: boolean;
5
+ /** Force every file in the path to belong to a single ecosystem, instead of
6
+ * relying on the canonical-layout auto-detect. Set via `--from-tool=<x>`. */
5
7
  fromTool?: string;
8
+ /** Skip the TUI and ingest everything discovery finds (with the default
9
+ * stub filter applied). Set via `--auto`. */
10
+ auto?: boolean;
11
+ /** Force the plain-text rendering path — disables the Ink TUI. Mirrors the
12
+ * `--no-ink` flag on `login` and `sync`. */
13
+ noInk?: boolean;
6
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;
52
+ export interface EcosystemDetector {
53
+ /** Path prefix the file's root-relative path must start with for auto-detect mode. */
54
+ pathPrefix: string;
55
+ /** Extension suffixes the filename must match. Tested via endsWith. */
56
+ extensions: string[];
57
+ /** Whether this ecosystem uses a nested SKILL.md layout (claude) vs flat.
58
+ * Discovery uses this to decide whether to recurse the scan directories. */
59
+ nested: boolean;
60
+ /**
61
+ * Given an absolute file path and the source root, return the slug or null
62
+ * if the file doesn't match this ecosystem's layout (e.g. wrong nesting).
63
+ */
64
+ slugFor: (absPath: string, root: string) => string | null;
65
+ /** Canonical filename inside a BotDoc directory, given the slug. */
66
+ canonicalFilename: (slug: string) => string;
67
+ /**
68
+ * Absolute directories to scan during zero-arg discovery mode. Receives the
69
+ * user's home dir, the current project root (cwd), and whether the project
70
+ * root is inside a git repo. Project-scoped paths should return `[]` when
71
+ * `isGitRepo` is false. Return `[]` for ecosystems that have no canonical
72
+ * on-disk location (e.g. chatgpt — manual-paste only).
73
+ */
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;
96
+ }
97
+ /**
98
+ * Single source of truth for ecosystem detection. New ecosystems should be
99
+ * added here — both auto-detect mode and future `--from-tool` mode consult
100
+ * this table.
101
+ */
102
+ export declare const DETECTORS: Record<string, EcosystemDetector>;
7
103
  export declare const SUPPORTED_TOOLS: readonly string[];
8
- export declare function ingest(rootPath: string, options: IngestOptions): Promise<void>;
104
+ export declare function ingest(rootPath: string | undefined, options: IngestOptions): Promise<void>;
9
105
  export {};