@botdocs/cli 0.8.1 → 0.9.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.
@@ -11,6 +11,10 @@ interface IngestOptions {
11
11
  /** Force the plain-text rendering path — disables the Ink TUI. Mirrors the
12
12
  * `--no-ink` flag on `login` and `sync`. */
13
13
  noInk?: boolean;
14
+ /** Replace existing *draft* skills that collide on slug. Published skills
15
+ * are never replaced — they surface as a blocking 409 either way. Set via
16
+ * `--force`. */
17
+ force?: boolean;
14
18
  }
15
19
  /** Per-file size cap. Any file larger than this is skipped with a warning. */
16
20
  export declare const PER_FILE_BYTE_CAP: number;
@@ -2,7 +2,8 @@ import React from 'react';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { render } from 'ink';
5
- import { apiFetch } from '../lib/api.js';
5
+ import { ApiError, apiFetch } from '../lib/api.js';
6
+ import { loadAuth } from '../lib/config.js';
6
7
  import { discoverSkills, ecosystemLabel, formatBytes, STUB_BYTE_THRESHOLD, summaryKey, titleFromContent, } from '../lib/ingest-discover.js';
7
8
  import { IngestDiscoverApp } from './views/ingest-discover-app.js';
8
9
  // ---------- Cap + skip rules (shared with discovery) ----------
@@ -279,12 +280,26 @@ export const DETECTORS = {
279
280
  scanPaths: () => [],
280
281
  },
281
282
  codex: {
283
+ // Codex skills are nested SKILL.md directories, mirroring claude:
284
+ // ~/.codex/skills/<slug>/SKILL.md (developers.openai.com/codex/skills).
282
285
  pathPrefix: 'codex/',
283
- extensions: ['.md'],
284
- nested: false,
285
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
286
- canonicalFilename: (slug) => `codex/${slug}.md`,
287
- scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.codex', 'skills')] : [],
286
+ extensions: ['/SKILL.md'],
287
+ nested: true,
288
+ slugFor: (abs, root) => {
289
+ const rel = path.relative(root, abs).split(path.sep).join('/');
290
+ if (!rel.endsWith('/SKILL.md'))
291
+ return null;
292
+ const parts = rel.split('/');
293
+ if (parts.length < 2)
294
+ return null;
295
+ return parts[parts.length - 2] ?? null;
296
+ },
297
+ canonicalFilename: (slug) => `codex/${slug}/SKILL.md`,
298
+ // Global only — Codex skills live under ~/.codex/skills/.
299
+ scanPaths: (homeDir) => [path.join(homeDir, '.codex', 'skills')],
300
+ includeAdjacent: true,
301
+ skillRoot: (abs) => path.dirname(abs),
302
+ canonicalAdjacentFilename: (slug, relPath) => `codex/${slug}/${relPath}`,
288
303
  },
289
304
  copilot: {
290
305
  pathPrefix: 'copilot/instructions/',
@@ -296,36 +311,90 @@ export const DETECTORS = {
296
311
  scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.github', 'instructions')] : [],
297
312
  },
298
313
  windsurf: {
314
+ // Windsurf reads project rules from <proj>/.windsurf/rules/<slug>.md
315
+ // (docs.windsurf.com). Flat .md rule files, project-scoped (git repo).
299
316
  pathPrefix: 'windsurf/rules/',
300
317
  extensions: ['.md'],
301
318
  nested: false,
302
319
  slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
303
320
  canonicalFilename: (slug) => `windsurf/rules/${slug}.md`,
304
- scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.codeium', 'windsurf-rules')] : [],
321
+ scanPaths: (_homeDir, projectRoot, isGitRepo) => isGitRepo ? [path.join(projectRoot, '.windsurf', 'rules')] : [],
305
322
  },
306
323
  gemini: {
324
+ // Gemini CLI has NO per-skill file directory. It uses hierarchical
325
+ // GEMINI.md context files (~/.gemini/GEMINI.md global, ./GEMINI.md
326
+ // project) — there's nothing to auto-discover and nowhere to drop a
327
+ // per-skill file. The entry stays present so the ecosystem still exists
328
+ // for compile/variants, but discovery returns nothing (like chatgpt) and
329
+ // install routes it to `manual` (see detectDestination).
307
330
  pathPrefix: 'gemini/instructions/',
308
331
  extensions: ['.md'],
309
332
  nested: false,
310
333
  slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
311
334
  canonicalFilename: (slug) => `gemini/instructions/${slug}.md`,
312
- scanPaths: (homeDir) => [path.join(homeDir, '.gemini', 'instructions')],
335
+ // No canonical on-disk location — nothing to discover.
336
+ scanPaths: () => [],
313
337
  },
314
338
  antigravity: {
339
+ // Antigravity skills are nested SKILL.md directories, mirroring claude:
340
+ // ~/.gemini/antigravity/skills/<slug>/SKILL.md (global) and
341
+ // <proj>/.agent/skills/<slug>/SKILL.md (project)
342
+ // (antigravity.google/docs/skills + Google Codelabs). Keep the existing
343
+ // `antigravity/skills/` canonical prefix, now nested-with-SKILL.md.
315
344
  pathPrefix: 'antigravity/skills/',
316
- extensions: ['.md'],
317
- nested: false,
318
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
319
- canonicalFilename: (slug) => `antigravity/skills/${slug}.md`,
320
- scanPaths: (homeDir) => [path.join(homeDir, '.gemini', 'antigravity', 'skills')],
345
+ extensions: ['/SKILL.md'],
346
+ nested: true,
347
+ slugFor: (abs, root) => {
348
+ const rel = path.relative(root, abs).split(path.sep).join('/');
349
+ if (!rel.endsWith('/SKILL.md'))
350
+ return null;
351
+ const parts = rel.split('/');
352
+ if (parts.length < 2)
353
+ return null;
354
+ return parts[parts.length - 2] ?? null;
355
+ },
356
+ canonicalFilename: (slug) => `antigravity/skills/${slug}/SKILL.md`,
357
+ // Global ~/.gemini/antigravity/skills always; project <proj>/.agent/skills
358
+ // only inside a git repo.
359
+ scanPaths: (homeDir, projectRoot, isGitRepo) => {
360
+ const paths = [path.join(homeDir, '.gemini', 'antigravity', 'skills')];
361
+ if (isGitRepo)
362
+ paths.push(path.join(projectRoot, '.agent', 'skills'));
363
+ return paths;
364
+ },
365
+ includeAdjacent: true,
366
+ skillRoot: (abs) => path.dirname(abs),
367
+ canonicalAdjacentFilename: (slug, relPath) => `antigravity/skills/${slug}/${relPath}`,
321
368
  },
322
369
  opencode: {
323
- pathPrefix: 'opencode/instructions/',
324
- extensions: ['.md'],
325
- nested: false,
326
- slugFor: (abs) => path.basename(abs).replace(/\.md$/, '') || null,
327
- canonicalFilename: (slug) => `opencode/instructions/${slug}.md`,
328
- scanPaths: (homeDir) => [path.join(homeDir, '.config', 'opencode', 'instructions')],
370
+ // OpenCode skills are nested SKILL.md directories, mirroring claude:
371
+ // ~/.config/opencode/skills/<slug>/SKILL.md (global) and
372
+ // <proj>/.opencode/skills/<slug>/SKILL.md (project)
373
+ // (opencode.ai/docs/skills).
374
+ pathPrefix: 'opencode/',
375
+ extensions: ['/SKILL.md'],
376
+ nested: true,
377
+ slugFor: (abs, root) => {
378
+ const rel = path.relative(root, abs).split(path.sep).join('/');
379
+ if (!rel.endsWith('/SKILL.md'))
380
+ return null;
381
+ const parts = rel.split('/');
382
+ if (parts.length < 2)
383
+ return null;
384
+ return parts[parts.length - 2] ?? null;
385
+ },
386
+ canonicalFilename: (slug) => `opencode/${slug}/SKILL.md`,
387
+ // Global ~/.config/opencode/skills always; project <proj>/.opencode/skills
388
+ // only inside a git repo.
389
+ scanPaths: (homeDir, projectRoot, isGitRepo) => {
390
+ const paths = [path.join(homeDir, '.config', 'opencode', 'skills')];
391
+ if (isGitRepo)
392
+ paths.push(path.join(projectRoot, '.opencode', 'skills'));
393
+ return paths;
394
+ },
395
+ includeAdjacent: true,
396
+ skillRoot: (abs) => path.dirname(abs),
397
+ canonicalAdjacentFilename: (slug, relPath) => `opencode/${slug}/${relPath}`,
329
398
  },
330
399
  };
331
400
  export const SUPPORTED_TOOLS = Object.keys(DETECTORS);
@@ -452,15 +521,72 @@ function groupDiscoveredIntoSkills(discovered) {
452
521
  }
453
522
  return [...grouped.values()];
454
523
  }
524
+ /** Narrow an unknown ApiError body to the ingest 409 `{ conflicts }` shape. */
525
+ function parseConflicts(body) {
526
+ if (typeof body !== 'object' || body === null)
527
+ return null;
528
+ const conflicts = body.conflicts;
529
+ if (typeof conflicts !== 'object' || conflicts === null)
530
+ return null;
531
+ const { replaceable, blocking } = conflicts;
532
+ if (!Array.isArray(replaceable) || !Array.isArray(blocking))
533
+ return null;
534
+ return {
535
+ replaceable: replaceable.filter((s) => typeof s === 'string'),
536
+ blocking: blocking.filter((s) => typeof s === 'string'),
537
+ };
538
+ }
539
+ /** Render the delete-hint ref for a slug. Uses the logged-in username when
540
+ * it's readily available (so the user can copy-paste `botdocs delete
541
+ * @me/slug`); falls back to the bare slug otherwise. */
542
+ function deleteRef(slug) {
543
+ const username = loadAuth()?.username;
544
+ return username ? `@${username}/${slug}` : slug;
545
+ }
546
+ /** Print an actionable 409 message and exit non-zero. Honors `--json` by
547
+ * emitting the structured `{ ok: false, conflicts }` shape instead of prose. */
548
+ function reportConflicts(conflicts, options) {
549
+ if (options.json) {
550
+ console.log(JSON.stringify({ ok: false, conflicts }));
551
+ process.exit(1);
552
+ }
553
+ if (conflicts.blocking.length > 0) {
554
+ const refs = conflicts.blocking.map((s) => `botdocs delete ${deleteRef(s)}`).join(', ');
555
+ console.error(`\n ✗ These skills are already published and won't be replaced: ${conflicts.blocking.join(', ')}. ` +
556
+ `Unpublish or delete them first (${refs}), then re-ingest.`);
557
+ }
558
+ if (conflicts.replaceable.length > 0) {
559
+ console.error(`\n ✗ These skills already exist as drafts: ${conflicts.replaceable.join(', ')}. ` +
560
+ `Re-run with --force to replace them, or delete them first.`);
561
+ }
562
+ console.error('');
563
+ process.exit(1);
564
+ }
455
565
  /** POST the grouped skills to the ingest endpoint. Honors `--json` (emit raw
456
- * response) and the default human-friendly "drafts created" line. Returns
457
- * nothing on success. */
566
+ * response) and the default human-friendly "drafts created" line. On a 409
567
+ * slug-conflict, prints an actionable message and exits non-zero rather than
568
+ * letting the ApiError bubble up as a stack trace. Returns nothing on success. */
458
569
  async function uploadSkills(skills, options) {
459
- const result = await apiFetch('/api/cli/ingest', {
460
- method: 'POST',
461
- auth: true,
462
- body: { skills, bundle: options.bundle ? { name: options.bundle } : undefined },
463
- });
570
+ let result;
571
+ try {
572
+ result = await apiFetch('/api/cli/ingest', {
573
+ method: 'POST',
574
+ auth: true,
575
+ body: {
576
+ skills,
577
+ bundle: options.bundle ? { name: options.bundle } : undefined,
578
+ force: options.force,
579
+ },
580
+ });
581
+ }
582
+ catch (err) {
583
+ if (err instanceof ApiError && err.status === 409) {
584
+ const conflicts = parseConflicts(err.body);
585
+ if (conflicts)
586
+ reportConflicts(conflicts, options);
587
+ }
588
+ throw err;
589
+ }
464
590
  if (options.json) {
465
591
  console.log(JSON.stringify(result));
466
592
  return;
package/dist/index.js CHANGED
@@ -106,7 +106,7 @@ program
106
106
  });
107
107
  program
108
108
  .command('install <ref>')
109
- .description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, .codex/skills/, .github/instructions/, etc.)')
109
+ .description('Install a skill or bundle locally (skills go to ~/.claude/skills/, project files to .cursor/rules/, .github/instructions/, .windsurf/rules/, etc.)')
110
110
  .option('--project <dir>', 'Override the project root used for project-local files')
111
111
  .option('--flat', 'Skip the {scope} subdirectory in install paths (collision-prone, not recommended)')
112
112
  .option('--clean', 'Wipe-and-reinstall instead of additive')
@@ -160,6 +160,7 @@ program
160
160
  .option('--dry-run', 'Show what would be ingested without uploading')
161
161
  .option('--from-tool <ecosystem>', `Treat every file in the path as belonging to a single ecosystem (one of: ${INGEST_SUPPORTED_TOOLS.join(', ')}). Useful when ingesting directly from ~/.claude/commands/, .cursor/rules/, etc.`)
162
162
  .option('--auto', 'Skip the interactive selection and ingest everything discovery finds (zero-arg mode only)')
163
+ .option('--force', 'Replace existing draft skills with the same name (won\'t touch published skills)')
163
164
  .option('--no-ink', 'Disable the interactive TUI; use plain output (zero-arg mode only)')
164
165
  .action(async (sourcePath, opts) => {
165
166
  // Commander's --no-ink convention sets opts.ink = false; flip to noInk
package/dist/lib/api.d.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  export declare function getApiUrl(): string;
2
2
  /** Thrown by apiFetch when the server returns a non-2xx response. Carries the
3
3
  * status code and the server-provided message so callers can branch on
4
- * different HTTP error codes (404 vs 403 vs 5xx) with friendly hints. */
4
+ * different HTTP error codes (404 vs 403 vs 5xx) with friendly hints.
5
+ *
6
+ * `body` holds the parsed JSON error payload when the server returned one
7
+ * (e.g. ingest's `{ error, conflicts }`). It's `undefined` when the response
8
+ * body wasn't JSON. Callers that need structured fields (not just the
9
+ * `message` string) read it — most callers ignore it and the existing
10
+ * `status` + `message` branching keeps working unchanged. */
5
11
  export declare class ApiError extends Error {
6
12
  readonly status: number;
7
- constructor(status: number, message: string);
13
+ readonly body?: unknown;
14
+ constructor(status: number, message: string, body?: unknown);
8
15
  }
9
16
  interface FetchOptions {
10
17
  method?: string;
package/dist/lib/api.js CHANGED
@@ -5,13 +5,21 @@ export function getApiUrl() {
5
5
  }
6
6
  /** Thrown by apiFetch when the server returns a non-2xx response. Carries the
7
7
  * status code and the server-provided message so callers can branch on
8
- * different HTTP error codes (404 vs 403 vs 5xx) with friendly hints. */
8
+ * different HTTP error codes (404 vs 403 vs 5xx) with friendly hints.
9
+ *
10
+ * `body` holds the parsed JSON error payload when the server returned one
11
+ * (e.g. ingest's `{ error, conflicts }`). It's `undefined` when the response
12
+ * body wasn't JSON. Callers that need structured fields (not just the
13
+ * `message` string) read it — most callers ignore it and the existing
14
+ * `status` + `message` branching keeps working unchanged. */
9
15
  export class ApiError extends Error {
10
16
  status;
11
- constructor(status, message) {
17
+ body;
18
+ constructor(status, message, body) {
12
19
  super(message);
13
20
  this.name = 'ApiError';
14
21
  this.status = status;
22
+ this.body = body;
15
23
  }
16
24
  }
17
25
  export async function apiFetch(path, options = {}) {
@@ -42,8 +50,13 @@ export async function apiFetch(path, options = {}) {
42
50
  if (!response.ok) {
43
51
  const text = await response.text();
44
52
  let message;
53
+ // `parsedBody` is the structured error payload when the server sent JSON
54
+ // (e.g. ingest's `{ error, conflicts }`). Left undefined for non-JSON
55
+ // bodies so callers can distinguish "no structured data" from "{}".
56
+ let parsedBody;
45
57
  try {
46
58
  const json = JSON.parse(text);
59
+ parsedBody = json;
47
60
  message = json.error || text;
48
61
  }
49
62
  catch {
@@ -54,7 +67,7 @@ export async function apiFetch(path, options = {}) {
54
67
  if (response.status === 401 && auth) {
55
68
  message = 'Authentication failed. Run `botdocs login` to sign in again.';
56
69
  }
57
- throw new ApiError(response.status, message);
70
+ throw new ApiError(response.status, message, parsedBody);
58
71
  }
59
72
  const contentType = response.headers.get('content-type') || '';
60
73
  if (contentType.includes('application/json')) {
@@ -47,10 +47,24 @@ export function detectDestination(srcRelative, ctx) {
47
47
  };
48
48
  }
49
49
  if (src.startsWith('codex/')) {
50
- return {
51
- kind: 'project',
52
- dest: path.join(ctx.projectDir, '.codex', 'skills', path.basename(src)),
53
- };
50
+ // Codex skills are nested SKILL.md directories at
51
+ // ~/.codex/skills/<slug>/SKILL.md (developers.openai.com/codex/skills).
52
+ // New canonical form: codex/<slug>/SKILL.md (+ adjacent files). Strip the
53
+ // `codex/<slug>/` prefix and keep the relpath under ~/.codex/skills/<slug>/.
54
+ //
55
+ // Backward-compat: BotDocs published before this fix stored flat
56
+ // `codex/<slug>.md`. We detect the flat form (no inner slash) and route it
57
+ // to the new nested destination ~/.codex/skills/<slug>/SKILL.md so already-
58
+ // published docs still install where Codex reads them.
59
+ const remainder = src.slice('codex/'.length);
60
+ const codexBase = path.join(ctx.homeDir, '.codex', 'skills');
61
+ if (!remainder.includes('/')) {
62
+ // Flat legacy form `codex/<slug>.md` → nested SKILL.md.
63
+ const slug = remainder.replace(/\.md$/, '');
64
+ return { kind: 'global', dest: path.join(codexBase, slug, 'SKILL.md') };
65
+ }
66
+ const finalName = remainder.replace(/^[^/]+\//, '');
67
+ return { kind: 'global', dest: path.join(codexBase, ctx.slug, finalName) };
54
68
  }
55
69
  if (src.startsWith('copilot/instructions/')) {
56
70
  // GitHub Copilot custom instructions live in .github/instructions/
@@ -61,36 +75,66 @@ export function detectDestination(srcRelative, ctx) {
61
75
  };
62
76
  }
63
77
  if (src.startsWith('windsurf/rules/')) {
64
- // Windsurf (Codeium) reads project rules from .codeium/windsurf-rules/
65
- // (https://docs.codeium.com/windsurf/cascade#windsurfrules).
78
+ // Windsurf reads project rules from <proj>/.windsurf/rules/<slug>.md
79
+ // (docs.windsurf.com). Flat .md rule, project-scoped. The canonical form
80
+ // is already flat, so basename() is the right leaf either way.
66
81
  return {
67
82
  kind: 'project',
68
- dest: path.join(ctx.projectDir, '.codeium', 'windsurf-rules', path.basename(src)),
83
+ dest: path.join(ctx.projectDir, '.windsurf', 'rules', path.basename(src)),
69
84
  };
70
85
  }
71
- if (src.startsWith('gemini/instructions/')) {
72
- // Gemini CLI reads global instructions from ~/.gemini/instructions/
73
- // (https://github.com/google-gemini/gemini-cli).
74
- return {
75
- kind: 'global',
76
- dest: path.join(ctx.homeDir, '.gemini', 'instructions', path.basename(src)),
77
- };
86
+ if (src.startsWith('gemini/')) {
87
+ // Gemini CLI has NO per-skill file directory — it uses hierarchical
88
+ // GEMINI.md context files (~/.gemini/GEMINI.md global, ./GEMINI.md
89
+ // project). There's no real path to write to, so route to `manual` (like
90
+ // chatgpt): install surfaces the content for the user to paste into their
91
+ // GEMINI.md or @import it, rather than fabricating ~/.gemini/instructions/.
92
+ return { kind: 'manual', dest: src };
78
93
  }
79
94
  if (src.startsWith('antigravity/skills/')) {
80
- // Google Antigravity reads skills from ~/.gemini/antigravity/skills/
81
- // (shares the gemini config tree).
82
- return {
83
- kind: 'global',
84
- dest: path.join(ctx.homeDir, '.gemini', 'antigravity', 'skills', path.basename(src)),
85
- };
95
+ // Antigravity skills are nested SKILL.md directories
96
+ // (antigravity.google/docs/skills). Project: <proj>/.agent/skills/<slug>/…
97
+ // Global mirror: ~/.gemini/antigravity/skills/<slug>/… We prefer the
98
+ // project destination when a project dir applies (matching cursor/codex-
99
+ // commands which default to project).
100
+ //
101
+ // New canonical form: antigravity/skills/<slug>/SKILL.md (+ adjacent).
102
+ // Strip the `antigravity/skills/<slug>/` prefix, keep the relpath.
103
+ //
104
+ // Backward-compat: docs published before this fix stored flat
105
+ // `antigravity/skills/<slug>.md`. Detect the flat form and route it to the
106
+ // new nested destination so already-published docs still install.
107
+ const remainder = src.slice('antigravity/skills/'.length);
108
+ const agentBase = path.join(ctx.projectDir, '.agent', 'skills');
109
+ if (!remainder.includes('/')) {
110
+ const slug = remainder.replace(/\.md$/, '');
111
+ return { kind: 'project', dest: path.join(agentBase, slug, 'SKILL.md') };
112
+ }
113
+ const finalName = remainder.replace(/^[^/]+\//, '');
114
+ return { kind: 'project', dest: path.join(agentBase, ctx.slug, finalName) };
86
115
  }
87
- if (src.startsWith('opencode/instructions/')) {
88
- // OpenCode (SST) reads instructions from ~/.config/opencode/instructions/
89
- // (https://github.com/sst/opencode).
90
- return {
91
- kind: 'global',
92
- dest: path.join(ctx.homeDir, '.config', 'opencode', 'instructions', path.basename(src)),
93
- };
116
+ if (src.startsWith('opencode/')) {
117
+ // OpenCode skills are nested SKILL.md directories (opencode.ai/docs/skills).
118
+ // Project: <proj>/.opencode/skills/<slug>/… Global mirror:
119
+ // ~/.config/opencode/skills/<slug>/… We prefer the project destination
120
+ // when a project dir applies (matching cursor/codex-commands).
121
+ //
122
+ // New canonical form: opencode/<slug>/SKILL.md (+ adjacent). Strip the
123
+ // `opencode/<slug>/` prefix, keep the relpath.
124
+ //
125
+ // Backward-compat: docs published before this fix stored flat
126
+ // `opencode/instructions/<slug>.md`. Detect that legacy prefix and route
127
+ // it to the new nested destination so already-published docs still install.
128
+ const opencodeBase = path.join(ctx.projectDir, '.opencode', 'skills');
129
+ if (src.startsWith('opencode/instructions/')) {
130
+ const slug = path.basename(src).replace(/\.md$/, '');
131
+ return { kind: 'project', dest: path.join(opencodeBase, slug, 'SKILL.md') };
132
+ }
133
+ const remainder = src.slice('opencode/'.length);
134
+ const finalName = remainder.includes('/')
135
+ ? remainder.replace(/^[^/]+\//, '')
136
+ : remainder;
137
+ return { kind: 'project', dest: path.join(opencodeBase, ctx.slug, finalName) };
94
138
  }
95
139
  if (src.startsWith('chatgpt/')) {
96
140
  return { kind: 'manual', dest: src };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botdocs/cli",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
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",