@speakai/mcp-server 1.0.5 → 1.0.7
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 +419 -161
- package/dist/index.js +1518 -59
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function getApiKey() {
|
|
|
47
47
|
async function authenticate() {
|
|
48
48
|
const apiKey = getApiKey();
|
|
49
49
|
if (!apiKey) {
|
|
50
|
-
throw new Error("SPEAK_API_KEY is not set. Run '
|
|
50
|
+
throw new Error("SPEAK_API_KEY is not set. Run 'speakai-mcp config set-key' or set the environment variable.");
|
|
51
51
|
}
|
|
52
52
|
try {
|
|
53
53
|
const res = await import_axios.default.post(
|
|
@@ -64,11 +64,11 @@ async function authenticate() {
|
|
|
64
64
|
accessToken = res.data.data.accessToken;
|
|
65
65
|
refreshToken = res.data.data.refreshToken ?? "";
|
|
66
66
|
tokenExpiresAt = Date.now() + 50 * 60 * 1e3;
|
|
67
|
-
process.stderr.write("[
|
|
67
|
+
process.stderr.write("[speakai-mcp] Authenticated successfully\n");
|
|
68
68
|
}
|
|
69
69
|
} catch (err) {
|
|
70
70
|
process.stderr.write(
|
|
71
|
-
`[
|
|
71
|
+
`[speakai-mcp] Authentication failed: ${err instanceof Error ? err.message : err}
|
|
72
72
|
`
|
|
73
73
|
);
|
|
74
74
|
}
|
|
@@ -93,7 +93,7 @@ async function refreshAccessToken() {
|
|
|
93
93
|
accessToken = res.data.data.accessToken;
|
|
94
94
|
refreshToken = res.data.data.refreshToken ?? refreshToken;
|
|
95
95
|
tokenExpiresAt = Date.now() + 50 * 60 * 1e3;
|
|
96
|
-
process.stderr.write("[
|
|
96
|
+
process.stderr.write("[speakai-mcp] Token refreshed\n");
|
|
97
97
|
}
|
|
98
98
|
} catch {
|
|
99
99
|
return authenticate();
|
|
@@ -178,7 +178,7 @@ function register(server, client) {
|
|
|
178
178
|
const api = client ?? speakClient;
|
|
179
179
|
server.tool(
|
|
180
180
|
"get_signed_upload_url",
|
|
181
|
-
"Get a pre-signed S3 URL for direct
|
|
181
|
+
"Get a pre-signed S3 URL for direct file upload to Speak AI storage. After getting the URL, PUT your file to it, then call upload_media with the S3 URL. For a simpler workflow, use upload_local_file instead which handles all steps automatically.",
|
|
182
182
|
{
|
|
183
183
|
isVideo: import_zod.z.boolean().describe("Set true for video files, false for audio files"),
|
|
184
184
|
filename: import_zod.z.string().min(1).describe("Original filename including extension"),
|
|
@@ -204,11 +204,11 @@ function register(server, client) {
|
|
|
204
204
|
);
|
|
205
205
|
server.tool(
|
|
206
206
|
"upload_media",
|
|
207
|
-
"Upload a
|
|
207
|
+
"Upload media from a publicly accessible URL. Processing is asynchronous \u2014 after uploading, use get_media_status to poll until state is 'processed' (typically 1-3 minutes for audio under 60 min), then use get_transcript and get_media_insights to retrieve results. For a single call that handles everything, use upload_and_analyze instead. For local files, use upload_local_file.",
|
|
208
208
|
{
|
|
209
209
|
name: import_zod.z.string().min(1).describe("Display name for the media file"),
|
|
210
210
|
url: import_zod.z.string().describe("Publicly accessible URL of the media file (or pre-signed S3 URL)"),
|
|
211
|
-
mediaType: import_zod.z.enum([
|
|
211
|
+
mediaType: import_zod.z.enum([import_shared.MediaType.AUDIO, import_shared.MediaType.VIDEO]).describe('Type of media: "audio" or "video"'),
|
|
212
212
|
description: import_zod.z.string().optional().describe("Description of the media file"),
|
|
213
213
|
sourceLanguage: import_zod.z.string().optional().describe('BCP-47 language code for transcription, e.g. "en-US" or "he-IL"'),
|
|
214
214
|
tags: import_zod.z.string().optional().describe("Comma-separated tags for the media"),
|
|
@@ -239,9 +239,9 @@ function register(server, client) {
|
|
|
239
239
|
);
|
|
240
240
|
server.tool(
|
|
241
241
|
"list_media",
|
|
242
|
-
"List
|
|
242
|
+
"List and search media files in the workspace with filtering, pagination, and sorting. Use filterName for text search, mediaType to filter by audio/video/text, folderId for folder-specific results, and from/to for date ranges. Returns mediaIds you can pass to get_transcript, get_media_insights, or ask_magic_prompt. For deep full-text search across transcripts, use search_media instead.",
|
|
243
243
|
{
|
|
244
|
-
mediaType: import_zod.z.enum([
|
|
244
|
+
mediaType: import_zod.z.enum([import_shared.MediaType.AUDIO, import_shared.MediaType.VIDEO, import_shared.MediaType.TEXT]).optional().describe('Filter by media type: "audio", "video", or "text"'),
|
|
245
245
|
page: import_zod.z.number().int().positive().optional().describe("Page number for pagination (default: 1)"),
|
|
246
246
|
pageSize: import_zod.z.number().int().positive().optional().describe("Number of results per page (default: 20)"),
|
|
247
247
|
sortBy: import_zod.z.string().optional().describe('Sort field and direction, e.g. "createdAt:desc" or "name:asc"'),
|
|
@@ -270,7 +270,7 @@ function register(server, client) {
|
|
|
270
270
|
);
|
|
271
271
|
server.tool(
|
|
272
272
|
"get_media_insights",
|
|
273
|
-
"Retrieve AI-generated insights for a media file
|
|
273
|
+
"Retrieve AI-generated insights for a processed media file \u2014 topics, sentiment, keywords, action items, summaries, and more. The media must be in 'processed' state (check with get_media_status first). For asking custom questions about a media file, use ask_magic_prompt instead.",
|
|
274
274
|
{
|
|
275
275
|
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file")
|
|
276
276
|
},
|
|
@@ -292,7 +292,7 @@ function register(server, client) {
|
|
|
292
292
|
);
|
|
293
293
|
server.tool(
|
|
294
294
|
"get_transcript",
|
|
295
|
-
"Retrieve the full transcript for a media file
|
|
295
|
+
"Retrieve the full transcript for a processed media file with speaker labels and timestamps. The media must be in 'processed' state. Use update_transcript_speakers to rename speaker labels after reviewing. For subtitle-formatted output, use get_captions instead.",
|
|
296
296
|
{
|
|
297
297
|
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file")
|
|
298
298
|
},
|
|
@@ -345,7 +345,7 @@ function register(server, client) {
|
|
|
345
345
|
);
|
|
346
346
|
server.tool(
|
|
347
347
|
"get_media_status",
|
|
348
|
-
"Check the processing status of a media file
|
|
348
|
+
"Check the processing status of a media file. States: pending \u2192 transcribing \u2192 analyzing \u2192 processed (or failed). Poll this after upload_media until state is 'processed', then use get_transcript and get_media_insights to retrieve results.",
|
|
349
349
|
{
|
|
350
350
|
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file")
|
|
351
351
|
},
|
|
@@ -416,13 +416,120 @@ function register(server, client) {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
);
|
|
419
|
+
server.tool(
|
|
420
|
+
"get_captions",
|
|
421
|
+
"Get captions for a media file. Captions are separate from full transcripts and are formatted for display/subtitles.",
|
|
422
|
+
{
|
|
423
|
+
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file")
|
|
424
|
+
},
|
|
425
|
+
async ({ mediaId }) => {
|
|
426
|
+
try {
|
|
427
|
+
const result = await api.get(`/v1/media/caption/${mediaId}`);
|
|
428
|
+
return {
|
|
429
|
+
content: [
|
|
430
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) }
|
|
431
|
+
]
|
|
432
|
+
};
|
|
433
|
+
} catch (err) {
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
436
|
+
isError: true
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
);
|
|
441
|
+
server.tool(
|
|
442
|
+
"list_supported_languages",
|
|
443
|
+
"List all languages supported for transcription. Use the language codes when uploading media with a specific sourceLanguage.",
|
|
444
|
+
{},
|
|
445
|
+
async () => {
|
|
446
|
+
try {
|
|
447
|
+
const result = await api.get("/v1/media/supportedLanguages");
|
|
448
|
+
return {
|
|
449
|
+
content: [
|
|
450
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) }
|
|
451
|
+
]
|
|
452
|
+
};
|
|
453
|
+
} catch (err) {
|
|
454
|
+
return {
|
|
455
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
456
|
+
isError: true
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
server.tool(
|
|
462
|
+
"get_media_statistics",
|
|
463
|
+
"Get workspace-level media statistics \u2014 total counts, processing status breakdown, storage usage, etc.",
|
|
464
|
+
{},
|
|
465
|
+
async () => {
|
|
466
|
+
try {
|
|
467
|
+
const result = await api.get("/v1/media/statistics");
|
|
468
|
+
return {
|
|
469
|
+
content: [
|
|
470
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) }
|
|
471
|
+
]
|
|
472
|
+
};
|
|
473
|
+
} catch (err) {
|
|
474
|
+
return {
|
|
475
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
476
|
+
isError: true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
server.tool(
|
|
482
|
+
"toggle_media_favorite",
|
|
483
|
+
"Mark or unmark a media file as a favorite for quick access.",
|
|
484
|
+
{
|
|
485
|
+
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file")
|
|
486
|
+
},
|
|
487
|
+
async (body) => {
|
|
488
|
+
try {
|
|
489
|
+
const result = await api.post("/v1/media/favorites", body);
|
|
490
|
+
return {
|
|
491
|
+
content: [
|
|
492
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) }
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
} catch (err) {
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
498
|
+
isError: true
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
);
|
|
503
|
+
server.tool(
|
|
504
|
+
"reanalyze_media",
|
|
505
|
+
"Re-run AI analysis on a media file using the latest models. Use this after Speak AI has updated its analysis capabilities or if the original analysis was incomplete.",
|
|
506
|
+
{
|
|
507
|
+
mediaId: import_zod.z.string().min(1).describe("Unique identifier of the media file to re-analyze")
|
|
508
|
+
},
|
|
509
|
+
async ({ mediaId }) => {
|
|
510
|
+
try {
|
|
511
|
+
const result = await api.get(`/v1/media/reanalyze/${mediaId}`);
|
|
512
|
+
return {
|
|
513
|
+
content: [
|
|
514
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) }
|
|
515
|
+
]
|
|
516
|
+
};
|
|
517
|
+
} catch (err) {
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
520
|
+
isError: true
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
);
|
|
419
525
|
}
|
|
420
|
-
var import_zod;
|
|
526
|
+
var import_zod, import_shared;
|
|
421
527
|
var init_media = __esm({
|
|
422
528
|
"src/tools/media.ts"() {
|
|
423
529
|
"use strict";
|
|
424
530
|
import_zod = require("zod");
|
|
425
531
|
init_client();
|
|
532
|
+
import_shared = require("@speakai/shared");
|
|
426
533
|
}
|
|
427
534
|
});
|
|
428
535
|
|
|
@@ -1236,9 +1343,135 @@ __export(prompt_exports, {
|
|
|
1236
1343
|
});
|
|
1237
1344
|
function register7(server, client) {
|
|
1238
1345
|
const api = client ?? speakClient;
|
|
1346
|
+
server.tool(
|
|
1347
|
+
"ask_magic_prompt",
|
|
1348
|
+
[
|
|
1349
|
+
"Ask an AI-powered question about your media using Speak AI's Magic Prompt.",
|
|
1350
|
+
"Supports querying a single file, multiple files, entire folders, or your whole workspace.",
|
|
1351
|
+
"Pass mediaIds for specific files, folderIds for entire folders, or omit both to search across all media.",
|
|
1352
|
+
"Use assistantType to get specialized responses (e.g., 'researcher' for academic analysis, 'sales' for deal insights).",
|
|
1353
|
+
"To continue a conversation, pass the promptId from a previous response.",
|
|
1354
|
+
"Returns a promptId \u2014 save it to continue the conversation with follow-up questions."
|
|
1355
|
+
].join(" "),
|
|
1356
|
+
{
|
|
1357
|
+
prompt: import_zod7.z.string().min(1).describe("The question or prompt to ask about the media"),
|
|
1358
|
+
mediaIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("Array of media IDs to query. Omit along with folderIds to search across all media in your workspace."),
|
|
1359
|
+
folderIds: import_zod7.z.array(import_zod7.z.string()).optional().describe("Array of folder IDs to scope the query to. Omit along with mediaIds to search across all media."),
|
|
1360
|
+
folderId: import_zod7.z.string().optional().describe("Single folder ID to scope the query to. Use folderIds for multiple folders."),
|
|
1361
|
+
assistantType: import_zod7.z.enum(Object.values(import_shared2.AssistantType)).optional().describe("Assistant persona: 'general' (default), 'researcher' (academic), 'marketer' (content), 'sales' (deals), 'recruiter' (hiring). Use 'custom' with assistantTemplateId."),
|
|
1362
|
+
assistantTemplateId: import_zod7.z.string().optional().describe("Required when assistantType is 'custom'. ID of a custom assistant template from list_prompts."),
|
|
1363
|
+
promptId: import_zod7.z.string().optional().describe("ID of an existing conversation to continue. Pass this to maintain chat context across multiple questions."),
|
|
1364
|
+
speakers: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter to specific speaker IDs from the transcript"),
|
|
1365
|
+
tags: import_zod7.z.array(import_zod7.z.string()).optional().describe("Filter media by tags"),
|
|
1366
|
+
startDate: import_zod7.z.string().optional().describe("Start date for date range filter (ISO 8601, e.g., '2025-01-01')"),
|
|
1367
|
+
endDate: import_zod7.z.string().optional().describe("End date for date range filter (ISO 8601, e.g., '2025-03-31')"),
|
|
1368
|
+
isIndividualPrompt: import_zod7.z.boolean().optional().describe("When true, processes each media file separately instead of combining context. Useful for comparing responses across files.")
|
|
1369
|
+
},
|
|
1370
|
+
async (params) => {
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await api.post("/v1/prompt", params);
|
|
1373
|
+
return {
|
|
1374
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1375
|
+
};
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
return {
|
|
1378
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1379
|
+
isError: true
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
);
|
|
1384
|
+
server.tool(
|
|
1385
|
+
"retry_magic_prompt",
|
|
1386
|
+
"Retry a failed or incomplete Magic Prompt response. Use when a previous ask_magic_prompt call returned an error or incomplete answer.",
|
|
1387
|
+
{
|
|
1388
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the conversation containing the failed message"),
|
|
1389
|
+
messageId: import_zod7.z.string().min(1).describe("ID of the specific message to retry")
|
|
1390
|
+
},
|
|
1391
|
+
async (body) => {
|
|
1392
|
+
try {
|
|
1393
|
+
const result = await api.post("/v1/prompt/retry", body);
|
|
1394
|
+
return {
|
|
1395
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1396
|
+
};
|
|
1397
|
+
} catch (err) {
|
|
1398
|
+
return {
|
|
1399
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1400
|
+
isError: true
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
);
|
|
1405
|
+
server.tool(
|
|
1406
|
+
"get_chat_history",
|
|
1407
|
+
"Get a list of recent Magic Prompt conversations. Returns conversation summaries with promptIds that can be used to continue conversations via ask_magic_prompt or retrieve full messages via get_chat_messages.",
|
|
1408
|
+
{
|
|
1409
|
+
limit: import_zod7.z.number().int().positive().optional().describe("Number of recent conversations to return (default: 10)")
|
|
1410
|
+
},
|
|
1411
|
+
async ({ limit }) => {
|
|
1412
|
+
try {
|
|
1413
|
+
const result = await api.get("/v1/prompt/history", {
|
|
1414
|
+
params: limit ? { limit } : void 0
|
|
1415
|
+
});
|
|
1416
|
+
return {
|
|
1417
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1418
|
+
};
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
return {
|
|
1421
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1422
|
+
isError: true
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
server.tool(
|
|
1428
|
+
"get_chat_messages",
|
|
1429
|
+
"Get full message history for conversations. Can filter by promptId for a specific conversation, by media/folder, or search across all chat messages. Returns questions, answers, references, and metadata.",
|
|
1430
|
+
{
|
|
1431
|
+
promptId: import_zod7.z.string().optional().describe("Filter to a specific conversation by its ID"),
|
|
1432
|
+
folderId: import_zod7.z.string().optional().describe("Filter messages by folder ID"),
|
|
1433
|
+
mediaIds: import_zod7.z.string().optional().describe("Filter by media IDs (comma-separated)"),
|
|
1434
|
+
query: import_zod7.z.string().optional().describe("Search text in prompts and answers"),
|
|
1435
|
+
page: import_zod7.z.number().int().optional().describe("Page number for pagination (default: 0)"),
|
|
1436
|
+
pageSize: import_zod7.z.number().int().optional().describe("Results per page (default: 25)")
|
|
1437
|
+
},
|
|
1438
|
+
async (params) => {
|
|
1439
|
+
try {
|
|
1440
|
+
const result = await api.get("/v1/prompt/messages", { params });
|
|
1441
|
+
return {
|
|
1442
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1443
|
+
};
|
|
1444
|
+
} catch (err) {
|
|
1445
|
+
return {
|
|
1446
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1447
|
+
isError: true
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
);
|
|
1452
|
+
server.tool(
|
|
1453
|
+
"delete_chat_message",
|
|
1454
|
+
"Delete a specific chat message from conversation history.",
|
|
1455
|
+
{
|
|
1456
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the message to delete")
|
|
1457
|
+
},
|
|
1458
|
+
async ({ promptId }) => {
|
|
1459
|
+
try {
|
|
1460
|
+
const result = await api.delete(`/v1/prompt/message/${promptId}`);
|
|
1461
|
+
return {
|
|
1462
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1463
|
+
};
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
return {
|
|
1466
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1467
|
+
isError: true
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
);
|
|
1239
1472
|
server.tool(
|
|
1240
1473
|
"list_prompts",
|
|
1241
|
-
"List all available Magic Prompt templates
|
|
1474
|
+
"List all available Magic Prompt templates. Use template IDs with ask_magic_prompt's assistantTemplateId parameter when using assistantType 'custom'.",
|
|
1242
1475
|
{},
|
|
1243
1476
|
async () => {
|
|
1244
1477
|
try {
|
|
@@ -1255,16 +1488,119 @@ function register7(server, client) {
|
|
|
1255
1488
|
}
|
|
1256
1489
|
);
|
|
1257
1490
|
server.tool(
|
|
1258
|
-
"
|
|
1259
|
-
"
|
|
1491
|
+
"get_favorite_prompts",
|
|
1492
|
+
"Get all prompts and answers that have been marked as favorites. Useful for finding saved insights and important AI-generated analysis.",
|
|
1493
|
+
{},
|
|
1494
|
+
async () => {
|
|
1495
|
+
try {
|
|
1496
|
+
const result = await api.get("/v1/prompt/favorites");
|
|
1497
|
+
return {
|
|
1498
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1499
|
+
};
|
|
1500
|
+
} catch (err) {
|
|
1501
|
+
return {
|
|
1502
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1503
|
+
isError: true
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
);
|
|
1508
|
+
server.tool(
|
|
1509
|
+
"toggle_prompt_favorite",
|
|
1510
|
+
"Mark or unmark a chat message as a favorite for easy retrieval later.",
|
|
1260
1511
|
{
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1512
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the conversation"),
|
|
1513
|
+
messageId: import_zod7.z.string().min(1).describe("ID of the specific message to favorite/unfavorite"),
|
|
1514
|
+
isFavorite: import_zod7.z.boolean().describe("true to mark as favorite, false to remove")
|
|
1515
|
+
},
|
|
1516
|
+
async (body) => {
|
|
1517
|
+
try {
|
|
1518
|
+
const result = await api.post("/v1/prompt/favorites", body);
|
|
1519
|
+
return {
|
|
1520
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1521
|
+
};
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
return {
|
|
1524
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1525
|
+
isError: true
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
);
|
|
1530
|
+
server.tool(
|
|
1531
|
+
"update_chat_title",
|
|
1532
|
+
"Update the title of a chat conversation for easier identification in history.",
|
|
1533
|
+
{
|
|
1534
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the conversation to rename"),
|
|
1535
|
+
title: import_zod7.z.string().min(1).describe("New title for the conversation")
|
|
1536
|
+
},
|
|
1537
|
+
async ({ promptId, title }) => {
|
|
1538
|
+
try {
|
|
1539
|
+
const result = await api.put(`/v1/prompt/${promptId}`, { title });
|
|
1540
|
+
return {
|
|
1541
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1542
|
+
};
|
|
1543
|
+
} catch (err) {
|
|
1544
|
+
return {
|
|
1545
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1546
|
+
isError: true
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
server.tool(
|
|
1552
|
+
"submit_chat_feedback",
|
|
1553
|
+
"Submit feedback on a chat response (thumbs up/down). Helps improve AI answer quality.",
|
|
1554
|
+
{
|
|
1555
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the conversation"),
|
|
1556
|
+
messageId: import_zod7.z.string().min(1).describe("ID of the message to rate"),
|
|
1557
|
+
score: import_zod7.z.number().describe("Feedback score: 1 for thumbs up, -1 for thumbs down"),
|
|
1558
|
+
reason: import_zod7.z.string().optional().describe("Optional explanation for the feedback")
|
|
1559
|
+
},
|
|
1560
|
+
async (body) => {
|
|
1561
|
+
try {
|
|
1562
|
+
const result = await api.post("/v1/prompt/feedback", body);
|
|
1563
|
+
return {
|
|
1564
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1565
|
+
};
|
|
1566
|
+
} catch (err) {
|
|
1567
|
+
return {
|
|
1568
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1569
|
+
isError: true
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
server.tool(
|
|
1575
|
+
"get_chat_statistics",
|
|
1576
|
+
"Get usage statistics for Magic Prompt / chat. Returns metrics on prompt usage, optionally filtered by date range.",
|
|
1577
|
+
{
|
|
1578
|
+
startDate: import_zod7.z.string().optional().describe("Start date for stats (ISO 8601)"),
|
|
1579
|
+
endDate: import_zod7.z.string().optional().describe("End date for stats (ISO 8601)")
|
|
1580
|
+
},
|
|
1581
|
+
async (params) => {
|
|
1582
|
+
try {
|
|
1583
|
+
const result = await api.get("/v1/prompt/statistics", { params });
|
|
1584
|
+
return {
|
|
1585
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1586
|
+
};
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
return {
|
|
1589
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
1590
|
+
isError: true
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
);
|
|
1595
|
+
server.tool(
|
|
1596
|
+
"export_chat_answer",
|
|
1597
|
+
"Export a Magic Prompt conversation or answer. Useful for saving AI-generated summaries, reports, or analysis results.",
|
|
1598
|
+
{
|
|
1599
|
+
promptId: import_zod7.z.string().min(1).describe("ID of the conversation to export")
|
|
1264
1600
|
},
|
|
1265
1601
|
async (body) => {
|
|
1266
1602
|
try {
|
|
1267
|
-
const result = await api.post("/v1/prompt", body);
|
|
1603
|
+
const result = await api.post("/v1/prompt/export", body);
|
|
1268
1604
|
return {
|
|
1269
1605
|
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
1270
1606
|
};
|
|
@@ -1277,12 +1613,13 @@ function register7(server, client) {
|
|
|
1277
1613
|
}
|
|
1278
1614
|
);
|
|
1279
1615
|
}
|
|
1280
|
-
var import_zod7;
|
|
1616
|
+
var import_zod7, import_shared2;
|
|
1281
1617
|
var init_prompt = __esm({
|
|
1282
1618
|
"src/tools/prompt.ts"() {
|
|
1283
1619
|
"use strict";
|
|
1284
1620
|
import_zod7 = require("zod");
|
|
1285
1621
|
init_client();
|
|
1622
|
+
import_shared2 = require("@speakai/shared");
|
|
1286
1623
|
}
|
|
1287
1624
|
});
|
|
1288
1625
|
|
|
@@ -1727,12 +2064,406 @@ var init_webhooks = __esm({
|
|
|
1727
2064
|
}
|
|
1728
2065
|
});
|
|
1729
2066
|
|
|
1730
|
-
// src/tools/
|
|
1731
|
-
var
|
|
1732
|
-
__export(
|
|
1733
|
-
|
|
2067
|
+
// src/tools/analytics.ts
|
|
2068
|
+
var analytics_exports = {};
|
|
2069
|
+
__export(analytics_exports, {
|
|
2070
|
+
register: () => register12
|
|
1734
2071
|
});
|
|
1735
|
-
function
|
|
2072
|
+
function register12(server, client) {
|
|
2073
|
+
const api = client ?? speakClient;
|
|
2074
|
+
server.tool(
|
|
2075
|
+
"search_media",
|
|
2076
|
+
[
|
|
2077
|
+
"Deep search across all media transcripts, insights, and metadata.",
|
|
2078
|
+
"Returns matching media with sentiment data, tags, and content excerpts.",
|
|
2079
|
+
"Use this to find specific topics, keywords, or themes across your entire library.",
|
|
2080
|
+
"For filtering by media type, folder, tags, or speakers, use the filterList parameter.",
|
|
2081
|
+
"Results are scoped by date range \u2014 defaults to current month if not specified."
|
|
2082
|
+
].join(" "),
|
|
2083
|
+
{
|
|
2084
|
+
query: import_zod12.z.string().min(1).describe("Search query \u2014 searches across transcripts, insights, and metadata"),
|
|
2085
|
+
startDate: import_zod12.z.string().optional().describe("Start date for search range (ISO 8601). Defaults to start of current month."),
|
|
2086
|
+
endDate: import_zod12.z.string().optional().describe("End date for search range (ISO 8601). Defaults to now."),
|
|
2087
|
+
filterList: import_zod12.z.array(
|
|
2088
|
+
import_zod12.z.object({
|
|
2089
|
+
fieldName: import_zod12.z.enum(Object.values(import_shared3.FilterFieldName)).describe("Field to filter on"),
|
|
2090
|
+
fieldOperator: import_zod12.z.enum(Object.values(import_shared3.FilterOperator)).describe("Filter operator"),
|
|
2091
|
+
fieldValue: import_zod12.z.array(import_zod12.z.string()).describe("Values to filter by"),
|
|
2092
|
+
fieldCondition: import_zod12.z.enum(Object.values(import_shared3.FilterCondition)).describe("Condition linking multiple filters")
|
|
2093
|
+
})
|
|
2094
|
+
).optional().describe("Advanced filters for narrowing search results by tags, speakers, media type, sentiment, folder, etc.")
|
|
2095
|
+
},
|
|
2096
|
+
async (params) => {
|
|
2097
|
+
try {
|
|
2098
|
+
const result = await api.post("/v1/analytics/search", params);
|
|
2099
|
+
return {
|
|
2100
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
2101
|
+
};
|
|
2102
|
+
} catch (err) {
|
|
2103
|
+
return {
|
|
2104
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2105
|
+
isError: true
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
var import_zod12, import_shared3;
|
|
2112
|
+
var init_analytics = __esm({
|
|
2113
|
+
"src/tools/analytics.ts"() {
|
|
2114
|
+
"use strict";
|
|
2115
|
+
import_zod12 = require("zod");
|
|
2116
|
+
init_client();
|
|
2117
|
+
import_shared3 = require("@speakai/shared");
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
|
|
2121
|
+
// src/tools/clips.ts
|
|
2122
|
+
var clips_exports = {};
|
|
2123
|
+
__export(clips_exports, {
|
|
2124
|
+
register: () => register13
|
|
2125
|
+
});
|
|
2126
|
+
function register13(server, client) {
|
|
2127
|
+
const api = client ?? speakClient;
|
|
2128
|
+
server.tool(
|
|
2129
|
+
"create_clip",
|
|
2130
|
+
[
|
|
2131
|
+
"Create a highlight clip from one or more media files by specifying time ranges.",
|
|
2132
|
+
`Clips are processed asynchronously (states: ${Object.values(import_shared4.ClipState).join(", ")}) \u2014 use get_clips to check status.`,
|
|
2133
|
+
"Maximum total clip duration is 30 minutes.",
|
|
2134
|
+
"Use multiple timeRanges to stitch segments from different media files together."
|
|
2135
|
+
].join(" "),
|
|
2136
|
+
{
|
|
2137
|
+
title: import_zod13.z.string().min(1).describe("Title for the clip"),
|
|
2138
|
+
mediaType: import_zod13.z.enum([import_shared4.MediaType.AUDIO, import_shared4.MediaType.VIDEO]).describe("Output media type"),
|
|
2139
|
+
timeRanges: import_zod13.z.array(
|
|
2140
|
+
import_zod13.z.object({
|
|
2141
|
+
mediaId: import_zod13.z.string().min(1).describe("Source media file ID"),
|
|
2142
|
+
startTime: import_zod13.z.number().min(0).describe("Start time in seconds"),
|
|
2143
|
+
endTime: import_zod13.z.number().describe("End time in seconds (must be > startTime)")
|
|
2144
|
+
})
|
|
2145
|
+
).min(1).describe("Array of time ranges to include in the clip. Each specifies a source media and start/end times."),
|
|
2146
|
+
description: import_zod13.z.string().optional().describe("Description of the clip"),
|
|
2147
|
+
tags: import_zod13.z.array(import_zod13.z.string()).optional().describe("Tags for the clip"),
|
|
2148
|
+
mergeStrategy: import_zod13.z.enum(["CONCATENATE"]).optional().describe("How to merge multiple segments (default: CONCATENATE)")
|
|
2149
|
+
},
|
|
2150
|
+
async (body) => {
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await api.post("/v1/clips", body);
|
|
2153
|
+
return {
|
|
2154
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
2155
|
+
};
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
return {
|
|
2158
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2159
|
+
isError: true
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2164
|
+
server.tool(
|
|
2165
|
+
"get_clips",
|
|
2166
|
+
"List clips, optionally filtered by folder or media files. If clipId is provided, returns a single clip with its download URL (when processed).",
|
|
2167
|
+
{
|
|
2168
|
+
clipId: import_zod13.z.string().optional().describe("Get a specific clip by ID"),
|
|
2169
|
+
folderId: import_zod13.z.string().optional().describe("Filter clips by folder ID"),
|
|
2170
|
+
mediaIds: import_zod13.z.array(import_zod13.z.string()).optional().describe("Filter clips by source media file IDs")
|
|
2171
|
+
},
|
|
2172
|
+
async ({ clipId, ...params }) => {
|
|
2173
|
+
try {
|
|
2174
|
+
const url = clipId ? `/v1/clips/${clipId}` : "/v1/clips";
|
|
2175
|
+
const result = await api.get(url, { params });
|
|
2176
|
+
return {
|
|
2177
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
2178
|
+
};
|
|
2179
|
+
} catch (err) {
|
|
2180
|
+
return {
|
|
2181
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2182
|
+
isError: true
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
);
|
|
2187
|
+
server.tool(
|
|
2188
|
+
"update_clip",
|
|
2189
|
+
"Update a clip's title, description, or tags.",
|
|
2190
|
+
{
|
|
2191
|
+
clipId: import_zod13.z.string().min(1).describe("ID of the clip to update"),
|
|
2192
|
+
title: import_zod13.z.string().optional().describe("New title"),
|
|
2193
|
+
description: import_zod13.z.string().optional().describe("New description"),
|
|
2194
|
+
tags: import_zod13.z.array(import_zod13.z.string()).optional().describe("New tags")
|
|
2195
|
+
},
|
|
2196
|
+
async ({ clipId, ...body }) => {
|
|
2197
|
+
try {
|
|
2198
|
+
const result = await api.put(`/v1/clips/${clipId}`, body);
|
|
2199
|
+
return {
|
|
2200
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
2201
|
+
};
|
|
2202
|
+
} catch (err) {
|
|
2203
|
+
return {
|
|
2204
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2205
|
+
isError: true
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
);
|
|
2210
|
+
server.tool(
|
|
2211
|
+
"delete_clip",
|
|
2212
|
+
"Permanently delete a clip and its associated media file.",
|
|
2213
|
+
{
|
|
2214
|
+
clipId: import_zod13.z.string().min(1).describe("ID of the clip to delete")
|
|
2215
|
+
},
|
|
2216
|
+
async ({ clipId }) => {
|
|
2217
|
+
try {
|
|
2218
|
+
const result = await api.delete(`/v1/clips/${clipId}`);
|
|
2219
|
+
return {
|
|
2220
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }]
|
|
2221
|
+
};
|
|
2222
|
+
} catch (err) {
|
|
2223
|
+
return {
|
|
2224
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2225
|
+
isError: true
|
|
2226
|
+
};
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2231
|
+
var import_zod13, import_shared4;
|
|
2232
|
+
var init_clips = __esm({
|
|
2233
|
+
"src/tools/clips.ts"() {
|
|
2234
|
+
"use strict";
|
|
2235
|
+
import_zod13 = require("zod");
|
|
2236
|
+
init_client();
|
|
2237
|
+
import_shared4 = require("@speakai/shared");
|
|
2238
|
+
}
|
|
2239
|
+
});
|
|
2240
|
+
|
|
2241
|
+
// src/media-utils.ts
|
|
2242
|
+
function isVideoFile(filePath) {
|
|
2243
|
+
return VIDEO_EXTENSIONS.includes(path.extname(filePath).toLowerCase());
|
|
2244
|
+
}
|
|
2245
|
+
function getMimeType(filePath) {
|
|
2246
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2247
|
+
const isVideo = isVideoFile(filePath);
|
|
2248
|
+
if (ext === ".mp4") return isVideo ? "video/mp4" : "audio/mp4";
|
|
2249
|
+
if (ext === ".webm") return isVideo ? "video/webm" : "audio/webm";
|
|
2250
|
+
return MIME_TYPES[ext] ?? (isVideo ? "video/mp4" : "audio/mpeg");
|
|
2251
|
+
}
|
|
2252
|
+
function detectMediaType(filePath) {
|
|
2253
|
+
return isVideoFile(filePath) ? "video" : "audio";
|
|
2254
|
+
}
|
|
2255
|
+
var path, VIDEO_EXTENSIONS, MIME_TYPES;
|
|
2256
|
+
var init_media_utils = __esm({
|
|
2257
|
+
"src/media-utils.ts"() {
|
|
2258
|
+
"use strict";
|
|
2259
|
+
path = __toESM(require("path"));
|
|
2260
|
+
VIDEO_EXTENSIONS = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".wmv"];
|
|
2261
|
+
MIME_TYPES = {
|
|
2262
|
+
".mp3": "audio/mpeg",
|
|
2263
|
+
".m4a": "audio/mp4",
|
|
2264
|
+
".wav": "audio/wav",
|
|
2265
|
+
".ogg": "audio/ogg",
|
|
2266
|
+
".flac": "audio/flac",
|
|
2267
|
+
".mov": "video/quicktime",
|
|
2268
|
+
".avi": "video/x-msvideo",
|
|
2269
|
+
".mkv": "video/x-matroska",
|
|
2270
|
+
".wmv": "video/x-ms-wmv"
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
});
|
|
2274
|
+
|
|
2275
|
+
// src/tools/workflows.ts
|
|
2276
|
+
var workflows_exports = {};
|
|
2277
|
+
__export(workflows_exports, {
|
|
2278
|
+
register: () => register14
|
|
2279
|
+
});
|
|
2280
|
+
function register14(server, client) {
|
|
2281
|
+
const api = client ?? speakClient;
|
|
2282
|
+
server.tool(
|
|
2283
|
+
"upload_and_analyze",
|
|
2284
|
+
[
|
|
2285
|
+
"Upload media from a URL, wait for processing to complete, then return the transcript and AI insights \u2014 all in one call.",
|
|
2286
|
+
"This is a convenience tool that combines upload_media + polling get_media_status + get_transcript + get_media_insights.",
|
|
2287
|
+
"Processing typically takes 1-3 minutes for audio under 60 minutes.",
|
|
2288
|
+
"Use this when you want the full analysis result without managing the polling loop yourself."
|
|
2289
|
+
].join(" "),
|
|
2290
|
+
{
|
|
2291
|
+
url: import_zod14.z.string().describe("Publicly accessible URL of the media file"),
|
|
2292
|
+
name: import_zod14.z.string().optional().describe("Display name for the media (defaults to filename from URL)"),
|
|
2293
|
+
mediaType: import_zod14.z.enum([import_shared5.MediaType.AUDIO, import_shared5.MediaType.VIDEO]).optional().describe("Media type (default: audio)"),
|
|
2294
|
+
sourceLanguage: import_zod14.z.string().optional().describe("BCP-47 language code (e.g., 'en-US', 'he-IL')"),
|
|
2295
|
+
folderId: import_zod14.z.string().optional().describe("Folder ID to place the media in"),
|
|
2296
|
+
tags: import_zod14.z.string().optional().describe("Comma-separated tags")
|
|
2297
|
+
},
|
|
2298
|
+
async (params) => {
|
|
2299
|
+
try {
|
|
2300
|
+
const uploadBody = {
|
|
2301
|
+
name: params.name ?? params.url.split("/").pop()?.split("?")[0] ?? "Upload",
|
|
2302
|
+
url: params.url,
|
|
2303
|
+
mediaType: params.mediaType ?? "audio"
|
|
2304
|
+
};
|
|
2305
|
+
if (params.sourceLanguage) uploadBody.sourceLanguage = params.sourceLanguage;
|
|
2306
|
+
if (params.folderId) uploadBody.folderId = params.folderId;
|
|
2307
|
+
if (params.tags) uploadBody.tags = params.tags;
|
|
2308
|
+
const uploadRes = await api.post("/v1/media/upload", uploadBody);
|
|
2309
|
+
const mediaId = uploadRes.data?.data?.mediaId;
|
|
2310
|
+
if (!mediaId) {
|
|
2311
|
+
return {
|
|
2312
|
+
content: [{ type: "text", text: `Error: Upload succeeded but no mediaId returned.
|
|
2313
|
+
${JSON.stringify(uploadRes.data, null, 2)}` }],
|
|
2314
|
+
isError: true
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
let state = uploadRes.data?.data?.state;
|
|
2318
|
+
let attempts = 0;
|
|
2319
|
+
while (state !== import_shared5.MediaState.PROCESSED && state !== import_shared5.MediaState.FAILED && attempts < MAX_POLL_ATTEMPTS) {
|
|
2320
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
2321
|
+
const statusRes = await api.get(`/v1/media/status/${mediaId}`);
|
|
2322
|
+
state = statusRes.data?.data?.state;
|
|
2323
|
+
attempts++;
|
|
2324
|
+
}
|
|
2325
|
+
if (state === import_shared5.MediaState.FAILED) {
|
|
2326
|
+
return {
|
|
2327
|
+
content: [{ type: "text", text: `Error: Processing failed for media ${mediaId}` }],
|
|
2328
|
+
isError: true
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
if (state !== import_shared5.MediaState.PROCESSED) {
|
|
2332
|
+
return {
|
|
2333
|
+
content: [{ type: "text", text: `Timeout: Media ${mediaId} is still processing (state: ${state}). Use get_media_status to check later.` }],
|
|
2334
|
+
isError: true
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
const [transcriptRes, insightsRes] = await Promise.all([
|
|
2338
|
+
api.get(`/v1/media/transcript/${mediaId}`),
|
|
2339
|
+
api.get(`/v1/media/insight/${mediaId}`)
|
|
2340
|
+
]);
|
|
2341
|
+
const result = {
|
|
2342
|
+
mediaId,
|
|
2343
|
+
state: "processed",
|
|
2344
|
+
transcript: transcriptRes.data?.data,
|
|
2345
|
+
insights: insightsRes.data?.data
|
|
2346
|
+
};
|
|
2347
|
+
return {
|
|
2348
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
2349
|
+
};
|
|
2350
|
+
} catch (err) {
|
|
2351
|
+
return {
|
|
2352
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2353
|
+
isError: true
|
|
2354
|
+
};
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
);
|
|
2358
|
+
server.tool(
|
|
2359
|
+
"upload_local_file",
|
|
2360
|
+
[
|
|
2361
|
+
"Upload a local file to Speak AI for transcription and analysis.",
|
|
2362
|
+
"Reads the file from disk, gets a pre-signed S3 URL, uploads the file, then creates the media entry.",
|
|
2363
|
+
"Works with any audio or video file on the local filesystem.",
|
|
2364
|
+
"After upload, use get_media_status to poll for completion, then get_transcript and get_media_insights."
|
|
2365
|
+
].join(" "),
|
|
2366
|
+
{
|
|
2367
|
+
filePath: import_zod14.z.string().describe("Absolute path to the local audio or video file"),
|
|
2368
|
+
name: import_zod14.z.string().optional().describe("Display name (defaults to filename)"),
|
|
2369
|
+
mediaType: import_zod14.z.enum([import_shared5.MediaType.AUDIO, import_shared5.MediaType.VIDEO]).optional().describe("Media type (auto-detected from extension if omitted)"),
|
|
2370
|
+
sourceLanguage: import_zod14.z.string().optional().describe("BCP-47 language code (e.g., 'en-US')"),
|
|
2371
|
+
folderId: import_zod14.z.string().optional().describe("Folder ID to place the media in"),
|
|
2372
|
+
tags: import_zod14.z.string().optional().describe("Comma-separated tags")
|
|
2373
|
+
},
|
|
2374
|
+
async (params) => {
|
|
2375
|
+
try {
|
|
2376
|
+
const filePath = params.filePath;
|
|
2377
|
+
if (!fs.existsSync(filePath)) {
|
|
2378
|
+
return {
|
|
2379
|
+
content: [{ type: "text", text: `Error: File not found: ${filePath}` }],
|
|
2380
|
+
isError: true
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
const filename = path2.basename(filePath);
|
|
2384
|
+
const isVideo = isVideoFile(filePath);
|
|
2385
|
+
const mediaType = params.mediaType ?? detectMediaType(filePath);
|
|
2386
|
+
const mimeType = getMimeType(filePath);
|
|
2387
|
+
const signedRes = await api.get("/v1/media/upload/signedurl", {
|
|
2388
|
+
params: { isVideo, filename, mimeType }
|
|
2389
|
+
});
|
|
2390
|
+
const signedData = signedRes.data?.data;
|
|
2391
|
+
const uploadUrl = signedData?.signedUrl ?? signedData?.url;
|
|
2392
|
+
const s3Key = signedData?.key ?? signedData?.s3Key;
|
|
2393
|
+
if (!uploadUrl) {
|
|
2394
|
+
return {
|
|
2395
|
+
content: [{ type: "text", text: `Error: Could not get signed upload URL.
|
|
2396
|
+
${JSON.stringify(signedRes.data, null, 2)}` }],
|
|
2397
|
+
isError: true
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
const fileBuffer = fs.readFileSync(filePath);
|
|
2401
|
+
const axios2 = (await import("axios")).default;
|
|
2402
|
+
await axios2.put(uploadUrl, fileBuffer, {
|
|
2403
|
+
headers: {
|
|
2404
|
+
"Content-Type": mimeType
|
|
2405
|
+
},
|
|
2406
|
+
maxBodyLength: Infinity,
|
|
2407
|
+
maxContentLength: Infinity
|
|
2408
|
+
});
|
|
2409
|
+
const createBody = {
|
|
2410
|
+
name: params.name ?? filename,
|
|
2411
|
+
url: uploadUrl.split("?")[0],
|
|
2412
|
+
// S3 URL without query params
|
|
2413
|
+
mediaType
|
|
2414
|
+
};
|
|
2415
|
+
if (s3Key) createBody.s3Key = s3Key;
|
|
2416
|
+
if (params.sourceLanguage) createBody.sourceLanguage = params.sourceLanguage;
|
|
2417
|
+
if (params.folderId) createBody.folderId = params.folderId;
|
|
2418
|
+
if (params.tags) createBody.tags = params.tags;
|
|
2419
|
+
const createRes = await api.post("/v1/media/upload", createBody);
|
|
2420
|
+
const data = createRes.data?.data;
|
|
2421
|
+
return {
|
|
2422
|
+
content: [
|
|
2423
|
+
{
|
|
2424
|
+
type: "text",
|
|
2425
|
+
text: JSON.stringify(
|
|
2426
|
+
{
|
|
2427
|
+
mediaId: data?.mediaId,
|
|
2428
|
+
state: data?.state,
|
|
2429
|
+
message: `File uploaded successfully. Use get_media_status to poll until state is 'processed', then use get_transcript and get_media_insights.`
|
|
2430
|
+
},
|
|
2431
|
+
null,
|
|
2432
|
+
2
|
|
2433
|
+
)
|
|
2434
|
+
}
|
|
2435
|
+
]
|
|
2436
|
+
};
|
|
2437
|
+
} catch (err) {
|
|
2438
|
+
return {
|
|
2439
|
+
content: [{ type: "text", text: `Error: ${formatAxiosError(err)}` }],
|
|
2440
|
+
isError: true
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
var import_zod14, import_shared5, fs, path2, POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS;
|
|
2447
|
+
var init_workflows = __esm({
|
|
2448
|
+
"src/tools/workflows.ts"() {
|
|
2449
|
+
"use strict";
|
|
2450
|
+
import_zod14 = require("zod");
|
|
2451
|
+
init_client();
|
|
2452
|
+
import_shared5 = require("@speakai/shared");
|
|
2453
|
+
fs = __toESM(require("fs"));
|
|
2454
|
+
path2 = __toESM(require("path"));
|
|
2455
|
+
init_media_utils();
|
|
2456
|
+
POLL_INTERVAL_MS = 5e3;
|
|
2457
|
+
MAX_POLL_ATTEMPTS = 120;
|
|
2458
|
+
}
|
|
2459
|
+
});
|
|
2460
|
+
|
|
2461
|
+
// src/tools/index.ts
|
|
2462
|
+
var tools_exports = {};
|
|
2463
|
+
__export(tools_exports, {
|
|
2464
|
+
registerAllTools: () => registerAllTools
|
|
2465
|
+
});
|
|
2466
|
+
function registerAllTools(server, client) {
|
|
1736
2467
|
for (const mod of modules) {
|
|
1737
2468
|
mod.register(server, client);
|
|
1738
2469
|
}
|
|
@@ -1752,6 +2483,9 @@ var init_tools = __esm({
|
|
|
1752
2483
|
init_fields();
|
|
1753
2484
|
init_automations();
|
|
1754
2485
|
init_webhooks();
|
|
2486
|
+
init_analytics();
|
|
2487
|
+
init_clips();
|
|
2488
|
+
init_workflows();
|
|
1755
2489
|
modules = [
|
|
1756
2490
|
media_exports,
|
|
1757
2491
|
text_exports,
|
|
@@ -1763,11 +2497,263 @@ var init_tools = __esm({
|
|
|
1763
2497
|
meeting_exports,
|
|
1764
2498
|
fields_exports,
|
|
1765
2499
|
automations_exports,
|
|
1766
|
-
webhooks_exports
|
|
2500
|
+
webhooks_exports,
|
|
2501
|
+
analytics_exports,
|
|
2502
|
+
clips_exports,
|
|
2503
|
+
workflows_exports
|
|
1767
2504
|
];
|
|
1768
2505
|
}
|
|
1769
2506
|
});
|
|
1770
2507
|
|
|
2508
|
+
// src/resources.ts
|
|
2509
|
+
var resources_exports = {};
|
|
2510
|
+
__export(resources_exports, {
|
|
2511
|
+
registerResources: () => registerResources
|
|
2512
|
+
});
|
|
2513
|
+
function registerResources(server, client) {
|
|
2514
|
+
const api = client ?? speakClient;
|
|
2515
|
+
server.resource(
|
|
2516
|
+
"media-library",
|
|
2517
|
+
"speakai://media",
|
|
2518
|
+
{ description: "List of all media files in your Speak AI workspace" },
|
|
2519
|
+
async () => {
|
|
2520
|
+
try {
|
|
2521
|
+
const result = await api.get("/v1/media", {
|
|
2522
|
+
params: { page: 0, pageSize: 50, sortBy: "createdAt:desc", filterMedia: 2 }
|
|
2523
|
+
});
|
|
2524
|
+
return {
|
|
2525
|
+
contents: [
|
|
2526
|
+
{
|
|
2527
|
+
uri: "speakai://media",
|
|
2528
|
+
mimeType: "application/json",
|
|
2529
|
+
text: JSON.stringify(result.data?.data, null, 2)
|
|
2530
|
+
}
|
|
2531
|
+
]
|
|
2532
|
+
};
|
|
2533
|
+
} catch {
|
|
2534
|
+
return { contents: [] };
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
);
|
|
2538
|
+
server.resource(
|
|
2539
|
+
"folders",
|
|
2540
|
+
"speakai://folders",
|
|
2541
|
+
{ description: "List of all folders in your Speak AI workspace" },
|
|
2542
|
+
async () => {
|
|
2543
|
+
try {
|
|
2544
|
+
const result = await api.get("/v1/folder", {
|
|
2545
|
+
params: { page: 0, pageSize: 100, sortBy: "createdAt:desc" }
|
|
2546
|
+
});
|
|
2547
|
+
return {
|
|
2548
|
+
contents: [
|
|
2549
|
+
{
|
|
2550
|
+
uri: "speakai://folders",
|
|
2551
|
+
mimeType: "application/json",
|
|
2552
|
+
text: JSON.stringify(result.data?.data, null, 2)
|
|
2553
|
+
}
|
|
2554
|
+
]
|
|
2555
|
+
};
|
|
2556
|
+
} catch {
|
|
2557
|
+
return { contents: [] };
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
);
|
|
2561
|
+
server.resource(
|
|
2562
|
+
"supported-languages",
|
|
2563
|
+
"speakai://languages",
|
|
2564
|
+
{ description: "List of supported transcription languages" },
|
|
2565
|
+
async () => {
|
|
2566
|
+
try {
|
|
2567
|
+
const result = await api.get("/v1/media/supportedLanguages");
|
|
2568
|
+
return {
|
|
2569
|
+
contents: [
|
|
2570
|
+
{
|
|
2571
|
+
uri: "speakai://languages",
|
|
2572
|
+
mimeType: "application/json",
|
|
2573
|
+
text: JSON.stringify(result.data?.data, null, 2)
|
|
2574
|
+
}
|
|
2575
|
+
]
|
|
2576
|
+
};
|
|
2577
|
+
} catch {
|
|
2578
|
+
return { contents: [] };
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
);
|
|
2582
|
+
server.resource(
|
|
2583
|
+
"transcript",
|
|
2584
|
+
new import_mcp.ResourceTemplate("speakai://media/{mediaId}/transcript", { list: void 0 }),
|
|
2585
|
+
{ description: "Full transcript for a specific media file" },
|
|
2586
|
+
async (uri, { mediaId }) => {
|
|
2587
|
+
try {
|
|
2588
|
+
const result = await api.get(`/v1/media/transcript/${mediaId}`);
|
|
2589
|
+
return {
|
|
2590
|
+
contents: [
|
|
2591
|
+
{
|
|
2592
|
+
uri: uri.href,
|
|
2593
|
+
mimeType: "application/json",
|
|
2594
|
+
text: JSON.stringify(result.data?.data, null, 2)
|
|
2595
|
+
}
|
|
2596
|
+
]
|
|
2597
|
+
};
|
|
2598
|
+
} catch {
|
|
2599
|
+
return { contents: [] };
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
);
|
|
2603
|
+
server.resource(
|
|
2604
|
+
"insights",
|
|
2605
|
+
new import_mcp.ResourceTemplate("speakai://media/{mediaId}/insights", { list: void 0 }),
|
|
2606
|
+
{ description: "AI-generated insights for a specific media file" },
|
|
2607
|
+
async (uri, { mediaId }) => {
|
|
2608
|
+
try {
|
|
2609
|
+
const result = await api.get(`/v1/media/insight/${mediaId}`);
|
|
2610
|
+
return {
|
|
2611
|
+
contents: [
|
|
2612
|
+
{
|
|
2613
|
+
uri: uri.href,
|
|
2614
|
+
mimeType: "application/json",
|
|
2615
|
+
text: JSON.stringify(result.data?.data, null, 2)
|
|
2616
|
+
}
|
|
2617
|
+
]
|
|
2618
|
+
};
|
|
2619
|
+
} catch {
|
|
2620
|
+
return { contents: [] };
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
);
|
|
2624
|
+
}
|
|
2625
|
+
var import_mcp;
|
|
2626
|
+
var init_resources = __esm({
|
|
2627
|
+
"src/resources.ts"() {
|
|
2628
|
+
"use strict";
|
|
2629
|
+
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
2630
|
+
init_client();
|
|
2631
|
+
}
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
// src/prompts.ts
|
|
2635
|
+
var prompts_exports = {};
|
|
2636
|
+
__export(prompts_exports, {
|
|
2637
|
+
registerPrompts: () => registerPrompts
|
|
2638
|
+
});
|
|
2639
|
+
function registerPrompts(server) {
|
|
2640
|
+
server.prompt(
|
|
2641
|
+
"analyze-meeting",
|
|
2642
|
+
"Upload a meeting recording and get a full analysis \u2014 transcript, insights, action items, and key takeaways.",
|
|
2643
|
+
{
|
|
2644
|
+
url: import_zod15.z.string().describe("URL of the meeting recording"),
|
|
2645
|
+
name: import_zod15.z.string().optional().describe("Meeting name (optional)")
|
|
2646
|
+
},
|
|
2647
|
+
async ({ url, name }) => ({
|
|
2648
|
+
messages: [
|
|
2649
|
+
{
|
|
2650
|
+
role: "user",
|
|
2651
|
+
content: {
|
|
2652
|
+
type: "text",
|
|
2653
|
+
text: [
|
|
2654
|
+
`Please analyze this meeting recording:`,
|
|
2655
|
+
``,
|
|
2656
|
+
`1. Upload "${name ?? "Meeting"}" from: ${url}`,
|
|
2657
|
+
`2. Wait for processing to complete`,
|
|
2658
|
+
`3. Get the full transcript and AI insights`,
|
|
2659
|
+
`4. Summarize:`,
|
|
2660
|
+
` - Key discussion points`,
|
|
2661
|
+
` - Action items with owners (if identifiable from speakers)`,
|
|
2662
|
+
` - Decisions made`,
|
|
2663
|
+
` - Open questions or follow-ups needed`,
|
|
2664
|
+
` - Overall sentiment`,
|
|
2665
|
+
``,
|
|
2666
|
+
`Use upload_and_analyze to handle the upload and processing in one step.`
|
|
2667
|
+
].join("\n")
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
]
|
|
2671
|
+
})
|
|
2672
|
+
);
|
|
2673
|
+
server.prompt(
|
|
2674
|
+
"research-across-media",
|
|
2675
|
+
"Search for themes, patterns, or topics across multiple recordings or your entire media library.",
|
|
2676
|
+
{
|
|
2677
|
+
topic: import_zod15.z.string().describe("The topic, theme, or question to research"),
|
|
2678
|
+
folder: import_zod15.z.string().optional().describe("Folder ID to scope the research (optional)")
|
|
2679
|
+
},
|
|
2680
|
+
async ({ topic, folder }) => ({
|
|
2681
|
+
messages: [
|
|
2682
|
+
{
|
|
2683
|
+
role: "user",
|
|
2684
|
+
content: {
|
|
2685
|
+
type: "text",
|
|
2686
|
+
text: [
|
|
2687
|
+
`Research this topic across my media library: "${topic}"`,
|
|
2688
|
+
``,
|
|
2689
|
+
folder ? `Scope: folder ${folder}` : `Scope: entire workspace`,
|
|
2690
|
+
``,
|
|
2691
|
+
`Steps:`,
|
|
2692
|
+
`1. Use search_media to find relevant media matching this topic`,
|
|
2693
|
+
`2. For the most relevant results, use ask_magic_prompt with the matching mediaIds to ask: "${topic}"`,
|
|
2694
|
+
`3. Synthesize findings across all results:`,
|
|
2695
|
+
` - Common themes and patterns`,
|
|
2696
|
+
` - Notable quotes or data points`,
|
|
2697
|
+
` - Contradictions or differing perspectives`,
|
|
2698
|
+
` - Trends over time (if date range is available)`,
|
|
2699
|
+
``,
|
|
2700
|
+
`Present a research summary with citations (media name + timestamp where possible).`
|
|
2701
|
+
].join("\n")
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
]
|
|
2705
|
+
})
|
|
2706
|
+
);
|
|
2707
|
+
server.prompt(
|
|
2708
|
+
"meeting-brief",
|
|
2709
|
+
"Prepare a brief from recent meetings \u2014 pull transcripts, extract decisions, and summarize open items.",
|
|
2710
|
+
{
|
|
2711
|
+
days: import_zod15.z.string().optional().describe("Number of days to look back (default: 7)"),
|
|
2712
|
+
folder: import_zod15.z.string().optional().describe("Folder ID to scope to (optional)")
|
|
2713
|
+
},
|
|
2714
|
+
async ({ days, folder }) => {
|
|
2715
|
+
const lookback = parseInt(days ?? "7");
|
|
2716
|
+
const fromDate = /* @__PURE__ */ new Date();
|
|
2717
|
+
fromDate.setDate(fromDate.getDate() - lookback);
|
|
2718
|
+
return {
|
|
2719
|
+
messages: [
|
|
2720
|
+
{
|
|
2721
|
+
role: "user",
|
|
2722
|
+
content: {
|
|
2723
|
+
type: "text",
|
|
2724
|
+
text: [
|
|
2725
|
+
`Prepare a meeting brief from the last ${lookback} days.`,
|
|
2726
|
+
``,
|
|
2727
|
+
folder ? `Scope: folder ${folder}` : `Scope: all media`,
|
|
2728
|
+
`Date range: ${fromDate.toISOString().split("T")[0]} to today`,
|
|
2729
|
+
``,
|
|
2730
|
+
`Steps:`,
|
|
2731
|
+
`1. Use list_media to find recent recordings (from: ${fromDate.toISOString().split("T")[0]})`,
|
|
2732
|
+
`2. For each meeting, use get_media_insights to get summaries and action items`,
|
|
2733
|
+
`3. Compile a brief with:`,
|
|
2734
|
+
` - Summary of each meeting (2-3 sentences)`,
|
|
2735
|
+
` - All action items consolidated (grouped by owner if possible)`,
|
|
2736
|
+
` - Key decisions made across meetings`,
|
|
2737
|
+
` - Open questions or unresolved topics`,
|
|
2738
|
+
` - Upcoming items that were mentioned`,
|
|
2739
|
+
``,
|
|
2740
|
+
`Format as a clean, scannable document.`
|
|
2741
|
+
].join("\n")
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
]
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
);
|
|
2748
|
+
}
|
|
2749
|
+
var import_zod15;
|
|
2750
|
+
var init_prompts = __esm({
|
|
2751
|
+
"src/prompts.ts"() {
|
|
2752
|
+
"use strict";
|
|
2753
|
+
import_zod15 = require("zod");
|
|
2754
|
+
}
|
|
2755
|
+
});
|
|
2756
|
+
|
|
1771
2757
|
// src/cli/config.ts
|
|
1772
2758
|
var config_exports = {};
|
|
1773
2759
|
__export(config_exports, {
|
|
@@ -1886,16 +2872,16 @@ function requireApiKey() {
|
|
|
1886
2872
|
resolveBaseUrl();
|
|
1887
2873
|
if (!key) {
|
|
1888
2874
|
printError(
|
|
1889
|
-
'No API key configured. Run "
|
|
2875
|
+
'No API key configured. Run "speakai-mcp config set-key" or set SPEAK_API_KEY.'
|
|
1890
2876
|
);
|
|
1891
2877
|
process.exit(1);
|
|
1892
2878
|
}
|
|
1893
2879
|
}
|
|
1894
2880
|
function createCli() {
|
|
1895
2881
|
const program = new import_commander.Command();
|
|
1896
|
-
program.name("
|
|
2882
|
+
program.name("speakai-mcp").description(
|
|
1897
2883
|
"Speak AI CLI & MCP Server \u2014 transcribe, analyze, and manage media from the command line"
|
|
1898
|
-
).version("
|
|
2884
|
+
).version("2.0.0");
|
|
1899
2885
|
const config = program.command("config").description("Manage configuration");
|
|
1900
2886
|
config.command("set-key").description("Set your Speak AI API key").argument("[key]", "API key (omit for interactive prompt)").action(async (key) => {
|
|
1901
2887
|
if (!key) {
|
|
@@ -1935,12 +2921,146 @@ function createCli() {
|
|
|
1935
2921
|
);
|
|
1936
2922
|
}
|
|
1937
2923
|
});
|
|
2924
|
+
config.command("test").description("Validate your API key and test connectivity").action(async () => {
|
|
2925
|
+
const key = resolveApiKey();
|
|
2926
|
+
resolveBaseUrl();
|
|
2927
|
+
if (!key) {
|
|
2928
|
+
printError('No API key configured. Run "speakai-mcp config set-key" or set SPEAK_API_KEY.');
|
|
2929
|
+
process.exit(1);
|
|
2930
|
+
}
|
|
2931
|
+
try {
|
|
2932
|
+
const axios2 = (await import("axios")).default;
|
|
2933
|
+
const baseUrl = process.env.SPEAK_BASE_URL ?? "https://api.speakai.co";
|
|
2934
|
+
const res = await axios2.post(
|
|
2935
|
+
`${baseUrl}/v1/auth/accessToken`,
|
|
2936
|
+
{},
|
|
2937
|
+
{ headers: { "Content-Type": "application/json", "x-speakai-key": key } }
|
|
2938
|
+
);
|
|
2939
|
+
if (res.data?.data?.accessToken) {
|
|
2940
|
+
printSuccess("API key is valid. Connection successful.");
|
|
2941
|
+
} else {
|
|
2942
|
+
printError("Unexpected response \u2014 key may be invalid.");
|
|
2943
|
+
process.exit(1);
|
|
2944
|
+
}
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
printError(`Authentication failed: ${err.response?.data?.message ?? err.message}`);
|
|
2947
|
+
process.exit(1);
|
|
2948
|
+
}
|
|
2949
|
+
});
|
|
1938
2950
|
config.command("set-url").description("Set custom API base URL").argument("<url>", "Base URL (e.g. https://api.speakai.co)").action((url) => {
|
|
1939
2951
|
const cfg = loadConfig();
|
|
1940
2952
|
cfg.baseUrl = url;
|
|
1941
2953
|
saveConfig(cfg);
|
|
1942
2954
|
printSuccess(`Base URL set to ${url}`);
|
|
1943
2955
|
});
|
|
2956
|
+
program.command("init").description("Interactive setup \u2014 configure API key and auto-detect MCP clients").action(async () => {
|
|
2957
|
+
const rl = (0, import_readline.createInterface)({ input: process.stdin, output: process.stdout });
|
|
2958
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, (a) => resolve(a.trim())));
|
|
2959
|
+
console.log("\n Speak AI MCP Server \u2014 Setup\n");
|
|
2960
|
+
const existingKey = resolveApiKey();
|
|
2961
|
+
let key = existingKey;
|
|
2962
|
+
if (existingKey) {
|
|
2963
|
+
console.log(` API key: ${existingKey.slice(0, 8)}... (already configured)`);
|
|
2964
|
+
const change = await ask(" Change it? (y/N) ");
|
|
2965
|
+
if (change.toLowerCase() === "y") key = "";
|
|
2966
|
+
}
|
|
2967
|
+
if (!key) {
|
|
2968
|
+
key = await ask(" Enter your Speak AI API key: ");
|
|
2969
|
+
if (!key) {
|
|
2970
|
+
printError("No key provided.");
|
|
2971
|
+
rl.close();
|
|
2972
|
+
process.exit(1);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
process.stdout.write(" Validating...");
|
|
2976
|
+
try {
|
|
2977
|
+
const axios2 = (await import("axios")).default;
|
|
2978
|
+
const baseUrl = process.env.SPEAK_BASE_URL ?? "https://api.speakai.co";
|
|
2979
|
+
const res = await axios2.post(
|
|
2980
|
+
`${baseUrl}/v1/auth/accessToken`,
|
|
2981
|
+
{},
|
|
2982
|
+
{ headers: { "Content-Type": "application/json", "x-speakai-key": key } }
|
|
2983
|
+
);
|
|
2984
|
+
if (!res.data?.data?.accessToken) throw new Error("Invalid response");
|
|
2985
|
+
console.log(" valid!\n");
|
|
2986
|
+
} catch {
|
|
2987
|
+
console.log(" failed!");
|
|
2988
|
+
printError("API key is invalid. Get your key at https://app.speakai.co/developers/apikeys");
|
|
2989
|
+
rl.close();
|
|
2990
|
+
process.exit(1);
|
|
2991
|
+
}
|
|
2992
|
+
const cfg = loadConfig();
|
|
2993
|
+
cfg.apiKey = key;
|
|
2994
|
+
saveConfig(cfg);
|
|
2995
|
+
printSuccess(`API key saved to ${getConfigPath()}`);
|
|
2996
|
+
const os2 = await import("os");
|
|
2997
|
+
const fs3 = await import("fs");
|
|
2998
|
+
const pathMod = await import("path");
|
|
2999
|
+
const home = os2.homedir();
|
|
3000
|
+
const clients = [
|
|
3001
|
+
{
|
|
3002
|
+
name: "Claude Desktop",
|
|
3003
|
+
configPath: process.platform === "darwin" ? pathMod.join(home, "Library/Application Support/Claude/claude_desktop_config.json") : pathMod.join(home, "AppData/Roaming/Claude/claude_desktop_config.json"),
|
|
3004
|
+
exists: false
|
|
3005
|
+
},
|
|
3006
|
+
{
|
|
3007
|
+
name: "Cursor",
|
|
3008
|
+
configPath: pathMod.join(home, ".cursor/mcp.json"),
|
|
3009
|
+
exists: false
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
name: "Windsurf",
|
|
3013
|
+
configPath: pathMod.join(home, ".windsurf/mcp.json"),
|
|
3014
|
+
exists: false
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
name: "VS Code",
|
|
3018
|
+
configPath: pathMod.join(home, ".vscode/mcp.json"),
|
|
3019
|
+
exists: false
|
|
3020
|
+
}
|
|
3021
|
+
];
|
|
3022
|
+
for (const c of clients) {
|
|
3023
|
+
const dir = pathMod.dirname(c.configPath);
|
|
3024
|
+
c.exists = fs3.existsSync(dir);
|
|
3025
|
+
}
|
|
3026
|
+
const detected = clients.filter((c) => c.exists);
|
|
3027
|
+
if (detected.length > 0) {
|
|
3028
|
+
console.log("\n Detected MCP clients:");
|
|
3029
|
+
for (const c of detected) {
|
|
3030
|
+
console.log(` - ${c.name}`);
|
|
3031
|
+
}
|
|
3032
|
+
const configure = await ask("\n Auto-configure MCP server in these clients? (Y/n) ");
|
|
3033
|
+
if (configure.toLowerCase() !== "n") {
|
|
3034
|
+
const mcpEntry = {
|
|
3035
|
+
command: "npx",
|
|
3036
|
+
args: ["-y", "@speakai/mcp-server"],
|
|
3037
|
+
env: { SPEAK_API_KEY: key }
|
|
3038
|
+
};
|
|
3039
|
+
for (const c of detected) {
|
|
3040
|
+
try {
|
|
3041
|
+
let config2 = {};
|
|
3042
|
+
if (fs3.existsSync(c.configPath)) {
|
|
3043
|
+
config2 = JSON.parse(fs3.readFileSync(c.configPath, "utf-8"));
|
|
3044
|
+
}
|
|
3045
|
+
const servers = config2.mcpServers ?? {};
|
|
3046
|
+
servers["speak-ai"] = mcpEntry;
|
|
3047
|
+
config2.mcpServers = servers;
|
|
3048
|
+
const dir = pathMod.dirname(c.configPath);
|
|
3049
|
+
if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
|
|
3050
|
+
fs3.writeFileSync(c.configPath, JSON.stringify(config2, null, 2) + "\n");
|
|
3051
|
+
printSuccess(`Configured ${c.name}: ${c.configPath}`);
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
printError(`Failed to configure ${c.name}: ${err.message}`);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
console.log("\n For Claude Code, run:");
|
|
3059
|
+
console.log(` export SPEAK_API_KEY="your-api-key"`);
|
|
3060
|
+
console.log(" claude mcp add speak-ai -- npx -y @speakai/mcp-server\n");
|
|
3061
|
+
rl.close();
|
|
3062
|
+
printSuccess("Setup complete! You're ready to go.");
|
|
3063
|
+
});
|
|
1944
3064
|
program.command("list-media").alias("ls").description("List media files").option("-t, --type <type>", "Filter by type (audio, video, text)").option("-p, --page <n>", "Page number (0-based)", "0").option("-s, --page-size <n>", "Results per page", "20").option("--sort <field>", "Sort field", "createdAt:desc").option("-f, --folder <id>", "Filter by folder ID").option("-n, --name <filter>", "Filter by name").option("--favorites", "Show only favorites").option("--json", "Output raw JSON").action(async (opts) => {
|
|
1945
3065
|
requireApiKey();
|
|
1946
3066
|
const client = await getClient();
|
|
@@ -2056,41 +3176,89 @@ function createCli() {
|
|
|
2056
3176
|
process.exit(1);
|
|
2057
3177
|
}
|
|
2058
3178
|
});
|
|
2059
|
-
program.command("upload").description("Upload media from a URL").argument("<
|
|
3179
|
+
program.command("upload").description("Upload media from a URL or local file").argument("<source>", "Media URL or local file path").option("-n, --name <name>", "Display name").option("-t, --type <type>", "Media type (audio or video)").option("-l, --language <lang>", "Source language (BCP-47)", "en-US").option("-f, --folder <id>", "Destination folder ID").option("--tags <tags>", "Comma-separated tags").option("--wait", "Wait for processing to complete").option("--json", "Output raw JSON").action(async (source, opts) => {
|
|
2060
3180
|
requireApiKey();
|
|
2061
3181
|
const client = await getClient();
|
|
2062
3182
|
try {
|
|
2063
|
-
const
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
3183
|
+
const fs3 = await import("fs");
|
|
3184
|
+
const pathMod = await import("path");
|
|
3185
|
+
const isLocalFile = fs3.existsSync(source);
|
|
3186
|
+
let mediaId;
|
|
3187
|
+
let state;
|
|
3188
|
+
if (isLocalFile) {
|
|
3189
|
+
const filename = pathMod.basename(source);
|
|
3190
|
+
const isVideo = isVideoFile(source);
|
|
3191
|
+
const mediaType = opts.type ?? detectMediaType(source);
|
|
3192
|
+
const mimeType = getMimeType(source);
|
|
3193
|
+
const signedRes = await client.get("/v1/media/upload/signedurl", {
|
|
3194
|
+
params: { isVideo, filename, mimeType }
|
|
3195
|
+
});
|
|
3196
|
+
const signedData = signedRes.data?.data;
|
|
3197
|
+
const uploadUrl = signedData?.signedUrl ?? signedData?.url;
|
|
3198
|
+
if (!uploadUrl) {
|
|
3199
|
+
printError("Could not get signed upload URL");
|
|
3200
|
+
process.exit(1);
|
|
3201
|
+
}
|
|
3202
|
+
process.stdout.write("Uploading...");
|
|
3203
|
+
const fileBuffer = fs3.readFileSync(source);
|
|
3204
|
+
const axios2 = (await import("axios")).default;
|
|
3205
|
+
await axios2.put(uploadUrl, fileBuffer, {
|
|
3206
|
+
headers: { "Content-Type": mimeType },
|
|
3207
|
+
maxBodyLength: Infinity,
|
|
3208
|
+
maxContentLength: Infinity
|
|
3209
|
+
});
|
|
3210
|
+
console.log(" done");
|
|
3211
|
+
const createBody = {
|
|
3212
|
+
name: opts.name ?? filename,
|
|
3213
|
+
url: uploadUrl.split("?")[0],
|
|
3214
|
+
mediaType,
|
|
3215
|
+
sourceLanguage: opts.language
|
|
3216
|
+
};
|
|
3217
|
+
if (opts.folder) createBody.folderId = opts.folder;
|
|
3218
|
+
if (opts.tags) createBody.tags = opts.tags;
|
|
3219
|
+
const res = await client.post("/v1/media/upload", createBody);
|
|
3220
|
+
const data = res.data?.data;
|
|
3221
|
+
mediaId = data?.mediaId;
|
|
3222
|
+
state = data?.state;
|
|
3223
|
+
} else {
|
|
3224
|
+
const body = {
|
|
3225
|
+
name: opts.name ?? source.split("/").pop()?.split("?")[0] ?? "Upload",
|
|
3226
|
+
url: source,
|
|
3227
|
+
mediaType: opts.type ?? "audio",
|
|
3228
|
+
sourceLanguage: opts.language
|
|
3229
|
+
};
|
|
3230
|
+
if (opts.folder) body.folderId = opts.folder;
|
|
3231
|
+
if (opts.tags) body.tags = opts.tags;
|
|
3232
|
+
const res = await client.post("/v1/media/upload", body);
|
|
3233
|
+
const data = res.data?.data;
|
|
3234
|
+
if (opts.json && !opts.wait) {
|
|
3235
|
+
printJson(data);
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
mediaId = data?.mediaId;
|
|
3239
|
+
state = data?.state;
|
|
2076
3240
|
}
|
|
2077
|
-
|
|
2078
|
-
printSuccess(`Uploaded: ${mediaId} (state: ${data?.state})`);
|
|
3241
|
+
printSuccess(`Uploaded: ${mediaId} (state: ${state})`);
|
|
2079
3242
|
if (opts.wait && mediaId) {
|
|
2080
3243
|
process.stdout.write("Processing");
|
|
2081
|
-
let
|
|
2082
|
-
|
|
3244
|
+
let attempts = 0;
|
|
3245
|
+
const maxAttempts = 120;
|
|
3246
|
+
while (state !== "processed" && state !== "failed" && attempts < maxAttempts) {
|
|
2083
3247
|
await new Promise((r) => setTimeout(r, 5e3));
|
|
2084
3248
|
process.stdout.write(".");
|
|
2085
3249
|
const statusRes = await client.get(`/v1/media/status/${mediaId}`);
|
|
2086
|
-
|
|
3250
|
+
state = statusRes.data?.data?.state;
|
|
3251
|
+
attempts++;
|
|
2087
3252
|
}
|
|
2088
3253
|
console.log();
|
|
2089
|
-
if (
|
|
3254
|
+
if (state === "processed") {
|
|
2090
3255
|
printSuccess(`Done! Media ${mediaId} is ready.`);
|
|
2091
|
-
} else {
|
|
3256
|
+
} else if (state === "failed") {
|
|
2092
3257
|
printError(`Processing failed for ${mediaId}`);
|
|
2093
3258
|
process.exit(1);
|
|
3259
|
+
} else {
|
|
3260
|
+
printError(`Timeout: ${mediaId} still processing (state: ${state}). Check with: speakai-mcp status ${mediaId}`);
|
|
3261
|
+
process.exit(1);
|
|
2094
3262
|
}
|
|
2095
3263
|
}
|
|
2096
3264
|
} catch (err) {
|
|
@@ -2201,26 +3369,291 @@ function createCli() {
|
|
|
2201
3369
|
process.exit(1);
|
|
2202
3370
|
}
|
|
2203
3371
|
});
|
|
2204
|
-
program.command("ask").description("Ask an AI question about
|
|
3372
|
+
program.command("ask").description("Ask an AI question about media files, folders, or your entire workspace").argument("<prompt>", "Your question").argument("[mediaId]", "Optional media file ID (shorthand for -m <id>)").option("-m, --media <ids...>", "Media file IDs to query (space-separated)").option("-f, --folder <ids...>", "Folder IDs to scope the query to").option("--assistant <type>", "Assistant type (general, researcher, marketer, sales, recruiter)", "general").option("--speakers <ids...>", "Filter by speaker IDs").option("--tags <tags...>", "Filter by tags").option("--from <date>", "Start date (ISO 8601)").option("--to <date>", "End date (ISO 8601)").option("--individual", "Process each media file separately").option("--continue <promptId>", "Continue an existing conversation").option("--json", "Output raw JSON").action(async (prompt, mediaId, opts) => {
|
|
2205
3373
|
requireApiKey();
|
|
2206
3374
|
const client = await getClient();
|
|
2207
3375
|
try {
|
|
2208
|
-
const
|
|
2209
|
-
mediaIds: [mediaId],
|
|
3376
|
+
const body = {
|
|
2210
3377
|
prompt,
|
|
2211
3378
|
assistantType: opts.assistant
|
|
2212
|
-
}
|
|
3379
|
+
};
|
|
3380
|
+
if (mediaId) body.mediaIds = [mediaId];
|
|
3381
|
+
if (opts.media) body.mediaIds = opts.media;
|
|
3382
|
+
if (opts.folder) body.folderIds = opts.folder;
|
|
3383
|
+
if (opts.speakers) body.speakers = opts.speakers;
|
|
3384
|
+
if (opts.tags) body.tags = opts.tags;
|
|
3385
|
+
if (opts.from) body.startDate = opts.from;
|
|
3386
|
+
if (opts.to) body.endDate = opts.to;
|
|
3387
|
+
if (opts.individual) body.isIndividualPrompt = true;
|
|
3388
|
+
if (opts.continue) body.promptId = opts.continue;
|
|
3389
|
+
const res = await client.post("/v1/prompt", body);
|
|
2213
3390
|
const data = res.data?.data;
|
|
2214
3391
|
if (opts.json) {
|
|
2215
3392
|
printJson(data);
|
|
2216
3393
|
} else {
|
|
2217
3394
|
console.log(data?.answer ?? data?.message ?? JSON.stringify(data, null, 2));
|
|
3395
|
+
if (data?.promptId) {
|
|
3396
|
+
console.log(`
|
|
3397
|
+
(conversation: ${data.promptId} \u2014 use --continue to follow up)`);
|
|
3398
|
+
}
|
|
2218
3399
|
}
|
|
2219
3400
|
} catch (err) {
|
|
2220
3401
|
printError(err.response?.data?.message ?? err.message);
|
|
2221
3402
|
process.exit(1);
|
|
2222
3403
|
}
|
|
2223
3404
|
});
|
|
3405
|
+
program.command("chat-history").description("List past Magic Prompt conversations").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3406
|
+
requireApiKey();
|
|
3407
|
+
const client = await getClient();
|
|
3408
|
+
try {
|
|
3409
|
+
const res = await client.get("/v1/prompt/history");
|
|
3410
|
+
const data = res.data?.data;
|
|
3411
|
+
if (opts.json) {
|
|
3412
|
+
printJson(data);
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
const items = Array.isArray(data) ? data : data?.prompts ?? data?.history ?? [];
|
|
3416
|
+
printTable(items, [
|
|
3417
|
+
{ key: "_id", label: "ID", width: 26 },
|
|
3418
|
+
{ key: "title", label: "Title", width: 40 },
|
|
3419
|
+
{ key: "createdAt", label: "Created", width: 20 }
|
|
3420
|
+
]);
|
|
3421
|
+
} catch (err) {
|
|
3422
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3423
|
+
process.exit(1);
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
program.command("search").description("Search across all media transcripts, insights, and metadata").argument("<query>", "Search query").option("--from <date>", "Start date (ISO 8601, defaults to start of month)").option("--to <date>", "End date (ISO 8601, defaults to now)").option("--json", "Output raw JSON").action(async (query, opts) => {
|
|
3427
|
+
requireApiKey();
|
|
3428
|
+
const client = await getClient();
|
|
3429
|
+
try {
|
|
3430
|
+
const body = { query };
|
|
3431
|
+
if (opts.from) body.startDate = opts.from;
|
|
3432
|
+
if (opts.to) body.endDate = opts.to;
|
|
3433
|
+
const res = await client.post("/v1/analytics/search", body);
|
|
3434
|
+
const data = res.data?.data;
|
|
3435
|
+
if (opts.json) {
|
|
3436
|
+
printJson(data);
|
|
3437
|
+
return;
|
|
3438
|
+
}
|
|
3439
|
+
const items = Array.isArray(data) ? data : data?.results ?? data?.mediaNodes ?? [];
|
|
3440
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
3441
|
+
console.log(`Found ${items.length} result(s)
|
|
3442
|
+
`);
|
|
3443
|
+
printTable(items, [
|
|
3444
|
+
{ key: "_id", label: "ID", width: 14 },
|
|
3445
|
+
{ key: "name", label: "Name", width: 35 },
|
|
3446
|
+
{ key: "mediaType", label: "Type", width: 6 },
|
|
3447
|
+
{ key: "tags", label: "Tags", width: 20 }
|
|
3448
|
+
]);
|
|
3449
|
+
} else {
|
|
3450
|
+
printJson(data);
|
|
3451
|
+
}
|
|
3452
|
+
} catch (err) {
|
|
3453
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3454
|
+
process.exit(1);
|
|
3455
|
+
}
|
|
3456
|
+
});
|
|
3457
|
+
program.command("clips").description("List clips, optionally for a specific media file").option("-m, --media <ids...>", "Filter by source media IDs").option("-f, --folder <id>", "Filter by folder ID").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3458
|
+
requireApiKey();
|
|
3459
|
+
const client = await getClient();
|
|
3460
|
+
try {
|
|
3461
|
+
const params = {};
|
|
3462
|
+
if (opts.media) params.mediaIds = opts.media;
|
|
3463
|
+
if (opts.folder) params.folderId = opts.folder;
|
|
3464
|
+
const res = await client.get("/v1/clips", { params });
|
|
3465
|
+
const data = res.data?.data;
|
|
3466
|
+
if (opts.json) {
|
|
3467
|
+
printJson(data);
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
const items = Array.isArray(data) ? data : data?.clips ?? [];
|
|
3471
|
+
printTable(items, [
|
|
3472
|
+
{ key: "clipId", label: "ID", width: 14 },
|
|
3473
|
+
{ key: "title", label: "Title", width: 30 },
|
|
3474
|
+
{ key: "state", label: "Status", width: 12 },
|
|
3475
|
+
{ key: "duration", label: "Duration", width: 10 },
|
|
3476
|
+
{ key: "createdAt", label: "Created", width: 20 }
|
|
3477
|
+
]);
|
|
3478
|
+
} catch (err) {
|
|
3479
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3480
|
+
process.exit(1);
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
program.command("clip").description("Create a clip from a media file").argument("<mediaId>", "Source media file ID").requiredOption("--start <seconds>", "Start time in seconds").requiredOption("--end <seconds>", "End time in seconds").option("-n, --name <title>", "Clip title", "Clip").option("-t, --type <type>", "Media type (audio or video)", "audio").option("--description <text>", "Clip description").option("--tags <tags...>", "Tags for the clip").option("--json", "Output raw JSON").action(async (mediaId, opts) => {
|
|
3484
|
+
requireApiKey();
|
|
3485
|
+
const client = await getClient();
|
|
3486
|
+
try {
|
|
3487
|
+
const body = {
|
|
3488
|
+
title: opts.name,
|
|
3489
|
+
mediaType: opts.type,
|
|
3490
|
+
timeRanges: [
|
|
3491
|
+
{
|
|
3492
|
+
mediaId,
|
|
3493
|
+
startTime: parseFloat(opts.start),
|
|
3494
|
+
endTime: parseFloat(opts.end)
|
|
3495
|
+
}
|
|
3496
|
+
]
|
|
3497
|
+
};
|
|
3498
|
+
if (opts.description) body.description = opts.description;
|
|
3499
|
+
if (opts.tags) body.tags = opts.tags;
|
|
3500
|
+
const res = await client.post("/v1/clips", body);
|
|
3501
|
+
const data = res.data?.data;
|
|
3502
|
+
if (opts.json) {
|
|
3503
|
+
printJson(data);
|
|
3504
|
+
} else {
|
|
3505
|
+
printSuccess(`Clip created: ${data?.clipId ?? data?._id ?? "OK"} (processing...)`);
|
|
3506
|
+
}
|
|
3507
|
+
} catch (err) {
|
|
3508
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3509
|
+
process.exit(1);
|
|
3510
|
+
}
|
|
3511
|
+
});
|
|
3512
|
+
program.command("delete").description("Delete a media file").argument("<mediaId>", "Media file ID to delete").action(async (mediaId) => {
|
|
3513
|
+
requireApiKey();
|
|
3514
|
+
const client = await getClient();
|
|
3515
|
+
try {
|
|
3516
|
+
await client.delete(`/v1/media/${mediaId}`);
|
|
3517
|
+
printSuccess(`Deleted: ${mediaId}`);
|
|
3518
|
+
} catch (err) {
|
|
3519
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
3523
|
+
program.command("update").description("Update media metadata").argument("<mediaId>", "Media file ID to update").option("-n, --name <name>", "New display name").option("-d, --description <text>", "New description").option("--tags <tags...>", "New tags").option("-f, --folder <id>", "Move to folder ID").option("--json", "Output raw JSON").action(async (mediaId, opts) => {
|
|
3524
|
+
requireApiKey();
|
|
3525
|
+
const client = await getClient();
|
|
3526
|
+
try {
|
|
3527
|
+
const body = {};
|
|
3528
|
+
if (opts.name) body.name = opts.name;
|
|
3529
|
+
if (opts.description) body.description = opts.description;
|
|
3530
|
+
if (opts.tags) body.tags = opts.tags;
|
|
3531
|
+
if (opts.folder) body.folderId = opts.folder;
|
|
3532
|
+
if (Object.keys(body).length === 0) {
|
|
3533
|
+
printError("Provide at least one field to update (--name, --description, --tags, --folder)");
|
|
3534
|
+
process.exit(1);
|
|
3535
|
+
}
|
|
3536
|
+
const res = await client.put(`/v1/media/${mediaId}`, body);
|
|
3537
|
+
const data = res.data?.data;
|
|
3538
|
+
if (opts.json) {
|
|
3539
|
+
printJson(data);
|
|
3540
|
+
} else {
|
|
3541
|
+
printSuccess(`Updated: ${mediaId}`);
|
|
3542
|
+
}
|
|
3543
|
+
} catch (err) {
|
|
3544
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3545
|
+
process.exit(1);
|
|
3546
|
+
}
|
|
3547
|
+
});
|
|
3548
|
+
program.command("create-folder").description("Create a new folder").argument("<name>", "Folder name").option("--json", "Output raw JSON").action(async (name, opts) => {
|
|
3549
|
+
requireApiKey();
|
|
3550
|
+
const client = await getClient();
|
|
3551
|
+
try {
|
|
3552
|
+
const res = await client.post("/v1/folder", { name });
|
|
3553
|
+
const data = res.data?.data;
|
|
3554
|
+
if (opts.json) {
|
|
3555
|
+
printJson(data);
|
|
3556
|
+
} else {
|
|
3557
|
+
printSuccess(`Folder created: ${data?._id ?? "OK"} \u2014 ${name}`);
|
|
3558
|
+
}
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3561
|
+
process.exit(1);
|
|
3562
|
+
}
|
|
3563
|
+
});
|
|
3564
|
+
program.command("favorites").description("Toggle favorite status for a media file").argument("<mediaId>", "Media file ID").action(async (mediaId) => {
|
|
3565
|
+
requireApiKey();
|
|
3566
|
+
const client = await getClient();
|
|
3567
|
+
try {
|
|
3568
|
+
const res = await client.post("/v1/media/favorites", { mediaId });
|
|
3569
|
+
const data = res.data?.data;
|
|
3570
|
+
printSuccess(data?.message ?? `Favorite toggled for ${mediaId}`);
|
|
3571
|
+
} catch (err) {
|
|
3572
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3573
|
+
process.exit(1);
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
program.command("stats").description("Show workspace media statistics").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3577
|
+
requireApiKey();
|
|
3578
|
+
const client = await getClient();
|
|
3579
|
+
try {
|
|
3580
|
+
const res = await client.get("/v1/media/statistics");
|
|
3581
|
+
const data = res.data?.data;
|
|
3582
|
+
if (opts.json) {
|
|
3583
|
+
printJson(data);
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
const total = data?.totalCount ?? data?.total ?? "\u2014";
|
|
3587
|
+
const audio = data?.audioCount ?? data?.audio ?? "\u2014";
|
|
3588
|
+
const video = data?.videoCount ?? data?.video ?? "\u2014";
|
|
3589
|
+
const text = data?.textCount ?? data?.text ?? "\u2014";
|
|
3590
|
+
console.log(`Total media: ${total}`);
|
|
3591
|
+
console.log(` Audio: ${audio}`);
|
|
3592
|
+
console.log(` Video: ${video}`);
|
|
3593
|
+
console.log(` Text: ${text}`);
|
|
3594
|
+
if (data?.totalDuration) {
|
|
3595
|
+
const hrs = Math.round(data.totalDuration / 3600 * 10) / 10;
|
|
3596
|
+
console.log(`Duration: ${hrs}h total`);
|
|
3597
|
+
}
|
|
3598
|
+
if (data?.totalSize) {
|
|
3599
|
+
const gb = Math.round(data.totalSize / (1024 * 1024 * 1024) * 100) / 100;
|
|
3600
|
+
console.log(`Storage: ${gb} GB`);
|
|
3601
|
+
}
|
|
3602
|
+
} catch (err) {
|
|
3603
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3604
|
+
process.exit(1);
|
|
3605
|
+
}
|
|
3606
|
+
});
|
|
3607
|
+
program.command("languages").description("List supported transcription languages").option("--json", "Output raw JSON").action(async (opts) => {
|
|
3608
|
+
requireApiKey();
|
|
3609
|
+
const client = await getClient();
|
|
3610
|
+
try {
|
|
3611
|
+
const res = await client.get("/v1/media/supportedLanguages");
|
|
3612
|
+
const data = res.data?.data;
|
|
3613
|
+
if (opts.json) {
|
|
3614
|
+
printJson(data);
|
|
3615
|
+
} else {
|
|
3616
|
+
const langs = Array.isArray(data) ? data : data?.languages ?? [];
|
|
3617
|
+
for (const lang of langs) {
|
|
3618
|
+
const name = typeof lang === "string" ? lang : lang.name ?? lang.code ?? JSON.stringify(lang);
|
|
3619
|
+
console.log(` ${name}`);
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
} catch (err) {
|
|
3623
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3624
|
+
process.exit(1);
|
|
3625
|
+
}
|
|
3626
|
+
});
|
|
3627
|
+
program.command("captions").description("Get captions for a media file").argument("<mediaId>", "Media file ID").option("--json", "Output raw JSON").action(async (mediaId, opts) => {
|
|
3628
|
+
requireApiKey();
|
|
3629
|
+
const client = await getClient();
|
|
3630
|
+
try {
|
|
3631
|
+
const res = await client.get(`/v1/media/caption/${mediaId}`);
|
|
3632
|
+
const data = res.data?.data;
|
|
3633
|
+
if (opts.json) {
|
|
3634
|
+
printJson(data);
|
|
3635
|
+
} else {
|
|
3636
|
+
const captions = Array.isArray(data) ? data : data?.captions ?? [];
|
|
3637
|
+
for (const cap of captions) {
|
|
3638
|
+
console.log(cap.text ?? cap);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
} catch (err) {
|
|
3642
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3643
|
+
process.exit(1);
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
program.command("reanalyze").description("Re-run AI analysis on a media file with latest models").argument("<mediaId>", "Media file ID").action(async (mediaId) => {
|
|
3647
|
+
requireApiKey();
|
|
3648
|
+
const client = await getClient();
|
|
3649
|
+
try {
|
|
3650
|
+
await client.get(`/v1/media/reanalyze/${mediaId}`);
|
|
3651
|
+
printSuccess(`Re-analysis started for ${mediaId}`);
|
|
3652
|
+
} catch (err) {
|
|
3653
|
+
printError(err.response?.data?.message ?? err.message);
|
|
3654
|
+
process.exit(1);
|
|
3655
|
+
}
|
|
3656
|
+
});
|
|
2224
3657
|
program.command("schedule-meeting").description("Schedule AI assistant to join a meeting").argument("<url>", "Meeting URL (Zoom, Meet, Teams)").option("-t, --title <title>", "Meeting title").option("-d, --date <datetime>", "Meeting date/time (ISO 8601, omit to join now)").option("-l, --language <lang>", "Meeting language", "en-US").option("--json", "Output raw JSON").action(async (url, opts) => {
|
|
2225
3658
|
requireApiKey();
|
|
2226
3659
|
const client = await getClient();
|
|
@@ -2257,6 +3690,7 @@ var init_cli = __esm({
|
|
|
2257
3690
|
import_readline = require("readline");
|
|
2258
3691
|
init_config();
|
|
2259
3692
|
init_format();
|
|
3693
|
+
init_media_utils();
|
|
2260
3694
|
}
|
|
2261
3695
|
});
|
|
2262
3696
|
|
|
@@ -2265,14 +3699,19 @@ var index_exports = {};
|
|
|
2265
3699
|
__export(index_exports, {
|
|
2266
3700
|
createSpeakClient: () => createSpeakClient,
|
|
2267
3701
|
formatAxiosError: () => formatAxiosError,
|
|
2268
|
-
registerAllTools: () => registerAllTools
|
|
3702
|
+
registerAllTools: () => registerAllTools,
|
|
3703
|
+
registerPrompts: () => registerPrompts,
|
|
3704
|
+
registerResources: () => registerResources
|
|
2269
3705
|
});
|
|
2270
3706
|
module.exports = __toCommonJS(index_exports);
|
|
2271
3707
|
init_tools();
|
|
3708
|
+
init_resources();
|
|
3709
|
+
init_prompts();
|
|
2272
3710
|
init_client();
|
|
2273
3711
|
var args = process.argv.slice(2);
|
|
2274
3712
|
var cliCommands = [
|
|
2275
3713
|
"config",
|
|
3714
|
+
"init",
|
|
2276
3715
|
"list-media",
|
|
2277
3716
|
"ls",
|
|
2278
3717
|
"get-transcript",
|
|
@@ -2286,6 +3725,18 @@ var cliCommands = [
|
|
|
2286
3725
|
"list-folders",
|
|
2287
3726
|
"folders",
|
|
2288
3727
|
"ask",
|
|
3728
|
+
"chat-history",
|
|
3729
|
+
"search",
|
|
3730
|
+
"delete",
|
|
3731
|
+
"update",
|
|
3732
|
+
"create-folder",
|
|
3733
|
+
"favorites",
|
|
3734
|
+
"stats",
|
|
3735
|
+
"languages",
|
|
3736
|
+
"captions",
|
|
3737
|
+
"reanalyze",
|
|
3738
|
+
"clips",
|
|
3739
|
+
"clip",
|
|
2289
3740
|
"schedule-meeting",
|
|
2290
3741
|
"help"
|
|
2291
3742
|
];
|
|
@@ -2306,16 +3757,22 @@ if (isCliMode) {
|
|
|
2306
3757
|
import("@modelcontextprotocol/sdk/server/mcp.js").then(({ McpServer }) => {
|
|
2307
3758
|
import("@modelcontextprotocol/sdk/server/stdio.js").then(
|
|
2308
3759
|
({ StdioServerTransport }) => {
|
|
2309
|
-
Promise.
|
|
3760
|
+
Promise.all([
|
|
3761
|
+
Promise.resolve().then(() => (init_tools(), tools_exports)),
|
|
3762
|
+
Promise.resolve().then(() => (init_resources(), resources_exports)),
|
|
3763
|
+
Promise.resolve().then(() => (init_prompts(), prompts_exports))
|
|
3764
|
+
]).then(([{ registerAllTools: registerAllTools2 }, { registerResources: registerResources2 }, { registerPrompts: registerPrompts2 }]) => {
|
|
2310
3765
|
const server = new McpServer({
|
|
2311
3766
|
name: "speak-ai",
|
|
2312
3767
|
version: "1.0.0"
|
|
2313
3768
|
});
|
|
2314
3769
|
registerAllTools2(server);
|
|
3770
|
+
registerResources2(server);
|
|
3771
|
+
registerPrompts2(server);
|
|
2315
3772
|
const transport = new StdioServerTransport();
|
|
2316
3773
|
server.connect(transport).then(() => {
|
|
2317
3774
|
process.stderr.write(
|
|
2318
|
-
"[
|
|
3775
|
+
"[speakai-mcp] Server started on stdio transport\n"
|
|
2319
3776
|
);
|
|
2320
3777
|
});
|
|
2321
3778
|
});
|
|
@@ -2327,5 +3784,7 @@ if (isCliMode) {
|
|
|
2327
3784
|
0 && (module.exports = {
|
|
2328
3785
|
createSpeakClient,
|
|
2329
3786
|
formatAxiosError,
|
|
2330
|
-
registerAllTools
|
|
3787
|
+
registerAllTools,
|
|
3788
|
+
registerPrompts,
|
|
3789
|
+
registerResources
|
|
2331
3790
|
});
|