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.
@@ -224247,7 +224247,7 @@ function reconcilePrice(model, live) {
224247
224247
  }
224248
224248
 
224249
224249
  // src/constants.ts
224250
- var CASCADE_VERSION = "0.51.0";
224250
+ var CASCADE_VERSION = "0.53.0";
224251
224251
  var CASCADE_CONFIG_DIR = ".cascade";
224252
224252
  var CASCADE_MD_FILE = "CASCADE.md";
224253
224253
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -233085,22 +233085,23 @@ var OpenAIProvider = class extends BaseProvider {
233085
233085
 
233086
233086
  // src/providers/azure.ts
233087
233087
  var DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
233088
+ var AZURE_BASE_MODEL_RULES = [
233089
+ [/gpt-?5\.5/, "gpt-5.5"],
233090
+ [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
233091
+ [/gpt-?5\.4/, "gpt-5.4"],
233092
+ [/gpt-?5.*nano/, "gpt-5-nano"],
233093
+ [/gpt-?5.*mini/, "gpt-5-mini"],
233094
+ [/gpt-?5/, "gpt-5"],
233095
+ [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
233096
+ [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
233097
+ [/gpt-?4\.1/, "gpt-4.1"],
233098
+ [/gpt-?4o-mini/, "gpt-4o-mini"],
233099
+ [/gpt-?4o/, "gpt-4o"]
233100
+ ];
233101
+ var AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
233088
233102
  function inferAzureBaseModel(deploymentName) {
233089
233103
  const n8 = deploymentName.toLowerCase();
233090
- const rules = [
233091
- [/gpt-?5\.5/, "gpt-5.5"],
233092
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
233093
- [/gpt-?5\.4/, "gpt-5.4"],
233094
- [/gpt-?5.*nano/, "gpt-5-nano"],
233095
- [/gpt-?5.*mini/, "gpt-5-mini"],
233096
- [/gpt-?5/, "gpt-5"],
233097
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
233098
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
233099
- [/gpt-?4\.1/, "gpt-4.1"],
233100
- [/gpt-?4o-mini/, "gpt-4o-mini"],
233101
- [/gpt-?4o/, "gpt-4o"]
233102
- ];
233103
- for (const [re3, base] of rules) if (re3.test(n8)) return base;
233104
+ for (const [re3, base] of AZURE_BASE_MODEL_RULES) if (re3.test(n8)) return base;
233104
233105
  return null;
233105
233106
  }
233106
233107
  function azureModelForDeployment(cfg) {
@@ -235059,11 +235060,11 @@ function mcpToolsToGeminiTool(mcpTools, config2 = {}) {
235059
235060
  const functionDeclarations = [];
235060
235061
  const toolNames = /* @__PURE__ */ new Set();
235061
235062
  for (const mcpTool of mcpTools) {
235062
- const mcpToolName = mcpTool.name;
235063
- if (toolNames.has(mcpToolName)) {
235064
- throw new Error(`Duplicate function name ${mcpToolName} found in MCP tools. Please ensure function names are unique.`);
235063
+ const mcpToolName2 = mcpTool.name;
235064
+ if (toolNames.has(mcpToolName2)) {
235065
+ throw new Error(`Duplicate function name ${mcpToolName2} found in MCP tools. Please ensure function names are unique.`);
235065
235066
  }
235066
- toolNames.add(mcpToolName);
235067
+ toolNames.add(mcpToolName2);
235067
235068
  const geminiTool = mcpToGeminiTool(mcpTool, config2);
235068
235069
  if (geminiTool.functionDeclarations) {
235069
235070
  functionDeclarations.push(...geminiTool.functionDeclarations);
@@ -244710,11 +244711,11 @@ var McpCallableTool = class _McpCallableTool {
244710
244711
  _d = false;
244711
244712
  const mcpTool = _c;
244712
244713
  mcpTools.push(mcpTool);
244713
- const mcpToolName = mcpTool.name;
244714
- if (functionMap[mcpToolName]) {
244715
- throw new Error(`Duplicate function name ${mcpToolName} found in MCP tools. Please ensure function names are unique.`);
244714
+ const mcpToolName2 = mcpTool.name;
244715
+ if (functionMap[mcpToolName2]) {
244716
+ throw new Error(`Duplicate function name ${mcpToolName2} found in MCP tools. Please ensure function names are unique.`);
244716
244717
  }
244717
- functionMap[mcpToolName] = mcpClient;
244718
+ functionMap[mcpToolName2] = mcpClient;
244718
244719
  }
244719
244720
  } catch (e_1_1) {
244720
244721
  e_1 = { error: e_1_1 };
@@ -257153,17 +257154,13 @@ var CAPABILITIES = [
257153
257154
  },
257154
257155
  // ── Video ──
257155
257156
  {
257156
- // Listed so `describe()` can tell the truth about what exists, and so the
257157
- // planner is told video is out of reach rather than left to invent a tool.
257158
- // Veo is a long-running operation: submit, poll, then fetch bytes from a
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".
257157
+ // Long-running: submit returns an operation name, the video lands minutes
257158
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
257159
+ // that polling loop; the registry only records the shape via `api`.
257162
257160
  modality: "video",
257163
257161
  provider: "gemini",
257164
257162
  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."
257163
+ api: "gemini-predict-lro"
257167
257164
  }
257168
257165
  ];
257169
257166
  function capabilityCost(cap) {
@@ -257476,6 +257473,75 @@ async function generateSpeech(cap, cfg, req, signal) {
257476
257473
  };
257477
257474
  }, cap.modelId);
257478
257475
  }
257476
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
257477
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
257478
+ function sleep4(ms2, signal) {
257479
+ return new Promise((resolve2, reject) => {
257480
+ if (signal?.aborted) return reject(new Error("Cancelled."));
257481
+ const timer = setTimeout(() => {
257482
+ signal?.removeEventListener("abort", onAbort);
257483
+ resolve2();
257484
+ }, ms2);
257485
+ function onAbort() {
257486
+ clearTimeout(timer);
257487
+ reject(new Error("Cancelled."));
257488
+ }
257489
+ signal?.addEventListener("abort", onAbort, { once: true });
257490
+ });
257491
+ }
257492
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
257493
+ return callProvider(async () => {
257494
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
257495
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
257496
+ const key = cfg.apiKey ?? "";
257497
+ const headers = { "x-goog-api-key": key };
257498
+ const submit = await postJson(
257499
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
257500
+ {
257501
+ instances: [{ prompt: req.prompt }],
257502
+ parameters: {
257503
+ aspectRatio: req.aspectRatio ?? "16:9",
257504
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
257505
+ }
257506
+ },
257507
+ headers,
257508
+ signal
257509
+ );
257510
+ const started = await submit.json();
257511
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
257512
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
257513
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
257514
+ let operation = {};
257515
+ while (Date.now() < deadline) {
257516
+ await sleep4(VIDEO_POLL_INTERVAL_MS, signal);
257517
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
257518
+ if (!poll.ok) {
257519
+ const text = await poll.text().catch(() => "");
257520
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
257521
+ }
257522
+ operation = await poll.json();
257523
+ if (operation.done) break;
257524
+ onProgress?.("Still rendering\u2026");
257525
+ }
257526
+ if (!operation.done) {
257527
+ throw new Error(
257528
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
257529
+ );
257530
+ }
257531
+ if (operation.error?.message) throw new Error(operation.error.message);
257532
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
257533
+ if (!uri) throw new Error("Video finished but the response contained no video.");
257534
+ const media = await fetch(uri, { headers, signal });
257535
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
257536
+ return {
257537
+ data: Buffer.from(await media.arrayBuffer()),
257538
+ mimeType: "video/mp4",
257539
+ filename: stamp("mp4"),
257540
+ modelId: cap.modelId,
257541
+ provider: cap.provider
257542
+ };
257543
+ }, cap.modelId);
257544
+ }
257479
257545
  async function transcribeAudio(cap, cfg, req, signal) {
257480
257546
  return callProvider(async () => {
257481
257547
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -257600,6 +257666,58 @@ var GenerateSpeechTool = class extends BaseTool {
257600
257666
  Model: ${asset.modelId} \u2014 ${r4.selected.reason}`;
257601
257667
  }
257602
257668
  };
257669
+ var DEFAULT_VIDEO_SECONDS = 5;
257670
+ var MAX_VIDEO_SECONDS = 8;
257671
+ var GenerateVideoTool = class extends BaseTool {
257672
+ constructor(deps) {
257673
+ super();
257674
+ this.deps = deps;
257675
+ }
257676
+ deps;
257677
+ name = "generate_video";
257678
+ // The price is in the description on purpose. Video is ~1000x the cost of an
257679
+ // image per call, it is billed per SECOND, and the model choosing the
257680
+ // duration is the one deciding the bill — so the number has to be where the
257681
+ // model reads before calling, not only in the result afterwards.
257682
+ 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.";
257683
+ inputSchema = {
257684
+ type: "object",
257685
+ properties: {
257686
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
257687
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
257688
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
257689
+ model: { type: "string", description: "Optional: force a specific video model." }
257690
+ },
257691
+ required: ["prompt"]
257692
+ };
257693
+ async execute(input, options) {
257694
+ const prompt = String(input["prompt"] ?? "").trim();
257695
+ if (!prompt) return 'Provide a "prompt" describing the video.';
257696
+ const r4 = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
257697
+ if (!r4.ok) return r4.error;
257698
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
257699
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
257700
+ const asset = await generateVideo(
257701
+ r4.selected.capability,
257702
+ r4.cfg,
257703
+ {
257704
+ prompt,
257705
+ seconds,
257706
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
257707
+ },
257708
+ options.signal,
257709
+ // Progress goes to the run log so a two-minute wait doesn't look like a
257710
+ // hang — the single most likely reason someone kills a working render.
257711
+ (note) => options.onProgress?.(note)
257712
+ );
257713
+ const location = await this.deps.sink(asset);
257714
+ const unit = r4.selected.cost;
257715
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
257716
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
257717
+ return `Video generated and saved to ${location}.
257718
+ Model: ${asset.modelId}.${estimate}${clamped}`;
257719
+ }
257720
+ };
257603
257721
  var TranscribeAudioTool = class extends BaseTool {
257604
257722
  constructor(deps, readFile) {
257605
257723
  super();
@@ -257650,6 +257768,7 @@ function buildMediaTools(deps, readFile) {
257650
257768
  const modalities = new Set(deps.registry.availableModalities());
257651
257769
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
257652
257770
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
257771
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
257653
257772
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
257654
257773
  return tools;
257655
257774
  }
@@ -286023,6 +286142,20 @@ ${text}`;
286023
286142
  }
286024
286143
  };
286025
286144
 
286145
+ // src/tools/tool-name.ts
286146
+ var MCP_TOOL_PREFIX = "mcp__";
286147
+ function sanitizeToolNameSegment(segment) {
286148
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
286149
+ let start = 0;
286150
+ let end = cleaned.length;
286151
+ while (start < end && cleaned[start] === "_") start++;
286152
+ while (end > start && cleaned[end - 1] === "_") end--;
286153
+ return cleaned.slice(start, end) || "_";
286154
+ }
286155
+ function mcpToolName(serverName, toolName) {
286156
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
286157
+ }
286158
+
286026
286159
  // src/tools/mcp.ts
286027
286160
  var McpToolWrapper = class extends BaseTool {
286028
286161
  name;
@@ -286036,7 +286169,7 @@ var McpToolWrapper = class extends BaseTool {
286036
286169
  this.mcpClient = mcpClient;
286037
286170
  this.serverName = serverName;
286038
286171
  this.toolName = toolName;
286039
- this.name = `mcp::${serverName}::${toolName}`;
286172
+ this.name = mcpToolName(serverName, toolName);
286040
286173
  this.description = `[MCP:${serverName}] ${description}`;
286041
286174
  this.inputSchema = inputSchema;
286042
286175
  }
@@ -300272,7 +300405,14 @@ var ToolsConfigSchema = external_exports.object({
300272
300405
  * built-in tool (shell/file/git/…) from existing at all, rather than just
300273
300406
  * gating it behind approval. Omitted = full default set (unchanged).
300274
300407
  */
300275
- enabledTools: external_exports.array(external_exports.string()).optional()
300408
+ enabledTools: external_exports.array(external_exports.string()).optional(),
300409
+ /**
300410
+ * Tools to remove even when they would otherwise be registered. Unlike
300411
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
300412
+ * tools registered outside it — MCP tools and media generation — which is
300413
+ * the only way to turn those off.
300414
+ */
300415
+ disabledTools: external_exports.array(external_exports.string()).optional()
300276
300416
  });
300277
300417
  var HookDefinitionSchema = external_exports.object({
300278
300418
  name: external_exports.string().optional(),
@@ -302821,6 +302961,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
302821
302961
  router;
302822
302962
  toolRegistry;
302823
302963
  multimodal;
302964
+ mediaSink;
302824
302965
  mcpClient;
302825
302966
  codeIndex;
302826
302967
  config;
@@ -303140,14 +303281,27 @@ ${input}` }],
303140
303281
  * server stores a file row) constructs the tools itself via buildMediaTools
303141
303282
  * with its own sink.
303142
303283
  */
303284
+ /**
303285
+ * Replace where generated media is written.
303286
+ *
303287
+ * The default sink writes into the workspace, which is what a desktop or CLI
303288
+ * run wants. A host with different storage — the cloud server keeps a file
303289
+ * row plus bytes under the tenant's directory — supplies its own here, and
303290
+ * the media tools are rebuilt around it. Re-registering by name replaces the
303291
+ * existing tools rather than duplicating them.
303292
+ */
303293
+ setMediaSink(sink) {
303294
+ this.mediaSink = sink;
303295
+ this.registerMediaTools(this.workspacePath);
303296
+ }
303143
303297
  registerMediaTools(workspacePath) {
303144
- const allowlist = this.config.tools?.enabledTools;
303145
- const permitted = (name) => !allowlist || allowlist.includes(name);
303298
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
303299
+ const permitted = (name) => !denied.has(name);
303146
303300
  const tools = buildMediaTools(
303147
303301
  {
303148
303302
  registry: this.multimodal,
303149
303303
  lookupProvider: (provider) => (this.config.providers ?? []).find((p3) => p3.type === provider),
303150
- sink: async (asset) => {
303304
+ sink: this.mediaSink ?? (async (asset) => {
303151
303305
  const fsp = await import('fs/promises');
303152
303306
  const nodePath = await import('path');
303153
303307
  const dir = nodePath.join(workspacePath, "generated");
@@ -303155,7 +303309,7 @@ ${input}` }],
303155
303309
  const dest = nodePath.join(dir, asset.filename);
303156
303310
  await fsp.writeFile(dest, asset.data);
303157
303311
  return nodePath.relative(workspacePath, dest);
303158
- }
303312
+ })
303159
303313
  },
303160
303314
  async (p3) => {
303161
303315
  const fsp = await import('fs/promises');
@@ -307953,7 +308107,7 @@ var CloudClient = class _CloudClient {
307953
308107
  const deadline = Date.now() + start.expiresIn * 1e3;
307954
308108
  while (Date.now() < deadline) {
307955
308109
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
307956
- await sleep4(interval * 1e3, opts.signal);
308110
+ await sleep5(interval * 1e3, opts.signal);
307957
308111
  const r4 = await this.pollDevice(start.deviceCode);
307958
308112
  if (r4 === "slow_down") {
307959
308113
  interval += 5;
@@ -308164,7 +308318,7 @@ var CloudClient = class _CloudClient {
308164
308318
  this.store.clear();
308165
308319
  }
308166
308320
  };
308167
- function sleep4(ms2, signal) {
308321
+ function sleep5(ms2, signal) {
308168
308322
  return new Promise((resolve2, reject) => {
308169
308323
  const t4 = setTimeout(resolve2, ms2);
308170
308324
  signal?.addEventListener("abort", () => {
@@ -308747,6 +308901,7 @@ object-assign/index.js:
308747
308901
  (*! noble-ciphers - MIT License (c) 2023 Paul Miller (paulmillr.com) *)
308748
308902
  */
308749
308903
 
308904
+ exports.AZURE_BASE_MODELS = AZURE_BASE_MODELS;
308750
308905
  exports.AZURE_BASE_URL_TEMPLATE = AZURE_BASE_URL_TEMPLATE;
308751
308906
  exports.AuditLogger = AuditLogger;
308752
308907
  exports.CASCADE_AUDIT_FILE = CASCADE_AUDIT_FILE;