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.js CHANGED
@@ -1740,7 +1740,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1740
1740
  var init_constants = __esm({
1741
1741
  "src/constants.ts"() {
1742
1742
  init_pricing();
1743
- CASCADE_VERSION = "0.51.0";
1743
+ CASCADE_VERSION = "0.52.0";
1744
1744
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1745
1745
  CASCADE_DB_FILE = ".cascade/memory.db";
1746
1746
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -4997,7 +4997,14 @@ var ToolsConfigSchema = z.object({
4997
4997
  * built-in tool (shell/file/git/…) from existing at all, rather than just
4998
4998
  * gating it behind approval. Omitted = full default set (unchanged).
4999
4999
  */
5000
- enabledTools: z.array(z.string()).optional()
5000
+ enabledTools: z.array(z.string()).optional(),
5001
+ /**
5002
+ * Tools to remove even when they would otherwise be registered. Unlike
5003
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
5004
+ * tools registered outside it — MCP tools and media generation — which is
5005
+ * the only way to turn those off.
5006
+ */
5007
+ disabledTools: z.array(z.string()).optional()
5001
5008
  });
5002
5009
  var HookDefinitionSchema = z.object({
5003
5010
  name: z.string().optional(),
@@ -11022,17 +11029,13 @@ var CAPABILITIES = [
11022
11029
  },
11023
11030
  // ── Video ──
11024
11031
  {
11025
- // Listed so `describe()` can tell the truth about what exists, and so the
11026
- // planner is told video is out of reach rather than left to invent a tool.
11027
- // Veo is a long-running operation: submit, poll, then fetch bytes from a
11028
- // signed URL. That is a different execution shape from every other entry
11029
- // here and is not wired up — claiming otherwise would produce a tool that
11030
- // fails at call time instead of an honest "not yet".
11032
+ // Long-running: submit returns an operation name, the video lands minutes
11033
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
11034
+ // that polling loop; the registry only records the shape via `api`.
11031
11035
  modality: "video",
11032
11036
  provider: "gemini",
11033
11037
  modelId: "veo-3.1-generate-001",
11034
- api: "gemini-predict",
11035
- unsupported: "Veo runs as a long-running operation (submit \u2192 poll \u2192 fetch). Not wired up yet."
11038
+ api: "gemini-predict-lro"
11036
11039
  }
11037
11040
  ];
11038
11041
  function capabilityCost(cap) {
@@ -11345,6 +11348,75 @@ async function generateSpeech(cap, cfg, req, signal) {
11345
11348
  };
11346
11349
  }, cap.modelId);
11347
11350
  }
11351
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
11352
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
11353
+ function sleep(ms, signal) {
11354
+ return new Promise((resolve2, reject) => {
11355
+ if (signal?.aborted) return reject(new Error("Cancelled."));
11356
+ const timer = setTimeout(() => {
11357
+ signal?.removeEventListener("abort", onAbort);
11358
+ resolve2();
11359
+ }, ms);
11360
+ function onAbort() {
11361
+ clearTimeout(timer);
11362
+ reject(new Error("Cancelled."));
11363
+ }
11364
+ signal?.addEventListener("abort", onAbort, { once: true });
11365
+ });
11366
+ }
11367
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
11368
+ return callProvider(async () => {
11369
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
11370
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11371
+ const key = cfg.apiKey ?? "";
11372
+ const headers = { "x-goog-api-key": key };
11373
+ const submit = await postJson(
11374
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
11375
+ {
11376
+ instances: [{ prompt: req.prompt }],
11377
+ parameters: {
11378
+ aspectRatio: req.aspectRatio ?? "16:9",
11379
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
11380
+ }
11381
+ },
11382
+ headers,
11383
+ signal
11384
+ );
11385
+ const started = await submit.json();
11386
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
11387
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
11388
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
11389
+ let operation = {};
11390
+ while (Date.now() < deadline) {
11391
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
11392
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
11393
+ if (!poll.ok) {
11394
+ const text = await poll.text().catch(() => "");
11395
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
11396
+ }
11397
+ operation = await poll.json();
11398
+ if (operation.done) break;
11399
+ onProgress?.("Still rendering\u2026");
11400
+ }
11401
+ if (!operation.done) {
11402
+ throw new Error(
11403
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
11404
+ );
11405
+ }
11406
+ if (operation.error?.message) throw new Error(operation.error.message);
11407
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
11408
+ if (!uri) throw new Error("Video finished but the response contained no video.");
11409
+ const media = await fetch(uri, { headers, signal });
11410
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
11411
+ return {
11412
+ data: Buffer.from(await media.arrayBuffer()),
11413
+ mimeType: "video/mp4",
11414
+ filename: stamp("mp4"),
11415
+ modelId: cap.modelId,
11416
+ provider: cap.provider
11417
+ };
11418
+ }, cap.modelId);
11419
+ }
11348
11420
  async function transcribeAudio(cap, cfg, req, signal) {
11349
11421
  return callProvider(async () => {
11350
11422
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -11469,6 +11541,58 @@ var GenerateSpeechTool = class extends BaseTool {
11469
11541
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11470
11542
  }
11471
11543
  };
11544
+ var DEFAULT_VIDEO_SECONDS = 5;
11545
+ var MAX_VIDEO_SECONDS = 8;
11546
+ var GenerateVideoTool = class extends BaseTool {
11547
+ constructor(deps) {
11548
+ super();
11549
+ this.deps = deps;
11550
+ }
11551
+ deps;
11552
+ name = "generate_video";
11553
+ // The price is in the description on purpose. Video is ~1000x the cost of an
11554
+ // image per call, it is billed per SECOND, and the model choosing the
11555
+ // duration is the one deciding the bill — so the number has to be where the
11556
+ // model reads before calling, not only in the result afterwards.
11557
+ 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.";
11558
+ inputSchema = {
11559
+ type: "object",
11560
+ properties: {
11561
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
11562
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
11563
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
11564
+ model: { type: "string", description: "Optional: force a specific video model." }
11565
+ },
11566
+ required: ["prompt"]
11567
+ };
11568
+ async execute(input, options) {
11569
+ const prompt = String(input["prompt"] ?? "").trim();
11570
+ if (!prompt) return 'Provide a "prompt" describing the video.';
11571
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
11572
+ if (!r.ok) return r.error;
11573
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
11574
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
11575
+ const asset = await generateVideo(
11576
+ r.selected.capability,
11577
+ r.cfg,
11578
+ {
11579
+ prompt,
11580
+ seconds,
11581
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
11582
+ },
11583
+ options.signal,
11584
+ // Progress goes to the run log so a two-minute wait doesn't look like a
11585
+ // hang — the single most likely reason someone kills a working render.
11586
+ (note) => options.onProgress?.(note)
11587
+ );
11588
+ const location = await this.deps.sink(asset);
11589
+ const unit = r.selected.cost;
11590
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
11591
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
11592
+ return `Video generated and saved to ${location}.
11593
+ Model: ${asset.modelId}.${estimate}${clamped}`;
11594
+ }
11595
+ };
11472
11596
  var TranscribeAudioTool = class extends BaseTool {
11473
11597
  constructor(deps, readFile) {
11474
11598
  super();
@@ -11519,6 +11643,7 @@ function buildMediaTools(deps, readFile) {
11519
11643
  const modalities = new Set(deps.registry.availableModalities());
11520
11644
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11521
11645
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11646
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
11522
11647
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11523
11648
  return tools;
11524
11649
  }
@@ -15938,6 +16063,7 @@ var Cascade = class _Cascade extends EventEmitter {
15938
16063
  router;
15939
16064
  toolRegistry;
15940
16065
  multimodal;
16066
+ mediaSink;
15941
16067
  mcpClient;
15942
16068
  codeIndex;
15943
16069
  config;
@@ -16257,14 +16383,27 @@ ${input}` }],
16257
16383
  * server stores a file row) constructs the tools itself via buildMediaTools
16258
16384
  * with its own sink.
16259
16385
  */
16386
+ /**
16387
+ * Replace where generated media is written.
16388
+ *
16389
+ * The default sink writes into the workspace, which is what a desktop or CLI
16390
+ * run wants. A host with different storage — the cloud server keeps a file
16391
+ * row plus bytes under the tenant's directory — supplies its own here, and
16392
+ * the media tools are rebuilt around it. Re-registering by name replaces the
16393
+ * existing tools rather than duplicating them.
16394
+ */
16395
+ setMediaSink(sink) {
16396
+ this.mediaSink = sink;
16397
+ this.registerMediaTools(this.workspacePath);
16398
+ }
16260
16399
  registerMediaTools(workspacePath) {
16261
- const allowlist = this.config.tools?.enabledTools;
16262
- const permitted = (name) => !allowlist || allowlist.includes(name);
16400
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
16401
+ const permitted = (name) => !denied.has(name);
16263
16402
  const tools = buildMediaTools(
16264
16403
  {
16265
16404
  registry: this.multimodal,
16266
16405
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16267
- sink: async (asset) => {
16406
+ sink: this.mediaSink ?? (async (asset) => {
16268
16407
  const fsp = await import('fs/promises');
16269
16408
  const nodePath = await import('path');
16270
16409
  const dir = nodePath.join(workspacePath, "generated");
@@ -16272,7 +16411,7 @@ ${input}` }],
16272
16411
  const dest = nodePath.join(dir, asset.filename);
16273
16412
  await fsp.writeFile(dest, asset.data);
16274
16413
  return nodePath.relative(workspacePath, dest);
16275
- }
16414
+ })
16276
16415
  },
16277
16416
  async (p) => {
16278
16417
  const fsp = await import('fs/promises');
@@ -17276,7 +17415,7 @@ var CloudClient = class _CloudClient {
17276
17415
  const deadline = Date.now() + start.expiresIn * 1e3;
17277
17416
  while (Date.now() < deadline) {
17278
17417
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
17279
- await sleep(interval * 1e3, opts.signal);
17418
+ await sleep2(interval * 1e3, opts.signal);
17280
17419
  const r = await this.pollDevice(start.deviceCode);
17281
17420
  if (r === "slow_down") {
17282
17421
  interval += 5;
@@ -17487,7 +17626,7 @@ var CloudClient = class _CloudClient {
17487
17626
  this.store.clear();
17488
17627
  }
17489
17628
  };
17490
- function sleep(ms, signal) {
17629
+ function sleep2(ms, signal) {
17491
17630
  return new Promise((resolve2, reject) => {
17492
17631
  const t = setTimeout(resolve2, ms);
17493
17632
  signal?.addEventListener("abort", () => {