@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.
Files changed (3) hide show
  1. package/README.md +419 -161
  2. package/dist/index.js +1518 -59
  3. 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 'speak-mcp config set-key' or set the environment variable.");
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("[speak-mcp] Authenticated successfully\n");
67
+ process.stderr.write("[speakai-mcp] Authenticated successfully\n");
68
68
  }
69
69
  } catch (err) {
70
70
  process.stderr.write(
71
- `[speak-mcp] Authentication failed: ${err instanceof Error ? err.message : err}
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("[speak-mcp] Token refreshed\n");
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 media file upload. Use this before uploading a file directly to Speak AI storage.",
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 media file to Speak AI by providing a publicly accessible URL. Speak AI will fetch and process the file asynchronously.",
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(["audio", "video"]).describe('Type of media: "audio" or "video"'),
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 all media files in the workspace with optional filtering, pagination, and sorting.",
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(["audio", "video", "text"]).optional().describe('Filter by media type: "audio", "video", or "text"'),
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, including topics, sentiment, action items, and summaries.",
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, including speaker labels and timestamps.",
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 (e.g. pending, transcribing, completed, failed).",
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 for AI-powered questions about your media.",
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
- "ask_magic_prompt",
1259
- "Ask an AI-powered question about a specific media file using Speak AI's Magic Prompt.",
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
- mediaId: import_zod7.z.string().min(1).describe("Unique identifier of the media file to query"),
1262
- prompt: import_zod7.z.string().min(1).describe("The question or prompt to ask about the media"),
1263
- promptId: import_zod7.z.string().optional().describe("ID of a predefined prompt template to use")
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/index.ts
1731
- var tools_exports = {};
1732
- __export(tools_exports, {
1733
- registerAllTools: () => registerAllTools
2067
+ // src/tools/analytics.ts
2068
+ var analytics_exports = {};
2069
+ __export(analytics_exports, {
2070
+ register: () => register12
1734
2071
  });
1735
- function registerAllTools(server, client) {
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 "speak-mcp config set-key" or set SPEAK_API_KEY.'
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("speak-mcp").description(
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("1.0.0");
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("<url>", "Publicly accessible media URL").option("-n, --name <name>", "Display name").option("-t, --type <type>", "Media type (audio or video)", "audio").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 (url, opts) => {
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 body = {
2064
- name: opts.name ?? url.split("/").pop()?.split("?")[0] ?? "Upload",
2065
- url,
2066
- mediaType: opts.type,
2067
- sourceLanguage: opts.language
2068
- };
2069
- if (opts.folder) body.folderId = opts.folder;
2070
- if (opts.tags) body.tags = opts.tags;
2071
- const res = await client.post("/v1/media/upload", body);
2072
- const data = res.data?.data;
2073
- if (opts.json && !opts.wait) {
2074
- printJson(data);
2075
- return;
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
- const mediaId = data?.mediaId;
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 status = data?.state;
2082
- while (status !== "processed" && status !== "failed") {
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
- status = statusRes.data?.data?.state;
3250
+ state = statusRes.data?.data?.state;
3251
+ attempts++;
2087
3252
  }
2088
3253
  console.log();
2089
- if (status === "processed") {
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 a media file").argument("<mediaId>", "Media file ID").argument("<prompt>", "Your question").option("--assistant <type>", "Assistant type (general, researcher, marketer, sales, recruiter)", "general").option("--json", "Output raw JSON").action(async (mediaId, prompt, opts) => {
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 res = await client.post("/v1/prompt", {
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.resolve().then(() => (init_tools(), tools_exports)).then(({ registerAllTools: registerAllTools2 }) => {
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
- "[speak-mcp] Server started on stdio transport\n"
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
  });