cascade-ai 0.50.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 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.50.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";
@@ -2311,49 +2311,40 @@ function isChatModel(id, methods) {
2311
2311
  }
2312
2312
  return !NON_CHAT.test(id);
2313
2313
  }
2314
- var NON_CHAT_PATTERNS, NON_CHAT;
2314
+ var MODALITY_PATTERNS, NON_CHAT;
2315
2315
  var init_model_filter = __esm({
2316
2316
  "src/providers/model-filter.ts"() {
2317
- NON_CHAT_PATTERNS = [
2318
- "embed",
2317
+ MODALITY_PATTERNS = [
2318
+ ["embedding", ["embed"]],
2319
2319
  // text-embedding-3, nomic-embed, mxbai-embed, snowflake-arctic-embed
2320
- "(?:^|[-_/])tts(?:[-_/]|$)",
2321
- "text-to-speech",
2322
- "speech",
2323
- // text-to-speech
2324
- "whisper",
2325
- "transcrib",
2326
- "(?:^|[-_/])stt(?:[-_/]|$)",
2327
- // speech-to-text
2328
- "dall[-]?e",
2329
- "imagen",
2330
- "image-generation",
2331
- "gpt-image",
2332
- "(?:^|[-_/])image(?:[-_/]|$)",
2333
- "stable-diffusion",
2334
- "sdxl",
2335
- "flux",
2336
- // image gen
2337
- "moderation",
2338
- "(?:^|[-_/])guard(?:[-_/]|$)",
2339
- // moderation / safety
2340
- "rerank",
2341
- "(?:^|[-_/])audio(?:[-_/]|$)",
2342
- "native-audio",
2343
- "realtime",
2344
- // audio / realtime
2345
- "veo",
2346
- "sora",
2347
- "(?:^|[-_/])video(?:[-_/]|$)",
2348
- // video
2349
- "(?:^|[-_/])aqa(?:[-_/]|$)",
2350
- // attributed QA
2351
- "(?:^|[-_/])live(?:[-_/]|$)",
2352
- // live-audio variants
2353
- "(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"
2354
- // legacy completion-only base models
2320
+ ["transcription", ["whisper", "transcrib", "(?:^|[-_/])stt(?:[-_/]|$)"]],
2321
+ ["speech", ["(?:^|[-_/])tts(?:[-_/]|$)", "text-to-speech", "speech"]],
2322
+ ["video", ["veo", "sora", "(?:^|[-_/])video(?:[-_/]|$)"]],
2323
+ ["image", [
2324
+ "dall[-]?e",
2325
+ "imagen",
2326
+ "image-generation",
2327
+ "gpt-image",
2328
+ "(?:^|[-_/])image(?:[-_/]|$)",
2329
+ "stable-diffusion",
2330
+ "sdxl",
2331
+ "flux"
2332
+ ]],
2333
+ ["moderation", ["moderation", "(?:^|[-_/])guard(?:[-_/]|$)"]],
2334
+ ["rerank", ["rerank"]],
2335
+ ["realtime", [
2336
+ "(?:^|[-_/])audio(?:[-_/]|$)",
2337
+ "native-audio",
2338
+ "realtime",
2339
+ "(?:^|[-_/])live(?:[-_/]|$)",
2340
+ // live-audio variants
2341
+ "(?:^|[-_/])aqa(?:[-_/]|$)"
2342
+ // attributed QA
2343
+ ]],
2344
+ ["legacy-completion", ["(?:^|[-_/])(?:davinci|babbage)(?:[-_/]|$)"]]
2355
2345
  ];
2356
- NON_CHAT = new RegExp(NON_CHAT_PATTERNS.join("|"), "i");
2346
+ MODALITY_PATTERNS.map(([m, pats]) => [m, new RegExp(pats.join("|"), "i")]);
2347
+ NON_CHAT = new RegExp(MODALITY_PATTERNS.flatMap(([, p]) => p).join("|"), "i");
2357
2348
  }
2358
2349
  });
2359
2350
 
@@ -2921,6 +2912,118 @@ var init_azure = __esm({
2921
2912
  }
2922
2913
  });
2923
2914
 
2915
+ // src/providers/gemini-schema.ts
2916
+ function isPlainObject(v) {
2917
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2918
+ }
2919
+ function normalizeType(value) {
2920
+ if (typeof value === "string") {
2921
+ return ALLOWED_TYPES.has(value) ? { type: value } : {};
2922
+ }
2923
+ if (Array.isArray(value)) {
2924
+ const types = value.filter((t) => typeof t === "string");
2925
+ const nullable = types.includes("null");
2926
+ const concrete = types.find((t) => t !== "null" && ALLOWED_TYPES.has(t));
2927
+ return {
2928
+ ...concrete ? { type: concrete } : {},
2929
+ ...nullable ? { nullable: true } : {}
2930
+ };
2931
+ }
2932
+ return {};
2933
+ }
2934
+ function normalizeEnum(value) {
2935
+ if (!Array.isArray(value) || value.length === 0) return void 0;
2936
+ return value.map((v) => typeof v === "string" ? v : JSON.stringify(v));
2937
+ }
2938
+ function sanitizeGeminiSchema(schema) {
2939
+ if (!isPlainObject(schema)) return void 0;
2940
+ const out = {};
2941
+ for (const [key, value] of Object.entries(schema)) {
2942
+ if (!ALLOWED_KEYS.has(key)) continue;
2943
+ switch (key) {
2944
+ case "type": {
2945
+ Object.assign(out, normalizeType(value));
2946
+ break;
2947
+ }
2948
+ case "enum": {
2949
+ const e = normalizeEnum(value);
2950
+ if (e) out["enum"] = e;
2951
+ break;
2952
+ }
2953
+ case "properties": {
2954
+ if (!isPlainObject(value)) break;
2955
+ const props = {};
2956
+ for (const [propName, propSchema] of Object.entries(value)) {
2957
+ const cleaned = sanitizeGeminiSchema(propSchema);
2958
+ props[propName] = cleaned && Object.keys(cleaned).length ? cleaned : { type: "string" };
2959
+ }
2960
+ out["properties"] = props;
2961
+ break;
2962
+ }
2963
+ case "items": {
2964
+ const cleaned = sanitizeGeminiSchema(value);
2965
+ if (cleaned) out["items"] = cleaned;
2966
+ break;
2967
+ }
2968
+ case "anyOf": {
2969
+ if (!Array.isArray(value)) break;
2970
+ const variants = value.map((v) => sanitizeGeminiSchema(v)).filter((v) => !!v && Object.keys(v).length > 0);
2971
+ if (variants.length) out["anyOf"] = variants;
2972
+ break;
2973
+ }
2974
+ case "required": {
2975
+ if (Array.isArray(value)) {
2976
+ out["required"] = value.filter((r) => typeof r === "string");
2977
+ }
2978
+ break;
2979
+ }
2980
+ default:
2981
+ out[key] = value;
2982
+ }
2983
+ }
2984
+ if (!out["type"] && out["properties"]) out["type"] = "object";
2985
+ if (!out["type"] && out["items"]) out["type"] = "array";
2986
+ return out;
2987
+ }
2988
+ function toGeminiParameters(inputSchema) {
2989
+ const cleaned = sanitizeGeminiSchema(inputSchema);
2990
+ if (!cleaned) return void 0;
2991
+ const props = cleaned["properties"];
2992
+ if (isPlainObject(props) && Object.keys(props).length === 0) return void 0;
2993
+ if (Object.keys(cleaned).length === 0) return void 0;
2994
+ return cleaned;
2995
+ }
2996
+ var ALLOWED_KEYS, ALLOWED_TYPES;
2997
+ var init_gemini_schema = __esm({
2998
+ "src/providers/gemini-schema.ts"() {
2999
+ ALLOWED_KEYS = /* @__PURE__ */ new Set([
3000
+ "type",
3001
+ "format",
3002
+ "title",
3003
+ "description",
3004
+ "nullable",
3005
+ "enum",
3006
+ "items",
3007
+ "properties",
3008
+ "required",
3009
+ "propertyOrdering",
3010
+ "minItems",
3011
+ "maxItems",
3012
+ "minProperties",
3013
+ "maxProperties",
3014
+ "minimum",
3015
+ "maximum",
3016
+ "minLength",
3017
+ "maxLength",
3018
+ "pattern",
3019
+ "example",
3020
+ "default",
3021
+ "anyOf"
3022
+ ]);
3023
+ ALLOWED_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "array", "object"]);
3024
+ }
3025
+ });
3026
+
2924
3027
  // src/providers/gemini.ts
2925
3028
  var gemini_exports = {};
2926
3029
  __export(gemini_exports, {
@@ -2933,6 +3036,7 @@ var init_gemini = __esm({
2933
3036
  init_base();
2934
3037
  init_pricing();
2935
3038
  init_model_filter();
3039
+ init_gemini_schema();
2936
3040
  GeminiProvider = class extends BaseProvider {
2937
3041
  client;
2938
3042
  constructor(config, model) {
@@ -3153,7 +3257,12 @@ var init_gemini = __esm({
3153
3257
  return {
3154
3258
  name: tool.name,
3155
3259
  description: tool.description,
3156
- parameters: tool.inputSchema
3260
+ // Convert, never cast. Gemini's parameters field is an OpenAPI subset
3261
+ // that 400s on unknown keys, and MCP servers ship schemas carrying their
3262
+ // own extensions (GitHub's adds `x-mcp-header` to every header-bound
3263
+ // property). Casting sent those through verbatim and every request failed
3264
+ // before the model saw it. See providers/gemini-schema.ts.
3265
+ parameters: toGeminiParameters(tool.inputSchema)
3157
3266
  };
3158
3267
  }
3159
3268
  };
@@ -3178,7 +3287,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3178
3287
  headers["accept-encoding"] = "gzip, deflate, br";
3179
3288
  }
3180
3289
  const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3181
- return new Promise((resolve, reject) => {
3290
+ return new Promise((resolve2, reject) => {
3182
3291
  const req = lib.request(
3183
3292
  {
3184
3293
  hostname: u.hostname,
@@ -3195,7 +3304,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3195
3304
  const nextUrl = new URL(location, u).href;
3196
3305
  const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3197
3306
  const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3198
- resolve(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3307
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1));
3199
3308
  return;
3200
3309
  }
3201
3310
  const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
@@ -3210,7 +3319,7 @@ async function nodeHttpFetch(input, init = {}, redirectCount = 0) {
3210
3319
  if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3211
3320
  else if (typeof v === "string") respHeaders.set(k, v);
3212
3321
  }
3213
- resolve(new Response(stream$1, {
3322
+ resolve2(new Response(stream$1, {
3214
3323
  status,
3215
3324
  statusText: res.statusMessage ?? "",
3216
3325
  headers: respHeaders
@@ -4941,7 +5050,14 @@ var ToolsConfigSchema = zod.z.object({
4941
5050
  * built-in tool (shell/file/git/…) from existing at all, rather than just
4942
5051
  * gating it behind approval. Omitted = full default set (unchanged).
4943
5052
  */
4944
- 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()
4945
5061
  });
4946
5062
  var HookDefinitionSchema = zod.z.object({
4947
5063
  name: zod.z.string().optional(),
@@ -5009,6 +5125,14 @@ var BudgetConfigSchema = zod.z.object({
5009
5125
  maxTokensPerRun: zod.z.number().int().positive().default(2e5),
5010
5126
  /** Optional hard per-task cost ceiling (USD). Unset = only the token cap applies. */
5011
5127
  maxCostPerRunUsd: zod.z.number().positive().optional(),
5128
+ /**
5129
+ * Consecutive systemic failures against ONE model before the run stops
5130
+ * launching work that would fail the same way (dead key, wrong model id,
5131
+ * exhausted quota). Lower reacts sooner and wastes less; higher absorbs a
5132
+ * longer transient before giving up. Only failures that will certainly repeat
5133
+ * are counted — see core/run-breaker.ts.
5134
+ */
5135
+ failureThreshold: zod.z.number().int().min(1).max(20).default(3),
5012
5136
  warnAtPct: zod.z.number().default(80)
5013
5137
  });
5014
5138
  var WorkspaceConfigSchema = zod.z.object({
@@ -6061,7 +6185,7 @@ var TpmLimiter = class {
6061
6185
  }
6062
6186
  const deficit = want - bucket.available;
6063
6187
  const waitMs = Math.max(50, Math.ceil(deficit / bucket.tokensPerMinute * 6e4));
6064
- await new Promise((resolve) => setTimeout(resolve, waitMs));
6188
+ await new Promise((resolve2) => setTimeout(resolve2, waitMs));
6065
6189
  }
6066
6190
  }
6067
6191
  /**
@@ -6124,14 +6248,14 @@ var LocalRequestQueue = class {
6124
6248
  this.active++;
6125
6249
  return this.makeRelease();
6126
6250
  }
6127
- return new Promise((resolve, reject) => {
6251
+ return new Promise((resolve2, reject) => {
6128
6252
  let settled = false;
6129
6253
  let timer;
6130
6254
  const resolver = (release) => {
6131
6255
  if (settled) return;
6132
6256
  settled = true;
6133
6257
  if (timer !== void 0) clearTimeout(timer);
6134
- resolve(release);
6258
+ resolve2(release);
6135
6259
  };
6136
6260
  if (timeoutMs !== void 0 && timeoutMs > 0) {
6137
6261
  timer = setTimeout(() => {
@@ -6758,6 +6882,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
6758
6882
  tpmLimiter;
6759
6883
  localQueue;
6760
6884
  taskAnalyzer;
6885
+ pendingFeedbackSource;
6761
6886
  worldStateDB;
6762
6887
  privacyPaths;
6763
6888
  guidanceQueue;
@@ -7212,8 +7337,18 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
7212
7337
  return windows.length ? Math.min(...windows) : void 0;
7213
7338
  }
7214
7339
  /** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
7340
+ /**
7341
+ * Feed per-model thumbs counts into Auto routing. Forwarded to the analyzer
7342
+ * because that is where the public benchmark score is combined; the prior
7343
+ * adjusts that number rather than sitting anywhere else in the pipeline.
7344
+ */
7345
+ setFeedbackSource(source) {
7346
+ this.taskAnalyzer?.setFeedbackSource(source);
7347
+ this.pendingFeedbackSource = source;
7348
+ }
7215
7349
  setTaskAnalyzer(analyzer) {
7216
7350
  this.taskAnalyzer = analyzer;
7351
+ if (this.pendingFeedbackSource) analyzer.setFeedbackSource(this.pendingFeedbackSource);
7217
7352
  }
7218
7353
  setWorldStateDB(db) {
7219
7354
  this.worldStateDB = db;
@@ -8308,8 +8443,11 @@ Now execute your subtask using this context where relevant.`
8308
8443
  const result = await this.runAgentLoop(systemPrompt, this.tools);
8309
8444
  output = result.output;
8310
8445
  toolCalls = result.toolCalls;
8311
- this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8312
- const artifactCheck = await this.verifyArtifacts(assignment);
8446
+ const mustProduceArtifact = this.requiresArtifact();
8447
+ if (mustProduceArtifact) {
8448
+ this.sendStatusUpdate({ progressPct: 65, currentAction: "Verifying required artifacts", status: "IN_PROGRESS" });
8449
+ }
8450
+ const artifactCheck = mustProduceArtifact ? await this.verifyArtifacts(assignment) : { ok: true, issues: [] };
8313
8451
  if (!artifactCheck.ok) {
8314
8452
  correctionAttempts = 1;
8315
8453
  issues.push(...artifactCheck.issues);
@@ -8589,11 +8727,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8589
8727
  if (!wasApproved) return `Tool ${tc.name} was denied by user.`;
8590
8728
  } else {
8591
8729
  const LEGACY_APPROVAL_TIMEOUT_MS = 6e5;
8592
- const legacyDecision = await new Promise((resolve) => {
8730
+ const legacyDecision = await new Promise((resolve2) => {
8593
8731
  const eventName = `tool:approval-response:${this.id}-${tc.id}`;
8594
8732
  const timer = setTimeout(() => {
8595
8733
  this.removeAllListeners(eventName);
8596
- resolve({ approved: false });
8734
+ resolve2({ approved: false });
8597
8735
  }, LEGACY_APPROVAL_TIMEOUT_MS);
8598
8736
  timer.unref?.();
8599
8737
  this.emit("tool:approval-request", {
@@ -8606,7 +8744,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8606
8744
  });
8607
8745
  this.once(eventName, (d) => {
8608
8746
  clearTimeout(timer);
8609
- resolve(d);
8747
+ resolve2(d);
8610
8748
  });
8611
8749
  });
8612
8750
  if (legacyDecision.always) this.sessionApprovals.set(tc.name, legacyDecision.approved);
@@ -8626,6 +8764,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8626
8764
  tierId: this.id,
8627
8765
  sessionId: this.taskId,
8628
8766
  requireApproval: false,
8767
+ // Media generation can run for a minute; without this a cancelled run
8768
+ // still pays for an image nobody will see.
8769
+ ...this.signal ? { signal: this.signal } : {},
8629
8770
  saveSnapshot: async (path37, content) => {
8630
8771
  this.store?.addFileSnapshot(this.taskId, path37, content);
8631
8772
  },
@@ -8673,7 +8814,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8673
8814
  const result = await this.toolRegistry.execute(altTool, tc.input, {
8674
8815
  tierId: this.id,
8675
8816
  sessionId: this.taskId,
8676
- requireApproval: false
8817
+ requireApproval: false,
8818
+ ...this.signal ? { signal: this.signal } : {}
8677
8819
  });
8678
8820
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8679
8821
  if (!str2.startsWith("Tool error:") && !str2.startsWith("Error:")) {
@@ -8695,7 +8837,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
8695
8837
  const result = await this.toolRegistry.execute(newToolName, tc.input, {
8696
8838
  tierId: this.id,
8697
8839
  sessionId: this.taskId,
8698
- requireApproval: false
8840
+ requireApproval: false,
8841
+ ...this.signal ? { signal: this.signal } : {}
8699
8842
  });
8700
8843
  const str2 = typeof result === "string" ? result : JSON.stringify(result);
8701
8844
  if (!str2.startsWith("Tool error:")) return `[Synthesized ${newToolName}]: ${str2}`;
@@ -9113,7 +9256,7 @@ var PeerBus = class extends EventEmitter__default.default {
9113
9256
  this.outputs.set(subtaskId, entry);
9114
9257
  this.emit("output:ready", entry);
9115
9258
  const waiting = this.waiters.get(subtaskId) ?? [];
9116
- for (const resolve of waiting) resolve(entry);
9259
+ for (const resolve2 of waiting) resolve2(entry);
9117
9260
  this.waiters.delete(subtaskId);
9118
9261
  }
9119
9262
  /**
@@ -9144,10 +9287,10 @@ var PeerBus = class extends EventEmitter__default.default {
9144
9287
  waitFor(subtaskId, timeoutMs = 12e4) {
9145
9288
  const existing = this.outputs.get(subtaskId);
9146
9289
  if (existing && !this.retryPending.has(subtaskId)) return Promise.resolve(existing);
9147
- return new Promise((resolve, reject) => {
9290
+ return new Promise((resolve2, reject) => {
9148
9291
  const resolver = (output) => {
9149
9292
  clearTimeout(timer);
9150
- resolve(output);
9293
+ resolve2(output);
9151
9294
  };
9152
9295
  const timer = setTimeout(() => {
9153
9296
  const waiting = this.waiters.get(subtaskId);
@@ -9201,10 +9344,10 @@ var PeerBus = class extends EventEmitter__default.default {
9201
9344
  */
9202
9345
  collect(timeoutMs) {
9203
9346
  const collected = [...this.broadcastLog];
9204
- return new Promise((resolve) => {
9347
+ return new Promise((resolve2) => {
9205
9348
  const timer = setTimeout(() => {
9206
9349
  off();
9207
- resolve(collected);
9350
+ resolve2(collected);
9208
9351
  }, timeoutMs);
9209
9352
  const handler = (msg) => {
9210
9353
  collected.push({ fromId: msg.fromId, payload: msg.payload, timestamp: msg.timestamp });
@@ -9218,7 +9361,7 @@ var PeerBus = class extends EventEmitter__default.default {
9218
9361
  const broadcasters = new Set(collected.map((m) => m.fromId));
9219
9362
  if (broadcasters.size >= this.members.size) {
9220
9363
  off();
9221
- resolve(collected);
9364
+ resolve2(collected);
9222
9365
  }
9223
9366
  };
9224
9367
  this.on("broadcast", checkComplete);
@@ -9236,7 +9379,7 @@ var PeerBus = class extends EventEmitter__default.default {
9236
9379
  return;
9237
9380
  }
9238
9381
  this.emitCoordination(tierId, `\u23F3 waiting for ${filePath} (held by ${existing.holderId})`);
9239
- return new Promise((resolve, reject) => {
9382
+ return new Promise((resolve2, reject) => {
9240
9383
  const timer = setTimeout(() => {
9241
9384
  reject(new Error(`File lock timeout for ${filePath} (held by ${existing.holderId})`));
9242
9385
  }, timeoutMs);
@@ -9244,7 +9387,7 @@ var PeerBus = class extends EventEmitter__default.default {
9244
9387
  clearTimeout(timer);
9245
9388
  this.fileLocks.set(filePath, { holderId: tierId, lockedAt: (/* @__PURE__ */ new Date()).toISOString(), waiters: [] });
9246
9389
  this.emitCoordination(tierId, `\u{1F512} locked ${filePath}`);
9247
- resolve();
9390
+ resolve2();
9248
9391
  });
9249
9392
  });
9250
9393
  }
@@ -9269,11 +9412,11 @@ var PeerBus = class extends EventEmitter__default.default {
9269
9412
  */
9270
9413
  waitForFileRelease(filePath, timeoutMs = 3e4) {
9271
9414
  if (!this.fileLocks.has(filePath)) return Promise.resolve();
9272
- return new Promise((resolve, reject) => {
9415
+ return new Promise((resolve2, reject) => {
9273
9416
  const timer = setTimeout(() => reject(new Error(`Timeout waiting for file release: ${filePath}`)), timeoutMs);
9274
9417
  this.once(`file:released:${filePath}`, () => {
9275
9418
  clearTimeout(timer);
9276
- resolve();
9419
+ resolve2();
9277
9420
  });
9278
9421
  });
9279
9422
  }
@@ -9339,13 +9482,13 @@ var PeerBus = class extends EventEmitter__default.default {
9339
9482
  this.emit(`barrier:${barrierName}`);
9340
9483
  return;
9341
9484
  }
9342
- return new Promise((resolve, reject) => {
9485
+ return new Promise((resolve2, reject) => {
9343
9486
  const timer = setTimeout(() => {
9344
9487
  reject(new Error(`Barrier timeout: ${barrierName} (${bar.arrived.size}/${bar.total} arrived)`));
9345
9488
  }, 12e4);
9346
9489
  this.once(`barrier:${barrierName}`, () => {
9347
9490
  clearTimeout(timer);
9348
- resolve();
9491
+ resolve2();
9349
9492
  });
9350
9493
  });
9351
9494
  }
@@ -10896,6 +11039,171 @@ Reply with exactly one word: YES, NO, or UNSURE.
10896
11039
  // src/core/cascade.ts
10897
11040
  init_cost();
10898
11041
 
11042
+ // src/core/multimodal/registry.ts
11043
+ init_pricing();
11044
+ var CAPABILITIES = [
11045
+ // ── Image ──
11046
+ {
11047
+ modality: "image",
11048
+ provider: "openai",
11049
+ modelId: "dall-e-3",
11050
+ api: "openai-images"
11051
+ },
11052
+ {
11053
+ modality: "image",
11054
+ provider: "gemini",
11055
+ modelId: "gemini-3-pro-image",
11056
+ api: "gemini-predict"
11057
+ },
11058
+ // ── Speech (text → audio) ──
11059
+ {
11060
+ modality: "speech",
11061
+ provider: "openai",
11062
+ modelId: "tts-1",
11063
+ api: "openai-speech"
11064
+ },
11065
+ {
11066
+ // The only quality claim in this table that needs no leaderboard: OpenAI
11067
+ // ships these as the same voice at two fidelities and charges double for
11068
+ // this one. That is the vendor's own ordering, not an inference.
11069
+ modality: "speech",
11070
+ provider: "openai",
11071
+ modelId: "tts-1-hd",
11072
+ api: "openai-speech",
11073
+ quality: 70,
11074
+ qualityBasis: "OpenAI's own hd variant of tts-1, priced 2x"
11075
+ },
11076
+ // ── Transcription (audio → text) ──
11077
+ {
11078
+ modality: "transcription",
11079
+ provider: "openai",
11080
+ modelId: "whisper-1",
11081
+ api: "openai-transcriptions"
11082
+ },
11083
+ // ── Video ──
11084
+ {
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`.
11088
+ modality: "video",
11089
+ provider: "gemini",
11090
+ modelId: "veo-3.1-generate-001",
11091
+ api: "gemini-predict-lro"
11092
+ }
11093
+ ];
11094
+ function capabilityCost(cap) {
11095
+ const match = findPricing(cap.modelId, cap.provider, {
11096
+ fallbackProviders: cap.provider === "gemini" ? ["vertex"] : []
11097
+ });
11098
+ const rate = match?.entry.rates?.[0];
11099
+ if (!rate) return null;
11100
+ const unitLabel = {
11101
+ per_image: "per image",
11102
+ per_second: "per second of video",
11103
+ per_character: "per character",
11104
+ per_audio_minute: "per audio minute"
11105
+ };
11106
+ const unit = unitLabel[rate.unit] ?? rate.unit;
11107
+ const amount = rate.amount;
11108
+ const shown = amount < 1e-3 ? amount.toExponential(2) : amount.toFixed(4);
11109
+ return { label: `$${shown} ${unit}`, amount, unit };
11110
+ }
11111
+ var MultimodalRegistry = class {
11112
+ /** Provider types the user has actually configured a key for. */
11113
+ available;
11114
+ constructor(availableProviders) {
11115
+ this.available = new Set(availableProviders);
11116
+ }
11117
+ /** Every capability whose provider is configured AND which Cascade can call. */
11118
+ usable(modality) {
11119
+ return CAPABILITIES.filter(
11120
+ (c) => !c.unsupported && this.available.has(c.provider) && (!modality || c.modality === modality)
11121
+ );
11122
+ }
11123
+ /** Modalities this account can actually generate right now. */
11124
+ availableModalities() {
11125
+ return [...new Set(this.usable().map((c) => c.modality))];
11126
+ }
11127
+ /**
11128
+ * Pick a model for a modality.
11129
+ *
11130
+ * Ranked models beat unranked ones, then higher quality, then lower unit
11131
+ * price. An explicit `preferModel` wins outright when it is usable — the user
11132
+ * naming a model is a stronger signal than any prior.
11133
+ */
11134
+ select(modality, opts = {}) {
11135
+ const pool = this.usable(modality);
11136
+ if (!pool.length) return null;
11137
+ if (opts.preferModel) {
11138
+ const wanted = pool.find((c) => c.modelId === opts.preferModel);
11139
+ if (wanted) {
11140
+ return { capability: wanted, cost: capabilityCost(wanted), reason: `Requested explicitly: ${wanted.modelId}.` };
11141
+ }
11142
+ }
11143
+ const scored = pool.map((c) => ({ c, cost: capabilityCost(c) })).sort((a, b) => {
11144
+ const qa = a.c.quality ?? -1;
11145
+ const qb = b.c.quality ?? -1;
11146
+ if (qa !== qb) return qb - qa;
11147
+ const pa = a.cost?.amount ?? Number.POSITIVE_INFINITY;
11148
+ const pb = b.cost?.amount ?? Number.POSITIVE_INFINITY;
11149
+ return pa - pb;
11150
+ });
11151
+ const best = scored[0];
11152
+ const ranked = best.c.quality !== void 0;
11153
+ const reason = ranked ? `${best.c.modelId} (${best.c.provider}) \u2014 ranked highest for ${modality}: ${best.c.qualityBasis}.` + (best.cost ? ` ${best.cost.label}.` : "") : `${best.c.modelId} (${best.c.provider}) \u2014 cheapest usable ${modality} model` + (best.cost ? ` at ${best.cost.label}` : "") + ". No published quality ranking is applied for this modality, so this is a cost choice, not a quality one.";
11154
+ return { capability: best.c, cost: best.cost, reason };
11155
+ }
11156
+ /**
11157
+ * A plain-language account of what this account can and cannot generate —
11158
+ * including the things it cannot, and why. The planner sees this, so being
11159
+ * candid here is what stops it inventing a `generate_music` call.
11160
+ */
11161
+ describe() {
11162
+ const lines = [];
11163
+ const usable = this.usable();
11164
+ if (usable.length) {
11165
+ lines.push("Generation models available on your configured providers:");
11166
+ for (const modality of ["image", "speech", "transcription", "video"]) {
11167
+ const forModality = usable.filter((c) => c.modality === modality);
11168
+ if (!forModality.length) continue;
11169
+ const shown = forModality.map((c) => {
11170
+ const cost = capabilityCost(c);
11171
+ return `${c.modelId}${cost ? ` (${cost.label})` : ""}`;
11172
+ });
11173
+ lines.push(` ${modality}: ${shown.join(", ")}`);
11174
+ }
11175
+ } else {
11176
+ lines.push("No generation models are available \u2014 none of your configured providers offers one.");
11177
+ }
11178
+ const blocked = CAPABILITIES.filter((c) => c.unsupported && this.available.has(c.provider));
11179
+ for (const c of blocked) {
11180
+ lines.push(` ${c.modality}: ${c.modelId} exists on your account but is not callable. ${c.unsupported}`);
11181
+ }
11182
+ lines.push(
11183
+ " music: no supported provider exposes a music-generation API, so Cascade cannot generate music."
11184
+ );
11185
+ return lines.join("\n");
11186
+ }
11187
+ };
11188
+
11189
+ // src/tools/base.ts
11190
+ var BaseTool = class {
11191
+ workspaceRoot = process.cwd();
11192
+ setWorkspaceRoot(root) {
11193
+ this.workspaceRoot = root;
11194
+ }
11195
+ isDangerous() {
11196
+ return false;
11197
+ }
11198
+ getDefinition() {
11199
+ return {
11200
+ name: this.name,
11201
+ description: this.description,
11202
+ inputSchema: this.inputSchema
11203
+ };
11204
+ }
11205
+ };
11206
+
10899
11207
  // src/core/router/provider-errors.ts
10900
11208
  var SYSTEMIC = /* @__PURE__ */ new Set([
10901
11209
  "rate_limit",
@@ -10960,7 +11268,7 @@ function classifyProviderError(err) {
10960
11268
  return { kind, systemic: isSystemicKind(kind), status, raw };
10961
11269
  }
10962
11270
  function describeProviderError(c, modelId) {
10963
- const on = ` on ${modelId}` ;
11271
+ const on = modelId ? ` on ${modelId}` : "";
10964
11272
  const detail = c.raw ? ` Provider said: ${c.raw}` : "";
10965
11273
  switch (c.kind) {
10966
11274
  case "rate_limit":
@@ -10982,6 +11290,417 @@ function describeProviderError(c, modelId) {
10982
11290
  }
10983
11291
  }
10984
11292
 
11293
+ // src/core/multimodal/generate.ts
11294
+ var DEFAULT_TIMEOUT_MS = 12e4;
11295
+ async function callProvider(fn, modelId) {
11296
+ try {
11297
+ return await fn();
11298
+ } catch (err) {
11299
+ const classified = classifyProviderError(err);
11300
+ throw new Error(describeProviderError(classified, modelId));
11301
+ }
11302
+ }
11303
+ async function postJson(url, body, headers, signal) {
11304
+ const ac = new AbortController();
11305
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11306
+ const onAbort = () => ac.abort();
11307
+ signal?.addEventListener("abort", onAbort);
11308
+ try {
11309
+ const res = await fetch(url, {
11310
+ method: "POST",
11311
+ headers: { "Content-Type": "application/json", ...headers },
11312
+ body: JSON.stringify(body),
11313
+ signal: ac.signal
11314
+ });
11315
+ if (!res.ok) {
11316
+ const text = await res.text().catch(() => "");
11317
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11318
+ }
11319
+ return res;
11320
+ } finally {
11321
+ clearTimeout(timer);
11322
+ signal?.removeEventListener("abort", onAbort);
11323
+ }
11324
+ }
11325
+ function baseUrlFor(cfg, fallback) {
11326
+ return (cfg.baseUrl ?? fallback).replace(/\/+$/, "");
11327
+ }
11328
+ function stamp(ext) {
11329
+ return `cascade-${Date.now()}.${ext}`;
11330
+ }
11331
+ async function generateImage(cap, cfg, req, signal) {
11332
+ return callProvider(async () => {
11333
+ if (cap.api === "openai-images") {
11334
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/images/generations`;
11335
+ const res = await postJson(url, {
11336
+ model: cap.modelId,
11337
+ prompt: req.prompt,
11338
+ size: req.size ?? "1024x1024",
11339
+ n: 1,
11340
+ // b64 rather than a URL: see the file header — provider URLs expire.
11341
+ response_format: "b64_json"
11342
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11343
+ const body = await res.json();
11344
+ const first = body.data?.[0];
11345
+ if (!first?.b64_json) throw new Error("Image response contained no image data.");
11346
+ return {
11347
+ data: Buffer.from(first.b64_json, "base64"),
11348
+ mimeType: "image/png",
11349
+ filename: stamp("png"),
11350
+ modelId: cap.modelId,
11351
+ provider: cap.provider,
11352
+ ...first.revised_prompt ? { revisedPrompt: first.revised_prompt } : {}
11353
+ };
11354
+ }
11355
+ if (cap.api === "gemini-predict") {
11356
+ const base = baseUrlFor(cfg, "https://generativelanguage.googleapis.com/v1beta");
11357
+ const url = `${base}/models/${encodeURIComponent(cap.modelId)}:predict`;
11358
+ const res = await postJson(url, {
11359
+ instances: [{ prompt: req.prompt }],
11360
+ parameters: { sampleCount: 1 }
11361
+ }, { "x-goog-api-key": cfg.apiKey ?? "" }, signal);
11362
+ const body = await res.json();
11363
+ const first = body.predictions?.[0];
11364
+ if (!first?.bytesBase64Encoded) throw new Error("Image response contained no image data.");
11365
+ const mimeType = first.mimeType ?? "image/png";
11366
+ return {
11367
+ data: Buffer.from(first.bytesBase64Encoded, "base64"),
11368
+ mimeType,
11369
+ filename: stamp(mimeType.includes("jpeg") ? "jpg" : "png"),
11370
+ modelId: cap.modelId,
11371
+ provider: cap.provider
11372
+ };
11373
+ }
11374
+ throw new Error(`${cap.modelId} is not an image generator.`);
11375
+ }, cap.modelId);
11376
+ }
11377
+ async function generateSpeech(cap, cfg, req, signal) {
11378
+ return callProvider(async () => {
11379
+ if (cap.api !== "openai-speech") throw new Error(`${cap.modelId} is not a speech model.`);
11380
+ const format = req.format ?? "mp3";
11381
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/speech`;
11382
+ const res = await postJson(url, {
11383
+ model: cap.modelId,
11384
+ input: req.text,
11385
+ voice: req.voice ?? "alloy",
11386
+ response_format: format
11387
+ }, { Authorization: `Bearer ${cfg.apiKey ?? ""}` }, signal);
11388
+ const mime = {
11389
+ mp3: "audio/mpeg",
11390
+ opus: "audio/opus",
11391
+ aac: "audio/aac",
11392
+ flac: "audio/flac",
11393
+ wav: "audio/wav"
11394
+ };
11395
+ return {
11396
+ data: Buffer.from(await res.arrayBuffer()),
11397
+ mimeType: mime[format] ?? "application/octet-stream",
11398
+ filename: stamp(format),
11399
+ modelId: cap.modelId,
11400
+ provider: cap.provider
11401
+ };
11402
+ }, cap.modelId);
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
+ }
11473
+ async function transcribeAudio(cap, cfg, req, signal) {
11474
+ return callProvider(async () => {
11475
+ if (cap.api !== "openai-transcriptions") throw new Error(`${cap.modelId} is not a transcription model.`);
11476
+ const url = `${baseUrlFor(cfg, "https://api.openai.com/v1")}/audio/transcriptions`;
11477
+ const form = new FormData();
11478
+ form.append("model", cap.modelId);
11479
+ if (req.language) form.append("language", req.language);
11480
+ form.append("file", new Blob([new Uint8Array(req.audio)]), req.filename);
11481
+ const ac = new AbortController();
11482
+ const timer = setTimeout(() => ac.abort(), DEFAULT_TIMEOUT_MS);
11483
+ const onAbort = () => ac.abort();
11484
+ signal?.addEventListener("abort", onAbort);
11485
+ try {
11486
+ const res = await fetch(url, {
11487
+ method: "POST",
11488
+ headers: { Authorization: `Bearer ${cfg.apiKey ?? ""}` },
11489
+ body: form,
11490
+ signal: ac.signal
11491
+ });
11492
+ if (!res.ok) {
11493
+ const text = await res.text().catch(() => "");
11494
+ throw Object.assign(new Error(text || res.statusText), { status: res.status });
11495
+ }
11496
+ const body = await res.json();
11497
+ return { text: body.text ?? "", modelId: cap.modelId };
11498
+ } finally {
11499
+ clearTimeout(timer);
11500
+ signal?.removeEventListener("abort", onAbort);
11501
+ }
11502
+ }, cap.modelId);
11503
+ }
11504
+
11505
+ // src/tools/generate-media.ts
11506
+ function resolve(deps, modality, preferModel) {
11507
+ const selected = deps.registry.select(modality, preferModel ? { preferModel } : {});
11508
+ if (!selected) {
11509
+ return { ok: false, error: `No ${modality} model is available on your configured providers.` };
11510
+ }
11511
+ const cfg = deps.lookupProvider(selected.capability.provider);
11512
+ if (!cfg) {
11513
+ return {
11514
+ ok: false,
11515
+ error: `${selected.capability.modelId} needs the ${selected.capability.provider} provider, which is not configured.`
11516
+ };
11517
+ }
11518
+ return { ok: true, selected, cfg };
11519
+ }
11520
+ var GenerateImageTool = class extends BaseTool {
11521
+ constructor(deps) {
11522
+ super();
11523
+ this.deps = deps;
11524
+ }
11525
+ deps;
11526
+ name = "generate_image";
11527
+ description = "Generate an image from a text description using the best available image model. Returns where the image was saved. Use when the user asks for a picture, illustration, diagram render, logo or any visual asset.";
11528
+ inputSchema = {
11529
+ type: "object",
11530
+ properties: {
11531
+ prompt: { type: "string", description: "What the image should show. Be specific about subject, style and composition." },
11532
+ size: { type: "string", description: 'Pixel size, e.g. "1024x1024" (default), "1792x1024", "1024x1792".' },
11533
+ model: { type: "string", description: "Optional: force a specific image model instead of the automatic choice." }
11534
+ },
11535
+ required: ["prompt"]
11536
+ };
11537
+ async execute(input, options) {
11538
+ const prompt = String(input["prompt"] ?? "").trim();
11539
+ if (!prompt) return 'Provide a "prompt" describing the image.';
11540
+ const r = resolve(this.deps, "image", input["model"] ? String(input["model"]) : void 0);
11541
+ if (!r.ok) return r.error;
11542
+ const size = input["size"] ? String(input["size"]) : void 0;
11543
+ const asset = await generateImage(
11544
+ r.selected.capability,
11545
+ r.cfg,
11546
+ { prompt, ...size ? { size } : {} },
11547
+ options.signal
11548
+ );
11549
+ const location = await this.deps.sink(asset);
11550
+ return [
11551
+ `Image generated and saved to ${location}.`,
11552
+ `Model: ${asset.modelId} \u2014 ${r.selected.reason}`,
11553
+ // DALL·E rewrites the prompt before drawing; the model needs to know what
11554
+ // was actually rendered, not what it asked for, or its description of the
11555
+ // result will not match the image.
11556
+ asset.revisedPrompt ? `The provider rewrote the prompt to: ${asset.revisedPrompt}` : ""
11557
+ ].filter(Boolean).join("\n");
11558
+ }
11559
+ };
11560
+ var GenerateSpeechTool = class extends BaseTool {
11561
+ constructor(deps) {
11562
+ super();
11563
+ this.deps = deps;
11564
+ }
11565
+ deps;
11566
+ name = "generate_speech";
11567
+ description = "Turn text into spoken audio using the best available text-to-speech model. Returns where the audio was saved. Use when the user asks for narration, a voiceover or an audio version of some text.";
11568
+ inputSchema = {
11569
+ type: "object",
11570
+ properties: {
11571
+ text: { type: "string", description: "The text to speak." },
11572
+ voice: { type: "string", description: "Voice id: alloy, echo, fable, onyx, nova, shimmer. Default alloy." },
11573
+ format: { type: "string", description: "Audio format: mp3 (default), opus, aac, flac, wav." },
11574
+ model: { type: "string", description: "Optional: force a specific speech model." }
11575
+ },
11576
+ required: ["text"]
11577
+ };
11578
+ async execute(input, options) {
11579
+ const text = String(input["text"] ?? "").trim();
11580
+ if (!text) return 'Provide "text" to speak.';
11581
+ const r = resolve(this.deps, "speech", input["model"] ? String(input["model"]) : void 0);
11582
+ if (!r.ok) return r.error;
11583
+ const fmt = String(input["format"] ?? "mp3");
11584
+ const allowed = ["mp3", "opus", "aac", "flac", "wav"];
11585
+ const format = allowed.includes(fmt) ? fmt : "mp3";
11586
+ const asset = await generateSpeech(
11587
+ r.selected.capability,
11588
+ r.cfg,
11589
+ { text, voice: input["voice"] ? String(input["voice"]) : void 0, format },
11590
+ options.signal
11591
+ );
11592
+ const location = await this.deps.sink(asset);
11593
+ return `Speech generated and saved to ${location}.
11594
+ Model: ${asset.modelId} \u2014 ${r.selected.reason}`;
11595
+ }
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
+ };
11649
+ var TranscribeAudioTool = class extends BaseTool {
11650
+ constructor(deps, readFile) {
11651
+ super();
11652
+ this.deps = deps;
11653
+ this.readFile = readFile;
11654
+ }
11655
+ deps;
11656
+ readFile;
11657
+ name = "transcribe_audio";
11658
+ description = "Transcribe a local audio file to text using the best available speech-to-text model. Use when the user supplies audio and wants its contents in writing.";
11659
+ inputSchema = {
11660
+ type: "object",
11661
+ properties: {
11662
+ path: { type: "string", description: "Path to the audio file to transcribe." },
11663
+ language: { type: "string", description: 'Optional ISO-639-1 language hint, e.g. "en", improves accuracy.' },
11664
+ model: { type: "string", description: "Optional: force a specific transcription model." }
11665
+ },
11666
+ required: ["path"]
11667
+ };
11668
+ async execute(input, options) {
11669
+ const filePath = String(input["path"] ?? "").trim();
11670
+ if (!filePath) return 'Provide a "path" to the audio file.';
11671
+ const r = resolve(this.deps, "transcription", input["model"] ? String(input["model"]) : void 0);
11672
+ if (!r.ok) return r.error;
11673
+ let audio;
11674
+ try {
11675
+ audio = await this.readFile(filePath);
11676
+ } catch {
11677
+ return `Could not read the audio file at ${filePath}.`;
11678
+ }
11679
+ const { text, modelId } = await transcribeAudio(
11680
+ r.selected.capability,
11681
+ r.cfg,
11682
+ {
11683
+ audio,
11684
+ filename: filePath.split(/[\\/]/).pop() || "audio",
11685
+ ...input["language"] ? { language: String(input["language"]) } : {}
11686
+ },
11687
+ options.signal
11688
+ );
11689
+ return text ? `Transcript (${modelId}):
11690
+
11691
+ ${text}` : `${modelId} returned an empty transcript \u2014 the file may be silent or in an unsupported format.`;
11692
+ }
11693
+ };
11694
+ function buildMediaTools(deps, readFile) {
11695
+ const tools = [];
11696
+ const modalities = new Set(deps.registry.availableModalities());
11697
+ if (modalities.has("image")) tools.push(new GenerateImageTool(deps));
11698
+ if (modalities.has("speech")) tools.push(new GenerateSpeechTool(deps));
11699
+ if (modalities.has("video")) tools.push(new GenerateVideoTool(deps));
11700
+ if (modalities.has("transcription") && readFile) tools.push(new TranscribeAudioTool(deps, readFile));
11701
+ return tools;
11702
+ }
11703
+
10985
11704
  // src/core/run-breaker.ts
10986
11705
  var DEFAULT_FAILURE_THRESHOLD = 3;
10987
11706
  var RunBreaker = class {
@@ -11050,26 +11769,6 @@ var RunBreaker = class {
11050
11769
 
11051
11770
  // src/tools/registry.ts
11052
11771
  init_constants();
11053
-
11054
- // src/tools/base.ts
11055
- var BaseTool = class {
11056
- workspaceRoot = process.cwd();
11057
- setWorkspaceRoot(root) {
11058
- this.workspaceRoot = root;
11059
- }
11060
- isDangerous() {
11061
- return false;
11062
- }
11063
- getDefinition() {
11064
- return {
11065
- name: this.name,
11066
- description: this.description,
11067
- inputSchema: this.inputSchema
11068
- };
11069
- }
11070
- };
11071
-
11072
- // src/tools/shell.ts
11073
11772
  var execAsync = util.promisify(child_process.exec);
11074
11773
  var ShellTool = class extends BaseTool {
11075
11774
  name = "shell";
@@ -11765,7 +12464,7 @@ var PDFCreateTool = class extends BaseTool {
11765
12464
  if (!fs29__default.default.existsSync(dir)) {
11766
12465
  fs29__default.default.mkdirSync(dir, { recursive: true });
11767
12466
  }
11768
- return new Promise((resolve, reject) => {
12467
+ return new Promise((resolve2, reject) => {
11769
12468
  try {
11770
12469
  const doc = new PDFDocument__default.default({ margin: 50 });
11771
12470
  const stream = fs29__default.default.createWriteStream(filePath);
@@ -11783,7 +12482,7 @@ var PDFCreateTool = class extends BaseTool {
11783
12482
  });
11784
12483
  doc.end();
11785
12484
  stream.on("finish", () => {
11786
- resolve(`Successfully created PDF at ${filePath} (${content.length} characters)`);
12485
+ resolve2(`Successfully created PDF at ${filePath} (${content.length} characters)`);
11787
12486
  });
11788
12487
  stream.on("error", (err) => {
11789
12488
  reject(new Error(`Failed to write PDF: ${err.message}`));
@@ -11855,7 +12554,7 @@ var CodeInterpreterTool = class extends BaseTool {
11855
12554
  const filePath = path32__default.default.join(tmpDir, fileName);
11856
12555
  fs29__default.default.writeFileSync(filePath, code, "utf-8");
11857
12556
  const execArgs = [filePath, ...args];
11858
- return new Promise((resolve) => {
12557
+ return new Promise((resolve2) => {
11859
12558
  const startMs = Date.now();
11860
12559
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
11861
12560
  const duration = Date.now() - startMs;
@@ -11869,17 +12568,17 @@ var CodeInterpreterTool = class extends BaseTool {
11869
12568
  if (error) {
11870
12569
  const timedOut = error.killed && duration >= 3e4;
11871
12570
  if (timedOut) {
11872
- resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
12571
+ resolve2(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
11873
12572
  Partial stdout: ${stdout}
11874
12573
  Stderr: ${stderr}`);
11875
12574
  } else {
11876
- resolve(`Execution failed (${duration}ms):
12575
+ resolve2(`Execution failed (${duration}ms):
11877
12576
  Error: ${error.message}
11878
12577
  Stderr: ${stderr}
11879
12578
  Stdout: ${stdout}`);
11880
12579
  }
11881
12580
  } else {
11882
- resolve(`Execution successful (${duration}ms):
12581
+ resolve2(`Execution successful (${duration}ms):
11883
12582
  Stdout: ${stdout}
11884
12583
  Stderr: ${stderr}`);
11885
12584
  }
@@ -12533,7 +13232,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
12533
13232
  */
12534
13233
  waitForTool(toolName, timeoutMs = 6e4) {
12535
13234
  if (this.tools.has(toolName)) return Promise.resolve();
12536
- return new Promise((resolve, reject) => {
13235
+ return new Promise((resolve2, reject) => {
12537
13236
  const timer = setTimeout(() => {
12538
13237
  this.off("tool:added", handler);
12539
13238
  reject(new Error(`Timeout waiting for tool: ${toolName}`));
@@ -12542,7 +13241,7 @@ var ToolRegistry = class extends EventEmitter__default.default {
12542
13241
  if (name === toolName) {
12543
13242
  clearTimeout(timer);
12544
13243
  this.off("tool:added", handler);
12545
- resolve();
13244
+ resolve2();
12546
13245
  }
12547
13246
  };
12548
13247
  this.on("tool:added", handler);
@@ -13195,20 +13894,20 @@ var PermissionEscalator = class extends EventEmitter__default.default {
13195
13894
  resolver(decision);
13196
13895
  }
13197
13896
  waitForUserDecision(req) {
13198
- return new Promise((resolve) => {
13897
+ return new Promise((resolve2) => {
13199
13898
  let timer;
13200
13899
  const wrappedResolver = (decision) => {
13201
13900
  if (timer) clearTimeout(timer);
13202
13901
  if (decision.always) {
13203
13902
  this.taskWideCache.set(req.toolName, decision.approved);
13204
13903
  }
13205
- resolve(decision);
13904
+ resolve2(decision);
13206
13905
  };
13207
13906
  this.pendingUserDecisions.set(req.id, wrappedResolver);
13208
13907
  if (this.approvalTimeoutMs > 0 && Number.isFinite(this.approvalTimeoutMs)) {
13209
13908
  timer = setTimeout(() => {
13210
13909
  if (this.pendingUserDecisions.delete(req.id)) {
13211
- resolve({
13910
+ resolve2({
13212
13911
  requestId: req.id,
13213
13912
  approved: false,
13214
13913
  decidedBy: "USER",
@@ -13276,6 +13975,23 @@ var noopTelemetry = {
13276
13975
  }
13277
13976
  };
13278
13977
 
13978
+ // src/core/router/feedback-prior.ts
13979
+ var MAX_FEEDBACK_ADJUSTMENT = 0.05;
13980
+ var PRIOR_STRENGTH = 10;
13981
+ function feedbackAdjustment(counts) {
13982
+ const good = Math.max(0, Math.floor(counts.good));
13983
+ const bad = Math.max(0, Math.floor(counts.bad));
13984
+ const n = good + bad;
13985
+ if (n === 0) return 0;
13986
+ const half = PRIOR_STRENGTH / 2;
13987
+ const shrunk = (good + half) / (n + PRIOR_STRENGTH);
13988
+ return (shrunk - 0.5) * 2 * MAX_FEEDBACK_ADJUSTMENT;
13989
+ }
13990
+ function applyFeedback(score01, counts) {
13991
+ const adjusted = score01 + feedbackAdjustment(counts);
13992
+ return Math.max(0, Math.min(1, adjusted));
13993
+ }
13994
+
13279
13995
  // src/core/router/task-analyzer.ts
13280
13996
  init_pricing();
13281
13997
  var CODE_SIGNALS = [
@@ -13351,6 +14067,8 @@ var TASK_TYPE_TAGS = {
13351
14067
  };
13352
14068
  var TaskAnalyzer = class {
13353
14069
  tracker;
14070
+ /** Per-model thumbs counts, when the host collects them. */
14071
+ feedback;
13354
14072
  bias;
13355
14073
  lastProfile = null;
13356
14074
  lastSelectedModels = /* @__PURE__ */ new Map();
@@ -13358,6 +14076,14 @@ var TaskAnalyzer = class {
13358
14076
  this.tracker = tracker;
13359
14077
  this.bias = bias;
13360
14078
  }
14079
+ /**
14080
+ * Supply per-model rating counts. Optional by design: a host that collects no
14081
+ * feedback (the CLI, a fresh install) scores exactly as it did before, since
14082
+ * feedbackAdjustment returns 0 for an empty record.
14083
+ */
14084
+ setFeedbackSource(source) {
14085
+ this.feedback = source;
14086
+ }
13361
14087
  setTracker(tracker) {
13362
14088
  this.tracker = tracker;
13363
14089
  }
@@ -13441,7 +14167,10 @@ var TaskAnalyzer = class {
13441
14167
  const perf = this.tracker?.performanceScore(model.id, profile.type) ?? 0.5;
13442
14168
  const costEff = this.costEfficiency(model, profile.complexity);
13443
14169
  const match = this.taskMatchScore(model, profile);
13444
- const benchmark = 0.3 + 0.7 * benchmarkScore01(model, profile.type);
14170
+ const rated = this.feedback?.(model.id);
14171
+ const publicScore = benchmarkScore01(model, profile.type);
14172
+ const adjusted = rated ? applyFeedback(publicScore, rated) : publicScore;
14173
+ const benchmark = 0.3 + 0.7 * adjusted;
13445
14174
  switch (this.bias) {
13446
14175
  case "quality":
13447
14176
  return perf * match * benchmark ** 2 * (0.85 + 0.15 * costEff);
@@ -13846,10 +14575,10 @@ var DynamicTool = class extends BaseTool {
13846
14575
  `;
13847
14576
  const script = await isolate.compileScript(bootstrap);
13848
14577
  const runPromise = script.run(context, { timeout: timeoutMs, promise: true }).then((v) => String(v ?? ""));
13849
- const timeoutPromise = new Promise((resolve) => {
14578
+ const timeoutPromise = new Promise((resolve2) => {
13850
14579
  const t = setTimeout(() => {
13851
14580
  dispose();
13852
- resolve(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
14581
+ resolve2(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`);
13853
14582
  }, timeoutMs + 500);
13854
14583
  t.unref?.();
13855
14584
  });
@@ -13867,7 +14596,7 @@ var DynamicTool = class extends BaseTool {
13867
14596
  /** Spawn the worker, service its callTool/fetch bridge, enforce the kill timeout. */
13868
14597
  runInWorker(input, callTool) {
13869
14598
  const timeoutMs = Math.max(200, Number(process.env["CASCADE_DYNAMIC_TOOL_TIMEOUT_MS"]) || DYNAMIC_TOOL_TIMEOUT_MS);
13870
- return new Promise((resolve) => {
14599
+ return new Promise((resolve2) => {
13871
14600
  let settled = false;
13872
14601
  const worker = new worker_threads.Worker(HARNESS_SRC, {
13873
14602
  eval: true,
@@ -13879,7 +14608,7 @@ var DynamicTool = class extends BaseTool {
13879
14608
  settled = true;
13880
14609
  clearTimeout(timer);
13881
14610
  void worker.terminate();
13882
- resolve(value);
14611
+ resolve2(value);
13883
14612
  };
13884
14613
  const timer = setTimeout(
13885
14614
  () => finish(`Dynamic tool "${this.name}" timed out after ${timeoutMs}ms and was terminated.`),
@@ -15386,6 +16115,8 @@ ${prompt}`;
15386
16115
  var Cascade = class _Cascade extends EventEmitter__default.default {
15387
16116
  router;
15388
16117
  toolRegistry;
16118
+ multimodal;
16119
+ mediaSink;
15389
16120
  mcpClient;
15390
16121
  codeIndex;
15391
16122
  config;
@@ -15424,6 +16155,10 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15424
16155
  }
15425
16156
  });
15426
16157
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
16158
+ this.multimodal = new MultimodalRegistry(
16159
+ (this.config.providers ?? []).map((p) => p.type)
16160
+ );
16161
+ this.registerMediaTools(workspacePath);
15427
16162
  this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
15428
16163
  this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
15429
16164
  this.router.setWorldStateDB(this.worldStateDB);
@@ -15477,14 +16212,14 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15477
16212
  if (this.listenerCount("mcp:approval-required") === 0) {
15478
16213
  return false;
15479
16214
  }
15480
- return await new Promise((resolve) => {
15481
- this.pendingMcpApprovals.set(server.name, resolve);
16215
+ return await new Promise((resolve2) => {
16216
+ this.pendingMcpApprovals.set(server.name, resolve2);
15482
16217
  const timeout = setTimeout(() => {
15483
- if (this.pendingMcpApprovals.delete(server.name)) resolve(false);
16218
+ if (this.pendingMcpApprovals.delete(server.name)) resolve2(false);
15484
16219
  }, 3e4);
15485
16220
  const wrap = (approved) => {
15486
16221
  clearTimeout(timeout);
15487
- resolve(approved);
16222
+ resolve2(approved);
15488
16223
  };
15489
16224
  this.pendingMcpApprovals.set(server.name, wrap);
15490
16225
  this.emit("mcp:approval-required", { server });
@@ -15558,17 +16293,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15558
16293
  }
15559
16294
  const t2Count = plan.sections.length;
15560
16295
  const t3Count = plan.sections.reduce((sum, s) => sum + (s.t3Subtasks?.length ?? 0), 0);
15561
- return await new Promise((resolve) => {
16296
+ return await new Promise((resolve2) => {
15562
16297
  const timeout = setTimeout(() => {
15563
16298
  if (this.pendingPlanApproval) {
15564
16299
  this.pendingPlanApproval = void 0;
15565
- resolve({ approved: true });
16300
+ resolve2({ approved: true });
15566
16301
  }
15567
16302
  }, 12e4);
15568
16303
  this.pendingPlanApproval = (decision) => {
15569
16304
  clearTimeout(timeout);
15570
16305
  this.pendingPlanApproval = void 0;
15571
- resolve(decision);
16306
+ resolve2(decision);
15572
16307
  };
15573
16308
  this.emit("plan:approval-required", {
15574
16309
  taskId,
@@ -15601,17 +16336,17 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
15601
16336
  }
15602
16337
  async requestContextApproval(info) {
15603
16338
  if (this.listenerCount("context:approval-required") === 0) return true;
15604
- return await new Promise((resolve) => {
16339
+ return await new Promise((resolve2) => {
15605
16340
  const timeout = setTimeout(() => {
15606
16341
  if (this.pendingContextApproval) {
15607
16342
  this.pendingContextApproval = void 0;
15608
- resolve(true);
16343
+ resolve2(true);
15609
16344
  }
15610
16345
  }, 12e4);
15611
16346
  this.pendingContextApproval = (approved) => {
15612
16347
  clearTimeout(timeout);
15613
16348
  this.pendingContextApproval = void 0;
15614
- resolve(approved);
16349
+ resolve2(approved);
15615
16350
  };
15616
16351
  this.emit("context:approval-required", info);
15617
16352
  });
@@ -15691,6 +16426,68 @@ ${input}` }],
15691
16426
  if (this.store) t1.setStore(this.store);
15692
16427
  return t1.previewPlan(prompt);
15693
16428
  }
16429
+ /**
16430
+ * Register image / speech / transcription tools for the modalities this
16431
+ * config can actually serve.
16432
+ *
16433
+ * The sink writes bytes into the workspace, because that is what a desktop or
16434
+ * CLI run wants and it keeps a generated asset indistinguishable from any
16435
+ * other file the run produced. A host with different storage (the cloud
16436
+ * server stores a file row) constructs the tools itself via buildMediaTools
16437
+ * with its own sink.
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
+ }
16452
+ registerMediaTools(workspacePath) {
16453
+ const denied = new Set(this.config.tools?.disabledTools ?? []);
16454
+ const permitted = (name) => !denied.has(name);
16455
+ const tools = buildMediaTools(
16456
+ {
16457
+ registry: this.multimodal,
16458
+ lookupProvider: (provider) => (this.config.providers ?? []).find((p) => p.type === provider),
16459
+ sink: this.mediaSink ?? (async (asset) => {
16460
+ const fsp = await import('fs/promises');
16461
+ const nodePath = await import('path');
16462
+ const dir = nodePath.join(workspacePath, "generated");
16463
+ await fsp.mkdir(dir, { recursive: true });
16464
+ const dest = nodePath.join(dir, asset.filename);
16465
+ await fsp.writeFile(dest, asset.data);
16466
+ return nodePath.relative(workspacePath, dest);
16467
+ })
16468
+ },
16469
+ async (p) => {
16470
+ const fsp = await import('fs/promises');
16471
+ return fsp.readFile(p);
16472
+ }
16473
+ );
16474
+ for (const tool of tools) {
16475
+ if (!permitted(tool.name)) continue;
16476
+ tool.setWorkspaceRoot(workspacePath);
16477
+ this.toolRegistry.register(tool);
16478
+ }
16479
+ }
16480
+ /**
16481
+ * Supply per-model thumbs-up/down counts so Auto routing can weigh how models
16482
+ * have actually performed for this user, on top of public benchmarks.
16483
+ *
16484
+ * Optional everywhere: with no source, or an empty record, scoring is
16485
+ * byte-identical to before. The adjustment is capped and sample-size shrunk —
16486
+ * see core/router/feedback-prior.ts.
16487
+ */
16488
+ setFeedbackSource(source) {
16489
+ this.router.setFeedbackSource(source);
16490
+ }
15694
16491
  /** True when a task stopped at the budget cap and can be resumed via /continue. */
15695
16492
  hasResumableRun() {
15696
16493
  return this.lastInterruptedRun != null;
@@ -16180,7 +16977,7 @@ ${prompt}` : prompt;
16180
16977
  if (toolCreator) toolCreator.setPermissionEscalator(escalator);
16181
16978
  const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
16182
16979
  let finalOutput = "";
16183
- const runBreaker = new RunBreaker();
16980
+ const runBreaker = new RunBreaker(this.config.budget?.failureThreshold);
16184
16981
  let t2Results = [];
16185
16982
  let runError = null;
16186
16983
  let identityPrompt = "";
@@ -16671,7 +17468,7 @@ var CloudClient = class _CloudClient {
16671
17468
  const deadline = Date.now() + start.expiresIn * 1e3;
16672
17469
  while (Date.now() < deadline) {
16673
17470
  if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
16674
- await sleep(interval * 1e3, opts.signal);
17471
+ await sleep2(interval * 1e3, opts.signal);
16675
17472
  const r = await this.pollDevice(start.deviceCode);
16676
17473
  if (r === "slow_down") {
16677
17474
  interval += 5;
@@ -16882,9 +17679,9 @@ var CloudClient = class _CloudClient {
16882
17679
  this.store.clear();
16883
17680
  }
16884
17681
  };
16885
- function sleep(ms, signal) {
16886
- return new Promise((resolve, reject) => {
16887
- const t = setTimeout(resolve, ms);
17682
+ function sleep2(ms, signal) {
17683
+ return new Promise((resolve2, reject) => {
17684
+ const t = setTimeout(resolve2, ms);
16888
17685
  signal?.addEventListener("abort", () => {
16889
17686
  clearTimeout(t);
16890
17687
  reject(new Error("aborted"));
@@ -18676,8 +19473,8 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
18676
19473
  onExit: () => exit(),
18677
19474
  onThemeChange: (name) => setTheme(getTheme(name)),
18678
19475
  onExport: async (fmt) => {
18679
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
18680
- const exportPath = path32__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
19476
+ const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
19477
+ const exportPath = path32__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
18681
19478
  if (fmt === "json") {
18682
19479
  await fs10__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
18683
19480
  } else {
@@ -19109,8 +19906,8 @@ ${lastUser.content}`;
19109
19906
  conversationHistory: toConversationHistory(state.messages),
19110
19907
  approvalCallback: async (req) => {
19111
19908
  dispatch({ type: "SET_APPROVAL", request: req });
19112
- return new Promise((resolve) => {
19113
- approvalResolverRef.current = resolve;
19909
+ return new Promise((resolve2) => {
19910
+ approvalResolverRef.current = resolve2;
19114
19911
  });
19115
19912
  }
19116
19913
  });
@@ -20376,13 +21173,13 @@ function SetupWizard({ workspacePath, onComplete }) {
20376
21173
  }
20377
21174
  async function runSetupWizard(workspacePath) {
20378
21175
  printTelemetryBanner();
20379
- return new Promise((resolve) => {
21176
+ return new Promise((resolve2) => {
20380
21177
  const { unmount } = ink.render(
20381
21178
  React2__default.default.createElement(SetupWizard, {
20382
21179
  workspacePath,
20383
21180
  onComplete: (config) => {
20384
21181
  unmount();
20385
- resolve(config);
21182
+ resolve2(config);
20386
21183
  }
20387
21184
  }),
20388
21185
  { exitOnCtrlC: true }
@@ -20850,7 +21647,7 @@ function notSignedIn() {
20850
21647
  process.exitCode = 1;
20851
21648
  }
20852
21649
  function promptHidden(question) {
20853
- return new Promise((resolve) => {
21650
+ return new Promise((resolve2) => {
20854
21651
  const rl = readline__default.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
20855
21652
  let masked = false;
20856
21653
  rl._writeToOutput = (s) => {
@@ -20859,7 +21656,7 @@ function promptHidden(question) {
20859
21656
  rl.question(question, (answer) => {
20860
21657
  rl.close();
20861
21658
  process.stdout.write("\n");
20862
- resolve(answer.trim());
21659
+ resolve2(answer.trim());
20863
21660
  });
20864
21661
  masked = true;
20865
21662
  });
@@ -21786,12 +22583,12 @@ var DashboardServer = class {
21786
22583
  console.warn("\u26A0 Dashboard auth is DISABLED while bound to a non-loopback interface \u2014 this allows unauthenticated remote task execution.");
21787
22584
  }
21788
22585
  }
21789
- await new Promise((resolve, reject) => {
22586
+ await new Promise((resolve2, reject) => {
21790
22587
  const onError = (err) => reject(err);
21791
22588
  this.httpServer.once("error", onError);
21792
22589
  this.httpServer.listen(this.port, this.host, () => {
21793
22590
  this.httpServer.off("error", onError);
21794
- resolve();
22591
+ resolve2();
21795
22592
  });
21796
22593
  });
21797
22594
  try {
@@ -21815,8 +22612,8 @@ var DashboardServer = class {
21815
22612
  } catch {
21816
22613
  }
21817
22614
  this.globalStore = null;
21818
- await new Promise((resolve, reject) => {
21819
- this.httpServer.close((err) => err ? reject(err) : resolve());
22615
+ await new Promise((resolve2, reject) => {
22616
+ this.httpServer.close((err) => err ? reject(err) : resolve2());
21820
22617
  });
21821
22618
  }
21822
22619
  getSocket() {
@@ -22013,8 +22810,8 @@ var DashboardServer = class {
22013
22810
  * timeout denies it.
22014
22811
  */
22015
22812
  makeApprovalCallback(sessionId) {
22016
- return (request) => new Promise((resolve) => {
22017
- this.pendingApprovals.set(request.id, { resolve, sessionId });
22813
+ return (request) => new Promise((resolve2) => {
22814
+ this.pendingApprovals.set(request.id, { resolve: resolve2, sessionId });
22018
22815
  });
22019
22816
  }
22020
22817
  /**