runline 0.31.0 → 0.33.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.
Files changed (30) hide show
  1. package/dist/plugin/schema.d.ts +2 -4
  2. package/dist/plugin/schema.js +47 -12
  3. package/dist/plugins/_shared/{imageFile.js → mediaFile.js} +33 -12
  4. package/dist/plugins/_shared/provider.js +22 -16
  5. package/dist/plugins/_shared/shiftCloud.js +34 -10
  6. package/dist/plugins/_shared/shiftUpload.js +1 -1
  7. package/dist/plugins/elevenlabs/src/index.js +277 -0
  8. package/dist/plugins/elevenlabs/src/schemas.js +115 -0
  9. package/dist/plugins/elevenlabs/src/shared.js +272 -0
  10. package/dist/plugins/elevenlabs/src/workflows.js +191 -0
  11. package/dist/plugins/fal/src/index.js +181 -0
  12. package/dist/plugins/fal/src/shared.js +303 -0
  13. package/dist/plugins/googleImage/src/index.js +1 -1
  14. package/dist/plugins/openai/src/index.js +1 -1
  15. package/dist/plugins/recraft/src/index.js +1 -1
  16. package/dist/plugins/replicate/src/index.js +5 -6
  17. package/dist/plugins/shiftBwm/src/collections.js +201 -0
  18. package/dist/plugins/shiftBwm/src/index.js +33 -0
  19. package/dist/plugins/shiftBwm/src/records.js +180 -0
  20. package/dist/plugins/shiftBwm/src/shared.js +143 -0
  21. package/dist/plugins/shiftBwm/src/vendor/client.js +227 -0
  22. package/dist/plugins/shiftBwm/src/vendor/contracts.js +59 -0
  23. package/dist/plugins/shiftBwm/src/vendor/models.js +525 -0
  24. package/dist/plugins/shiftObjects/src/objects.js +1 -2
  25. package/dist/plugins/shiftOcr/src/index.js +3 -1
  26. package/dist/plugins/shiftTranscription/src/shared.js +1 -1
  27. package/dist/plugins/shiftTranscription/src/transcription.js +4 -3
  28. package/dist/plugins/together/src/index.js +1 -1
  29. package/dist/plugins/xai/src/index.js +1 -1
  30. package/package.json +3 -2
@@ -18,10 +18,8 @@ export declare function validateLegacyInput(schema: LegacyInputSchema, input: un
18
18
  /**
19
19
  * A call written with no arguments is a call with an empty argument set.
20
20
  *
21
- * `linear.user.me()` is the most natural way to invoke an action that
22
- * takes nothing, and validating `undefined` against an object schema
23
- * rejected it every time. It also made a genuinely missing input report
24
- * "must be object" instead of naming the field.
21
+ * `linear.user.me()` supplies no argument, so object validation needs an
22
+ * empty object to accept no-input actions and name missing required fields.
25
23
  *
26
24
  * Only `undefined` is normalized: an explicit `null` or `42` is an
27
25
  * argument, and a wrong one.
@@ -53,12 +53,43 @@ export function helpInputs(schema) {
53
53
  },
54
54
  ]));
55
55
  }
56
- const metadata = schema;
57
- if (metadata.type !== "object")
58
- return {};
59
- return describeProperties(metadata, 0);
56
+ return describeProperties(schema, 0);
60
57
  }
61
58
  function describeProperties(schema, depth) {
59
+ if (depth > MAX_DESCRIBE_DEPTH)
60
+ return {};
61
+ const branches = branchesOf(schema);
62
+ if (branches && !schema.properties) {
63
+ const alternatives = branches.map((branch) => describeProperties(branch, depth + 1));
64
+ const names = new Set(alternatives.flatMap((fields) => Object.keys(fields)));
65
+ return Object.fromEntries([...names].map((name) => {
66
+ const fields = alternatives.flatMap((fields) => Object.hasOwn(fields, name)
67
+ ? [{ ...fields[name], required: false }]
68
+ : []);
69
+ // Descriptions are depth-bounded, so deduplication is safe for recursive schemas.
70
+ const unique = [
71
+ ...new Map(fields.map((field) => [JSON.stringify(field), field])).values(),
72
+ ];
73
+ const required = alternatives.every((fields) => Object.hasOwn(fields, name) && fields[name].required);
74
+ if (unique.length === 1)
75
+ return [name, { ...unique[0], required }];
76
+ const values = unique.map((field) => field.enum ??
77
+ (field.const !== undefined ? [field.const] : undefined));
78
+ return [
79
+ name,
80
+ {
81
+ type: [...new Set(unique.map((field) => field.type))].join(" | "),
82
+ displayType: [
83
+ ...new Set(unique.map((field) => field.displayType)),
84
+ ].join(" | "),
85
+ required,
86
+ ...(values.every((value) => value !== undefined)
87
+ ? { enum: [...new Set(values.flat())] }
88
+ : { variants: unique }),
89
+ },
90
+ ];
91
+ }));
92
+ }
62
93
  const required = new Set(schema.required ?? []);
63
94
  return Object.fromEntries(Object.entries(schema.properties ?? {}).map(([key, field]) => [
64
95
  key,
@@ -158,10 +189,8 @@ export function validateLegacyInput(schema, input) {
158
189
  /**
159
190
  * A call written with no arguments is a call with an empty argument set.
160
191
  *
161
- * `linear.user.me()` is the most natural way to invoke an action that
162
- * takes nothing, and validating `undefined` against an object schema
163
- * rejected it every time. It also made a genuinely missing input report
164
- * "must be object" instead of naming the field.
192
+ * `linear.user.me()` supplies no argument, so object validation needs an
193
+ * empty object to accept no-input actions and name missing required fields.
165
194
  *
166
195
  * Only `undefined` is normalized: an explicit `null` or `42` is an
167
196
  * argument, and a wrong one.
@@ -224,20 +253,26 @@ function branchesOf(schema) {
224
253
  const branches = schema.anyOf ?? schema.oneOf;
225
254
  return branches?.length ? branches : undefined;
226
255
  }
227
- function displayType(schema) {
256
+ function displayType(schema, depth = 0) {
257
+ if (depth >= MAX_DESCRIBE_DEPTH)
258
+ return schema.type ?? "unknown";
228
259
  const branches = branchesOf(schema);
229
260
  if (branches)
230
- return branches.map(displayType).join(" | ");
261
+ return branches.map((branch) => displayType(branch, depth + 1)).join(" | ");
231
262
  if (schema.enum?.length)
232
263
  return schema.enum.map(String).join(" | ");
233
264
  if (schema.const !== undefined)
234
265
  return JSON.stringify(schema.const);
235
266
  return schema.type ?? "unknown";
236
267
  }
237
- function baseType(schema) {
268
+ function baseType(schema, depth = 0) {
269
+ if (depth >= MAX_DESCRIBE_DEPTH)
270
+ return schema.type ?? "unknown";
238
271
  const branches = branchesOf(schema);
239
272
  if (branches) {
240
- const types = [...new Set(branches.map(baseType))];
273
+ const types = [
274
+ ...new Set(branches.map((branch) => baseType(branch, depth + 1))),
275
+ ];
241
276
  return types.length === 1 ? types[0] : types.join(" | ");
242
277
  }
243
278
  if (schema.type)
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Shared helper for image-generation plugins: write generated bytes to a
2
+ * Shared helper for media-generating plugins: write generated bytes to a
3
3
  * file and return its path, instead of returning raw base64.
4
4
  *
5
- * Base64 image payloads in an action result bloat the agent context and are
5
+ * Base64 payloads in an action result bloat the agent context and are
6
6
  * stripped by many hosts before reaching the model, so the agent can never
7
- * actually deliver the image. A file `path` hands straight to a host
7
+ * actually deliver the file. A file `path` hands straight to a host
8
8
  * send_file/attachment tool. Plugins load in-process via jiti, so node:fs is
9
9
  * available (same pattern as googleDrive/googleSlides).
10
10
  */
11
+ import { randomUUID } from "node:crypto";
11
12
  import { readFileSync, writeFileSync } from "node:fs";
12
13
  import { tmpdir } from "node:os";
13
14
  import { basename, extname, join } from "node:path";
@@ -18,26 +19,46 @@ const MIME_EXT = {
18
19
  "image/webp": "webp",
19
20
  "image/gif": "gif",
20
21
  "image/svg+xml": "svg",
22
+ "video/mp4": "mp4",
23
+ "video/webm": "webm",
24
+ "video/quicktime": "mov",
25
+ "audio/mpeg": "mp3",
26
+ "audio/mp3": "mp3",
27
+ "audio/wav": "wav",
28
+ "audio/x-wav": "wav",
29
+ "audio/ogg": "ogg",
30
+ "audio/opus": "opus",
31
+ "audio/x-pcm": "pcm",
21
32
  };
22
33
  export function extForMime(mimeType) {
23
34
  if (!mimeType)
24
35
  return "png";
25
- return MIME_EXT[mimeType] ?? mimeType.split("/")[1]?.split("+")[0] ?? "png";
36
+ const mime = mimeType.split(";")[0].trim().toLowerCase();
37
+ return Object.hasOwn(MIME_EXT, mime) ? MIME_EXT[mime] : "bin";
26
38
  }
27
39
  /**
28
- * Decode base64 image bytes and write them to `saveDir` (or the OS temp dir),
29
- * named `<provider>-<stamp>-<index>.<ext>`. Returns the file path + size.
40
+ * Write media bytes with a unique name in `saveDir` (or the OS temp dir).
41
+ * Exclusive creation prevents overwrites and following a pre-existing file symlink.
30
42
  */
31
- export function writeImageFile(opts) {
43
+ export function writeMediaFile(opts) {
32
44
  const mimeType = opts.mimeType ?? "image/png";
33
45
  const dir = (typeof opts.saveDir === "string" && opts.saveDir.trim()) || tmpdir();
34
- const bytes = Buffer.from(opts.base64 ?? "", "base64");
35
46
  const stamp = opts.stamp ?? Date.now();
36
- const path = join(dir, `${opts.provider}-${stamp}-${opts.index}.${extForMime(mimeType)}`);
37
- writeFileSync(path, bytes);
38
- return { path, mimeType, byteLength: bytes.length };
47
+ if (!/^[\w-]+$/.test(opts.provider))
48
+ throw new Error("invalid media provider name");
49
+ const path = join(dir, `${opts.provider}-${stamp}-${opts.index}-${randomUUID()}.${extForMime(mimeType)}`);
50
+ writeFileSync(path, opts.bytes, { flag: "wx", mode: 0o600 });
51
+ return { path, mimeType, byteLength: opts.bytes.byteLength };
52
+ }
53
+ /**
54
+ * Decode base64 media bytes and write them, for the providers that answer
55
+ * with base64 rather than a URL to download.
56
+ */
57
+ export function writeImageFile(opts) {
58
+ const { base64, ...rest } = opts;
59
+ return writeMediaFile({ ...rest, bytes: Buffer.from(base64 ?? "", "base64") });
39
60
  }
40
- export const SEND_FILE_NOTE = "Image(s) written to disk. Deliver each to the user with send_file using its `path`.";
61
+ export const SEND_FILE_NOTE = "File(s) written to disk. Deliver each to the user with send_file using its `path`.";
41
62
  const EXT_MIME = {
42
63
  png: "image/png",
43
64
  jpg: "image/jpeg",
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Helpers for plugins that consume an undocumented provider JSON API over a
3
- * bearer token (gett, wolt): total readers for unshaped payloads, path-segment
4
- * safety, and a bounded body read.
2
+ * Provider helpers: total readers for unshaped JSON, path-segment safety,
3
+ * and bounded text or binary body reads.
5
4
  *
6
5
  * The readers are total by design. These payloads are deep and change without
7
6
  * notice, so a missing or wrongly-typed field yields the empty value for its
@@ -52,23 +51,30 @@ export function seg(value, what, plugin) {
52
51
  * make the process hold an unbounded string.
53
52
  */
54
53
  export async function readBounded(res, maxBytes, oversizeMessage) {
54
+ return new TextDecoder().decode(await readBoundedBytes(res, maxBytes, oversizeMessage));
55
+ }
56
+ /** Text and binary consumers share the same streaming byte limit. */
57
+ export async function readBoundedBytes(res, maxBytes, oversizeMessage) {
55
58
  const reader = res.body?.getReader();
56
59
  if (!reader)
57
- return "";
60
+ return new Uint8Array();
58
61
  const chunks = [];
59
62
  let total = 0;
60
- for (;;) {
61
- const { done, value } = await reader.read();
62
- if (done)
63
- break;
64
- if (!value)
65
- continue;
66
- total += value.byteLength;
67
- if (total > maxBytes) {
68
- await reader.cancel();
69
- throw new Error(oversizeMessage);
63
+ try {
64
+ for (;;) {
65
+ const { done, value } = await reader.read();
66
+ if (done)
67
+ break;
68
+ total += value.byteLength;
69
+ if (total > maxBytes) {
70
+ await reader.cancel();
71
+ throw new Error(oversizeMessage);
72
+ }
73
+ chunks.push(value);
70
74
  }
71
- chunks.push(value);
75
+ }
76
+ finally {
77
+ reader.releaseLock();
72
78
  }
73
79
  const joined = new Uint8Array(total);
74
80
  let offset = 0;
@@ -76,5 +82,5 @@ export async function readBounded(res, maxBytes, oversizeMessage) {
76
82
  joined.set(chunk, offset);
77
83
  offset += chunk.byteLength;
78
84
  }
79
- return new TextDecoder().decode(joined);
85
+ return joined;
80
86
  }
@@ -1,4 +1,6 @@
1
1
  import * as t from "typebox";
2
+ import { authedFetch } from "./authedFetch.js";
3
+ import { readBounded } from "./provider.js";
2
4
  export const STRICT_OBJECT = { additionalProperties: false };
3
5
  export const STRICT_UPDATE_OBJECT = {
4
6
  additionalProperties: false,
@@ -23,24 +25,46 @@ export function enumDescription(name, values) {
23
25
  export function enumSchema(name, values) {
24
26
  return t.Union(values.map((value) => t.Literal(value)), { description: enumDescription(name, values) });
25
27
  }
26
- const SHIFT_API_URL = "https://cloud.shift-labs.ai";
28
+ export const SHIFT_API_URL = "https://cloud.shift-labs.ai";
27
29
  export function baseUrl() {
28
30
  return `${SHIFT_API_URL}/`;
29
31
  }
30
- export async function request(ctx, path, init = {}) {
31
- const headers = new Headers(init.headers);
32
- const apiKey = ctx.connection.config.apiKey;
33
- if (typeof apiKey !== "string" || !apiKey) {
32
+ /** Default deadline; endpoints that hold the request open pass their own. */
33
+ export const SHIFT_REQUEST_TIMEOUT_MS = 60_000;
34
+ const MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
35
+ /**
36
+ * The transport for every request carrying a Shift API key. Redirects are
37
+ * refused so the key never reaches another host; one deadline covers the
38
+ * whole exchange including the body, in place of any caller signal; the
39
+ * text body is read with a ceiling and returned buffered, so JSON and text
40
+ * callers consume it as usual.
41
+ */
42
+ export async function shiftFetch(input, init = {}, timeoutMs = SHIFT_REQUEST_TIMEOUT_MS) {
43
+ const response = await authedFetch(input, {
44
+ ...init,
45
+ signal: AbortSignal.timeout(timeoutMs),
46
+ });
47
+ const text = await readBounded(response, MAX_RESPONSE_BYTES, "Shift API response exceeds 16 MiB");
48
+ return new Response(text || null, {
49
+ status: response.status,
50
+ statusText: response.statusText,
51
+ headers: response.headers,
52
+ });
53
+ }
54
+ export function apiKey(ctx) {
55
+ const key = ctx.connection.config.apiKey;
56
+ if (typeof key !== "string" || !key) {
34
57
  throw new Error("Shift Labs apiKey is required");
35
58
  }
36
- headers.set("authorization", `Bearer ${apiKey}`);
59
+ return key;
60
+ }
61
+ export async function request(ctx, path, init = {}, timeoutMs = SHIFT_REQUEST_TIMEOUT_MS) {
62
+ const headers = new Headers(init.headers);
63
+ headers.set("authorization", `Bearer ${apiKey(ctx)}`);
37
64
  if (init.body && !headers.has("content-type")) {
38
65
  headers.set("content-type", "application/json");
39
66
  }
40
- const response = await fetch(new URL(path, baseUrl()), {
41
- ...init,
42
- headers,
43
- });
67
+ const response = await shiftFetch(new URL(path, baseUrl()), { ...init, headers }, timeoutMs);
44
68
  if (!response.ok) {
45
69
  throw new Error(`Shift Labs API error ${response.status}: ${await response.text()}`);
46
70
  }
@@ -95,7 +95,7 @@ async function fileBody(path, sizeBytes) {
95
95
  if (sizeBytes > MAX_BUFFERED_BYTES) {
96
96
  throw new Error("This runtime cannot stream uploads and the file is too large to buffer.");
97
97
  }
98
- return await readFile(path);
98
+ return new Blob([await readFile(path)]);
99
99
  }
100
100
  }
101
101
  /**
@@ -0,0 +1,277 @@
1
+ import * as t from "typebox";
2
+ import { audioOptions, formatSchema, musicSchema as music, STRICT, seed, speechControls, text, timeout, voiceSettings, } from "./schemas.js";
3
+ import { appendFile, audioRequest, jsonRequest, voiceForm, voicePath, } from "./shared.js";
4
+ import { registerWorkflows } from "./workflows.js";
5
+ const voice = t.Object({ voiceId: text }, STRICT);
6
+ const speech = t.Object({
7
+ ...audioOptions,
8
+ voiceId: text,
9
+ text,
10
+ ...speechControls,
11
+ timestamps: t.Optional(t.Boolean({
12
+ description: "Return character alignment with the saved audio, not base64.",
13
+ })),
14
+ model: t.Optional(t.String({ default: "eleven_multilingual_v2", minLength: 1 })),
15
+ languageCode: t.Optional(t.String({
16
+ pattern: "^[a-z]{2}$",
17
+ description: "ISO 639-1 language. Not supported by eleven_multilingual_v2; use a compatible model such as Flash or Turbo.",
18
+ })),
19
+ voiceSettings: t.Optional(voiceSettings),
20
+ }, STRICT);
21
+ const convert = t.Object({
22
+ ...audioOptions,
23
+ voiceId: text,
24
+ audioPath: t.String({
25
+ minLength: 1,
26
+ description: "Local audio file, at most 25 MiB.",
27
+ }),
28
+ model: t.Optional(t.String({ default: "eleven_multilingual_sts_v2", minLength: 1 })),
29
+ voiceSettings: t.Optional(voiceSettings),
30
+ removeBackgroundNoise: t.Optional(t.Boolean()),
31
+ seed: t.Optional(seed),
32
+ }, STRICT);
33
+ const transcribe = t.Object({
34
+ filePath: t.Optional(t.String({
35
+ minLength: 1,
36
+ description: "Local audio/video file, at most 25 MiB. Exactly one of filePath or sourceUrl is required.",
37
+ })),
38
+ sourceUrl: t.Optional(t.String({
39
+ pattern: "^https://",
40
+ description: "Remote source fetched by ElevenLabs, not this host. Exactly one of filePath or sourceUrl is required.",
41
+ })),
42
+ timestampsGranularity: t.Optional(t.Union([t.Literal("none"), t.Literal("word"), t.Literal("character")])),
43
+ keyterms: t.Optional(t.Array(t.String({
44
+ minLength: 1,
45
+ maxLength: 49,
46
+ pattern: "^[^<>{}\\[\\]\\\\]+$",
47
+ }), {
48
+ maxItems: 1000,
49
+ description: "Up to 5 words per term. Adds 20% cost; over 100 terms has a 20-second minimum billable duration.",
50
+ })),
51
+ multiChannel: t.Optional(t.Boolean({
52
+ description: "Transcribe up to 5 channels independently; each channel is billed at the full audio duration.",
53
+ })),
54
+ channelOutput: t.Optional(t.Union([t.Literal("separate"), t.Literal("combined")])),
55
+ model: t.Optional(t.Union([t.Literal("scribe_v1"), t.Literal("scribe_v2")], {
56
+ default: "scribe_v2",
57
+ })),
58
+ languageCode: t.Optional(text),
59
+ diarize: t.Optional(t.Boolean()),
60
+ numSpeakers: t.Optional(t.Integer({ minimum: 1, maximum: 32 })),
61
+ tagAudioEvents: t.Optional(t.Boolean()),
62
+ timeoutMs: t.Optional(timeout),
63
+ }, STRICT);
64
+ const list = t.Object({
65
+ limit: t.Optional(t.Integer({ minimum: 1, maximum: 100, default: 10 })),
66
+ cursor: t.Optional(text),
67
+ search: t.Optional(text),
68
+ }, STRICT);
69
+ const clone = t.Object({
70
+ name: text,
71
+ audioPaths: t.Array(text, {
72
+ minItems: 1,
73
+ maxItems: 10,
74
+ description: "Local samples, each at most 25 MiB. Only voices you own or have permission to clone.",
75
+ }),
76
+ description: t.Optional(text),
77
+ labels: t.Optional(t.Record(t.String(), t.String())),
78
+ removeBackgroundNoise: t.Optional(t.Boolean()),
79
+ }, STRICT);
80
+ const sound = t.Object({
81
+ ...audioOptions,
82
+ outputFormat: t.Optional(formatSchema("sound")),
83
+ text,
84
+ durationSeconds: t.Optional(t.Number({ minimum: 0.5, maximum: 30 })),
85
+ loop: t.Optional(t.Boolean()),
86
+ promptInfluence: t.Optional(t.Number({ minimum: 0, maximum: 1 })),
87
+ }, STRICT);
88
+ /** Audio returns local file paths; catalog responses preserve provider pagination metadata. */
89
+ export default function elevenlabs(rl) {
90
+ rl.setName("elevenlabs");
91
+ rl.setVersion("0.1.0");
92
+ registerWorkflows(rl);
93
+ rl.setConnectionSchema(t.Object({
94
+ apiKey: t.String({
95
+ minLength: 1,
96
+ env: "ELEVENLABS_API_KEY",
97
+ description: "ElevenLabs key with permissions for the requested endpoints.",
98
+ }),
99
+ }));
100
+ rl.registerAction("models.list", {
101
+ access: "read",
102
+ description: "List ElevenLabs models and their supported capabilities.",
103
+ inputSchema: t.Object({}, STRICT),
104
+ async execute(_input, ctx) {
105
+ return jsonRequest(ctx, "/v1/models", "models");
106
+ },
107
+ });
108
+ rl.registerAction("voices.list", {
109
+ access: "read",
110
+ description: "Search one page of voices. Pass next_page_token back as cursor while has_more is true.",
111
+ inputSchema: list,
112
+ async execute(input, ctx) {
113
+ const p = input;
114
+ const query = new URLSearchParams({ page_size: String(p.limit ?? 10) });
115
+ if (p.cursor !== undefined)
116
+ query.set("next_page_token", p.cursor);
117
+ if (p.search !== undefined)
118
+ query.set("search", p.search);
119
+ return jsonRequest(ctx, `/v2/voices?${query}`, "voices");
120
+ },
121
+ });
122
+ rl.registerAction("voices.get", {
123
+ access: "read",
124
+ description: "Get voice metadata and settings by ID.",
125
+ inputSchema: voice,
126
+ async execute(input, ctx) {
127
+ return jsonRequest(ctx, `/v1/voices/${voicePath(input.voiceId)}`, "voice");
128
+ },
129
+ });
130
+ rl.registerAction("voices.delete", {
131
+ access: "write",
132
+ description: "Delete a voice from your ElevenLabs account.",
133
+ inputSchema: voice,
134
+ async execute(input, ctx) {
135
+ return jsonRequest(ctx, `/v1/voices/${voicePath(input.voiceId)}`, "acknowledgement", { method: "DELETE" });
136
+ },
137
+ });
138
+ rl.registerAction("voices.clone", {
139
+ access: "write",
140
+ description: "Create an instant voice clone from local audio samples you own or have permission to use. Returns voice_id and any verification requirement.",
141
+ inputSchema: clone,
142
+ async execute(input, ctx) {
143
+ const p = input;
144
+ return jsonRequest(ctx, "/v1/voices/add", "clone", {
145
+ method: "POST",
146
+ body: await voiceForm(p),
147
+ });
148
+ },
149
+ });
150
+ rl.registerAction("speech.create", {
151
+ access: "write",
152
+ description: "Convert text to speech (billed). Returns audio.path for send_file and optional character timestamps. Select a voice with voices.list; format support depends on the endpoint and account tier.",
153
+ inputSchema: speech,
154
+ async execute(input, ctx) {
155
+ const p = input;
156
+ return audioRequest(ctx, `/v1/text-to-speech/${voicePath(p.voiceId)}${p.timestamps ? "/with-timestamps" : ""}`, {
157
+ text: p.text,
158
+ model_id: p.model ?? "eleven_multilingual_v2",
159
+ language_code: p.languageCode,
160
+ voice_settings: p.voiceSettings,
161
+ seed: p.seed,
162
+ pronunciation_dictionary_locators: p.pronunciationDictionaries,
163
+ apply_text_normalization: p.normalization,
164
+ apply_language_text_normalization: p.languageNormalization,
165
+ previous_text: p.previousText,
166
+ next_text: p.nextText,
167
+ previous_request_ids: p.previousRequestIds,
168
+ next_request_ids: p.nextRequestIds,
169
+ }, p);
170
+ },
171
+ });
172
+ rl.registerAction("speech.convert", {
173
+ access: "write",
174
+ description: "Change a local recording to another voice (billed), preserving delivery. Returns audio.path in the requested format.",
175
+ inputSchema: convert,
176
+ async execute(input, ctx) {
177
+ const p = input;
178
+ const path = `/v1/speech-to-speech/${voicePath(p.voiceId)}`;
179
+ const form = new FormData();
180
+ await appendFile(form, "audio", p.audioPath);
181
+ form.append("model_id", p.model ?? "eleven_multilingual_sts_v2");
182
+ if (p.seed !== undefined)
183
+ form.append("seed", String(p.seed));
184
+ if (p.voiceSettings !== undefined)
185
+ form.append("voice_settings", JSON.stringify(p.voiceSettings));
186
+ if (p.removeBackgroundNoise !== undefined)
187
+ form.append("remove_background_noise", String(p.removeBackgroundNoise));
188
+ return audioRequest(ctx, path, form, p);
189
+ },
190
+ });
191
+ rl.registerAction("transcription.create", {
192
+ access: "write",
193
+ description: "Transcribe local audio/video or an HTTPS source with Scribe (billed). Returns text, selectable timestamps, and speaker labels; multichannel mode can return transcripts[]. Keyterms add 20%; each channel is billed separately.",
194
+ inputSchema: transcribe,
195
+ async execute(input, ctx) {
196
+ const p = input;
197
+ if (Boolean(p.filePath) === Boolean(p.sourceUrl))
198
+ throw new Error("elevenlabs: supply exactly one of filePath or sourceUrl");
199
+ if (p.channelOutput && !p.multiChannel)
200
+ throw new Error("elevenlabs: channelOutput requires multiChannel");
201
+ if (p.channelOutput === "combined" && p.timestampsGranularity === "none")
202
+ throw new Error("elevenlabs: combined channels require timestamps");
203
+ if (p.keyterms?.some((term) => !term.trim() || term.trim().split(/\s+/u).length > 5))
204
+ throw new Error("elevenlabs: keyterms allow at most 5 words each");
205
+ const form = new FormData();
206
+ if (p.filePath)
207
+ await appendFile(form, "file", p.filePath);
208
+ if (p.sourceUrl) {
209
+ const url = new URL(p.sourceUrl);
210
+ if (url.protocol !== "https:" || url.username || url.password)
211
+ throw new Error("elevenlabs: sourceUrl requires HTTPS without embedded credentials");
212
+ form.append("source_url", url.href);
213
+ }
214
+ for (const term of p.keyterms ?? [])
215
+ form.append("keyterms", term);
216
+ form.append("model_id", p.model ?? "scribe_v2");
217
+ for (const [key, value] of Object.entries({
218
+ language_code: p.languageCode,
219
+ diarize: p.diarize,
220
+ num_speakers: p.numSpeakers,
221
+ tag_audio_events: p.tagAudioEvents,
222
+ timestamps_granularity: p.timestampsGranularity,
223
+ use_multi_channel: p.multiChannel,
224
+ multichannel_output_style: p.channelOutput,
225
+ })) {
226
+ if (value !== undefined)
227
+ form.append(key, String(value));
228
+ }
229
+ return jsonRequest(ctx, "/v1/speech-to-text", "transcription", { method: "POST", body: form }, p.timeoutMs ?? 300_000);
230
+ },
231
+ });
232
+ rl.registerAction("music.create", {
233
+ access: "write",
234
+ description: "Compose music (billed) from either prompt + durationMs, or a model-specific compositionPlan with explicit section durations. Supports v1 sections and v2/v2.5 chunks. Returns audio.path; plans are capped at 10 minutes total.",
235
+ inputSchema: music,
236
+ async execute(input, ctx) {
237
+ const p = input;
238
+ if ("compositionPlan" in p) {
239
+ const parts = "sections" in p.compositionPlan
240
+ ? p.compositionPlan.sections
241
+ : p.compositionPlan.chunks;
242
+ if (parts.reduce((sum, part) => sum + part.duration_ms, 0) > 600000)
243
+ throw new Error("elevenlabs: composition plans are limited to 600000ms total");
244
+ }
245
+ return audioRequest(ctx, "/v1/music", "prompt" in p
246
+ ? {
247
+ prompt: p.prompt,
248
+ music_length_ms: p.durationMs,
249
+ model_id: p.model ?? "music_v1",
250
+ force_instrumental: p.instrumental ?? false,
251
+ }
252
+ : {
253
+ composition_plan: p.compositionPlan,
254
+ model_id: p.model,
255
+ seed: p.seed,
256
+ respect_sections_durations: "respectSectionsDurations" in p
257
+ ? p.respectSectionsDurations
258
+ : undefined,
259
+ }, { ...p, outputFormat: p.outputFormat ?? "auto" });
260
+ },
261
+ });
262
+ rl.registerAction("sound.create", {
263
+ access: "write",
264
+ description: "Generate sound effects from text (billed) using eleven_text_to_sound_v2. Returns audio.path in the requested format.",
265
+ inputSchema: sound,
266
+ async execute(input, ctx) {
267
+ const p = input;
268
+ return audioRequest(ctx, "/v1/sound-generation", {
269
+ text: p.text,
270
+ model_id: "eleven_text_to_sound_v2",
271
+ duration_seconds: p.durationSeconds,
272
+ loop: p.loop,
273
+ prompt_influence: p.promptInfluence,
274
+ }, p);
275
+ },
276
+ });
277
+ }