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.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.
|
|
1796
|
+
CASCADE_VERSION = "0.52.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";
|
|
@@ -5050,7 +5050,14 @@ var ToolsConfigSchema = zod.z.object({
|
|
|
5050
5050
|
* built-in tool (shell/file/git/…) from existing at all, rather than just
|
|
5051
5051
|
* gating it behind approval. Omitted = full default set (unchanged).
|
|
5052
5052
|
*/
|
|
5053
|
-
enabledTools: zod.z.array(zod.z.string()).optional()
|
|
5053
|
+
enabledTools: zod.z.array(zod.z.string()).optional(),
|
|
5054
|
+
/**
|
|
5055
|
+
* Tools to remove even when they would otherwise be registered. Unlike
|
|
5056
|
+
* `enabledTools` (an allowlist over the built-in set), this also reaches
|
|
5057
|
+
* tools registered outside it — MCP tools and media generation — which is
|
|
5058
|
+
* the only way to turn those off.
|
|
5059
|
+
*/
|
|
5060
|
+
disabledTools: zod.z.array(zod.z.string()).optional()
|
|
5054
5061
|
});
|
|
5055
5062
|
var HookDefinitionSchema = zod.z.object({
|
|
5056
5063
|
name: zod.z.string().optional(),
|
|
@@ -11075,17 +11082,13 @@ var CAPABILITIES = [
|
|
|
11075
11082
|
},
|
|
11076
11083
|
// ── Video ──
|
|
11077
11084
|
{
|
|
11078
|
-
//
|
|
11079
|
-
//
|
|
11080
|
-
//
|
|
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".
|
|
11085
|
+
// Long-running: submit returns an operation name, the video lands minutes
|
|
11086
|
+
// later, and the bytes come from a short-lived signed URL. generate.ts owns
|
|
11087
|
+
// that polling loop; the registry only records the shape via `api`.
|
|
11084
11088
|
modality: "video",
|
|
11085
11089
|
provider: "gemini",
|
|
11086
11090
|
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."
|
|
11091
|
+
api: "gemini-predict-lro"
|
|
11089
11092
|
}
|
|
11090
11093
|
];
|
|
11091
11094
|
function capabilityCost(cap) {
|
|
@@ -11398,6 +11401,75 @@ async function generateSpeech(cap, cfg, req, signal) {
|
|
|
11398
11401
|
};
|
|
11399
11402
|
}, cap.modelId);
|
|
11400
11403
|
}
|
|
11404
|
+
var VIDEO_POLL_INTERVAL_MS = 1e4;
|
|
11405
|
+
var VIDEO_TIMEOUT_MS = 8 * 6e4;
|
|
11406
|
+
function sleep(ms, signal) {
|
|
11407
|
+
return new Promise((resolve2, reject) => {
|
|
11408
|
+
if (signal?.aborted) return reject(new Error("Cancelled."));
|
|
11409
|
+
const timer = setTimeout(() => {
|
|
11410
|
+
signal?.removeEventListener("abort", onAbort);
|
|
11411
|
+
resolve2();
|
|
11412
|
+
}, ms);
|
|
11413
|
+
function onAbort() {
|
|
11414
|
+
clearTimeout(timer);
|
|
11415
|
+
reject(new Error("Cancelled."));
|
|
11416
|
+
}
|
|
11417
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
11418
|
+
});
|
|
11419
|
+
}
|
|
11420
|
+
async function generateVideo(cap, cfg, req, signal, onProgress) {
|
|
11421
|
+
return callProvider(async () => {
|
|
11422
|
+
if (cap.api !== "gemini-predict-lro") throw new Error(`${cap.modelId} is not a video generator.`);
|
|
11423
|
+
const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
|
|
11424
|
+
const key = cfg.apiKey ?? "";
|
|
11425
|
+
const headers = { "x-goog-api-key": key };
|
|
11426
|
+
const submit = await postJson(
|
|
11427
|
+
`${base}/models/${encodeURIComponent(cap.modelId)}:predictLongRunning`,
|
|
11428
|
+
{
|
|
11429
|
+
instances: [{ prompt: req.prompt }],
|
|
11430
|
+
parameters: {
|
|
11431
|
+
aspectRatio: req.aspectRatio ?? "16:9",
|
|
11432
|
+
...req.seconds ? { durationSeconds: req.seconds } : {}
|
|
11433
|
+
}
|
|
11434
|
+
},
|
|
11435
|
+
headers,
|
|
11436
|
+
signal
|
|
11437
|
+
);
|
|
11438
|
+
const started = await submit.json();
|
|
11439
|
+
if (!started.name) throw new Error("Video request was accepted but returned no operation to poll.");
|
|
11440
|
+
onProgress?.(`Video generation started (${cap.modelId}). This usually takes 1-3 minutes.`);
|
|
11441
|
+
const deadline = Date.now() + VIDEO_TIMEOUT_MS;
|
|
11442
|
+
let operation = {};
|
|
11443
|
+
while (Date.now() < deadline) {
|
|
11444
|
+
await sleep(VIDEO_POLL_INTERVAL_MS, signal);
|
|
11445
|
+
const poll = await fetch(`${base}/${started.name}`, { headers, signal });
|
|
11446
|
+
if (!poll.ok) {
|
|
11447
|
+
const text = await poll.text().catch(() => "");
|
|
11448
|
+
throw Object.assign(new Error(text || poll.statusText), { status: poll.status });
|
|
11449
|
+
}
|
|
11450
|
+
operation = await poll.json();
|
|
11451
|
+
if (operation.done) break;
|
|
11452
|
+
onProgress?.("Still rendering\u2026");
|
|
11453
|
+
}
|
|
11454
|
+
if (!operation.done) {
|
|
11455
|
+
throw new Error(
|
|
11456
|
+
`Video was still rendering after ${Math.round(VIDEO_TIMEOUT_MS / 6e4)} minutes. It may still complete on the provider side, but Cascade stopped waiting.`
|
|
11457
|
+
);
|
|
11458
|
+
}
|
|
11459
|
+
if (operation.error?.message) throw new Error(operation.error.message);
|
|
11460
|
+
const uri = operation.response?.generateVideoResponse?.generatedSamples?.[0]?.video?.uri;
|
|
11461
|
+
if (!uri) throw new Error("Video finished but the response contained no video.");
|
|
11462
|
+
const media = await fetch(uri, { headers, signal });
|
|
11463
|
+
if (!media.ok) throw new Error(`Video finished but could not be downloaded (HTTP ${media.status}).`);
|
|
11464
|
+
return {
|
|
11465
|
+
data: Buffer.from(await media.arrayBuffer()),
|
|
11466
|
+
mimeType: "video/mp4",
|
|
11467
|
+
filename: stamp("mp4"),
|
|
11468
|
+
modelId: cap.modelId,
|
|
11469
|
+
provider: cap.provider
|
|
11470
|
+
};
|
|
11471
|
+
}, cap.modelId);
|
|
11472
|
+
}
|
|
11401
11473
|
async function transcribeAudio(cap, cfg, req, signal) {
|
|
11402
11474
|
return callProvider(async () => {
|
|
11403
11475
|
if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
|
|
@@ -11522,6 +11594,58 @@ var GenerateSpeechTool = class extends BaseTool {
|
|
|
11522
11594
|
Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
|
|
11523
11595
|
}
|
|
11524
11596
|
};
|
|
11597
|
+
var DEFAULT_VIDEO_SECONDS = 5;
|
|
11598
|
+
var MAX_VIDEO_SECONDS = 8;
|
|
11599
|
+
var GenerateVideoTool = class extends BaseTool {
|
|
11600
|
+
constructor(deps) {
|
|
11601
|
+
super();
|
|
11602
|
+
this.deps = deps;
|
|
11603
|
+
}
|
|
11604
|
+
deps;
|
|
11605
|
+
name = "generate_video";
|
|
11606
|
+
// The price is in the description on purpose. Video is ~1000x the cost of an
|
|
11607
|
+
// image per call, it is billed per SECOND, and the model choosing the
|
|
11608
|
+
// duration is the one deciding the bill — so the number has to be where the
|
|
11609
|
+
// model reads before calling, not only in the result afterwards.
|
|
11610
|
+
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.";
|
|
11611
|
+
inputSchema = {
|
|
11612
|
+
type: "object",
|
|
11613
|
+
properties: {
|
|
11614
|
+
prompt: { type: "string", description: "What the video should show: subject, action, camera movement, style." },
|
|
11615
|
+
seconds: { type: "number", description: `Clip length, 1-${MAX_VIDEO_SECONDS} seconds (default ${DEFAULT_VIDEO_SECONDS}). Billed per second \u2014 keep it short.` },
|
|
11616
|
+
aspectRatio: { type: "string", description: 'Frame shape: "16:9" (default) or "9:16" for vertical.' },
|
|
11617
|
+
model: { type: "string", description: "Optional: force a specific video model." }
|
|
11618
|
+
},
|
|
11619
|
+
required: ["prompt"]
|
|
11620
|
+
};
|
|
11621
|
+
async execute(input, options) {
|
|
11622
|
+
const prompt = String(input["prompt"] ?? "").trim();
|
|
11623
|
+
if (!prompt) return 'Provide a "prompt" describing the video.';
|
|
11624
|
+
const r = resolve(this.deps, "video", input["model"] ? String(input["model"]) : void 0);
|
|
11625
|
+
if (!r.ok) return r.error;
|
|
11626
|
+
const asked = typeof input["seconds"] === "number" ? input["seconds"] : DEFAULT_VIDEO_SECONDS;
|
|
11627
|
+
const seconds = Math.max(1, Math.min(MAX_VIDEO_SECONDS, Math.round(asked)));
|
|
11628
|
+
const asset = await generateVideo(
|
|
11629
|
+
r.selected.capability,
|
|
11630
|
+
r.cfg,
|
|
11631
|
+
{
|
|
11632
|
+
prompt,
|
|
11633
|
+
seconds,
|
|
11634
|
+
...input["aspectRatio"] ? { aspectRatio: String(input["aspectRatio"]) } : {}
|
|
11635
|
+
},
|
|
11636
|
+
options.signal,
|
|
11637
|
+
// Progress goes to the run log so a two-minute wait doesn't look like a
|
|
11638
|
+
// hang — the single most likely reason someone kills a working render.
|
|
11639
|
+
(note) => options.onProgress?.(note)
|
|
11640
|
+
);
|
|
11641
|
+
const location = await this.deps.sink(asset);
|
|
11642
|
+
const unit = r.selected.cost;
|
|
11643
|
+
const estimate = unit ? ` Estimated cost: $${(unit.amount * seconds).toFixed(2)} (${seconds}s x ${unit.label}).` : "";
|
|
11644
|
+
const clamped = seconds !== Math.round(asked) ? ` Duration was clamped to ${seconds}s (max ${MAX_VIDEO_SECONDS}).` : "";
|
|
11645
|
+
return `Video generated and saved to ${location}.
|
|
11646
|
+
Model: ${asset.modelId}.${estimate}${clamped}`;
|
|
11647
|
+
}
|
|
11648
|
+
};
|
|
11525
11649
|
var TranscribeAudioTool = class extends BaseTool {
|
|
11526
11650
|
constructor(deps, readFile) {
|
|
11527
11651
|
super();
|
|
@@ -11572,6 +11696,7 @@ function buildMediaTools(deps, readFile) {
|
|
|
11572
11696
|
const modalities = new Set(deps.registry.availableModalities());
|
|
11573
11697
|
if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
|
|
11574
11698
|
if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
|
|
11699
|
+
if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
|
|
11575
11700
|
if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
|
|
11576
11701
|
return tools;
|
|
11577
11702
|
}
|
|
@@ -15991,6 +16116,7 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
15991
16116
|
router;
|
|
15992
16117
|
toolRegistry;
|
|
15993
16118
|
multimodal;
|
|
16119
|
+
mediaSink;
|
|
15994
16120
|
mcpClient;
|
|
15995
16121
|
codeIndex;
|
|
15996
16122
|
config;
|
|
@@ -16310,14 +16436,27 @@ ${input}` }],
|
|
|
16310
16436
|
* server stores a file row) constructs the tools itself via buildMediaTools
|
|
16311
16437
|
* with its own sink.
|
|
16312
16438
|
*/
|
|
16439
|
+
/**
|
|
16440
|
+
* Replace where generated media is written.
|
|
16441
|
+
*
|
|
16442
|
+
* The default sink writes into the workspace, which is what a desktop or CLI
|
|
16443
|
+
* run wants. A host with different storage — the cloud server keeps a file
|
|
16444
|
+
* row plus bytes under the tenant's directory — supplies its own here, and
|
|
16445
|
+
* the media tools are rebuilt around it. Re-registering by name replaces the
|
|
16446
|
+
* existing tools rather than duplicating them.
|
|
16447
|
+
*/
|
|
16448
|
+
setMediaSink(sink) {
|
|
16449
|
+
this.mediaSink = sink;
|
|
16450
|
+
this.registerMediaTools(this.workspacePath);
|
|
16451
|
+
}
|
|
16313
16452
|
registerMediaTools(workspacePath) {
|
|
16314
|
-
const
|
|
16315
|
-
const permitted = (name) => !
|
|
16453
|
+
const denied = new Set(this.config.tools?.disabledTools ?? []);
|
|
16454
|
+
const permitted = (name) => !denied.has(name);
|
|
16316
16455
|
const tools = buildMediaTools(
|
|
16317
16456
|
{
|
|
16318
16457
|
registry: this.multimodal,
|
|
16319
16458
|
lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
|
|
16320
|
-
sink: async (asset) => {
|
|
16459
|
+
sink: this.mediaSink ?? (async (asset) => {
|
|
16321
16460
|
const fsp = await import('fs/promises');
|
|
16322
16461
|
const nodePath = await import('path');
|
|
16323
16462
|
const dir = nodePath.join(workspacePath, "generated");
|
|
@@ -16325,7 +16464,7 @@ ${input}` }],
|
|
|
16325
16464
|
const dest = nodePath.join(dir, asset.filename);
|
|
16326
16465
|
await fsp.writeFile(dest, asset.data);
|
|
16327
16466
|
return nodePath.relative(workspacePath, dest);
|
|
16328
|
-
}
|
|
16467
|
+
})
|
|
16329
16468
|
},
|
|
16330
16469
|
async (p) => {
|
|
16331
16470
|
const fsp = await import('fs/promises');
|
|
@@ -17329,7 +17468,7 @@ var CloudClient = class _CloudClient {
|
|
|
17329
17468
|
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
17330
17469
|
while (Date.now() < deadline) {
|
|
17331
17470
|
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
17332
|
-
await
|
|
17471
|
+
await sleep2(interval * 1e3, opts.signal);
|
|
17333
17472
|
const r = await this.pollDevice(start.deviceCode);
|
|
17334
17473
|
if (r === "slow_down") {
|
|
17335
17474
|
interval += 5;
|
|
@@ -17540,7 +17679,7 @@ var CloudClient = class _CloudClient {
|
|
|
17540
17679
|
this.store.clear();
|
|
17541
17680
|
}
|
|
17542
17681
|
};
|
|
17543
|
-
function
|
|
17682
|
+
function sleep2(ms, signal) {
|
|
17544
17683
|
return new Promise((resolve2, reject) => {
|
|
17545
17684
|
const t = setTimeout(resolve2, ms);
|
|
17546
17685
|
signal?.addEventListener("abort", () => {
|