@bike4mind/cli 3.3.0 → 4.0.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.
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ import { nn as splitSkillMdFrontmatter, p as requireApiUrl, t as ConfigStore, tn as readSkillMdSourceUrl } from "../ConfigStore-BwqsQ0U9.mjs";
3
+ import { r as isReservedCommandName, t as RemoteSkillSource } from "../RemoteSkillSource-qTvRkaNA.mjs";
4
+ import { t as ApiClient } from "../ApiClient-_JrM_xWw.mjs";
5
+ import { promises } from "fs";
6
+ import os from "os";
7
+ import path from "path";
8
+ import { createInterface } from "readline";
9
+ import YAML from "yaml";
10
+ const THIRD_PARTY_LINE = new RegExp(`^\\s*#\\s*Third-party skill from the Bike4Mind Skill Market - review before use`, "m");
11
+ const ARGUMENT_HINT_LINE = /^(argument-hint:[ \t]*)([^\r\n]+?)[ \t]*$/gm;
12
+ function quoteRawArgumentHint(frontmatter) {
13
+ return frontmatter.replace(ARGUMENT_HINT_LINE, (line, key, raw) => {
14
+ if (/^["'|>]/.test(raw)) return line;
15
+ const hint = (raw.startsWith("[") || raw.startsWith("<") ? raw.replace(/([\]>])[ \t]+#.*$/, "$1") : raw.replace(/[ \t]+#.*$/, "")).trimEnd();
16
+ return hint.startsWith("[") || hint.startsWith("<") || hint.includes(": ") ? `${key}${JSON.stringify(hint)}` : line;
17
+ });
18
+ }
19
+ const toText = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : void 0;
20
+ /** Parses a SKILL.md into skill form fields. Never throws; a malformed file yields `ok: false`. */
21
+ function parseSkillMd(input) {
22
+ const text = input.replace(/\r\n?/g, "\n");
23
+ const split = splitSkillMdFrontmatter(text);
24
+ if (!split) {
25
+ if (/^\uFEFF?---/.test(text)) return {
26
+ ok: false,
27
+ error: "The frontmatter block has no closing --- line."
28
+ };
29
+ return {
30
+ ok: true,
31
+ fields: { body: stripBodyNewlines(text.replace(/^\uFEFF/, "")) },
32
+ dropped: []
33
+ };
34
+ }
35
+ if (split.lang && split.lang !== "yaml" && split.lang !== "yml") return {
36
+ ok: false,
37
+ error: `Unsupported frontmatter language "${split.lang}"; only YAML is supported.`
38
+ };
39
+ let data;
40
+ try {
41
+ const frontmatter = quoteRawArgumentHint(split.frontmatter);
42
+ data = YAML.parse(frontmatter) ?? {};
43
+ } catch (error) {
44
+ return {
45
+ ok: false,
46
+ error: `Invalid YAML frontmatter: ${error instanceof Error ? error.message : String(error)}`
47
+ };
48
+ }
49
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return {
50
+ ok: false,
51
+ error: "The frontmatter must be a set of key: value pairs."
52
+ };
53
+ const fields = { body: stripBodyNewlines(split.rest) };
54
+ const dropped = [];
55
+ for (const [key, value] of Object.entries(data)) switch (key) {
56
+ case "name":
57
+ fields.name = toText(value);
58
+ break;
59
+ case "description":
60
+ fields.description = toText(value);
61
+ break;
62
+ case "argument-hint":
63
+ fields.argumentHint = toText(value);
64
+ break;
65
+ case "disable-model-invocation":
66
+ fields.disableModelInvocation = fields.disableModelInvocation || value === true || value === "true";
67
+ break;
68
+ case "autoinvoke":
69
+ fields.disableModelInvocation = fields.disableModelInvocation || value === false || value === "false";
70
+ break;
71
+ default: dropped.push(key);
72
+ }
73
+ const url = readSkillMdSourceUrl(split.frontmatter);
74
+ return {
75
+ ok: true,
76
+ fields: Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== void 0)),
77
+ dropped,
78
+ ...url !== void 0 && { source: {
79
+ url,
80
+ thirdParty: THIRD_PARTY_LINE.test(split.frontmatter)
81
+ } }
82
+ };
83
+ }
84
+ /** Drops the blank line after the closing fence and the single trailing newline toSkillMd adds. */
85
+ function stripBodyNewlines(rest) {
86
+ return rest.replace(/^\r?\n/, "").replace(/\r?\n$/, "");
87
+ }
88
+ //#endregion
89
+ //#region src/commands/skillsCommand.ts
90
+ /**
91
+ * External skills command (b4m skills push <path>).
92
+ * Imports a Claude Code SKILL.md (or commands/*.md) as the user's own
93
+ * Bike4Mind skill. Runs outside the interactive CLI session. Validation is
94
+ * left to the server (apps/client/server/utils/skillValidation.ts).
95
+ */
96
+ const MAX_LISTED_FILES = 10;
97
+ const DESCRIPTION_MAX = 500;
98
+ /** The server requires a description, but many Claude Code commands/*.md files have none. */
99
+ function descriptionFor(fields) {
100
+ if (fields.description?.trim()) return fields.description;
101
+ return ((fields.body ?? "").split("\n").map((line) => line.replace(/^#+\s*/, "").trim()).find(Boolean) ?? "").slice(0, DESCRIPTION_MAX);
102
+ }
103
+ /**
104
+ * Built from an explicit field list (never a spread of parsed data) so
105
+ * allowedTools can't reach the server: Claude Code tool names don't map to B4M.
106
+ * disableModelInvocation is always sent so an update mirrors the file.
107
+ */
108
+ function buildSkillPayload(fields, fallbackName) {
109
+ return {
110
+ name: (fields.name?.trim() || fallbackName).trim().toLowerCase(),
111
+ description: descriptionFor(fields),
112
+ body: fields.body ?? "",
113
+ ...fields.argumentHint !== void 0 && { argumentHint: fields.argumentHint },
114
+ disableModelInvocation: fields.disableModelInvocation ?? false
115
+ };
116
+ }
117
+ /** Resolves <dir>, <dir>/SKILL.md or any .md file and parses it. */
118
+ async function readSkillFile(input) {
119
+ const expanded = input === "~" || input.startsWith("~/") ? path.join(os.homedir(), input.slice(1)) : input;
120
+ const resolved = path.resolve(expanded);
121
+ let stat;
122
+ try {
123
+ stat = await promises.stat(resolved);
124
+ } catch {
125
+ throw new Error(`No such file or folder: ${input}`);
126
+ }
127
+ let filePath;
128
+ let fallbackName;
129
+ if (stat.isDirectory()) {
130
+ filePath = path.join(resolved, "SKILL.md");
131
+ fallbackName = path.basename(resolved);
132
+ } else if (stat.isFile() && resolved.toLowerCase().endsWith(".md")) {
133
+ filePath = resolved;
134
+ fallbackName = path.basename(resolved).toLowerCase() === "skill.md" ? path.basename(path.dirname(resolved)) : path.basename(resolved, path.extname(resolved));
135
+ } else throw new Error(`Not a skill: ${input} - pass a skill folder, its SKILL.md, or a .md file.`);
136
+ let text;
137
+ try {
138
+ text = await promises.readFile(filePath, "utf8");
139
+ } catch {
140
+ throw new Error(stat.isDirectory() ? "No SKILL.md in that folder." : `Cannot read ${input}`);
141
+ }
142
+ const parsed = parseSkillMd(text);
143
+ if (!parsed.ok) throw new Error(parsed.error);
144
+ const name = path.basename(filePath);
145
+ const extraFiles = name.toLowerCase() === "skill.md" ? (await promises.readdir(path.dirname(filePath))).filter((f) => f !== name && !f.startsWith(".")).sort() : [];
146
+ return {
147
+ payload: buildSkillPayload(parsed.fields, fallbackName),
148
+ dropped: parsed.dropped,
149
+ ...parsed.source && { source: parsed.source },
150
+ extraFiles
151
+ };
152
+ }
153
+ function serverMessage(error) {
154
+ const err = error;
155
+ return err?.response?.data?.error ?? err?.response?.data?.message ?? err?.message ?? String(error);
156
+ }
157
+ /** The stamp is plain comment text, so only a same-origin URL is presented as provenance. */
158
+ function isSameOrigin(url, apiOrigin) {
159
+ try {
160
+ return apiOrigin !== void 0 && new URL(url).origin === new URL(apiOrigin).origin;
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+ /**
166
+ * Prints the notices, applies the third-party and name-conflict gates, then
167
+ * creates or updates the skill. Nothing is sent to the server before the
168
+ * third-party gate passes. Throws on any exit-1 condition.
169
+ */
170
+ async function pushSkill(file, deps) {
171
+ const { payload, dropped, source, extraFiles } = file;
172
+ const { api, log } = deps;
173
+ if (dropped.length > 0) log(`Ignored (not supported in Bike4Mind skills): ${dropped.join(", ")}`);
174
+ if (extraFiles.length > 0) log(`Not uploaded: supporting files (${extraFiles.slice(0, MAX_LISTED_FILES).join(", ")}${extraFiles.length > MAX_LISTED_FILES ? ` and ${extraFiles.length - MAX_LISTED_FILES} more` : ""}) - only SKILL.md is pushed.`);
175
+ if (isReservedCommandName(payload.name)) log(`Note: /${payload.name} is a built-in B4M CLI command, so the CLI won't list this skill (the web still will).`);
176
+ if (source) {
177
+ log(isSameOrigin(source.url, deps.apiOrigin) ? `This skill came from Bike4Mind: ${source.url}` : `This file has a Bike4Mind source stamp from another site: ${source.url}`);
178
+ if (source.thirdParty) {
179
+ log("This is another author's Skill Market skill - import it from the market instead to keep updates and attribution.");
180
+ if (!deps.yes) {
181
+ if (!deps.isTTY) throw new Error("Re-run with --yes to push another author's skill as your own.");
182
+ if (!await deps.confirm("Push it as your own skill anyway? [y/N] ")) return "aborted";
183
+ }
184
+ }
185
+ }
186
+ try {
187
+ const me = await api.getCurrentUser();
188
+ if (!me) throw new Error("Not authenticated. Run `b4m /login` to authenticate.");
189
+ let existing;
190
+ for (let page = 1; !existing; page++) {
191
+ const { data, hasMore } = await api.get("/api/skills", { params: {
192
+ query: payload.name,
193
+ limit: 100,
194
+ page
195
+ } });
196
+ existing = data.find((s) => s.name === payload.name && s.userId === me.id);
197
+ if (!hasMore) break;
198
+ }
199
+ if (!existing) {
200
+ await api.post("/api/skills", payload);
201
+ return "created";
202
+ }
203
+ if (existing.importedFrom) throw new Error(`"${payload.name}" is the name of a Skill Market skill you imported - rename the local skill or delete the import first.`);
204
+ if (!deps.update) {
205
+ if (!deps.isTTY) throw new Error(`A skill named "${payload.name}" already exists - re-run with --update to overwrite it.`);
206
+ if (!await deps.confirm(`A skill named "${payload.name}" already exists. Update it? [y/N] `)) return "aborted";
207
+ }
208
+ await api.put(`/api/skills/${existing.id}`, {
209
+ ...payload,
210
+ argumentHint: payload.argumentHint ?? ""
211
+ });
212
+ return "updated";
213
+ } catch (error) {
214
+ throw new Error(serverMessage(error));
215
+ }
216
+ }
217
+ async function confirm(question) {
218
+ const rl = createInterface({
219
+ input: process.stdin,
220
+ output: process.stdout
221
+ });
222
+ try {
223
+ const answer = await new Promise((resolve) => rl.question(question, resolve));
224
+ return /^y(es)?$/i.test(answer.trim());
225
+ } finally {
226
+ rl.close();
227
+ }
228
+ }
229
+ async function handleSkillsCommand(subcommand, argv) {
230
+ if (subcommand !== "push") throw new Error(`Unknown skills subcommand: ${subcommand ?? "(none)"}`);
231
+ if (!argv.path) throw new Error("Usage: b4m skills push <path>");
232
+ const file = await readSkillFile(argv.path);
233
+ const configStore = new ConfigStore();
234
+ const config = await configStore.load();
235
+ if (!await configStore.getAuthTokens()) throw new Error("Not authenticated. Run `b4m /login` to authenticate.");
236
+ const apiUrl = requireApiUrl(config.apiConfig);
237
+ const apiClient = new ApiClient(apiUrl, configStore);
238
+ const result = await pushSkill(file, {
239
+ api: apiClient,
240
+ confirm,
241
+ isTTY: Boolean(process.stdin.isTTY),
242
+ update: Boolean(argv.update),
243
+ yes: Boolean(argv.yes),
244
+ apiOrigin: apiUrl,
245
+ log: (line) => console.log(line)
246
+ });
247
+ if (result === "aborted") {
248
+ console.log("Aborted");
249
+ return;
250
+ }
251
+ try {
252
+ await new RemoteSkillSource(apiClient).clearCache();
253
+ } catch (error) {
254
+ console.warn(`Skill saved, but the local skills cache could not be cleared: ${serverMessage(error)}`);
255
+ }
256
+ console.log(`${result === "created" ? "Created" : "Updated"} skill /${file.payload.name}`);
257
+ }
258
+ //#endregion
259
+ export { buildSkillPayload, handleSkillsCommand, pushSkill, readSkillFile };
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "../package-DYPVrada.mjs";
2
+ import { t as version } from "../package-CQaam4Dq.mjs";
3
3
  import { a as forceCheckForUpdate, c as isNpmPrefixWritable, d as shouldAttemptAutoUpdate, l as readCachedUpdate, n as REEXEC_GUARD_ENV, o as getAutoUpdatePreference, t as INSTALL_CMD, u as setAutoUpdatePreference } from "../updateChecker-CCLVVdH0.mjs";
4
4
  import { t as checkRipgrep } from "../ripgrepCheck-BmkyTK2i.mjs";
5
5
  import { execSync, spawnSync } from "child_process";
@@ -1,136 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { b as logger, g as DeviceFlowClient, h as AuthenticatedApiClient, t as ConfigStore } from "./ConfigStore-DUATzLxB.mjs";
3
- import { t as version } from "./package-DYPVrada.mjs";
4
- import { isAxiosError } from "axios";
5
- import { Readable } from "node:stream";
6
2
  import { createParser } from "eventsource-parser";
7
- //#region src/auth/OAuthClient.ts
8
- /**
9
- * The CLI's RFC 8628 `client_id`; the desktop app registers under its own. Typed against the
10
- * server allowlist so dropping this id there fails this build rather than this client's logins
11
- * (see OAUTH_DEVICE_CLIENT_IDS).
12
- */
13
- const CLI_OAUTH_CLIENT_ID = "b4m-cli";
14
- /**
15
- * The shared device-flow client, pinned to the CLI's registration.
16
- */
17
- var OAuthClient = class extends DeviceFlowClient {
18
- constructor(baseURL = "http://localhost:3000") {
19
- super({
20
- baseUrl: baseURL,
21
- clientId: CLI_OAUTH_CLIENT_ID
22
- });
23
- }
24
- };
25
- //#endregion
26
- //#region src/auth/providerKeyFailure.ts
27
- const PROVIDER_KEY_ERROR_CODES = /* @__PURE__ */ new Set(["provider_not_configured", "provider_rejected"]);
28
- /**
29
- * A 401 whose body names a missing or rejected AI-provider key (e.g. /api/ai/tts). The
30
- * Bike4Mind credential was accepted, so refreshing or re-authenticating cannot fix it.
31
- * Shared by the ApiClient refresh interceptor and mapApiError in mcp/b4mApiClient.ts.
32
- */
33
- function isProviderKeyFailure(data) {
34
- const body = decodeBinaryBody(data);
35
- if (!body || typeof body !== "object") return false;
36
- const { errorCode } = body;
37
- return typeof errorCode === "string" && PROVIDER_KEY_ERROR_CODES.has(errorCode);
38
- }
39
- /**
40
- * An arraybuffer-typed request (every @bike4mind/sdk call, through ApiClient.fetch) gets its
41
- * error body as raw bytes, and the refresh interceptor sees it before any caller can decode it.
42
- */
43
- function decodeBinaryBody(data) {
44
- const bytes = data instanceof ArrayBuffer ? Buffer.from(data) : ArrayBuffer.isView(data) ? Buffer.from(data.buffer, data.byteOffset, data.byteLength) : void 0;
45
- if (!bytes) return data;
46
- try {
47
- return JSON.parse(bytes.toString("utf8"));
48
- } catch {
49
- return;
50
- }
51
- }
52
- //#endregion
53
- //#region src/auth/ApiClient.ts
54
- const USER_AGENT = `b4m-cli/${version}`;
55
- /**
56
- * Per-request timeout. Generous by design: a waited chat (POST /api/chat with wait:true)
57
- * runs a full server-side quest, so the bound only exists to stop a hung backend from
58
- * wedging a caller forever, not to cap a normal long quest. Override with B4M_API_TIMEOUT_MS
59
- * (0 disables the timeout entirely).
60
- */
61
- const DEFAULT_API_TIMEOUT_MS = 6e5;
62
- function resolveTimeoutMs() {
63
- const raw = process.env.B4M_API_TIMEOUT_MS;
64
- if (raw === void 0 || raw.trim() === "") return DEFAULT_API_TIMEOUT_MS;
65
- const parsed = Number(raw);
66
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_API_TIMEOUT_MS;
67
- }
68
- /**
69
- * The shared authenticated client, wired to the CLI's storage, logger and branding.
70
- */
71
- var ApiClient = class extends AuthenticatedApiClient {
72
- /**
73
- * @param apiKey - When set, requests authenticate with this instance API key via
74
- * the `x-api-key` header and the OAuth-JWT path (Bearer injection + refresh-on-401)
75
- * is bypassed entirely. Omit to keep the default stored-JWT behavior unchanged.
76
- */
77
- constructor(baseURL = "http://localhost:3000", configStore, apiKey) {
78
- super({
79
- baseUrl: baseURL,
80
- tokenStore: configStore || new ConfigStore(),
81
- oauthClient: new OAuthClient(baseURL),
82
- logger,
83
- apiKey,
84
- timeoutMs: resolveTimeoutMs(),
85
- headers: {
86
- "User-Agent": USER_AGENT,
87
- "X-B4M-Client": USER_AGENT
88
- },
89
- reauthMessages: {
90
- refreshFailed: "Authentication expired. Please run `b4m login` again.",
91
- stillUnauthorized: "Authentication failed. Please run /login to authenticate.",
92
- notLoggedIn: "Authentication failed: not logged in. Please run `b4m login` to authenticate."
93
- },
94
- isPassThrough401: isProviderKeyFailure
95
- });
96
- this.fetch = async (input, init = {}) => {
97
- const headers = Object.fromEntries(new Headers(init.headers));
98
- try {
99
- return toFetchResponse(await this.getAxiosInstance().request({
100
- url: input instanceof Request ? input.url : String(input),
101
- method: init.method ?? "GET",
102
- headers,
103
- data: init.body ?? void 0,
104
- signal: init.signal ?? void 0,
105
- responseType: /text\/event-stream/i.test(headers.accept ?? "") ? "stream" : "arraybuffer",
106
- ...init.redirect && init.redirect !== "follow" ? { maxRedirects: 0 } : {}
107
- }));
108
- } catch (error) {
109
- if (isAxiosError(error) && error.response) return toFetchResponse(error.response);
110
- throw error;
111
- }
112
- };
113
- }
114
- get baseURL() {
115
- return this.getAxiosInstance().defaults.baseURL ?? "";
116
- }
117
- };
118
- function toFetchResponse(response) {
119
- const headers = new Headers();
120
- for (const [name, value] of Object.entries(response.headers)) if (value !== void 0 && value !== null) headers.set(name, Array.isArray(value) ? value.join(", ") : String(value));
121
- const body = [
122
- 101,
123
- 204,
124
- 205,
125
- 304
126
- ].includes(response.status) ? null : response.data instanceof Readable ? Readable.toWeb(response.data) : response.data;
127
- return new Response(body, {
128
- status: response.status,
129
- statusText: response.statusText,
130
- headers
131
- });
132
- }
133
- //#endregion
134
3
  //#region ../sdk/dist/index.mjs
135
4
  /**
136
5
  * A non-2xx response. `message` is the envelope's `error` (`ErrorResponse` in the spec); branch on `status` and
@@ -255,6 +124,10 @@ const operations = {
255
124
  method: "POST",
256
125
  path: "/api/v1/sessions"
257
126
  },
127
+ createTextRefinement: {
128
+ method: "POST",
129
+ path: "/api/v1/text-refinements"
130
+ },
258
131
  createTranscription: {
259
132
  method: "POST",
260
133
  path: "/api/v1/transcriptions"
@@ -331,6 +204,14 @@ const operations = {
331
204
  method: "GET",
332
205
  path: "/api/v1/artifacts/{id}/versions/{version}"
333
206
  },
207
+ getClaudeMarketplace: {
208
+ method: "GET",
209
+ path: "/api/v1/skills/market/claude-marketplace.json"
210
+ },
211
+ getClaudePluginZip: {
212
+ method: "GET",
213
+ path: "/api/v1/skills/market/{id}/claude-plugin.zip"
214
+ },
334
215
  getCreditBalance: {
335
216
  method: "GET",
336
217
  path: "/api/v1/credits"
@@ -371,6 +252,10 @@ const operations = {
371
252
  method: "GET",
372
253
  path: "/api/v1/video-generations/{id}"
373
254
  },
255
+ getWhatsNewMedia: {
256
+ method: "GET",
257
+ path: "/api/v1/whats-new/media/{id}/{asset}"
258
+ },
374
259
  listAgents: {
375
260
  method: "GET",
376
261
  path: "/api/v1/agents"
@@ -736,4 +621,4 @@ function toBase64(bytes) {
736
621
  return btoa(binary);
737
622
  }
738
623
  //#endregion
739
- export { ApiClient as a, parseRetryAfterSeconds as i, B4mStreamEndedError as n, isProviderKeyFailure as o, createClient as r, OAuthClient as s, B4mApiError as t };
624
+ export { parseRetryAfterSeconds as i, B4mStreamEndedError as n, createClient as r, B4mApiError as t };