cascade-ai 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +155 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +155 -16
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +155 -16
- package/dist/index.cjs +155 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +155 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/desktop-core.cjs
CHANGED
|
@@ -224247,7 +224247,7 @@ function reconcilePrice(model, live) {
|
|
|
224247
224247
|
}
|
|
224248
224248
|
|
|
224249
224249
|
// src/constants.ts
|
|
224250
|
-
var CASCADE_VERSION = "0.
|
|
224250
|
+
var CASCADE_VERSION = "0.52.0";
|
|
224251
224251
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
224252
224252
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
224253
224253
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -257153,17 +257153,13 @@ var CAPABILITIES = [
|
|
|
257153
257153
|
},
|
|
257154
257154
|
// ── Video ──
|
|
257155
257155
|
{
|
|
257156
|
-
//
|
|
257157
|
-
//
|
|
257158
|
-
//
|
|
257159
|
-
// signed URL. That is a different execution shape from every other entry
|
|
257160
|
-
// here and is not wired up — claiming otherwise would produce a tool that
|
|
257161
|
-
// fails at call time instead of an honest "not yet".
|
|
257156
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
257157
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
257158
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
257162
257159
|
modality: "video",
|
|
257163
257160
|
provider: "gemini",
|
|
257164
257161
|
modelId: "veo-3.1-generate-001",
|
|
257165
|
-
api: "gemini-predict"
|
|
257166
|
-
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
257162
|
+
api: "gemini-predict-lro"
|
|
257167
257163
|
}
|
|
257168
257164
|
];
|
|
257169
257165
|
function capabilityCost(cap) {
|
|
@@ -257476,6 +257472,75 @@ async function generateSpeech(cap, cfg, req, signal) {
|
|
|
257476
257472
|
};
|
|
257477
257473
|
}, cap.modelId);
|
|
257478
257474
|
}
|
|
257475
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
257476
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
257477
|
+
function sleep4(ms2, signal) {
|
|
257478
|
+
return new Promise((resolve2, reject) => {
|
|
257479
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
257480
|
+
const timer = setTimeout(() => {
|
|
257481
|
+
signal?.removeEventListener("abort", onAbort);
|
|
257482
|
+
resolve2();
|
|
257483
|
+
}, ms2);
|
|
257484
|
+
function onAbort() {
|
|
257485
|
+
clearTimeout(timer);
|
|
257486
|
+
reject(new Error("Cancelled."));
|
|
257487
|
+
}
|
|
257488
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
257489
|
+
});
|
|
257490
|
+
}
|
|
257491
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
257492
|
+
return callProvider(async () => {
|
|
257493
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
257494
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
257495
|
+
const key = cfg.apiKey ?? "";
|
|
257496
|
+
const headers = { "x-goog-api-key": key };
|
|
257497
|
+
const submit = await postJson(
|
|
257498
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
257499
|
+
{
|
|
257500
|
+
instances: [{ prompt: req.prompt }],
|
|
257501
|
+
parameters: {
|
|
257502
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
257503
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
257504
|
+
}
|
|
257505
|
+
},
|
|
257506
|
+
headers,
|
|
257507
|
+
signal
|
|
257508
|
+
);
|
|
257509
|
+
const started = await submit.json();
|
|
257510
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
257511
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
257512
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
257513
|
+
let operation = {};
|
|
257514
|
+
while (Date.now() < deadline) {
|
|
257515
|
+
await sleep4(VIDEO_POLL_INTERVAL_MS, signal);
|
|
257516
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
257517
|
+
if (!poll.ok) {
|
|
257518
|
+
const text = await poll.text().catch(() => "");
|
|
257519
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
257520
|
+
}
|
|
257521
|
+
operation = await poll.json();
|
|
257522
|
+
if (operation.done) break;
|
|
257523
|
+
onProgress?.("Still rendering\u2026");
|
|
257524
|
+
}
|
|
257525
|
+
if (!operation.done) {
|
|
257526
|
+
throw new Error(
|
|
257527
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
257528
|
+
);
|
|
257529
|
+
}
|
|
257530
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
257531
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
257532
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
257533
|
+
const media = await fetch(uri, { headers, signal });
|
|
257534
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
257535
|
+
return {
|
|
257536
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
257537
|
+
mimeType: "video/mp4",
|
|
257538
|
+
filename: stamp("mp4"),
|
|
257539
|
+
modelId: cap.modelId,
|
|
257540
|
+
provider: cap.provider
|
|
257541
|
+
};
|
|
257542
|
+
}, cap.modelId);
|
|
257543
|
+
}
|
|
257479
257544
|
async function transcribeAudio(cap, cfg, req, signal) {
|
|
257480
257545
|
return callProvider(async () => {
|
|
257481
257546
|
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
@@ -257600,6 +257665,58 @@ var GenerateSpeechTool = class extends BaseTool {
|
|
|
257600
257665
|
Model: ${asset.modelId} \u2014 ${r4.selected.reason}`;
|
|
257601
257666
|
}
|
|
257602
257667
|
};
|
|
257668
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
257669
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
257670
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
257671
|
+
constructor(deps) {
|
|
257672
|
+
super();
|
|
257673
|
+
this.deps = deps;
|
|
257674
|
+
}
|
|
257675
|
+
deps;
|
|
257676
|
+
name = "generate_video";
|
|
257677
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
257678
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
257679
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
257680
|
+
// model reads before calling, not only in the result afterwards.
|
|
257681
|
+
description = "Generate a short video clip from a text description. EXPENSIVE and slow: billed per second of output (about $0.40/second, so a 5-second clip costs roughly $2) and takes 1-3 minutes. Only use when the user explicitly asks for a video, and prefer the shortest duration that satisfies the request.";
|
|
257682
|
+
inputSchema = {
|
|
257683
|
+
type: "object",
|
|
257684
|
+
properties: {
|
|
257685
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
257686
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
257687
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
257688
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
257689
|
+
},
|
|
257690
|
+
required: ["prompt"]
|
|
257691
|
+
};
|
|
257692
|
+
async execute(input, options) {
|
|
257693
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
257694
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
257695
|
+
const r4 = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
257696
|
+
if (!r4.ok) return r4.error;
|
|
257697
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
257698
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
257699
|
+
const asset = await generateVideo(
|
|
257700
|
+
r4.selected.capability,
|
|
257701
|
+
r4.cfg,
|
|
257702
|
+
{
|
|
257703
|
+
prompt,
|
|
257704
|
+
seconds,
|
|
257705
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
257706
|
+
},
|
|
257707
|
+
options.signal,
|
|
257708
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
257709
|
+
// hang — the single most likely reason someone kills a working render.
|
|
257710
|
+
(note) => options.onProgress?.(note)
|
|
257711
|
+
);
|
|
257712
|
+
const location = await this.deps.sink(asset);
|
|
257713
|
+
const unit = r4.selected.cost;
|
|
257714
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
257715
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
257716
|
+
return `Video generated and saved to ${location}.
|
|
257717
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
257718
|
+
}
|
|
257719
|
+
};
|
|
257603
257720
|
var TranscribeAudioTool = class extends BaseTool {
|
|
257604
257721
|
constructor(deps, readFile) {
|
|
257605
257722
|
super();
|
|
@@ -257650,6 +257767,7 @@ function buildMediaTools(deps, readFile) {
|
|
|
257650
257767
|
const modalities = new Set(deps.registry.availableModalities());
|
|
257651
257768
|
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
257652
257769
|
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
257770
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
257653
257771
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
257654
257772
|
return tools;
|
|
257655
257773
|
}
|
|
@@ -300272,7 +300390,14 @@ var ToolsConfigSchema = external_exports.object({
|
|
|
300272
300390
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
300273
300391
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
300274
300392
|
*/
|
|
300275
|
-
enabledTools: external_exports.array(external_exports.string()).optional()
|
|
300393
|
+
enabledTools: external_exports.array(external_exports.string()).optional(),
|
|
300394
|
+
/**
|
|
300395
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
300396
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
300397
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
300398
|
+
* the only way to turn those off.
|
|
300399
|
+
*/
|
|
300400
|
+
disabledTools: external_exports.array(external_exports.string()).optional()
|
|
300276
300401
|
});
|
|
300277
300402
|
var HookDefinitionSchema = external_exports.object({
|
|
300278
300403
|
name: external_exports.string().optional(),
|
|
@@ -302821,6 +302946,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
302821
302946
|
router;
|
|
302822
302947
|
toolRegistry;
|
|
302823
302948
|
multimodal;
|
|
302949
|
+
mediaSink;
|
|
302824
302950
|
mcpClient;
|
|
302825
302951
|
codeIndex;
|
|
302826
302952
|
config;
|
|
@@ -303140,14 +303266,27 @@ ${input}` }],
|
|
|
303140
303266
|
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
303141
303267
|
* with its own sink.
|
|
303142
303268
|
*/
|
|
303269
|
+
/**
|
|
303270
|
+
* Replace where generated media is written.
|
|
303271
|
+
*
|
|
303272
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
303273
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
303274
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
303275
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
303276
|
+
* existing tools rather than duplicating them.
|
|
303277
|
+
*/
|
|
303278
|
+
setMediaSink(sink) {
|
|
303279
|
+
this.mediaSink = sink;
|
|
303280
|
+
this.registerMediaTools(this.workspacePath);
|
|
303281
|
+
}
|
|
303143
303282
|
registerMediaTools(workspacePath) {
|
|
303144
|
-
const
|
|
303145
|
-
const permitted = (name) => !
|
|
303283
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
303284
|
+
const permitted = (name) => !denied.has(name);
|
|
303146
303285
|
const tools = buildMediaTools(
|
|
303147
303286
|
{
|
|
303148
303287
|
registry: this.multimodal,
|
|
303149
303288
|
lookupProvider: (provider) => (this.config.providers ?? []).find((p3) => p3.type === provider),
|
|
303150
|
-
sink: async (asset) => {
|
|
303289
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
303151
303290
|
const fsp = await import('fs/promises');
|
|
303152
303291
|
const nodePath = await import('path');
|
|
303153
303292
|
const dir = nodePath.join(workspacePath, "generated");
|
|
@@ -303155,7 +303294,7 @@ ${input}` }],
|
|
|
303155
303294
|
const dest = nodePath.join(dir, asset.filename);
|
|
303156
303295
|
await fsp.writeFile(dest, asset.data);
|
|
303157
303296
|
return nodePath.relative(workspacePath, dest);
|
|
303158
|
-
}
|
|
303297
|
+
})
|
|
303159
303298
|
},
|
|
303160
303299
|
async (p3) => {
|
|
303161
303300
|
const fsp = await import('fs/promises');
|
|
@@ -307953,7 +308092,7 @@ var CloudClient = class _CloudClient {
|
|
|
307953
308092
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
307954
308093
|
while (Date.now() < deadline) {
|
|
307955
308094
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
307956
|
-
await
|
|
308095
|
+
await sleep5(interval * 1e3, opts.signal);
|
|
307957
308096
|
const r4 = await this.pollDevice(start.deviceCode);
|
|
307958
308097
|
if (r4 === "slow_down") {
|
|
307959
308098
|
interval += 5;
|
|
@@ -308164,7 +308303,7 @@ var CloudClient = class _CloudClient {
|
|
|
308164
308303
|
this.store.clear();
|
|
308165
308304
|
}
|
|
308166
308305
|
};
|
|
308167
|
-
function
|
|
308306
|
+
function sleep5(ms2, signal) {
|
|
308168
308307
|
return new Promise((resolve2, reject) => {
|
|
308169
308308
|
const t4 = setTimeout(resolve2, ms2);
|
|
308170
308309
|
signal?.addEventListener("abort", () => {
|
package/dist/index.cjs
CHANGED
|
@@ -1891,7 +1891,7 @@ function reconcilePrice(model, live) {
|
|
|
1891
1891
|
}
|
|
1892
1892
|
|
|
1893
1893
|
// src/constants.ts
|
|
1894
|
-
var CASCADE_VERSION = "0.
|
|
1894
|
+
var CASCADE_VERSION = "0.52.0";
|
|
1895
1895
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
1896
1896
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
1897
1897
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -8894,17 +8894,13 @@ var CAPABILITIES = [
|
|
|
8894
8894
|
},
|
|
8895
8895
|
// ── Video ──
|
|
8896
8896
|
{
|
|
8897
|
-
//
|
|
8898
|
-
//
|
|
8899
|
-
//
|
|
8900
|
-
// signed URL. That is a different execution shape from every other entry
|
|
8901
|
-
// here and is not wired up — claiming otherwise would produce a tool that
|
|
8902
|
-
// fails at call time instead of an honest "not yet".
|
|
8897
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
8898
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
8899
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
8903
8900
|
modality: "video",
|
|
8904
8901
|
provider: "gemini",
|
|
8905
8902
|
modelId: "veo-3.1-generate-001",
|
|
8906
|
-
api: "gemini-predict"
|
|
8907
|
-
unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
|
|
8903
|
+
api: "gemini-predict-lro"
|
|
8908
8904
|
}
|
|
8909
8905
|
];
|
|
8910
8906
|
function capabilityCost(cap) {
|
|
@@ -9217,6 +9213,75 @@ async function generateSpeech(cap, cfg, req, signal) {
|
|
|
9217
9213
|
};
|
|
9218
9214
|
}, cap.modelId);
|
|
9219
9215
|
}
|
|
9216
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
9217
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
9218
|
+
function sleep(ms, signal) {
|
|
9219
|
+
return new Promise((resolve2, reject) => {
|
|
9220
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
9221
|
+
const timer = setTimeout(() => {
|
|
9222
|
+
signal?.removeEventListener("abort", onAbort);
|
|
9223
|
+
resolve2();
|
|
9224
|
+
}, ms);
|
|
9225
|
+
function onAbort() {
|
|
9226
|
+
clearTimeout(timer);
|
|
9227
|
+
reject(new Error("Cancelled."));
|
|
9228
|
+
}
|
|
9229
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
9230
|
+
});
|
|
9231
|
+
}
|
|
9232
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
9233
|
+
return callProvider(async () => {
|
|
9234
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
9235
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
9236
|
+
const key = cfg.apiKey ?? "";
|
|
9237
|
+
const headers = { "x-goog-api-key": key };
|
|
9238
|
+
const submit = await postJson(
|
|
9239
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
9240
|
+
{
|
|
9241
|
+
instances: [{ prompt: req.prompt }],
|
|
9242
|
+
parameters: {
|
|
9243
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
9244
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
9245
|
+
}
|
|
9246
|
+
},
|
|
9247
|
+
headers,
|
|
9248
|
+
signal
|
|
9249
|
+
);
|
|
9250
|
+
const started = await submit.json();
|
|
9251
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
9252
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
9253
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
9254
|
+
let operation = {};
|
|
9255
|
+
while (Date.now() < deadline) {
|
|
9256
|
+
await sleep(VIDEO_POLL_INTERVAL_MS, signal);
|
|
9257
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
9258
|
+
if (!poll.ok) {
|
|
9259
|
+
const text = await poll.text().catch(() => "");
|
|
9260
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
9261
|
+
}
|
|
9262
|
+
operation = await poll.json();
|
|
9263
|
+
if (operation.done) break;
|
|
9264
|
+
onProgress?.("Still rendering\u2026");
|
|
9265
|
+
}
|
|
9266
|
+
if (!operation.done) {
|
|
9267
|
+
throw new Error(
|
|
9268
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
9269
|
+
);
|
|
9270
|
+
}
|
|
9271
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
9272
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
9273
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
9274
|
+
const media = await fetch(uri, { headers, signal });
|
|
9275
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
9276
|
+
return {
|
|
9277
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
9278
|
+
mimeType: "video/mp4",
|
|
9279
|
+
filename: stamp("mp4"),
|
|
9280
|
+
modelId: cap.modelId,
|
|
9281
|
+
provider: cap.provider
|
|
9282
|
+
};
|
|
9283
|
+
}, cap.modelId);
|
|
9284
|
+
}
|
|
9220
9285
|
async function transcribeAudio(cap, cfg, req, signal) {
|
|
9221
9286
|
return callProvider(async () => {
|
|
9222
9287
|
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
@@ -9341,6 +9406,58 @@ var GenerateSpeechTool = class extends BaseTool {
|
|
|
9341
9406
|
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
9342
9407
|
}
|
|
9343
9408
|
};
|
|
9409
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
9410
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
9411
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
9412
|
+
constructor(deps) {
|
|
9413
|
+
super();
|
|
9414
|
+
this.deps = deps;
|
|
9415
|
+
}
|
|
9416
|
+
deps;
|
|
9417
|
+
name = "generate_video";
|
|
9418
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
9419
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
9420
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
9421
|
+
// model reads before calling, not only in the result afterwards.
|
|
9422
|
+
description = "Generate a short video clip from a text description. EXPENSIVE and slow: billed per second of output (about $0.40/second, so a 5-second clip costs roughly $2) and takes 1-3 minutes. Only use when the user explicitly asks for a video, and prefer the shortest duration that satisfies the request.";
|
|
9423
|
+
inputSchema = {
|
|
9424
|
+
type: "object",
|
|
9425
|
+
properties: {
|
|
9426
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
9427
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
9428
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
9429
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
9430
|
+
},
|
|
9431
|
+
required: ["prompt"]
|
|
9432
|
+
};
|
|
9433
|
+
async execute(input, options) {
|
|
9434
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
9435
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
9436
|
+
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
9437
|
+
if (!r.ok) return r.error;
|
|
9438
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
9439
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
9440
|
+
const asset = await generateVideo(
|
|
9441
|
+
r.selected.capability,
|
|
9442
|
+
r.cfg,
|
|
9443
|
+
{
|
|
9444
|
+
prompt,
|
|
9445
|
+
seconds,
|
|
9446
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
9447
|
+
},
|
|
9448
|
+
options.signal,
|
|
9449
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
9450
|
+
// hang — the single most likely reason someone kills a working render.
|
|
9451
|
+
(note) => options.onProgress?.(note)
|
|
9452
|
+
);
|
|
9453
|
+
const location = await this.deps.sink(asset);
|
|
9454
|
+
const unit = r.selected.cost;
|
|
9455
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
9456
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
9457
|
+
return `Video generated and saved to ${location}.
|
|
9458
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
9459
|
+
}
|
|
9460
|
+
};
|
|
9344
9461
|
var TranscribeAudioTool = class extends BaseTool {
|
|
9345
9462
|
constructor(deps, readFile) {
|
|
9346
9463
|
super();
|
|
@@ -9391,6 +9508,7 @@ function buildMediaTools(deps, readFile) {
|
|
|
9391
9508
|
const modalities = new Set(deps.registry.availableModalities());
|
|
9392
9509
|
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
9393
9510
|
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
9511
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
9394
9512
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
9395
9513
|
return tools;
|
|
9396
9514
|
}
|
|
@@ -11719,7 +11837,14 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
11719
11837
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
11720
11838
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
11721
11839
|
*/
|
|
11722
|
-
enabledTools: zod.z.array(zod.z.string()).optional()
|
|
11840
|
+
enabledTools: zod.z.array(zod.z.string()).optional(),
|
|
11841
|
+
/**
|
|
11842
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
11843
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
11844
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
11845
|
+
* the only way to turn those off.
|
|
11846
|
+
*/
|
|
11847
|
+
disabledTools: zod.z.array(zod.z.string()).optional()
|
|
11723
11848
|
});
|
|
11724
11849
|
var HookDefinitionSchema = zod.z.object({
|
|
11725
11850
|
name: zod.z.string().optional(),
|
|
@@ -14258,6 +14383,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
14258
14383
|
router;
|
|
14259
14384
|
toolRegistry;
|
|
14260
14385
|
multimodal;
|
|
14386
|
+
mediaSink;
|
|
14261
14387
|
mcpClient;
|
|
14262
14388
|
codeIndex;
|
|
14263
14389
|
config;
|
|
@@ -14577,14 +14703,27 @@ ${input}` }],
|
|
|
14577
14703
|
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
14578
14704
|
* with its own sink.
|
|
14579
14705
|
*/
|
|
14706
|
+
/**
|
|
14707
|
+
* Replace where generated media is written.
|
|
14708
|
+
*
|
|
14709
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
14710
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
14711
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
14712
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
14713
|
+
* existing tools rather than duplicating them.
|
|
14714
|
+
*/
|
|
14715
|
+
setMediaSink(sink) {
|
|
14716
|
+
this.mediaSink = sink;
|
|
14717
|
+
this.registerMediaTools(this.workspacePath);
|
|
14718
|
+
}
|
|
14580
14719
|
registerMediaTools(workspacePath) {
|
|
14581
|
-
const
|
|
14582
|
-
const permitted = (name) => !
|
|
14720
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
14721
|
+
const permitted = (name) => !denied.has(name);
|
|
14583
14722
|
const tools = buildMediaTools(
|
|
14584
14723
|
{
|
|
14585
14724
|
registry: this.multimodal,
|
|
14586
14725
|
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
14587
|
-
sink: async (asset) => {
|
|
14726
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
14588
14727
|
const fsp = await import('fs/promises');
|
|
14589
14728
|
const nodePath = await import('path');
|
|
14590
14729
|
const dir = nodePath.join(workspacePath, "generated");
|
|
@@ -14592,7 +14731,7 @@ ${input}` }],
|
|
|
14592
14731
|
const dest = nodePath.join(dir, asset.filename);
|
|
14593
14732
|
await fsp.writeFile(dest, asset.data);
|
|
14594
14733
|
return nodePath.relative(workspacePath, dest);
|
|
14595
|
-
}
|
|
14734
|
+
})
|
|
14596
14735
|
},
|
|
14597
14736
|
async (p) => {
|
|
14598
14737
|
const fsp = await import('fs/promises');
|
|
@@ -18606,7 +18745,7 @@ var CloudClient = class _CloudClient {
|
|
|
18606
18745
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
18607
18746
|
while (Date.now() < deadline) {
|
|
18608
18747
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
18609
|
-
await
|
|
18748
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
18610
18749
|
const r = await this.pollDevice(start.deviceCode);
|
|
18611
18750
|
if (r === "slow_down") {
|
|
18612
18751
|
interval += 5;
|
|
@@ -18817,7 +18956,7 @@ var CloudClient = class _CloudClient {
|
|
|
18817
18956
|
this.store.clear();
|
|
18818
18957
|
}
|
|
18819
18958
|
};
|
|
18820
|
-
function
|
|
18959
|
+
function sleep2(ms, signal) {
|
|
18821
18960
|
return new Promise((resolve2, reject) => {
|
|
18822
18961
|
const t = setTimeout(resolve2, ms);
|
|
18823
18962
|
signal?.addEventListener("abort", () => {
|