@clipform/mcp-server 1.47.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ import {
6
6
  getWorkflowText,
7
7
  objectType,
8
8
  registerPrompts
9
- } from "./chunk-L3YIUY5R.js";
9
+ } from "./chunk-BBNREY2Q.js";
10
10
  import {
11
11
  GUIDE_TYPES,
12
12
  QUIZ_VARIANTS,
@@ -14,7 +14,7 @@ import {
14
14
  getGuideUri,
15
15
  guideFallbackText,
16
16
  registerResources
17
- } from "./chunk-MOKUP5DR.js";
17
+ } from "./chunk-WUORO245.js";
18
18
  import {
19
19
  ACTIVE_NODE_TYPES,
20
20
  BUSINESS,
@@ -33,8 +33,9 @@ import {
33
33
  callApi,
34
34
  errorResult,
35
35
  resolveFormType,
36
+ structuredResult,
36
37
  textResult
37
- } from "./chunk-F2JA2N5N.js";
38
+ } from "./chunk-4CY5EJCR.js";
38
39
  import {
39
40
  __commonJS,
40
41
  __export,
@@ -17325,6 +17326,21 @@ function registerListFormsTool(server) {
17325
17326
  sort: external_exports.enum(["created_at", "updated_at"]).optional().describe("Sort field (default: created_at)"),
17326
17327
  order: external_exports.enum(["asc", "desc"]).optional().describe("Sort order (default: desc, newest first)")
17327
17328
  },
17329
+ outputSchema: {
17330
+ forms: external_exports.array(
17331
+ external_exports.object({
17332
+ id: external_exports.string(),
17333
+ title: external_exports.string(),
17334
+ share_id: external_exports.string(),
17335
+ is_live: external_exports.boolean(),
17336
+ has_unpublished_changes: external_exports.boolean(),
17337
+ created_at: external_exports.string(),
17338
+ updated_at: external_exports.string(),
17339
+ tags: external_exports.array(external_exports.object({ id: external_exports.string(), name: external_exports.string(), color: external_exports.string().nullable() }))
17340
+ })
17341
+ ).describe("Matching forms (empty if none)"),
17342
+ next_cursor: external_exports.string().nullable().describe("Pass as cursor to fetch the next page, or null when no more")
17343
+ },
17328
17344
  annotations: {
17329
17345
  readOnlyHint: true,
17330
17346
  destructiveHint: false,
@@ -17346,8 +17362,21 @@ function registerListFormsTool(server) {
17346
17362
  return errorResult(result.error);
17347
17363
  }
17348
17364
  const data = result.data;
17365
+ const structured = {
17366
+ forms: data.forms.map((f) => ({
17367
+ id: f.id,
17368
+ title: f.title,
17369
+ share_id: f.share_id,
17370
+ is_live: f.is_live,
17371
+ has_unpublished_changes: f.has_unpublished_changes,
17372
+ created_at: f.created_at,
17373
+ updated_at: f.updated_at,
17374
+ tags: f.tags.map((t) => ({ id: t.id, name: t.name, color: t.color }))
17375
+ })),
17376
+ next_cursor: data.next_cursor
17377
+ };
17349
17378
  if (data.forms.length === 0) {
17350
- return textResult("No forms found matching the criteria.");
17379
+ return structuredResult("No forms found matching the criteria.", structured);
17351
17380
  }
17352
17381
  const lines = [`Found ${data.forms.length} form(s):
17353
17382
  `];
@@ -17361,14 +17390,36 @@ function registerListFormsTool(server) {
17361
17390
  `More results available. Pass cursor: "${data.next_cursor}" to get the next page.`
17362
17391
  );
17363
17392
  }
17364
- return textResult(lines.join("\n"));
17393
+ return structuredResult(lines.join("\n"), structured);
17365
17394
  }
17366
17395
  );
17367
17396
  }
17368
17397
 
17369
17398
  // src/lib/format-form.ts
17399
+ function getFormNodes(data) {
17400
+ return data.node ?? data.nodes ?? [];
17401
+ }
17402
+ function projectFormState(data) {
17403
+ return {
17404
+ form_id: String(data.form_id ?? ""),
17405
+ title: String(data.title ?? ""),
17406
+ is_live: Boolean(data.is_live),
17407
+ has_unpublished_changes: Boolean(data.has_unpublished_changes),
17408
+ nodes: getFormNodes(data).map((q) => {
17409
+ const options = q.node_options ?? q.options;
17410
+ return {
17411
+ id: q.id,
17412
+ type: q.type,
17413
+ prompt: q.prompt ?? "",
17414
+ required: Boolean(q.required),
17415
+ ...q.config && Object.keys(q.config).length > 0 ? { config: q.config } : {},
17416
+ ...options && options.length > 0 ? { options: options.map((o) => ({ id: o.id, content: o.content })) } : {}
17417
+ };
17418
+ })
17419
+ };
17420
+ }
17370
17421
  function formatFormState(data) {
17371
- const nodes = data.node ?? data.nodes ?? [];
17422
+ const nodes = getFormNodes(data);
17372
17423
  const lines = [
17373
17424
  `Form: ${data.title}`,
17374
17425
  `Form ID: ${data.form_id}`,
@@ -17408,6 +17459,22 @@ function registerGetFormTool(server) {
17408
17459
  inputSchema: {
17409
17460
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)")
17410
17461
  },
17462
+ outputSchema: {
17463
+ form_id: external_exports.string().describe("Form UUID"),
17464
+ title: external_exports.string().describe("Form title"),
17465
+ is_live: external_exports.boolean().describe("Whether the form is published"),
17466
+ has_unpublished_changes: external_exports.boolean().describe("Live form has edits not yet republished"),
17467
+ nodes: external_exports.array(
17468
+ external_exports.object({
17469
+ id: external_exports.string(),
17470
+ type: external_exports.string(),
17471
+ prompt: external_exports.string(),
17472
+ required: external_exports.boolean(),
17473
+ config: external_exports.record(external_exports.unknown()).optional(),
17474
+ options: external_exports.array(external_exports.object({ id: external_exports.string(), content: external_exports.string() })).optional()
17475
+ })
17476
+ ).describe("Nodes in sequential order - IDs for update_node / upload_node_media")
17477
+ },
17411
17478
  annotations: {
17412
17479
  readOnlyHint: true,
17413
17480
  destructiveHint: false,
@@ -17420,7 +17487,7 @@ function registerGetFormTool(server) {
17420
17487
  if (!result.ok) {
17421
17488
  return errorResult(result.error);
17422
17489
  }
17423
- return textResult(formatFormState(result.data));
17490
+ return structuredResult(formatFormState(result.data), projectFormState(result.data));
17424
17491
  }
17425
17492
  );
17426
17493
  }
@@ -17597,6 +17664,10 @@ function registerAddNodeTool(server) {
17597
17664
  "Insert after this node ID. Omit to append before the end screen."
17598
17665
  )
17599
17666
  },
17667
+ outputSchema: {
17668
+ node_id: external_exports.string().describe("ID of the created node - pass to update_node / upload_node_media"),
17669
+ type: external_exports.string().describe("Node type that was added")
17670
+ },
17600
17671
  annotations: {
17601
17672
  readOnlyHint: false,
17602
17673
  destructiveHint: false,
@@ -17615,8 +17686,10 @@ function registerAddNodeTool(server) {
17615
17686
  if (!result.ok) {
17616
17687
  return errorResult(result.error);
17617
17688
  }
17618
- return textResult(
17619
- `Node added. ID: ${result.data.node_id} | Type: ${node.type}`
17689
+ const nodeId = result.data.node_id;
17690
+ return structuredResult(
17691
+ `Node added. ID: ${nodeId} | Type: ${node.type}`,
17692
+ { node_id: nodeId, type: node.type }
17620
17693
  );
17621
17694
  }
17622
17695
  );
@@ -17644,6 +17717,16 @@ function registerUpdateNodeTool(server) {
17644
17717
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17645
17718
  updates: external_exports.array(NodeUpdateSchema).min(1).max(20).describe("One or more node updates to apply")
17646
17719
  },
17720
+ outputSchema: {
17721
+ results: external_exports.array(
17722
+ external_exports.object({
17723
+ node_id: external_exports.string(),
17724
+ ok: external_exports.boolean(),
17725
+ warnings: external_exports.array(external_exports.string()).optional(),
17726
+ error: external_exports.string().optional()
17727
+ })
17728
+ ).describe("One result per requested update, in order")
17729
+ },
17647
17730
  annotations: {
17648
17731
  readOnlyHint: false,
17649
17732
  destructiveHint: false,
@@ -17653,6 +17736,7 @@ function registerUpdateNodeTool(server) {
17653
17736
  },
17654
17737
  async ({ form_id, updates }) => {
17655
17738
  const allLines = [];
17739
+ const results = [];
17656
17740
  for (const { node_id, prompt, label, type, required: required2, config: config2, options } of updates) {
17657
17741
  const body = {};
17658
17742
  if (prompt !== void 0) body.prompt = prompt;
@@ -17667,6 +17751,7 @@ function registerUpdateNodeTool(server) {
17667
17751
  );
17668
17752
  if (!result.ok) {
17669
17753
  allLines.push(`Node ${node_id}: FAILED \u2014 ${result.error}`);
17754
+ results.push({ node_id, ok: false, error: result.error });
17670
17755
  continue;
17671
17756
  }
17672
17757
  const changes = [];
@@ -17682,8 +17767,9 @@ function registerUpdateNodeTool(server) {
17682
17767
  if (warnings?.length) {
17683
17768
  for (const w of warnings) allLines.push(`Node ${node_id} warning: ${w}`);
17684
17769
  }
17770
+ results.push({ node_id, ok: true, ...warnings?.length ? { warnings } : {} });
17685
17771
  }
17686
- return textResult(allLines.join("\n"));
17772
+ return structuredResult(allLines.join("\n"), { results });
17687
17773
  }
17688
17774
  );
17689
17775
  }
@@ -17749,7 +17835,7 @@ var MediaItemSchema = external_exports.object({
17749
17835
  ).optional().describe("Per-word timestamps within the segment")
17750
17836
  })
17751
17837
  ).optional().describe("Word-level captions from clipform_generate_tts. Required for per-word highlighting - pass the full objects including 'words' arrays."),
17752
- show_captions: external_exports.boolean().optional().default(true).describe("Display captions/subtitles on the node"),
17838
+ show_captions: external_exports.boolean().optional().describe("Display captions/subtitles on the node. Defaults to on; pass false to hide them."),
17753
17839
  fit_media: external_exports.boolean().optional().describe("Show the whole frame (contain) instead of cover-cropping. ALWAYS set true when attaching renders from clipform_render_composition or clipform_generate_video - they are composed 9:16 frames that must never be cropped. Leave unset for user-supplied media (arbitrary aspect ratios want the cover default).")
17754
17840
  });
17755
17841
  function registerUploadNodeMediaTool(server) {
@@ -17759,11 +17845,23 @@ function registerUploadNodeMediaTool(server) {
17759
17845
  title: "Upload Node Media",
17760
17846
  description: `Upload media for one or more nodes. Pass one item or many (max 10). Multiple items upload sequentially.
17761
17847
 
17762
- When a public URL is provided, the media is fetched and stored automatically. Only works on node types that support media (${MEDIA_SUPPORTED_TYPES.join(", ")}). For video: ingested via Mux. For image: stored in Supabase. Captions from clipform_generate_tts enable per-word highlighting in the viewer. When attaching renders from clipform_render_composition or clipform_generate_video, set fit_media: true on each item.`,
17848
+ When a public URL is provided, the media is fetched and stored automatically. Only works on node types that support media (${MEDIA_SUPPORTED_TYPES.join(", ")}). For video: ingested via Mux. For image: stored in Supabase. Each upload lands in your workspace media library and is then attached to the node (so it is reusable across nodes), matching the builder. Captions from clipform_generate_tts enable per-word highlighting in the viewer. When attaching renders from clipform_render_composition or clipform_generate_video, set fit_media: true on each item.`,
17763
17849
  inputSchema: {
17764
17850
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17765
17851
  items: external_exports.array(MediaItemSchema).min(1).max(10).describe("One or more media items to upload")
17766
17852
  },
17853
+ outputSchema: {
17854
+ results: external_exports.array(
17855
+ external_exports.object({
17856
+ node_id: external_exports.string(),
17857
+ ok: external_exports.boolean(),
17858
+ stored: external_exports.boolean().describe("Bytes already stored (URL-based); false when an upload_url is pending"),
17859
+ upload_url: external_exports.string().optional(),
17860
+ upload_method: external_exports.string().optional(),
17861
+ error: external_exports.string().optional()
17862
+ })
17863
+ ).describe("One result per item, in order")
17864
+ },
17767
17865
  annotations: {
17768
17866
  readOnlyHint: false,
17769
17867
  destructiveHint: false,
@@ -17772,42 +17870,89 @@ When a public URL is provided, the media is fetched and stored automatically. On
17772
17870
  }
17773
17871
  },
17774
17872
  async ({ form_id, items }) => {
17873
+ const workspaceId = getMcpAuth()?.workspace_id;
17775
17874
  const lines = [];
17875
+ const results = [];
17776
17876
  let successCount = 0;
17777
17877
  for (let i = 0; i < items.length; i++) {
17778
17878
  const item = items[i];
17779
17879
  if (items.length > 1) lines.push(`--- Item ${i + 1} (node ${item.node_id}) ---`);
17780
- const body = {
17880
+ if (!workspaceId) {
17881
+ const error2 = "no workspace in context - reconnect the MCP or set CLIPFORM_API_KEY so media can land in your library.";
17882
+ lines.push(`FAILED: ${error2}`);
17883
+ lines.push("");
17884
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17885
+ continue;
17886
+ }
17887
+ const createBody = {
17781
17888
  media_type: item.media_type,
17782
17889
  media_source: item.media_source
17783
17890
  };
17784
- if (item.url) body.url = item.url;
17785
- if (item.captions) body.captions = item.captions;
17786
- if (item.show_captions !== void 0) body.show_captions = item.show_captions;
17787
- if (item.fit_media !== void 0) body.fit_media = item.fit_media;
17788
- const result = await callApi(
17789
- `/forms/${form_id}/nodes/${item.node_id}/media`,
17790
- { method: "POST", body }
17791
- );
17792
- if (result.ok) {
17793
- successCount++;
17794
- const resultLines = [];
17795
- if (result.data.upload_url) {
17796
- resultLines.push(
17797
- `Upload URL: ${result.data.upload_url}`,
17798
- `Upload method: ${result.data.upload_method}`,
17799
- `Upload the file directly to the upload URL using ${result.data.upload_method === "tus" ? "TUS resumable upload" : "HTTP PUT"}.`
17800
- );
17801
- } else {
17802
- resultLines.push(`Media stored.`);
17803
- }
17804
- if (item.captions) {
17805
- resultLines.push(`Captions: ${item.captions.length} segments saved`);
17891
+ if (item.url) createBody.url = item.url;
17892
+ if (item.captions) createBody.captions = item.captions;
17893
+ const created = await callApi(`/workspaces/${workspaceId}/media`, { method: "POST", body: createBody });
17894
+ if (!created.ok) {
17895
+ lines.push(`FAILED: ${created.error}`);
17896
+ lines.push("");
17897
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: created.error });
17898
+ continue;
17899
+ }
17900
+ const mediaAssetId = created.data.media_asset_id;
17901
+ if (!mediaAssetId) {
17902
+ const error2 = `upload did not return a media asset id (route ${String(created.data.route ?? "unknown")}).`;
17903
+ lines.push(`FAILED: ${error2}`);
17904
+ lines.push("");
17905
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: error2 });
17906
+ continue;
17907
+ }
17908
+ const attached = await callApi(`/forms/${form_id}/nodes/${item.node_id}/media/attach`, {
17909
+ method: "POST",
17910
+ body: { media_asset_id: mediaAssetId }
17911
+ });
17912
+ if (!attached.ok) {
17913
+ lines.push(`FAILED to attach the uploaded media to the node: ${attached.error}`);
17914
+ lines.push("");
17915
+ results.push({ node_id: item.node_id, ok: false, stored: false, error: attached.error });
17916
+ continue;
17917
+ }
17918
+ const presentation = {};
17919
+ if (item.fit_media !== void 0) presentation.fit_media = item.fit_media;
17920
+ if (item.show_captions !== void 0) presentation.show_captions = item.show_captions;
17921
+ let presentationNote = "";
17922
+ if (Object.keys(presentation).length > 0) {
17923
+ const patched = await callApi(`/forms/${form_id}/nodes/${item.node_id}/media`, {
17924
+ method: "PATCH",
17925
+ body: presentation
17926
+ });
17927
+ if (!patched.ok) {
17928
+ presentationNote = `Note: media attached, but display settings were not applied: ${patched.error}`;
17806
17929
  }
17807
- lines.push(resultLines.join("\n"));
17930
+ }
17931
+ successCount++;
17932
+ const uploadUrl = created.data.upload_url;
17933
+ const uploadMethod = created.data.upload_method;
17934
+ results.push({
17935
+ node_id: item.node_id,
17936
+ ok: true,
17937
+ stored: !uploadUrl,
17938
+ ...uploadUrl ? { upload_url: uploadUrl } : {},
17939
+ ...uploadUrl && uploadMethod ? { upload_method: uploadMethod } : {}
17940
+ });
17941
+ const resultLines = [];
17942
+ if (created.data.upload_url) {
17943
+ resultLines.push(
17944
+ `Upload URL: ${created.data.upload_url}`,
17945
+ `Upload method: ${created.data.upload_method}`,
17946
+ `Upload the file directly to the upload URL using ${created.data.upload_method === "tus" ? "TUS resumable upload" : "HTTP PUT"}.`
17947
+ );
17808
17948
  } else {
17809
- lines.push(`FAILED: ${result.error}`);
17949
+ resultLines.push(`Media stored in the workspace library and attached to the node.`);
17810
17950
  }
17951
+ if (item.captions) {
17952
+ resultLines.push(`Captions: ${item.captions.length} segments saved`);
17953
+ }
17954
+ if (presentationNote) resultLines.push(presentationNote);
17955
+ lines.push(resultLines.join("\n"));
17811
17956
  lines.push("");
17812
17957
  }
17813
17958
  if (items.length > 1) {
@@ -17815,7 +17960,7 @@ When a public URL is provided, the media is fetched and stored automatically. On
17815
17960
  `);
17816
17961
  }
17817
17962
  if (successCount === 0) return errorResult(lines.join("\n"));
17818
- return textResult(lines.join("\n"));
17963
+ return structuredResult(lines.join("\n"), { results });
17819
17964
  }
17820
17965
  );
17821
17966
  }
@@ -17831,6 +17976,14 @@ function registerGetNodeMediaTool(server) {
17831
17976
  form_id: external_exports.string().uuid().describe("The form UUID (returned by clipform_create_form, not the short share_id from the URL)"),
17832
17977
  node_id: external_exports.string().describe("The node ID")
17833
17978
  },
17979
+ outputSchema: {
17980
+ media: external_exports.object({
17981
+ media_type: external_exports.string(),
17982
+ status: external_exports.string(),
17983
+ duration: external_exports.number().optional(),
17984
+ transcription_status: external_exports.string().nullable().optional()
17985
+ }).nullable().describe("Attached media (null if the node has none)")
17986
+ },
17834
17987
  annotations: {
17835
17988
  readOnlyHint: true,
17836
17989
  destructiveHint: false,
@@ -17847,15 +18000,22 @@ function registerGetNodeMediaTool(server) {
17847
18000
  }
17848
18001
  const media = result.data.media;
17849
18002
  if (!media) {
17850
- return textResult("No media attached to this node.");
18003
+ return structuredResult("No media attached to this node.", { media: null });
17851
18004
  }
17852
- return textResult(
18005
+ const projected = {
18006
+ media_type: String(media.media_type),
18007
+ status: String(media.status),
18008
+ ...typeof media.duration === "number" ? { duration: media.duration } : {},
18009
+ transcription_status: media.transcription_status == null ? null : String(media.transcription_status)
18010
+ };
18011
+ return structuredResult(
17853
18012
  [
17854
18013
  `Type: ${media.media_type}`,
17855
18014
  `Status: ${media.status}`,
17856
18015
  media.duration ? `Duration: ${media.duration}s` : "",
17857
18016
  `Transcription: ${media.transcription_status}`
17858
- ].filter(Boolean).join("\n")
18017
+ ].filter(Boolean).join("\n"),
18018
+ { media: projected }
17859
18019
  );
17860
18020
  }
17861
18021
  );
@@ -18009,6 +18169,20 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18009
18169
  query: external_exports.string().describe("News search query (e.g. 'Iran war 2026', 'Australian Open final', 'UK election')"),
18010
18170
  count: external_exports.number().min(1).max(15).default(5).optional().describe("Max results per provider (default 5)")
18011
18171
  },
18172
+ outputSchema: {
18173
+ query: external_exports.string(),
18174
+ articles: external_exports.array(
18175
+ external_exports.object({
18176
+ title: external_exports.string().optional(),
18177
+ source: external_exports.string().optional(),
18178
+ description: external_exports.string().optional(),
18179
+ author: external_exports.string().optional(),
18180
+ published_at: external_exports.string().optional(),
18181
+ url: external_exports.string(),
18182
+ image_url: external_exports.string().optional()
18183
+ })
18184
+ ).describe("Matching articles (empty if none)")
18185
+ },
18012
18186
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18013
18187
  },
18014
18188
  async ({ query, count }) => {
@@ -18017,7 +18191,16 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18017
18191
  });
18018
18192
  if (!result.ok) return errorResult(result.error);
18019
18193
  const results = result.data.results;
18020
- if (!results.length) return textResult(`No news articles found for "${query}".`);
18194
+ if (!results.length) return structuredResult(`No news articles found for "${query}".`, { query, articles: [] });
18195
+ const articles = results.map((article) => ({
18196
+ ...article.title ? { title: article.title } : {},
18197
+ ...article.source ? { source: article.source } : {},
18198
+ ...article.description ? { description: article.description } : {},
18199
+ ...article.author ? { author: article.author } : {},
18200
+ ...article.publishedAt ? { published_at: article.publishedAt } : {},
18201
+ url: article.url,
18202
+ ...article.imageUrl ? { image_url: article.imageUrl } : {}
18203
+ }));
18021
18204
  const lines = [`Found ${results.length} articles for "${query}":
18022
18205
  `];
18023
18206
  for (const article of results) {
@@ -18029,7 +18212,7 @@ Returns: article title, source, author, date, URL, description, and image URL pe
18029
18212
  if (article.imageUrl) lines.push(` Image: ${article.imageUrl}`);
18030
18213
  lines.push("");
18031
18214
  }
18032
- return textResult(lines.join("\n"));
18215
+ return structuredResult(lines.join("\n"), { query, articles });
18033
18216
  }
18034
18217
  );
18035
18218
  }
@@ -18085,6 +18268,12 @@ Supports any public YouTube video with captions enabled. Does NOT work for priva
18085
18268
  }
18086
18269
 
18087
18270
  // src/tools/generate-tts.ts
18271
+ var CaptionSchema = external_exports.object({
18272
+ start: external_exports.number(),
18273
+ end: external_exports.number(),
18274
+ text: external_exports.string(),
18275
+ words: external_exports.array(external_exports.object({ word: external_exports.string(), start: external_exports.number(), end: external_exports.number() })).optional()
18276
+ });
18088
18277
  var TtsItemSchema = external_exports.object({
18089
18278
  text: external_exports.string().min(1).max(5e3).describe("Narration text"),
18090
18279
  voice: external_exports.enum(["ryan", "sonia", "andrew", "ava", "guy"]).optional().default("ryan").describe("Voice: ryan (British male, clear/articulate), sonia (British female, warm/bright), andrew (American male, smooth/neutral), ava (American female, vibrant/friendly), guy (American male, deep/authoritative). Pick based on the form's topic and audience."),
@@ -18105,6 +18294,16 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18105
18294
  inputSchema: {
18106
18295
  items: external_exports.array(TtsItemSchema).min(1).max(10).describe("One or more TTS items to generate")
18107
18296
  },
18297
+ outputSchema: {
18298
+ results: external_exports.array(
18299
+ external_exports.object({
18300
+ ok: external_exports.boolean(),
18301
+ audio_url: external_exports.string().optional(),
18302
+ captions: external_exports.array(CaptionSchema).optional().describe("Pass as the captions param to upload_node_media"),
18303
+ error: external_exports.string().optional()
18304
+ })
18305
+ ).describe("One result per item, in order")
18306
+ },
18108
18307
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18109
18308
  },
18110
18309
  async ({ items }) => {
@@ -18118,6 +18317,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18118
18317
  )
18119
18318
  );
18120
18319
  const lines = [];
18320
+ const structured = [];
18121
18321
  let successCount = 0;
18122
18322
  for (let i = 0; i < results.length; i++) {
18123
18323
  const r = results[i];
@@ -18134,11 +18334,17 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18134
18334
  ...Array.isArray(c.words) ? { words: c.words.map((w) => ({ word: w.word, start: w.start, end: w.end })) } : {}
18135
18335
  }));
18136
18336
  lines.push(`Captions: ${JSON.stringify(captions)}`);
18337
+ structured.push({
18338
+ ok: true,
18339
+ ...data.audioUrl ? { audio_url: String(data.audioUrl) } : {},
18340
+ captions
18341
+ });
18137
18342
  } else {
18138
18343
  const error2 = r.status === "rejected" ? r.reason?.message || String(r.reason) : r.value.error;
18139
18344
  const status = r.status === "fulfilled" ? r.value.status : 0;
18140
18345
  const hint = status === 500 || status === 0 ? " (transient - retrying may help)" : "";
18141
18346
  lines.push(`FAILED: ${error2}${hint}`);
18347
+ structured.push({ ok: false, error: String(error2) });
18142
18348
  }
18143
18349
  lines.push("");
18144
18350
  }
@@ -18148,7 +18354,7 @@ Pass one item or many (max 10) - multiple items run in parallel. Returns audio U
18148
18354
  }
18149
18355
  lines.push(`Pass the complete Captions JSON as the "captions" parameter when uploading.`);
18150
18356
  if (successCount === 0) return errorResult(lines.join("\n"));
18151
- return textResult(lines.join("\n"));
18357
+ return structuredResult(lines.join("\n"), { results: structured });
18152
18358
  }
18153
18359
  );
18154
18360
  }
@@ -18172,10 +18378,30 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18172
18378
  })
18173
18379
  ).min(1).max(10).describe("One or more search queries to run")
18174
18380
  },
18381
+ outputSchema: {
18382
+ searches: external_exports.array(
18383
+ external_exports.object({
18384
+ query: external_exports.string(),
18385
+ kind: external_exports.enum(["image", "video"]),
18386
+ items: external_exports.array(
18387
+ external_exports.object({
18388
+ title: external_exports.string().optional(),
18389
+ url: external_exports.string(),
18390
+ width: external_exports.number().optional(),
18391
+ height: external_exports.number().optional(),
18392
+ duration: external_exports.number().optional(),
18393
+ source: external_exports.string().optional(),
18394
+ attribution: external_exports.string().optional()
18395
+ })
18396
+ )
18397
+ })
18398
+ ).describe("Results grouped by query (items empty if a query found nothing or failed)")
18399
+ },
18175
18400
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18176
18401
  },
18177
18402
  async ({ queries }) => {
18178
18403
  const allLines = [];
18404
+ const searches = [];
18179
18405
  for (const q of queries) {
18180
18406
  const { query, count, orientation } = q;
18181
18407
  const kind = q.kind ?? "image";
@@ -18185,16 +18411,32 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18185
18411
  if (queries.length > 1) allLines.push(`--- "${query}" (${kind}) ---`);
18186
18412
  if (!result.ok) {
18187
18413
  allLines.push(`FAILED: ${result.error}`, "");
18414
+ searches.push({ query, kind, items: [] });
18188
18415
  continue;
18189
18416
  }
18190
18417
  const results = result.data.results;
18191
18418
  if (!results.length) {
18192
18419
  allLines.push(`No ${kind}s found.`, "");
18420
+ searches.push({ query, kind, items: [] });
18193
18421
  continue;
18194
18422
  }
18423
+ const shown = results.slice(0, 10);
18424
+ searches.push({
18425
+ query,
18426
+ kind,
18427
+ items: shown.map((item) => ({
18428
+ ...item.title ? { title: item.title } : {},
18429
+ url: item.url,
18430
+ ...typeof item.width === "number" ? { width: item.width } : {},
18431
+ ...typeof item.height === "number" ? { height: item.height } : {},
18432
+ ...kind === "video" && typeof item.duration === "number" ? { duration: item.duration } : {},
18433
+ ...item.source ? { source: item.source } : {},
18434
+ ...item.attribution ? { attribution: item.attribution } : {}
18435
+ }))
18436
+ });
18195
18437
  allLines.push(`Found ${results.length} ${kind}s:
18196
18438
  `);
18197
- for (const item of results.slice(0, 10)) {
18439
+ for (const item of shown) {
18198
18440
  allLines.push(`- ${item.title}`);
18199
18441
  allLines.push(` URL: ${item.url}`);
18200
18442
  const meta = [];
@@ -18209,7 +18451,7 @@ Example: { queries: [{ query: "saturn rings" }, { query: "mars surface", count:
18209
18451
  allLines.push("");
18210
18452
  }
18211
18453
  }
18212
- return textResult(allLines.join("\n"));
18454
+ return structuredResult(allLines.join("\n"), { searches });
18213
18455
  }
18214
18456
  );
18215
18457
  }
@@ -18345,6 +18587,17 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18345
18587
  wait: external_exports.boolean().optional().default(true).describe("Single render only: true (default) blocks until the render is ready and returns its URL; false returns a job ID for clipform_check_render. Batch items always run fire-and-poll."),
18346
18588
  items: external_exports.array(RenderItemSchema).min(1).max(10).optional().describe("Batch mode: multiple renders in one call. All fire in parallel; returns one job ID per item - collect with clipform_check_render (job_ids). Use this whenever rendering more than one clip.")
18347
18589
  },
18590
+ outputSchema: {
18591
+ jobs: external_exports.array(
18592
+ external_exports.object({
18593
+ status: external_exports.enum(["rendering", "complete"]),
18594
+ job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18595
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18596
+ composition_id: external_exports.string().optional(),
18597
+ output_format: external_exports.string().optional()
18598
+ })
18599
+ ).describe("One entry per render (single or batch)")
18600
+ },
18348
18601
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18349
18602
  },
18350
18603
  async ({ compositionId, outputFormat, inputProps, wait, items }) => {
@@ -18360,23 +18613,34 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18360
18613
  }
18361
18614
  if (items?.length) {
18362
18615
  const jobs2 = items.map((item) => ({ item, job: fireRender(item) }));
18363
- return textResult(
18616
+ return structuredResult(
18364
18617
  [
18365
18618
  `${jobs2.length} renders started in parallel:`,
18366
18619
  ...jobs2.map(({ item, job }) => `- ${item.compositionId} (${item.outputFormat}): job ${job.id}`),
18367
18620
  ``,
18368
18621
  `Collect with ONE clipform_check_render call (job_ids: [...]). Renders typically take ${RENDER_TIMING.expectedRange}.`
18369
- ].join("\n")
18622
+ ].join("\n"),
18623
+ {
18624
+ jobs: jobs2.map(({ item, job }) => ({
18625
+ status: "rendering",
18626
+ job_id: job.id,
18627
+ composition_id: item.compositionId,
18628
+ output_format: item.outputFormat
18629
+ }))
18630
+ }
18370
18631
  );
18371
18632
  }
18372
18633
  if (wait === false) {
18373
18634
  const job = fireRender({ compositionId, outputFormat, inputProps });
18374
- return textResult(
18635
+ return structuredResult(
18375
18636
  [
18376
18637
  `Render started (${compositionId}).`,
18377
18638
  `Job ID: ${job.id}`,
18378
18639
  `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18379
- ].join("\n")
18640
+ ].join("\n"),
18641
+ {
18642
+ jobs: [{ status: "rendering", job_id: job.id, composition_id: compositionId, output_format: outputFormat }]
18643
+ }
18380
18644
  );
18381
18645
  }
18382
18646
  const result = await callApi("/internal/render", {
@@ -18385,11 +18649,21 @@ For multi-render builds (e.g. one clip per quiz question), pass everything in ON
18385
18649
  });
18386
18650
  if (!result.ok) return errorResult(result.error);
18387
18651
  const data = result.data;
18388
- return textResult(
18652
+ return structuredResult(
18389
18653
  [
18390
18654
  `Render complete. Public URL: ${data.public_url}`,
18391
18655
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18392
- ].join("\n")
18656
+ ].join("\n"),
18657
+ {
18658
+ jobs: [
18659
+ {
18660
+ status: "complete",
18661
+ ...data.public_url ? { public_url: data.public_url } : {},
18662
+ composition_id: compositionId,
18663
+ output_format: outputFormat
18664
+ }
18665
+ ]
18666
+ }
18393
18667
  );
18394
18668
  }
18395
18669
  );
@@ -18410,6 +18684,19 @@ function registerSearchMusicTool(server) {
18410
18684
  maxDuration: external_exports.number().optional().describe("Maximum duration in seconds"),
18411
18685
  tags: external_exports.array(external_exports.string()).optional().describe("Genre/mood tags to filter by")
18412
18686
  },
18687
+ outputSchema: {
18688
+ query: external_exports.string(),
18689
+ tracks: external_exports.array(
18690
+ external_exports.object({
18691
+ title: external_exports.string().optional(),
18692
+ artist: external_exports.string().optional(),
18693
+ url: external_exports.string(),
18694
+ duration: external_exports.number().optional(),
18695
+ source: external_exports.string().optional(),
18696
+ license: external_exports.string().optional()
18697
+ })
18698
+ ).describe("Matching tracks (empty if none); pass a url as the audio track to generate_video")
18699
+ },
18413
18700
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18414
18701
  },
18415
18702
  async ({ query, count, instrumentalOnly, minDuration, maxDuration, tags }) => {
@@ -18418,7 +18705,15 @@ function registerSearchMusicTool(server) {
18418
18705
  });
18419
18706
  if (!result.ok) return errorResult(result.error);
18420
18707
  const results = result.data.results;
18421
- if (!results.length) return textResult(`No music found for "${query}".`);
18708
+ if (!results.length) return structuredResult(`No music found for "${query}".`, { query, tracks: [] });
18709
+ const tracks = results.map((item) => ({
18710
+ ...item.title ? { title: item.title } : {},
18711
+ ...item.artist ? { artist: item.artist } : {},
18712
+ url: item.url,
18713
+ ...typeof item.duration === "number" ? { duration: item.duration } : {},
18714
+ ...item.source ? { source: item.source } : {},
18715
+ ...item.license ? { license: item.license } : {}
18716
+ }));
18422
18717
  const lines = [`Found ${results.length} tracks for "${query}":
18423
18718
  `];
18424
18719
  for (const item of results) {
@@ -18429,7 +18724,7 @@ function registerSearchMusicTool(server) {
18429
18724
  if (item.license) lines.push(` License: ${item.license}`);
18430
18725
  lines.push("");
18431
18726
  }
18432
- return textResult(lines.join("\n"));
18727
+ return structuredResult(lines.join("\n"), { query, tracks });
18433
18728
  }
18434
18729
  );
18435
18730
  }
@@ -18470,6 +18765,19 @@ function registerListCompositionsTool(server) {
18470
18765
  title: "List Compositions",
18471
18766
  description: `Browse available video compositions and their expected props schemas. Call this before using clipform_render_composition to discover visual styles and their input props. For narrated slideshows from images, use clipform_generate_video instead.`,
18472
18767
  inputSchema: {},
18768
+ outputSchema: {
18769
+ compositions: external_exports.array(
18770
+ external_exports.object({
18771
+ id: external_exports.string(),
18772
+ width: external_exports.number(),
18773
+ height: external_exports.number(),
18774
+ fps: external_exports.number(),
18775
+ durationInFrames: external_exports.number(),
18776
+ props: external_exports.string().describe("Compact props summary (name*: type)"),
18777
+ propsSchema: external_exports.record(external_exports.unknown()).optional().describe("Full JSON Schema for inputProps")
18778
+ })
18779
+ ).describe("Available compositions - pass an id to render_composition")
18780
+ },
18473
18781
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18474
18782
  },
18475
18783
  async () => {
@@ -18484,7 +18792,18 @@ function registerListCompositionsTool(server) {
18484
18792
  }
18485
18793
  const all = result.data.compositions;
18486
18794
  const compositions = all.filter((c) => EXPOSED_COMPOSITIONS.includes(c.id));
18487
- if (!compositions.length) return textResult("No compositions found.");
18795
+ if (!compositions.length) return structuredResult("No compositions found.", { compositions: [] });
18796
+ const structured = {
18797
+ compositions: compositions.map((comp) => ({
18798
+ id: comp.id,
18799
+ width: comp.width,
18800
+ height: comp.height,
18801
+ fps: comp.fps,
18802
+ durationInFrames: comp.durationInFrames,
18803
+ props: summarizeSchema(comp.propsSchema),
18804
+ ...comp.propsSchema ? { propsSchema: comp.propsSchema } : {}
18805
+ }))
18806
+ };
18488
18807
  const lines = [`Available compositions (${compositions.length}):
18489
18808
  `];
18490
18809
  for (const comp of compositions) {
@@ -18494,7 +18813,7 @@ function registerListCompositionsTool(server) {
18494
18813
  }
18495
18814
  lines.push("");
18496
18815
  lines.push("Use clipform_render_composition with a compositionId to render. inputProps are validated strictly against the schema - unknown or missing props fail.");
18497
- return textResult(lines.join("\n"));
18816
+ return structuredResult(lines.join("\n"), structured);
18498
18817
  }
18499
18818
  );
18500
18819
  }
@@ -18509,6 +18828,11 @@ function registerListAssetsTool(server) {
18509
18828
  inputSchema: {
18510
18829
  type: external_exports.enum(["sfx", "animation", "font", "all"]).default("all").optional().describe("Asset type to list (default: all)")
18511
18830
  },
18831
+ outputSchema: {
18832
+ sfx: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
18833
+ animations: external_exports.array(external_exports.object({ name: external_exports.string(), description: external_exports.string().nullable().optional(), path: external_exports.string().optional() })),
18834
+ fonts: external_exports.array(external_exports.object({ name: external_exports.string(), weights: external_exports.array(external_exports.union([external_exports.string(), external_exports.number()])).optional() }))
18835
+ },
18512
18836
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18513
18837
  },
18514
18838
  async ({ type }) => {
@@ -18519,6 +18843,11 @@ function registerListAssetsTool(server) {
18519
18843
  if (!result.ok) return errorResult(result.error);
18520
18844
  const data = result.data;
18521
18845
  const lines = [];
18846
+ const structured = {
18847
+ sfx: (data.sfx ?? []).map((s) => ({ name: s.name, description: s.description ?? null, path: s.path })),
18848
+ animations: (data.animations ?? []).map((a) => ({ name: a.name, description: a.description ?? null, path: a.path })),
18849
+ fonts: (data.fonts ?? []).map((f) => ({ name: f.name, ...Array.isArray(f.weights) ? { weights: f.weights } : {} }))
18850
+ };
18522
18851
  if (data.sfx?.length) {
18523
18852
  lines.push(`## Sound Effects (${data.sfx.length})
18524
18853
  `);
@@ -18543,7 +18872,7 @@ function registerListAssetsTool(server) {
18543
18872
  }
18544
18873
  lines.push("");
18545
18874
  }
18546
- return textResult(lines.length ? lines.join("\n") : "No assets found.");
18875
+ return structuredResult(lines.length ? lines.join("\n") : "No assets found.", structured);
18547
18876
  }
18548
18877
  );
18549
18878
  }
@@ -18594,6 +18923,12 @@ For multi-question builds, pass wait: false on every render: each call returns a
18594
18923
  background_color: external_exports.string().optional().describe("Background color (default '#000')"),
18595
18924
  wait: external_exports.boolean().optional().default(true).describe("true (default) blocks until the video is ready and returns its URL. false returns a job ID immediately - fire all renders first, then poll clipform_check_render. Use false whenever rendering more than one video.")
18596
18925
  },
18926
+ outputSchema: {
18927
+ status: external_exports.enum(["rendering", "complete"]).describe("'rendering' when wait:false (poll check_render); 'complete' with a public_url when wait:true"),
18928
+ job_id: external_exports.string().optional().describe("Present when status is 'rendering' - pass to check_render"),
18929
+ public_url: external_exports.string().optional().describe("Present when status is 'complete' - attach via upload_node_media (fit_media: true)"),
18930
+ duration_seconds: external_exports.number().optional()
18931
+ },
18597
18932
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
18598
18933
  },
18599
18934
  async ({ items, audio_url, background_audio_url, background_audio_volume, duration_seconds, random_effects, transition, style_preset, texture, duotone, background_color, wait }) => {
@@ -18625,30 +18960,52 @@ For multi-question builds, pass wait: false on every render: each call returns a
18625
18960
  if (r.ok) completeJob(job.id, r.data);
18626
18961
  else failJob(job.id, r.error);
18627
18962
  }).catch((err) => failJob(job.id, err instanceof Error ? err.message : String(err)));
18628
- return textResult(
18963
+ return structuredResult(
18629
18964
  [
18630
18965
  `Render started (${items.length} item${items.length > 1 ? "s" : ""}).`,
18631
18966
  `Job ID: ${job.id}`,
18632
18967
  `Fire any remaining renders now, then poll clipform_check_render. Renders typically take ${RENDER_TIMING.expectedRange}.`
18633
- ].join("\n")
18968
+ ].join("\n"),
18969
+ { status: "rendering", job_id: job.id }
18634
18970
  );
18635
18971
  }
18636
18972
  const result = await apiCall();
18637
18973
  if (!result.ok) return errorResult(result.error);
18638
18974
  const data = result.data;
18639
- return textResult(
18975
+ return structuredResult(
18640
18976
  [
18641
18977
  `Video rendered (${items.length} item${items.length > 1 ? "s" : ""}).`,
18642
18978
  `Public URL: ${data.public_url}`,
18643
18979
  data.duration_seconds ? `Duration: ${data.duration_seconds}s` : "",
18644
18980
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18645
- ].filter(Boolean).join("\n")
18981
+ ].filter(Boolean).join("\n"),
18982
+ {
18983
+ status: "complete",
18984
+ ...data.public_url ? { public_url: data.public_url } : {},
18985
+ ...typeof data.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
18986
+ }
18646
18987
  );
18647
18988
  }
18648
18989
  );
18649
18990
  }
18650
18991
 
18651
18992
  // src/tools/check-render.ts
18993
+ function projectJob(id, job) {
18994
+ if (!job) return { job_id: id, status: "not_found" };
18995
+ if (job.status === "rendering") {
18996
+ return { job_id: id, status: "rendering", elapsed_seconds: Math.round((Date.now() - job.createdAt) / 1e3) };
18997
+ }
18998
+ if (job.status === "failed") {
18999
+ return { job_id: id, status: "failed", ...job.error ? { error: job.error } : {} };
19000
+ }
19001
+ const data = job.result;
19002
+ return {
19003
+ job_id: id,
19004
+ status: "complete",
19005
+ ...data?.public_url ? { public_url: data.public_url } : {},
19006
+ ...typeof data?.duration_seconds === "number" ? { duration_seconds: data.duration_seconds } : {}
19007
+ };
19008
+ }
18652
19009
  var LOST_JOB_RECOVERY = [
18653
19010
  `Jobs expire after 30 minutes and are lost if the server restarts - the render itself may still have completed.`,
18654
19011
  `To recover, re-run the original render call with identical arguments:`,
@@ -18685,6 +19042,18 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
18685
19042
  job_id: external_exports.string().uuid().optional().describe("A single job ID returned by the render tool"),
18686
19043
  job_ids: external_exports.array(external_exports.string().uuid()).min(1).max(20).optional().describe("Multiple job IDs - check the whole batch in one call instead of one call per job")
18687
19044
  },
19045
+ outputSchema: {
19046
+ jobs: external_exports.array(
19047
+ external_exports.object({
19048
+ job_id: external_exports.string(),
19049
+ status: external_exports.enum(["rendering", "complete", "failed", "not_found"]),
19050
+ public_url: external_exports.string().optional(),
19051
+ duration_seconds: external_exports.number().optional(),
19052
+ elapsed_seconds: external_exports.number().optional(),
19053
+ error: external_exports.string().optional()
19054
+ })
19055
+ ).describe("One entry per queried job; attach completed public_urls via upload_node_media")
19056
+ },
18688
19057
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
18689
19058
  },
18690
19059
  async ({ job_id, job_ids }) => {
@@ -18700,38 +19069,41 @@ Pass job_ids to check a whole batch in ONE call - one line of status per job. Pa
18700
19069
  }
18701
19070
  if (job.status === "rendering") {
18702
19071
  const elapsed = Math.round((Date.now() - job.createdAt) / 1e3);
18703
- return textResult(
19072
+ return structuredResult(
18704
19073
  [
18705
19074
  `Status: rendering (${elapsed}s elapsed)`,
18706
19075
  `Tool: ${job.tool}`,
18707
19076
  ``,
18708
19077
  `Still in progress. Check again in ${RENDER_TIMING.pollDelay}.`
18709
- ].join("\n")
19078
+ ].join("\n"),
19079
+ { jobs: [projectJob(ids[0], job)] }
18710
19080
  );
18711
19081
  }
18712
19082
  if (job.status === "failed") {
18713
19083
  return errorResult(`Render failed: ${job.error}`);
18714
19084
  }
18715
19085
  const data = job.result;
18716
- return textResult(
19086
+ return structuredResult(
18717
19087
  [
18718
19088
  `Render complete.`,
18719
19089
  ...data.public_url ? [`Public URL: ${data.public_url}`] : [],
18720
19090
  ...data.duration_seconds ? [`Duration: ${data.duration_seconds}s`] : [],
18721
19091
  `Attach via clipform_upload_node_media with fit_media: true (composed 9:16 frame - contain, never crop).`
18722
- ].join("\n")
19092
+ ].join("\n"),
19093
+ { jobs: [projectJob(ids[0], job)] }
18723
19094
  );
18724
19095
  }
18725
19096
  const results = ids.map((id) => describeJob(id, getJob(id)));
18726
19097
  const pending = results.filter((r) => !r.done).length;
18727
19098
  const lost = results.some((r) => r.line.includes("NOT FOUND"));
18728
- return textResult(
19099
+ return structuredResult(
18729
19100
  [
18730
19101
  `Render batch: ${results.length - pending}/${results.length} finished${pending ? ` - check again in ${RENDER_TIMING.pollDelay}` : ""}.`,
18731
19102
  ...results.map((r) => r.line),
18732
19103
  ...pending === 0 ? [``, `Attach completed renders via clipform_upload_node_media with fit_media: true (composed 9:16 frames - contain, never crop).`] : [],
18733
19104
  ...lost ? [``, LOST_JOB_RECOVERY] : []
18734
- ].join("\n")
19105
+ ].join("\n"),
19106
+ { jobs: ids.map((id) => projectJob(id, getJob(id))) }
18735
19107
  );
18736
19108
  }
18737
19109
  );
@@ -19047,4 +19419,4 @@ export {
19047
19419
  JSONRPCMessageSchema,
19048
19420
  createServer
19049
19421
  };
19050
- //# sourceMappingURL=chunk-KP64A2KW.js.map
19422
+ //# sourceMappingURL=chunk-3MTEM3VC.js.map