@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.
- package/dist/commands/ingest.d.ts +4 -0
- package/dist/commands/ingest.js +152 -26
- package/dist/index.js +2 -1
- package/dist/lib/api.d.ts +9 -2
- package/dist/lib/api.js +16 -3
- package/dist/lib/auto-detect.js +71 -27
- package/package.json +1 -1
|
@@ -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;
|
package/dist/commands/ingest.js
CHANGED
|
@@ -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:
|
|
285
|
-
slugFor: (abs) =>
|
|
286
|
-
|
|
287
|
-
|
|
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, '.
|
|
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
|
-
|
|
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:
|
|
318
|
-
slugFor: (abs) =>
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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.
|
|
457
|
-
*
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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/, .
|
|
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
|
-
|
|
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
|
-
|
|
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')) {
|
package/dist/lib/auto-detect.js
CHANGED
|
@@ -47,10 +47,24 @@ export function detectDestination(srcRelative, ctx) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
if (src.startsWith('codex/')) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
65
|
-
// (
|
|
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, '.
|
|
83
|
+
dest: path.join(ctx.projectDir, '.windsurf', 'rules', path.basename(src)),
|
|
69
84
|
};
|
|
70
85
|
}
|
|
71
|
-
if (src.startsWith('gemini/
|
|
72
|
-
// Gemini CLI
|
|
73
|
-
// (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
81
|
-
// (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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/
|
|
88
|
-
// OpenCode
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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",
|