cascade-ai 0.73.0 → 0.75.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,20 +1,20 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
- import OpenAI, { AzureOpenAI } from 'openai';
3
- import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
4
1
  import dns from 'dns';
5
2
  import http, { createServer } from 'http';
6
3
  import https from 'https';
7
4
  import zlib from 'zlib';
8
5
  import { Readable } from 'stream';
6
+ import Anthropic from '@anthropic-ai/sdk';
7
+ import OpenAI, { AzureOpenAI } from 'openai';
8
+ import { HarmBlockThreshold, HarmCategory, GoogleGenAI } from '@google/genai';
9
9
  import Database2 from 'better-sqlite3';
10
- import path34, { dirname } from 'path';
11
- import fs31, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
10
+ import path35, { dirname } from 'path';
11
+ import fs32, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
12
12
  import os10 from 'os';
13
13
  import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
14
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
15
15
  import { Command } from 'commander';
16
16
  import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
17
- import chalk7 from 'chalk';
17
+ import chalk8 from 'chalk';
18
18
  import dotenv from 'dotenv';
19
19
  import { fileURLToPath } from 'url';
20
20
  import fs11 from 'fs/promises';
@@ -1766,7 +1766,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1766
1766
  var init_constants = __esm({
1767
1767
  "src/constants.ts"() {
1768
1768
  init_pricing();
1769
- CASCADE_VERSION = "0.73.0";
1769
+ CASCADE_VERSION = "0.75.0";
1770
1770
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1771
1771
  CASCADE_DB_FILE = ".cascade/memory.db";
1772
1772
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2195,6 +2195,244 @@ var init_constants = __esm({
2195
2195
  ];
2196
2196
  }
2197
2197
  });
2198
+ function preferIpv4Host(url) {
2199
+ return url;
2200
+ }
2201
+ function stripTrailingSlashes(value) {
2202
+ let end = value.length;
2203
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
2204
+ return end === value.length ? value : value.slice(0, end);
2205
+ }
2206
+ function normalizeEndpoint(url) {
2207
+ const raw = stripTrailingSlashes((url ?? "").trim());
2208
+ if (!raw) return "";
2209
+ try {
2210
+ const u = new URL(raw);
2211
+ const path40 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2212
+ return `${u.protocol}//${u.host.toLowerCase()}${path40}`;
2213
+ } catch {
2214
+ return raw.toLowerCase();
2215
+ }
2216
+ }
2217
+ function stripPathSegment(url, segment) {
2218
+ const trimmed = stripTrailingSlashes(url.trim());
2219
+ const suffix = `/${segment}`;
2220
+ return trimmed.endsWith(suffix) ? trimmed.slice(0, -suffix.length) : trimmed;
2221
+ }
2222
+ function clientApiRoot(type, url) {
2223
+ const segment = CLIENT_VERSION_SEGMENT[type];
2224
+ return segment ? stripPathSegment(url, segment) : stripTrailingSlashes(url.trim());
2225
+ }
2226
+ function headersFrom(raw) {
2227
+ const out = new Headers();
2228
+ for (const [k, v] of Object.entries(raw)) {
2229
+ if (k === "content-encoding" || k === "content-length") continue;
2230
+ if (Array.isArray(v)) out.set(k, v.join(", "));
2231
+ else if (typeof v === "string") out.set(k, v);
2232
+ }
2233
+ return out;
2234
+ }
2235
+ async function fetchSameOrigin(url, init = {}, maxRedirects = MAX_REDIRECTS) {
2236
+ const origin = new URL(url).origin;
2237
+ let current = url;
2238
+ let options = { ...init };
2239
+ for (let hop = 0; hop <= maxRedirects; hop++) {
2240
+ const res = await fetch(current, { ...options, redirect: "manual" });
2241
+ if (![301, 302, 303, 307, 308].includes(res.status)) return res;
2242
+ const location = res.headers.get("location");
2243
+ if (!location) return res;
2244
+ await res.body?.cancel().catch(() => {
2245
+ });
2246
+ const next = new URL(location, current);
2247
+ if (next.origin !== origin) {
2248
+ throw new Error(
2249
+ `Refusing cross-origin redirect to ${next.origin}: the request carries a credential for ${origin}.`
2250
+ );
2251
+ }
2252
+ const method = (options.method ?? "GET").toUpperCase();
2253
+ const downgrades = res.status === 303 ? method !== "GET" && method !== "HEAD" : (res.status === 301 || res.status === 302) && method === "POST";
2254
+ if (downgrades) {
2255
+ const headers = new Headers(options.headers);
2256
+ for (const h of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2257
+ headers.delete(h);
2258
+ }
2259
+ options = { ...options, method: "GET", body: void 0, headers };
2260
+ }
2261
+ current = next.toString();
2262
+ }
2263
+ throw new Error(`Too many redirects from ${url}`);
2264
+ }
2265
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
2266
+ const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2267
+ const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
2268
+ const lib = u.protocol === "https:" ? https : http;
2269
+ const method = (init.method ?? "GET").toUpperCase();
2270
+ const headers = {};
2271
+ const h = init.headers;
2272
+ if (h instanceof Headers) h.forEach((v, k) => {
2273
+ headers[k] = v;
2274
+ });
2275
+ else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
2276
+ else if (h) Object.assign(headers, h);
2277
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
2278
+ headers["accept-encoding"] = "gzip, deflate, br";
2279
+ }
2280
+ const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
2281
+ return new Promise((resolve2, reject) => {
2282
+ const req = lib.request(
2283
+ {
2284
+ hostname: u.hostname,
2285
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
2286
+ path: u.pathname + u.search,
2287
+ method,
2288
+ headers
2289
+ },
2290
+ (res) => {
2291
+ const status = res.statusCode ?? 200;
2292
+ const location = res.headers.location;
2293
+ if (REDIRECT_STATUSES.has(status) && location && redirectCount < MAX_REDIRECTS) {
2294
+ res.resume();
2295
+ const nextUrl = new URL(location, u).href;
2296
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
2297
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
2298
+ return;
2299
+ }
2300
+ const downgrade = status === 303 ? method !== "GET" && method !== "HEAD" : (status === 301 || status === 302) && method === "POST";
2301
+ let nextInit = init;
2302
+ if (downgrade) {
2303
+ const headers2 = new Headers(init.headers);
2304
+ for (const h2 of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2305
+ headers2.delete(h2);
2306
+ }
2307
+ nextInit = { ...init, method: "GET", body: void 0, headers: headers2 };
2308
+ }
2309
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
2310
+ return;
2311
+ }
2312
+ const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
2313
+ let bodyStream = res;
2314
+ if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib.createGunzip());
2315
+ else if (encoding === "deflate") bodyStream = res.pipe(zlib.createInflate());
2316
+ else if (encoding === "br") bodyStream = res.pipe(zlib.createBrotliDecompress());
2317
+ if (NULL_BODY_STATUSES.has(status)) {
2318
+ bodyStream.resume();
2319
+ resolve2(new Response(null, {
2320
+ status,
2321
+ statusText: res.statusMessage ?? "",
2322
+ headers: headersFrom(res.headers)
2323
+ }));
2324
+ return;
2325
+ }
2326
+ const stream = Readable.toWeb(bodyStream);
2327
+ resolve2(new Response(stream, {
2328
+ status,
2329
+ statusText: res.statusMessage ?? "",
2330
+ headers: headersFrom(res.headers)
2331
+ }));
2332
+ }
2333
+ );
2334
+ req.on("error", reject);
2335
+ req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
2336
+ if (body !== void 0) req.write(body);
2337
+ req.end();
2338
+ });
2339
+ }
2340
+ var CLIENT_VERSION_SEGMENT, REDIRECT_STATUSES, NULL_BODY_STATUSES, MAX_REDIRECTS;
2341
+ var init_net = __esm({
2342
+ "src/utils/net.ts"() {
2343
+ try {
2344
+ dns.setDefaultResultOrder("ipv4first");
2345
+ } catch {
2346
+ }
2347
+ CLIENT_VERSION_SEGMENT = {
2348
+ anthropic: "v1",
2349
+ gemini: "v1beta"
2350
+ };
2351
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
2352
+ NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
2353
+ MAX_REDIRECTS = 5;
2354
+ }
2355
+ });
2356
+
2357
+ // src/config/revoked-credentials.ts
2358
+ function isRevokedSubscriptionCredential(p) {
2359
+ if (typeof p !== "object" || p === null) return false;
2360
+ if (p.type !== "anthropic") return false;
2361
+ if (!p.authToken && !p.apiKey) return false;
2362
+ return isSubscriptionToken(p.authToken) || isSubscriptionToken(p.apiKey) || Boolean(p.authToken) && /claude\s*code/i.test(p.credentialSource ?? "");
2363
+ }
2364
+ function isSubscriptionToken(secret) {
2365
+ return !!secret && secret.trim().startsWith("sk-ant-oat");
2366
+ }
2367
+ function stripRevokedCredentials(providers, { keepForEndpoint = true } = {}) {
2368
+ let removed = 0;
2369
+ const kept = [];
2370
+ for (const p of providers) {
2371
+ if (!isRevokedSubscriptionCredential(p)) {
2372
+ kept.push(p);
2373
+ continue;
2374
+ }
2375
+ removed++;
2376
+ const cleaned = { ...p };
2377
+ if (isSubscriptionToken(cleaned.apiKey)) delete cleaned.apiKey;
2378
+ const bearerIsDead = isSubscriptionToken(cleaned.authToken) || Boolean(cleaned.authToken) && /claude\s*code/i.test(cleaned.credentialSource ?? "");
2379
+ if (bearerIsDead) {
2380
+ delete cleaned.authToken;
2381
+ delete cleaned.credentialSource;
2382
+ }
2383
+ const worthKeeping = Boolean(
2384
+ cleaned.apiKey || cleaned.authToken || keepForEndpoint && cleaned.baseUrl
2385
+ );
2386
+ if (worthKeeping) kept.push(cleaned);
2387
+ }
2388
+ return { kept, removed };
2389
+ }
2390
+ function stripRevokedFromConfig(raw) {
2391
+ const result = { removed: 0 };
2392
+ if (typeof raw !== "object" || raw === null) return result;
2393
+ const cfg = raw;
2394
+ if (Array.isArray(cfg["providers"])) {
2395
+ const pass = stripRevokedCredentials(cfg["providers"]);
2396
+ result.removed = pass.removed;
2397
+ if (pass.removed > 0) cfg["providers"] = pass.kept;
2398
+ }
2399
+ return result;
2400
+ }
2401
+ function hasUsableAnthropic(providers) {
2402
+ return providers.some((p) => p?.type === "anthropic" && Boolean(p.apiKey || p.authToken && p.baseUrl));
2403
+ }
2404
+ function clearAnthropicPins(models, providers) {
2405
+ const cleared = [];
2406
+ if (typeof models !== "object" || models === null) return cleared;
2407
+ const tiers = models;
2408
+ for (const tier of ["t1", "t2", "t3"]) {
2409
+ const pin = tiers[tier];
2410
+ if (typeof pin !== "string" || !namesAnthropicModel(pin, providers)) continue;
2411
+ delete tiers[tier];
2412
+ cleared.push({ tier, model: pin });
2413
+ }
2414
+ return cleared;
2415
+ }
2416
+ function namesAnthropicModel(pin, providers) {
2417
+ const value = pin.trim().toLowerCase();
2418
+ if (!value) return false;
2419
+ if (value.includes(":")) return value.startsWith("anthropic:");
2420
+ const known = Object.values(MODELS).some(
2421
+ (m) => m.provider === "anthropic" && m.id.toLowerCase() === value
2422
+ );
2423
+ if (!known && !value.startsWith("claude-")) return false;
2424
+ return !canBeServedElsewhere(value, providers);
2425
+ }
2426
+ function canBeServedElsewhere(modelId, providers) {
2427
+ return providers.some((p) => p && p.type === "azure" && (p.deploymentName ?? "").trim().toLowerCase() === modelId);
2428
+ }
2429
+ var REVOKED_CREDENTIAL_REASON;
2430
+ var init_revoked_credentials = __esm({
2431
+ "src/config/revoked-credentials.ts"() {
2432
+ init_constants();
2433
+ REVOKED_CREDENTIAL_REASON = "Anthropic no longer permits third-party tools to use Claude subscription credentials and refuses them at the API, so the linked Claude Code token could not have worked. Add an API key from the Claude Console, or set ANTHROPIC_AUTH_TOKEN with a gateway.";
2434
+ }
2435
+ });
2198
2436
 
2199
2437
  // src/utils/cost.ts
2200
2438
  function resolveModelPricing(model, opts = {}) {
@@ -2264,10 +2502,17 @@ var init_cost = __esm({
2264
2502
  });
2265
2503
 
2266
2504
  // src/providers/base.ts
2267
- var BaseProvider;
2505
+ var ProviderUnreachableError, BaseProvider;
2268
2506
  var init_base = __esm({
2269
2507
  "src/providers/base.ts"() {
2270
2508
  init_cost();
2509
+ ProviderUnreachableError = class extends Error {
2510
+ transient = true;
2511
+ constructor(message) {
2512
+ super(message);
2513
+ this.name = "ProviderUnreachableError";
2514
+ }
2515
+ };
2271
2516
  BaseProvider = class {
2272
2517
  config;
2273
2518
  model;
@@ -2340,6 +2585,8 @@ var init_model_filter = __esm({
2340
2585
  var anthropic_exports = {};
2341
2586
  __export(anthropic_exports, {
2342
2587
  AnthropicProvider: () => AnthropicProvider,
2588
+ anthropicApiRoot: () => anthropicApiRoot,
2589
+ anthropicAuth: () => anthropicAuth,
2343
2590
  toAnthropicToolUse: () => toAnthropicToolUse,
2344
2591
  toAnthropicTools: () => toAnthropicTools
2345
2592
  });
@@ -2364,25 +2611,56 @@ function toAnthropicToolUse(toolCalls) {
2364
2611
  input: tc.input
2365
2612
  }));
2366
2613
  }
2367
- var AnthropicProvider;
2614
+ function anthropicApiRoot(configured) {
2615
+ if (!configured) return void 0;
2616
+ const root = clientApiRoot("anthropic", configured);
2617
+ return root || void 0;
2618
+ }
2619
+ function anthropicAuth(config) {
2620
+ const gateway = anthropicApiRoot(config.baseUrl);
2621
+ const bearerIsSubscription = isSubscriptionToken(config.authToken);
2622
+ const keyIsSubscription = isSubscriptionToken(config.apiKey);
2623
+ if (bearerIsSubscription || keyIsSubscription) {
2624
+ if (config.apiKey && !keyIsSubscription) return { mode: "apiKey", key: config.apiKey };
2625
+ throw new Error(
2626
+ "This Anthropic credential is a Claude subscription token, which Anthropic does not permit third-party clients to use. Configure an API key from console.anthropic.com instead."
2627
+ );
2628
+ }
2629
+ if (config.authToken && gateway) return { mode: "bearer", token: config.authToken };
2630
+ if (config.apiKey) return { mode: "apiKey", key: config.apiKey };
2631
+ if (config.authToken) {
2632
+ throw new Error(
2633
+ "Anthropic bearer token configured without a gateway URL. A bearer is only valid at the gateway that issued it, so it will not be sent to the public API. Set `baseUrl` to that gateway, or configure `apiKey` instead."
2634
+ );
2635
+ }
2636
+ return { mode: "none" };
2637
+ }
2638
+ var sameOriginFetch, AnthropicProvider;
2368
2639
  var init_anthropic = __esm({
2369
2640
  "src/providers/anthropic.ts"() {
2370
2641
  init_constants();
2371
2642
  init_base();
2372
2643
  init_pricing();
2373
2644
  init_model_filter();
2645
+ init_net();
2646
+ init_revoked_credentials();
2647
+ sameOriginFetch = ((input, init) => {
2648
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2649
+ return fetchSameOrigin(url, init);
2650
+ });
2374
2651
  AnthropicProvider = class extends BaseProvider {
2375
2652
  client;
2376
2653
  constructor(config, model) {
2377
2654
  super(config, model);
2378
- if (config.authToken) {
2379
- this.client = new Anthropic({
2380
- authToken: config.authToken,
2381
- defaultHeaders: { "anthropic-beta": "oauth-2025-04-20" }
2382
- });
2655
+ const baseURL = anthropicApiRoot(config.baseUrl);
2656
+ const auth2 = anthropicAuth(config);
2657
+ if (auth2.mode === "bearer") {
2658
+ this.client = new Anthropic({ authToken: auth2.token, fetch: sameOriginFetch, baseURL });
2383
2659
  } else {
2384
2660
  this.client = new Anthropic({
2385
- apiKey: config.apiKey
2661
+ apiKey: auth2.mode === "apiKey" ? auth2.key : void 0,
2662
+ fetch: sameOriginFetch,
2663
+ ...baseURL ? { baseURL } : {}
2386
2664
  });
2387
2665
  }
2388
2666
  }
@@ -2458,21 +2736,24 @@ var init_anthropic = __esm({
2458
2736
  async countTokens(text) {
2459
2737
  return Math.ceil(text.length / 4);
2460
2738
  }
2461
- async listModels() {
2739
+ async listModels(options = {}) {
2740
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "anthropic");
2462
2741
  try {
2463
- const resp = await fetch("https://api.anthropic.com/v1/models", {
2742
+ const base = anthropicApiRoot(this.config.baseUrl) ?? "https://api.anthropic.com";
2743
+ const modelsUrl = `${base}/v1/models`;
2744
+ const auth2 = anthropicAuth(this.config);
2745
+ const resp = await fetchSameOrigin(modelsUrl, {
2464
2746
  headers: {
2465
- "x-api-key": this.config.apiKey ?? "",
2747
+ // Same decision the constructor makes — see anthropicAuth(). Read
2748
+ // straight off `config`, this branch sent a bearer to the public
2749
+ // default host whenever no gateway was configured.
2750
+ ...auth2.mode === "bearer" ? { authorization: `Bearer ${auth2.token}` } : { "x-api-key": auth2.mode === "apiKey" ? auth2.key : "" },
2466
2751
  "anthropic-version": "2023-06-01"
2467
2752
  }
2468
2753
  });
2469
- if (!resp.ok) {
2470
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2471
- }
2754
+ if (!resp.ok) return staticCatalog();
2472
2755
  const data = await resp.json();
2473
- if (!Array.isArray(data?.data)) {
2474
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2475
- }
2756
+ if (!Array.isArray(data?.data)) return staticCatalog();
2476
2757
  return data.data.filter((m) => isChatModel(m.id)).map((m) => {
2477
2758
  const known = Object.values(MODELS).find((km) => km.id === m.id && km.provider === "anthropic");
2478
2759
  if (known) return known;
@@ -2490,7 +2771,7 @@ var init_anthropic = __esm({
2490
2771
  });
2491
2772
  });
2492
2773
  } catch {
2493
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2774
+ return staticCatalog();
2494
2775
  }
2495
2776
  }
2496
2777
  async isAvailable() {
@@ -2710,7 +2991,8 @@ var init_openai = __esm({
2710
2991
  async countTokens(text) {
2711
2992
  return Math.ceil(text.length / 4);
2712
2993
  }
2713
- async listModels() {
2994
+ async listModels(options = {}) {
2995
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "openai");
2714
2996
  try {
2715
2997
  const response = await this.client.models.list();
2716
2998
  return response.data.filter((m) => isChatModel(m.id)).map((m) => {
@@ -2730,7 +3012,7 @@ var init_openai = __esm({
2730
3012
  });
2731
3013
  });
2732
3014
  } catch {
2733
- return Object.values(MODELS).filter((m) => m.provider === "openai");
3015
+ return staticCatalog();
2734
3016
  }
2735
3017
  }
2736
3018
  async isAvailable() {
@@ -3032,9 +3314,46 @@ var init_gemini_schema = __esm({
3032
3314
  var gemini_exports = {};
3033
3315
  __export(gemini_exports, {
3034
3316
  GeminiProvider: () => GeminiProvider,
3317
+ geminiApiRoot: () => geminiApiRoot,
3035
3318
  toGeminiFunctionCalls: () => toGeminiFunctionCalls,
3036
3319
  toGeminiTools: () => toGeminiTools
3037
3320
  });
3321
+ async function* parseSseChunks(body) {
3322
+ const reader = body.getReader();
3323
+ const decoder = new TextDecoder();
3324
+ let buffered = "";
3325
+ try {
3326
+ for (; ; ) {
3327
+ const { done, value } = await reader.read();
3328
+ if (done) break;
3329
+ buffered += decoder.decode(value, { stream: true });
3330
+ let cut = buffered.indexOf("\n");
3331
+ while (cut !== -1) {
3332
+ const line = buffered.slice(0, cut).trim();
3333
+ buffered = buffered.slice(cut + 1);
3334
+ if (line.startsWith("data:")) {
3335
+ const payload = line.slice(5).trim();
3336
+ if (payload && payload !== "[DONE]") {
3337
+ try {
3338
+ yield JSON.parse(payload);
3339
+ } catch {
3340
+ }
3341
+ }
3342
+ }
3343
+ cut = buffered.indexOf("\n");
3344
+ }
3345
+ }
3346
+ } finally {
3347
+ reader.releaseLock();
3348
+ await body.cancel().catch(() => {
3349
+ });
3350
+ }
3351
+ }
3352
+ function geminiApiRoot(baseUrl) {
3353
+ const configured = baseUrl?.trim();
3354
+ if (!configured) return GEMINI_PUBLIC_ROOT;
3355
+ return clientApiRoot("gemini", configured) || GEMINI_PUBLIC_ROOT;
3356
+ }
3038
3357
  function toGeminiTools(tools) {
3039
3358
  return tools.map((tool) => ({
3040
3359
  name: tool.name,
@@ -3055,7 +3374,19 @@ function toGeminiFunctionCalls(toolCalls) {
3055
3374
  }
3056
3375
  }));
3057
3376
  }
3058
- var GeminiProvider;
3377
+ function isGeminiAuthStatus(status) {
3378
+ return status === 400 || status === 401 || status === 403;
3379
+ }
3380
+ async function describeGeminiError(resp) {
3381
+ try {
3382
+ const body = await resp.json();
3383
+ const detail = body?.error?.message ?? body?.error?.status;
3384
+ return detail ? ` \u2014 ${detail}` : "";
3385
+ } catch {
3386
+ return "";
3387
+ }
3388
+ }
3389
+ var GEMINI_API_VERSION, GEMINI_PUBLIC_ROOT, SAFETY_SETTINGS, GeminiProvider;
3059
3390
  var init_gemini = __esm({
3060
3391
  "src/providers/gemini.ts"() {
3061
3392
  init_constants();
@@ -3063,11 +3394,21 @@ var init_gemini = __esm({
3063
3394
  init_pricing();
3064
3395
  init_model_filter();
3065
3396
  init_gemini_schema();
3397
+ init_net();
3398
+ GEMINI_API_VERSION = "v1beta";
3399
+ GEMINI_PUBLIC_ROOT = "https://generativelanguage.googleapis.com";
3400
+ SAFETY_SETTINGS = [
3401
+ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
3402
+ { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
3403
+ ];
3066
3404
  GeminiProvider = class extends BaseProvider {
3067
3405
  client;
3068
3406
  constructor(config, model) {
3069
3407
  super(config, model);
3070
- this.client = new GoogleGenAI({ apiKey: config.apiKey ?? "" });
3408
+ this.client = new GoogleGenAI({
3409
+ apiKey: config.apiKey ?? "",
3410
+ ...config.baseUrl ? { httpOptions: { baseUrl: geminiApiRoot(config.baseUrl) } } : {}
3411
+ });
3071
3412
  }
3072
3413
  async generate(options) {
3073
3414
  const chunks = [];
@@ -3075,15 +3416,12 @@ var init_gemini = __esm({
3075
3416
  }
3076
3417
  async generateStream(options, onChunk) {
3077
3418
  const contents = this.buildContents(options.messages, options.images);
3078
- const stream = await this.client.models.generateContentStream({
3419
+ const stream = this.config.baseUrl ? await this.streamOverRest(contents, options) : await this.client.models.generateContentStream({
3079
3420
  model: this.model.id,
3080
3421
  contents,
3081
3422
  config: {
3082
3423
  systemInstruction: options.systemPrompt,
3083
- safetySettings: [
3084
- { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
3085
- { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
3086
- ],
3424
+ safetySettings: SAFETY_SETTINGS,
3087
3425
  tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
3088
3426
  abortSignal: options.signal
3089
3427
  }
@@ -3134,27 +3472,40 @@ var init_gemini = __esm({
3134
3472
  };
3135
3473
  }
3136
3474
  async countTokens(text) {
3475
+ const contents = [{ role: "user", parts: [{ text }] }];
3137
3476
  try {
3138
- const result = await this.client.models.countTokens({
3139
- model: this.model.id,
3140
- contents: [{ role: "user", parts: [{ text }] }]
3141
- });
3477
+ if (this.config.baseUrl) {
3478
+ const resp = await fetchSameOrigin(
3479
+ `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:countTokens`,
3480
+ {
3481
+ method: "POST",
3482
+ headers: {
3483
+ "x-goog-api-key": this.config.apiKey ?? "",
3484
+ "content-type": "application/json"
3485
+ },
3486
+ body: JSON.stringify({ contents })
3487
+ }
3488
+ );
3489
+ if (!resp.ok) return Math.ceil(text.length / 4);
3490
+ const data = await resp.json();
3491
+ return data.totalTokens ?? 0;
3492
+ }
3493
+ const result = await this.client.models.countTokens({ model: this.model.id, contents });
3142
3494
  return result.totalTokens ?? 0;
3143
3495
  } catch {
3144
3496
  return Math.ceil(text.length / 4);
3145
3497
  }
3146
3498
  }
3147
- async listModels() {
3499
+ async listModels(options = {}) {
3500
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "gemini");
3148
3501
  try {
3149
- const resp = await fetch(
3150
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`
3151
- );
3502
+ const resp = await this.fetchModelList();
3152
3503
  if (!resp.ok) {
3153
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3504
+ return staticCatalog();
3154
3505
  }
3155
3506
  const data = await resp.json();
3156
3507
  if (!Array.isArray(data?.models)) {
3157
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3508
+ return staticCatalog();
3158
3509
  }
3159
3510
  return data.models.filter((m) => isChatModel(m.name.replace("models/", ""), m.supportedGenerationMethods)).map((m) => {
3160
3511
  const id = m.name.replace("models/", "");
@@ -3176,21 +3527,99 @@ var init_gemini = __esm({
3176
3527
  });
3177
3528
  });
3178
3529
  } catch {
3179
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3530
+ return staticCatalog();
3180
3531
  }
3181
3532
  }
3533
+ /**
3534
+ * Is this KEY usable — not "does one particular model answer".
3535
+ *
3536
+ * This used to call countTokens() against `this.model.id`, which the router
3537
+ * fills from the first Gemini entry in the bundled catalogue. That made the
3538
+ * whole provider's fate depend on one hard-coded model id: a key that cannot
3539
+ * reach that one model — retired, not enabled for the key's project, served
3540
+ * on a different API version — failed the probe, and because every later step
3541
+ * is gated on the result (`validateCloudProviderModels` and
3542
+ * `discoverProviderModels` both return early for an unavailable provider) the
3543
+ * real model list was never fetched. Gemini vanished entirely, and the CLI
3544
+ * reported "add a provider API key first" about a key that was present and
3545
+ * working.
3546
+ *
3547
+ * Listing models asks the account-level question instead, the same one
3548
+ * OpenAI's probe asks, and it is the same request `listModels()` already
3549
+ * makes for discovery.
3550
+ *
3551
+ * Throws rather than returning false. The router logs the reason it catches,
3552
+ * and "bad key, wrong endpoint/deployment, or unreachable" — three guesses,
3553
+ * no answer — is what made this take a user report to find. A swallowed probe
3554
+ * error is the same defect that once hid a misconfigured Azure deployment.
3555
+ */
3182
3556
  async isAvailable() {
3557
+ if (!this.config.apiKey) throw new Error("no Gemini API key configured");
3558
+ let resp;
3183
3559
  try {
3184
- await this.client.models.countTokens({
3185
- model: this.model.id,
3186
- contents: [{ role: "user", parts: [{ text: "ping" }] }]
3187
- });
3188
- return true;
3189
- } catch {
3190
- return false;
3560
+ resp = await this.fetchModelList();
3561
+ } catch (err) {
3562
+ throw new ProviderUnreachableError(
3563
+ `could not reach the Gemini API: ${err instanceof Error ? err.message : String(err)}`
3564
+ );
3565
+ }
3566
+ if (resp.ok) return true;
3567
+ const status = `HTTP ${resp.status} ${resp.statusText}`.trim();
3568
+ const detail = await describeGeminiError(resp);
3569
+ if (isGeminiAuthStatus(resp.status)) {
3570
+ throw new Error(`Gemini rejected the API key: ${status}${detail}`);
3191
3571
  }
3572
+ throw new ProviderUnreachableError(`Gemini model list failed: ${status}${detail}`);
3192
3573
  }
3193
3574
  // ── Private ──────────────────────────────────
3575
+ /**
3576
+ * The account's model list. One place so the availability probe and
3577
+ * discovery cannot ask different questions of the same endpoint.
3578
+ *
3579
+ * The key goes in a header, not the query string: a URL carries into proxy
3580
+ * logs, error reports and shell history, and this one would carry the key
3581
+ * with it.
3582
+ */
3583
+ /**
3584
+ * Generation against a configured endpoint, issued by hand.
3585
+ *
3586
+ * Same wire protocol the SDK speaks — `:streamGenerateContent?alt=sse` on
3587
+ * `/v1beta`, with the same body — so the chunk shapes the caller walks are
3588
+ * identical and neither transport needs its own parsing. The difference that
3589
+ * matters is `fetchSameOrigin`: the key is never replayed on a host the
3590
+ * configured origin redirects to.
3591
+ */
3592
+ async streamOverRest(contents, options) {
3593
+ const url = `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:streamGenerateContent?alt=sse`;
3594
+ const resp = await fetchSameOrigin(url, {
3595
+ method: "POST",
3596
+ headers: {
3597
+ "x-goog-api-key": this.config.apiKey ?? "",
3598
+ "content-type": "application/json"
3599
+ },
3600
+ body: JSON.stringify({
3601
+ contents,
3602
+ // REST wants the object form; the SDK accepts a bare string and wraps
3603
+ // it itself.
3604
+ ...options.systemPrompt ? { systemInstruction: { parts: [{ text: options.systemPrompt }] } } : {},
3605
+ safetySettings: SAFETY_SETTINGS,
3606
+ ...options.tools?.length ? { tools: [{ functionDeclarations: toGeminiTools(options.tools) }] } : {}
3607
+ }),
3608
+ signal: options.signal
3609
+ });
3610
+ if (!resp.ok || !resp.body) {
3611
+ const detail = await resp.text().catch(() => "");
3612
+ throw new ProviderUnreachableError(
3613
+ `Gemini generation failed at ${geminiApiRoot(this.config.baseUrl)}: ${resp.status}${detail ? ` \u2014 ${detail.slice(0, 200)}` : ""}`
3614
+ );
3615
+ }
3616
+ return parseSseChunks(resp.body);
3617
+ }
3618
+ fetchModelList() {
3619
+ return fetchSameOrigin(`${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models`, {
3620
+ headers: { "x-goog-api-key": this.config.apiKey ?? "" }
3621
+ });
3622
+ }
3194
3623
  buildContents(messages, extraImages) {
3195
3624
  const contents = [];
3196
3625
  for (const m of messages) {
@@ -3275,95 +3704,17 @@ var init_gemini = __esm({
3275
3704
  };
3276
3705
  }
3277
3706
  });
3278
- function preferIpv4Host(url) {
3279
- return url;
3280
- }
3281
- async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3282
- const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3283
- const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3284
- const lib = u.protocol === "https:" ? https : http;
3285
- const method = (init.method ?? "GET").toUpperCase();
3286
- const headers = {};
3287
- const h = init.headers;
3288
- if (h instanceof Headers) h.forEach((v, k) => {
3289
- headers[k] = v;
3290
- });
3291
- else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
3292
- else if (h) Object.assign(headers, h);
3293
- if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
3294
- headers["accept-encoding"] = "gzip, deflate, br";
3295
- }
3296
- const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3297
- return new Promise((resolve2, reject) => {
3298
- const req = lib.request(
3299
- {
3300
- hostname: u.hostname,
3301
- port: u.port || (u.protocol === "https:" ? 443 : 80),
3302
- path: u.pathname + u.search,
3303
- method,
3304
- headers
3305
- },
3306
- (res) => {
3307
- const status = res.statusCode ?? 200;
3308
- const location = res.headers.location;
3309
- if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3310
- res.resume();
3311
- const nextUrl = new URL(location, u).href;
3312
- if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3313
- reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3314
- return;
3315
- }
3316
- const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3317
- const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3318
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3319
- return;
3320
- }
3321
- const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
3322
- let bodyStream = res;
3323
- if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib.createGunzip());
3324
- else if (encoding === "deflate") bodyStream = res.pipe(zlib.createInflate());
3325
- else if (encoding === "br") bodyStream = res.pipe(zlib.createBrotliDecompress());
3326
- const stream = Readable.toWeb(bodyStream);
3327
- const respHeaders = new Headers();
3328
- for (const [k, v] of Object.entries(res.headers)) {
3329
- if (k === "content-encoding" || k === "content-length") continue;
3330
- if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3331
- else if (typeof v === "string") respHeaders.set(k, v);
3332
- }
3333
- resolve2(new Response(stream, {
3334
- status,
3335
- statusText: res.statusMessage ?? "",
3336
- headers: respHeaders
3337
- }));
3338
- }
3339
- );
3340
- req.on("error", reject);
3341
- req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
3342
- if (body !== void 0) req.write(body);
3343
- req.end();
3344
- });
3345
- }
3346
- var MAX_REDIRECTS;
3347
- var init_net = __esm({
3348
- "src/utils/net.ts"() {
3349
- try {
3350
- dns.setDefaultResultOrder("ipv4first");
3351
- } catch {
3352
- }
3353
- MAX_REDIRECTS = 5;
3354
- }
3355
- });
3356
-
3357
- // src/providers/ollama.ts
3358
- var ollama_exports = {};
3359
- __export(ollama_exports, {
3360
- OllamaProvider: () => OllamaProvider,
3361
- toOllamaToolCalls: () => toOllamaToolCalls,
3362
- toOllamaTools: () => toOllamaTools
3363
- });
3364
- function isToolCapable(modelName) {
3365
- const name = modelName.toLowerCase();
3366
- return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
3707
+
3708
+ // src/providers/ollama.ts
3709
+ var ollama_exports = {};
3710
+ __export(ollama_exports, {
3711
+ OllamaProvider: () => OllamaProvider,
3712
+ toOllamaToolCalls: () => toOllamaToolCalls,
3713
+ toOllamaTools: () => toOllamaTools
3714
+ });
3715
+ function isToolCapable(modelName) {
3716
+ const name = modelName.toLowerCase();
3717
+ return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
3367
3718
  }
3368
3719
  function toOllamaTools(tools) {
3369
3720
  return tools.map((t) => ({
@@ -3641,12 +3992,38 @@ var init_openai_compatible = __esm({
3641
3992
  OpenAICompatibleProvider = class extends OpenAIProvider {
3642
3993
  constructor(config, model) {
3643
3994
  super({ ...config, apiKey: config.apiKey ?? "not-required" }, model);
3995
+ if (!config.baseUrl?.trim()) {
3996
+ throw new Error(
3997
+ "An OpenAI-compatible provider requires `baseUrl`: without one the OpenAI SDK sends to api.openai.com, so a key issued by another service would go to OpenAI."
3998
+ );
3999
+ }
3644
4000
  this.client = new OpenAI({
3645
4001
  apiKey: config.apiKey ?? "not-required",
3646
4002
  baseURL: preferIpv4Host(config.baseUrl),
3647
- fetch: nodeHttpFetch
4003
+ // Bound to the configured origin, NOT bare `nodeHttpFetch`. That helper
4004
+ // follows a 3xx by reusing `init` verbatim — headers included — so an
4005
+ // endpoint that redirected to another host was handed
4006
+ // `Authorization: Bearer <apiKey>` on the second one. This is the same
4007
+ // leak the Anthropic paths were fixed for; `allowedRedirectOrigin` exists
4008
+ // precisely for callers that attach a credential, and this one attaches
4009
+ // it on every request.
4010
+ fetch: ((input, init) => nodeHttpFetch(input, init ?? {}, 0, this.redirectPolicy()))
3648
4011
  });
3649
4012
  }
4013
+ /**
4014
+ * Redirects may only stay on the host the credential was configured for.
4015
+ *
4016
+ * An unparseable `baseUrl` yields no policy, which is the pre-existing
4017
+ * behaviour — but such a config cannot address anything anyway, so the
4018
+ * request fails before a redirect is possible.
4019
+ */
4020
+ redirectPolicy() {
4021
+ try {
4022
+ return { allowedRedirectOrigin: new URL(preferIpv4Host(this.config.baseUrl) ?? "").origin };
4023
+ } catch {
4024
+ return {};
4025
+ }
4026
+ }
3650
4027
  modelsUrl() {
3651
4028
  const base = (preferIpv4Host(this.config.baseUrl) ?? "").replace(/\/+$/, "");
3652
4029
  return base + "/models";
@@ -3657,7 +4034,7 @@ var init_openai_compatible = __esm({
3657
4034
  return h;
3658
4035
  }
3659
4036
  async listModels() {
3660
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4037
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3661
4038
  if (!res.ok) throw new Error(`models endpoint ${this.modelsUrl()} returned HTTP ${res.status}`);
3662
4039
  const body = await res.json();
3663
4040
  const raw = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
@@ -3687,7 +4064,7 @@ var init_openai_compatible = __esm({
3687
4064
  }
3688
4065
  async isAvailable() {
3689
4066
  try {
3690
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4067
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3691
4068
  return res.ok;
3692
4069
  } catch {
3693
4070
  return false;
@@ -3709,12 +4086,12 @@ var init_audit_logger = __esm({
3709
4086
  constructor(workspacePath, debugMode = false) {
3710
4087
  this.workspacePath = workspacePath;
3711
4088
  this.debugMode = debugMode;
3712
- const cascadeDir = path34.join(workspacePath, ".cascade");
3713
- if (!fs31.existsSync(cascadeDir)) {
3714
- fs31.mkdirSync(cascadeDir, { recursive: true });
4089
+ const cascadeDir = path35.join(workspacePath, ".cascade");
4090
+ if (!fs32.existsSync(cascadeDir)) {
4091
+ fs32.mkdirSync(cascadeDir, { recursive: true });
3715
4092
  }
3716
- this.keyPath = path34.join(cascadeDir, "audit_log.key");
3717
- this.dbPath = path34.join(cascadeDir, "audit_log.db");
4093
+ this.keyPath = path35.join(cascadeDir, "audit_log.key");
4094
+ this.dbPath = path35.join(cascadeDir, "audit_log.db");
3718
4095
  this.initEncryptionKey();
3719
4096
  this.db = new Database2(this.dbPath);
3720
4097
  this.db.pragma("journal_mode = WAL");
@@ -3744,11 +4121,11 @@ var init_audit_logger = __esm({
3744
4121
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3745
4122
  }
3746
4123
  initEncryptionKey() {
3747
- if (fs31.existsSync(this.keyPath)) {
3748
- this.encryptionKey = fs31.readFileSync(this.keyPath);
4124
+ if (fs32.existsSync(this.keyPath)) {
4125
+ this.encryptionKey = fs32.readFileSync(this.keyPath);
3749
4126
  } else {
3750
4127
  this.encryptionKey = crypto.randomBytes(32);
3751
- fs31.writeFileSync(this.keyPath, this.encryptionKey);
4128
+ fs32.writeFileSync(this.keyPath, this.encryptionKey);
3752
4129
  }
3753
4130
  }
3754
4131
  encrypt(text) {
@@ -3826,9 +4203,9 @@ var init_audit_logger = __esm({
3826
4203
  dumpDebugIfNeeded() {
3827
4204
  if (!this.debugMode) return;
3828
4205
  try {
3829
- const dumpPath = path34.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
4206
+ const dumpPath = path35.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
3830
4207
  const entries = this.getAllLogs();
3831
- fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4208
+ fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
3832
4209
  } catch (err) {
3833
4210
  console.error("Failed to dump debug audit logs", err);
3834
4211
  }
@@ -3883,7 +4260,7 @@ var Keystore = class {
3883
4260
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
3884
4261
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
3885
4262
  this.backend = "keytar";
3886
- if (password && fs31.existsSync(this.storePath)) {
4263
+ if (password && fs32.existsSync(this.storePath)) {
3887
4264
  try {
3888
4265
  const fileEntries = this.decryptFile(password);
3889
4266
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -3902,7 +4279,7 @@ var Keystore = class {
3902
4279
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
3903
4280
  );
3904
4281
  }
3905
- if (!fs31.existsSync(this.storePath)) {
4282
+ if (!fs32.existsSync(this.storePath)) {
3906
4283
  const salt = crypto.randomBytes(SALT_LEN);
3907
4284
  this.masterKey = this.deriveKey(password, salt);
3908
4285
  this.writeWithSalt({}, salt);
@@ -3916,7 +4293,7 @@ var Keystore = class {
3916
4293
  }
3917
4294
  /** Synchronous legacy unlock kept for AES-only environments. */
3918
4295
  unlockSync(password) {
3919
- if (!fs31.existsSync(this.storePath)) {
4296
+ if (!fs32.existsSync(this.storePath)) {
3920
4297
  const salt = crypto.randomBytes(SALT_LEN);
3921
4298
  this.masterKey = this.deriveKey(password, salt);
3922
4299
  this.writeWithSalt({}, salt);
@@ -3974,7 +4351,7 @@ var Keystore = class {
3974
4351
  }
3975
4352
  }
3976
4353
  decryptFile(password, knownSalt) {
3977
- if (!fs31.existsSync(this.storePath)) return {};
4354
+ if (!fs32.existsSync(this.storePath)) return {};
3978
4355
  try {
3979
4356
  const { salt, ciphertext, iv, tag } = this.readRaw();
3980
4357
  const useSalt = knownSalt ?? salt;
@@ -3996,8 +4373,8 @@ var Keystore = class {
3996
4373
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
3997
4374
  const tag = cipher.getAuthTag();
3998
4375
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
3999
- fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4000
- fs31.writeFileSync(this.storePath, out, { mode: 384 });
4376
+ fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
4377
+ fs32.writeFileSync(this.storePath, out, { mode: 384 });
4001
4378
  }
4002
4379
  writeWithSalt(data, salt) {
4003
4380
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4007,11 +4384,11 @@ var Keystore = class {
4007
4384
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4008
4385
  const tag = cipher.getAuthTag();
4009
4386
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4010
- fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4011
- fs31.writeFileSync(this.storePath, out, { mode: 384 });
4387
+ fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
4388
+ fs32.writeFileSync(this.storePath, out, { mode: 384 });
4012
4389
  }
4013
4390
  readRaw() {
4014
- const buf = fs31.readFileSync(this.storePath);
4391
+ const buf = fs32.readFileSync(this.storePath);
4015
4392
  let offset = 0;
4016
4393
  const salt = buf.subarray(offset, offset + SALT_LEN);
4017
4394
  offset += SALT_LEN;
@@ -4044,7 +4421,7 @@ var CascadeIgnore = class {
4044
4421
  ]);
4045
4422
  }
4046
4423
  async load(workspacePath) {
4047
- const filePath = path34.join(workspacePath, ".cascadeignore");
4424
+ const filePath = path35.join(workspacePath, ".cascadeignore");
4048
4425
  try {
4049
4426
  const content = await fs11.readFile(filePath, "utf-8");
4050
4427
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4055,7 +4432,7 @@ var CascadeIgnore = class {
4055
4432
  }
4056
4433
  isIgnored(filePath, workspacePath) {
4057
4434
  try {
4058
- const relative = workspacePath ? path34.relative(workspacePath, filePath) : filePath;
4435
+ const relative = workspacePath ? path35.relative(workspacePath, filePath) : filePath;
4059
4436
  return this.ig.ignores(relative);
4060
4437
  } catch {
4061
4438
  return false;
@@ -4066,7 +4443,7 @@ var CascadeIgnore = class {
4066
4443
  }
4067
4444
  };
4068
4445
  async function createDefaultIgnoreFile(workspacePath) {
4069
- const filePath = path34.join(workspacePath, ".cascadeignore");
4446
+ const filePath = path35.join(workspacePath, ".cascadeignore");
4070
4447
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4071
4448
  # Syntax identical to .gitignore
4072
4449
 
@@ -4096,7 +4473,7 @@ Thumbs.db
4096
4473
  await fs11.writeFile(filePath, content, "utf-8");
4097
4474
  }
4098
4475
  async function loadCascadeMd(workspacePath) {
4099
- const filePath = path34.join(workspacePath, "CASCADE.md");
4476
+ const filePath = path35.join(workspacePath, "CASCADE.md");
4100
4477
  try {
4101
4478
  const raw = await fs11.readFile(filePath, "utf-8");
4102
4479
  return parseCascadeMd(raw);
@@ -4125,7 +4502,7 @@ ${raw.trim()}`;
4125
4502
  return { raw, sections, systemPrompt };
4126
4503
  }
4127
4504
  async function createDefaultCascadeMd(workspacePath) {
4128
- const filePath = path34.join(workspacePath, "CASCADE.md");
4505
+ const filePath = path35.join(workspacePath, "CASCADE.md");
4129
4506
  const content = `# Cascade Project Instructions
4130
4507
 
4131
4508
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4160,7 +4537,7 @@ List any areas the agent should not touch.
4160
4537
  var MemoryStore = class _MemoryStore {
4161
4538
  db;
4162
4539
  constructor(dbPath) {
4163
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
4540
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
4164
4541
  try {
4165
4542
  this.db = new Database2(dbPath, { timeout: 5e3 });
4166
4543
  this.db.pragma("journal_mode = WAL");
@@ -5424,21 +5801,80 @@ function validateConfig(raw) {
5424
5801
 
5425
5802
  // src/config/global-credentials.ts
5426
5803
  init_constants();
5804
+
5805
+ // src/config/azure-endpoint.ts
5806
+ init_net();
5807
+ function normalizeAzureEndpoint(url) {
5808
+ return normalizeEndpoint(url);
5809
+ }
5810
+ function sameAzureEndpoint(a, b) {
5811
+ return normalizeAzureEndpoint(a) === normalizeAzureEndpoint(b);
5812
+ }
5813
+
5814
+ // src/config/endpoint-identity.ts
5815
+ init_net();
5816
+ var DEFAULT_ENDPOINT = {
5817
+ anthropic: "https://api.anthropic.com",
5818
+ openai: "https://api.openai.com/v1",
5819
+ gemini: "https://generativelanguage.googleapis.com"
5820
+ };
5821
+ function hasDefaultEndpoint(type) {
5822
+ return Object.hasOwn(DEFAULT_ENDPOINT, type);
5823
+ }
5824
+ function credentialEndpointIdentity(type, baseUrl) {
5825
+ const configured = baseUrl?.trim();
5826
+ const effective = configured || DEFAULT_ENDPOINT[type];
5827
+ if (!effective) return null;
5828
+ return normalizeEndpoint(clientApiRoot(type, effective));
5829
+ }
5830
+ function sameCredentialEndpoint(type, a, b) {
5831
+ const left = credentialEndpointIdentity(type, a);
5832
+ const right = credentialEndpointIdentity(type, b);
5833
+ if (left === null && right === null) return true;
5834
+ if (left === null || right === null) return false;
5835
+ return left === right;
5836
+ }
5837
+ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
5838
+ const row = rowEndpoint?.trim() ? credentialEndpointIdentity(type, rowEndpoint) : null;
5839
+ const credential = credentialEndpointIdentity(type, credentialEndpoint);
5840
+ if (row === null) return false;
5841
+ if (credential === null) return true;
5842
+ return row !== credential;
5843
+ }
5844
+
5845
+ // src/config/global-credentials.ts
5427
5846
  function credentialsPath(globalDir2) {
5428
- return path34.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5847
+ return path35.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5429
5848
  }
5430
5849
  function providerKey(p) {
5431
- if (p.type === "azure") {
5432
- return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
5433
- }
5434
5850
  return p.type;
5435
5851
  }
5852
+ function azureEndpoint(p) {
5853
+ return p.baseUrl === void 0 ? void 0 : normalizeAzureEndpoint(p.baseUrl);
5854
+ }
5855
+ function sameAzureEntry(a, b) {
5856
+ const strong = [
5857
+ [a.deploymentName, b.deploymentName],
5858
+ [azureEndpoint(a), azureEndpoint(b)]
5859
+ ];
5860
+ let shared = 0;
5861
+ for (const [x, y] of strong) {
5862
+ if (!x || !y) continue;
5863
+ if (x !== y) return false;
5864
+ shared++;
5865
+ }
5866
+ if (shared > 0) return true;
5867
+ const aNamesSomething = Boolean(a.deploymentName || azureEndpoint(a));
5868
+ const bNamesSomething = Boolean(b.deploymentName || azureEndpoint(b));
5869
+ if (aNamesSomething || bNamesSomething) return false;
5870
+ return Boolean(a.label && b.label && a.label === b.label);
5871
+ }
5436
5872
  function isPersistable(p) {
5437
5873
  return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
5438
5874
  }
5439
5875
  function loadGlobalCredentials(globalDir2) {
5440
5876
  try {
5441
- const raw = fs31.readFileSync(credentialsPath(globalDir2), "utf-8");
5877
+ const raw = fs32.readFileSync(credentialsPath(globalDir2), "utf-8");
5442
5878
  const parsed = JSON.parse(raw);
5443
5879
  if (!Array.isArray(parsed.providers)) return [];
5444
5880
  return parsed.providers.filter(
@@ -5450,34 +5886,93 @@ function loadGlobalCredentials(globalDir2) {
5450
5886
  }
5451
5887
  function saveGlobalCredentials(globalDir2, providers) {
5452
5888
  const filePath = credentialsPath(globalDir2);
5453
- fs31.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5889
+ fs32.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5454
5890
  const body = { version: 1, providers: providers.filter(isPersistable) };
5455
5891
  const tmp = `${filePath}.tmp`;
5456
- fs31.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5457
- fs31.renameSync(tmp, filePath);
5892
+ fs32.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5893
+ fs32.renameSync(tmp, filePath);
5458
5894
  try {
5459
- fs31.chmodSync(filePath, 384);
5895
+ fs32.chmodSync(filePath, 384);
5460
5896
  } catch {
5461
5897
  }
5462
5898
  }
5463
5899
  function mergeGlobalCredentials(workspaceProviders, globalProviders) {
5464
5900
  const merged = [...workspaceProviders];
5465
- const byKey = new Map(merged.map((p) => [providerKey(p), p]));
5901
+ const byKey = new Map(merged.filter((p) => p.type !== "azure").map((p) => [providerKey(p), p]));
5466
5902
  for (const g of globalProviders) {
5467
- const existing = byKey.get(providerKey(g));
5903
+ if (g.type === "azure" && !g.deploymentName?.trim() && azureEndpoint(g)) {
5904
+ const onResource = merged.filter((p) => p.type === "azure" && sameAzureEntry(p, g));
5905
+ if (onResource.length > 0) {
5906
+ for (const row of onResource) fillFrom(row, g);
5907
+ continue;
5908
+ }
5909
+ }
5910
+ const existing = g.type === "azure" ? merged.find((p) => p.type === "azure" && sameAzureEntry(p, g)) : byKey.get(providerKey(g));
5468
5911
  if (!existing) {
5469
5912
  merged.push({ ...g });
5470
- byKey.set(providerKey(g), merged[merged.length - 1]);
5913
+ if (g.type !== "azure") byKey.set(providerKey(g), merged[merged.length - 1]);
5471
5914
  continue;
5472
5915
  }
5473
- if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
5474
- if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
5475
- if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5476
- if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5477
- if (!existing.label && g.label) existing.label = g.label;
5916
+ fillFrom(existing, g);
5478
5917
  }
5479
5918
  return merged;
5480
5919
  }
5920
+ function fillFrom(existing, g) {
5921
+ const credentialled = Boolean(existing.apiKey || existing.authToken);
5922
+ const endpointsDiffer = credentialEndpointsConflict(existing.type, existing.baseUrl, g.baseUrl);
5923
+ if (!credentialled && !endpointsDiffer && g.apiKey) {
5924
+ existing.apiKey = g.apiKey;
5925
+ if (g.baseUrl) existing.baseUrl = g.baseUrl;
5926
+ }
5927
+ if (!credentialled && !endpointsDiffer && !existing.apiKey && g.authToken && g.baseUrl) {
5928
+ existing.authToken = g.authToken;
5929
+ existing.baseUrl = g.baseUrl;
5930
+ }
5931
+ if (!credentialled && !existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5932
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5933
+ if (!existing.label && g.label) existing.label = g.label;
5934
+ }
5935
+
5936
+ // src/config/azure-routing.ts
5937
+ function routableAzureRows(rows) {
5938
+ return rows.filter((p) => p.type === "azure" && p.baseUrl?.trim());
5939
+ }
5940
+ function resolveAzureRouting(view, write, env) {
5941
+ const rows = routableAzureRows(view);
5942
+ const resourcesOf = (list) => [...new Set(list.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
5943
+ if (rows.length === 0) return { ok: false, reason: "no-routable-rows", resources: [] };
5944
+ const endpoint = env.endpoint?.trim();
5945
+ const deployment = env.deployment?.trim();
5946
+ const named = deployment ? rows.filter((p) => (p.deploymentName?.trim() ?? "") === deployment) : [];
5947
+ let anchor;
5948
+ if (endpoint) {
5949
+ anchor = rows.find((p) => sameAzureEndpoint(p.baseUrl, endpoint));
5950
+ if (!anchor) {
5951
+ return { ok: false, reason: "endpoint-not-configured", resources: resourcesOf(rows) };
5952
+ }
5953
+ const elsewhere = named.filter((p) => !sameAzureEndpoint(p.baseUrl, endpoint));
5954
+ if (elsewhere.length > 0) {
5955
+ return { ok: false, reason: "name-on-another-resource", resources: resourcesOf(elsewhere) };
5956
+ }
5957
+ } else if (named.length > 0) {
5958
+ const owners = resourcesOf(named);
5959
+ if (owners.length > 1) return { ok: false, reason: "ambiguous-resource", resources: owners };
5960
+ anchor = named[0];
5961
+ } else {
5962
+ const all = resourcesOf(rows);
5963
+ if (all.length !== 1) return { ok: false, reason: "ambiguous-resource", resources: all };
5964
+ anchor = rows[0];
5965
+ }
5966
+ const resource = anchor.baseUrl;
5967
+ const onResource = routableAzureRows(write).filter((p) => sameAzureEndpoint(p.baseUrl, resource));
5968
+ const alreadyThere = deployment ? onResource.some((p) => (p.deploymentName?.trim() ?? "") === deployment) || named.some((p) => sameAzureEndpoint(p.baseUrl, resource)) : true;
5969
+ return {
5970
+ ok: true,
5971
+ resource,
5972
+ rows: onResource,
5973
+ ...deployment && !alreadyThere ? { createDeployment: deployment } : {}
5974
+ };
5975
+ }
5481
5976
 
5482
5977
  // src/config/retired-providers.ts
5483
5978
  var RETIRED_PROVIDER_TYPES = {
@@ -5487,7 +5982,7 @@ function isRetiredProviderType(type) {
5487
5982
  return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
5488
5983
  }
5489
5984
  function didCleanupChangeAnything(c) {
5490
- return c.removed.length > 0 || c.clearedPins.length > 0;
5985
+ return c.removed.length > 0 || c.clearedPins.length > 0 || (c.revokedCredentials ?? 0) > 0 || (c.unusableCredentials ?? 0) > 0;
5491
5986
  }
5492
5987
  function stripRetiredProviders(raw) {
5493
5988
  const cleanup = { removed: [], clearedPins: [] };
@@ -5534,11 +6029,24 @@ function describeCleanup(c) {
5534
6029
  for (const type of c.removed) {
5535
6030
  parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
5536
6031
  }
6032
+ if ((c.revokedCredentials ?? 0) > 0) {
6033
+ parts.push("discarded a linked Claude subscription token, which Anthropic no longer permits third-party tools to use");
6034
+ }
6035
+ const unusable = c.unusableCredentials ?? 0;
6036
+ if (unusable > 0) {
6037
+ parts.push(
6038
+ `discarded ${unusable} synced gateway token${unusable === 1 ? "" : "s"} that named no gateway URL \u2014 a bearer is only valid at the gateway that issued it`
6039
+ );
6040
+ }
5537
6041
  if (c.clearedPins.length > 0) {
5538
6042
  parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
5539
6043
  }
6044
+ if (parts.length === 0) return "";
5540
6045
  return `Cascade config migration: ${parts.join("; ")}.`;
5541
6046
  }
6047
+
6048
+ // src/config/index.ts
6049
+ init_revoked_credentials();
5542
6050
  var MCP_TOOL_PREFIX = "mcp__";
5543
6051
  var MAX_SEGMENT_LENGTH = 24;
5544
6052
  var SEGMENT_HASH_LENGTH = 8;
@@ -5720,10 +6228,31 @@ function removeMcpServerDenials(disabledTools, serverName) {
5720
6228
  // src/config/index.ts
5721
6229
  init_constants();
5722
6230
  var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
6231
+ function hasProviderCredential(p) {
6232
+ if (!p) return false;
6233
+ if (typeof p.apiKey === "string" && p.apiKey.length > 0) return true;
6234
+ return typeof p.authToken === "string" && p.authToken.length > 0 && typeof p.baseUrl === "string" && p.baseUrl.length > 0;
6235
+ }
6236
+ function isRoutable(p) {
6237
+ if (p.type === "openai-compatible") return Boolean(p.baseUrl?.trim());
6238
+ if (p.type === "azure") return Boolean(p.baseUrl?.trim() && p.deploymentName?.trim());
6239
+ return true;
6240
+ }
5723
6241
  function hasUsableProvider(providers) {
5724
6242
  if (!providers?.length) return false;
5725
- return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
6243
+ return providers.some((p) => isRoutable(p) && (KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || hasProviderCredential(p)));
6244
+ }
6245
+ function applyProviderApiKey(providers, type, apiKey, extra = {}) {
6246
+ const existing = providers.find((p) => p.type === type);
6247
+ if (existing) {
6248
+ existing.apiKey = apiKey;
6249
+ existing.authToken = void 0;
6250
+ if (extra.baseUrl) existing.baseUrl = extra.baseUrl;
6251
+ return;
6252
+ }
6253
+ providers.push({ type, apiKey, ...extra.baseUrl ? { baseUrl: extra.baseUrl } : {} });
5726
6254
  }
6255
+ var ENV_ENDPOINT_CHANNEL = /* @__PURE__ */ new Set(["anthropic"]);
5727
6256
  var ConfigManager = class {
5728
6257
  config;
5729
6258
  keystore;
@@ -5743,6 +6272,16 @@ var ConfigManager = class {
5743
6272
  * fallback for a list that is empty for some unrelated reason.
5744
6273
  */
5745
6274
  retiredCleanup;
6275
+ /**
6276
+ * How many dead subscription credentials THIS load removed. Per-load like
6277
+ * `retiredCleanup`, for the same reason: a second load() on the same instance
6278
+ * must not re-announce a migration that already happened.
6279
+ */
6280
+ revokedCredentials = 0;
6281
+ /** Tier pins cleared by the revoked-credential migration this load. */
6282
+ revokedPins = [];
6283
+ /** This load's revoked-credential explanation, joined with any retirement one. */
6284
+ revokedNotice;
5746
6285
  /**
5747
6286
  * Human-readable migration notice held for a UI to display. `console.warn`
5748
6287
  * is not sufficient: the REPL clears the TTY immediately after load(), and
@@ -5758,7 +6297,7 @@ var ConfigManager = class {
5758
6297
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5759
6298
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5760
6299
  this.workspacePath = workspacePath;
5761
- this.globalDir = globalDirOverride ?? path34.join(os10.homedir(), GLOBAL_CONFIG_DIR);
6300
+ this.globalDir = globalDirOverride ?? path35.join(os10.homedir(), GLOBAL_CONFIG_DIR);
5762
6301
  }
5763
6302
  async load() {
5764
6303
  this.config = await this.loadConfig();
@@ -5772,22 +6311,38 @@ var ConfigManager = class {
5772
6311
  this.ignore = new CascadeIgnore();
5773
6312
  await this.ignore.load(this.workspacePath);
5774
6313
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5775
- this.keystore = new Keystore(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5776
- this.store = new MemoryStore(path34.join(this.workspacePath, CASCADE_DB_FILE));
5777
- const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
5778
- if (globalCreds.removed.length > 0) {
6314
+ this.keystore = new Keystore(path35.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
6315
+ this.store = new MemoryStore(path35.join(this.workspacePath, CASCADE_DB_FILE));
6316
+ const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
6317
+ const globalCreds = filterRetiredCredentials(globalRevoked.kept);
6318
+ if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
6319
+ if (globalCreds.removed.length > 0 || globalRevoked.removed > 0) {
6320
+ const retiredHere = globalCreds.removed.length > 0;
5779
6321
  try {
5780
6322
  saveGlobalCredentials(this.globalDir, globalCreds.kept);
5781
6323
  } catch (err) {
5782
6324
  console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
5783
6325
  }
5784
- this.retiredCleanup = {
5785
- removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
5786
- clearedPins: this.retiredCleanup?.clearedPins ?? []
5787
- };
6326
+ if (retiredHere) {
6327
+ this.retiredCleanup = {
6328
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
6329
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
6330
+ };
6331
+ }
5788
6332
  }
5789
- await this.injectEnvKeys();
6333
+ await this.injectEnvKeys(globalCreds.kept);
5790
6334
  this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
6335
+ if (this.revokedCredentials > 0 && !hasUsableAnthropic(this.config.providers)) {
6336
+ this.revokedPins = clearAnthropicPins(this.config.models, this.config.providers);
6337
+ if (this.revokedPins.length > 0) {
6338
+ await this.persistClearedPins(this.revokedPins.map((p) => p.tier));
6339
+ }
6340
+ }
6341
+ if (this.revokedCredentials > 0) {
6342
+ const pins = this.revokedPins.length ? ` Cleared the ${this.revokedPins.map((p) => `${p.tier.toUpperCase()} pin (${p.model})`).join(" and ")}, since it named Anthropic.` : "";
6343
+ this.revokedNotice = `Cascade config migration: ${REVOKED_CREDENTIAL_REASON}${pins}`;
6344
+ console.warn(this.revokedNotice);
6345
+ }
5791
6346
  for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
5792
6347
  try {
5793
6348
  this.store.purgeCachedModelsForRetiredProvider(type);
@@ -5796,10 +6351,14 @@ var ConfigManager = class {
5796
6351
  }
5797
6352
  await this.ensureDefaultIdentity();
5798
6353
  if (mcpNamesChanged) await this.save();
6354
+ const notices = [];
5799
6355
  if (this.retiredCleanup) {
5800
- console.warn(describeCleanup(this.retiredCleanup));
5801
- this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
6356
+ const retired = describeCleanup(this.retiredCleanup);
6357
+ console.warn(retired);
6358
+ notices.push(retired);
5802
6359
  }
6360
+ if (this.revokedNotice) notices.push(this.revokedNotice);
6361
+ if (notices.length > 0) this.pendingRetiredNotice = notices.join(" ");
5803
6362
  }
5804
6363
  getConfig() {
5805
6364
  return this.config;
@@ -5819,12 +6378,18 @@ var ConfigManager = class {
5819
6378
  getWorkspacePath() {
5820
6379
  return this.workspacePath;
5821
6380
  }
5822
- async save() {
5823
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
5824
- await fs11.mkdir(path34.dirname(configPath), { recursive: true });
5825
- await fs11.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
6381
+ /**
6382
+ * @param config What to persist. Defaults to the manager's own, but a
6383
+ * transactional save hands in the validated copy it has NOT adopted yet —
6384
+ * writing before adopting is what stops a run picking up configuration that
6385
+ * never reached the disk (see `commitSettings`).
6386
+ */
6387
+ async save(config = this.config) {
6388
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
6389
+ await fs11.mkdir(path35.dirname(configPath), { recursive: true });
6390
+ await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
5826
6391
  try {
5827
- saveGlobalCredentials(this.globalDir, this.config.providers);
6392
+ saveGlobalCredentials(this.globalDir, config.providers);
5828
6393
  } catch (err) {
5829
6394
  console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
5830
6395
  }
@@ -5855,19 +6420,35 @@ var ConfigManager = class {
5855
6420
  const configProvider = this.config.providers.find((p) => p.type === provider);
5856
6421
  return configProvider?.apiKey;
5857
6422
  }
6423
+ /**
6424
+ * The bearer token configured for a provider, if any.
6425
+ *
6426
+ * Companion to getApiKey(). A provider can be fully configured with only
6427
+ * this — `cascade link` and ANTHROPIC_AUTH_TOKEN both produce it — so any
6428
+ * surface that asks "is this provider set up" has to consult both, or it
6429
+ * reports a working install as unconfigured.
6430
+ */
6431
+ getAuthToken(provider) {
6432
+ return this.config.providers.find((p) => p.type === provider)?.authToken;
6433
+ }
5858
6434
  async loadConfig() {
5859
6435
  this.retiredCleanup = void 0;
5860
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
6436
+ this.revokedCredentials = 0;
6437
+ this.revokedPins = [];
6438
+ this.revokedNotice = void 0;
6439
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
5861
6440
  try {
5862
6441
  const raw = await fs11.readFile(configPath, "utf-8");
5863
6442
  const parsed = JSON.parse(raw);
5864
6443
  const cleanup = stripRetiredProviders(parsed);
5865
- if (didCleanupChangeAnything(cleanup)) {
5866
- this.retiredCleanup = cleanup;
6444
+ const revokedHere = stripRevokedFromConfig(parsed).removed;
6445
+ if (revokedHere > 0) this.revokedCredentials += revokedHere;
6446
+ if (didCleanupChangeAnything(cleanup) || revokedHere > 0) {
6447
+ if (didCleanupChangeAnything(cleanup)) this.retiredCleanup = cleanup;
5867
6448
  try {
5868
6449
  await fs11.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
5869
6450
  } catch (err) {
5870
- console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
6451
+ console.warn(`Could not persist the config migration: ${err instanceof Error ? err.message : String(err)}`);
5871
6452
  }
5872
6453
  }
5873
6454
  return validateConfig(parsed);
@@ -5878,9 +6459,102 @@ var ConfigManager = class {
5878
6459
  throw err;
5879
6460
  }
5880
6461
  }
5881
- async injectEnvKeys() {
6462
+ /**
6463
+ * Rewrite the workspace file with those tier pins removed.
6464
+ *
6465
+ * A targeted edit of the RAW file, not save(). By this point `this.config`
6466
+ * carries environment keys and machine-global credentials, and save()
6467
+ * serializes the whole object — so persisting through it would copy secrets
6468
+ * kept 0600 in ~/.cascade-ai into a workspace file that may be 0644. Same
6469
+ * reasoning as the migration write in loadConfig(); best-effort for the same
6470
+ * reason, since a read-only config directory must not abort startup.
6471
+ */
6472
+ /**
6473
+ * The Azure entries `AZURE_OPENAI_KEY` belongs to — possibly several.
6474
+ *
6475
+ * `AZURE_OPENAI_ENDPOINT` names the resource. Without one, a single
6476
+ * configured resource is unambiguous and anything more is a guess — and
6477
+ * guessing wrong writes a key to a resource that will reject it.
6478
+ *
6479
+ * ALL the deployments on that resource, not the first. An Azure key is
6480
+ * resource-scoped, so every deployment there shares it, and Azure is
6481
+ * configured one entry per deployment — the router binds each model to its
6482
+ * own row, matching `deploymentName` against the model id, so filling only
6483
+ * the first left the rest issuing requests with no key at all.
6484
+ */
6485
+ azureEntriesForEnv(globalProviders = []) {
6486
+ const workspace = this.config.providers;
6487
+ const routing = resolveAzureRouting(
6488
+ [...workspace, ...globalProviders],
6489
+ workspace,
6490
+ {
6491
+ endpoint: process.env["AZURE_OPENAI_ENDPOINT"],
6492
+ deployment: process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"]
6493
+ }
6494
+ );
6495
+ if (!routing.ok) return [];
6496
+ const onResource = [...routing.rows];
6497
+ const claimEndpointlessRow = (name) => {
6498
+ const row = this.config.providers.find((p) => p.type === "azure" && !p.baseUrl?.trim() && (p.deploymentName?.trim() ?? "") === name);
6499
+ if (!row) return void 0;
6500
+ if (row.apiKey || row.authToken) {
6501
+ console.warn(
6502
+ `Ignoring the stored key on Azure deployment "${name}": it was saved without an endpoint, so there is nothing to show it belongs to ${routing.resource}. AZURE_OPENAI_KEY is being used instead.`
6503
+ );
6504
+ row.apiKey = void 0;
6505
+ row.authToken = void 0;
6506
+ }
6507
+ row.baseUrl = routing.resource;
6508
+ onResource.push(row);
6509
+ return row;
6510
+ };
6511
+ for (const g of globalProviders) {
6512
+ if (g.type !== "azure" || !g.baseUrl?.trim()) continue;
6513
+ if (!sameAzureEndpoint(g.baseUrl, routing.resource)) continue;
6514
+ const name = g.deploymentName?.trim() ?? "";
6515
+ if (onResource.some((p) => (p.deploymentName?.trim() ?? "") === name)) continue;
6516
+ if (claimEndpointlessRow(name)) continue;
6517
+ const row = { ...g, apiKey: void 0, authToken: void 0 };
6518
+ this.config.providers.push(row);
6519
+ onResource.push(row);
6520
+ }
6521
+ if (routing.createDeployment && !claimEndpointlessRow(routing.createDeployment)) {
6522
+ const row = {
6523
+ type: "azure",
6524
+ deploymentName: routing.createDeployment,
6525
+ baseUrl: routing.resource
6526
+ };
6527
+ this.config.providers.push(row);
6528
+ onResource.push(row);
6529
+ }
6530
+ return onResource;
6531
+ }
6532
+ async persistClearedPins(tiers) {
6533
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
6534
+ try {
6535
+ const raw = JSON.parse(await fs11.readFile(configPath, "utf-8"));
6536
+ const models = raw["models"];
6537
+ if (typeof models !== "object" || models === null) return;
6538
+ for (const tier of tiers) delete models[tier];
6539
+ await fs11.writeFile(configPath, JSON.stringify(raw, null, 2), "utf-8");
6540
+ } catch (err) {
6541
+ if (err.code === "ENOENT") return;
6542
+ console.warn(`Could not persist the cleared model pin: ${err instanceof Error ? err.message : String(err)}`);
6543
+ }
6544
+ }
6545
+ /**
6546
+ * `globalProviders` is the machine-global credential store, passed in
6547
+ * READ-ONLY. This runs before mergeGlobalCredentials() on purpose — an
6548
+ * exported key should outrank a stored one, which only holds while the
6549
+ * injection goes first — but a few of the questions asked here are about the
6550
+ * user's whole configuration rather than this workspace's file, and answering
6551
+ * those from the workspace alone is what produced the gateway bug this
6552
+ * argument fixes.
6553
+ */
6554
+ async injectEnvKeys(globalProviders = []) {
5882
6555
  const wasEmpty = this.config.providers.length === 0;
5883
- const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
6556
+ const emptiedByRetirement = wasEmpty && (!!this.retiredCleanup || this.revokedCredentials > 0);
6557
+ const mayCreate = (type) => wasEmpty || type === "anthropic" && this.revokedCredentials > 0;
5884
6558
  const envProviders = [
5885
6559
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5886
6560
  { env: "OPENAI_API_KEY", type: "openai" },
@@ -5890,11 +6564,66 @@ var ConfigManager = class {
5890
6564
  for (const { env, type } of envProviders) {
5891
6565
  const key = process.env[env];
5892
6566
  if (!key) continue;
5893
- const existing = this.config.providers.find((p) => p.type === type);
5894
- if (!existing && wasEmpty) {
5895
- this.config.providers.push({ type, apiKey: key });
5896
- } else if (existing && !existing.apiKey) {
5897
- existing.apiKey = key;
6567
+ if (isSubscriptionToken(key)) continue;
6568
+ const targets = type === "azure" ? this.azureEntriesForEnv(globalProviders) : [this.config.providers.find((p) => p.type === type)].filter((p) => !!p);
6569
+ const existing = targets[0];
6570
+ const globalMatch = type === "azure" ? void 0 : globalProviders.find((p) => p.type === type);
6571
+ const anthropicGateway = type === "anthropic" ? process.env["ANTHROPIC_BASE_URL"] : void 0;
6572
+ if (!existing && (mayCreate(type) || globalMatch)) {
6573
+ if (type === "azure") {
6574
+ const endpoint = process.env["AZURE_OPENAI_ENDPOINT"];
6575
+ const deploymentName = process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"];
6576
+ if (!endpoint || !deploymentName) continue;
6577
+ const apiVersion = process.env["AZURE_OPENAI_API_VERSION"];
6578
+ this.config.providers.push({
6579
+ type,
6580
+ apiKey: key,
6581
+ baseUrl: endpoint,
6582
+ deploymentName,
6583
+ ...apiVersion ? { apiVersion } : {}
6584
+ });
6585
+ continue;
6586
+ }
6587
+ const inherited = ENV_ENDPOINT_CHANNEL.has(type) ? anthropicGateway : anthropicGateway ?? globalMatch?.baseUrl;
6588
+ this.config.providers.push({
6589
+ type,
6590
+ apiKey: key,
6591
+ ...inherited ? { baseUrl: inherited } : {}
6592
+ });
6593
+ } else if (existing) {
6594
+ for (const target of targets) {
6595
+ if (target.apiKey || target.authToken) continue;
6596
+ if (anthropicGateway) {
6597
+ target.apiKey = key;
6598
+ target.baseUrl = anthropicGateway;
6599
+ continue;
6600
+ }
6601
+ if (target.baseUrl && hasDefaultEndpoint(type) && !sameCredentialEndpoint(type, void 0, target.baseUrl)) {
6602
+ if (ENV_ENDPOINT_CHANNEL.has(type)) target.baseUrl = void 0;
6603
+ else {
6604
+ console.warn(
6605
+ `${env} will be used at the ${type} endpoint configured in this workspace (${target.baseUrl}), not at the provider's public host. Remove that \`baseUrl\` if the key was issued by ${type} directly.`
6606
+ );
6607
+ }
6608
+ }
6609
+ target.apiKey = key;
6610
+ }
6611
+ }
6612
+ }
6613
+ const authToken = process.env["ANTHROPIC_AUTH_TOKEN"];
6614
+ if (authToken && !isSubscriptionToken(authToken)) {
6615
+ const existing = this.config.providers.find((p) => p.type === "anthropic");
6616
+ const globalAnthropic = globalProviders.find((p) => p.type === "anthropic");
6617
+ const gateway = process.env["ANTHROPIC_BASE_URL"] ?? existing?.baseUrl ?? globalAnthropic?.baseUrl;
6618
+ if (gateway) {
6619
+ if (existing) {
6620
+ if (!existing.apiKey && !existing.authToken) {
6621
+ existing.authToken = authToken;
6622
+ existing.baseUrl = gateway;
6623
+ }
6624
+ } else if (mayCreate("anthropic") || globalAnthropic) {
6625
+ this.config.providers.push({ type: "anthropic", authToken, baseUrl: gateway });
6626
+ }
5898
6627
  }
5899
6628
  }
5900
6629
  if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
@@ -6246,15 +6975,15 @@ function SafeTextInput(props) {
6246
6975
  const displayValue = mask ? mask.repeat(value.length) : value;
6247
6976
  let rendered;
6248
6977
  if (displayValue.length === 0) {
6249
- rendered = showCursor && focus ? placeholder.length > 0 ? chalk7.inverse(placeholder[0]) + chalk7.grey(placeholder.slice(1)) : chalk7.inverse(" ") : placeholder.length > 0 ? chalk7.grey(placeholder) : "";
6978
+ rendered = showCursor && focus ? placeholder.length > 0 ? chalk8.inverse(placeholder[0]) + chalk8.grey(placeholder.slice(1)) : chalk8.inverse(" ") : placeholder.length > 0 ? chalk8.grey(placeholder) : "";
6250
6979
  } else if (!showCursor || !focus) {
6251
6980
  rendered = displayValue;
6252
6981
  } else {
6253
6982
  let out = "";
6254
6983
  for (let i = 0; i < displayValue.length; i++) {
6255
- out += i === cursorOffset ? chalk7.inverse(displayValue[i]) : displayValue[i];
6984
+ out += i === cursorOffset ? chalk8.inverse(displayValue[i]) : displayValue[i];
6256
6985
  }
6257
- if (cursorOffset >= displayValue.length) out += chalk7.inverse(" ");
6986
+ if (cursorOffset >= displayValue.length) out += chalk8.inverse(" ");
6258
6987
  rendered = out;
6259
6988
  }
6260
6989
  return /* @__PURE__ */ jsx(Text, { children: rendered });
@@ -6270,6 +6999,7 @@ init_gemini();
6270
6999
  init_ollama();
6271
7000
  init_openai_compatible();
6272
7001
  init_openai();
7002
+ init_base();
6273
7003
 
6274
7004
  // src/core/router/selector.ts
6275
7005
  init_constants();
@@ -7297,7 +8027,7 @@ init_pricing();
7297
8027
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7298
8028
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7299
8029
  var FETCH_TIMEOUT_MS = 8e3;
7300
- var DEFAULT_CACHE_FILE = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
8030
+ var DEFAULT_CACHE_FILE = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7301
8031
  function normalizeModelId2(id) {
7302
8032
  let s = id.toLowerCase();
7303
8033
  const slash = s.lastIndexOf("/");
@@ -7444,7 +8174,7 @@ var LiveDataProvider = class {
7444
8174
  }
7445
8175
  async saveCache() {
7446
8176
  try {
7447
- await fs11.mkdir(path34.dirname(this.opts.cacheFile), { recursive: true });
8177
+ await fs11.mkdir(path35.dirname(this.opts.cacheFile), { recursive: true });
7448
8178
  const cache = {
7449
8179
  fetchedAt: this.fetchedAt,
7450
8180
  snapshot: this.snapshot ?? void 0,
@@ -7711,9 +8441,50 @@ function guardTokens(text) {
7711
8441
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
7712
8442
  var DISCOVERY_TIMEOUT_MS = 4e3;
7713
8443
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
8444
+ var IDENTITY_SWEEP_INTERVAL_MS = 60 * 1e3;
8445
+ var credentialIdentities = /* @__PURE__ */ new Map();
8446
+ var lastIdentitySweep = 0;
8447
+ var identitySweepTimer;
8448
+ function ensureIdentitySweeper() {
8449
+ if (identitySweepTimer) return;
8450
+ identitySweepTimer = setInterval(() => {
8451
+ sweepExpired(Date.now());
8452
+ if (credentialIdentities.size === 0 && identitySweepTimer) {
8453
+ clearInterval(identitySweepTimer);
8454
+ identitySweepTimer = void 0;
8455
+ }
8456
+ }, IDENTITY_SWEEP_INTERVAL_MS);
8457
+ identitySweepTimer.unref?.();
8458
+ }
8459
+ function sweepExpired(now) {
8460
+ for (const [key, held] of credentialIdentities) {
8461
+ if (now - held.at > DISCOVERY_TTL_MS) credentialIdentities.delete(key);
8462
+ }
8463
+ for (const [key, entry] of cloudDiscoveryCache) {
8464
+ if (now - entry.at > DISCOVERY_TTL_MS) cloudDiscoveryCache.delete(key);
8465
+ }
8466
+ }
8467
+ function credentialIdentity(secret) {
8468
+ if (!secret) return "-";
8469
+ const now = Date.now();
8470
+ if (now - lastIdentitySweep > IDENTITY_SWEEP_INTERVAL_MS) {
8471
+ lastIdentitySweep = now;
8472
+ sweepExpired(now);
8473
+ }
8474
+ const held = credentialIdentities.get(secret);
8475
+ if (held && now - held.at <= DISCOVERY_TTL_MS) return held.id;
8476
+ const id = crypto.randomUUID();
8477
+ credentialIdentities.set(secret, { id, at: now });
8478
+ ensureIdentitySweeper();
8479
+ return id;
8480
+ }
7714
8481
  function discoveryCacheKey(type, cfg) {
7715
- const raw = `${type}|${cfg.apiKey ?? ""}|${cfg.baseUrl ?? ""}`;
7716
- return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 24);
8482
+ return [
8483
+ type,
8484
+ credentialIdentity(cfg.apiKey),
8485
+ credentialIdentity(cfg.authToken),
8486
+ cfg.baseUrl ?? ""
8487
+ ].join("|");
7717
8488
  }
7718
8489
  function applyTierLimits(options, tier, limits) {
7719
8490
  if (!limits) return options;
@@ -8012,7 +8783,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8012
8783
  await Promise.all(providers.map(async (type) => {
8013
8784
  if (!this.selector.isProviderAvailable(type)) return;
8014
8785
  const cfg = config.providers.find((p) => p.type === type);
8015
- if (!cfg?.apiKey) return;
8786
+ if (!cfg || !hasProviderCredential(cfg)) return;
8016
8787
  const cacheKey = discoveryCacheKey(type, cfg);
8017
8788
  let entry = cloudDiscoveryCache.get(cacheKey);
8018
8789
  if (!entry || Date.now() - entry.at > DISCOVERY_TTL_MS) {
@@ -8021,7 +8792,11 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8021
8792
  try {
8022
8793
  const provider = this.createProvider(cfg, seed);
8023
8794
  if (typeof provider.listModels !== "function") return;
8024
- const models = await withTimeout(provider.listModels(), DISCOVERY_TIMEOUT_MS, `${type} model discovery timed out`);
8795
+ const models = await withTimeout(
8796
+ provider.listModels({ staticFallback: false }),
8797
+ DISCOVERY_TIMEOUT_MS,
8798
+ `${type} model discovery timed out`
8799
+ );
8025
8800
  if (!models.length) return;
8026
8801
  entry = { models, ids: models.map((m) => m.id), at: Date.now() };
8027
8802
  cloudDiscoveryCache.set(cacheKey, entry);
@@ -8529,12 +9304,28 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8529
9304
  return this.selector?.getAllAvailableModels() ?? [];
8530
9305
  }
8531
9306
  // ── Private ──────────────────────────────────
9307
+ /**
9308
+ * Why each configured provider failed its availability probe, kept so the
9309
+ * surfaces can say something better than "add an API key" to a user who
9310
+ * already has one. Cleared and refilled by each detectAvailableProviders().
9311
+ */
9312
+ probeFailures = /* @__PURE__ */ new Map();
9313
+ /**
9314
+ * What stopped the configured providers from being usable, if anything.
9315
+ * Empty when every configured provider passed — or when none was configured,
9316
+ * which is a different problem with a different answer.
9317
+ */
9318
+ providerProbeFailures() {
9319
+ return [...this.probeFailures].map(([provider, reason]) => ({ provider, reason }));
9320
+ }
8532
9321
  /** Logs why a configured provider failed its availability probe. */
8533
9322
  emitProbeFailure(type, reason) {
9323
+ this.probeFailures.set(type, reason);
8534
9324
  console.warn(`[router] provider "${type}" is not available: ${reason}`);
8535
9325
  }
8536
9326
  async detectAvailableProviders(configs) {
8537
9327
  const available = /* @__PURE__ */ new Set();
9328
+ this.probeFailures.clear();
8538
9329
  const checks = configs.map(async (cfg) => {
8539
9330
  try {
8540
9331
  const testModel = this.getAnyModelForProvider(cfg.type);
@@ -8544,7 +9335,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8544
9335
  if (ok) available.add(cfg.type);
8545
9336
  else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
8546
9337
  } catch (err) {
8547
- this.emitProbeFailure(cfg.type, err instanceof Error ? err.message : String(err));
9338
+ const reason = err instanceof Error ? err.message : String(err);
9339
+ if (err instanceof ProviderUnreachableError) {
9340
+ available.add(cfg.type);
9341
+ console.warn(`[router] provider "${cfg.type}" probe did not complete: ${reason} \u2014 continuing with it enabled`);
9342
+ return;
9343
+ }
9344
+ this.emitProbeFailure(cfg.type, reason);
8548
9345
  }
8549
9346
  });
8550
9347
  await Promise.allSettled(checks);
@@ -10263,8 +11060,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
10263
11060
  // Media generation can run for a minute; without this a cancelled run
10264
11061
  // still pays for an image nobody will see.
10265
11062
  ...this.signal ? { signal: this.signal } : {},
10266
- saveSnapshot: async (path39, content) => {
10267
- this.store?.addFileSnapshot(this.taskId, path39, content);
11063
+ saveSnapshot: async (path40, content) => {
11064
+ this.store?.addFileSnapshot(this.taskId, path40, content);
10268
11065
  },
10269
11066
  sendPeerSync: (to, syncType, content) => {
10270
11067
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -10467,7 +11264,7 @@ ${assignment.expectedOutput}`;
10467
11264
  const { promisify: promisify4 } = await import('util');
10468
11265
  const execFileAsync2 = promisify4(execFile3);
10469
11266
  for (const artifactPath of artifactPaths) {
10470
- const absolutePath = path34.resolve(this.artifactRoot(), artifactPath);
11267
+ const absolutePath = path35.resolve(this.artifactRoot(), artifactPath);
10471
11268
  try {
10472
11269
  const stat = await fs11.stat(absolutePath);
10473
11270
  if (!stat.isFile()) {
@@ -10502,7 +11299,7 @@ ${assignment.expectedOutput}`;
10502
11299
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
10503
11300
  continue;
10504
11301
  }
10505
- const ext = path34.extname(absolutePath).toLowerCase();
11302
+ const ext = path35.extname(absolutePath).toLowerCase();
10506
11303
  const check = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
10507
11304
  if (check) {
10508
11305
  try {
@@ -10594,7 +11391,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10594
11391
  async checkAcceptance(assignment) {
10595
11392
  const criteria = assignment.acceptance ?? [];
10596
11393
  if (!criteria.length) return [];
10597
- const resolve2 = (target) => path34.resolve(this.artifactRoot(), target);
11394
+ const resolve2 = (target) => path35.resolve(this.artifactRoot(), target);
10598
11395
  return evaluateAcceptance(criteria, assignment.files ?? [], {
10599
11396
  stat: async (target) => {
10600
11397
  try {
@@ -13823,16 +14620,16 @@ function resolveInWorkspace(workspaceRoot, input) {
13823
14620
  if (typeof input !== "string" || input.length === 0) {
13824
14621
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
13825
14622
  }
13826
- const root = path34.resolve(workspaceRoot);
13827
- const abs = path34.isAbsolute(input) ? path34.resolve(input) : path34.resolve(root, input);
13828
- const rel = path34.relative(root, abs);
13829
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34.isAbsolute(rel)) {
14623
+ const root = path35.resolve(workspaceRoot);
14624
+ const abs = path35.isAbsolute(input) ? path35.resolve(input) : path35.resolve(root, input);
14625
+ const rel = path35.relative(root, abs);
14626
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path35.isAbsolute(rel)) {
13830
14627
  throw new WorkspaceSandboxError(input, root);
13831
14628
  }
13832
14629
  try {
13833
- const real = fs31.realpathSync(abs);
13834
- const realRel = path34.relative(root, real);
13835
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34.isAbsolute(realRel))) {
14630
+ const real = fs32.realpathSync(abs);
14631
+ const realRel = path35.relative(root, real);
14632
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path35.isAbsolute(realRel))) {
13836
14633
  throw new WorkspaceSandboxError(input, root);
13837
14634
  }
13838
14635
  } catch (e) {
@@ -14485,7 +15282,7 @@ var GenerateDocumentTool = class extends BaseTool {
14485
15282
  }
14486
15283
  }
14487
15284
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
14488
- await fs11.mkdir(path34.dirname(absPath), { recursive: true });
15285
+ await fs11.mkdir(path35.dirname(absPath), { recursive: true });
14489
15286
  await fs11.writeFile(absPath, bytes);
14490
15287
  const charts = countCharts(content);
14491
15288
  return [
@@ -14706,7 +15503,7 @@ var FileWriteTool = class extends BaseTool {
14706
15503
  } catch {
14707
15504
  }
14708
15505
  }
14709
- await fs11.mkdir(path34.dirname(absPath), { recursive: true });
15506
+ await fs11.mkdir(path35.dirname(absPath), { recursive: true });
14710
15507
  await fs11.writeFile(absPath, content, "utf-8");
14711
15508
  return `Written ${content.length} characters to ${filePath}`;
14712
15509
  }
@@ -15204,7 +16001,7 @@ var ImageAnalyzeTool = class extends BaseTool {
15204
16001
  };
15205
16002
  async function fileToImageAttachment(filePath) {
15206
16003
  const data = await fs11.readFile(filePath);
15207
- const ext = path34.extname(filePath).toLowerCase();
16004
+ const ext = path35.extname(filePath).toLowerCase();
15208
16005
  const mimeMap = {
15209
16006
  ".jpg": "image/jpeg",
15210
16007
  ".jpeg": "image/jpeg",
@@ -15238,14 +16035,14 @@ var PDFCreateTool = class extends BaseTool {
15238
16035
  const filePath = input["path"];
15239
16036
  const content = input["content"];
15240
16037
  const title = input["title"];
15241
- const dir = path34.dirname(filePath);
15242
- if (!fs31.existsSync(dir)) {
15243
- fs31.mkdirSync(dir, { recursive: true });
16038
+ const dir = path35.dirname(filePath);
16039
+ if (!fs32.existsSync(dir)) {
16040
+ fs32.mkdirSync(dir, { recursive: true });
15244
16041
  }
15245
16042
  return new Promise((resolve2, reject) => {
15246
16043
  try {
15247
16044
  const doc = new PDFDocument({ margin: 50 });
15248
- const stream = fs31.createWriteStream(filePath);
16045
+ const stream = fs32.createWriteStream(filePath);
15249
16046
  doc.pipe(stream);
15250
16047
  if (title) {
15251
16048
  doc.info["Title"] = title;
@@ -15323,22 +16120,22 @@ var CodeInterpreterTool = class extends BaseTool {
15323
16120
  }
15324
16121
  cmdPrefix = NODE_CMD;
15325
16122
  }
15326
- const tmpDir = path34.join(this.workspaceRoot, ".cascade", "tmp");
15327
- if (!fs31.existsSync(tmpDir)) {
15328
- fs31.mkdirSync(tmpDir, { recursive: true });
16123
+ const tmpDir = path35.join(this.workspaceRoot, ".cascade", "tmp");
16124
+ if (!fs32.existsSync(tmpDir)) {
16125
+ fs32.mkdirSync(tmpDir, { recursive: true });
15329
16126
  }
15330
16127
  const extension = language === "python" ? "py" : "js";
15331
16128
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
15332
- const filePath = path34.join(tmpDir, fileName);
15333
- fs31.writeFileSync(filePath, code, "utf-8");
16129
+ const filePath = path35.join(tmpDir, fileName);
16130
+ fs32.writeFileSync(filePath, code, "utf-8");
15334
16131
  const execArgs = [filePath, ...args];
15335
16132
  return new Promise((resolve2) => {
15336
16133
  const startMs = Date.now();
15337
16134
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
15338
16135
  const duration = Date.now() - startMs;
15339
16136
  try {
15340
- if (fs31.existsSync(filePath)) {
15341
- fs31.unlinkSync(filePath);
16137
+ if (fs32.existsSync(filePath)) {
16138
+ fs32.unlinkSync(filePath);
15342
16139
  }
15343
16140
  } catch (cleanupErr) {
15344
16141
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -15650,7 +16447,7 @@ var GlobTool = class extends BaseTool {
15650
16447
  };
15651
16448
  async execute(input, _options) {
15652
16449
  const pattern = input["pattern"];
15653
- const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16450
+ const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15654
16451
  const matches = await glob(pattern, {
15655
16452
  cwd: searchPath,
15656
16453
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -15663,7 +16460,7 @@ var GlobTool = class extends BaseTool {
15663
16460
  const withMtime = await Promise.all(
15664
16461
  matches.map(async (rel) => {
15665
16462
  try {
15666
- const stat = await fs11.stat(path34.join(searchPath, rel));
16463
+ const stat = await fs11.stat(path35.join(searchPath, rel));
15667
16464
  return { rel, mtime: stat.mtimeMs };
15668
16465
  } catch {
15669
16466
  return { rel, mtime: 0 };
@@ -15712,7 +16509,7 @@ var GrepTool = class extends BaseTool {
15712
16509
  };
15713
16510
  async execute(input, _options) {
15714
16511
  const pattern = input["pattern"];
15715
- const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16512
+ const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15716
16513
  const globPattern = input["glob"];
15717
16514
  const outputMode = input["output_mode"] ?? "content";
15718
16515
  const context = input["context"] ?? 0;
@@ -15766,12 +16563,12 @@ var GrepTool = class extends BaseTool {
15766
16563
  nodir: true
15767
16564
  });
15768
16565
  } catch {
15769
- files = [path34.relative(searchPath, searchPath) || "."];
16566
+ files = [path35.relative(searchPath, searchPath) || "."];
15770
16567
  }
15771
16568
  const results = [];
15772
16569
  let totalCount = 0;
15773
16570
  for (const rel of files) {
15774
- const abs = path34.join(searchPath, rel);
16571
+ const abs = path35.join(searchPath, rel);
15775
16572
  let content;
15776
16573
  try {
15777
16574
  content = await fs11.readFile(abs, "utf-8");
@@ -16170,10 +16967,10 @@ var ToolRegistry = class extends EventEmitter {
16170
16967
  }
16171
16968
  isIgnored(filePath) {
16172
16969
  if (!filePath) return false;
16173
- const abs = path34.resolve(this.workspaceRoot, filePath);
16174
- const rel = path34.relative(this.workspaceRoot, abs);
16175
- if (!rel || rel.startsWith("..") || path34.isAbsolute(rel)) return true;
16176
- const posixRel = rel.split(path34.sep).join("/");
16970
+ const abs = path35.resolve(this.workspaceRoot, filePath);
16971
+ const rel = path35.relative(this.workspaceRoot, abs);
16972
+ if (!rel || rel.startsWith("..") || path35.isAbsolute(rel)) return true;
16973
+ const posixRel = rel.split(path35.sep).join("/");
16177
16974
  return this.ignoreMatcher.ignores(posixRel);
16178
16975
  }
16179
16976
  };
@@ -16407,22 +17204,22 @@ var FileMcpOAuthStore = class {
16407
17204
  filePath;
16408
17205
  load() {
16409
17206
  try {
16410
- return JSON.parse(fs31.readFileSync(this.filePath, "utf-8"));
17207
+ return JSON.parse(fs32.readFileSync(this.filePath, "utf-8"));
16411
17208
  } catch {
16412
17209
  return void 0;
16413
17210
  }
16414
17211
  }
16415
17212
  save(state) {
16416
- fs31.mkdirSync(path34.dirname(this.filePath), { recursive: true, mode: 448 });
16417
- fs31.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
17213
+ fs32.mkdirSync(path35.dirname(this.filePath), { recursive: true, mode: 448 });
17214
+ fs32.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
16418
17215
  try {
16419
- fs31.chmodSync(this.filePath, 384);
17216
+ fs32.chmodSync(this.filePath, 384);
16420
17217
  } catch {
16421
17218
  }
16422
17219
  }
16423
17220
  clear() {
16424
17221
  try {
16425
- fs31.rmSync(this.filePath);
17222
+ fs32.rmSync(this.filePath);
16426
17223
  } catch {
16427
17224
  }
16428
17225
  }
@@ -17070,7 +17867,7 @@ var TaskAnalyzer = class {
17070
17867
 
17071
17868
  // src/core/router/model-performance-tracker.ts
17072
17869
  init_pricing();
17073
- var DEFAULT_STATS_FILE = path34.join(os10.homedir(), ".cascade", "model-perf.json");
17870
+ var DEFAULT_STATS_FILE = path35.join(os10.homedir(), ".cascade", "model-perf.json");
17074
17871
  var ModelPerformanceTracker = class {
17075
17872
  stats = /* @__PURE__ */ new Map();
17076
17873
  featureStats = /* @__PURE__ */ new Map();
@@ -17110,7 +17907,7 @@ var ModelPerformanceTracker = class {
17110
17907
  async save() {
17111
17908
  if (this.readOnly) return;
17112
17909
  try {
17113
- await fs11.mkdir(path34.dirname(this.statsFile), { recursive: true });
17910
+ await fs11.mkdir(path35.dirname(this.statsFile), { recursive: true });
17114
17911
  const modelsObj = {};
17115
17912
  const featuresObj = {};
17116
17913
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -17658,7 +18455,7 @@ Required capability: ${description.slice(0, 300)}`;
17658
18455
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
17659
18456
  async loadPersistedTools() {
17660
18457
  if (!this.workspacePath || !this.persistEnabled) return;
17661
- const file = path34.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
18458
+ const file = path35.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17662
18459
  try {
17663
18460
  const raw = await fs11.readFile(file, "utf-8");
17664
18461
  const specs = JSON.parse(raw);
@@ -17682,8 +18479,8 @@ Required capability: ${description.slice(0, 300)}`;
17682
18479
  }
17683
18480
  async persist() {
17684
18481
  if (!this.workspacePath || !this.persistEnabled) return;
17685
- const dir = path34.join(this.workspacePath, ".cascade");
17686
- const file = path34.join(dir, DYNAMIC_TOOLS_FILE);
18482
+ const dir = path35.join(this.workspacePath, ".cascade");
18483
+ const file = path35.join(dir, DYNAMIC_TOOLS_FILE);
17687
18484
  try {
17688
18485
  await fs11.mkdir(dir, { recursive: true });
17689
18486
  await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -17703,12 +18500,12 @@ var WorldStateDB = class {
17703
18500
  constructor(workspacePath, debugMode = false) {
17704
18501
  this.workspacePath = workspacePath;
17705
18502
  this.debugMode = debugMode;
17706
- const cascadeDir = path34.join(workspacePath, ".cascade");
17707
- if (!fs31.existsSync(cascadeDir)) {
17708
- fs31.mkdirSync(cascadeDir, { recursive: true });
18503
+ const cascadeDir = path35.join(workspacePath, ".cascade");
18504
+ if (!fs32.existsSync(cascadeDir)) {
18505
+ fs32.mkdirSync(cascadeDir, { recursive: true });
17709
18506
  }
17710
- this.keyPath = path34.join(cascadeDir, "world_state.key");
17711
- this.dbPath = path34.join(cascadeDir, "world_state.db");
18507
+ this.keyPath = path35.join(cascadeDir, "world_state.key");
18508
+ this.dbPath = path35.join(cascadeDir, "world_state.db");
17712
18509
  this.initEncryptionKey();
17713
18510
  this.db = new Database2(this.dbPath);
17714
18511
  this.db.pragma("journal_mode = WAL");
@@ -17765,11 +18562,11 @@ var WorldStateDB = class {
17765
18562
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
17766
18563
  }
17767
18564
  initEncryptionKey() {
17768
- if (fs31.existsSync(this.keyPath)) {
17769
- this.encryptionKey = fs31.readFileSync(this.keyPath);
18565
+ if (fs32.existsSync(this.keyPath)) {
18566
+ this.encryptionKey = fs32.readFileSync(this.keyPath);
17770
18567
  } else {
17771
18568
  this.encryptionKey = crypto.randomBytes(32);
17772
- fs31.writeFileSync(this.keyPath, this.encryptionKey);
18569
+ fs32.writeFileSync(this.keyPath, this.encryptionKey);
17773
18570
  }
17774
18571
  }
17775
18572
  encrypt(text) {
@@ -18055,9 +18852,9 @@ var WorldStateDB = class {
18055
18852
  dumpDebugIfNeeded() {
18056
18853
  if (!this.debugMode) return;
18057
18854
  try {
18058
- const dumpPath = path34.join(os10.tmpdir(), "cascade_world_state_debug.json");
18855
+ const dumpPath = path35.join(os10.tmpdir(), "cascade_world_state_debug.json");
18059
18856
  const entries = this.getAllEntries();
18060
- fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18857
+ fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18061
18858
  } catch (err) {
18062
18859
  console.error("Failed to dump debug world state", err);
18063
18860
  }
@@ -18114,7 +18911,7 @@ var ResumeStore = class {
18114
18911
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
18115
18912
  };
18116
18913
  await promises.mkdir(this.dir, { recursive: true });
18117
- const finalPath = path34.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18914
+ const finalPath = path35.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18118
18915
  const tempPath = `${finalPath}.${process.pid}.tmp`;
18119
18916
  await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
18120
18917
  await promises.rename(tempPath, finalPath);
@@ -18137,7 +18934,7 @@ var ResumeStore = class {
18137
18934
  const checkpoints = [];
18138
18935
  for (const candidate of candidates2) {
18139
18936
  try {
18140
- const raw = await promises.readFile(path34.join(this.dir, candidate.name), "utf-8");
18937
+ const raw = await promises.readFile(path35.join(this.dir, candidate.name), "utf-8");
18141
18938
  const parsed = JSON.parse(raw);
18142
18939
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
18143
18940
  } catch {
@@ -18176,7 +18973,7 @@ var ResumeStore = class {
18176
18973
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
18177
18974
  if (!source) return null;
18178
18975
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
18179
- await promises.rename(path34.join(this.dir, source), path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
18976
+ await promises.rename(path35.join(this.dir, source), path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
18180
18977
  return { checkpoint: target, claimId };
18181
18978
  } catch {
18182
18979
  return null;
@@ -18186,8 +18983,8 @@ var ResumeStore = class {
18186
18983
  async release(claimId) {
18187
18984
  try {
18188
18985
  await promises.rename(
18189
- path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18190
- path34.join(this.dir, `${claimId}.json`)
18986
+ path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18987
+ path35.join(this.dir, `${claimId}.json`)
18191
18988
  );
18192
18989
  } catch {
18193
18990
  }
@@ -18195,7 +18992,7 @@ var ResumeStore = class {
18195
18992
  /** Discard a claim for good — the resumed run has taken ownership. */
18196
18993
  async settle(claimId) {
18197
18994
  try {
18198
- await promises.rm(path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
18995
+ await promises.rm(path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
18199
18996
  } catch {
18200
18997
  }
18201
18998
  }
@@ -18224,8 +19021,8 @@ var ResumeStore = class {
18224
19021
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
18225
19022
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
18226
19023
  await promises.rename(
18227
- path34.join(this.dir, name),
18228
- path34.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
19024
+ path35.join(this.dir, name),
19025
+ path35.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
18229
19026
  );
18230
19027
  reclaimed++;
18231
19028
  }
@@ -18241,7 +19038,7 @@ var ResumeStore = class {
18241
19038
  await Promise.all(entries.map(async (name) => {
18242
19039
  const match = FILE_PATTERN.exec(name);
18243
19040
  if (match?.[2] === wanted) {
18244
- await promises.rm(path34.join(this.dir, name), { force: true });
19041
+ await promises.rm(path35.join(this.dir, name), { force: true });
18245
19042
  }
18246
19043
  }));
18247
19044
  } catch {
@@ -18256,7 +19053,7 @@ var ResumeStore = class {
18256
19053
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
18257
19054
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
18258
19055
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
18259
- (name) => promises.rm(path34.join(this.dir, name), { force: true })
19056
+ (name) => promises.rm(path35.join(this.dir, name), { force: true })
18260
19057
  ));
18261
19058
  } catch {
18262
19059
  }
@@ -18633,7 +19430,7 @@ var WorkspaceIndex = class {
18633
19430
  isIgnored;
18634
19431
  chunker;
18635
19432
  constructor(opts) {
18636
- this.root = path34.resolve(opts.root);
19433
+ this.root = path35.resolve(opts.root);
18637
19434
  this.db = opts.db;
18638
19435
  this.namespace = opts.namespace ?? "code";
18639
19436
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -18665,14 +19462,14 @@ var WorkspaceIndex = class {
18665
19462
  return;
18666
19463
  }
18667
19464
  for (const entry of entries) {
18668
- const abs = path34.join(dir, entry.name);
19465
+ const abs = path35.join(dir, entry.name);
18669
19466
  if (entry.isDirectory()) {
18670
19467
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
18671
19468
  await walk(abs);
18672
19469
  } else if (entry.isFile()) {
18673
- const ext = path34.extname(entry.name).slice(1).toLowerCase();
19470
+ const ext = path35.extname(entry.name).slice(1).toLowerCase();
18674
19471
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
18675
- out.push(path34.relative(this.root, abs));
19472
+ out.push(path35.relative(this.root, abs));
18676
19473
  }
18677
19474
  }
18678
19475
  };
@@ -18702,7 +19499,7 @@ var WorkspaceIndex = class {
18702
19499
  const entries = [];
18703
19500
  for (const rel of rels) {
18704
19501
  try {
18705
- const abs = path34.join(this.root, rel);
19502
+ const abs = path35.join(this.root, rel);
18706
19503
  const stat = await fs11.stat(abs);
18707
19504
  if (stat.size > this.maxFileBytes) continue;
18708
19505
  const buf = await fs11.readFile(abs);
@@ -19192,13 +19989,13 @@ var Cascade = class _Cascade extends EventEmitter {
19192
19989
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
19193
19990
  try {
19194
19991
  this.router.setDeadModelStore(new DeadModelStore(
19195
- fileDeadModelPersistence(path34.join(workspacePath, ".cascade", "dead-models.json"))
19992
+ fileDeadModelPersistence(path35.join(workspacePath, ".cascade", "dead-models.json"))
19196
19993
  ));
19197
19994
  } catch {
19198
19995
  }
19199
19996
  try {
19200
19997
  this.resumeStore = new ResumeStore({
19201
- dir: path34.join(workspacePath, ".cascade", "resume")
19998
+ dir: path35.join(workspacePath, ".cascade", "resume")
19202
19999
  });
19203
20000
  } catch {
19204
20001
  }
@@ -19286,8 +20083,8 @@ var Cascade = class _Cascade extends EventEmitter {
19286
20083
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
19287
20084
  return;
19288
20085
  }
19289
- const dbPath = ci.dbPath || path34.join(this.workspacePath, ".cascade", "code-index.db");
19290
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
20086
+ const dbPath = ci.dbPath || path35.join(this.workspacePath, ".cascade", "code-index.db");
20087
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
19291
20088
  const db = new Database2(dbPath);
19292
20089
  db.pragma("journal_mode = WAL");
19293
20090
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -20592,7 +21389,16 @@ ${finalOutput}` : ""
20592
21389
  const selector = this.router.getSelector();
20593
21390
  const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
20594
21391
  if (!model) {
20595
- throw new Error("No model is available for a fast answer \u2014 add a provider API key first.");
21392
+ const failures2 = this.router.providerProbeFailures();
21393
+ const context = failures2.length ? ` Configured providers: ${failures2.map((f) => `${f.provider}: ${f.reason}`).join("; ")}` : "";
21394
+ if (options.fastAnswerModel) {
21395
+ throw new Error(
21396
+ `No model is available for a fast answer: the pinned model "${options.fastAnswerModel}" could not be resolved \u2014 check it is spelled correctly and served by a configured provider.${context}`
21397
+ );
21398
+ }
21399
+ throw new Error(
21400
+ failures2.length ? `No model is available for a fast answer.${context}` : "No model is available for a fast answer \u2014 add a provider API key first."
21401
+ );
20596
21402
  }
20597
21403
  this.recordDecision(
20598
21404
  "model",
@@ -20726,14 +21532,14 @@ ${finalOutput}` : ""
20726
21532
  init_constants();
20727
21533
  var CLOUD_SESSION_FILE = "cloud-session.json";
20728
21534
  function globalDir(homeDir = os10.homedir()) {
20729
- return path34.join(homeDir, GLOBAL_CONFIG_DIR);
21535
+ return path35.join(homeDir, GLOBAL_CONFIG_DIR);
20730
21536
  }
20731
21537
  function cloudSessionPath(dir = globalDir()) {
20732
- return path34.join(dir, CLOUD_SESSION_FILE);
21538
+ return path35.join(dir, CLOUD_SESSION_FILE);
20733
21539
  }
20734
21540
  function loadCloudSession(dir = globalDir()) {
20735
21541
  try {
20736
- const raw = fs31.readFileSync(cloudSessionPath(dir), "utf-8");
21542
+ const raw = fs32.readFileSync(cloudSessionPath(dir), "utf-8");
20737
21543
  const parsed = JSON.parse(raw);
20738
21544
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20739
21545
  return parsed;
@@ -20742,17 +21548,17 @@ function loadCloudSession(dir = globalDir()) {
20742
21548
  }
20743
21549
  }
20744
21550
  function saveCloudSession(session, dir = globalDir()) {
20745
- fs31.mkdirSync(dir, { recursive: true, mode: 448 });
21551
+ fs32.mkdirSync(dir, { recursive: true, mode: 448 });
20746
21552
  const file = cloudSessionPath(dir);
20747
- fs31.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21553
+ fs32.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20748
21554
  try {
20749
- fs31.chmodSync(file, 384);
21555
+ fs32.chmodSync(file, 384);
20750
21556
  } catch {
20751
21557
  }
20752
21558
  }
20753
21559
  function clearCloudSession(dir = globalDir()) {
20754
21560
  try {
20755
- fs31.rmSync(cloudSessionPath(dir));
21561
+ fs32.rmSync(cloudSessionPath(dir));
20756
21562
  } catch {
20757
21563
  }
20758
21564
  }
@@ -20924,16 +21730,16 @@ var CloudClient = class _CloudClient {
20924
21730
  return (await this.refresh()).accessToken;
20925
21731
  }
20926
21732
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
20927
- async authed(path39, init = {}) {
20928
- const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21733
+ async authed(path40, init = {}) {
21734
+ const call = (token) => fetch(this.url(path40), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
20929
21735
  let res = await call(await this.accessToken());
20930
21736
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
20931
21737
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
20932
21738
  const text = await res.text();
20933
21739
  return text ? JSON.parse(text) : {};
20934
21740
  }
20935
- authedGet(path39) {
20936
- return this.authed(path39);
21741
+ authedGet(path40) {
21742
+ return this.authed(path40);
20937
21743
  }
20938
21744
  async fetchMe(token) {
20939
21745
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21794,7 +22600,7 @@ function StatusBarInternal({
21794
22600
  const savedStr = savedUsd > 0 ? ` \xB7 saved $${savedUsd.toFixed(4)}` : "";
21795
22601
  const rightStr = ` ${formatTokens(tokens)} \xB7 $${costUsd.toFixed(4)}${savedStr} ${isExecuting ? "\u26A1" : "\xB7"} `;
21796
22602
  const gap = Math.max(0, width - leftStr.length - rightStr.length);
21797
- const line = chalk7.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk7.bold(leftStr) + " ".repeat(gap) + rightStr);
22603
+ const line = chalk8.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk8.bold(leftStr) + " ".repeat(gap) + rightStr);
21798
22604
  return /* @__PURE__ */ jsx(Text, { children: line });
21799
22605
  }
21800
22606
  function formatTokens(n) {
@@ -22263,7 +23069,7 @@ function renderContent(content, theme) {
22263
23069
  i
22264
23070
  );
22265
23071
  }
22266
- const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk7.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk7.bgHex(theme.colors.border)(t));
23072
+ const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk8.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk8.bgHex(theme.colors.border)(t));
22267
23073
  return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: rendered }, i);
22268
23074
  });
22269
23075
  }
@@ -22707,9 +23513,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22707
23513
  }
22708
23514
  } catch {
22709
23515
  }
22710
- const configPath = path34.join(workspacePath, ".cascade", "config.json");
23516
+ const configPath = path35.join(workspacePath, ".cascade", "config.json");
22711
23517
  try {
22712
- await fs11.mkdir(path34.dirname(configPath), { recursive: true });
23518
+ await fs11.mkdir(path35.dirname(configPath), { recursive: true });
22713
23519
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22714
23520
  } catch (err) {
22715
23521
  const msg = err instanceof Error ? err.message : String(err);
@@ -22761,9 +23567,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22761
23567
  if (msg.includes("non-text parts")) return;
22762
23568
  originalLog(...args);
22763
23569
  };
22764
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
23570
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
22765
23571
  storeRef.current = store;
22766
- globalStoreRef.current = new MemoryStore(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
23572
+ globalStoreRef.current = new MemoryStore(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22767
23573
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
22768
23574
  setIdentities(identityRows);
22769
23575
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -22843,7 +23649,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22843
23649
  onThemeChange: (name) => setTheme(getTheme(name)),
22844
23650
  onExport: async (fmt) => {
22845
23651
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22846
- const exportPath = path34.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
23652
+ const exportPath = path35.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22847
23653
  if (fmt === "json") {
22848
23654
  await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
22849
23655
  } else {
@@ -23751,7 +24557,7 @@ function stringifySlashOutput(val) {
23751
24557
  }
23752
24558
  async function searchSessionsAndMessages(query, workspacePath) {
23753
24559
  if (!query) return "Usage: /search <query>";
23754
- const dbPath = path34.join(workspacePath, CASCADE_DB_FILE);
24560
+ const dbPath = path35.join(workspacePath, CASCADE_DB_FILE);
23755
24561
  try {
23756
24562
  await fs11.access(dbPath);
23757
24563
  } catch {
@@ -23785,7 +24591,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
23785
24591
  async function diagnoseRuntime(config, workspacePath) {
23786
24592
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
23787
24593
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
23788
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24594
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23789
24595
  try {
23790
24596
  const sessions2 = store.listSessions(void 0, 3);
23791
24597
  return [
@@ -23804,7 +24610,7 @@ async function diagnoseRuntime(config, workspacePath) {
23804
24610
  }
23805
24611
  async function showRecentLogs(args, workspacePath) {
23806
24612
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
23807
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24613
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23808
24614
  try {
23809
24615
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
23810
24616
  if (!logs.length) return "No recent runtime logs.";
@@ -23816,7 +24622,7 @@ async function showRecentLogs(args, workspacePath) {
23816
24622
  async function loadSessionSnapshot(args, workspacePath) {
23817
24623
  const sessionId = args[0];
23818
24624
  if (!sessionId) return "Usage: /resume <sessionId>";
23819
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24625
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23820
24626
  try {
23821
24627
  const session = store.getSession(sessionId);
23822
24628
  if (!session) return `Session not found: ${sessionId}`;
@@ -24236,9 +25042,9 @@ function SetupWizard({ workspacePath, onComplete }) {
24236
25042
  ...anyAuto ? { cascadeAuto: true } : {}
24237
25043
  };
24238
25044
  const config = CascadeConfigSchema.parse(rawConfig);
24239
- const configDir = path34.join(workspacePath, ".cascade");
25045
+ const configDir = path35.join(workspacePath, ".cascade");
24240
25046
  await fs11.mkdir(configDir, { recursive: true });
24241
- const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
25047
+ const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
24242
25048
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
24243
25049
  savedConfigRef.current = config;
24244
25050
  dispatchRef.current({ type: "GO_DONE" });
@@ -24579,25 +25385,25 @@ function printTelemetryBanner() {
24579
25385
  async function initCommand(workspacePath = process.cwd()) {
24580
25386
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
24581
25387
  try {
24582
- const configDir = path34.join(workspacePath, ".cascade");
25388
+ const configDir = path35.join(workspacePath, ".cascade");
24583
25389
  await fs11.mkdir(configDir, { recursive: true });
24584
- const mdPath = path34.join(workspacePath, "CASCADE.md");
25390
+ const mdPath = path35.join(workspacePath, "CASCADE.md");
24585
25391
  if (!await fileExists(mdPath)) {
24586
25392
  await createDefaultCascadeMd(workspacePath);
24587
- spin.succeed(chalk7.green("Created CASCADE.md"));
25393
+ spin.succeed(chalk8.green("Created CASCADE.md"));
24588
25394
  }
24589
- const ignorePath = path34.join(workspacePath, ".cascadeignore");
25395
+ const ignorePath = path35.join(workspacePath, ".cascadeignore");
24590
25396
  if (!await fileExists(ignorePath)) {
24591
25397
  await createDefaultIgnoreFile(workspacePath);
24592
- spin.succeed(chalk7.green("Created .cascadeignore"));
25398
+ spin.succeed(chalk8.green("Created .cascadeignore"));
24593
25399
  }
24594
25400
  spin.stop();
24595
25401
  console.log();
24596
- console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
25402
+ console.log(chalk8.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
24597
25403
  console.log();
24598
- const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
25404
+ const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
24599
25405
  if (await fileExists(configPath)) {
24600
- console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
25406
+ console.log(chalk8.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
24601
25407
  console.log();
24602
25408
  }
24603
25409
  const config = await runSetupWizard(workspacePath);
@@ -24605,10 +25411,10 @@ async function initCommand(workspacePath = process.cwd()) {
24605
25411
  await cm.load();
24606
25412
  await cm.updateConfig(config);
24607
25413
  console.log();
24608
- console.log(chalk7.green(" \u25C8 Setup complete! Run `cascade` to start."));
25414
+ console.log(chalk8.green(" \u25C8 Setup complete! Run `cascade` to start."));
24609
25415
  console.log();
24610
25416
  } catch (err) {
24611
- console.error(chalk7.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
25417
+ console.error(chalk8.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
24612
25418
  process.exit(1);
24613
25419
  }
24614
25420
  }
@@ -24623,7 +25429,20 @@ async function fileExists(p) {
24623
25429
 
24624
25430
  // src/cli/commands/doctor.ts
24625
25431
  init_constants();
25432
+
25433
+ // src/config/credential-discovery.ts
25434
+ init_revoked_credentials();
24626
25435
  var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
25436
+ var ANTHROPIC_OAUTH_WARNING = "Anthropic does not permit third-party tools to route requests through Claude subscription credentials, and blocks them server-side. Use an API key from the Claude Console, or ANTHROPIC_AUTH_TOKEN with a gateway. See code.claude.com/docs/en/legal-and-compliance.";
25437
+ var OPENAI_COMPATIBLE_ENV = [
25438
+ { id: "openrouter", env: "OPENROUTER_API_KEY", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1" },
25439
+ { id: "groq", env: "GROQ_API_KEY", label: "Groq", baseUrl: "https://api.groq.com/openai/v1" },
25440
+ { id: "deepseek", env: "DEEPSEEK_API_KEY", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
25441
+ { id: "xai", env: "XAI_API_KEY", label: "xAI", baseUrl: "https://api.x.ai/v1" },
25442
+ { id: "mistral", env: "MISTRAL_API_KEY", label: "Mistral", baseUrl: "https://api.mistral.ai/v1" },
25443
+ { id: "together", env: "TOGETHER_API_KEY", label: "Together AI", baseUrl: "https://api.together.xyz/v1" },
25444
+ { id: "fireworks", env: "FIREWORKS_API_KEY", label: "Fireworks AI", baseUrl: "https://api.fireworks.ai/inference/v1" }
25445
+ ];
24627
25446
  async function readJson(file) {
24628
25447
  try {
24629
25448
  const raw = await fs11.readFile(file, "utf-8");
@@ -24647,36 +25466,118 @@ function fromEnv(env) {
24647
25466
  for (const { env: name, provider } of map) {
24648
25467
  const secret = str(env[name]);
24649
25468
  if (!secret || seen.has(provider)) continue;
25469
+ if (isSubscriptionToken(secret)) {
25470
+ out.push({
25471
+ provider,
25472
+ sourceTool: `Environment (${name})`,
25473
+ kind: "oauth",
25474
+ secret,
25475
+ directlyUsable: false,
25476
+ warning: ANTHROPIC_OAUTH_WARNING
25477
+ });
25478
+ continue;
25479
+ }
24650
25480
  seen.add(provider);
24651
- out.push({ provider, sourceTool: `Environment (${name})`, kind: "api-key", secret, directlyUsable: true });
25481
+ const gateway = provider === "anthropic" ? str(env["ANTHROPIC_BASE_URL"]) : void 0;
25482
+ out.push({
25483
+ provider,
25484
+ sourceTool: `Environment (${name})`,
25485
+ kind: "api-key",
25486
+ secret,
25487
+ directlyUsable: true,
25488
+ ...gateway ? { baseUrl: gateway } : {}
25489
+ });
25490
+ }
25491
+ const authToken = str(env["ANTHROPIC_AUTH_TOKEN"]);
25492
+ if (authToken && !seen.has("anthropic")) {
25493
+ seen.add("anthropic");
25494
+ if (isSubscriptionToken(authToken)) {
25495
+ out.push({
25496
+ provider: "anthropic",
25497
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25498
+ kind: "oauth",
25499
+ secret: authToken,
25500
+ directlyUsable: false,
25501
+ warning: ANTHROPIC_OAUTH_WARNING
25502
+ });
25503
+ } else {
25504
+ const gateway = str(env["ANTHROPIC_BASE_URL"]);
25505
+ out.push({
25506
+ provider: "anthropic",
25507
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25508
+ kind: "bearer",
25509
+ secret: authToken,
25510
+ directlyUsable: Boolean(gateway),
25511
+ ...gateway ? { baseUrl: gateway } : {},
25512
+ ...gateway ? {} : {
25513
+ warning: "A bearer token is only valid at the gateway that issued it. Set ANTHROPIC_BASE_URL to that gateway, or configure `baseUrl` for the anthropic provider, before linking it."
25514
+ }
25515
+ });
25516
+ }
25517
+ }
25518
+ const azureKey = str(env["AZURE_OPENAI_KEY"]) ?? str(env["AZURE_OPENAI_API_KEY"]);
25519
+ if (azureKey) {
25520
+ const endpoint = str(env["AZURE_OPENAI_ENDPOINT"]);
25521
+ const deployment = str(env["AZURE_OPENAI_DEPLOYMENT"]) ?? str(env["AZURE_OPENAI_DEPLOYMENT_NAME"]);
25522
+ const apiVersion = str(env["AZURE_OPENAI_API_VERSION"]);
25523
+ const missing = [
25524
+ !endpoint && "AZURE_OPENAI_ENDPOINT",
25525
+ !deployment && "AZURE_OPENAI_DEPLOYMENT"
25526
+ ].filter((x) => typeof x === "string");
25527
+ out.push({
25528
+ provider: "azure",
25529
+ sourceTool: "Environment (AZURE_OPENAI_KEY)",
25530
+ kind: "api-key",
25531
+ secret: azureKey,
25532
+ directlyUsable: missing.length === 0,
25533
+ ...endpoint ? { baseUrl: endpoint } : {},
25534
+ ...deployment ? { deploymentName: deployment } : {},
25535
+ ...apiVersion ? { apiVersion } : {},
25536
+ ...missing.length ? { warning: `Azure also needs ${missing.join(" and ")} \u2014 a key alone cannot address a deployment.` } : {}
25537
+ });
25538
+ }
25539
+ for (const { id, env: name, label, baseUrl } of OPENAI_COMPATIBLE_ENV) {
25540
+ const secret = str(env[name]);
25541
+ if (!secret) continue;
25542
+ out.push({
25543
+ provider: "openai-compatible",
25544
+ sourceTool: `Environment (${name}) \u2014 ${label}`,
25545
+ kind: "api-key",
25546
+ secret,
25547
+ directlyUsable: true,
25548
+ baseUrl,
25549
+ serviceId: id
25550
+ });
24652
25551
  }
24653
25552
  return out;
24654
25553
  }
24655
- async function fromClaudeCode(home) {
24656
- const file = path34.join(home, ".claude", ".credentials.json");
25554
+ async function fromClaudeCode(home, env) {
25555
+ const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path35.join(home, ".claude");
25556
+ const file = path35.join(dir, ".credentials.json");
24657
25557
  const data = await readJson(file);
24658
25558
  if (!data) return [];
25559
+ const found = [];
25560
+ const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
25561
+ if (apiKey) {
25562
+ found.push({ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file });
25563
+ }
24659
25564
  const oauth = data["claudeAiOauth"];
24660
25565
  const oauthToken = str(oauth?.["accessToken"]);
24661
25566
  if (oauthToken) {
24662
- return [{
25567
+ found.push({
24663
25568
  provider: "anthropic",
24664
25569
  sourceTool: "Claude Code",
24665
25570
  kind: "oauth",
24666
25571
  secret: oauthToken,
24667
- directlyUsable: true,
24668
- warning: TOS_WARNING,
25572
+ directlyUsable: false,
25573
+ warning: ANTHROPIC_OAUTH_WARNING,
24669
25574
  sourcePath: file
24670
- }];
24671
- }
24672
- const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
24673
- if (apiKey) {
24674
- return [{ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file }];
25575
+ });
24675
25576
  }
24676
- return [];
25577
+ return found;
24677
25578
  }
24678
25579
  async function fromCodex(home) {
24679
- const file = path34.join(home, ".codex", "auth.json");
25580
+ const file = path35.join(home, ".codex", "auth.json");
24680
25581
  const data = await readJson(file);
24681
25582
  if (!data) return [];
24682
25583
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -24699,7 +25600,7 @@ async function fromCodex(home) {
24699
25600
  return [];
24700
25601
  }
24701
25602
  async function fromGemini(home) {
24702
- const file = path34.join(home, ".gemini", "oauth_creds.json");
25603
+ const file = path35.join(home, ".gemini", "oauth_creds.json");
24703
25604
  const data = await readJson(file);
24704
25605
  const accessToken = str(data?.["access_token"]);
24705
25606
  if (!accessToken) return [];
@@ -24715,7 +25616,7 @@ async function fromGemini(home) {
24715
25616
  }
24716
25617
  async function fromCopilot(home) {
24717
25618
  for (const name of ["apps.json", "hosts.json"]) {
24718
- const file = path34.join(home, ".config", "github-copilot", name);
25619
+ const file = path35.join(home, ".config", "github-copilot", name);
24719
25620
  const data = await readJson(file);
24720
25621
  if (!data) continue;
24721
25622
  for (const value of Object.values(data)) {
@@ -24740,7 +25641,7 @@ async function discoverCredentials(opts = {}) {
24740
25641
  const env = opts.env ?? process.env;
24741
25642
  const groups = await Promise.all([
24742
25643
  Promise.resolve(fromEnv(env)),
24743
- fromClaudeCode(home),
25644
+ fromClaudeCode(home, env),
24744
25645
  fromCodex(home),
24745
25646
  fromGemini(home),
24746
25647
  fromCopilot(home)
@@ -24751,10 +25652,257 @@ function maskSecret(secret) {
24751
25652
  if (secret.length <= 8) return "\u2022\u2022\u2022\u2022";
24752
25653
  return `${secret.slice(0, 6)}\u2026${secret.slice(-2)}`;
24753
25654
  }
25655
+ async function linkCommand(target, options = {}) {
25656
+ const found = await discoverCredentials();
25657
+ if (found.length === 0) {
25658
+ console.log(chalk8.yellow("\n No reusable credentials found.\n"));
25659
+ console.log(chalk8.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
25660
+ console.log(chalk8.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
25661
+ return;
25662
+ }
25663
+ if (!target) {
25664
+ printDiscovered(found);
25665
+ return;
25666
+ }
25667
+ const resolved = normalizeProvider(target);
25668
+ if (!resolved) {
25669
+ const services2 = OPENAI_COMPATIBLE_ENV.map((s) => s.id).join(", ");
25670
+ console.log(chalk8.red(`
25671
+ Unknown provider "${target}".`));
25672
+ console.log(chalk8.gray(` Use one of: anthropic, openai, gemini, azure, openai-compatible`));
25673
+ console.log(chalk8.gray(` \u2026or name a compatible service directly: ${services2}
25674
+ `));
25675
+ return;
25676
+ }
25677
+ const { provider, serviceId } = resolved;
25678
+ const candidates2 = found.filter((c) => c.provider === provider && (!serviceId || c.serviceId === serviceId));
25679
+ const services = [...new Set(candidates2.map((c) => c.serviceId).filter((id) => !!id))];
25680
+ if (!serviceId && services.length > 1) {
25681
+ console.log(chalk8.yellow(`
25682
+ Keys for several OpenAI-compatible services are set, and "${target}" does not say which.`));
25683
+ console.log(chalk8.gray(" Name the one you mean:\n"));
25684
+ for (const id of services) console.log(chalk8.cyan(` cascade link ${id}`));
25685
+ console.log("");
25686
+ return;
25687
+ }
25688
+ const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
25689
+ if (!chosen) {
25690
+ console.log(chalk8.yellow(`
25691
+ No detected credential maps to "${provider}".
25692
+ `));
25693
+ return;
25694
+ }
25695
+ const cm = new ConfigManager(options.workspace ?? process.cwd());
25696
+ await cm.load();
25697
+ const configured = cm.getConfig().providers;
25698
+ const routedByConfig = isRoutedByConfig(chosen, configured);
25699
+ if (!chosen.directlyUsable && !routedByConfig) {
25700
+ console.log(chalk8.yellow(`
25701
+ Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
25702
+ if (chosen.warning) console.log(chalk8.gray(` ${chosen.warning}`));
25703
+ console.log(chalk8.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
25704
+ if (options.acceptRisk && chosen.kind === "oauth") {
25705
+ console.log(chalk8.gray(" --accept-risk no longer applies \u2014 a subscription token is refused by the"));
25706
+ console.log(chalk8.gray(" provider itself, so there is no working configuration to opt into.\n"));
25707
+ }
25708
+ console.log(chalk8.gray(` Set ${chalk8.white(envKeyFor(provider))} instead, or add a key with `) + chalk8.cyan("cascade init") + chalk8.gray(".\n"));
25709
+ return;
25710
+ }
25711
+ const adopted = await adoptCredential(chosen, cm);
25712
+ if (!adopted) return;
25713
+ console.log(chalk8.green(`
25714
+ \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
25715
+ if (chosen.kind === "bearer") {
25716
+ console.log(chalk8.gray(" Adopted as a bearer token \u2014 set `baseUrl` to the gateway that issued it."));
25717
+ }
25718
+ console.log(chalk8.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
25719
+ }
25720
+ function isRoutedByConfig(cred, configured) {
25721
+ if (cred.provider === "azure") {
25722
+ return configured.some((p) => p.type === "azure" && p.deploymentName?.trim() && p.baseUrl?.trim());
25723
+ }
25724
+ return cred.kind === "bearer" && configured.some((p) => p.type === cred.provider && p.baseUrl?.trim());
25725
+ }
25726
+ function willAdoptFromConfig(cred, configured) {
25727
+ if (cred.provider === "azure") {
25728
+ const target = cred.baseUrl?.trim();
25729
+ const deployment = cred.deploymentName?.trim();
25730
+ if (target && deployment) return !azureRoutedTarget(deployment, target, configured).collision;
25731
+ return azureDeploymentsForCredential(cred, configured).length > 0;
25732
+ }
25733
+ return cred.directlyUsable || isRoutedByConfig(cred, configured);
25734
+ }
25735
+ function azureRoutedTarget(deployment, target, configured) {
25736
+ const named = (p) => p.type === "azure" && (p.deploymentName?.trim() ?? "") === deployment;
25737
+ const existing = configured.find((p) => named(p) && (sameAzureEndpoint(p.baseUrl, target) || !p.baseUrl?.trim()));
25738
+ if (existing) return { existing, collision: false };
25739
+ return {
25740
+ collision: configured.some((p) => named(p) && !!p.baseUrl?.trim() && !sameAzureEndpoint(p.baseUrl, target))
25741
+ };
25742
+ }
25743
+ function azureDeploymentsForCredential(cred, configured) {
25744
+ const routing = resolveAzureRouting(configured, configured, {
25745
+ endpoint: cred.baseUrl,
25746
+ deployment: cred.deploymentName
25747
+ });
25748
+ return routing.ok ? routing.rows : [];
25749
+ }
25750
+ function azureRoutingForCredential(cred, configured) {
25751
+ return resolveAzureRouting(configured, configured, {
25752
+ endpoint: cred.baseUrl,
25753
+ deployment: cred.deploymentName
25754
+ });
25755
+ }
25756
+ function printDiscovered(found) {
25757
+ console.log(chalk8.magenta("\n \u25C8 Detected credentials\n"));
25758
+ for (const c of found) {
25759
+ const usable = c.directlyUsable ? chalk8.green("usable") : chalk8.yellow("needs vendor backend");
25760
+ const kind = c.kind === "oauth" ? chalk8.yellow("subscription") : c.kind === "bearer" ? chalk8.cyan("bearer") : chalk8.gray("api-key");
25761
+ console.log(` ${chalk8.white(c.provider.padEnd(18))} ${chalk8.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
25762
+ console.log(chalk8.gray(` from ${c.sourceTool}`));
25763
+ if (c.warning) console.log(chalk8.yellow(` \u26A0 ${c.warning}`));
25764
+ }
25765
+ console.log(chalk8.gray("\n Adopt one with: ") + chalk8.cyan("cascade link <provider>"));
25766
+ console.log(chalk8.gray(" Subscription tokens are listed for visibility and cannot be adopted.\n"));
25767
+ }
25768
+ function envKeyFor(provider) {
25769
+ if (provider === "openai") return "OPENAI_API_KEY";
25770
+ if (provider === "gemini") return "GEMINI_API_KEY";
25771
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
25772
+ if (provider === "azure") return "AZURE_OPENAI_KEY";
25773
+ return "the provider's API key";
25774
+ }
25775
+ function normalizeProvider(target) {
25776
+ const t = target.toLowerCase();
25777
+ if (t === "anthropic" || t === "claude" || t === "claude-code") return { provider: "anthropic" };
25778
+ if (t === "openai" || t === "codex" || t === "gpt") return { provider: "openai" };
25779
+ if (t === "gemini" || t === "google") return { provider: "gemini" };
25780
+ if (t === "azure" || t === "azure-openai") return { provider: "azure" };
25781
+ if (t === "openai-compatible" || t === "compatible") return { provider: "openai-compatible" };
25782
+ const service = OPENAI_COMPATIBLE_ENV.find((s) => s.id === t || s.label.toLowerCase() === t);
25783
+ if (service) return { provider: "openai-compatible", serviceId: service.id };
25784
+ return null;
25785
+ }
25786
+ async function adoptCredential(cred, cm) {
25787
+ const config = cm.getConfig();
25788
+ const azureDeployments = cred.provider === "azure" ? config.providers.filter((p) => p.type === "azure" && p.deploymentName?.trim()) : [];
25789
+ if (azureDeployments.length > 0) {
25790
+ const target = cred.baseUrl?.trim();
25791
+ const deployment = cred.deploymentName?.trim();
25792
+ if (target && deployment) {
25793
+ const { existing: existing2, collision: claimedElsewhere } = azureRoutedTarget(deployment, target, config.providers);
25794
+ if (claimedElsewhere) {
25795
+ console.log(chalk8.yellow(`
25796
+ A different Azure resource already has a deployment named "${deployment}".`));
25797
+ console.log(chalk8.gray(" Deployment names are model ids in Cascade, so two resources cannot share one \u2014"));
25798
+ console.log(chalk8.gray(" the second would never be selected. Rename one deployment, or remove the entry"));
25799
+ console.log(chalk8.gray(" you no longer use, then run this again.\n"));
25800
+ return false;
25801
+ }
25802
+ const patch2 = {
25803
+ apiKey: cred.secret,
25804
+ credentialSource: cred.sourceTool,
25805
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25806
+ };
25807
+ const updated = config.providers.map((p) => {
25808
+ if (p === existing2) return { ...p, baseUrl: target, ...patch2 };
25809
+ const sibling = p.type === "azure" && p.deploymentName?.trim() && sameAzureEndpoint(p.baseUrl, target);
25810
+ return sibling ? { ...p, ...patch2 } : p;
25811
+ });
25812
+ const providers3 = existing2 ? updated : [...updated, {
25813
+ type: "azure",
25814
+ baseUrl: target,
25815
+ deploymentName: deployment,
25816
+ ...patch2
25817
+ }];
25818
+ await cm.updateConfig({ providers: providers3 });
25819
+ return true;
25820
+ }
25821
+ const scoped = azureDeploymentsForCredential(cred, config.providers);
25822
+ if (scoped.length === 0) {
25823
+ const all = [...new Set(azureDeployments.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
25824
+ const onTarget = target ? azureDeployments.some((p) => sameAzureEndpoint(p.baseUrl, target)) : true;
25825
+ const anyRouted = azureDeployments.some((p) => p.baseUrl?.trim());
25826
+ if (!anyRouted) {
25827
+ console.log(chalk8.yellow("\n The configured Azure deployments have no endpoint."));
25828
+ console.log(chalk8.gray(" A deployment needs its resource URL before a key can reach it \u2014"));
25829
+ console.log(chalk8.gray(" set AZURE_OPENAI_ENDPOINT, or add `baseUrl` to the entries in"));
25830
+ console.log(chalk8.gray(" .cascade/config.json, then run this again.\n"));
25831
+ return false;
25832
+ }
25833
+ if (target && !onTarget) {
25834
+ console.log(chalk8.yellow(`
25835
+ No configured Azure deployment is on ${target}.`));
25836
+ console.log(chalk8.gray(" Set AZURE_OPENAI_DEPLOYMENT as well to add one, or point"));
25837
+ console.log(chalk8.gray(" AZURE_OPENAI_ENDPOINT at a resource you have configured:\n"));
25838
+ } else {
25839
+ console.log(chalk8.yellow("\n Several Azure resources are configured, and an Azure key belongs to one of them."));
25840
+ console.log(chalk8.gray(" Set AZURE_OPENAI_ENDPOINT to the resource this key is for, then run again:\n"));
25841
+ }
25842
+ for (const r of all) console.log(chalk8.gray(` ${r || "(no endpoint set)"}`));
25843
+ console.log("");
25844
+ return false;
25845
+ }
25846
+ const patch = {
25847
+ apiKey: cred.secret,
25848
+ credentialSource: cred.sourceTool,
25849
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25850
+ };
25851
+ const providers2 = config.providers.map((p) => scoped.includes(p) ? { ...p, ...patch } : p);
25852
+ const routing = azureRoutingForCredential(cred, config.providers);
25853
+ if (routing.ok && routing.createDeployment) {
25854
+ providers2.push({
25855
+ type: "azure",
25856
+ deploymentName: routing.createDeployment,
25857
+ baseUrl: routing.resource,
25858
+ ...patch
25859
+ });
25860
+ console.log(chalk8.gray(
25861
+ ` Added deployment "${routing.createDeployment}" on ${normalizeAzureEndpoint(routing.resource)}.`
25862
+ ));
25863
+ }
25864
+ await cm.updateConfig({ providers: providers2 });
25865
+ return true;
25866
+ }
25867
+ const existing = config.providers.find((p) => p.type === cred.provider);
25868
+ const next = {
25869
+ ...existing,
25870
+ type: cred.provider,
25871
+ credentialSource: cred.sourceTool,
25872
+ // Both cleared first: adopting a credential REPLACES the old one, and
25873
+ // leaving a stale key beside a new token makes which one is in use depend
25874
+ // on provider-internal precedence.
25875
+ apiKey: void 0,
25876
+ authToken: void 0
25877
+ };
25878
+ if (cred.kind === "bearer") {
25879
+ next.authToken = cred.secret;
25880
+ } else {
25881
+ next.apiKey = cred.secret;
25882
+ }
25883
+ if (cred.kind !== "bearer" && !cred.baseUrl && hasDefaultEndpoint(cred.provider) && next.baseUrl) {
25884
+ delete next.baseUrl;
25885
+ delete next.local;
25886
+ }
25887
+ if (cred.baseUrl) {
25888
+ next.baseUrl = cred.baseUrl;
25889
+ delete next.local;
25890
+ }
25891
+ if (cred.deploymentName) next.deploymentName = cred.deploymentName;
25892
+ if (cred.apiVersion) next.apiVersion = cred.apiVersion;
25893
+ const providers = config.providers.filter((p) => p.type !== cred.provider);
25894
+ providers.push(next);
25895
+ await cm.updateConfig({ providers });
25896
+ return true;
25897
+ }
24754
25898
 
24755
25899
  // src/cli/commands/doctor.ts
25900
+ function linkableCredentialsDetail(discovered, configured = []) {
25901
+ const usable = discovered.filter((d) => willAdoptFromConfig(d, configured)).length;
25902
+ return usable > 0 ? `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt` : `${discovered.length} found, none usable \u2014 run \`cascade link\` to see why`;
25903
+ }
24756
25904
  async function doctorCommand() {
24757
- console.log(chalk7.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
25905
+ console.log(chalk8.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
24758
25906
  const checks = [];
24759
25907
  const nodeVersion = process.versions.node;
24760
25908
  const [major] = nodeVersion.split(".").map(Number);
@@ -24769,7 +25917,7 @@ async function doctorCommand() {
24769
25917
  checks.push({
24770
25918
  label: "Cascade config",
24771
25919
  ok: true,
24772
- detail: `Loaded ${path34.join(process.cwd(), CASCADE_CONFIG_FILE)}`
25920
+ detail: `Loaded ${path35.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24773
25921
  });
24774
25922
  const providers = [
24775
25923
  { type: "anthropic", name: "Anthropic" },
@@ -24779,10 +25927,11 @@ async function doctorCommand() {
24779
25927
  ];
24780
25928
  for (const { type, name } of providers) {
24781
25929
  const key = cm.getApiKey(type);
25930
+ const token = key ? void 0 : cm.getAuthToken(type);
24782
25931
  checks.push({
24783
- label: `${name} API key`,
24784
- ok: Boolean(key),
24785
- detail: key ? "Set" : "Missing"
25932
+ label: `${name} credential`,
25933
+ ok: Boolean(key || token),
25934
+ detail: key ? "API key set" : token ? "Bearer token set" : "Missing"
24786
25935
  });
24787
25936
  }
24788
25937
  const ollamaOk = await checkEndpoint(OLLAMA_BASE_URL + "/api/tags");
@@ -24836,32 +25985,31 @@ async function doctorCommand() {
24836
25985
  try {
24837
25986
  const discovered = await discoverCredentials();
24838
25987
  if (discovered.length > 0) {
24839
- const usable = discovered.filter((d) => d.directlyUsable).length;
24840
25988
  checks.push({
24841
25989
  label: "Linkable credentials",
24842
25990
  ok: true,
24843
- detail: `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt`
25991
+ detail: linkableCredentialsDetail(discovered, config.providers ?? [])
24844
25992
  });
24845
25993
  }
24846
25994
  } catch {
24847
25995
  }
24848
25996
  for (const c of checks) {
24849
- const icon = c.ok ? chalk7.green(" \u2713") : chalk7.yellow(" \u25CB");
24850
- const label = c.ok ? chalk7.white(c.label) : chalk7.gray(c.label);
24851
- const detail = c.detail ? chalk7.gray(` \u2014 ${c.detail}`) : "";
25997
+ const icon = c.ok ? chalk8.green(" \u2713") : chalk8.yellow(" \u25CB");
25998
+ const label = c.ok ? chalk8.white(c.label) : chalk8.gray(c.label);
25999
+ const detail = c.detail ? chalk8.gray(` \u2014 ${c.detail}`) : "";
24852
26000
  console.log(`${icon} ${label}${detail}`);
24853
26001
  }
24854
26002
  const failures2 = checks.filter((c) => !c.ok);
24855
26003
  console.log();
24856
26004
  if (failures2.length === 0) {
24857
- console.log(chalk7.green(" All checks passed!\n"));
26005
+ console.log(chalk8.green(" All checks passed!\n"));
24858
26006
  } else {
24859
- const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
26007
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("credential"));
24860
26008
  if (critical.length) {
24861
- console.log(chalk7.yellow(` ${critical.length} issue(s) need attention.
26009
+ console.log(chalk8.yellow(` ${critical.length} issue(s) need attention.
24862
26010
  `));
24863
26011
  } else {
24864
- console.log(chalk7.gray(` ${failures2.length} optional item(s) not configured.
26012
+ console.log(chalk8.gray(` ${failures2.length} optional item(s) not configured.
24865
26013
  `));
24866
26014
  }
24867
26015
  }
@@ -24879,19 +26027,19 @@ async function checkEndpoint(url) {
24879
26027
  }
24880
26028
  }
24881
26029
  async function indexCommand(dirPath) {
24882
- const workspace = path34.resolve(dirPath || process.cwd());
26030
+ const workspace = path35.resolve(dirPath || process.cwd());
24883
26031
  const cm = new ConfigManager(workspace);
24884
26032
  await cm.load();
24885
26033
  const config = cm.getConfig();
24886
26034
  const embedder = embedderFromProviders(config.providers);
24887
26035
  if (!embedder) {
24888
- console.log(chalk7.red("\n No embeddings-capable provider configured."));
24889
- console.log(chalk7.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
26036
+ console.log(chalk8.red("\n No embeddings-capable provider configured."));
26037
+ console.log(chalk8.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
24890
26038
  process.exitCode = 1;
24891
26039
  return;
24892
26040
  }
24893
- const dbPath = config.codeIndex?.dbPath || path34.join(workspace, ".cascade", "code-index.db");
24894
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
26041
+ const dbPath = config.codeIndex?.dbPath || path35.join(workspace, ".cascade", "code-index.db");
26042
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
24895
26043
  const db = new Database2(dbPath);
24896
26044
  db.pragma("journal_mode = WAL");
24897
26045
  const complete = chatCompleterFromProviders(config.providers);
@@ -24905,20 +26053,20 @@ async function indexCommand(dirPath) {
24905
26053
  reranker,
24906
26054
  isIgnored: (abs) => ignore4.isIgnored(abs, workspace)
24907
26055
  });
24908
- console.log(chalk7.magenta(`
26056
+ console.log(chalk8.magenta(`
24909
26057
  \u25C8 Indexing ${workspace}
24910
26058
  `));
24911
26059
  let seen = 0;
24912
26060
  const res = await index.refresh(() => {
24913
26061
  seen++;
24914
- if (seen % 10 === 0) process.stdout.write(chalk7.dim(` \u2026embedded ${seen} changed files\r`));
26062
+ if (seen % 10 === 0) process.stdout.write(chalk8.dim(` \u2026embedded ${seen} changed files\r`));
24915
26063
  });
24916
26064
  console.log(
24917
- chalk7.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk7.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
26065
+ chalk8.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk8.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
24918
26066
  );
24919
- console.log(chalk7.dim(` Index: ${dbPath}`));
26067
+ console.log(chalk8.dim(` Index: ${dbPath}`));
24920
26068
  if (!config.codeIndex?.enabled) {
24921
- console.log(chalk7.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
26069
+ console.log(chalk8.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
24922
26070
  }
24923
26071
  console.log("");
24924
26072
  db.close();
@@ -24967,6 +26115,7 @@ async function decryptJSON(blob, passphrase) {
24967
26115
  }
24968
26116
 
24969
26117
  // src/cloud/keysync.ts
26118
+ init_revoked_credentials();
24970
26119
  function providerSig(p) {
24971
26120
  return [p.type, p.baseUrl ?? "", p.deploymentName ?? "", p.label ?? ""].join("\0");
24972
26121
  }
@@ -25039,6 +26188,19 @@ function gatherSyncBundle(config) {
25039
26188
  function applySyncBundle(bundle, config, cleanup) {
25040
26189
  const dropped = stripRetiredProviders(bundle);
25041
26190
  if (cleanup) cleanup.removed = dropped.removed;
26191
+ let revokedFromBundle = 0;
26192
+ let unusableFromBundle = 0;
26193
+ if (Array.isArray(bundle.providers)) {
26194
+ const pass = stripRevokedCredentials(bundle.providers, { keepForEndpoint: false });
26195
+ revokedFromBundle = pass.removed;
26196
+ if (pass.removed > 0) bundle.providers = pass.kept;
26197
+ const usable = bundle.providers.filter((p) => {
26198
+ const orphanBearer = Boolean(p.authToken) && !p.apiKey && !p.baseUrl;
26199
+ if (orphanBearer) unusableFromBundle++;
26200
+ return !orphanBearer;
26201
+ });
26202
+ if (unusableFromBundle > 0) bundle.providers = usable;
26203
+ }
25042
26204
  const next = { ...config, tools: { ...config.tools } };
25043
26205
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
25044
26206
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -25056,10 +26218,15 @@ function applySyncBundle(bundle, config, cleanup) {
25056
26218
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
25057
26219
  const merged = { ...next.models };
25058
26220
  const clearedFromMerge = clearRetiredPins(merged);
25059
- if (clearedFromMerge.length > 0) next.models = merged;
26221
+ const clearedRevoked = revokedFromBundle > 0 && !hasUsableAnthropic(next.providers ?? []) ? clearAnthropicPins(merged, next.providers ?? []) : [];
26222
+ if (clearedFromMerge.length > 0 || clearedRevoked.length > 0) {
26223
+ next.models = merged;
26224
+ }
25060
26225
  if (cleanup) {
26226
+ cleanup.revokedCredentials = revokedFromBundle;
26227
+ cleanup.unusableCredentials = unusableFromBundle;
25061
26228
  cleanup.clearedPins = ["t1", "t2", "t3"].filter(
25062
- (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
26229
+ (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t) || clearedRevoked.some((c) => c.tier === t)) && next.models?.[t] === void 0
25063
26230
  );
25064
26231
  }
25065
26232
  if (bundle.prefs) {
@@ -25085,43 +26252,43 @@ function openBrowser(url) {
25085
26252
  }
25086
26253
  }
25087
26254
  function notSignedIn() {
25088
- console.log(chalk7.dim("\n Not signed in. Run `cascade login` first.\n"));
26255
+ console.log(chalk8.dim("\n Not signed in. Run `cascade login` first.\n"));
25089
26256
  process.exitCode = 1;
25090
26257
  }
25091
26258
  function promptHidden(question) {
25092
26259
  return new Promise((resolve2) => {
25093
26260
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
25094
- let masked = false;
26261
+ let masked2 = false;
25095
26262
  rl._writeToOutput = (s) => {
25096
- if (!masked || !process.stdin.isTTY) process.stdout.write(s);
26263
+ if (!masked2 || !process.stdin.isTTY) process.stdout.write(s);
25097
26264
  };
25098
26265
  rl.question(question, (answer) => {
25099
26266
  rl.close();
25100
26267
  process.stdout.write("\n");
25101
26268
  resolve2(answer.trim());
25102
26269
  });
25103
- masked = true;
26270
+ masked2 = true;
25104
26271
  });
25105
26272
  }
25106
26273
  async function loginCommand(opts = {}) {
25107
26274
  const serverUrl = resolveServerUrl(opts.server);
25108
26275
  const client = new CloudClient(serverUrl);
25109
- console.log(chalk7.magenta("\n \u25C8 Sign in to Cascade Cloud"));
25110
- console.log(chalk7.dim(` ${serverUrl}
26276
+ console.log(chalk8.magenta("\n \u25C8 Sign in to Cascade Cloud"));
26277
+ console.log(chalk8.dim(` ${serverUrl}
25111
26278
  `));
25112
26279
  try {
25113
26280
  const session = await client.runDeviceLogin((d) => {
25114
- console.log(` 1. Open ${chalk7.cyan(d.verificationUri)}`);
25115
- console.log(` 2. Enter ${chalk7.bold.yellow(d.userCode)}`);
25116
- console.log(chalk7.dim("\n Waiting for you to approve\u2026"));
26281
+ console.log(` 1. Open ${chalk8.cyan(d.verificationUri)}`);
26282
+ console.log(` 2. Enter ${chalk8.bold.yellow(d.userCode)}`);
26283
+ console.log(chalk8.dim("\n Waiting for you to approve\u2026"));
25117
26284
  openBrowser(`${d.verificationUri}?code=${encodeURIComponent(d.userCode)}`);
25118
26285
  });
25119
26286
  const who = session.user.name || session.user.email || session.user.id;
25120
- console.log(chalk7.green(`
26287
+ console.log(chalk8.green(`
25121
26288
  \u2713 Signed in as ${who}
25122
26289
  `));
25123
26290
  } catch (err) {
25124
- console.log(chalk7.red(`
26291
+ console.log(chalk8.red(`
25125
26292
  ${err instanceof Error ? err.message : String(err)}
25126
26293
  `));
25127
26294
  process.exitCode = 1;
@@ -25130,11 +26297,11 @@ async function loginCommand(opts = {}) {
25130
26297
  async function logoutCommand() {
25131
26298
  const client = CloudClient.fromSession();
25132
26299
  if (!client) {
25133
- console.log(chalk7.dim("\n Not signed in.\n"));
26300
+ console.log(chalk8.dim("\n Not signed in.\n"));
25134
26301
  return;
25135
26302
  }
25136
26303
  await client.logout();
25137
- console.log(chalk7.green("\n \u2713 Signed out.\n"));
26304
+ console.log(chalk8.green("\n \u2713 Signed out.\n"));
25138
26305
  }
25139
26306
  async function whoamiCommand() {
25140
26307
  const client = CloudClient.fromSession();
@@ -25142,10 +26309,10 @@ async function whoamiCommand() {
25142
26309
  try {
25143
26310
  const me = await client.me();
25144
26311
  console.log(`
25145
- ${chalk7.bold(me.name || "\u2014")} ${chalk7.dim(me.email || "")}${me.plan ? chalk7.dim(` \xB7 ${me.plan}`) : ""}
26312
+ ${chalk8.bold(me.name || "\u2014")} ${chalk8.dim(me.email || "")}${me.plan ? chalk8.dim(` \xB7 ${me.plan}`) : ""}
25146
26313
  `);
25147
26314
  } catch (err) {
25148
- console.log(chalk7.red(`
26315
+ console.log(chalk8.red(`
25149
26316
  ${err instanceof Error ? err.message : String(err)}
25150
26317
  `));
25151
26318
  process.exitCode = 1;
@@ -25157,16 +26324,16 @@ async function sessionsCommand() {
25157
26324
  try {
25158
26325
  const convos = await client.listConversations();
25159
26326
  if (convos.length === 0) {
25160
- console.log(chalk7.dim("\n No cloud chats yet.\n"));
26327
+ console.log(chalk8.dim("\n No cloud chats yet.\n"));
25161
26328
  return;
25162
26329
  }
25163
- console.log(chalk7.magenta("\n \u25C8 Your cloud chats\n"));
26330
+ console.log(chalk8.magenta("\n \u25C8 Your cloud chats\n"));
25164
26331
  for (const c of convos) {
25165
- console.log(` ${chalk7.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk7.dim("(untitled)")}`);
26332
+ console.log(` ${chalk8.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk8.dim("(untitled)")}`);
25166
26333
  }
25167
- console.log(chalk7.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
26334
+ console.log(chalk8.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
25168
26335
  } catch (err) {
25169
- console.log(chalk7.red(`
26336
+ console.log(chalk8.red(`
25170
26337
  ${err instanceof Error ? err.message : String(err)}
25171
26338
  `));
25172
26339
  process.exitCode = 1;
@@ -25180,9 +26347,9 @@ function formatCloudTranscript(messages) {
25180
26347
  for (const m of messages) {
25181
26348
  const n = m.siblingIds?.length ?? 1;
25182
26349
  const i = m.siblingIds && m.id ? m.siblingIds.indexOf(m.id) + 1 : 1;
25183
- const branch = n > 1 ? chalk7.yellow(` \u2039${i}/${n}\u203A`) : "";
25184
- const tag = m.id ? chalk7.dim(` [${shortId2(m.id)}]`) : "";
25185
- const who = m.role === "user" ? chalk7.bold.cyan("You") : chalk7.bold.magenta("Cascade");
26350
+ const branch = n > 1 ? chalk8.yellow(` \u2039${i}/${n}\u203A`) : "";
26351
+ const tag = m.id ? chalk8.dim(` [${shortId2(m.id)}]`) : "";
26352
+ const who = m.role === "user" ? chalk8.bold.cyan("You") : chalk8.bold.magenta("Cascade");
25186
26353
  lines.push(` ${who}${tag}${branch}`);
25187
26354
  lines.push(" " + m.content.replace(/\n/g, "\n ") + "\n");
25188
26355
  }
@@ -25198,23 +26365,23 @@ async function sessionShowCommand(idOrPrefix) {
25198
26365
  try {
25199
26366
  const match = await resolveConversation(client, idOrPrefix);
25200
26367
  if (!match) {
25201
- console.log(chalk7.red(`
26368
+ console.log(chalk8.red(`
25202
26369
  No chat matching "${idOrPrefix}".
25203
26370
  `));
25204
26371
  process.exitCode = 1;
25205
26372
  return;
25206
26373
  }
25207
26374
  const msgs = await client.getMessages(match.id);
25208
- console.log(chalk7.magenta(`
26375
+ console.log(chalk8.magenta(`
25209
26376
  \u25C8 ${match.title?.trim() || "(untitled)"}
25210
26377
  `));
25211
26378
  console.log(formatCloudTranscript(msgs));
25212
26379
  if (msgs.some((m) => (m.siblingIds?.length ?? 1) > 1)) {
25213
- console.log(chalk7.dim(" cascade sessions branch <chat> <message> switch to another version"));
26380
+ console.log(chalk8.dim(" cascade sessions branch <chat> <message> switch to another version"));
25214
26381
  }
25215
- console.log(chalk7.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
26382
+ console.log(chalk8.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
25216
26383
  } catch (err) {
25217
- console.log(chalk7.red(`
26384
+ console.log(chalk8.red(`
25218
26385
  ${err instanceof Error ? err.message : String(err)}
25219
26386
  `));
25220
26387
  process.exitCode = 1;
@@ -25229,7 +26396,7 @@ async function sessionBranchCommand(chat, message) {
25229
26396
  try {
25230
26397
  const match = await resolveConversation(client, chat);
25231
26398
  if (!match) {
25232
- console.log(chalk7.red(`
26399
+ console.log(chalk8.red(`
25233
26400
  No chat matching "${chat}".
25234
26401
  `));
25235
26402
  process.exitCode = 1;
@@ -25238,12 +26405,12 @@ async function sessionBranchCommand(chat, message) {
25238
26405
  const active = await client.getMessages(match.id);
25239
26406
  const targetId = resolveMessage(active, message) ?? message;
25240
26407
  const next = await client.selectBranch(match.id, targetId);
25241
- console.log(chalk7.magenta(`
26408
+ console.log(chalk8.magenta(`
25242
26409
  \u25C8 Switched branch \u2014 ${match.title?.trim() || "(untitled)"}
25243
26410
  `));
25244
26411
  console.log(formatCloudTranscript(next));
25245
26412
  } catch (err) {
25246
- console.log(chalk7.red(`
26413
+ console.log(chalk8.red(`
25247
26414
  ${err instanceof Error ? err.message : String(err)}
25248
26415
  `));
25249
26416
  process.exitCode = 1;
@@ -25255,7 +26422,7 @@ async function sessionRemoveMessageCommand(chat, message) {
25255
26422
  try {
25256
26423
  const match = await resolveConversation(client, chat);
25257
26424
  if (!match) {
25258
- console.log(chalk7.red(`
26425
+ console.log(chalk8.red(`
25259
26426
  No chat matching "${chat}".
25260
26427
  `));
25261
26428
  process.exitCode = 1;
@@ -25264,11 +26431,11 @@ async function sessionRemoveMessageCommand(chat, message) {
25264
26431
  const active = await client.getMessages(match.id);
25265
26432
  const targetId = resolveMessage(active, message) ?? message;
25266
26433
  const next = await client.deleteMessage(match.id, targetId);
25267
- console.log(chalk7.green("\n \u2713 Deleted that message and its replies.\n"));
26434
+ console.log(chalk8.green("\n \u2713 Deleted that message and its replies.\n"));
25268
26435
  if (next.length) console.log(formatCloudTranscript(next));
25269
- else console.log(chalk7.dim(" (the conversation is now empty)\n"));
26436
+ else console.log(chalk8.dim(" (the conversation is now empty)\n"));
25270
26437
  } catch (err) {
25271
- console.log(chalk7.red(`
26438
+ console.log(chalk8.red(`
25272
26439
  ${err instanceof Error ? err.message : String(err)}
25273
26440
  `));
25274
26441
  process.exitCode = 1;
@@ -25280,18 +26447,18 @@ async function sessionRenameCommand(chat, title) {
25280
26447
  try {
25281
26448
  const match = await resolveConversation(client, chat);
25282
26449
  if (!match) {
25283
- console.log(chalk7.red(`
26450
+ console.log(chalk8.red(`
25284
26451
  No chat matching "${chat}".
25285
26452
  `));
25286
26453
  process.exitCode = 1;
25287
26454
  return;
25288
26455
  }
25289
26456
  await client.renameConversation(match.id, title);
25290
- console.log(chalk7.green(`
26457
+ console.log(chalk8.green(`
25291
26458
  \u2713 Renamed to "${title}".
25292
26459
  `));
25293
26460
  } catch (err) {
25294
- console.log(chalk7.red(`
26461
+ console.log(chalk8.red(`
25295
26462
  ${err instanceof Error ? err.message : String(err)}
25296
26463
  `));
25297
26464
  process.exitCode = 1;
@@ -25303,18 +26470,18 @@ async function sessionDeleteCommand(chat) {
25303
26470
  try {
25304
26471
  const match = await resolveConversation(client, chat);
25305
26472
  if (!match) {
25306
- console.log(chalk7.red(`
26473
+ console.log(chalk8.red(`
25307
26474
  No chat matching "${chat}".
25308
26475
  `));
25309
26476
  process.exitCode = 1;
25310
26477
  return;
25311
26478
  }
25312
26479
  await client.deleteConversation(match.id);
25313
- console.log(chalk7.green(`
26480
+ console.log(chalk8.green(`
25314
26481
  \u2713 Deleted "${match.title?.trim() || "(untitled)"}".
25315
26482
  `));
25316
26483
  } catch (err) {
25317
- console.log(chalk7.red(`
26484
+ console.log(chalk8.red(`
25318
26485
  ${err instanceof Error ? err.message : String(err)}
25319
26486
  `));
25320
26487
  process.exitCode = 1;
@@ -25326,21 +26493,21 @@ async function syncPushCommand() {
25326
26493
  const cm = new ConfigManager(process.cwd());
25327
26494
  await cm.load();
25328
26495
  const bundle = gatherSyncBundle(cm.getConfig());
25329
- console.log(chalk7.magenta("\n \u25C8 Sync your settings to your account"));
25330
- console.log(chalk7.dim(" Encrypted on this device with a passphrase we never see.\n"));
26496
+ console.log(chalk8.magenta("\n \u25C8 Sync your settings to your account"));
26497
+ console.log(chalk8.dim(" Encrypted on this device with a passphrase we never see.\n"));
25331
26498
  const pass = await promptHidden(" Passphrase: ");
25332
26499
  if (!pass) {
25333
- console.log(chalk7.dim("\n Cancelled.\n"));
26500
+ console.log(chalk8.dim("\n Cancelled.\n"));
25334
26501
  return;
25335
26502
  }
25336
26503
  try {
25337
26504
  const blob = await encryptJSON(bundle, pass);
25338
26505
  const r = await client.pushSecrets(blob);
25339
- console.log(chalk7.green(`
26506
+ console.log(chalk8.green(`
25340
26507
  \u2713 Synced (v${r.version}). Run \`cascade sync pull\` on another device.
25341
26508
  `));
25342
26509
  } catch (err) {
25343
- console.log(chalk7.red(`
26510
+ console.log(chalk8.red(`
25344
26511
  ${err instanceof Error ? err.message : String(err)}
25345
26512
  `));
25346
26513
  process.exitCode = 1;
@@ -25353,27 +26520,27 @@ async function syncPullCommand() {
25353
26520
  try {
25354
26521
  ({ blob } = await client.pullSecrets());
25355
26522
  } catch (err) {
25356
- console.log(chalk7.red(`
26523
+ console.log(chalk8.red(`
25357
26524
  ${err instanceof Error ? err.message : String(err)}
25358
26525
  `));
25359
26526
  process.exitCode = 1;
25360
26527
  return;
25361
26528
  }
25362
26529
  if (!blob) {
25363
- console.log(chalk7.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
26530
+ console.log(chalk8.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
25364
26531
  return;
25365
26532
  }
25366
- console.log(chalk7.magenta("\n \u25C8 Apply your synced settings to this device\n"));
26533
+ console.log(chalk8.magenta("\n \u25C8 Apply your synced settings to this device\n"));
25367
26534
  const pass = await promptHidden(" Passphrase: ");
25368
26535
  if (!pass) {
25369
- console.log(chalk7.dim("\n Cancelled.\n"));
26536
+ console.log(chalk8.dim("\n Cancelled.\n"));
25370
26537
  return;
25371
26538
  }
25372
26539
  let bundle;
25373
26540
  try {
25374
26541
  bundle = await decryptJSON(blob, pass);
25375
26542
  } catch {
25376
- console.log(chalk7.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
26543
+ console.log(chalk8.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
25377
26544
  process.exitCode = 1;
25378
26545
  return;
25379
26546
  }
@@ -25382,13 +26549,13 @@ async function syncPullCommand() {
25382
26549
  await cm.load();
25383
26550
  const cleanup = { removed: [], clearedPins: [] };
25384
26551
  await cm.updateConfig(applySyncBundle(bundle, cm.getConfig(), cleanup));
25385
- console.log(chalk7.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
26552
+ console.log(chalk8.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
25386
26553
  if (didCleanupChangeAnything(cleanup)) {
25387
- console.log(chalk7.yellow(` ${describeCleanup(cleanup)}
26554
+ console.log(chalk8.yellow(` ${describeCleanup(cleanup)}
25388
26555
  `));
25389
26556
  }
25390
26557
  } catch (err) {
25391
- console.log(chalk7.red(`
26558
+ console.log(chalk8.red(`
25392
26559
  Could not apply your synced settings: ${err instanceof Error ? err.message : String(err)}
25393
26560
  `));
25394
26561
  process.exitCode = 1;
@@ -25404,7 +26571,7 @@ function openBrowser2(url) {
25404
26571
  }
25405
26572
  function storePathFor(name) {
25406
26573
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
25407
- return path34.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
26574
+ return path35.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
25408
26575
  }
25409
26576
  function defaultName(url) {
25410
26577
  try {
@@ -25415,8 +26582,8 @@ function defaultName(url) {
25415
26582
  }
25416
26583
  async function mcpConnectCommand(url, opts = {}) {
25417
26584
  const desiredName = opts.name?.trim() || defaultName(url);
25418
- console.log(chalk7.magenta("\n \u25C8 Connect an MCP server via OAuth"));
25419
- console.log(chalk7.dim(` ${url}
26585
+ console.log(chalk8.magenta("\n \u25C8 Connect an MCP server via OAuth"));
26586
+ console.log(chalk8.dim(` ${url}
25420
26587
  `));
25421
26588
  try {
25422
26589
  const cm = new ConfigManager(process.cwd());
@@ -25432,7 +26599,7 @@ async function mcpConnectCommand(url, opts = {}) {
25432
26599
  store,
25433
26600
  clientName: "Cascade AI",
25434
26601
  openUrl: (u) => {
25435
- console.log(chalk7.dim(" Opening your browser to authorize\u2026"));
26602
+ console.log(chalk8.dim(" Opening your browser to authorize\u2026"));
25436
26603
  openBrowser2(u);
25437
26604
  }
25438
26605
  });
@@ -25442,11 +26609,11 @@ async function mcpConnectCommand(url, opts = {}) {
25442
26609
  config.tools.mcpServers = servers;
25443
26610
  config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
25444
26611
  await cm.updateConfig(config);
25445
- console.log(chalk7.green(`
26612
+ console.log(chalk8.green(`
25446
26613
  \u2713 Connected "${name}". Its tools are available to your runs.
25447
26614
  `));
25448
26615
  } catch (err) {
25449
- console.log(chalk7.red(`
26616
+ console.log(chalk8.red(`
25450
26617
  ${err instanceof Error ? err.message : String(err)}
25451
26618
  `));
25452
26619
  process.exitCode = 1;
@@ -25457,13 +26624,13 @@ async function mcpListCommand() {
25457
26624
  await cm.load();
25458
26625
  const servers = cm.getConfig().tools?.mcpServers ?? [];
25459
26626
  if (!servers.length) {
25460
- console.log(chalk7.dim("\n No MCP servers configured.\n"));
26627
+ console.log(chalk8.dim("\n No MCP servers configured.\n"));
25461
26628
  return;
25462
26629
  }
25463
- console.log(chalk7.magenta("\n \u25C8 MCP servers\n"));
26630
+ console.log(chalk8.magenta("\n \u25C8 MCP servers\n"));
25464
26631
  for (const s of servers) {
25465
- const kind = s.oauthStore ? chalk7.green("oauth") : s.headers ? chalk7.cyan("token") : s.command ? chalk7.dim("local") : chalk7.dim("open");
25466
- console.log(` ${chalk7.bold(s.name)} ${chalk7.dim(s.url || s.command || "")} ${kind}`);
26632
+ const kind = s.oauthStore ? chalk8.green("oauth") : s.headers ? chalk8.cyan("token") : s.command ? chalk8.dim("local") : chalk8.dim("open");
26633
+ console.log(` ${chalk8.bold(s.name)} ${chalk8.dim(s.url || s.command || "")} ${kind}`);
25467
26634
  }
25468
26635
  console.log();
25469
26636
  }
@@ -25474,7 +26641,7 @@ async function mcpRemoveCommand(name) {
25474
26641
  const servers = config.tools?.mcpServers ?? [];
25475
26642
  const match = servers.find((s) => s.name === name);
25476
26643
  if (!match) {
25477
- console.log(chalk7.red(`
26644
+ console.log(chalk8.red(`
25478
26645
  No MCP server named "${name}".
25479
26646
  `));
25480
26647
  process.exitCode = 1;
@@ -25490,7 +26657,7 @@ async function mcpRemoveCommand(name) {
25490
26657
  }
25491
26658
  }
25492
26659
  await cm.updateConfig(config);
25493
- console.log(chalk7.green(`
26660
+ console.log(chalk8.green(`
25494
26661
  \u2713 Removed "${name}".
25495
26662
  `));
25496
26663
  }
@@ -25504,16 +26671,302 @@ async function updateCommand() {
25504
26671
  const { stdout } = await execAsync2("npm show cascade-ai version", { timeout: 1e4 });
25505
26672
  const latest = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
25506
26673
  if (latest === CASCADE_VERSION) {
25507
- spin.succeed(chalk7.green(`Already up to date (v${CASCADE_VERSION})`));
26674
+ spin.succeed(chalk8.green(`Already up to date (v${CASCADE_VERSION})`));
25508
26675
  return;
25509
26676
  }
25510
26677
  spin.text = `Updating cascade-ai ${CASCADE_VERSION} \u2192 ${latest}\u2026`;
25511
26678
  await execAsync2("npm install -g cascade-ai@latest", { timeout: 6e4 });
25512
- spin.succeed(chalk7.green(`Updated to v${latest}! Restart your terminal.`));
26679
+ spin.succeed(chalk8.green(`Updated to v${latest}! Restart your terminal.`));
25513
26680
  } catch (err) {
25514
- spin.fail(chalk7.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26681
+ spin.fail(chalk8.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26682
+ }
26683
+ }
26684
+
26685
+ // src/config/credential-write.ts
26686
+ function endpointFromSettingsPayload(endpoints, type) {
26687
+ if (!endpoints || !Object.hasOwn(endpoints, type)) return { kind: "preserve" };
26688
+ const baseUrl = endpoints[type]?.trim();
26689
+ return baseUrl ? { kind: "at", baseUrl } : { kind: "cleared" };
26690
+ }
26691
+ function targetEndpoint(endpoint, existing) {
26692
+ switch (endpoint.kind) {
26693
+ case "at":
26694
+ return endpoint.baseUrl;
26695
+ // An explicit clear, always. Whether the row then falls back to a public
26696
+ // host or becomes unroutable is the provider's business, decided before we
26697
+ // get here.
26698
+ case "cleared":
26699
+ return void 0;
26700
+ case "preserve":
26701
+ return existing?.baseUrl;
25515
26702
  }
25516
26703
  }
26704
+ function namesCustomHost(row) {
26705
+ if (!row?.baseUrl) return false;
26706
+ return !sameCredentialEndpoint(row.type, row.baseUrl, void 0);
26707
+ }
26708
+ function applyProviderCredential(providers, type, apiKey, endpoint) {
26709
+ const existing = providers.find((p) => p.type === type);
26710
+ if (endpoint.kind === "cleared" && !hasDefaultEndpoint(type)) {
26711
+ if (existing) {
26712
+ existing.baseUrl = void 0;
26713
+ existing.apiKey = void 0;
26714
+ existing.authToken = void 0;
26715
+ delete existing.local;
26716
+ }
26717
+ return { written: false, reason: "unroutable" };
26718
+ }
26719
+ if (endpoint.kind === "preserve" && namesCustomHost(existing)) {
26720
+ return { written: false, reason: "ambiguous-scope" };
26721
+ }
26722
+ const nextBaseUrl = targetEndpoint(endpoint, existing);
26723
+ if (existing && !sameCredentialEndpoint(type, existing.baseUrl, nextBaseUrl)) {
26724
+ delete existing.local;
26725
+ }
26726
+ if (existing && nextBaseUrl === void 0) existing.baseUrl = void 0;
26727
+ applyProviderApiKey(providers, type, apiKey, nextBaseUrl ? { baseUrl: nextBaseUrl } : {});
26728
+ return { written: true };
26729
+ }
26730
+ function credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) {
26731
+ if (replacementKey) return "replaced";
26732
+ return sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl) ? "keep" : "clear";
26733
+ }
26734
+ function applyEndpointEdit(existing, nextBaseUrl, replacementKey) {
26735
+ if (credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) === "clear") {
26736
+ existing.apiKey = void 0;
26737
+ existing.authToken = void 0;
26738
+ }
26739
+ if (!sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl)) {
26740
+ delete existing.local;
26741
+ }
26742
+ existing.baseUrl = nextBaseUrl || void 0;
26743
+ }
26744
+ function explainRefusal(type, reason) {
26745
+ return reason === "unroutable" ? `The ${type} key was not saved: its endpoint was cleared, and ${type} has no default host, so there is nowhere the key could be sent. Enter the endpoint URL alongside the key.` : `The ${type} key was not saved: this provider is configured with a custom endpoint, and Cascade cannot tell whether the new key was issued by that endpoint or by the provider directly. Set the endpoint field alongside the key \u2014 clear it if the key is for the provider itself.`;
26746
+ }
26747
+ function applySettingsCredentials(providers, data) {
26748
+ const refused = [];
26749
+ if (data.keys) {
26750
+ for (const [type, apiKey] of Object.entries(data.keys)) {
26751
+ if (!apiKey) continue;
26752
+ const outcome = applyProviderCredential(
26753
+ providers,
26754
+ type,
26755
+ apiKey,
26756
+ endpointFromSettingsPayload(data.endpoints, type)
26757
+ );
26758
+ if (!outcome.written) refused.push({ type, reason: outcome.reason });
26759
+ }
26760
+ }
26761
+ if (!data.endpoints) return { refused };
26762
+ for (const [type, baseUrl] of Object.entries(data.endpoints)) {
26763
+ if (type === "azure") continue;
26764
+ if (!Object.hasOwn(data.endpoints, type)) continue;
26765
+ const existing = providers.find((p) => p.type === type);
26766
+ if (!existing) {
26767
+ if (baseUrl) providers.push({ type, baseUrl });
26768
+ continue;
26769
+ }
26770
+ applyEndpointEdit(existing, baseUrl, data.keys?.[type]);
26771
+ }
26772
+ return { refused };
26773
+ }
26774
+
26775
+ // src/config/settings-payload.ts
26776
+ function priorAzureRow(prior, incoming) {
26777
+ if (!incoming.deploymentName) return void 0;
26778
+ return prior.find((p) => p.deploymentName === incoming.deploymentName && sameAzureEndpoint(p.baseUrl, incoming.baseUrl));
26779
+ }
26780
+ function applySettingsPayload(config, data) {
26781
+ if (!Array.isArray(config.providers)) config.providers = [];
26782
+ const result = applySettingsCredentials(config.providers, data);
26783
+ if (Array.isArray(data.azureDeployments)) {
26784
+ const priorAzure = config.providers.filter((p) => p.type === "azure");
26785
+ const nonAzure = config.providers.filter((p) => p.type !== "azure");
26786
+ const nextAzure = data.azureDeployments.map((d) => {
26787
+ const prior = priorAzureRow(priorAzure, d);
26788
+ const apiKey = d.apiKey ? d.apiKey : prior?.apiKey;
26789
+ const model = d.model ?? prior?.model;
26790
+ const region = d.region ?? prior?.region;
26791
+ return {
26792
+ type: "azure",
26793
+ ...d.label ? { label: d.label } : {},
26794
+ ...d.baseUrl ? { baseUrl: d.baseUrl } : {},
26795
+ ...d.deploymentName ? { deploymentName: d.deploymentName } : {},
26796
+ ...d.apiVersion ? { apiVersion: d.apiVersion } : {},
26797
+ ...apiKey ? { apiKey } : {},
26798
+ ...model ? { model } : {},
26799
+ ...region ? { region } : {}
26800
+ };
26801
+ }).filter((d) => d.apiKey || d.baseUrl || d.deploymentName);
26802
+ config.providers = [...nonAzure, ...nextAzure];
26803
+ }
26804
+ if (data.models) {
26805
+ const models = config.models ?? {};
26806
+ for (const [tier, val] of Object.entries(data.models)) {
26807
+ if (val && val !== "auto") models[tier] = val;
26808
+ else delete models[tier];
26809
+ }
26810
+ config.models = models;
26811
+ }
26812
+ if (data.budget) {
26813
+ const b = data.budget;
26814
+ config.budget = config.budget ?? {};
26815
+ const cap = (key, target, accept, round = false) => {
26816
+ if (!Object.hasOwn(b, key)) return;
26817
+ const v = b[key];
26818
+ if (typeof v === "number" && accept(v)) config.budget[target] = round ? Math.floor(v) : v;
26819
+ else delete config.budget[target];
26820
+ };
26821
+ cap("maxCostPerRun", "maxCostPerRunUsd", (n) => n > 0);
26822
+ cap("dailyBudgetUsd", "dailyBudgetUsd", (n) => n > 0);
26823
+ cap("sessionBudgetUsd", "sessionBudgetUsd", (n) => n > 0);
26824
+ cap("maxTokensPerRun", "maxTokensPerRun", (n) => n > 0, true);
26825
+ cap("warnAtPct", "warnAtPct", (n) => n > 0 && n <= 100);
26826
+ if (b.autoBias === "balanced" || b.autoBias === "quality" || b.autoBias === "cost") {
26827
+ config.autoBias = b.autoBias;
26828
+ }
26829
+ }
26830
+ if (data.webSearch && typeof data.webSearch === "object") {
26831
+ config.tools = config.tools ?? {};
26832
+ const prior = config.tools.webSearch ?? {};
26833
+ const next = { ...prior };
26834
+ if (typeof data.webSearch.searxngUrl === "string") next.searxngUrl = data.webSearch.searxngUrl.trim() || void 0;
26835
+ if (data.webSearch.braveApiKey) next.braveApiKey = data.webSearch.braveApiKey;
26836
+ if (data.webSearch.tavilyApiKey) next.tavilyApiKey = data.webSearch.tavilyApiKey;
26837
+ config.tools.webSearch = next;
26838
+ }
26839
+ if (data.advanced && typeof data.advanced === "object") {
26840
+ const a = data.advanced;
26841
+ const num = (v, min, max) => {
26842
+ const n = Number(v);
26843
+ return Number.isFinite(n) && n >= min && n <= max ? n : void 0;
26844
+ };
26845
+ if (a["autonomy"] === "manual" || a["autonomy"] === "auto") config.autonomy = a["autonomy"];
26846
+ if (["never", "complex", "all", "always"].includes(a["planApproval"])) config.planApproval = a["planApproval"];
26847
+ {
26848
+ const n = num(a["approvalTimeoutMs"], 1e3, 864e5);
26849
+ if (n !== void 0) config.approvalTimeoutMs = Math.floor(n);
26850
+ }
26851
+ if (["auto", "parallel", "sequential"].includes(a["t3Execution"])) config.t3Execution = a["t3Execution"];
26852
+ {
26853
+ const n = num(a["localConcurrency"], 1, 16);
26854
+ if (n !== void 0) config.localConcurrency = Math.floor(n);
26855
+ }
26856
+ {
26857
+ const n = num(a["localInferenceTimeoutMs"], 1e3, 36e5);
26858
+ if (n !== void 0) config.localInferenceTimeoutMs = Math.floor(n);
26859
+ }
26860
+ {
26861
+ const n = num(a["cloudInferenceTimeoutMs"], 1e3, 36e5);
26862
+ if (n !== void 0) config.cloudInferenceTimeoutMs = Math.floor(n);
26863
+ }
26864
+ if (typeof a["reflectionEnabled"] === "boolean") config.reflection = { ...config.reflection ?? {}, enabled: a["reflectionEnabled"] };
26865
+ if (typeof a["cascadeAuto"] === "boolean") config.cascadeAuto = a["cascadeAuto"];
26866
+ if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
26867
+ if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
26868
+ if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
26869
+ if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
26870
+ if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
26871
+ if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
26872
+ if (typeof a["persistDynamicTools"] === "boolean") config.persistDynamicTools = a["persistDynamicTools"];
26873
+ if (typeof a["telemetryEnabled"] === "boolean") config.telemetry = { ...config.telemetry ?? {}, enabled: a["telemetryEnabled"] };
26874
+ }
26875
+ return result;
26876
+ }
26877
+ function settingsSnapshot(config) {
26878
+ const providers = config.providers ?? [];
26879
+ const endpoints = {};
26880
+ for (const p of providers) {
26881
+ if (p?.type && p?.baseUrl && p.type !== "azure") endpoints[p.type] = p.baseUrl;
26882
+ }
26883
+ const ws = config.tools?.webSearch ?? {};
26884
+ return {
26885
+ models: config.models ?? {},
26886
+ budget: {
26887
+ maxCostPerRun: config.budget?.maxCostPerRunUsd,
26888
+ autoBias: config.autoBias,
26889
+ dailyBudgetUsd: config.budget?.dailyBudgetUsd,
26890
+ sessionBudgetUsd: config.budget?.sessionBudgetUsd,
26891
+ maxTokensPerRun: config.budget?.maxTokensPerRun,
26892
+ warnAtPct: config.budget?.warnAtPct
26893
+ },
26894
+ // Through the shared predicate: `authToken` is a credential too, and
26895
+ // counting only `apiKey` showed a bearer-configured provider as unconfigured.
26896
+ providersWithKey: providers.filter((p) => hasProviderCredential(p)).map((p) => p.type),
26897
+ endpoints,
26898
+ azureDeployments: providers.filter((p) => p.type === "azure").map((p) => ({
26899
+ label: p.label,
26900
+ baseUrl: p.baseUrl,
26901
+ deploymentName: p.deploymentName,
26902
+ apiVersion: p.apiVersion,
26903
+ // Surfaced so a panel that round-trips the snapshot cannot drop them.
26904
+ model: p.model,
26905
+ region: p.region,
26906
+ hasKey: typeof p.apiKey === "string" && p.apiKey.length > 0
26907
+ })),
26908
+ webSearch: {
26909
+ searxngUrl: ws.searxngUrl,
26910
+ hasBraveKey: typeof ws.braveApiKey === "string" && ws.braveApiKey.length > 0,
26911
+ hasTavilyKey: typeof ws.tavilyApiKey === "string" && ws.tavilyApiKey.length > 0
26912
+ },
26913
+ advanced: {
26914
+ autonomy: config.autonomy,
26915
+ planApproval: config.planApproval,
26916
+ approvalTimeoutMs: config.approvalTimeoutMs,
26917
+ t3Execution: config.t3Execution,
26918
+ localConcurrency: config.localConcurrency,
26919
+ localInferenceTimeoutMs: config.localInferenceTimeoutMs,
26920
+ cloudInferenceTimeoutMs: config.cloudInferenceTimeoutMs,
26921
+ reflectionEnabled: config.reflection?.enabled,
26922
+ cascadeAuto: config.cascadeAuto,
26923
+ forceTier: config.routing?.forceTier,
26924
+ benchmarksLive: config.benchmarks?.live,
26925
+ dynamicToolSandbox: config.tools?.dynamicToolSandbox,
26926
+ factsExtraction: config.knowledge?.factsExtraction,
26927
+ rememberSessions: config.memory?.rememberSessions,
26928
+ enableToolCreation: config.enableToolCreation,
26929
+ persistDynamicTools: config.persistDynamicTools,
26930
+ telemetryEnabled: config.telemetry?.enabled
26931
+ }
26932
+ };
26933
+ }
26934
+ async function commitSettings(live, data, write) {
26935
+ const staged = structuredClone(live);
26936
+ const { refused } = applySettingsPayload(staged, data);
26937
+ let committed;
26938
+ try {
26939
+ committed = validateConfig(staged);
26940
+ } catch (err) {
26941
+ return {
26942
+ ok: false,
26943
+ refused,
26944
+ error: `Those settings are not valid: ${err instanceof Error ? err.message : String(err)}`
26945
+ };
26946
+ }
26947
+ const written = await write(committed);
26948
+ if (!written.ok) {
26949
+ return { ok: false, refused, error: `Could not write the config file: ${written.error}` };
26950
+ }
26951
+ replaceInPlace(live, committed);
26952
+ return { ok: true, refused };
26953
+ }
26954
+ function replaceInPlace(target, source) {
26955
+ const t = target;
26956
+ for (const key of Object.keys(t)) delete t[key];
26957
+ Object.assign(t, structuredClone(source));
26958
+ }
26959
+ function writeConfigFile(configPath, config) {
26960
+ try {
26961
+ fs32.mkdirSync(path35.dirname(configPath), { recursive: true });
26962
+ fs32.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
26963
+ return { ok: true };
26964
+ } catch (err) {
26965
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26966
+ }
26967
+ }
26968
+
26969
+ // src/dashboard/server.ts
25517
26970
  init_constants();
25518
26971
  var JWT_ALGORITHM = "HS256";
25519
26972
  function createToken(user, secret) {
@@ -25761,12 +27214,22 @@ var DashboardSocket = class {
25761
27214
  });
25762
27215
  });
25763
27216
  }
27217
+ /**
27218
+ * The handler's result is ACKNOWLEDGED to the caller.
27219
+ *
27220
+ * A settings save can be partly refused — a key whose host cannot be
27221
+ * determined is deliberately not stored — and this path had no way to say so,
27222
+ * so the panel cleared the input and reported success over a key the backend
27223
+ * had dropped. Socket.IO's own ack channel carries it back, which needs no
27224
+ * new event name and cannot be missed by a client that forgot to subscribe.
27225
+ */
25764
27226
  onConfigUpdate(callback) {
25765
27227
  this.io.on("connection", (socket) => {
25766
- socket.on("config:update", (payload) => {
25767
- if (typeof payload === "object" && payload !== null) {
25768
- callback(payload);
25769
- }
27228
+ socket.on("config:update", (payload, ack) => {
27229
+ if (typeof payload !== "object" || payload === null) return;
27230
+ void callback(payload).then((result) => {
27231
+ if (typeof ack === "function") ack(result);
27232
+ });
25770
27233
  });
25771
27234
  });
25772
27235
  }
@@ -25912,8 +27375,47 @@ function aggregateCostStats(sessions2, opts = {}) {
25912
27375
  }
25913
27376
 
25914
27377
  // src/dashboard/server.ts
25915
- var __dirname$1 = path34.dirname(fileURLToPath(import.meta.url));
27378
+ var __dirname$1 = path35.dirname(fileURLToPath(import.meta.url));
25916
27379
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
27380
+ var MASK = "***";
27381
+ function masked(key, value) {
27382
+ return { [key]: value ? MASK : void 0 };
27383
+ }
27384
+ function redactProviderSecrets(config) {
27385
+ const safe = { ...config };
27386
+ safe.providers = (config.providers ?? []).map((p) => ({
27387
+ ...p,
27388
+ ...masked("apiKey", p.apiKey),
27389
+ ...masked("authToken", p.authToken)
27390
+ }));
27391
+ if (config.tools) {
27392
+ safe.tools = { ...config.tools };
27393
+ if (config.tools.webSearch) {
27394
+ safe.tools.webSearch = {
27395
+ ...config.tools.webSearch,
27396
+ ...masked("braveApiKey", config.tools.webSearch.braveApiKey),
27397
+ ...masked("tavilyApiKey", config.tools.webSearch.tavilyApiKey)
27398
+ };
27399
+ }
27400
+ if (config.tools.mcpServers) {
27401
+ safe.tools.mcpServers = config.tools.mcpServers.map((server) => ({
27402
+ ...server,
27403
+ ...server.headers ? { headers: Object.fromEntries(Object.keys(server.headers).map((k) => [k, MASK])) } : {},
27404
+ ...server.env ? { env: Object.fromEntries(Object.keys(server.env).map((k) => [k, MASK])) } : {}
27405
+ }));
27406
+ }
27407
+ }
27408
+ if (config.dashboard) {
27409
+ safe.dashboard = { ...config.dashboard, ...masked("secret", config.dashboard.secret) };
27410
+ }
27411
+ if (config.telemetry) {
27412
+ safe.telemetry = {
27413
+ ...config.telemetry,
27414
+ ...masked("posthogApiKey", config.telemetry.posthogApiKey)
27415
+ };
27416
+ }
27417
+ return safe;
27418
+ }
25917
27419
  var DashboardServer = class {
25918
27420
  app;
25919
27421
  httpServer;
@@ -25988,40 +27490,21 @@ var DashboardServer = class {
25988
27490
  this.activeSessions.get(sessionId)?.rateLastRun(rating);
25989
27491
  });
25990
27492
  this.socket.onConfigGet((socketId) => {
25991
- this.socket.emitToSocket(socketId, "config:current", {
25992
- models: this.config.models ?? {},
25993
- budget: {
25994
- maxCostPerRun: this.config.budget?.maxCostPerRunUsd,
25995
- autoBias: this.config.autoBias
25996
- },
25997
- providersWithKey: (this.config.providers ?? []).filter((p) => typeof p.apiKey === "string" && p.apiKey.length > 0).map((p) => p.type)
25998
- });
27493
+ this.socket.emitToSocket(socketId, "config:current", settingsSnapshot(this.config));
25999
27494
  });
26000
- this.socket.onConfigUpdate((data) => {
26001
- if (data.keys) {
26002
- for (const [type, apiKey] of Object.entries(data.keys)) {
26003
- if (!apiKey) continue;
26004
- const provider = this.config.providers.find((p) => p.type === type);
26005
- if (provider) provider.apiKey = apiKey;
26006
- else this.config.providers.push({ type, apiKey });
26007
- }
26008
- }
26009
- if (data.models) {
26010
- const models = this.config.models;
26011
- for (const [tier, val] of Object.entries(data.models)) {
26012
- if (val && val !== "auto") models[tier] = val;
26013
- else delete models[tier];
26014
- }
26015
- }
26016
- if (data.budget) {
26017
- if (typeof data.budget.maxCostPerRun === "number") {
26018
- this.config.budget.maxCostPerRunUsd = data.budget.maxCostPerRun;
26019
- }
26020
- if (data.budget.autoBias === "balanced" || data.budget.autoBias === "quality" || data.budget.autoBias === "cost") {
26021
- this.config.autoBias = data.budget.autoBias;
26022
- }
27495
+ this.socket.onConfigUpdate(async (data) => {
27496
+ const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
27497
+ const refused = result.refused.map((r) => ({
27498
+ type: r.type,
27499
+ reason: r.reason,
27500
+ message: explainRefusal(r.type, r.reason)
27501
+ }));
27502
+ for (const r of refused) console.warn(r.message);
27503
+ if (!result.ok) {
27504
+ return { refused, snapshot: settingsSnapshot(this.config), error: result.error };
26023
27505
  }
26024
- this.persistConfig();
27506
+ this.syncGlobalCredentials();
27507
+ return { refused, snapshot: settingsSnapshot(this.config) };
26025
27508
  });
26026
27509
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
26027
27510
  const sessionId = requestedSessionId ?? randomUUID();
@@ -26190,16 +27673,47 @@ var DashboardServer = class {
26190
27673
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
26191
27674
  * a write failure is logged but never crashes the running dashboard.
26192
27675
  */
26193
- persistConfig() {
26194
- try {
26195
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
26196
- fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
26197
- fs31.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
26198
- } catch (err) {
26199
- console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
26200
- }
27676
+ /**
27677
+ * Returns the durability of the WORKSPACE write, because a caller
27678
+ * acknowledging a save has to know it happened.
27679
+ *
27680
+ * This was `void` and swallowed every filesystem error, so a read-only
27681
+ * config path or a full disk produced: the live object mutated, the write
27682
+ * failed, the socket acknowledged success, and the panel cleared the keys the
27683
+ * user had typed. The changes then vanished at the next restart. "The handler
27684
+ * ran" is not "the save is durable".
27685
+ *
27686
+ * The global-credential sync below is genuinely best-effort — it is a
27687
+ * convenience copy, and failing it does not lose what the user just entered.
27688
+ */
27689
+ /**
27690
+ * @param config REQUIRED, with no default. `commitSettings` writes the
27691
+ * validated copy BEFORE adopting it, so a writer that reached for
27692
+ * `this.config` instead would persist the configuration being replaced — and
27693
+ * a default parameter is exactly the affordance that lets that happen
27694
+ * silently and still typecheck.
27695
+ */
27696
+ persistConfig(config) {
27697
+ const result = writeConfigFile(path35.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
27698
+ if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
27699
+ return result;
27700
+ }
27701
+ /**
27702
+ * Copy provider credentials to the machine-global store, so keys saved over
27703
+ * the socket survive a workspace switch — the same sync `ConfigManager.save()`
27704
+ * performs.
27705
+ *
27706
+ * Separate from the workspace write, and called only AFTER it succeeds. It
27707
+ * used to run unconditionally inside `persistConfig()`, so a failed workspace
27708
+ * write still pushed the provider half of a rejected save into the global
27709
+ * store: half the change persisted, behind an error saying none of it had.
27710
+ *
27711
+ * Best-effort on its own account: this is a convenience copy, and failing it
27712
+ * does not lose what the user just entered.
27713
+ */
27714
+ syncGlobalCredentials() {
26201
27715
  try {
26202
- saveGlobalCredentials(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
27716
+ saveGlobalCredentials(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
26203
27717
  } catch (err) {
26204
27718
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
26205
27719
  }
@@ -26214,15 +27728,15 @@ var DashboardServer = class {
26214
27728
  resolveDashboardSecret() {
26215
27729
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
26216
27730
  if (fromConfig) return fromConfig;
26217
- const secretPath = path34.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
27731
+ const secretPath = path35.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
26218
27732
  try {
26219
- if (fs31.existsSync(secretPath)) {
26220
- const existing = fs31.readFileSync(secretPath, "utf-8").trim();
27733
+ if (fs32.existsSync(secretPath)) {
27734
+ const existing = fs32.readFileSync(secretPath, "utf-8").trim();
26221
27735
  if (existing.length >= 16) return existing;
26222
27736
  }
26223
27737
  const generated = randomUUID();
26224
- fs31.mkdirSync(path34.dirname(secretPath), { recursive: true });
26225
- fs31.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
27738
+ fs32.mkdirSync(path35.dirname(secretPath), { recursive: true });
27739
+ fs32.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
26226
27740
  if (this.config.dashboard.auth) {
26227
27741
  console.warn(
26228
27742
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -26249,7 +27763,7 @@ var DashboardServer = class {
26249
27763
  // ── Setup ─────────────────────────────────────
26250
27764
  getGlobalStore() {
26251
27765
  if (!this.globalStore) {
26252
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
27766
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26253
27767
  this.globalStore = new MemoryStore(globalDbPath);
26254
27768
  }
26255
27769
  return this.globalStore;
@@ -26629,12 +28143,12 @@ ${prompt}`;
26629
28143
  }
26630
28144
  }
26631
28145
  watchRuntimeChanges() {
26632
- const workspaceDbPath = path34.join(this.workspacePath, CASCADE_DB_FILE);
26633
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28146
+ const workspaceDbPath = path35.join(this.workspacePath, CASCADE_DB_FILE);
28147
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26634
28148
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
26635
28149
  for (const watchPath of watchPaths) {
26636
- if (!fs31.existsSync(watchPath)) continue;
26637
- fs31.watchFile(watchPath, { interval: 3e3 }, () => {
28150
+ if (!fs32.existsSync(watchPath)) continue;
28151
+ fs32.watchFile(watchPath, { interval: 3e3 }, () => {
26638
28152
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
26639
28153
  });
26640
28154
  }
@@ -26756,7 +28270,7 @@ ${prompt}`;
26756
28270
  const sessionId = req.params.id;
26757
28271
  this.store.deleteSession(sessionId);
26758
28272
  this.store.deleteRuntimeSession(sessionId);
26759
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28273
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26760
28274
  const globalStore = new MemoryStore(globalDbPath);
26761
28275
  try {
26762
28276
  globalStore.deleteRuntimeSession(sessionId);
@@ -26849,7 +28363,7 @@ ${prompt}`;
26849
28363
  });
26850
28364
  this.app.delete("/api/sessions", auth2, (req, res) => {
26851
28365
  const body = req.body;
26852
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28366
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26853
28367
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
26854
28368
  const globalStore = new MemoryStore(globalDbPath);
26855
28369
  try {
@@ -26872,7 +28386,7 @@ ${prompt}`;
26872
28386
  });
26873
28387
  this.app.delete("/api/runtime", auth2, (_req, res) => {
26874
28388
  this.store.deleteAllRuntimeNodes();
26875
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28389
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26876
28390
  const globalStore = new MemoryStore(globalDbPath);
26877
28391
  try {
26878
28392
  globalStore.deleteAllRuntimeNodes();
@@ -26941,8 +28455,7 @@ ${prompt}`;
26941
28455
  res.json(log);
26942
28456
  });
26943
28457
  this.app.get("/api/config", auth2, (_req, res) => {
26944
- const safe = { ...this.config };
26945
- safe.providers = safe.providers.map((p) => ({ ...p, apiKey: p.apiKey ? "***" : void 0 }));
28458
+ const safe = redactProviderSecrets(this.config);
26946
28459
  res.json(safe);
26947
28460
  });
26948
28461
  this.app.put("/api/config", auth2, async (req, res) => {
@@ -26958,12 +28471,12 @@ ${prompt}`;
26958
28471
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
26959
28472
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
26960
28473
  try {
26961
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
26962
- const existing = fs31.existsSync(configPath) ? JSON.parse(fs31.readFileSync(configPath, "utf-8")) : {};
28474
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
28475
+ const existing = fs32.existsSync(configPath) ? JSON.parse(fs32.readFileSync(configPath, "utf-8")) : {};
26963
28476
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
26964
28477
  const tmp = configPath + ".tmp";
26965
- fs31.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
26966
- fs31.renameSync(tmp, configPath);
28478
+ fs32.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
28479
+ fs32.renameSync(tmp, configPath);
26967
28480
  res.json({ ok: true });
26968
28481
  } catch (err) {
26969
28482
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -26991,7 +28504,7 @@ ${prompt}`;
26991
28504
  this.app.get("/api/runtime", auth2, (req, res) => {
26992
28505
  const scope = req.query["scope"] ?? "workspace";
26993
28506
  if (scope === "global") {
26994
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28507
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26995
28508
  const globalStore = new MemoryStore(globalDbPath);
26996
28509
  try {
26997
28510
  res.json({
@@ -27332,13 +28845,13 @@ ${prompt}`;
27332
28845
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
27333
28846
  }
27334
28847
  });
27335
- const prodPath = path34.resolve(__dirname$1, "../web/dist");
27336
- const devPath = path34.resolve(__dirname$1, "../../web/dist");
27337
- const webDistPath = fs31.existsSync(prodPath) ? prodPath : devPath;
27338
- if (fs31.existsSync(webDistPath)) {
28848
+ const prodPath = path35.resolve(__dirname$1, "../web/dist");
28849
+ const devPath = path35.resolve(__dirname$1, "../../web/dist");
28850
+ const webDistPath = fs32.existsSync(prodPath) ? prodPath : devPath;
28851
+ if (fs32.existsSync(webDistPath)) {
27339
28852
  this.app.use(express.static(webDistPath));
27340
28853
  this.app.get("*", (_req, res) => {
27341
- res.sendFile(path34.join(webDistPath, "index.html"));
28854
+ res.sendFile(path35.join(webDistPath, "index.html"));
27342
28855
  });
27343
28856
  } else {
27344
28857
  this.app.get("/", (_req, res) => {
@@ -27359,7 +28872,7 @@ init_constants();
27359
28872
  async function dashboardCommand(config, workspacePath = process.cwd()) {
27360
28873
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
27361
28874
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
27362
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28875
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27363
28876
  const server = new DashboardServer(config, store, workspacePath);
27364
28877
  server.watchRuntimeChanges();
27365
28878
  const gThis = globalThis;
@@ -27371,10 +28884,10 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27371
28884
  process.once("exit", onExit);
27372
28885
  try {
27373
28886
  await server.start();
27374
- spin.succeed(chalk7.green(`Dashboard running at http://localhost:${port}`));
28887
+ spin.succeed(chalk8.green(`Dashboard running at http://localhost:${port}`));
27375
28888
  server.refreshRuntime("workspace");
27376
28889
  server.refreshRuntime("global");
27377
- console.log(chalk7.gray(` Press Ctrl+C to stop
28890
+ console.log(chalk8.gray(` Press Ctrl+C to stop
27378
28891
  `));
27379
28892
  await new Promise(() => {
27380
28893
  });
@@ -27383,7 +28896,7 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27383
28896
  await server.stop().catch(() => {
27384
28897
  });
27385
28898
  onExit();
27386
- spin.fail(chalk7.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
28899
+ spin.fail(chalk8.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
27387
28900
  process.exit(1);
27388
28901
  }
27389
28902
  }
@@ -27391,18 +28904,18 @@ init_constants();
27391
28904
  function makeIdentityCommand(workspacePath = process.cwd()) {
27392
28905
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
27393
28906
  identity.command("list").description("List all available identities").action(() => {
27394
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28907
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27395
28908
  try {
27396
28909
  const identities = store.listIdentities();
27397
- console.log(chalk7.bold("\n Identities:"));
28910
+ console.log(chalk8.bold("\n Identities:"));
27398
28911
  if (identities.length === 0) {
27399
- console.log(chalk7.gray(" No identities found."));
28912
+ console.log(chalk8.gray(" No identities found."));
27400
28913
  } else {
27401
28914
  identities.forEach((id) => {
27402
- const defaultLabel = id.isDefault ? chalk7.green(" [Default]") : "";
27403
- console.log(` - ${chalk7.cyan(id.name)}${defaultLabel}`);
27404
- console.log(chalk7.gray(` ID: ${id.id}`));
27405
- if (id.description) console.log(chalk7.gray(` ${id.description}`));
28915
+ const defaultLabel = id.isDefault ? chalk8.green(" [Default]") : "";
28916
+ console.log(` - ${chalk8.cyan(id.name)}${defaultLabel}`);
28917
+ console.log(chalk8.gray(` ID: ${id.id}`));
28918
+ if (id.description) console.log(chalk8.gray(` ${id.description}`));
27406
28919
  });
27407
28920
  }
27408
28921
  console.log();
@@ -27411,7 +28924,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27411
28924
  }
27412
28925
  });
27413
28926
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
27414
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28927
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27415
28928
  try {
27416
28929
  if (options.default) {
27417
28930
  const existingDefault = store.getDefaultIdentity();
@@ -27428,21 +28941,21 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27428
28941
  isDefault: !!options.default,
27429
28942
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
27430
28943
  });
27431
- console.log(chalk7.green(`
28944
+ console.log(chalk8.green(`
27432
28945
  Successfully created identity: ${name} (${id})`));
27433
- if (options.default) console.log(chalk7.green(" Set as default."));
28946
+ if (options.default) console.log(chalk8.green(" Set as default."));
27434
28947
  console.log();
27435
28948
  } finally {
27436
28949
  store.close();
27437
28950
  }
27438
28951
  });
27439
28952
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
27440
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28953
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27441
28954
  try {
27442
28955
  const identities = store.listIdentities();
27443
28956
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
27444
28957
  if (!match) {
27445
- console.error(chalk7.red(`
28958
+ console.error(chalk8.red(`
27446
28959
  Identity '${query}' not found.
27447
28960
  `));
27448
28961
  process.exit(1);
@@ -27452,7 +28965,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27452
28965
  store.updateIdentity(existingDefault.id, { isDefault: false });
27453
28966
  }
27454
28967
  store.updateIdentity(match.id, { isDefault: true });
27455
- console.log(chalk7.green(`
28968
+ console.log(chalk8.green(`
27456
28969
  Identity ${match.name} is now the default.
27457
28970
  `));
27458
28971
  } finally {
@@ -27463,7 +28976,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27463
28976
  }
27464
28977
  init_pricing();
27465
28978
  async function modelsCommand(options = {}) {
27466
- console.log(chalk7.magenta("\n \u25C8 Cascade Models\n"));
28979
+ console.log(chalk8.magenta("\n \u25C8 Cascade Models\n"));
27467
28980
  const cm = new ConfigManager(process.cwd());
27468
28981
  await cm.load();
27469
28982
  const config = cm.getConfig();
@@ -27471,16 +28984,16 @@ async function modelsCommand(options = {}) {
27471
28984
  try {
27472
28985
  await router.init(config);
27473
28986
  } catch (err) {
27474
- console.error(chalk7.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
28987
+ console.error(chalk8.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
27475
28988
  process.exit(1);
27476
28989
  }
27477
28990
  await withTimeout(router.refreshLiveData(), 6e3, "live data timeout").catch(() => {
27478
28991
  });
27479
28992
  const liveData = router.getLiveData();
27480
28993
  const tiers = [
27481
- { tier: "T1", label: "T1 Administrator", color: chalk7.hex("#4C8DFF") },
27482
- { tier: "T2", label: "T2 Manager", color: chalk7.hex("#38B0DE") },
27483
- { tier: "T3", label: "T3 Worker", color: chalk7.hex("#2DD4BF") }
28994
+ { tier: "T1", label: "T1 Administrator", color: chalk8.hex("#4C8DFF") },
28995
+ { tier: "T2", label: "T2 Manager", color: chalk8.hex("#38B0DE") },
28996
+ { tier: "T3", label: "T3 Worker", color: chalk8.hex("#2DD4BF") }
27484
28997
  ];
27485
28998
  let anyMissing = false;
27486
28999
  let anyUnpriced = false;
@@ -27492,76 +29005,76 @@ async function modelsCommand(options = {}) {
27492
29005
  const costIn = unpriced ? "cost not tracked" : model.inputCostPer1kTokens === 0 ? "free" : `$${model.inputCostPer1kTokens.toFixed(4)}/1K in`;
27493
29006
  const costOut = unpriced ? "" : model.outputCostPer1kTokens === 0 ? "free" : `, $${model.outputCostPer1kTokens.toFixed(4)}/1K out`;
27494
29007
  const ctx = model.contextWindow >= 1e6 ? `${(model.contextWindow / 1e6).toFixed(1)}M ctx` : `${(model.contextWindow / 1e3).toFixed(0)}K ctx`;
27495
- const local = model.isLocal ? chalk7.gray(" [local]") : "";
27496
- const vision = model.isVisionCapable ? chalk7.gray(" \u{1F441}") : "";
29008
+ const local = model.isLocal ? chalk8.gray(" [local]") : "";
29009
+ const vision = model.isVisionCapable ? chalk8.gray(" \u{1F441}") : "";
27497
29010
  const bench = Math.round(benchmarkScore01(model, "mixed") * 100);
27498
29011
  console.log(
27499
- ` ${color.bold(tier)} ${chalk7.white(col(model.name, 24))}${chalk7.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk7.gray(col(ctx, 12))}${chalk7.gray(col(`bench ${bench}/100`, 14))}${chalk7.gray(`${costIn}${costOut}`)}` : `${chalk7.gray(col(ctx, 10))}${chalk7.gray(`bench ${bench}/100`)}`) + local + vision
29012
+ ` ${color.bold(tier)} ${chalk8.white(col(model.name, 24))}${chalk8.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk8.gray(col(ctx, 12))}${chalk8.gray(col(`bench ${bench}/100`, 14))}${chalk8.gray(`${costIn}${costOut}`)}` : `${chalk8.gray(col(ctx, 10))}${chalk8.gray(`bench ${bench}/100`)}`) + local + vision
27500
29013
  );
27501
29014
  } else {
27502
- console.log(` ${color.bold(tier)} ${chalk7.red("No model available")} ${chalk7.gray(`(check provider config for ${label})`)}`);
29015
+ console.log(` ${color.bold(tier)} ${chalk8.red("No model available")} ${chalk8.gray(`(check provider config for ${label})`)}`);
27503
29016
  anyMissing = true;
27504
29017
  }
27505
29018
  }
27506
29019
  console.log();
27507
29020
  const providers = config.providers.map((p) => p.type).join(", ") || "(none)";
27508
- console.log(chalk7.gray(` Configured providers: ${providers}`));
29021
+ console.log(chalk8.gray(` Configured providers: ${providers}`));
27509
29022
  if (liveData) {
27510
29023
  const src = liveData.getDataSource();
27511
29024
  const gen = liveData.getGeneratedAt();
27512
29025
  const srcLabel = src === "live" ? "live (just fetched)" : src === "cache" ? "cached" : "bundled";
27513
29026
  const pricingMeta = pricingDatasetMeta();
27514
- console.log(chalk7.gray(
29027
+ console.log(chalk8.gray(
27515
29028
  ` Benchmark data: ${srcLabel}` + (gen ? ` \xB7 updated ${gen.slice(0, 10)}` : "") + ` \xB7 pricing: ${liveData.hasLivePricing() ? "live (OpenRouter)" : "dataset"} (baseline ${pricingMeta.generatedAt})`
27516
29029
  ));
27517
29030
  const drift = liveData.getPriceDisagreements();
27518
29031
  if (drift.length) {
27519
- console.log(chalk7.yellow(`
29032
+ console.log(chalk8.yellow(`
27520
29033
  ${drift.length} model${drift.length === 1 ? "" : "s"} priced differently by the live source than by the bundled dataset:`));
27521
29034
  for (const d of drift.slice(0, 10)) {
27522
- console.log(chalk7.gray(
29035
+ console.log(chalk8.gray(
27523
29036
  ` ${d.provider}:${d.modelId} dataset $${d.dataset.input.toFixed(5)}/$${d.dataset.output.toFixed(5)} per 1K (as of ${d.datasetAsOf}) \u2192 live $${d.live.input.toFixed(5)}/$${d.live.output.toFixed(5)} (${d.ratio}\xD7)`
27524
29037
  ));
27525
29038
  }
27526
- console.log(chalk7.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
29039
+ console.log(chalk8.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
27527
29040
  }
27528
29041
  }
27529
29042
  if (anyUnpriced) {
27530
- console.log(chalk7.yellow(
29043
+ console.log(chalk8.yellow(
27531
29044
  '\n Some tiers use a model with no published price \u2014 their spend is reported as "cost not tracked", never $0.00,\n and is not counted toward a cost budget. Add the model to the pricing dataset, or set `local: true` on the\n provider if the endpoint genuinely costs nothing to run.'
27532
29045
  ));
27533
29046
  }
27534
29047
  if (options.verbose) {
27535
29048
  console.log();
27536
- console.log(chalk7.white(" Available models by provider:\n"));
29049
+ console.log(chalk8.white(" Available models by provider:\n"));
27537
29050
  const allProviderTypes = [...new Set(config.providers.map((p) => p.type))];
27538
29051
  for (const providerType of allProviderTypes) {
27539
29052
  const available = router.getModelsForProvider(providerType);
27540
29053
  if (available.length === 0) continue;
27541
- console.log(chalk7.gray(` ${providerType}:`));
29054
+ console.log(chalk8.gray(` ${providerType}:`));
27542
29055
  for (const m of available) {
27543
29056
  const override = config.models.t1 === m.id ? " \u2190 T1" : config.models.t2 === m.id ? " \u2190 T2" : config.models.t3 === m.id ? " \u2190 T3" : "";
27544
- console.log(` ${chalk7.white(col(m.name, 28))}${chalk7.gray(m.id)}${chalk7.yellow(override)}`);
29057
+ console.log(` ${chalk8.white(col(m.name, 28))}${chalk8.gray(m.id)}${chalk8.yellow(override)}`);
27545
29058
  }
27546
29059
  console.log();
27547
29060
  }
27548
29061
  }
27549
29062
  if (anyMissing) {
27550
- console.log(chalk7.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
29063
+ console.log(chalk8.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
27551
29064
  } else {
27552
- console.log(chalk7.green(" All tiers are configured.\n"));
29065
+ console.log(chalk8.green(" All tiers are configured.\n"));
27553
29066
  }
27554
29067
  }
27555
29068
  async function setModelCommand(action, tierArg, value) {
27556
29069
  const tier = (tierArg ?? "").toLowerCase();
27557
29070
  if (!["t1", "t2", "t3"].includes(tier)) {
27558
- console.error(chalk7.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
27559
- console.log(chalk7.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
29071
+ console.error(chalk8.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
29072
+ console.log(chalk8.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
27560
29073
  process.exit(1);
27561
29074
  }
27562
29075
  const override = action === "unset" ? "auto" : (value ?? "").trim();
27563
29076
  if (action === "set" && !override) {
27564
- console.error(chalk7.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
29077
+ console.error(chalk8.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
27565
29078
  process.exit(1);
27566
29079
  }
27567
29080
  const cm = new ConfigManager(process.cwd());
@@ -27571,8 +29084,8 @@ async function setModelCommand(action, tierArg, value) {
27571
29084
  if (override === "auto") delete models[tier];
27572
29085
  else models[tier] = override;
27573
29086
  await cm.updateConfig({ models });
27574
- const label = override === "auto" ? chalk7.gray("auto (routing decides)") : chalk7.white(override);
27575
- console.log(chalk7.green(`
29087
+ const label = override === "auto" ? chalk8.gray("auto (routing decides)") : chalk8.white(override);
29088
+ console.log(chalk8.green(`
27576
29089
  \u2713 ${tier.toUpperCase()} model set to ${label}
27577
29090
  `));
27578
29091
  }
@@ -27586,8 +29099,8 @@ async function exportCommand(options = {}) {
27586
29099
  let store;
27587
29100
  try {
27588
29101
  const workspacePath = options.workspacePath ?? process.cwd();
27589
- const workspaceDbPath = path34.join(workspacePath, CASCADE_DB_FILE);
27590
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
29102
+ const workspaceDbPath = path35.join(workspacePath, CASCADE_DB_FILE);
29103
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
27591
29104
  let dbPath = globalDbPath;
27592
29105
  try {
27593
29106
  await fs11.access(workspaceDbPath);
@@ -27596,7 +29109,7 @@ async function exportCommand(options = {}) {
27596
29109
  }
27597
29110
  store = new MemoryStore(dbPath);
27598
29111
  } catch (err) {
27599
- spin.fail(chalk7.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
29112
+ spin.fail(chalk8.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
27600
29113
  process.exit(1);
27601
29114
  }
27602
29115
  try {
@@ -27604,7 +29117,7 @@ async function exportCommand(options = {}) {
27604
29117
  if (options.sessionId) {
27605
29118
  const session2 = store.getSession(options.sessionId);
27606
29119
  if (!session2) {
27607
- spin.fail(chalk7.red(`Session "${options.sessionId}" not found.`));
29120
+ spin.fail(chalk8.red(`Session "${options.sessionId}" not found.`));
27608
29121
  process.exit(1);
27609
29122
  }
27610
29123
  sessions2 = [session2];
@@ -27612,14 +29125,14 @@ async function exportCommand(options = {}) {
27612
29125
  const limit = options.last ?? 10;
27613
29126
  sessions2 = store.listSessions(void 0, limit);
27614
29127
  if (sessions2.length === 0) {
27615
- spin.warn(chalk7.yellow("No sessions found."));
29128
+ spin.warn(chalk8.yellow("No sessions found."));
27616
29129
  return;
27617
29130
  }
27618
29131
  const latest = sessions2[0];
27619
29132
  const full = store.getSession(latest.id);
27620
29133
  sessions2 = full ? [full] : [];
27621
29134
  if (sessions2.length === 0) {
27622
- spin.fail(chalk7.red("Could not load latest session."));
29135
+ spin.fail(chalk8.red("Could not load latest session."));
27623
29136
  process.exit(1);
27624
29137
  }
27625
29138
  }
@@ -27629,17 +29142,17 @@ async function exportCommand(options = {}) {
27629
29142
  const ext = format === "json" ? ".json" : ".md";
27630
29143
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
27631
29144
  const defaultFile = `cascade-export-${safeName}${ext}`;
27632
- const outPath = options.output ? path34.resolve(options.output) : path34.join(process.cwd(), defaultFile);
29145
+ const outPath = options.output ? path35.resolve(options.output) : path35.join(process.cwd(), defaultFile);
27633
29146
  await fs11.writeFile(outPath, content, "utf-8");
27634
- spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
29147
+ spin.succeed(chalk8.green(`Exported to ${chalk8.white(outPath)}`));
27635
29148
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
27636
29149
  console.log();
27637
- console.log(chalk7.gray(` Session: ${session.title}`));
27638
- console.log(chalk7.gray(` Messages: ${messageCount}`));
27639
- console.log(chalk7.gray(` Format: ${format}`));
29150
+ console.log(chalk8.gray(` Session: ${session.title}`));
29151
+ console.log(chalk8.gray(` Messages: ${messageCount}`));
29152
+ console.log(chalk8.gray(` Format: ${format}`));
27640
29153
  console.log();
27641
29154
  } catch (err) {
27642
- spin.fail(chalk7.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
29155
+ spin.fail(chalk8.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
27643
29156
  process.exit(1);
27644
29157
  }
27645
29158
  }
@@ -27693,93 +29206,6 @@ function buildJsonExport(session) {
27693
29206
  };
27694
29207
  return JSON.stringify(payload, null, 2);
27695
29208
  }
27696
- async function linkCommand(target, options = {}) {
27697
- const found = await discoverCredentials();
27698
- if (found.length === 0) {
27699
- console.log(chalk7.yellow("\n No reusable credentials found.\n"));
27700
- console.log(chalk7.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
27701
- console.log(chalk7.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
27702
- return;
27703
- }
27704
- if (!target) {
27705
- printDiscovered(found);
27706
- return;
27707
- }
27708
- const provider = normalizeProvider(target);
27709
- if (!provider) {
27710
- console.log(chalk7.red(`
27711
- Unknown provider "${target}". Use one of: anthropic, openai, gemini.
27712
- `));
27713
- return;
27714
- }
27715
- const candidates2 = found.filter((c) => c.provider === provider);
27716
- const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
27717
- if (!chosen) {
27718
- console.log(chalk7.yellow(`
27719
- No detected credential maps to "${provider}".
27720
- `));
27721
- return;
27722
- }
27723
- if (!chosen.directlyUsable) {
27724
- console.log(chalk7.yellow(`
27725
- Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
27726
- if (chosen.warning) console.log(chalk7.gray(` ${chosen.warning}`));
27727
- console.log(chalk7.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
27728
- return;
27729
- }
27730
- if (chosen.kind === "oauth" && !options.acceptRisk) {
27731
- console.log(chalk7.yellow(`
27732
- ${chosen.sourceTool} provides a subscription OAuth token, not an API key.`));
27733
- if (chosen.warning) console.log(chalk7.gray(` ${chosen.warning}`));
27734
- console.log(chalk7.gray(" Re-run with --accept-risk to adopt it anyway:\n"));
27735
- console.log(chalk7.cyan(` cascade link ${provider} --accept-risk
27736
- `));
27737
- return;
27738
- }
27739
- await adoptCredential(chosen, options.workspace ?? process.cwd());
27740
- console.log(chalk7.green(`
27741
- \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
27742
- if (chosen.kind === "oauth") {
27743
- console.log(chalk7.gray(" Adopted as an OAuth bearer token \u2014 revoke it in the source tool to disable."));
27744
- }
27745
- console.log(chalk7.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
27746
- }
27747
- function printDiscovered(found) {
27748
- console.log(chalk7.magenta("\n \u25C8 Detected credentials\n"));
27749
- for (const c of found) {
27750
- const usable = c.directlyUsable ? chalk7.green("usable") : chalk7.yellow("needs vendor backend");
27751
- const kind = c.kind === "oauth" ? chalk7.yellow("oauth") : chalk7.gray("api-key");
27752
- console.log(` ${chalk7.white(c.provider.padEnd(18))} ${chalk7.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
27753
- console.log(chalk7.gray(` from ${c.sourceTool}`));
27754
- if (c.warning) console.log(chalk7.yellow(` \u26A0 ${c.warning}`));
27755
- }
27756
- console.log(chalk7.gray("\n Adopt one with: ") + chalk7.cyan("cascade link <provider> [--accept-risk]"));
27757
- console.log(chalk7.gray(" --accept-risk is required for subscription OAuth tokens.\n"));
27758
- }
27759
- function normalizeProvider(target) {
27760
- const t = target.toLowerCase();
27761
- if (t === "anthropic" || t === "claude" || t === "claude-code") return "anthropic";
27762
- if (t === "openai" || t === "codex" || t === "gpt") return "openai";
27763
- if (t === "gemini" || t === "google") return "gemini";
27764
- return null;
27765
- }
27766
- async function adoptCredential(cred, workspace) {
27767
- const cm = new ConfigManager(workspace);
27768
- await cm.load();
27769
- const config = cm.getConfig();
27770
- const next = {
27771
- type: cred.provider,
27772
- credentialSource: cred.sourceTool
27773
- };
27774
- if (cred.kind === "oauth" && cred.provider === "anthropic") {
27775
- next.authToken = cred.secret;
27776
- } else {
27777
- next.apiKey = cred.secret;
27778
- }
27779
- const providers = config.providers.filter((p) => p.type !== cred.provider);
27780
- providers.push(next);
27781
- await cm.updateConfig({ providers });
27782
- }
27783
29209
  async function telemetryCommand(action) {
27784
29210
  const cm = new ConfigManager(process.cwd());
27785
29211
  await cm.load();
@@ -27787,12 +29213,12 @@ async function telemetryCommand(action) {
27787
29213
  if (action === "status") {
27788
29214
  const state = config.telemetry?.enabled ? "ON" : "OFF";
27789
29215
  console.log();
27790
- console.log(chalk7.magenta(" \u25C8 Cascade Telemetry"));
29216
+ console.log(chalk8.magenta(" \u25C8 Cascade Telemetry"));
27791
29217
  console.log();
27792
- console.log(` Status: ${config.telemetry?.enabled ? chalk7.green(state) : chalk7.gray(state)}`);
27793
- console.log(chalk7.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
29218
+ console.log(` Status: ${config.telemetry?.enabled ? chalk8.green(state) : chalk8.gray(state)}`);
29219
+ console.log(chalk8.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
27794
29220
  console.log();
27795
- console.log(chalk7.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
29221
+ console.log(chalk8.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
27796
29222
  console.log();
27797
29223
  return;
27798
29224
  }
@@ -27806,11 +29232,11 @@ async function telemetryCommand(action) {
27806
29232
  });
27807
29233
  console.log();
27808
29234
  if (enabled) {
27809
- console.log(chalk7.green(` \u2713 Telemetry enabled.`));
27810
- console.log(chalk7.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
29235
+ console.log(chalk8.green(` \u2713 Telemetry enabled.`));
29236
+ console.log(chalk8.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
27811
29237
  } else {
27812
- console.log(chalk7.yellow(` \u2713 Telemetry disabled.`));
27813
- console.log(chalk7.gray(" No events will be transmitted from this workspace."));
29238
+ console.log(chalk8.yellow(` \u2713 Telemetry disabled.`));
29239
+ console.log(chalk8.gray(" No events will be transmitted from this workspace."));
27814
29240
  }
27815
29241
  console.log();
27816
29242
  }
@@ -27820,11 +29246,11 @@ async function statsCommand() {
27820
29246
  await tracker.load();
27821
29247
  const all = tracker.getAll();
27822
29248
  if (all.size === 0) {
27823
- console.log(chalk7.dim("\n No routing history yet \u2014 run some tasks first.\n"));
29249
+ console.log(chalk8.dim("\n No routing history yet \u2014 run some tasks first.\n"));
27824
29250
  return;
27825
29251
  }
27826
- console.log(chalk7.magenta("\n \u25C8 Auto-Routing History\n"));
27827
- console.log(chalk7.dim(" Per-task-type model performance learned from past runs.\n"));
29252
+ console.log(chalk8.magenta("\n \u25C8 Auto-Routing History\n"));
29253
+ console.log(chalk8.dim(" Per-task-type model performance learned from past runs.\n"));
27828
29254
  for (const taskType of TASK_TYPES) {
27829
29255
  const entries = [];
27830
29256
  for (const [key, stat] of all) {
@@ -27836,36 +29262,36 @@ async function statsCommand() {
27836
29262
  }
27837
29263
  if (entries.length === 0) continue;
27838
29264
  entries.sort((a, b) => b.successRate - a.successRate || b.samples - a.samples);
27839
- console.log(chalk7.bold(` ${taskType.toUpperCase()}`));
29265
+ console.log(chalk8.bold(` ${taskType.toUpperCase()}`));
27840
29266
  const header = ` ${"Model".padEnd(36)} ${"Success".padEnd(9)} ${"Samples".padEnd(9)} Avg cost`;
27841
- console.log(chalk7.dim(header));
27842
- console.log(chalk7.dim(" " + "\u2500".repeat(62)));
29267
+ console.log(chalk8.dim(header));
29268
+ console.log(chalk8.dim(" " + "\u2500".repeat(62)));
27843
29269
  for (const e of entries) {
27844
29270
  const pct = `${Math.round(e.successRate * 100)}%`;
27845
29271
  const cost = e.avgCostUsd < 1e-4 ? "<$0.0001" : `$${e.avgCostUsd.toFixed(4)}`;
27846
- const color = e.successRate >= 0.8 ? chalk7.green : e.successRate >= 0.5 ? chalk7.yellow : chalk7.red;
29272
+ const color = e.successRate >= 0.8 ? chalk8.green : e.successRate >= 0.5 ? chalk8.yellow : chalk8.red;
27847
29273
  console.log(
27848
- ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk7.dim(cost)}`
29274
+ ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk8.dim(cost)}`
27849
29275
  );
27850
29276
  }
27851
29277
  console.log();
27852
29278
  }
27853
- console.log(chalk7.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
29279
+ console.log(chalk8.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
27854
29280
  }
27855
29281
 
27856
29282
  // src/cli/index.ts
27857
29283
  dotenv.config();
27858
29284
  function warnIfBuildIsStale() {
27859
29285
  try {
27860
- const here = path34.dirname(fileURLToPath(import.meta.url));
29286
+ const here = path35.dirname(fileURLToPath(import.meta.url));
27861
29287
  for (const rel of ["..", "../.."]) {
27862
- const pkgPath = path34.join(here, rel, "package.json");
27863
- if (!fs31.existsSync(pkgPath)) continue;
27864
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
29288
+ const pkgPath = path35.join(here, rel, "package.json");
29289
+ if (!fs32.existsSync(pkgPath)) continue;
29290
+ const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf-8"));
27865
29291
  if (pkg.name !== "cascade-ai") continue;
27866
29292
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
27867
29293
  console.error(
27868
- chalk7.yellow(
29294
+ chalk8.yellow(
27869
29295
  `\u26A0 Stale build: compiled output is v${CASCADE_VERSION} but the source tree is v${pkg.version}.
27870
29296
  Run: npm install && npm run build`
27871
29297
  )
@@ -27964,7 +29390,7 @@ mcp.command("list").description("List configured MCP servers").action(async () =
27964
29390
  mcp.command("remove <name>").description("Remove an MCP server (and its stored OAuth tokens)").action(async (name) => {
27965
29391
  await mcpRemoveCommand(name);
27966
29392
  });
27967
- program.command("link [provider]").description("Reuse credentials from other AI CLIs (Claude Code, Codex, Gemini CLI, Copilot)").option("--accept-risk", "Adopt a subscription OAuth token despite the ToS warning").action(async (provider, opts) => {
29393
+ program.command("link [provider]").description("Reuse credentials from other AI CLIs (Claude Code, Codex, Gemini CLI, Copilot)").option("--accept-risk", "Deprecated \u2014 subscription tokens can no longer be adopted").action(async (provider, opts) => {
27968
29394
  await linkCommand(provider, { acceptRisk: opts.acceptRisk, workspace: process.cwd() });
27969
29395
  });
27970
29396
  program.command("update").description("Update Cascade to the latest version").action(async () => {
@@ -27990,7 +29416,7 @@ program.command("models [action] [tier] [value]").description("List AI models pe
27990
29416
  program.command("telemetry [action]").description("Toggle anonymous usage telemetry (on | off | status). Default: status").action(async (action) => {
27991
29417
  const normalized = (action ?? "status").toLowerCase();
27992
29418
  if (normalized !== "on" && normalized !== "off" && normalized !== "status") {
27993
- console.error(chalk7.red(`Unknown action: ${action}. Use: on | off | status`));
29419
+ console.error(chalk8.red(`Unknown action: ${action}. Use: on | off | status`));
27994
29420
  process.exit(1);
27995
29421
  }
27996
29422
  await telemetryCommand(normalized);
@@ -28012,15 +29438,15 @@ async function startRepl(options) {
28012
29438
  try {
28013
29439
  await cm.load();
28014
29440
  } catch (err) {
28015
- console.error(chalk7.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28016
- console.error(chalk7.gray("Run `cascade init` to set up this directory."));
29441
+ console.error(chalk8.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29442
+ console.error(chalk8.gray("Run `cascade init` to set up this directory."));
28017
29443
  process.exit(1);
28018
29444
  }
28019
29445
  let config = cm.getConfig();
28020
29446
  const startupNotice = cm.takeRetiredNotice();
28021
29447
  const needsSetup = !hasUsableProvider(config.providers);
28022
29448
  if (needsSetup) {
28023
- console.log(chalk7.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
29449
+ console.log(chalk8.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
28024
29450
  console.log();
28025
29451
  config = await runSetupWizard(workspacePath);
28026
29452
  await cm.updateConfig(config);
@@ -28055,24 +29481,24 @@ async function runHeadless(prompt, options) {
28055
29481
  try {
28056
29482
  await cm.load();
28057
29483
  } catch (err) {
28058
- console.error(chalk7.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28059
- console.error(chalk7.gray("Run `cascade init` to set up this directory."));
29484
+ console.error(chalk8.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29485
+ console.error(chalk8.gray("Run `cascade init` to set up this directory."));
28060
29486
  process.exit(1);
28061
29487
  }
28062
29488
  const config = cm.getConfig();
28063
29489
  const needsSetup = !hasUsableProvider(config.providers);
28064
29490
  if (needsSetup) {
28065
- console.error(chalk7.red("No providers configured. Run `cascade init` first."));
29491
+ console.error(chalk8.red("No providers configured. Run `cascade init` first."));
28066
29492
  process.exit(1);
28067
29493
  }
28068
29494
  const cascade = new Cascade(config, workspacePath);
28069
29495
  try {
28070
29496
  await cascade.init();
28071
29497
  } catch (err) {
28072
- console.error(chalk7.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
29498
+ console.error(chalk8.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
28073
29499
  process.exit(1);
28074
29500
  }
28075
- console.error(chalk7.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
29501
+ console.error(chalk8.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
28076
29502
  let lastProgress = "";
28077
29503
  cascade.on("tier:status", (ev) => {
28078
29504
  const action = ev?.currentAction?.trim();
@@ -28080,7 +29506,7 @@ async function runHeadless(prompt, options) {
28080
29506
  const line = ` \xB7 ${ev.role ?? ""} ${action}`.trimEnd();
28081
29507
  if (line === lastProgress) return;
28082
29508
  lastProgress = line;
28083
- console.error(chalk7.gray(line));
29509
+ console.error(chalk8.gray(line));
28084
29510
  });
28085
29511
  try {
28086
29512
  const result = await cascade.run({
@@ -28091,7 +29517,7 @@ async function runHeadless(prompt, options) {
28091
29517
  });
28092
29518
  process.stdout.write(result.output.trimEnd() + "\n");
28093
29519
  } catch (err) {
28094
- console.error(chalk7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
29520
+ console.error(chalk8.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
28095
29521
  await cascade.close().catch(() => {
28096
29522
  });
28097
29523
  process.exit(1);
@@ -28103,9 +29529,9 @@ async function runHeadless(prompt, options) {
28103
29529
  function printBanner() {
28104
29530
  if (process.stdout.columns < 60) return;
28105
29531
  console.log();
28106
- console.log(" " + chalk7.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
28107
- console.log(" " + chalk7.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7.white.bold("CASCADE") + chalk7.gray(" v" + CASCADE_VERSION));
28108
- console.log(" " + chalk7.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7.hex("#8A93A8")("multi-tier orchestration"));
29532
+ console.log(" " + chalk8.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
29533
+ console.log(" " + chalk8.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8.white.bold("CASCADE") + chalk8.gray(" v" + CASCADE_VERSION));
29534
+ console.log(" " + chalk8.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8.hex("#8A93A8")("multi-tier orchestration"));
28109
29535
  console.log();
28110
29536
  }
28111
29537
  program.parse(process.argv);