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.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.53.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";
@@ -2739,26 +2739,14 @@ var init_openai = __esm({
2739
2739
  // src/providers/azure.ts
2740
2740
  var azure_exports = {};
2741
2741
  __export(azure_exports, {
2742
+ AZURE_BASE_MODELS: () => AZURE_BASE_MODELS,
2742
2743
  AzureOpenAIProvider: () => AzureOpenAIProvider,
2743
2744
  azureModelForDeployment: () => azureModelForDeployment,
2744
2745
  inferAzureBaseModel: () => inferAzureBaseModel
2745
2746
  });
2746
2747
  function inferAzureBaseModel(deploymentName) {
2747
2748
  const n = deploymentName.toLowerCase();
2748
- const rules = [
2749
- [/gpt-?5\.5/, "gpt-5.5"],
2750
- [/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
2751
- [/gpt-?5\.4/, "gpt-5.4"],
2752
- [/gpt-?5.*nano/, "gpt-5-nano"],
2753
- [/gpt-?5.*mini/, "gpt-5-mini"],
2754
- [/gpt-?5/, "gpt-5"],
2755
- [/gpt-?4\.1-nano/, "gpt-4.1-nano"],
2756
- [/gpt-?4\.1-mini/, "gpt-4.1-mini"],
2757
- [/gpt-?4\.1/, "gpt-4.1"],
2758
- [/gpt-?4o-mini/, "gpt-4o-mini"],
2759
- [/gpt-?4o/, "gpt-4o"]
2760
- ];
2761
- for (const [re, base] of rules) if (re.test(n)) return base;
2749
+ for (const [re, base] of AZURE_BASE_MODEL_RULES) if (re.test(n)) return base;
2762
2750
  return null;
2763
2751
  }
2764
2752
  function azureModelForDeployment(cfg) {
@@ -2803,13 +2791,27 @@ function azureModelForDeployment(cfg) {
2803
2791
  supportsToolUse: true
2804
2792
  };
2805
2793
  }
2806
- var DEFAULT_AZURE_API_VERSION, AzureOpenAIProvider;
2794
+ var DEFAULT_AZURE_API_VERSION, AZURE_BASE_MODEL_RULES, AZURE_BASE_MODELS, AzureOpenAIProvider;
2807
2795
  var init_azure = __esm({
2808
2796
  "src/providers/azure.ts"() {
2809
2797
  init_constants();
2810
2798
  init_openai();
2811
2799
  init_pricing();
2812
2800
  DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
2801
+ AZURE_BASE_MODEL_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
+ AZURE_BASE_MODELS = AZURE_BASE_MODEL_RULES.map(([, id]) => id);
2813
2815
  AzureOpenAIProvider = class extends OpenAIProvider {
2814
2816
  constructor(config, model) {
2815
2817
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -4997,7 +4999,14 @@ var ToolsConfigSchema = z.object({
4997
4999
  * built-in tool (shell/file/git/…) from existing at all, rather than just
4998
5000
  * gating it behind approval. Omitted = full default set (unchanged).
4999
5001
  */
5000
- enabledTools: z.array(z.string()).optional()
5002
+ enabledTools: z.array(z.string()).optional(),
5003
+ /**
5004
+ * Tools to remove even when they would otherwise be registered. Unlike
5005
+ * `enabledTools` (an allowlist over the built-in set), this also reaches
5006
+ * tools registered outside it — MCP tools and media generation — which is
5007
+ * the only way to turn those off.
5008
+ */
5009
+ disabledTools: z.array(z.string()).optional()
5001
5010
  });
5002
5011
  var HookDefinitionSchema = z.object({
5003
5012
  name: z.string().optional(),
@@ -5503,6 +5512,26 @@ var ConfigManager = class {
5503
5512
  }
5504
5513
  };
5505
5514
 
5515
+ // src/tools/tool-name.ts
5516
+ var MCP_TOOL_PREFIX = "mcp__";
5517
+ function sanitizeToolNameSegment(segment) {
5518
+ const cleaned = segment.replace(/[^a-zA-Z0-9-]+/g, "_");
5519
+ let start = 0;
5520
+ let end = cleaned.length;
5521
+ while (start < end && cleaned[start] === "_") start++;
5522
+ while (end > start && cleaned[end - 1] === "_") end--;
5523
+ return cleaned.slice(start, end) || "_";
5524
+ }
5525
+ function mcpToolName(serverName, toolName) {
5526
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__${sanitizeToolNameSegment(toolName)}`;
5527
+ }
5528
+ function isMcpToolName(name) {
5529
+ return name.startsWith(MCP_TOOL_PREFIX);
5530
+ }
5531
+ function mcpServerPrefix(serverName) {
5532
+ return `${MCP_TOOL_PREFIX}${sanitizeToolNameSegment(serverName)}__`;
5533
+ }
5534
+
5506
5535
  // src/cli/utils/terminal-input.ts
5507
5536
  var PASTE_START = "\x1B[200~";
5508
5537
  var PASTE_END = "\x1B[201~";
@@ -11022,17 +11051,13 @@ var CAPABILITIES = [
11022
11051
  },
11023
11052
  // ── Video ──
11024
11053
  {
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".
11054
+ // Long-running: submit returns an operation name, the video lands minutes
11055
+ // later, and the bytes come from a short-lived signed URL. generate.ts owns
11056
+ // that polling loop; the registry only records the shape via `api`.
11031
11057
  modality: "video",
11032
11058
  provider: "gemini",
11033
11059
  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."
11060
+ api: "gemini-predict-lro"
11036
11061
  }
11037
11062
  ];
11038
11063
  function capabilityCost(cap) {
@@ -11345,6 +11370,75 @@ async function generateSpeech(cap, cfg, req, signal) {
11345
11370
  };
11346
11371
  }, cap.modelId);
11347
11372
  }
11373
+ var VIDEO_POLL_INTERVAL_MS = 1e4;
11374
+ var VIDEO_TIMEOUT_MS = 8 * 6e4;
11375
+ function sleep(ms, signal) {
11376
+ return new Promise((resolve2, reject) => {
11377
+ if (signal?.aborted) return reject(new Error("Cancelled."));
11378
+ const timer = setTimeout(() => {
11379
+ signal?.removeEventListener("abort", onAbort);
11380
+ resolve2();
11381
+ }, ms);
11382
+ function onAbort() {
11383
+ clearTimeout(timer);
11384
+ reject(new Error("Cancelled."));
11385
+ }
11386
+ signal?.addEventListener("abort", onAbort, { once: true });
11387
+ });
11388
+ }
11389
+ async function generateVideo(cap, cfg, req, signal, onProgress) {
11390
+ return callProvider(async () => {
11391
+ if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
11392
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11393
+ const key = cfg.apiKey ?? "";
11394
+ const headers = { "x-goog-api-key": key };
11395
+ const submit = await postJson(
11396
+ `${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
11397
+ {
11398
+ instances: [{ prompt: req.prompt }],
11399
+ parameters: {
11400
+ aspectRatio: req.aspectRatio ?? "16:9",
11401
+ ...req.seconds ? { durationSeconds: req.seconds } : {}
11402
+ }
11403
+ },
11404
+ headers,
11405
+ signal
11406
+ );
11407
+ const started = await submit.json();
11408
+ if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
11409
+ onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
11410
+ const deadline = Date.now() + VIDEO_TIMEOUT_MS;
11411
+ let operation = {};
11412
+ while (Date.now() < deadline) {
11413
+ await sleep(VIDEO_POLL_INTERVAL_MS, signal);
11414
+ const poll = await fetch(`${base}/${started.name}`, { headers, signal });
11415
+ if (!poll.ok) {
11416
+ const text = await poll.text().catch(() => "");
11417
+ throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
11418
+ }
11419
+ operation = await poll.json();
11420
+ if (operation.done) break;
11421
+ onProgress?.("Still rendering\u2026");
11422
+ }
11423
+ if (!operation.done) {
11424
+ throw new Error(
11425
+ `Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
11426
+ );
11427
+ }
11428
+ if (operation.error?.message) throw new Error(operation.error.message);
11429
+ const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
11430
+ if (!uri) throw new Error("Video finished but the response contained no video.");
11431
+ const media = await fetch(uri, { headers, signal });
11432
+ if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
11433
+ return {
11434
+ data: Buffer.from(await media.arrayBuffer()),
11435
+ mimeType: "video/mp4",
11436
+ filename: stamp("mp4"),
11437
+ modelId: cap.modelId,
11438
+ provider: cap.provider
11439
+ };
11440
+ }, cap.modelId);
11441
+ }
11348
11442
  async function transcribeAudio(cap, cfg, req, signal) {
11349
11443
  return callProvider(async () => {
11350
11444
  if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
@@ -11469,6 +11563,58 @@ var GenerateSpeechTool = class extends BaseTool {
11469
11563
  Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11470
11564
  }
11471
11565
  };
11566
+ var DEFAULT_VIDEO_SECONDS = 5;
11567
+ var MAX_VIDEO_SECONDS = 8;
11568
+ var GenerateVideoTool = class extends BaseTool {
11569
+ constructor(deps) {
11570
+ super();
11571
+ this.deps = deps;
11572
+ }
11573
+ deps;
11574
+ name = "generate_video";
11575
+ // The price is in the description on purpose. Video is ~1000x the cost of an
11576
+ // image per call, it is billed per SECOND, and the model choosing the
11577
+ // duration is the one deciding the bill — so the number has to be where the
11578
+ // model reads before calling, not only in the result afterwards.
11579
+ 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.";
11580
+ inputSchema = {
11581
+ type: "object",
11582
+ properties: {
11583
+ prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
11584
+ seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
11585
+ aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
11586
+ model: { type: "string", description: "Optional: force a specific video model." }
11587
+ },
11588
+ required: ["prompt"]
11589
+ };
11590
+ async execute(input, options) {
11591
+ const prompt = String(input["prompt"] ?? "").trim();
11592
+ if (!prompt) return 'Provide a "prompt" describing the video.';
11593
+ const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
11594
+ if (!r.ok) return r.error;
11595
+ const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
11596
+ const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
11597
+ const asset = await generateVideo(
11598
+ r.selected.capability,
11599
+ r.cfg,
11600
+ {
11601
+ prompt,
11602
+ seconds,
11603
+ ...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
11604
+ },
11605
+ options.signal,
11606
+ // Progress goes to the run log so a two-minute wait doesn't look like a
11607
+ // hang — the single most likely reason someone kills a working render.
11608
+ (note) => options.onProgress?.(note)
11609
+ );
11610
+ const location = await this.deps.sink(asset);
11611
+ const unit = r.selected.cost;
11612
+ const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
11613
+ const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
11614
+ return `Video generated and saved to ${location}.
11615
+ Model: ${asset.modelId}.${estimate}${clamped}`;
11616
+ }
11617
+ };
11472
11618
  var TranscribeAudioTool = class extends BaseTool {
11473
11619
  constructor(deps, readFile) {
11474
11620
  super();
@@ -11519,6 +11665,7 @@ function buildMediaTools(deps, readFile) {
11519
11665
  const modalities = new Set(deps.registry.availableModalities());
11520
11666
  if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11521
11667
  if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11668
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
11522
11669
  if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11523
11670
  return tools;
11524
11671
  }
@@ -13019,7 +13166,7 @@ var McpToolWrapper = class extends BaseTool {
13019
13166
  this.mcpClient = mcpClient;
13020
13167
  this.serverName = serverName;
13021
13168
  this.toolName = toolName;
13022
- this.name = `mcp::${serverName}::${toolName}`;
13169
+ this.name = mcpToolName(serverName, toolName);
13023
13170
  this.description = `[MCP:${serverName}] ${description}`;
13024
13171
  this.inputSchema = inputSchema;
13025
13172
  }
@@ -15938,6 +16085,7 @@ var Cascade = class _Cascade extends EventEmitter {
15938
16085
  router;
15939
16086
  toolRegistry;
15940
16087
  multimodal;
16088
+ mediaSink;
15941
16089
  mcpClient;
15942
16090
  codeIndex;
15943
16091
  config;
@@ -16257,14 +16405,27 @@ ${input}` }],
16257
16405
  * server stores a file row) constructs the tools itself via buildMediaTools
16258
16406
  * with its own sink.
16259
16407
  */
16408
+ /**
16409
+ * Replace where generated media is written.
16410
+ *
16411
+ * The default sink writes into the workspace, which is what a desktop or CLI
16412
+ * run wants. A host with different storage — the cloud server keeps a file
16413
+ * row plus bytes under the tenant's directory — supplies its own here, and
16414
+ * the media tools are rebuilt around it. Re-registering by name replaces the
16415
+ * existing tools rather than duplicating them.
16416
+ */
16417
+ setMediaSink(sink) {
16418
+ this.mediaSink = sink;
16419
+ this.registerMediaTools(this.workspacePath);
16420
+ }
16260
16421
  registerMediaTools(workspacePath) {
16261
- const allowlist = this.config.tools?.enabledTools;
16262
- const permitted = (name) => !allowlist || allowlist.includes(name);
16422
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
16423
+ const permitted = (name) => !denied.has(name);
16263
16424
  const tools = buildMediaTools(
16264
16425
  {
16265
16426
  registry: this.multimodal,
16266
16427
  lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16267
- sink: async (asset) => {
16428
+ sink: this.mediaSink ?? (async (asset) => {
16268
16429
  const fsp = await import('fs/promises');
16269
16430
  const nodePath = await import('path');
16270
16431
  const dir = nodePath.join(workspacePath, "generated");
@@ -16272,7 +16433,7 @@ ${input}` }],
16272
16433
  const dest = nodePath.join(dir, asset.filename);
16273
16434
  await fsp.writeFile(dest, asset.data);
16274
16435
  return nodePath.relative(workspacePath, dest);
16275
- }
16436
+ })
16276
16437
  },
16277
16438
  async (p) => {
16278
16439
  const fsp = await import('fs/promises');
@@ -17276,7 +17437,7 @@ var CloudClient = class _CloudClient {
17276
17437
  const deadline = Date.now() + start.expiresIn * 1e3;
17277
17438
  while (Date.now() < deadline) {
17278
17439
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
17279
- await sleep(interval * 1e3, opts.signal);
17440
+ await sleep2(interval * 1e3, opts.signal);
17280
17441
  const r = await this.pollDevice(start.deviceCode);
17281
17442
  if (r === "slow_down") {
17282
17443
  interval += 5;
@@ -17487,7 +17648,7 @@ var CloudClient = class _CloudClient {
17487
17648
  this.store.clear();
17488
17649
  }
17489
17650
  };
17490
- function sleep(ms, signal) {
17651
+ function sleep2(ms, signal) {
17491
17652
  return new Promise((resolve2, reject) => {
17492
17653
  const t = setTimeout(resolve2, ms);
17493
17654
  signal?.addEventListener("abort", () => {
@@ -19366,13 +19527,13 @@ ${msg.content}`).join("\n\n");
19366
19527
  const cascade = cascadeRef.current;
19367
19528
  if (!cascade) return "Cascade not initialized.";
19368
19529
  const toolRegistry = cascade.getToolRegistry();
19369
- const tools = toolRegistry.getToolDefinitions().filter((t) => t.name.startsWith("mcp::"));
19530
+ const tools = toolRegistry.getToolDefinitions().filter((t) => isMcpToolName(t.name));
19370
19531
  if (tools.length === 0) return "No MCP tools connected.";
19371
19532
  const servers = /* @__PURE__ */ new Set();
19372
19533
  tools.forEach((t) => servers.add(t.name.split("::")[1]));
19373
19534
  const lines = [`Connected MCP Servers (${servers.size}):`];
19374
19535
  for (const server of servers) {
19375
- const serverTools = tools.filter((t) => t.name.startsWith(`mcp::${server}::`));
19536
+ const serverTools = tools.filter((t) => t.name.startsWith(mcpServerPrefix(server)));
19376
19537
  lines.push(`
19377
19538
  \u25CF ${server} (${serverTools.length} tools)`);
19378
19539
  serverTools.forEach((t) => lines.push(` - ${t.name.split("::")[2]} \u2014 ${t.description.replace(`[MCP:${server}] `, "")}`));