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 +195 -34
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +195 -34
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +194 -39
- package/dist/index.cjs +186 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -10
- package/dist/index.d.ts +44 -10
- package/dist/index.js +186 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
//
|
|
8898
|
-
//
|
|
8899
|
-
//
|
|
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 =
|
|
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
|
|
14582
|
-
const permitted = (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
|
|
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
|
|
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;
|