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/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.53.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";
@@ -2792,26 +2792,14 @@ var init_openai = __esm({
2792
2792
  // src/providers/azure.ts
2793
2793
  var azure_exports = {};
2794
2794
  __export(azure_exports, {
2795
+ AZURE_BASE_MODELS: () => AZURE_BASE_MODELS,
2795
2796
  AzureOpenAIProvider: () => AzureOpenAIProvider,
2796
2797
  azureModelForDeployment: () => azureModelForDeployment,
2797
2798
  inferAzureBaseModel: () => inferAzureBaseModel
2798
2799
  });
2799
2800
  function inferAzureBaseModel(deploymentName) {
2800
2801
  const n = deploymentName.toLowerCase();
2801
- const rules = [
2802
- [/gpt-?5\.5/, "gpt-5.5"],
2803
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2804
- [/gpt-?5\.4/, "gpt-5.4"],
2805
- [/gpt-?5.*nano/, "gpt-5-nano"],
2806
- [/gpt-?5.*mini/, "gpt-5-mini"],
2807
- [/gpt-?5/, "gpt-5"],
2808
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2809
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2810
- [/gpt-?4\.1/, "gpt-4.1"],
2811
- [/gpt-?4o-mini/, "gpt-4o-mini"],
2812
- [/gpt-?4o/, "gpt-4o"]
2813
- ];
2814
- for (const [re, base] of rules) if (re.test(n)) return base;
2802
+ for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
2815
2803
  return null;
2816
2804
  }
2817
2805
  function azureModelForDeployment(cfg) {
@@ -2856,13 +2844,27 @@ function azureModelForDeployment(cfg) {
2856
2844
  supportsToolUse: true
2857
2845
  };
2858
2846
  }
2859
- var DEFAULT_AZURE_API_VERSION, AzureOpenAIProvider;
2847
+ var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
2860
2848
  var init_azure = __esm({
2861
2849
  "src/providers/azure.ts"() {
2862
2850
  init_constants();
2863
2851
  init_openai();
2864
2852
  init_pricing();
2865
2853
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
2854
+ AZURE_BASE_MODEL_RULES = [
2855
+ [/gpt-?5\.5/, "gpt-5.5"],
2856
+ [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2857
+ [/gpt-?5\.4/, "gpt-5.4"],
2858
+ [/gpt-?5.*nano/, "gpt-5-nano"],
2859
+ [/gpt-?5.*mini/, "gpt-5-mini"],
2860
+ [/gpt-?5/, "gpt-5"],
2861
+ [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2862
+ [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2863
+ [/gpt-?4\.1/, "gpt-4.1"],
2864
+ [/gpt-?4o-mini/, "gpt-4o-mini"],
2865
+ [/gpt-?4o/, "gpt-4o"]
2866
+ ];
2867
+ AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
2866
2868
  AzureOpenAIProvider = class extends OpenAIProvider {
2867
2869
  constructor(config, model) {
2868
2870
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -5050,7 +5052,14 @@ var ToolsConfigSchema = zod.z.object({
5050
5052
  * built-in tool (shell/file/git/…) from existing at all, rather than just
5051
5053
  * gating it behind approval. Omitted = full default set (unchanged).
5052
5054
  */
5053
- enabledTools: zod.z.array(zod.z.string()).optional()
5055
+ enabledTools: zod.z.array(zod.z.string()).optional(),
5056
+ /**
5057
+ * Tools to remove even when they would otherwise be registered. Unlike
5058
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
5059
+ * tools registered outside it — MCP tools and media generation — which is
5060
+ * the only way to turn those off.
5061
+ */
5062
+ disabledTools: zod.z.array(zod.z.string()).optional()
5054
5063
  });
5055
5064
  var HookDefinitionSchema = zod.z.object({
5056
5065
  name: zod.z.string().optional(),
@@ -5556,6 +5565,26 @@ var ConfigManager = class {
5556
5565
  }
5557
5566
  };
5558
5567
 
5568
+ // src/tools/tool-name.ts
5569
+ var MCP_TOOL_PREFIX = "mcp__";
5570
+ function sanitizeToolNameSegment(segment) {
5571
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
5572
+ let start = 0;
5573
+ let end = cleaned.length;
5574
+ while (start < end && cleaned[start] === "_") start++;
5575
+ while (end > start && cleaned[end - 1] === "_") end--;
5576
+ return cleaned.slice(start, end) || "_";
5577
+ }
5578
+ function mcpToolName(serverName, toolName) {
5579
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
5580
+ }
5581
+ function isMcpToolName(name) {
5582
+ return name.startsWith(MCP_TOOL_PREFIX);
5583
+ }
5584
+ function mcpServerPrefix(serverName) {
5585
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
5586
+ }
5587
+
5559
5588
  // src/cli/utils/terminal-input.ts
5560
5589
  var PASTE_START = "\x1B[200~";
5561
5590
  var PASTE_END = "\x1B[201~";
@@ -11075,17 +11104,13 @@ var CAPABILITIES = [
11075
11104
  },
11076
11105
  // ── Video ──
11077
11106
  {
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".
11107
+ // Long-running: submit returns an operation name, the video lands minutes
11108
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
11109
+ // that polling loop; the registry only records the shape via `api`.
11084
11110
  modality: "video",
11085
11111
  provider: "gemini",
11086
11112
  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."
11113
+ api: "gemini-predict-lro"
11089
11114
  }
11090
11115
  ];
11091
11116
  function capabilityCost(cap) {
@@ -11398,6 +11423,75 @@ async function generateSpeech(cap, cfg, req, signal) {
11398
11423
  };
11399
11424
  }, cap.modelId);
11400
11425
  }
11426
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
11427
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
11428
+ function sleep(ms, signal) {
11429
+ return new Promise((resolve2, reject) => {
11430
+ if (signal?.aborted) return reject(new Error("Cancelled."));
11431
+ const timer = setTimeout(() => {
11432
+ signal?.removeEventListener("abort", onAbort);
11433
+ resolve2();
11434
+ }, ms);
11435
+ function onAbort() {
11436
+ clearTimeout(timer);
11437
+ reject(new Error("Cancelled."));
11438
+ }
11439
+ signal?.addEventListener("abort", onAbort, { once: true });
11440
+ });
11441
+ }
11442
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
11443
+ return callProvider(async () => {
11444
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
11445
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11446
+ const key = cfg.apiKey ?? "";
11447
+ const headers = { "x-goog-api-key": key };
11448
+ const submit = await postJson(
11449
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
11450
+ {
11451
+ instances: [{ prompt: req.prompt }],
11452
+ parameters: {
11453
+ aspectRatio: req.aspectRatio ?? "16:9",
11454
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
11455
+ }
11456
+ },
11457
+ headers,
11458
+ signal
11459
+ );
11460
+ const started = await submit.json();
11461
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
11462
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
11463
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
11464
+ let operation = {};
11465
+ while (Date.now() < deadline) {
11466
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
11467
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
11468
+ if (!poll.ok) {
11469
+ const text = await poll.text().catch(() => "");
11470
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
11471
+ }
11472
+ operation = await poll.json();
11473
+ if (operation.done) break;
11474
+ onProgress?.("Still rendering\u2026");
11475
+ }
11476
+ if (!operation.done) {
11477
+ throw new Error(
11478
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
11479
+ );
11480
+ }
11481
+ if (operation.error?.message) throw new Error(operation.error.message);
11482
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
11483
+ if (!uri) throw new Error("Video finished but the response contained no video.");
11484
+ const media = await fetch(uri, { headers, signal });
11485
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
11486
+ return {
11487
+ data: Buffer.from(await media.arrayBuffer()),
11488
+ mimeType: "video/mp4",
11489
+ filename: stamp("mp4"),
11490
+ modelId: cap.modelId,
11491
+ provider: cap.provider
11492
+ };
11493
+ }, cap.modelId);
11494
+ }
11401
11495
  async function transcribeAudio(cap, cfg, req, signal) {
11402
11496
  return callProvider(async () => {
11403
11497
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -11522,6 +11616,58 @@ var GenerateSpeechTool = class extends BaseTool {
11522
11616
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11523
11617
  }
11524
11618
  };
11619
+ var DEFAULT_VIDEO_SECONDS = 5;
11620
+ var MAX_VIDEO_SECONDS = 8;
11621
+ var GenerateVideoTool = class extends BaseTool {
11622
+ constructor(deps) {
11623
+ super();
11624
+ this.deps = deps;
11625
+ }
11626
+ deps;
11627
+ name = "generate_video";
11628
+ // The price is in the description on purpose. Video is ~1000x the cost of an
11629
+ // image per call, it is billed per SECOND, and the model choosing the
11630
+ // duration is the one deciding the bill — so the number has to be where the
11631
+ // model reads before calling, not only in the result afterwards.
11632
+ 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.";
11633
+ inputSchema = {
11634
+ type: "object",
11635
+ properties: {
11636
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
11637
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
11638
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
11639
+ model: { type: "string", description: "Optional: force a specific video model." }
11640
+ },
11641
+ required: ["prompt"]
11642
+ };
11643
+ async execute(input, options) {
11644
+ const prompt = String(input["prompt"] ?? "").trim();
11645
+ if (!prompt) return 'Provide a "prompt" describing the video.';
11646
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
11647
+ if (!r.ok) return r.error;
11648
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
11649
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
11650
+ const asset = await generateVideo(
11651
+ r.selected.capability,
11652
+ r.cfg,
11653
+ {
11654
+ prompt,
11655
+ seconds,
11656
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
11657
+ },
11658
+ options.signal,
11659
+ // Progress goes to the run log so a two-minute wait doesn't look like a
11660
+ // hang — the single most likely reason someone kills a working render.
11661
+ (note) => options.onProgress?.(note)
11662
+ );
11663
+ const location = await this.deps.sink(asset);
11664
+ const unit = r.selected.cost;
11665
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
11666
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
11667
+ return `Video generated and saved to ${location}.
11668
+ Model: ${asset.modelId}.${estimate}${clamped}`;
11669
+ }
11670
+ };
11525
11671
  var TranscribeAudioTool = class extends BaseTool {
11526
11672
  constructor(deps, readFile) {
11527
11673
  super();
@@ -11572,6 +11718,7 @@ function buildMediaTools(deps, readFile) {
11572
11718
  const modalities = new Set(deps.registry.availableModalities());
11573
11719
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11574
11720
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11721
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
11575
11722
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11576
11723
  return tools;
11577
11724
  }
@@ -13072,7 +13219,7 @@ var McpToolWrapper = class extends BaseTool {
13072
13219
  this.mcpClient = mcpClient;
13073
13220
  this.serverName = serverName;
13074
13221
  this.toolName = toolName;
13075
- this.name = `mcp::${serverName}::${toolName}`;
13222
+ this.name = mcpToolName(serverName, toolName);
13076
13223
  this.description = `[MCP:${serverName}] ${description}`;
13077
13224
  this.inputSchema = inputSchema;
13078
13225
  }
@@ -15991,6 +16138,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15991
16138
  router;
15992
16139
  toolRegistry;
15993
16140
  multimodal;
16141
+ mediaSink;
15994
16142
  mcpClient;
15995
16143
  codeIndex;
15996
16144
  config;
@@ -16310,14 +16458,27 @@ ${input}` }],
16310
16458
  * server stores a file row) constructs the tools itself via buildMediaTools
16311
16459
  * with its own sink.
16312
16460
  */
16461
+ /**
16462
+ * Replace where generated media is written.
16463
+ *
16464
+ * The default sink writes into the workspace, which is what a desktop or CLI
16465
+ * run wants. A host with different storage — the cloud server keeps a file
16466
+ * row plus bytes under the tenant's directory — supplies its own here, and
16467
+ * the media tools are rebuilt around it. Re-registering by name replaces the
16468
+ * existing tools rather than duplicating them.
16469
+ */
16470
+ setMediaSink(sink) {
16471
+ this.mediaSink = sink;
16472
+ this.registerMediaTools(this.workspacePath);
16473
+ }
16313
16474
  registerMediaTools(workspacePath) {
16314
- const allowlist = this.config.tools?.enabledTools;
16315
- const permitted = (name) => !allowlist || allowlist.includes(name);
16475
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
16476
+ const permitted = (name) => !denied.has(name);
16316
16477
  const tools = buildMediaTools(
16317
16478
  {
16318
16479
  registry: this.multimodal,
16319
16480
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16320
- sink: async (asset) => {
16481
+ sink: this.mediaSink ?? (async (asset) => {
16321
16482
  const fsp = await import('fs/promises');
16322
16483
  const nodePath = await import('path');
16323
16484
  const dir = nodePath.join(workspacePath, "generated");
@@ -16325,7 +16486,7 @@ ${input}` }],
16325
16486
  const dest = nodePath.join(dir, asset.filename);
16326
16487
  await fsp.writeFile(dest, asset.data);
16327
16488
  return nodePath.relative(workspacePath, dest);
16328
- }
16489
+ })
16329
16490
  },
16330
16491
  async (p) => {
16331
16492
  const fsp = await import('fs/promises');
@@ -17329,7 +17490,7 @@ var CloudClient = class _CloudClient {
17329
17490
  const deadline = Date.now() + start.expiresIn * 1e3;
17330
17491
  while (Date.now() < deadline) {
17331
17492
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
17332
- await sleep(interval * 1e3, opts.signal);
17493
+ await sleep2(interval * 1e3, opts.signal);
17333
17494
  const r = await this.pollDevice(start.deviceCode);
17334
17495
  if (r === "slow_down") {
17335
17496
  interval += 5;
@@ -17540,7 +17701,7 @@ var CloudClient = class _CloudClient {
17540
17701
  this.store.clear();
17541
17702
  }
17542
17703
  };
17543
- function sleep(ms, signal) {
17704
+ function sleep2(ms, signal) {
17544
17705
  return new Promise((resolve2, reject) => {
17545
17706
  const t = setTimeout(resolve2, ms);
17546
17707
  signal?.addEventListener("abort", () => {
@@ -19419,13 +19580,13 @@ ${msg.content}`).join("\n\n");
19419
19580
  const cascade = cascadeRef.current;
19420
19581
  if (!cascade) return "Cascade not initialized.";
19421
19582
  const toolRegistry = cascade.getToolRegistry();
19422
- const tools = toolRegistry.getToolDefinitions().filter((t) => t.name.startsWith("mcp::"));
19583
+ const tools = toolRegistry.getToolDefinitions().filter((t) => isMcpToolName(t.name));
19423
19584
  if (tools.length === 0) return "No MCP tools connected.";
19424
19585
  const servers = /* @__PURE__ */ new Set();
19425
19586
  tools.forEach((t) => servers.add(t.name.split("::")[1]));
19426
19587
  const lines = [`Connected MCP Servers (${servers.size}):`];
19427
19588
  for (const server of servers) {
19428
- const serverTools = tools.filter((t) => t.name.startsWith(`mcp::${server}::`));
19589
+ const serverTools = tools.filter((t) => t.name.startsWith(mcpServerPrefix(server)));
19429
19590
  lines.push(`
19430
19591
  \u25CF ${server} (${serverTools.length} tools)`);
19431
19592
  serverTools.forEach((t) => lines.push(` - ${t.name.split("::")[2]} \u2014 ${t.description.replace(`[MCP:${server}] `, "")}`));