@botdocs/cli 0.10.2 → 0.11.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 +7 -5
- package/bin/botdocs.cjs +78 -0
- package/dist/commands/check-updates.d.ts +22 -0
- package/dist/commands/check-updates.js +73 -18
- package/dist/commands/edit.js +10 -2
- package/dist/commands/ingest.d.ts +20 -0
- package/dist/commands/ingest.js +264 -11
- package/dist/commands/init.d.ts +24 -0
- package/dist/commands/init.js +43 -6
- package/dist/commands/install.js +146 -5
- package/dist/commands/list.js +62 -0
- package/dist/commands/login.js +56 -2
- package/dist/commands/publish.d.ts +30 -3
- package/dist/commands/publish.js +353 -40
- package/dist/commands/sync.js +252 -40
- package/dist/commands/uninstall.js +12 -0
- package/dist/commands/validate.js +82 -8
- package/dist/index.js +152 -6
- package/dist/lib/api.d.ts +72 -2
- package/dist/lib/api.js +204 -11
- package/dist/lib/auto-detect.js +70 -30
- package/dist/lib/config.d.ts +15 -0
- package/dist/lib/config.js +83 -2
- package/dist/lib/ingest-session-client.d.ts +93 -0
- package/dist/lib/ingest-session-client.js +217 -0
- package/dist/lib/lockfile.d.ts +13 -0
- package/dist/lib/manifest.d.ts +12 -0
- package/dist/lib/manifest.js +29 -2
- package/dist/lib/node-preflight.d.ts +20 -0
- package/dist/lib/node-preflight.js +11 -0
- package/dist/lib/skill-caps.d.ts +17 -0
- package/dist/lib/skill-caps.js +19 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,6 +10,8 @@ publish your own.
|
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
|
+
**Requires Node.js 20+.** [Don't have it? Install from nodejs.org →](https://nodejs.org)
|
|
14
|
+
|
|
13
15
|
```bash
|
|
14
16
|
# one-off
|
|
15
17
|
npx @botdocs/cli <command>
|
|
@@ -26,8 +28,6 @@ After a global install the binary is just `botdocs`:
|
|
|
26
28
|
botdocs --help
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
Requires Node.js 20 or newer.
|
|
30
|
-
|
|
31
31
|
## Quick start
|
|
32
32
|
|
|
33
33
|
```bash
|
|
@@ -50,7 +50,7 @@ botdocs publish my-skill/
|
|
|
50
50
|
|
|
51
51
|
| Command | Purpose |
|
|
52
52
|
|---|---|
|
|
53
|
-
| `init [name]` | Scaffold a new skill directory (`--
|
|
53
|
+
| `init [name]` | Scaffold a new skill directory (canonical multi-ecosystem layout by default; `--spec` for the legacy SPEC scaffold). |
|
|
54
54
|
| `compile <path>` | Generate per-ecosystem skill drafts from a canonical source (BYOK). |
|
|
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. |
|
|
@@ -164,8 +164,10 @@ your machine.
|
|
|
164
164
|
```bash
|
|
165
165
|
export BOTDOCS_ANTHROPIC_KEY=sk-ant-... # or BOTDOCS_OPENAI_KEY=sk-...
|
|
166
166
|
|
|
167
|
-
botdocs init my-skill
|
|
168
|
-
# + ecosystems list
|
|
167
|
+
botdocs init my-skill # canonical scaffold by default:
|
|
168
|
+
# claude-code source + ecosystems list
|
|
169
|
+
# in botdocs.json
|
|
170
|
+
|
|
169
171
|
# edit claude-code/commands/my-skill.md
|
|
170
172
|
|
|
171
173
|
botdocs compile my-skill/ # generates claude/SKILL.md,
|
package/bin/botdocs.cjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CommonJS preflight shim.
|
|
6
|
+
*
|
|
7
|
+
* This file is the actual `bin` entry — `package.json#bin.botdocs` points
|
|
8
|
+
* here, not at the ESM `dist/index.js`. The shim's only job is to gate the
|
|
9
|
+
* Node version BEFORE the ESM entry is loaded.
|
|
10
|
+
*
|
|
11
|
+
* Why this exists:
|
|
12
|
+
* The previous preflight at the top of `src/index.ts` was structurally
|
|
13
|
+
* defeated by ESM import hoisting. `import { checkNodeVersion } from …`
|
|
14
|
+
* syntactically runs before the `{ … process.exit(1) }` block, but Node
|
|
15
|
+
* evaluates the WHOLE module graph (every `import` in `index.ts` AND
|
|
16
|
+
* every transitively-imported file) before any top-level statement
|
|
17
|
+
* executes. On Node 16/18 that means a `SyntaxError` from any modern-
|
|
18
|
+
* syntax dep (commander, Ink, the openai SDK, etc.) fires first, and
|
|
19
|
+
* the user never sees our friendly "install Node 20" line.
|
|
20
|
+
*
|
|
21
|
+
* A CJS shim sidesteps the problem entirely: CommonJS evaluates this
|
|
22
|
+
* file synchronously, top-to-bottom, with NO module graph eagerly
|
|
23
|
+
* loaded. We can read `process.versions.node`, print a stderr line,
|
|
24
|
+
* and exit cleanly before requiring `dist/index.js`. The only globals
|
|
25
|
+
* used here (`process`, `String.prototype.split`, `parseInt`) exist on
|
|
26
|
+
* every Node version, including the legacy ones we're trying to reject.
|
|
27
|
+
*
|
|
28
|
+
* Keep this file LANGUAGE-MINIMAL — no optional chaining, no nullish
|
|
29
|
+
* coalescing, no `const` at top level (technically fine, but staying
|
|
30
|
+
* conservative makes the shim robust against any future runtime quirk
|
|
31
|
+
* on a very old Node). The downstream ESM code can use whatever it wants.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
var MIN_NODE_MAJOR = 20;
|
|
35
|
+
|
|
36
|
+
function preflight() {
|
|
37
|
+
var version = process.versions && process.versions.node ? String(process.versions.node) : '';
|
|
38
|
+
var firstSegment = version.split('.')[0] || '0';
|
|
39
|
+
var major = parseInt(firstSegment, 10);
|
|
40
|
+
if (!isFinite(major) || major < MIN_NODE_MAJOR) {
|
|
41
|
+
process.stderr.write(
|
|
42
|
+
'BotDocs CLI requires Node.js ' +
|
|
43
|
+
MIN_NODE_MAJOR +
|
|
44
|
+
" or newer. You're on Node " +
|
|
45
|
+
version +
|
|
46
|
+
'. Install from https://nodejs.org\n',
|
|
47
|
+
);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
preflight();
|
|
53
|
+
|
|
54
|
+
// Hand off to the ESM entry. `require()` of an ES module works in Node 22+
|
|
55
|
+
// via `require(esm)`, but for portability across Node 20.x we use a
|
|
56
|
+
// dynamic import — `import()` is available in CJS on every Node ≥ 13.2.
|
|
57
|
+
// The promise rejection is funneled through the same process-wide
|
|
58
|
+
// unhandledRejection handler that `dist/index.js` installs once it loads,
|
|
59
|
+
// so we don't need a local catch here.
|
|
60
|
+
//
|
|
61
|
+
// `__dirname` is set in CJS; we resolve the ESM entry relative to this
|
|
62
|
+
// file so the shim works whether it's invoked from the local checkout
|
|
63
|
+
// (`packages/cli/bin/botdocs.cjs` next to `packages/cli/dist/index.js`)
|
|
64
|
+
// or from a published install (`node_modules/@botdocs/cli/bin/...` next
|
|
65
|
+
// to `node_modules/@botdocs/cli/dist/...`).
|
|
66
|
+
var path = require('path');
|
|
67
|
+
var url = require('url');
|
|
68
|
+
var entry = path.join(__dirname, '..', 'dist', 'index.js');
|
|
69
|
+
import(url.pathToFileURL(entry).href).catch(function (err) {
|
|
70
|
+
// Should be unreachable in practice — dist/index.js installs its own
|
|
71
|
+
// unhandledRejection handler the moment it's loaded. This catch only
|
|
72
|
+
// fires if loading the module itself fails (e.g. missing dist/, which
|
|
73
|
+
// would only happen for a broken install).
|
|
74
|
+
process.stderr.write(
|
|
75
|
+
'Failed to load BotDocs CLI entry: ' + (err && err.message ? err.message : String(err)) + '\n',
|
|
76
|
+
);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
});
|
|
@@ -2,5 +2,27 @@ interface CheckUpdatesOptions {
|
|
|
2
2
|
quiet?: boolean;
|
|
3
3
|
json?: boolean;
|
|
4
4
|
}
|
|
5
|
+
export interface UpdateResult {
|
|
6
|
+
/** When false, the server didn't actually check anything — the user
|
|
7
|
+
* hasn't opted in via `botdocs login --sync-library` so there's no
|
|
8
|
+
* snapshot to diff against. Older servers omit this field; the CLI
|
|
9
|
+
* defaults to the legacy "trusted result" interpretation in that case
|
|
10
|
+
* (so a fresh CLI against an older server doesn't regress). */
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
total: number;
|
|
13
|
+
updates: Array<{
|
|
14
|
+
ref: string;
|
|
15
|
+
from: string;
|
|
16
|
+
to: string;
|
|
17
|
+
}>;
|
|
18
|
+
removed: Array<{
|
|
19
|
+
ref: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
/** Fetch the current update result, served from the cache when fresh. Shared
|
|
24
|
+
* with `botdocs list --outdated` so the two commands don't duplicate
|
|
25
|
+
* fingerprint + cache + fetch logic. */
|
|
26
|
+
export declare function fetchUpdates(): Promise<UpdateResult>;
|
|
5
27
|
export declare function checkUpdates(options: CheckUpdatesOptions): Promise<void>;
|
|
6
28
|
export {};
|
|
@@ -3,17 +3,24 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { apiFetch } from '../lib/api.js';
|
|
6
|
+
import { loadAuth } from '../lib/config.js';
|
|
6
7
|
import { loadLockfile } from '../lib/lockfile.js';
|
|
7
8
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
8
9
|
function cachePath() {
|
|
9
10
|
return path.join(os.homedir(), '.botdocs', 'check-updates-cache.json');
|
|
10
11
|
}
|
|
11
|
-
/** Hash of (ref, version) tuples
|
|
12
|
-
* installed set changes (e.g.
|
|
12
|
+
/** Hash of (ref, version) tuples PLUS the current logged-in username —
|
|
13
|
+
* invalidates the cache both when the user's installed set changes (e.g.
|
|
14
|
+
* new install, version bump after sync) AND when the user switches
|
|
15
|
+
* accounts on a shared machine via `botdocs login`. The "anon" sentinel
|
|
16
|
+
* keeps the fingerprint stable across unauthenticated runs (no auth file)
|
|
17
|
+
* so the cache still works for users who haven't logged in. */
|
|
13
18
|
function lockfileFingerprint() {
|
|
14
19
|
const lf = loadLockfile();
|
|
20
|
+
const auth = loadAuth();
|
|
21
|
+
const username = auth?.username ?? 'anon';
|
|
15
22
|
const summary = lf.installs.map((i) => `${i.ref}@${i.version}`).sort().join('\n');
|
|
16
|
-
return createHash('sha256').update(summary).digest('hex').slice(0, 16);
|
|
23
|
+
return createHash('sha256').update(`${username}\n${summary}`).digest('hex').slice(0, 16);
|
|
17
24
|
}
|
|
18
25
|
function loadCache(currentFingerprint) {
|
|
19
26
|
const p = cachePath();
|
|
@@ -36,22 +43,25 @@ function saveCache(fingerprint, result) {
|
|
|
36
43
|
fs.mkdirSync(path.dirname(cachePath()), { recursive: true });
|
|
37
44
|
fs.writeFileSync(cachePath(), JSON.stringify({ cachedAt: new Date().toISOString(), fingerprint, result }, null, 2));
|
|
38
45
|
}
|
|
39
|
-
|
|
46
|
+
/** Fetch the current update result, served from the cache when fresh. Shared
|
|
47
|
+
* with `botdocs list --outdated` so the two commands don't duplicate
|
|
48
|
+
* fingerprint + cache + fetch logic. */
|
|
49
|
+
export async function fetchUpdates() {
|
|
40
50
|
const fingerprint = lockfileFingerprint();
|
|
41
51
|
const cached = loadCache(fingerprint);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (cached)
|
|
53
|
+
return cached;
|
|
54
|
+
// Server reads from user_library.lockfile (set by syncLibrary helper).
|
|
55
|
+
// No body — auth-gated endpoint trusts the server-side snapshot, not the wire.
|
|
56
|
+
const result = await apiFetch('/api/library/check-updates', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
auth: true,
|
|
59
|
+
});
|
|
60
|
+
saveCache(fingerprint, result);
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
export async function checkUpdates(options) {
|
|
64
|
+
const result = await fetchUpdates();
|
|
55
65
|
if (options.json) {
|
|
56
66
|
console.log(JSON.stringify(result));
|
|
57
67
|
return;
|
|
@@ -63,6 +73,19 @@ export async function checkUpdates(options) {
|
|
|
63
73
|
return;
|
|
64
74
|
}
|
|
65
75
|
if (result.total === 0) {
|
|
76
|
+
// When the server reports `enabled: false`, it didn't actually compare
|
|
77
|
+
// anything — the user never opted in to library-sync, so there's no
|
|
78
|
+
// server-side lockfile snapshot to diff against. Don't lie with "all up
|
|
79
|
+
// to date"; surface the opt-in path. `auth.syncLibrary` is also checked
|
|
80
|
+
// as a belt-and-suspenders for older servers that don't return the
|
|
81
|
+
// flag (the local flag is set by `botdocs login --sync-library`).
|
|
82
|
+
const auth = loadAuth();
|
|
83
|
+
const enabled = result.enabled !== false && (auth?.syncLibrary === true || !auth);
|
|
84
|
+
if (!enabled && auth) {
|
|
85
|
+
console.log('\n No library snapshot to check.');
|
|
86
|
+
console.log(' Run `botdocs login --sync-library` to enable check-updates.\n');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
66
89
|
console.log('\n All installed skills + bundles are up to date.\n');
|
|
67
90
|
return;
|
|
68
91
|
}
|
|
@@ -73,5 +96,37 @@ export async function checkUpdates(options) {
|
|
|
73
96
|
for (const r of result.removed) {
|
|
74
97
|
console.log(` ⌀ ${r.ref}: ${r.reason}`);
|
|
75
98
|
}
|
|
76
|
-
|
|
99
|
+
// Branch the suffix on what's actionable. `botdocs sync` does NOT remove
|
|
100
|
+
// entries for removed-upstream refs (it just skips them), so telling the
|
|
101
|
+
// user "sync to apply" when their only diff is removals would do nothing.
|
|
102
|
+
// Surface specific uninstall commands when the list is small enough to
|
|
103
|
+
// print inline.
|
|
104
|
+
const hasUpdates = result.updates.length > 0;
|
|
105
|
+
const hasRemovals = result.removed.length > 0;
|
|
106
|
+
console.log('');
|
|
107
|
+
if (hasUpdates && hasRemovals) {
|
|
108
|
+
console.log(' Run `botdocs sync` to apply updates.');
|
|
109
|
+
if (result.removed.length <= 3) {
|
|
110
|
+
for (const r of result.removed) {
|
|
111
|
+
console.log(` Run \`botdocs uninstall ${r.ref}\` to remove the stale skill.`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.log(' Run `botdocs uninstall <ref>` to remove stale skills.');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (hasUpdates) {
|
|
119
|
+
console.log(' Run `botdocs sync` to apply.');
|
|
120
|
+
}
|
|
121
|
+
else if (hasRemovals) {
|
|
122
|
+
if (result.removed.length <= 3) {
|
|
123
|
+
for (const r of result.removed) {
|
|
124
|
+
console.log(` Run \`botdocs uninstall ${r.ref}\` to clean up.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
console.log(' Run `botdocs uninstall <ref>` to clean up.');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
console.log('');
|
|
77
132
|
}
|
package/dist/commands/edit.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
|
-
import { ApiError, apiFetch, fetchRawContent } from '../lib/api.js';
|
|
5
|
+
import { ApiError, apiFetch, fetchRawContent, friendlyApiErrorDetail } from '../lib/api.js';
|
|
6
6
|
import { complete, detectProvider, LlmError } from '../lib/llm.js';
|
|
7
7
|
import { renderDiff } from '../lib/diff.js';
|
|
8
8
|
const TEMPLATES_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'templates', 'ecosystem-prompts');
|
|
@@ -42,12 +42,20 @@ export async function edit(rawRef, options) {
|
|
|
42
42
|
throw err;
|
|
43
43
|
}
|
|
44
44
|
let manifest;
|
|
45
|
+
const refStr = `@${ref.username}/${ref.slug}`;
|
|
45
46
|
try {
|
|
46
47
|
manifest = await apiFetch(`/api/skills/${ref.username}/${ref.slug}/manifest`);
|
|
47
48
|
}
|
|
48
49
|
catch (err) {
|
|
49
50
|
if (err instanceof ApiError && err.status === 404) {
|
|
50
|
-
console.error(`\n ✗ Skill not found:
|
|
51
|
+
console.error(`\n ✗ Skill not found: ${refStr}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
// Route 403/429/5xx through the shared helper so users get the same
|
|
55
|
+
// wording sync/install give (lost team access, rate-limited, server
|
|
56
|
+
// error) instead of a raw ApiError stack.
|
|
57
|
+
if (err instanceof ApiError) {
|
|
58
|
+
console.error(`\n ✗ ${refStr}: ${friendlyApiErrorDetail(err, refStr)}\n`);
|
|
51
59
|
process.exit(1);
|
|
52
60
|
}
|
|
53
61
|
throw err;
|
|
@@ -15,6 +15,12 @@ interface IngestOptions {
|
|
|
15
15
|
* are never replaced — they surface as a blocking 409 either way. Set via
|
|
16
16
|
* `--force`. */
|
|
17
17
|
force?: boolean;
|
|
18
|
+
/** Pair this ingest run with the web onboarding step so the user can
|
|
19
|
+
* watch progress in their browser. Set via `--pair`. If `--pair-code` is
|
|
20
|
+
* also provided we skip the interactive prompt. */
|
|
21
|
+
pair?: boolean;
|
|
22
|
+
/** Pre-supplied pairing code (BD-XXXXX). Implies `--pair`. */
|
|
23
|
+
pairCode?: string;
|
|
18
24
|
}
|
|
19
25
|
/** Per-file size cap. Any file larger than this is skipped with a warning. */
|
|
20
26
|
export declare const PER_FILE_BYTE_CAP: number;
|
|
@@ -105,5 +111,19 @@ export interface EcosystemDetector {
|
|
|
105
111
|
*/
|
|
106
112
|
export declare const DETECTORS: Record<string, EcosystemDetector>;
|
|
107
113
|
export declare const SUPPORTED_TOOLS: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* Recursively collect absolute file paths under `root`.
|
|
116
|
+
*
|
|
117
|
+
* Symlinks are skipped (with a warning) to prevent local file exfiltration:
|
|
118
|
+
* without this check a directory containing a symlink to e.g. `~/.ssh/id_rsa`
|
|
119
|
+
* would have its target's contents read into memory and uploaded as part of
|
|
120
|
+
* the ingest payload. `entry.isDirectory()` and `entry.isFile()` come from
|
|
121
|
+
* the `Dirent` produced by `withFileTypes: true`, which reports the entry's
|
|
122
|
+
* OWN type (NOT the symlink target's), so the gate is sound. Mirrors
|
|
123
|
+
* `walkAll` in `src/lib/ingest-discover.ts`.
|
|
124
|
+
*
|
|
125
|
+
* Exported for unit testing.
|
|
126
|
+
*/
|
|
127
|
+
export declare function walkFiles(root: string): string[];
|
|
108
128
|
export declare function ingest(rootPath: string | undefined, options: IngestOptions): Promise<void>;
|
|
109
129
|
export {};
|