cascade-ai 0.51.0 → 0.53.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/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.51.0";
1894
+ var CASCADE_VERSION = "0.53.0";
1895
1895
  var CASCADE_CONFIG_DIR = ".cascade";
1896
1896
  var CASCADE_MD_FILE = "CASCADE.md";
1897
1897
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -2856,22 +2856,23 @@ var OpenAIProvider = class extends BaseProvider {
2856
2856
 
2857
2857
  // src/providers/azure.ts
2858
2858
  var DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
2859
+ var AZURE_BASE_MODEL_RULES = [
2860
+ [/gpt-?5\.5/, "gpt-5.5"],
2861
+ [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2862
+ [/gpt-?5\.4/, "gpt-5.4"],
2863
+ [/gpt-?5.*nano/, "gpt-5-nano"],
2864
+ [/gpt-?5.*mini/, "gpt-5-mini"],
2865
+ [/gpt-?5/, "gpt-5"],
2866
+ [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2867
+ [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2868
+ [/gpt-?4\.1/, "gpt-4.1"],
2869
+ [/gpt-?4o-mini/, "gpt-4o-mini"],
2870
+ [/gpt-?4o/, "gpt-4o"]
2871
+ ];
2872
+ var AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
2859
2873
  function inferAzureBaseModel(deploymentName) {
2860
2874
  const n = deploymentName.toLowerCase();
2861
- const rules = [
2862
- [/gpt-?5\.5/, "gpt-5.5"],
2863
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2864
- [/gpt-?5\.4/, "gpt-5.4"],
2865
- [/gpt-?5.*nano/, "gpt-5-nano"],
2866
- [/gpt-?5.*mini/, "gpt-5-mini"],
2867
- [/gpt-?5/, "gpt-5"],
2868
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2869
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2870
- [/gpt-?4\.1/, "gpt-4.1"],
2871
- [/gpt-?4o-mini/, "gpt-4o-mini"],
2872
- [/gpt-?4o/, "gpt-4o"]
2873
- ];
2874
- for (const [re, base] of rules) if (re.test(n)) return base;
2875
+ for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
2875
2876
  return null;
2876
2877
  }
2877
2878
  function azureModelForDeployment(cfg) {
@@ -8894,17 +8895,13 @@ var CAPABILITIES = [
8894
8895
  },
8895
8896
  // ── Video ──
8896
8897
  {
8897
- // Listed so `describe()` can tell the truth about what exists, and so the
8898
- // planner is told video is out of reach rather than left to invent a tool.
8899
- // Veo is a long-running operation: submit, poll, then fetch bytes from a
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".
8898
+ // Long-running: submit returns an operation name, the video lands minutes
8899
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
8900
+ // that polling loop; the registry only records the shape via `api`.
8903
8901
  modality: "video",
8904
8902
  provider: "gemini",
8905
8903
  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."
8904
+ api: "gemini-predict-lro"
8908
8905
  }
8909
8906
  ];
8910
8907
  function capabilityCost(cap) {
@@ -9217,6 +9214,75 @@ async function generateSpeech(cap, cfg, req, signal) {
9217
9214
  };
9218
9215
  }, cap.modelId);
9219
9216
  }
9217
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
9218
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
9219
+ function sleep(ms, signal) {
9220
+ return new Promise((resolve2, reject) => {
9221
+ if (signal?.aborted) return reject(new Error("Cancelled."));
9222
+ const timer = setTimeout(() => {
9223
+ signal?.removeEventListener("abort", onAbort);
9224
+ resolve2();
9225
+ }, ms);
9226
+ function onAbort() {
9227
+ clearTimeout(timer);
9228
+ reject(new Error("Cancelled."));
9229
+ }
9230
+ signal?.addEventListener("abort", onAbort, { once: true });
9231
+ });
9232
+ }
9233
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
9234
+ return callProvider(async () => {
9235
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
9236
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
9237
+ const key = cfg.apiKey ?? "";
9238
+ const headers = { "x-goog-api-key": key };
9239
+ const submit = await postJson(
9240
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
9241
+ {
9242
+ instances: [{ prompt: req.prompt }],
9243
+ parameters: {
9244
+ aspectRatio: req.aspectRatio ?? "16:9",
9245
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
9246
+ }
9247
+ },
9248
+ headers,
9249
+ signal
9250
+ );
9251
+ const started = await submit.json();
9252
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
9253
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
9254
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
9255
+ let operation = {};
9256
+ while (Date.now() < deadline) {
9257
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
9258
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
9259
+ if (!poll.ok) {
9260
+ const text = await poll.text().catch(() => "");
9261
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
9262
+ }
9263
+ operation = await poll.json();
9264
+ if (operation.done) break;
9265
+ onProgress?.("Still rendering\u2026");
9266
+ }
9267
+ if (!operation.done) {
9268
+ throw new Error(
9269
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
9270
+ );
9271
+ }
9272
+ if (operation.error?.message) throw new Error(operation.error.message);
9273
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
9274
+ if (!uri) throw new Error("Video finished but the response contained no video.");
9275
+ const media = await fetch(uri, { headers, signal });
9276
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
9277
+ return {
9278
+ data: Buffer.from(await media.arrayBuffer()),
9279
+ mimeType: "video/mp4",
9280
+ filename: stamp("mp4"),
9281
+ modelId: cap.modelId,
9282
+ provider: cap.provider
9283
+ };
9284
+ }, cap.modelId);
9285
+ }
9220
9286
  async function transcribeAudio(cap, cfg, req, signal) {
9221
9287
  return callProvider(async () => {
9222
9288
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -9341,6 +9407,58 @@ var GenerateSpeechTool = class extends BaseTool {
9341
9407
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
9342
9408
  }
9343
9409
  };
9410
+ var DEFAULT_VIDEO_SECONDS = 5;
9411
+ var MAX_VIDEO_SECONDS = 8;
9412
+ var GenerateVideoTool = class extends BaseTool {
9413
+ constructor(deps) {
9414
+ super();
9415
+ this.deps = deps;
9416
+ }
9417
+ deps;
9418
+ name = "generate_video";
9419
+ // The price is in the description on purpose. Video is ~1000x the cost of an
9420
+ // image per call, it is billed per SECOND, and the model choosing the
9421
+ // duration is the one deciding the bill — so the number has to be where the
9422
+ // model reads before calling, not only in the result afterwards.
9423
+ 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.";
9424
+ inputSchema = {
9425
+ type: "object",
9426
+ properties: {
9427
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
9428
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
9429
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
9430
+ model: { type: "string", description: "Optional: force a specific video model." }
9431
+ },
9432
+ required: ["prompt"]
9433
+ };
9434
+ async execute(input, options) {
9435
+ const prompt = String(input["prompt"] ?? "").trim();
9436
+ if (!prompt) return 'Provide a "prompt" describing the video.';
9437
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
9438
+ if (!r.ok) return r.error;
9439
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
9440
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
9441
+ const asset = await generateVideo(
9442
+ r.selected.capability,
9443
+ r.cfg,
9444
+ {
9445
+ prompt,
9446
+ seconds,
9447
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
9448
+ },
9449
+ options.signal,
9450
+ // Progress goes to the run log so a two-minute wait doesn't look like a
9451
+ // hang — the single most likely reason someone kills a working render.
9452
+ (note) => options.onProgress?.(note)
9453
+ );
9454
+ const location = await this.deps.sink(asset);
9455
+ const unit = r.selected.cost;
9456
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
9457
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
9458
+ return `Video generated and saved to ${location}.
9459
+ Model: ${asset.modelId}.${estimate}${clamped}`;
9460
+ }
9461
+ };
9344
9462
  var TranscribeAudioTool = class extends BaseTool {
9345
9463
  constructor(deps, readFile) {
9346
9464
  super();
@@ -9391,6 +9509,7 @@ function buildMediaTools(deps, readFile) {
9391
9509
  const modalities = new Set(deps.registry.availableModalities());
9392
9510
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
9393
9511
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
9512
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
9394
9513
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
9395
9514
  return tools;
9396
9515
  }
@@ -10875,6 +10994,20 @@ ${text}`;
10875
10994
  }
10876
10995
  };
10877
10996
 
10997
+ // src/tools/tool-name.ts
10998
+ var MCP_TOOL_PREFIX = "mcp__";
10999
+ function sanitizeToolNameSegment(segment) {
11000
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
11001
+ let start = 0;
11002
+ let end = cleaned.length;
11003
+ while (start < end && cleaned[start] === "_") start++;
11004
+ while (end > start && cleaned[end - 1] === "_") end--;
11005
+ return cleaned.slice(start, end) || "_";
11006
+ }
11007
+ function mcpToolName(serverName, toolName) {
11008
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
11009
+ }
11010
+
10878
11011
  // src/tools/mcp.ts
10879
11012
  var McpToolWrapper = class extends BaseTool {
10880
11013
  name;
@@ -10888,7 +11021,7 @@ var McpToolWrapper = class extends BaseTool {
10888
11021
  this.mcpClient = mcpClient;
10889
11022
  this.serverName = serverName;
10890
11023
  this.toolName = toolName;
10891
- this.name = `mcp::${serverName}::${toolName}`;
11024
+ this.name = mcpToolName(serverName, toolName);
10892
11025
  this.description = `[MCP:${serverName}] ${description}`;
10893
11026
  this.inputSchema = inputSchema;
10894
11027
  }
@@ -11719,7 +11852,14 @@ var ToolsConfigSchema = zod.z.object({
11719
11852
  * built-in tool (shell/file/git/…) from existing at all, rather than just
11720
11853
  * gating it behind approval. Omitted = full default set (unchanged).
11721
11854
  */
11722
- enabledTools: zod.z.array(zod.z.string()).optional()
11855
+ enabledTools: zod.z.array(zod.z.string()).optional(),
11856
+ /**
11857
+ * Tools to remove even when they would otherwise be registered. Unlike
11858
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
11859
+ * tools registered outside it — MCP tools and media generation — which is
11860
+ * the only way to turn those off.
11861
+ */
11862
+ disabledTools: zod.z.array(zod.z.string()).optional()
11723
11863
  });
11724
11864
  var HookDefinitionSchema = zod.z.object({
11725
11865
  name: zod.z.string().optional(),
@@ -14258,6 +14398,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
14258
14398
  router;
14259
14399
  toolRegistry;
14260
14400
  multimodal;
14401
+ mediaSink;
14261
14402
  mcpClient;
14262
14403
  codeIndex;
14263
14404
  config;
@@ -14577,14 +14718,27 @@ ${input}` }],
14577
14718
  * server stores a file row) constructs the tools itself via buildMediaTools
14578
14719
  * with its own sink.
14579
14720
  */
14721
+ /**
14722
+ * Replace where generated media is written.
14723
+ *
14724
+ * The default sink writes into the workspace, which is what a desktop or CLI
14725
+ * run wants. A host with different storage — the cloud server keeps a file
14726
+ * row plus bytes under the tenant's directory — supplies its own here, and
14727
+ * the media tools are rebuilt around it. Re-registering by name replaces the
14728
+ * existing tools rather than duplicating them.
14729
+ */
14730
+ setMediaSink(sink) {
14731
+ this.mediaSink = sink;
14732
+ this.registerMediaTools(this.workspacePath);
14733
+ }
14580
14734
  registerMediaTools(workspacePath) {
14581
- const allowlist = this.config.tools?.enabledTools;
14582
- const permitted = (name) => !allowlist || allowlist.includes(name);
14735
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
14736
+ const permitted = (name) => !denied.has(name);
14583
14737
  const tools = buildMediaTools(
14584
14738
  {
14585
14739
  registry: this.multimodal,
14586
14740
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
14587
- sink: async (asset) => {
14741
+ sink: this.mediaSink ?? (async (asset) => {
14588
14742
  const fsp = await import('fs/promises');
14589
14743
  const nodePath = await import('path');
14590
14744
  const dir = nodePath.join(workspacePath, "generated");
@@ -14592,7 +14746,7 @@ ${input}` }],
14592
14746
  const dest = nodePath.join(dir, asset.filename);
14593
14747
  await fsp.writeFile(dest, asset.data);
14594
14748
  return nodePath.relative(workspacePath, dest);
14595
- }
14749
+ })
14596
14750
  },
14597
14751
  async (p) => {
14598
14752
  const fsp = await import('fs/promises');
@@ -18606,7 +18760,7 @@ var CloudClient = class _CloudClient {
18606
18760
  const deadline = Date.now() + start.expiresIn * 1e3;
18607
18761
  while (Date.now() < deadline) {
18608
18762
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
18609
- await sleep(interval * 1e3, opts.signal);
18763
+ await sleep2(interval * 1e3, opts.signal);
18610
18764
  const r = await this.pollDevice(start.deviceCode);
18611
18765
  if (r === "slow_down") {
18612
18766
  interval += 5;
@@ -18817,7 +18971,7 @@ var CloudClient = class _CloudClient {
18817
18971
  this.store.clear();
18818
18972
  }
18819
18973
  };
18820
- function sleep(ms, signal) {
18974
+ function sleep2(ms, signal) {
18821
18975
  return new Promise((resolve2, reject) => {
18822
18976
  const t = setTimeout(resolve2, ms);
18823
18977
  signal?.addEventListener("abort", () => {
@@ -19000,6 +19154,7 @@ function applySyncBundle(bundle, config) {
19000
19154
  return next;
19001
19155
  }
19002
19156
 
19157
+ exports.AZURE_BASE_MODELS = AZURE_BASE_MODELS;
19003
19158
  exports.AZURE_BASE_URL_TEMPLATE = AZURE_BASE_URL_TEMPLATE;
19004
19159
  exports.AuditLogger = AuditLogger;
19005
19160
  exports.CASCADE_AUDIT_FILE = CASCADE_AUDIT_FILE;