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 CHANGED
@@ -1793,7 +1793,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1793
1793
  var init_constants = __esm({
1794
1794
  "src/constants.ts"() {
1795
1795
  init_pricing();
1796
- CASCADE_VERSION = "0.51.0";
1796
+ CASCADE_VERSION = "0.52.0";
1797
1797
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1798
1798
  CASCADE_DB_FILE = ".cascade/memory.db";
1799
1799
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -5050,7 +5050,14 @@ var ToolsConfigSchema = zod.z.object({
5050
5050
  * built-in tool (shell/file/git/…) from existing at all, rather than just
5051
5051
  * gating it behind approval. Omitted = full default set (unchanged).
5052
5052
  */
5053
- enabledTools: zod.z.array(zod.z.string()).optional()
5053
+ enabledTools: zod.z.array(zod.z.string()).optional(),
5054
+ /**
5055
+ * Tools to remove even when they would otherwise be registered. Unlike
5056
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
5057
+ * tools registered outside it — MCP tools and media generation — which is
5058
+ * the only way to turn those off.
5059
+ */
5060
+ disabledTools: zod.z.array(zod.z.string()).optional()
5054
5061
  });
5055
5062
  var HookDefinitionSchema = zod.z.object({
5056
5063
  name: zod.z.string().optional(),
@@ -11075,17 +11082,13 @@ var CAPABILITIES = [
11075
11082
  },
11076
11083
  // ── Video ──
11077
11084
  {
11078
- // Listed so `describe()` can tell the truth about what exists, and so the
11079
- // planner is told video is out of reach rather than left to invent a tool.
11080
- // Veo is a long-running operation: submit, poll, then fetch bytes from a
11081
- // signed URL. That is a different execution shape from every other entry
11082
- // here and is not wired up — claiming otherwise would produce a tool that
11083
- // fails at call time instead of an honest "not yet".
11085
+ // Long-running: submit returns an operation name, the video lands minutes
11086
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
11087
+ // that polling loop; the registry only records the shape via `api`.
11084
11088
  modality: "video",
11085
11089
  provider: "gemini",
11086
11090
  modelId: "veo-3.1-generate-001",
11087
- api: "gemini-predict",
11088
- unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
11091
+ api: "gemini-predict-lro"
11089
11092
  }
11090
11093
  ];
11091
11094
  function capabilityCost(cap) {
@@ -11398,6 +11401,75 @@ async function generateSpeech(cap, cfg, req, signal) {
11398
11401
  };
11399
11402
  }, cap.modelId);
11400
11403
  }
11404
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
11405
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
11406
+ function sleep(ms, signal) {
11407
+ return new Promise((resolve2, reject) => {
11408
+ if (signal?.aborted) return reject(new Error("Cancelled."));
11409
+ const timer = setTimeout(() => {
11410
+ signal?.removeEventListener("abort", onAbort);
11411
+ resolve2();
11412
+ }, ms);
11413
+ function onAbort() {
11414
+ clearTimeout(timer);
11415
+ reject(new Error("Cancelled."));
11416
+ }
11417
+ signal?.addEventListener("abort", onAbort, { once: true });
11418
+ });
11419
+ }
11420
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
11421
+ return callProvider(async () => {
11422
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
11423
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11424
+ const key = cfg.apiKey ?? "";
11425
+ const headers = { "x-goog-api-key": key };
11426
+ const submit = await postJson(
11427
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
11428
+ {
11429
+ instances: [{ prompt: req.prompt }],
11430
+ parameters: {
11431
+ aspectRatio: req.aspectRatio ?? "16:9",
11432
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
11433
+ }
11434
+ },
11435
+ headers,
11436
+ signal
11437
+ );
11438
+ const started = await submit.json();
11439
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
11440
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
11441
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
11442
+ let operation = {};
11443
+ while (Date.now() < deadline) {
11444
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
11445
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
11446
+ if (!poll.ok) {
11447
+ const text = await poll.text().catch(() => "");
11448
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
11449
+ }
11450
+ operation = await poll.json();
11451
+ if (operation.done) break;
11452
+ onProgress?.("Still rendering\u2026");
11453
+ }
11454
+ if (!operation.done) {
11455
+ throw new Error(
11456
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
11457
+ );
11458
+ }
11459
+ if (operation.error?.message) throw new Error(operation.error.message);
11460
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
11461
+ if (!uri) throw new Error("Video finished but the response contained no video.");
11462
+ const media = await fetch(uri, { headers, signal });
11463
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
11464
+ return {
11465
+ data: Buffer.from(await media.arrayBuffer()),
11466
+ mimeType: "video/mp4",
11467
+ filename: stamp("mp4"),
11468
+ modelId: cap.modelId,
11469
+ provider: cap.provider
11470
+ };
11471
+ }, cap.modelId);
11472
+ }
11401
11473
  async function transcribeAudio(cap, cfg, req, signal) {
11402
11474
  return callProvider(async () => {
11403
11475
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -11522,6 +11594,58 @@ var GenerateSpeechTool = class extends BaseTool {
11522
11594
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11523
11595
  }
11524
11596
  };
11597
+ var DEFAULT_VIDEO_SECONDS = 5;
11598
+ var MAX_VIDEO_SECONDS = 8;
11599
+ var GenerateVideoTool = class extends BaseTool {
11600
+ constructor(deps) {
11601
+ super();
11602
+ this.deps = deps;
11603
+ }
11604
+ deps;
11605
+ name = "generate_video";
11606
+ // The price is in the description on purpose. Video is ~1000x the cost of an
11607
+ // image per call, it is billed per SECOND, and the model choosing the
11608
+ // duration is the one deciding the bill — so the number has to be where the
11609
+ // model reads before calling, not only in the result afterwards.
11610
+ 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.";
11611
+ inputSchema = {
11612
+ type: "object",
11613
+ properties: {
11614
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
11615
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
11616
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
11617
+ model: { type: "string", description: "Optional: force a specific video model." }
11618
+ },
11619
+ required: ["prompt"]
11620
+ };
11621
+ async execute(input, options) {
11622
+ const prompt = String(input["prompt"] ?? "").trim();
11623
+ if (!prompt) return 'Provide a "prompt" describing the video.';
11624
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
11625
+ if (!r.ok) return r.error;
11626
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
11627
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
11628
+ const asset = await generateVideo(
11629
+ r.selected.capability,
11630
+ r.cfg,
11631
+ {
11632
+ prompt,
11633
+ seconds,
11634
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
11635
+ },
11636
+ options.signal,
11637
+ // Progress goes to the run log so a two-minute wait doesn't look like a
11638
+ // hang — the single most likely reason someone kills a working render.
11639
+ (note) => options.onProgress?.(note)
11640
+ );
11641
+ const location = await this.deps.sink(asset);
11642
+ const unit = r.selected.cost;
11643
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
11644
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
11645
+ return `Video generated and saved to ${location}.
11646
+ Model: ${asset.modelId}.${estimate}${clamped}`;
11647
+ }
11648
+ };
11525
11649
  var TranscribeAudioTool = class extends BaseTool {
11526
11650
  constructor(deps, readFile) {
11527
11651
  super();
@@ -11572,6 +11696,7 @@ function buildMediaTools(deps, readFile) {
11572
11696
  const modalities = new Set(deps.registry.availableModalities());
11573
11697
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11574
11698
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11699
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
11575
11700
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11576
11701
  return tools;
11577
11702
  }
@@ -15991,6 +16116,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15991
16116
  router;
15992
16117
  toolRegistry;
15993
16118
  multimodal;
16119
+ mediaSink;
15994
16120
  mcpClient;
15995
16121
  codeIndex;
15996
16122
  config;
@@ -16310,14 +16436,27 @@ ${input}` }],
16310
16436
  * server stores a file row) constructs the tools itself via buildMediaTools
16311
16437
  * with its own sink.
16312
16438
  */
16439
+ /**
16440
+ * Replace where generated media is written.
16441
+ *
16442
+ * The default sink writes into the workspace, which is what a desktop or CLI
16443
+ * run wants. A host with different storage — the cloud server keeps a file
16444
+ * row plus bytes under the tenant's directory — supplies its own here, and
16445
+ * the media tools are rebuilt around it. Re-registering by name replaces the
16446
+ * existing tools rather than duplicating them.
16447
+ */
16448
+ setMediaSink(sink) {
16449
+ this.mediaSink = sink;
16450
+ this.registerMediaTools(this.workspacePath);
16451
+ }
16313
16452
  registerMediaTools(workspacePath) {
16314
- const allowlist = this.config.tools?.enabledTools;
16315
- const permitted = (name) => !allowlist || allowlist.includes(name);
16453
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
16454
+ const permitted = (name) => !denied.has(name);
16316
16455
  const tools = buildMediaTools(
16317
16456
  {
16318
16457
  registry: this.multimodal,
16319
16458
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16320
- sink: async (asset) => {
16459
+ sink: this.mediaSink ?? (async (asset) => {
16321
16460
  const fsp = await import('fs/promises');
16322
16461
  const nodePath = await import('path');
16323
16462
  const dir = nodePath.join(workspacePath, "generated");
@@ -16325,7 +16464,7 @@ ${input}` }],
16325
16464
  const dest = nodePath.join(dir, asset.filename);
16326
16465
  await fsp.writeFile(dest, asset.data);
16327
16466
  return nodePath.relative(workspacePath, dest);
16328
- }
16467
+ })
16329
16468
  },
16330
16469
  async (p) => {
16331
16470
  const fsp = await import('fs/promises');
@@ -17329,7 +17468,7 @@ var CloudClient = class _CloudClient {
17329
17468
  const deadline = Date.now() + start.expiresIn * 1e3;
17330
17469
  while (Date.now() < deadline) {
17331
17470
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
17332
- await sleep(interval * 1e3, opts.signal);
17471
+ await sleep2(interval * 1e3, opts.signal);
17333
17472
  const r = await this.pollDevice(start.deviceCode);
17334
17473
  if (r === "slow_down") {
17335
17474
  interval += 5;
@@ -17540,7 +17679,7 @@ var CloudClient = class _CloudClient {
17540
17679
  this.store.clear();
17541
17680
  }
17542
17681
  };
17543
- function sleep(ms, signal) {
17682
+ function sleep2(ms, signal) {
17544
17683
  return new Promise((resolve2, reject) => {
17545
17684
  const t = setTimeout(resolve2, ms);
17546
17685
  signal?.addEventListener("abort", () => {