cascade-ai 0.51.0 → 0.52.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 +155 -16
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +155 -16
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +155 -16
- package/dist/index.cjs +155 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +155 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
1743
|
+
CASCADE_VERSION = "0.52.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";
|
|
@@ -4997,7 +4997,14 @@ var ToolsConfigSchema = z.object({
|
|
|
4997
4997
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
4998
4998
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
4999
4999
|
*/
|
|
5000
|
-
enabledTools: z.array(z.string()).optional()
|
|
5000
|
+
enabledTools: z.array(z.string()).optional(),
|
|
5001
|
+
/**
|
|
5002
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
5003
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
5004
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
5005
|
+
* the only way to turn those off.
|
|
5006
|
+
*/
|
|
5007
|
+
disabledTools: z.array(z.string()).optional()
|
|
5001
5008
|
});
|
|
5002
5009
|
var HookDefinitionSchema = z.object({
|
|
5003
5010
|
name: z.string().optional(),
|
|
@@ -11022,17 +11029,13 @@ var CAPABILITIES = [
|
|
|
11022
11029
|
},
|
|
11023
11030
|
// ── Video ──
|
|
11024
11031
|
{
|
|
11025
|
-
//
|
|
11026
|
-
//
|
|
11027
|
-
//
|
|
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".
|
|
11032
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
11033
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
11034
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
11031
11035
|
modality: "video",
|
|
11032
11036
|
provider: "gemini",
|
|
11033
11037
|
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."
|
|
11038
|
+
api: "gemini-predict-lro"
|
|
11036
11039
|
}
|
|
11037
11040
|
];
|
|
11038
11041
|
function capabilityCost(cap) {
|
|
@@ -11345,6 +11348,75 @@ async function generateSpeech(cap, cfg, req, signal) {
|
|
|
11345
11348
|
};
|
|
11346
11349
|
}, cap.modelId);
|
|
11347
11350
|
}
|
|
11351
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
11352
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
11353
|
+
function sleep(ms, signal) {
|
|
11354
|
+
return new Promise((resolve2, reject) => {
|
|
11355
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
11356
|
+
const timer = setTimeout(() => {
|
|
11357
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11358
|
+
resolve2();
|
|
11359
|
+
}, ms);
|
|
11360
|
+
function onAbort() {
|
|
11361
|
+
clearTimeout(timer);
|
|
11362
|
+
reject(new Error("Cancelled."));
|
|
11363
|
+
}
|
|
11364
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11365
|
+
});
|
|
11366
|
+
}
|
|
11367
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
11368
|
+
return callProvider(async () => {
|
|
11369
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
11370
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11371
|
+
const key = cfg.apiKey ?? "";
|
|
11372
|
+
const headers = { "x-goog-api-key": key };
|
|
11373
|
+
const submit = await postJson(
|
|
11374
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
11375
|
+
{
|
|
11376
|
+
instances: [{ prompt: req.prompt }],
|
|
11377
|
+
parameters: {
|
|
11378
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
11379
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
11380
|
+
}
|
|
11381
|
+
},
|
|
11382
|
+
headers,
|
|
11383
|
+
signal
|
|
11384
|
+
);
|
|
11385
|
+
const started = await submit.json();
|
|
11386
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
11387
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
11388
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
11389
|
+
let operation = {};
|
|
11390
|
+
while (Date.now() < deadline) {
|
|
11391
|
+
await sleep(VIDEO_POLL_INTERVAL_MS, signal);
|
|
11392
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
11393
|
+
if (!poll.ok) {
|
|
11394
|
+
const text = await poll.text().catch(() => "");
|
|
11395
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
11396
|
+
}
|
|
11397
|
+
operation = await poll.json();
|
|
11398
|
+
if (operation.done) break;
|
|
11399
|
+
onProgress?.("Still rendering\u2026");
|
|
11400
|
+
}
|
|
11401
|
+
if (!operation.done) {
|
|
11402
|
+
throw new Error(
|
|
11403
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
11404
|
+
);
|
|
11405
|
+
}
|
|
11406
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
11407
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
11408
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
11409
|
+
const media = await fetch(uri, { headers, signal });
|
|
11410
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
11411
|
+
return {
|
|
11412
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
11413
|
+
mimeType: "video/mp4",
|
|
11414
|
+
filename: stamp("mp4"),
|
|
11415
|
+
modelId: cap.modelId,
|
|
11416
|
+
provider: cap.provider
|
|
11417
|
+
};
|
|
11418
|
+
}, cap.modelId);
|
|
11419
|
+
}
|
|
11348
11420
|
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11349
11421
|
return callProvider(async () => {
|
|
11350
11422
|
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
@@ -11469,6 +11541,58 @@ var GenerateSpeechTool = class extends BaseTool {
|
|
|
11469
11541
|
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11470
11542
|
}
|
|
11471
11543
|
};
|
|
11544
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
11545
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
11546
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
11547
|
+
constructor(deps) {
|
|
11548
|
+
super();
|
|
11549
|
+
this.deps = deps;
|
|
11550
|
+
}
|
|
11551
|
+
deps;
|
|
11552
|
+
name = "generate_video";
|
|
11553
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
11554
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
11555
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
11556
|
+
// model reads before calling, not only in the result afterwards.
|
|
11557
|
+
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.";
|
|
11558
|
+
inputSchema = {
|
|
11559
|
+
type: "object",
|
|
11560
|
+
properties: {
|
|
11561
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
11562
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
11563
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
11564
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
11565
|
+
},
|
|
11566
|
+
required: ["prompt"]
|
|
11567
|
+
};
|
|
11568
|
+
async execute(input, options) {
|
|
11569
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11570
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
11571
|
+
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
11572
|
+
if (!r.ok) return r.error;
|
|
11573
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
11574
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
11575
|
+
const asset = await generateVideo(
|
|
11576
|
+
r.selected.capability,
|
|
11577
|
+
r.cfg,
|
|
11578
|
+
{
|
|
11579
|
+
prompt,
|
|
11580
|
+
seconds,
|
|
11581
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
11582
|
+
},
|
|
11583
|
+
options.signal,
|
|
11584
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
11585
|
+
// hang — the single most likely reason someone kills a working render.
|
|
11586
|
+
(note) => options.onProgress?.(note)
|
|
11587
|
+
);
|
|
11588
|
+
const location = await this.deps.sink(asset);
|
|
11589
|
+
const unit = r.selected.cost;
|
|
11590
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
11591
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
11592
|
+
return `Video generated and saved to ${location}.
|
|
11593
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
11594
|
+
}
|
|
11595
|
+
};
|
|
11472
11596
|
var TranscribeAudioTool = class extends BaseTool {
|
|
11473
11597
|
constructor(deps, readFile) {
|
|
11474
11598
|
super();
|
|
@@ -11519,6 +11643,7 @@ function buildMediaTools(deps, readFile) {
|
|
|
11519
11643
|
const modalities = new Set(deps.registry.availableModalities());
|
|
11520
11644
|
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11521
11645
|
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11646
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
11522
11647
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11523
11648
|
return tools;
|
|
11524
11649
|
}
|
|
@@ -15938,6 +16063,7 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
15938
16063
|
router;
|
|
15939
16064
|
toolRegistry;
|
|
15940
16065
|
multimodal;
|
|
16066
|
+
mediaSink;
|
|
15941
16067
|
mcpClient;
|
|
15942
16068
|
codeIndex;
|
|
15943
16069
|
config;
|
|
@@ -16257,14 +16383,27 @@ ${input}` }],
|
|
|
16257
16383
|
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16258
16384
|
* with its own sink.
|
|
16259
16385
|
*/
|
|
16386
|
+
/**
|
|
16387
|
+
* Replace where generated media is written.
|
|
16388
|
+
*
|
|
16389
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
16390
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
16391
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
16392
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
16393
|
+
* existing tools rather than duplicating them.
|
|
16394
|
+
*/
|
|
16395
|
+
setMediaSink(sink) {
|
|
16396
|
+
this.mediaSink = sink;
|
|
16397
|
+
this.registerMediaTools(this.workspacePath);
|
|
16398
|
+
}
|
|
16260
16399
|
registerMediaTools(workspacePath) {
|
|
16261
|
-
const
|
|
16262
|
-
const permitted = (name) => !
|
|
16400
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
16401
|
+
const permitted = (name) => !denied.has(name);
|
|
16263
16402
|
const tools = buildMediaTools(
|
|
16264
16403
|
{
|
|
16265
16404
|
registry: this.multimodal,
|
|
16266
16405
|
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16267
|
-
sink: async (asset) => {
|
|
16406
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
16268
16407
|
const fsp = await import('fs/promises');
|
|
16269
16408
|
const nodePath = await import('path');
|
|
16270
16409
|
const dir = nodePath.join(workspacePath, "generated");
|
|
@@ -16272,7 +16411,7 @@ ${input}` }],
|
|
|
16272
16411
|
const dest = nodePath.join(dir, asset.filename);
|
|
16273
16412
|
await fsp.writeFile(dest, asset.data);
|
|
16274
16413
|
return nodePath.relative(workspacePath, dest);
|
|
16275
|
-
}
|
|
16414
|
+
})
|
|
16276
16415
|
},
|
|
16277
16416
|
async (p) => {
|
|
16278
16417
|
const fsp = await import('fs/promises');
|
|
@@ -17276,7 +17415,7 @@ var CloudClient = class _CloudClient {
|
|
|
17276
17415
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
17277
17416
|
while (Date.now() < deadline) {
|
|
17278
17417
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
17279
|
-
await
|
|
17418
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
17280
17419
|
const r = await this.pollDevice(start.deviceCode);
|
|
17281
17420
|
if (r === "slow_down") {
|
|
17282
17421
|
interval += 5;
|
|
@@ -17487,7 +17626,7 @@ var CloudClient = class _CloudClient {
|
|
|
17487
17626
|
this.store.clear();
|
|
17488
17627
|
}
|
|
17489
17628
|
};
|
|
17490
|
-
function
|
|
17629
|
+
function sleep2(ms, signal) {
|
|
17491
17630
|
return new Promise((resolve2, reject) => {
|
|
17492
17631
|
const t = setTimeout(resolve2, ms);
|
|
17493
17632
|
signal?.addEventListener("abort", () => {
|