@bike4mind/cli 1.2.0 → 2.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.
- package/README.md +3 -1
- package/dist/{AgentHistoryStore-B2NEOvSW.mjs → AgentHistoryStore-DIMzvf-y.mjs} +460 -274
- package/dist/{ApiClient-D0fQ2FT6.mjs → ApiClient-BTI0-m0g.mjs} +44 -5
- package/dist/{ConfigStore-qV7NrCgZ.mjs → ConfigStore-DIs-K9oD.mjs} +3205 -1078
- package/dist/{buildAgent-C-C-VGff.mjs → buildAgent-DOvEp92a.mjs} +115 -10
- package/dist/commands/acpCommand.mjs +5 -4
- package/dist/commands/apiCommand.mjs +1 -1
- package/dist/commands/doctorCommand.mjs +1 -1
- package/dist/commands/envCommand.mjs +1 -1
- package/dist/commands/headlessCommand.mjs +4 -3
- package/dist/commands/mcpCommand.mjs +3 -3
- package/dist/commands/pluginCommand.mjs +1 -1
- package/dist/commands/updateCommand.mjs +1 -1
- package/dist/index.mjs +522 -34
- package/dist/{package-CGZIoxcs.mjs → package-Dl_F2Fah.mjs} +1 -1
- package/dist/{serve-CPXqcEZr.mjs → serve-Y2zMbfGs.mjs} +237 -73
- package/package.json +11 -11
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { t as ApiClient } from "./ApiClient-
|
|
2
|
+
import { R as PROMPT_TEXT_MAX, Y as generatedAudioResponseSchema, _ as requireApiUrl, b as AGENT_QUEST_MANIFEST, dt as ttsBase64ResponseSchema, ft as ttsRequestSchema, g as parseApiUrl, k as DEFAULT_TTS_PROVIDER, n as logger, pt as ttsResponseTooLargeSchema, t as ConfigStore, ut as supportedVoiceGenerationVendor, x as AGENT_QUEST_MCP_URI, y as AGENT_QUEST_ID } from "./ConfigStore-DIs-K9oD.mjs";
|
|
3
|
+
import { n as NotAuthenticatedError, r as isProviderKeyFailure, t as ApiClient } from "./ApiClient-BTI0-m0g.mjs";
|
|
4
4
|
import { z as z$1 } from "zod";
|
|
5
5
|
import { isAxiosError } from "axios";
|
|
6
6
|
import { Writable } from "node:stream";
|
|
@@ -46,12 +46,27 @@ var B4mApiClient = class {
|
|
|
46
46
|
async createNotebook(args) {
|
|
47
47
|
return this.client.post("/api/sessions/create", {
|
|
48
48
|
...args.name ? { name: args.name } : {},
|
|
49
|
-
...args.projectId ? { projectId: args.projectId } : {}
|
|
49
|
+
...args.projectId ? { projectId: args.projectId } : {},
|
|
50
|
+
...args.dataLakeId ? { dataLakeId: args.dataLakeId } : {}
|
|
50
51
|
});
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* GET /api/v1/data-lakes is cursor-paginated (flat `limit`/`cursor` params,
|
|
55
|
+
* `{ data, next_cursor }` body), so `toList` does not apply.
|
|
56
|
+
*/
|
|
57
|
+
async listDataLakes(args) {
|
|
58
|
+
const result = await this.client.get("/api/v1/data-lakes", { params: {
|
|
59
|
+
limit: args.limit,
|
|
60
|
+
...args.cursor ? { cursor: args.cursor } : {}
|
|
61
|
+
} });
|
|
62
|
+
return {
|
|
63
|
+
data: result.data ?? [],
|
|
64
|
+
nextCursor: result.next_cursor ?? null
|
|
65
|
+
};
|
|
66
|
+
}
|
|
52
67
|
async sendChat(args) {
|
|
53
68
|
return this.client.post("/api/chat", {
|
|
54
|
-
...args.notebookId ? { sessionId: args.notebookId } : {},
|
|
69
|
+
...args.notebookId ? { sessionId: args.notebookId } : { newConversation: true },
|
|
55
70
|
message: args.message,
|
|
56
71
|
...args.model ? { model: args.model } : {},
|
|
57
72
|
...args.systemPrompt ? { systemPrompt: args.systemPrompt } : {},
|
|
@@ -81,36 +96,72 @@ var B4mApiClient = class {
|
|
|
81
96
|
async getFile(fileId) {
|
|
82
97
|
return this.client.get(`/api/files/${encodeURIComponent(fileId)}`);
|
|
83
98
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Generate a sound effect. Unlike the JSON routes, this one streams raw audio
|
|
86
|
-
* bytes, so it goes through the axios instance directly to read the response
|
|
87
|
-
* headers (content type + the persisted-FabFile side channel). On failure the
|
|
88
|
-
* error body arrives as bytes; {@link decodeArrayBufferErrorBody} restores the
|
|
89
|
-
* JSON shape so {@link mapApiError} can surface the server's message.
|
|
90
|
-
*/
|
|
99
|
+
/** Generate a sound effect; see {@link postGeneratedAudio} for the normalized response. */
|
|
91
100
|
async generateSoundEffect(args) {
|
|
101
|
+
return this.postGeneratedAudio("/api/ai/sound-effects", {
|
|
102
|
+
provider: args.provider,
|
|
103
|
+
text: args.text,
|
|
104
|
+
...args.durationSeconds !== void 0 ? { durationSeconds: args.durationSeconds } : {},
|
|
105
|
+
...args.promptInfluence !== void 0 ? { promptInfluence: args.promptInfluence } : {},
|
|
106
|
+
...args.format ? { format: args.format } : {}
|
|
107
|
+
}, generatedAudioResponseSchema);
|
|
108
|
+
}
|
|
109
|
+
async synthesizeSpeech(args) {
|
|
92
110
|
try {
|
|
93
|
-
const response = await this.client.getAxiosInstance().post("/api/ai/sound-effects", {
|
|
94
|
-
provider: args.provider,
|
|
95
|
-
text: args.text,
|
|
96
|
-
...args.durationSeconds !== void 0 ? { durationSeconds: args.durationSeconds } : {},
|
|
97
|
-
...args.promptInfluence !== void 0 ? { promptInfluence: args.promptInfluence } : {},
|
|
98
|
-
...args.format ? { format: args.format } : {}
|
|
99
|
-
}, { responseType: "arraybuffer" });
|
|
100
|
-
const headerString = (value) => typeof value === "string" ? value : void 0;
|
|
101
|
-
const saved = String(response.headers["x-b4m-audio-saved"] ?? "") === "true";
|
|
102
111
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
saved,
|
|
106
|
-
fabFileId: saved ? headerString(response.headers["x-b4m-audio-fab-file-id"]) : void 0,
|
|
107
|
-
fileName: saved ? headerString(response.headers["x-b4m-audio-file-name"]) : void 0,
|
|
108
|
-
fileUrl: saved ? headerString(response.headers["x-b4m-audio-file-url"]) : void 0
|
|
112
|
+
kind: "audio",
|
|
113
|
+
data: await this.postGeneratedAudio("/api/ai/tts", args, ttsBase64ResponseSchema)
|
|
109
114
|
};
|
|
110
115
|
} catch (error) {
|
|
111
|
-
|
|
116
|
+
if (isAxiosError(error) && error.response?.status === 413) {
|
|
117
|
+
const oversized = ttsResponseTooLargeSchema.safeParse(error.response.data);
|
|
118
|
+
if (oversized.success && oversized.data.saved && oversized.data.fabFileId) {
|
|
119
|
+
const fallbackFrom = supportedVoiceGenerationVendor.safeParse(error.response.headers?.["x-b4m-tts-provider-fallback-from"]);
|
|
120
|
+
return {
|
|
121
|
+
kind: "saved-too-large",
|
|
122
|
+
data: {
|
|
123
|
+
...oversized.data,
|
|
124
|
+
fabFileId: oversized.data.fabFileId
|
|
125
|
+
},
|
|
126
|
+
...fallbackFrom.success ? { fallbackFrom: fallbackFrom.data } : {}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
112
131
|
}
|
|
113
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* POST a generated-audio request with `encoding: 'base64'` and normalize any
|
|
135
|
+
* server's answer into the JSON shape `schema` describes. Base64 (never binary)
|
|
136
|
+
* because an oversized result is a 303 in binary mode, and axios drops the
|
|
137
|
+
* X-B4M headers when following it. A server predating the `encoding` field
|
|
138
|
+
* ignores it and streams raw bytes with the save result in X-B4M-Audio-* headers;
|
|
139
|
+
* those are rebuilt into the inline variant. The request is arraybuffer-typed, so
|
|
140
|
+
* a failure body arrives as bytes; {@link decodeArrayBufferErrorBody} restores
|
|
141
|
+
* its JSON shape for {@link mapApiError}.
|
|
142
|
+
*/
|
|
143
|
+
async postGeneratedAudio(path, body, schema) {
|
|
144
|
+
const response = await this.client.getAxiosInstance().post(path, {
|
|
145
|
+
...body,
|
|
146
|
+
encoding: "base64"
|
|
147
|
+
}, { responseType: "arraybuffer" }).catch((error) => {
|
|
148
|
+
throw decodeArrayBufferErrorBody(error);
|
|
149
|
+
});
|
|
150
|
+
const contentType = String(response.headers["content-type"] ?? "application/octet-stream");
|
|
151
|
+
const bytes = Buffer.from(response.data);
|
|
152
|
+
if (/json/i.test(contentType)) return schema.parse(JSON.parse(bytes.toString("utf8")));
|
|
153
|
+
const headerString = (value) => typeof value === "string" ? value : void 0;
|
|
154
|
+
const saved = String(response.headers["x-b4m-audio-saved"] ?? "") === "true";
|
|
155
|
+
return schema.parse({
|
|
156
|
+
delivery: "inline",
|
|
157
|
+
audio: bytes.toString("base64"),
|
|
158
|
+
contentType,
|
|
159
|
+
saved,
|
|
160
|
+
fabFileId: saved ? headerString(response.headers["x-b4m-audio-fab-file-id"]) : void 0,
|
|
161
|
+
fileName: saved ? headerString(response.headers["x-b4m-audio-file-name"]) : void 0,
|
|
162
|
+
fileUrl: saved ? headerString(response.headers["x-b4m-audio-file-url"]) : void 0
|
|
163
|
+
});
|
|
164
|
+
}
|
|
114
165
|
async listProjects(args) {
|
|
115
166
|
const result = await this.client.get("/api/projects", { params: {
|
|
116
167
|
...args.search ? { search: args.search } : {},
|
|
@@ -152,10 +203,16 @@ var B4mApiClient = class {
|
|
|
152
203
|
* gap that may not be the cause.
|
|
153
204
|
*/
|
|
154
205
|
function mapApiError(error, baseURL, scope) {
|
|
206
|
+
if (error instanceof NotAuthenticatedError) return "not authenticated: no credential configured (set B4M_API_KEY or run `b4m login`)";
|
|
155
207
|
if (isAxiosError(error)) {
|
|
156
208
|
const status = error.response?.status;
|
|
157
|
-
if (status === 401)
|
|
209
|
+
if (status === 401) {
|
|
210
|
+
const providerKeyMessage = providerKeyFailureMessage(error.response?.data);
|
|
211
|
+
if (providerKeyMessage) return providerKeyMessage;
|
|
212
|
+
return "authentication failed (run `b4m login` or set B4M_API_KEY)";
|
|
213
|
+
}
|
|
158
214
|
if (status === 403) {
|
|
215
|
+
if ((error.response?.data)?.code === "FEATURE_DISABLED") return "feature disabled on this Bike4Mind instance (ask an admin to enable it)";
|
|
159
216
|
const base = "API key forbidden: check the key's scopes and account access";
|
|
160
217
|
return scope ? `${base} (recommended scope: ${scope})` : base;
|
|
161
218
|
}
|
|
@@ -215,6 +272,10 @@ function extractServerMessage(data) {
|
|
|
215
272
|
if (typeof record.message === "string") return record.message;
|
|
216
273
|
}
|
|
217
274
|
}
|
|
275
|
+
function providerKeyFailureMessage(data) {
|
|
276
|
+
if (!isProviderKeyFailure(data)) return void 0;
|
|
277
|
+
return `${extractServerMessage(data) ?? "the AI provider could not be used"} (configure or fix the provider API key in Bike4Mind; this is not a Bike4Mind login problem)`;
|
|
278
|
+
}
|
|
218
279
|
//#endregion
|
|
219
280
|
//#region src/mcp/tools.ts
|
|
220
281
|
const TOOL_META = [
|
|
@@ -233,13 +294,13 @@ const TOOL_META = [
|
|
|
233
294
|
{
|
|
234
295
|
name: "create_notebook",
|
|
235
296
|
title: "Create notebook",
|
|
236
|
-
description: "Create a new notebook, optionally inside a project. Defaults the name to \"New Notebook\" when omitted.",
|
|
297
|
+
description: "Create a new notebook, optionally inside a project or grounded in a data lake (dataLakeId, see list_lakes). Defaults the name to \"New Notebook\" when omitted.",
|
|
237
298
|
scope: "notebooks:write"
|
|
238
299
|
},
|
|
239
300
|
{
|
|
240
301
|
name: "send_message",
|
|
241
302
|
title: "Send message",
|
|
242
|
-
description: "Send a chat message and wait for the assistant reply.",
|
|
303
|
+
description: "Send a chat message and wait for the assistant reply; returns the cited sources (citables) the answer was grounded in.",
|
|
243
304
|
scope: "ai:chat"
|
|
244
305
|
},
|
|
245
306
|
{
|
|
@@ -248,6 +309,12 @@ const TOOL_META = [
|
|
|
248
309
|
description: "Semantic search across the caller's notebooks.",
|
|
249
310
|
scope: "notebooks:read"
|
|
250
311
|
},
|
|
312
|
+
{
|
|
313
|
+
name: "list_lakes",
|
|
314
|
+
title: "List data lakes",
|
|
315
|
+
description: "List the data lakes the caller can reach. Pass a lake's id as dataLakeId to create_notebook to ground a notebook in it.",
|
|
316
|
+
scope: "datalake:read"
|
|
317
|
+
},
|
|
251
318
|
{
|
|
252
319
|
name: "list_files",
|
|
253
320
|
title: "List files",
|
|
@@ -265,6 +332,12 @@ const TOOL_META = [
|
|
|
265
332
|
title: "Generate sound effect",
|
|
266
333
|
description: "Generate a sound effect from a text description. Returns the saved audio file (with a signed download URL) when the caller keeps generated audio, otherwise the audio inline.",
|
|
267
334
|
scope: "ai:generate"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: "text_to_speech",
|
|
338
|
+
title: "Text to speech",
|
|
339
|
+
description: "Synthesize speech from text. Returns a saved audio file with a signed download URL when available, otherwise audio inline.",
|
|
340
|
+
scope: "ai:generate"
|
|
268
341
|
}
|
|
269
342
|
];
|
|
270
343
|
TOOL_META.map((t) => t.name);
|
|
@@ -276,11 +349,12 @@ const listNotebooksShape = {
|
|
|
276
349
|
const getNotebookShape = { notebookId: z$1.string().describe("The notebook (session) id") };
|
|
277
350
|
const createNotebookShape = {
|
|
278
351
|
name: z$1.string().optional().describe("Name for the new notebook"),
|
|
279
|
-
projectId: z$1.string().optional().describe("Project to create the notebook in")
|
|
352
|
+
projectId: z$1.string().optional().describe("Project to create the notebook in"),
|
|
353
|
+
dataLakeId: z$1.string().optional().describe("Data lake id or slug to ground the notebook in (see list_lakes); seeds the lake's retrieval defaults")
|
|
280
354
|
};
|
|
281
355
|
const sendMessageShape = {
|
|
282
356
|
message: z$1.string().describe("The message to send"),
|
|
283
|
-
notebookId: z$1.string().optional().describe("Notebook to send to;
|
|
357
|
+
notebookId: z$1.string().optional().describe("Notebook to send to; omit to start a new notebook (its id is returned as notebookId, pass it back to continue the thread)"),
|
|
284
358
|
model: z$1.string().optional().describe("Model id to use; defaults to the instance default"),
|
|
285
359
|
systemPrompt: z$1.string().max(PROMPT_TEXT_MAX).optional().describe("Caller-supplied system-prompt text for this message only; refines but never overrides other guidance")
|
|
286
360
|
};
|
|
@@ -289,6 +363,10 @@ const searchKnowledgeBaseShape = {
|
|
|
289
363
|
limit: z$1.number().int().min(1).max(100).default(10).describe("Maximum results to return"),
|
|
290
364
|
minSimilarity: z$1.number().min(0).max(1).optional().describe("Minimum cosine similarity threshold")
|
|
291
365
|
};
|
|
366
|
+
const listLakesShape = {
|
|
367
|
+
limit: z$1.number().int().min(1).max(100).default(25).describe("Maximum lakes to return"),
|
|
368
|
+
cursor: z$1.string().optional().describe("Pass back nextCursor from the previous page")
|
|
369
|
+
};
|
|
292
370
|
const listFilesShape = {
|
|
293
371
|
search: z$1.string().optional().describe("Filter files by name/content"),
|
|
294
372
|
limit: z$1.number().int().min(1).max(100).default(25).describe("Maximum files to return"),
|
|
@@ -302,6 +380,12 @@ const generateSoundEffectShape = {
|
|
|
302
380
|
promptInfluence: z$1.number().min(0).max(1).optional().describe("How strictly to follow the prompt (0 = loose, 1 = strict)"),
|
|
303
381
|
format: z$1.string().optional().describe("Provider output encoding token, e.g. mp3_44100_128")
|
|
304
382
|
};
|
|
383
|
+
const textToSpeechShape = {
|
|
384
|
+
...ttsRequestSchema.omit({ encoding: true }).shape,
|
|
385
|
+
text: ttsRequestSchema.shape.text.describe("Text to speak"),
|
|
386
|
+
provider: ttsRequestSchema.shape.provider.describe(`Speech provider; defaults to ${DEFAULT_TTS_PROVIDER}`),
|
|
387
|
+
preview: ttsRequestSchema.shape.preview.describe("Skip saving a copy to the file browser")
|
|
388
|
+
};
|
|
305
389
|
function notebookSummary(n) {
|
|
306
390
|
return {
|
|
307
391
|
id: n.id,
|
|
@@ -311,6 +395,17 @@ function notebookSummary(n) {
|
|
|
311
395
|
updatedAt: n.updatedAt ?? n.lastUpdated
|
|
312
396
|
};
|
|
313
397
|
}
|
|
398
|
+
function lakeSummary(l) {
|
|
399
|
+
return {
|
|
400
|
+
id: l.id,
|
|
401
|
+
name: l.name,
|
|
402
|
+
slug: l.slug,
|
|
403
|
+
description: l.description ?? void 0,
|
|
404
|
+
builtIn: l.built_in,
|
|
405
|
+
status: l.status,
|
|
406
|
+
fileCount: l.file_count
|
|
407
|
+
};
|
|
408
|
+
}
|
|
314
409
|
async function listNotebooks(client, args) {
|
|
315
410
|
const { data, hasMore } = await client.listNotebooks(args);
|
|
316
411
|
return {
|
|
@@ -330,23 +425,38 @@ async function createNotebook(client, args) {
|
|
|
330
425
|
async function sendMessage(client, args) {
|
|
331
426
|
const res = await client.sendChat(args);
|
|
332
427
|
const questId = res.id;
|
|
333
|
-
let
|
|
334
|
-
|
|
335
|
-
|
|
428
|
+
let quest;
|
|
429
|
+
try {
|
|
430
|
+
quest = await client.getQuest(questId);
|
|
336
431
|
} catch {
|
|
337
|
-
|
|
432
|
+
quest = void 0;
|
|
338
433
|
}
|
|
339
|
-
const
|
|
434
|
+
const notebookId = args.notebookId ?? res.sessionId ?? quest?.sessionId;
|
|
435
|
+
const citables = quest ? (quest.promptMeta?.citables ?? []).map((c) => ({
|
|
436
|
+
id: c.id,
|
|
437
|
+
type: c.type,
|
|
438
|
+
title: c.title,
|
|
439
|
+
url: c.url,
|
|
440
|
+
description: c.description
|
|
441
|
+
})) : void 0;
|
|
340
442
|
return {
|
|
341
443
|
notebookId,
|
|
342
444
|
questId,
|
|
343
|
-
reply,
|
|
344
|
-
model: res.model
|
|
445
|
+
reply: res.responses && res.responses.length > 0 ? res.responses.join("\n\n") : res.response ?? "",
|
|
446
|
+
model: res.model,
|
|
447
|
+
citables
|
|
345
448
|
};
|
|
346
449
|
}
|
|
347
450
|
async function searchKnowledgeBase(client, args) {
|
|
348
451
|
return { results: await client.searchKnowledgeBase(args) };
|
|
349
452
|
}
|
|
453
|
+
async function listLakes(client, args) {
|
|
454
|
+
const { data, nextCursor } = await client.listDataLakes(args);
|
|
455
|
+
return {
|
|
456
|
+
lakes: data.map(lakeSummary),
|
|
457
|
+
nextCursor
|
|
458
|
+
};
|
|
459
|
+
}
|
|
350
460
|
async function listFiles(client, args) {
|
|
351
461
|
const { data, hasMore } = await client.listFiles(args);
|
|
352
462
|
return {
|
|
@@ -358,26 +468,7 @@ async function getFile(client, args) {
|
|
|
358
468
|
return client.getFile(args.fileId);
|
|
359
469
|
}
|
|
360
470
|
async function generateSoundEffect(client, args) {
|
|
361
|
-
|
|
362
|
-
const base = {
|
|
363
|
-
provider: args.provider,
|
|
364
|
-
contentType,
|
|
365
|
-
byteLength: audio.length
|
|
366
|
-
};
|
|
367
|
-
if (saved && fabFileId && fileUrl) return {
|
|
368
|
-
...base,
|
|
369
|
-
saved: true,
|
|
370
|
-
file: {
|
|
371
|
-
id: fabFileId,
|
|
372
|
-
fileName,
|
|
373
|
-
fileUrl
|
|
374
|
-
}
|
|
375
|
-
};
|
|
376
|
-
return {
|
|
377
|
-
...base,
|
|
378
|
-
saved: false,
|
|
379
|
-
audioBase64: audio.toString("base64")
|
|
380
|
-
};
|
|
471
|
+
return generatedAudioResult(await client.generateSoundEffect(args), { provider: args.provider });
|
|
381
472
|
}
|
|
382
473
|
function toResult(value) {
|
|
383
474
|
const structuredContent = value && typeof value === "object" && !Array.isArray(value) ? value : { result: value };
|
|
@@ -399,21 +490,57 @@ function errorResult(message) {
|
|
|
399
490
|
};
|
|
400
491
|
}
|
|
401
492
|
/**
|
|
402
|
-
* Render a
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
493
|
+
* Render a generated-audio response as an MCP result, shared by every audio tool.
|
|
494
|
+
* Oversized audio is reported by its signed URL; a saved copy with a usable URL is
|
|
495
|
+
* reported as a file (like get_file) with no inline bytes. Otherwise the audio rides
|
|
496
|
+
* inline as an `audio` block so a billed result is never lost, with any saved copy
|
|
497
|
+
* still named by id and a skipped save explained. The route forwards the signed URL
|
|
498
|
+
* it minted at upload, so no getFile re-fetch is needed (that fails closed until
|
|
499
|
+
* the async moderation scan runs).
|
|
407
500
|
*/
|
|
408
|
-
function
|
|
409
|
-
|
|
501
|
+
function generatedAudioResult(response, endpointMetadata) {
|
|
502
|
+
const metadata = {
|
|
503
|
+
...endpointMetadata,
|
|
504
|
+
contentType: response.contentType
|
|
505
|
+
};
|
|
506
|
+
const saveSkippedReason = response.saveSkippedReason ? { saveSkippedReason: response.saveSkippedReason } : {};
|
|
507
|
+
const file = response.fabFileId ? {
|
|
508
|
+
id: response.fabFileId,
|
|
509
|
+
...response.fileName ? { fileName: response.fileName } : {},
|
|
510
|
+
...response.fileUrl ? { fileUrl: response.fileUrl } : {}
|
|
511
|
+
} : void 0;
|
|
512
|
+
if (response.delivery === "url") return toResult({
|
|
513
|
+
...metadata,
|
|
514
|
+
byteLength: response.bytes,
|
|
515
|
+
url: response.url,
|
|
516
|
+
saved: response.saved === true,
|
|
517
|
+
...response.saved && file ? { file } : {},
|
|
518
|
+
...saveSkippedReason
|
|
519
|
+
});
|
|
520
|
+
const byteLength = Buffer.from(response.audio, "base64").length;
|
|
521
|
+
if (response.saved && file && file.fileUrl) return toResult({
|
|
522
|
+
...metadata,
|
|
523
|
+
byteLength,
|
|
410
524
|
saved: true,
|
|
411
|
-
|
|
412
|
-
contentType: outcome.contentType,
|
|
413
|
-
byteLength: outcome.byteLength,
|
|
414
|
-
file: outcome.file
|
|
525
|
+
file
|
|
415
526
|
});
|
|
416
|
-
|
|
527
|
+
return inlineAudioResult(response.saved && file ? {
|
|
528
|
+
...metadata,
|
|
529
|
+
byteLength,
|
|
530
|
+
saved: true,
|
|
531
|
+
file
|
|
532
|
+
} : {
|
|
533
|
+
...metadata,
|
|
534
|
+
byteLength,
|
|
535
|
+
saved: false,
|
|
536
|
+
...saveSkippedReason
|
|
537
|
+
}, response.audio, response.contentType);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Metadata as JSON text plus the audio as an MCP `audio` block. The base64 stays
|
|
541
|
+
* out of structuredContent so a potentially large payload is not duplicated.
|
|
542
|
+
*/
|
|
543
|
+
function inlineAudioResult(meta, audioBase64, mimeType) {
|
|
417
544
|
return {
|
|
418
545
|
content: [{
|
|
419
546
|
type: "text",
|
|
@@ -421,11 +548,32 @@ function soundEffectResult(outcome) {
|
|
|
421
548
|
}, {
|
|
422
549
|
type: "audio",
|
|
423
550
|
data: audioBase64,
|
|
424
|
-
mimeType
|
|
551
|
+
mimeType
|
|
425
552
|
}],
|
|
426
553
|
structuredContent: meta
|
|
427
554
|
};
|
|
428
555
|
}
|
|
556
|
+
async function textToSpeech(client, args) {
|
|
557
|
+
const response = await client.synthesizeSpeech(args);
|
|
558
|
+
if (response.kind === "saved-too-large") {
|
|
559
|
+
const { provider, fabFileId, fileUrl } = response.data;
|
|
560
|
+
return toResult({
|
|
561
|
+
saved: true,
|
|
562
|
+
provider,
|
|
563
|
+
...response.fallbackFrom ? { fallbackFrom: response.fallbackFrom } : {},
|
|
564
|
+
file: {
|
|
565
|
+
id: fabFileId,
|
|
566
|
+
...fileUrl ? { fileUrl } : {}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const result = response.data;
|
|
571
|
+
return generatedAudioResult(result, {
|
|
572
|
+
provider: result.provider ?? args.provider ?? "openai",
|
|
573
|
+
...result.fallbackFrom ? { fallbackFrom: result.fallbackFrom } : {},
|
|
574
|
+
format: result.format
|
|
575
|
+
});
|
|
576
|
+
}
|
|
429
577
|
/**
|
|
430
578
|
* Register the Bike4Mind MCP tools on `server`. Each handler is wrapped so an
|
|
431
579
|
* API failure becomes a structured `isError` result carrying a friendly message
|
|
@@ -466,6 +614,11 @@ function registerTools(server, client) {
|
|
|
466
614
|
description: meta("search_knowledge_base").description,
|
|
467
615
|
inputSchema: searchKnowledgeBaseShape
|
|
468
616
|
}, (args) => run("notebooks:read", () => searchKnowledgeBase(client, args)));
|
|
617
|
+
server.registerTool("list_lakes", {
|
|
618
|
+
title: meta("list_lakes").title,
|
|
619
|
+
description: meta("list_lakes").description,
|
|
620
|
+
inputSchema: listLakesShape
|
|
621
|
+
}, (args) => run("datalake:read", () => listLakes(client, args)));
|
|
469
622
|
server.registerTool("list_files", {
|
|
470
623
|
title: meta("list_files").title,
|
|
471
624
|
description: meta("list_files").description,
|
|
@@ -482,7 +635,18 @@ function registerTools(server, client) {
|
|
|
482
635
|
inputSchema: generateSoundEffectShape
|
|
483
636
|
}, async (args) => {
|
|
484
637
|
try {
|
|
485
|
-
return
|
|
638
|
+
return await generateSoundEffect(client, args);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
return errorResult(mapApiError(err, baseURL, "ai:generate"));
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
server.registerTool("text_to_speech", {
|
|
644
|
+
title: meta("text_to_speech").title,
|
|
645
|
+
description: meta("text_to_speech").description,
|
|
646
|
+
inputSchema: textToSpeechShape
|
|
647
|
+
}, async (args) => {
|
|
648
|
+
try {
|
|
649
|
+
return await textToSpeech(client, args);
|
|
486
650
|
} catch (err) {
|
|
487
651
|
return errorResult(mapApiError(err, baseURL, "ai:generate"));
|
|
488
652
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bike4mind/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"mcpName": "io.github.bike4mind/bike4mind",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Interactive CLI tool for Bike4Mind with ReAct agents",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@opensearch-project/opensearch": "2.11.0",
|
|
56
56
|
"@smithy/node-http-handler": "^4.9.3",
|
|
57
57
|
"async-mutex": "^0.5.0",
|
|
58
|
-
"axios": "1.
|
|
58
|
+
"axios": "1.20.0",
|
|
59
59
|
"bcryptjs": "^3.0.3",
|
|
60
60
|
"better-sqlite3": "^13.0.3",
|
|
61
61
|
"cheerio": "1.2.0",
|
|
@@ -124,16 +124,16 @@
|
|
|
124
124
|
"tsdown": "^0.22.3",
|
|
125
125
|
"tsx": "^4.23.0",
|
|
126
126
|
"typescript": "^5.9.3",
|
|
127
|
-
"vitest": "^4.1.
|
|
128
|
-
"@bike4mind/agents": "1.
|
|
129
|
-
"@bike4mind/common": "
|
|
127
|
+
"vitest": "^4.1.11",
|
|
128
|
+
"@bike4mind/agents": "1.1.1",
|
|
129
|
+
"@bike4mind/common": "13.0.0",
|
|
130
130
|
"@bike4mind/hearth": "0.3.0",
|
|
131
|
-
"@bike4mind/
|
|
132
|
-
"@bike4mind/
|
|
133
|
-
"@bike4mind/
|
|
134
|
-
"@bike4mind/observability": "0.2.
|
|
135
|
-
"@bike4mind/utils": "6.
|
|
136
|
-
"@bike4mind/
|
|
131
|
+
"@bike4mind/fab-pipeline": "1.5.3",
|
|
132
|
+
"@bike4mind/llm-adapters": "0.16.1",
|
|
133
|
+
"@bike4mind/mcp": "2.0.17",
|
|
134
|
+
"@bike4mind/observability": "0.2.3",
|
|
135
|
+
"@bike4mind/utils": "6.5.1",
|
|
136
|
+
"@bike4mind/services": "15.0.0"
|
|
137
137
|
},
|
|
138
138
|
"optionalDependencies": {
|
|
139
139
|
"@vscode/ripgrep": "^1.18.0"
|