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.cjs CHANGED
@@ -1,22 +1,22 @@
1
1
  'use strict';
2
2
 
3
- var Anthropic = require('@anthropic-ai/sdk');
4
- var OpenAI = require('openai');
5
- var genai = require('@google/genai');
6
3
  var dns = require('dns');
7
4
  var http = require('http');
8
5
  var https = require('https');
9
6
  var zlib = require('zlib');
10
7
  var stream = require('stream');
8
+ var Anthropic = require('@anthropic-ai/sdk');
9
+ var OpenAI = require('openai');
10
+ var genai = require('@google/genai');
11
11
  var Database2 = require('better-sqlite3');
12
- var path34 = require('path');
13
- var fs31 = require('fs');
12
+ var path35 = require('path');
13
+ var fs32 = require('fs');
14
14
  var os10 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var ink = require('ink');
17
17
  var commander = require('commander');
18
18
  var React2 = require('react');
19
- var chalk7 = require('chalk');
19
+ var chalk8 = require('chalk');
20
20
  var dotenv = require('dotenv');
21
21
  var url = require('url');
22
22
  var fs11 = require('fs/promises');
@@ -70,19 +70,19 @@ function _interopNamespace(e) {
70
70
  return Object.freeze(n);
71
71
  }
72
72
 
73
- var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
74
- var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
75
73
  var dns__default = /*#__PURE__*/_interopDefault(dns);
76
74
  var http__default = /*#__PURE__*/_interopDefault(http);
77
75
  var https__default = /*#__PURE__*/_interopDefault(https);
78
76
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
77
+ var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
78
+ var OpenAI__default = /*#__PURE__*/_interopDefault(OpenAI);
79
79
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
80
- var path34__default = /*#__PURE__*/_interopDefault(path34);
81
- var fs31__default = /*#__PURE__*/_interopDefault(fs31);
80
+ var path35__default = /*#__PURE__*/_interopDefault(path35);
81
+ var fs32__default = /*#__PURE__*/_interopDefault(fs32);
82
82
  var os10__default = /*#__PURE__*/_interopDefault(os10);
83
83
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
84
84
  var React2__default = /*#__PURE__*/_interopDefault(React2);
85
- var chalk7__default = /*#__PURE__*/_interopDefault(chalk7);
85
+ var chalk8__default = /*#__PURE__*/_interopDefault(chalk8);
86
86
  var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
87
87
  var fs11__default = /*#__PURE__*/_interopDefault(fs11);
88
88
  var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
@@ -1819,7 +1819,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1819
1819
  var init_constants = __esm({
1820
1820
  "src/constants.ts"() {
1821
1821
  init_pricing();
1822
- CASCADE_VERSION = "0.73.0";
1822
+ CASCADE_VERSION = "0.75.0";
1823
1823
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1824
1824
  CASCADE_DB_FILE = ".cascade/memory.db";
1825
1825
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2248,6 +2248,244 @@ var init_constants = __esm({
2248
2248
  ];
2249
2249
  }
2250
2250
  });
2251
+ function preferIpv4Host(url) {
2252
+ return url;
2253
+ }
2254
+ function stripTrailingSlashes(value) {
2255
+ let end = value.length;
2256
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
2257
+ return end === value.length ? value : value.slice(0, end);
2258
+ }
2259
+ function normalizeEndpoint(url) {
2260
+ const raw = stripTrailingSlashes((url ?? "").trim());
2261
+ if (!raw) return "";
2262
+ try {
2263
+ const u = new URL(raw);
2264
+ const path40 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2265
+ return `${u.protocol}//${u.host.toLowerCase()}${path40}`;
2266
+ } catch {
2267
+ return raw.toLowerCase();
2268
+ }
2269
+ }
2270
+ function stripPathSegment(url, segment) {
2271
+ const trimmed = stripTrailingSlashes(url.trim());
2272
+ const suffix = `/${segment}`;
2273
+ return trimmed.endsWith(suffix) ? trimmed.slice(0, -suffix.length) : trimmed;
2274
+ }
2275
+ function clientApiRoot(type, url) {
2276
+ const segment = CLIENT_VERSION_SEGMENT[type];
2277
+ return segment ? stripPathSegment(url, segment) : stripTrailingSlashes(url.trim());
2278
+ }
2279
+ function headersFrom(raw) {
2280
+ const out = new Headers();
2281
+ for (const [k, v] of Object.entries(raw)) {
2282
+ if (k === "content-encoding" || k === "content-length") continue;
2283
+ if (Array.isArray(v)) out.set(k, v.join(", "));
2284
+ else if (typeof v === "string") out.set(k, v);
2285
+ }
2286
+ return out;
2287
+ }
2288
+ async function fetchSameOrigin(url, init = {}, maxRedirects = MAX_REDIRECTS) {
2289
+ const origin = new URL(url).origin;
2290
+ let current = url;
2291
+ let options = { ...init };
2292
+ for (let hop = 0; hop <= maxRedirects; hop++) {
2293
+ const res = await fetch(current, { ...options, redirect: "manual" });
2294
+ if (![301, 302, 303, 307, 308].includes(res.status)) return res;
2295
+ const location = res.headers.get("location");
2296
+ if (!location) return res;
2297
+ await res.body?.cancel().catch(() => {
2298
+ });
2299
+ const next = new URL(location, current);
2300
+ if (next.origin !== origin) {
2301
+ throw new Error(
2302
+ `Refusing cross-origin redirect to ${next.origin}: the request carries a credential for ${origin}.`
2303
+ );
2304
+ }
2305
+ const method = (options.method ?? "GET").toUpperCase();
2306
+ const downgrades = res.status === 303 ? method !== "GET" && method !== "HEAD" : (res.status === 301 || res.status === 302) && method === "POST";
2307
+ if (downgrades) {
2308
+ const headers = new Headers(options.headers);
2309
+ for (const h of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2310
+ headers.delete(h);
2311
+ }
2312
+ options = { ...options, method: "GET", body: void 0, headers };
2313
+ }
2314
+ current = next.toString();
2315
+ }
2316
+ throw new Error(`Too many redirects from ${url}`);
2317
+ }
2318
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
2319
+ const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2320
+ const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
2321
+ const lib = u.protocol === "https:" ? https__default.default : http__default.default;
2322
+ const method = (init.method ?? "GET").toUpperCase();
2323
+ const headers = {};
2324
+ const h = init.headers;
2325
+ if (h instanceof Headers) h.forEach((v, k) => {
2326
+ headers[k] = v;
2327
+ });
2328
+ else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
2329
+ else if (h) Object.assign(headers, h);
2330
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
2331
+ headers["accept-encoding"] = "gzip, deflate, br";
2332
+ }
2333
+ const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
2334
+ return new Promise((resolve2, reject) => {
2335
+ const req = lib.request(
2336
+ {
2337
+ hostname: u.hostname,
2338
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
2339
+ path: u.pathname + u.search,
2340
+ method,
2341
+ headers
2342
+ },
2343
+ (res) => {
2344
+ const status = res.statusCode ?? 200;
2345
+ const location = res.headers.location;
2346
+ if (REDIRECT_STATUSES.has(status) && location && redirectCount < MAX_REDIRECTS) {
2347
+ res.resume();
2348
+ const nextUrl = new URL(location, u).href;
2349
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
2350
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
2351
+ return;
2352
+ }
2353
+ const downgrade = status === 303 ? method !== "GET" && method !== "HEAD" : (status === 301 || status === 302) && method === "POST";
2354
+ let nextInit = init;
2355
+ if (downgrade) {
2356
+ const headers2 = new Headers(init.headers);
2357
+ for (const h2 of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2358
+ headers2.delete(h2);
2359
+ }
2360
+ nextInit = { ...init, method: "GET", body: void 0, headers: headers2 };
2361
+ }
2362
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
2363
+ return;
2364
+ }
2365
+ const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
2366
+ let bodyStream = res;
2367
+ if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib__default.default.createGunzip());
2368
+ else if (encoding === "deflate") bodyStream = res.pipe(zlib__default.default.createInflate());
2369
+ else if (encoding === "br") bodyStream = res.pipe(zlib__default.default.createBrotliDecompress());
2370
+ if (NULL_BODY_STATUSES.has(status)) {
2371
+ bodyStream.resume();
2372
+ resolve2(new Response(null, {
2373
+ status,
2374
+ statusText: res.statusMessage ?? "",
2375
+ headers: headersFrom(res.headers)
2376
+ }));
2377
+ return;
2378
+ }
2379
+ const stream$1 = stream.Readable.toWeb(bodyStream);
2380
+ resolve2(new Response(stream$1, {
2381
+ status,
2382
+ statusText: res.statusMessage ?? "",
2383
+ headers: headersFrom(res.headers)
2384
+ }));
2385
+ }
2386
+ );
2387
+ req.on("error", reject);
2388
+ req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
2389
+ if (body !== void 0) req.write(body);
2390
+ req.end();
2391
+ });
2392
+ }
2393
+ var CLIENT_VERSION_SEGMENT, REDIRECT_STATUSES, NULL_BODY_STATUSES, MAX_REDIRECTS;
2394
+ var init_net = __esm({
2395
+ "src/utils/net.ts"() {
2396
+ try {
2397
+ dns__default.default.setDefaultResultOrder("ipv4first");
2398
+ } catch {
2399
+ }
2400
+ CLIENT_VERSION_SEGMENT = {
2401
+ anthropic: "v1",
2402
+ gemini: "v1beta"
2403
+ };
2404
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
2405
+ NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
2406
+ MAX_REDIRECTS = 5;
2407
+ }
2408
+ });
2409
+
2410
+ // src/config/revoked-credentials.ts
2411
+ function isRevokedSubscriptionCredential(p) {
2412
+ if (typeof p !== "object" || p === null) return false;
2413
+ if (p.type !== "anthropic") return false;
2414
+ if (!p.authToken && !p.apiKey) return false;
2415
+ return isSubscriptionToken(p.authToken) || isSubscriptionToken(p.apiKey) || Boolean(p.authToken) && /claude\s*code/i.test(p.credentialSource ?? "");
2416
+ }
2417
+ function isSubscriptionToken(secret) {
2418
+ return !!secret && secret.trim().startsWith("sk-ant-oat");
2419
+ }
2420
+ function stripRevokedCredentials(providers, { keepForEndpoint = true } = {}) {
2421
+ let removed = 0;
2422
+ const kept = [];
2423
+ for (const p of providers) {
2424
+ if (!isRevokedSubscriptionCredential(p)) {
2425
+ kept.push(p);
2426
+ continue;
2427
+ }
2428
+ removed++;
2429
+ const cleaned = { ...p };
2430
+ if (isSubscriptionToken(cleaned.apiKey)) delete cleaned.apiKey;
2431
+ const bearerIsDead = isSubscriptionToken(cleaned.authToken) || Boolean(cleaned.authToken) && /claude\s*code/i.test(cleaned.credentialSource ?? "");
2432
+ if (bearerIsDead) {
2433
+ delete cleaned.authToken;
2434
+ delete cleaned.credentialSource;
2435
+ }
2436
+ const worthKeeping = Boolean(
2437
+ cleaned.apiKey || cleaned.authToken || keepForEndpoint && cleaned.baseUrl
2438
+ );
2439
+ if (worthKeeping) kept.push(cleaned);
2440
+ }
2441
+ return { kept, removed };
2442
+ }
2443
+ function stripRevokedFromConfig(raw) {
2444
+ const result = { removed: 0 };
2445
+ if (typeof raw !== "object" || raw === null) return result;
2446
+ const cfg = raw;
2447
+ if (Array.isArray(cfg["providers"])) {
2448
+ const pass = stripRevokedCredentials(cfg["providers"]);
2449
+ result.removed = pass.removed;
2450
+ if (pass.removed > 0) cfg["providers"] = pass.kept;
2451
+ }
2452
+ return result;
2453
+ }
2454
+ function hasUsableAnthropic(providers) {
2455
+ return providers.some((p) => p?.type === "anthropic" && Boolean(p.apiKey || p.authToken && p.baseUrl));
2456
+ }
2457
+ function clearAnthropicPins(models, providers) {
2458
+ const cleared = [];
2459
+ if (typeof models !== "object" || models === null) return cleared;
2460
+ const tiers = models;
2461
+ for (const tier of ["t1", "t2", "t3"]) {
2462
+ const pin = tiers[tier];
2463
+ if (typeof pin !== "string" || !namesAnthropicModel(pin, providers)) continue;
2464
+ delete tiers[tier];
2465
+ cleared.push({ tier, model: pin });
2466
+ }
2467
+ return cleared;
2468
+ }
2469
+ function namesAnthropicModel(pin, providers) {
2470
+ const value = pin.trim().toLowerCase();
2471
+ if (!value) return false;
2472
+ if (value.includes(":")) return value.startsWith("anthropic:");
2473
+ const known = Object.values(MODELS).some(
2474
+ (m) => m.provider === "anthropic" && m.id.toLowerCase() === value
2475
+ );
2476
+ if (!known && !value.startsWith("claude-")) return false;
2477
+ return !canBeServedElsewhere(value, providers);
2478
+ }
2479
+ function canBeServedElsewhere(modelId, providers) {
2480
+ return providers.some((p) => p && p.type === "azure" && (p.deploymentName ?? "").trim().toLowerCase() === modelId);
2481
+ }
2482
+ var REVOKED_CREDENTIAL_REASON;
2483
+ var init_revoked_credentials = __esm({
2484
+ "src/config/revoked-credentials.ts"() {
2485
+ init_constants();
2486
+ 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.";
2487
+ }
2488
+ });
2251
2489
 
2252
2490
  // src/utils/cost.ts
2253
2491
  function resolveModelPricing(model, opts = {}) {
@@ -2317,10 +2555,17 @@ var init_cost = __esm({
2317
2555
  });
2318
2556
 
2319
2557
  // src/providers/base.ts
2320
- var BaseProvider;
2558
+ var ProviderUnreachableError, BaseProvider;
2321
2559
  var init_base = __esm({
2322
2560
  "src/providers/base.ts"() {
2323
2561
  init_cost();
2562
+ ProviderUnreachableError = class extends Error {
2563
+ transient = true;
2564
+ constructor(message) {
2565
+ super(message);
2566
+ this.name = "ProviderUnreachableError";
2567
+ }
2568
+ };
2324
2569
  BaseProvider = class {
2325
2570
  config;
2326
2571
  model;
@@ -2393,6 +2638,8 @@ var init_model_filter = __esm({
2393
2638
  var anthropic_exports = {};
2394
2639
  __export(anthropic_exports, {
2395
2640
  AnthropicProvider: () => AnthropicProvider,
2641
+ anthropicApiRoot: () => anthropicApiRoot,
2642
+ anthropicAuth: () => anthropicAuth,
2396
2643
  toAnthropicToolUse: () => toAnthropicToolUse,
2397
2644
  toAnthropicTools: () => toAnthropicTools
2398
2645
  });
@@ -2417,25 +2664,56 @@ function toAnthropicToolUse(toolCalls) {
2417
2664
  input: tc.input
2418
2665
  }));
2419
2666
  }
2420
- var AnthropicProvider;
2667
+ function anthropicApiRoot(configured) {
2668
+ if (!configured) return void 0;
2669
+ const root = clientApiRoot("anthropic", configured);
2670
+ return root || void 0;
2671
+ }
2672
+ function anthropicAuth(config) {
2673
+ const gateway = anthropicApiRoot(config.baseUrl);
2674
+ const bearerIsSubscription = isSubscriptionToken(config.authToken);
2675
+ const keyIsSubscription = isSubscriptionToken(config.apiKey);
2676
+ if (bearerIsSubscription || keyIsSubscription) {
2677
+ if (config.apiKey && !keyIsSubscription) return { mode: "apiKey", key: config.apiKey };
2678
+ throw new Error(
2679
+ "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."
2680
+ );
2681
+ }
2682
+ if (config.authToken && gateway) return { mode: "bearer", token: config.authToken };
2683
+ if (config.apiKey) return { mode: "apiKey", key: config.apiKey };
2684
+ if (config.authToken) {
2685
+ throw new Error(
2686
+ "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."
2687
+ );
2688
+ }
2689
+ return { mode: "none" };
2690
+ }
2691
+ var sameOriginFetch, AnthropicProvider;
2421
2692
  var init_anthropic = __esm({
2422
2693
  "src/providers/anthropic.ts"() {
2423
2694
  init_constants();
2424
2695
  init_base();
2425
2696
  init_pricing();
2426
2697
  init_model_filter();
2698
+ init_net();
2699
+ init_revoked_credentials();
2700
+ sameOriginFetch = ((input, init) => {
2701
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2702
+ return fetchSameOrigin(url, init);
2703
+ });
2427
2704
  AnthropicProvider = class extends BaseProvider {
2428
2705
  client;
2429
2706
  constructor(config, model) {
2430
2707
  super(config, model);
2431
- if (config.authToken) {
2432
- this.client = new Anthropic__default.default({
2433
- authToken: config.authToken,
2434
- defaultHeaders: { "anthropic-beta": "oauth-2025-04-20" }
2435
- });
2708
+ const baseURL = anthropicApiRoot(config.baseUrl);
2709
+ const auth2 = anthropicAuth(config);
2710
+ if (auth2.mode === "bearer") {
2711
+ this.client = new Anthropic__default.default({ authToken: auth2.token, fetch: sameOriginFetch, baseURL });
2436
2712
  } else {
2437
2713
  this.client = new Anthropic__default.default({
2438
- apiKey: config.apiKey
2714
+ apiKey: auth2.mode === "apiKey" ? auth2.key : void 0,
2715
+ fetch: sameOriginFetch,
2716
+ ...baseURL ? { baseURL } : {}
2439
2717
  });
2440
2718
  }
2441
2719
  }
@@ -2511,21 +2789,24 @@ var init_anthropic = __esm({
2511
2789
  async countTokens(text) {
2512
2790
  return Math.ceil(text.length / 4);
2513
2791
  }
2514
- async listModels() {
2792
+ async listModels(options = {}) {
2793
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "anthropic");
2515
2794
  try {
2516
- const resp = await fetch("https://api.anthropic.com/v1/models", {
2795
+ const base = anthropicApiRoot(this.config.baseUrl) ?? "https://api.anthropic.com";
2796
+ const modelsUrl = `${base}/v1/models`;
2797
+ const auth2 = anthropicAuth(this.config);
2798
+ const resp = await fetchSameOrigin(modelsUrl, {
2517
2799
  headers: {
2518
- "x-api-key": this.config.apiKey ?? "",
2800
+ // Same decision the constructor makes — see anthropicAuth(). Read
2801
+ // straight off `config`, this branch sent a bearer to the public
2802
+ // default host whenever no gateway was configured.
2803
+ ...auth2.mode === "bearer" ? { authorization: `Bearer ${auth2.token}` } : { "x-api-key": auth2.mode === "apiKey" ? auth2.key : "" },
2519
2804
  "anthropic-version": "2023-06-01"
2520
2805
  }
2521
2806
  });
2522
- if (!resp.ok) {
2523
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2524
- }
2807
+ if (!resp.ok) return staticCatalog();
2525
2808
  const data = await resp.json();
2526
- if (!Array.isArray(data?.data)) {
2527
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2528
- }
2809
+ if (!Array.isArray(data?.data)) return staticCatalog();
2529
2810
  return data.data.filter((m) => isChatModel(m.id)).map((m) => {
2530
2811
  const known = Object.values(MODELS).find((km) => km.id === m.id && km.provider === "anthropic");
2531
2812
  if (known) return known;
@@ -2543,7 +2824,7 @@ var init_anthropic = __esm({
2543
2824
  });
2544
2825
  });
2545
2826
  } catch {
2546
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2827
+ return staticCatalog();
2547
2828
  }
2548
2829
  }
2549
2830
  async isAvailable() {
@@ -2763,7 +3044,8 @@ var init_openai = __esm({
2763
3044
  async countTokens(text) {
2764
3045
  return Math.ceil(text.length / 4);
2765
3046
  }
2766
- async listModels() {
3047
+ async listModels(options = {}) {
3048
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "openai");
2767
3049
  try {
2768
3050
  const response = await this.client.models.list();
2769
3051
  return response.data.filter((m) => isChatModel(m.id)).map((m) => {
@@ -2783,7 +3065,7 @@ var init_openai = __esm({
2783
3065
  });
2784
3066
  });
2785
3067
  } catch {
2786
- return Object.values(MODELS).filter((m) => m.provider === "openai");
3068
+ return staticCatalog();
2787
3069
  }
2788
3070
  }
2789
3071
  async isAvailable() {
@@ -3085,9 +3367,46 @@ var init_gemini_schema = __esm({
3085
3367
  var gemini_exports = {};
3086
3368
  __export(gemini_exports, {
3087
3369
  GeminiProvider: () => GeminiProvider,
3370
+ geminiApiRoot: () => geminiApiRoot,
3088
3371
  toGeminiFunctionCalls: () => toGeminiFunctionCalls,
3089
3372
  toGeminiTools: () => toGeminiTools
3090
3373
  });
3374
+ async function* parseSseChunks(body) {
3375
+ const reader = body.getReader();
3376
+ const decoder = new TextDecoder();
3377
+ let buffered = "";
3378
+ try {
3379
+ for (; ; ) {
3380
+ const { done, value } = await reader.read();
3381
+ if (done) break;
3382
+ buffered += decoder.decode(value, { stream: true });
3383
+ let cut = buffered.indexOf("\n");
3384
+ while (cut !== -1) {
3385
+ const line = buffered.slice(0, cut).trim();
3386
+ buffered = buffered.slice(cut + 1);
3387
+ if (line.startsWith("data:")) {
3388
+ const payload = line.slice(5).trim();
3389
+ if (payload && payload !== "[DONE]") {
3390
+ try {
3391
+ yield JSON.parse(payload);
3392
+ } catch {
3393
+ }
3394
+ }
3395
+ }
3396
+ cut = buffered.indexOf("\n");
3397
+ }
3398
+ }
3399
+ } finally {
3400
+ reader.releaseLock();
3401
+ await body.cancel().catch(() => {
3402
+ });
3403
+ }
3404
+ }
3405
+ function geminiApiRoot(baseUrl) {
3406
+ const configured = baseUrl?.trim();
3407
+ if (!configured) return GEMINI_PUBLIC_ROOT;
3408
+ return clientApiRoot("gemini", configured) || GEMINI_PUBLIC_ROOT;
3409
+ }
3091
3410
  function toGeminiTools(tools) {
3092
3411
  return tools.map((tool) => ({
3093
3412
  name: tool.name,
@@ -3108,7 +3427,19 @@ function toGeminiFunctionCalls(toolCalls) {
3108
3427
  }
3109
3428
  }));
3110
3429
  }
3111
- var GeminiProvider;
3430
+ function isGeminiAuthStatus(status) {
3431
+ return status === 400 || status === 401 || status === 403;
3432
+ }
3433
+ async function describeGeminiError(resp) {
3434
+ try {
3435
+ const body = await resp.json();
3436
+ const detail = body?.error?.message ?? body?.error?.status;
3437
+ return detail ? ` \u2014 ${detail}` : "";
3438
+ } catch {
3439
+ return "";
3440
+ }
3441
+ }
3442
+ var GEMINI_API_VERSION, GEMINI_PUBLIC_ROOT, SAFETY_SETTINGS, GeminiProvider;
3112
3443
  var init_gemini = __esm({
3113
3444
  "src/providers/gemini.ts"() {
3114
3445
  init_constants();
@@ -3116,11 +3447,21 @@ var init_gemini = __esm({
3116
3447
  init_pricing();
3117
3448
  init_model_filter();
3118
3449
  init_gemini_schema();
3450
+ init_net();
3451
+ GEMINI_API_VERSION = "v1beta";
3452
+ GEMINI_PUBLIC_ROOT = "https://generativelanguage.googleapis.com";
3453
+ SAFETY_SETTINGS = [
3454
+ { category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
3455
+ { category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
3456
+ ];
3119
3457
  GeminiProvider = class extends BaseProvider {
3120
3458
  client;
3121
3459
  constructor(config, model) {
3122
3460
  super(config, model);
3123
- this.client = new genai.GoogleGenAI({ apiKey: config.apiKey ?? "" });
3461
+ this.client = new genai.GoogleGenAI({
3462
+ apiKey: config.apiKey ?? "",
3463
+ ...config.baseUrl ? { httpOptions: { baseUrl: geminiApiRoot(config.baseUrl) } } : {}
3464
+ });
3124
3465
  }
3125
3466
  async generate(options) {
3126
3467
  const chunks = [];
@@ -3128,15 +3469,12 @@ var init_gemini = __esm({
3128
3469
  }
3129
3470
  async generateStream(options, onChunk) {
3130
3471
  const contents = this.buildContents(options.messages, options.images);
3131
- const stream = await this.client.models.generateContentStream({
3472
+ const stream = this.config.baseUrl ? await this.streamOverRest(contents, options) : await this.client.models.generateContentStream({
3132
3473
  model: this.model.id,
3133
3474
  contents,
3134
3475
  config: {
3135
3476
  systemInstruction: options.systemPrompt,
3136
- safetySettings: [
3137
- { category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
3138
- { category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
3139
- ],
3477
+ safetySettings: SAFETY_SETTINGS,
3140
3478
  tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
3141
3479
  abortSignal: options.signal
3142
3480
  }
@@ -3187,27 +3525,40 @@ var init_gemini = __esm({
3187
3525
  };
3188
3526
  }
3189
3527
  async countTokens(text) {
3528
+ const contents = [{ role: "user", parts: [{ text }] }];
3190
3529
  try {
3191
- const result = await this.client.models.countTokens({
3192
- model: this.model.id,
3193
- contents: [{ role: "user", parts: [{ text }] }]
3194
- });
3530
+ if (this.config.baseUrl) {
3531
+ const resp = await fetchSameOrigin(
3532
+ `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:countTokens`,
3533
+ {
3534
+ method: "POST",
3535
+ headers: {
3536
+ "x-goog-api-key": this.config.apiKey ?? "",
3537
+ "content-type": "application/json"
3538
+ },
3539
+ body: JSON.stringify({ contents })
3540
+ }
3541
+ );
3542
+ if (!resp.ok) return Math.ceil(text.length / 4);
3543
+ const data = await resp.json();
3544
+ return data.totalTokens ?? 0;
3545
+ }
3546
+ const result = await this.client.models.countTokens({ model: this.model.id, contents });
3195
3547
  return result.totalTokens ?? 0;
3196
3548
  } catch {
3197
3549
  return Math.ceil(text.length / 4);
3198
3550
  }
3199
3551
  }
3200
- async listModels() {
3552
+ async listModels(options = {}) {
3553
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "gemini");
3201
3554
  try {
3202
- const resp = await fetch(
3203
- `https://generativelanguage.googleapis.com/v1beta/models?key=${this.config.apiKey}`
3204
- );
3555
+ const resp = await this.fetchModelList();
3205
3556
  if (!resp.ok) {
3206
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3557
+ return staticCatalog();
3207
3558
  }
3208
3559
  const data = await resp.json();
3209
3560
  if (!Array.isArray(data?.models)) {
3210
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3561
+ return staticCatalog();
3211
3562
  }
3212
3563
  return data.models.filter((m) => isChatModel(m.name.replace("models/", ""), m.supportedGenerationMethods)).map((m) => {
3213
3564
  const id = m.name.replace("models/", "");
@@ -3229,21 +3580,99 @@ var init_gemini = __esm({
3229
3580
  });
3230
3581
  });
3231
3582
  } catch {
3232
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3583
+ return staticCatalog();
3233
3584
  }
3234
3585
  }
3586
+ /**
3587
+ * Is this KEY usable — not "does one particular model answer".
3588
+ *
3589
+ * This used to call countTokens() against `this.model.id`, which the router
3590
+ * fills from the first Gemini entry in the bundled catalogue. That made the
3591
+ * whole provider's fate depend on one hard-coded model id: a key that cannot
3592
+ * reach that one model — retired, not enabled for the key's project, served
3593
+ * on a different API version — failed the probe, and because every later step
3594
+ * is gated on the result (`validateCloudProviderModels` and
3595
+ * `discoverProviderModels` both return early for an unavailable provider) the
3596
+ * real model list was never fetched. Gemini vanished entirely, and the CLI
3597
+ * reported "add a provider API key first" about a key that was present and
3598
+ * working.
3599
+ *
3600
+ * Listing models asks the account-level question instead, the same one
3601
+ * OpenAI's probe asks, and it is the same request `listModels()` already
3602
+ * makes for discovery.
3603
+ *
3604
+ * Throws rather than returning false. The router logs the reason it catches,
3605
+ * and "bad key, wrong endpoint/deployment, or unreachable" — three guesses,
3606
+ * no answer — is what made this take a user report to find. A swallowed probe
3607
+ * error is the same defect that once hid a misconfigured Azure deployment.
3608
+ */
3235
3609
  async isAvailable() {
3610
+ if (!this.config.apiKey) throw new Error("no Gemini API key configured");
3611
+ let resp;
3236
3612
  try {
3237
- await this.client.models.countTokens({
3238
- model: this.model.id,
3239
- contents: [{ role: "user", parts: [{ text: "ping" }] }]
3240
- });
3241
- return true;
3242
- } catch {
3243
- return false;
3613
+ resp = await this.fetchModelList();
3614
+ } catch (err) {
3615
+ throw new ProviderUnreachableError(
3616
+ `could not reach the Gemini API: ${err instanceof Error ? err.message : String(err)}`
3617
+ );
3618
+ }
3619
+ if (resp.ok) return true;
3620
+ const status = `HTTP ${resp.status} ${resp.statusText}`.trim();
3621
+ const detail = await describeGeminiError(resp);
3622
+ if (isGeminiAuthStatus(resp.status)) {
3623
+ throw new Error(`Gemini rejected the API key: ${status}${detail}`);
3244
3624
  }
3625
+ throw new ProviderUnreachableError(`Gemini model list failed: ${status}${detail}`);
3245
3626
  }
3246
3627
  // ── Private ──────────────────────────────────
3628
+ /**
3629
+ * The account's model list. One place so the availability probe and
3630
+ * discovery cannot ask different questions of the same endpoint.
3631
+ *
3632
+ * The key goes in a header, not the query string: a URL carries into proxy
3633
+ * logs, error reports and shell history, and this one would carry the key
3634
+ * with it.
3635
+ */
3636
+ /**
3637
+ * Generation against a configured endpoint, issued by hand.
3638
+ *
3639
+ * Same wire protocol the SDK speaks — `:streamGenerateContent?alt=sse` on
3640
+ * `/v1beta`, with the same body — so the chunk shapes the caller walks are
3641
+ * identical and neither transport needs its own parsing. The difference that
3642
+ * matters is `fetchSameOrigin`: the key is never replayed on a host the
3643
+ * configured origin redirects to.
3644
+ */
3645
+ async streamOverRest(contents, options) {
3646
+ const url = `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:streamGenerateContent?alt=sse`;
3647
+ const resp = await fetchSameOrigin(url, {
3648
+ method: "POST",
3649
+ headers: {
3650
+ "x-goog-api-key": this.config.apiKey ?? "",
3651
+ "content-type": "application/json"
3652
+ },
3653
+ body: JSON.stringify({
3654
+ contents,
3655
+ // REST wants the object form; the SDK accepts a bare string and wraps
3656
+ // it itself.
3657
+ ...options.systemPrompt ? { systemInstruction: { parts: [{ text: options.systemPrompt }] } } : {},
3658
+ safetySettings: SAFETY_SETTINGS,
3659
+ ...options.tools?.length ? { tools: [{ functionDeclarations: toGeminiTools(options.tools) }] } : {}
3660
+ }),
3661
+ signal: options.signal
3662
+ });
3663
+ if (!resp.ok || !resp.body) {
3664
+ const detail = await resp.text().catch(() => "");
3665
+ throw new ProviderUnreachableError(
3666
+ `Gemini generation failed at ${geminiApiRoot(this.config.baseUrl)}: ${resp.status}${detail ? ` \u2014 ${detail.slice(0, 200)}` : ""}`
3667
+ );
3668
+ }
3669
+ return parseSseChunks(resp.body);
3670
+ }
3671
+ fetchModelList() {
3672
+ return fetchSameOrigin(`${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models`, {
3673
+ headers: { "x-goog-api-key": this.config.apiKey ?? "" }
3674
+ });
3675
+ }
3247
3676
  buildContents(messages, extraImages) {
3248
3677
  const contents = [];
3249
3678
  for (const m of messages) {
@@ -3328,95 +3757,17 @@ var init_gemini = __esm({
3328
3757
  };
3329
3758
  }
3330
3759
  });
3331
- function preferIpv4Host(url) {
3332
- return url;
3333
- }
3334
- async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3335
- const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3336
- const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3337
- const lib = u.protocol === "https:" ? https__default.default : http__default.default;
3338
- const method = (init.method ?? "GET").toUpperCase();
3339
- const headers = {};
3340
- const h = init.headers;
3341
- if (h instanceof Headers) h.forEach((v, k) => {
3342
- headers[k] = v;
3343
- });
3344
- else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
3345
- else if (h) Object.assign(headers, h);
3346
- if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
3347
- headers["accept-encoding"] = "gzip, deflate, br";
3348
- }
3349
- const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3350
- return new Promise((resolve2, reject) => {
3351
- const req = lib.request(
3352
- {
3353
- hostname: u.hostname,
3354
- port: u.port || (u.protocol === "https:" ? 443 : 80),
3355
- path: u.pathname + u.search,
3356
- method,
3357
- headers
3358
- },
3359
- (res) => {
3360
- const status = res.statusCode ?? 200;
3361
- const location = res.headers.location;
3362
- if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3363
- res.resume();
3364
- const nextUrl = new URL(location, u).href;
3365
- if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3366
- reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3367
- return;
3368
- }
3369
- const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3370
- const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3371
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3372
- return;
3373
- }
3374
- const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
3375
- let bodyStream = res;
3376
- if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib__default.default.createGunzip());
3377
- else if (encoding === "deflate") bodyStream = res.pipe(zlib__default.default.createInflate());
3378
- else if (encoding === "br") bodyStream = res.pipe(zlib__default.default.createBrotliDecompress());
3379
- const stream$1 = stream.Readable.toWeb(bodyStream);
3380
- const respHeaders = new Headers();
3381
- for (const [k, v] of Object.entries(res.headers)) {
3382
- if (k === "content-encoding" || k === "content-length") continue;
3383
- if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3384
- else if (typeof v === "string") respHeaders.set(k, v);
3385
- }
3386
- resolve2(new Response(stream$1, {
3387
- status,
3388
- statusText: res.statusMessage ?? "",
3389
- headers: respHeaders
3390
- }));
3391
- }
3392
- );
3393
- req.on("error", reject);
3394
- req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
3395
- if (body !== void 0) req.write(body);
3396
- req.end();
3397
- });
3398
- }
3399
- var MAX_REDIRECTS;
3400
- var init_net = __esm({
3401
- "src/utils/net.ts"() {
3402
- try {
3403
- dns__default.default.setDefaultResultOrder("ipv4first");
3404
- } catch {
3405
- }
3406
- MAX_REDIRECTS = 5;
3407
- }
3408
- });
3409
-
3410
- // src/providers/ollama.ts
3411
- var ollama_exports = {};
3412
- __export(ollama_exports, {
3413
- OllamaProvider: () => OllamaProvider,
3414
- toOllamaToolCalls: () => toOllamaToolCalls,
3415
- toOllamaTools: () => toOllamaTools
3416
- });
3417
- function isToolCapable(modelName) {
3418
- const name = modelName.toLowerCase();
3419
- return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
3760
+
3761
+ // src/providers/ollama.ts
3762
+ var ollama_exports = {};
3763
+ __export(ollama_exports, {
3764
+ OllamaProvider: () => OllamaProvider,
3765
+ toOllamaToolCalls: () => toOllamaToolCalls,
3766
+ toOllamaTools: () => toOllamaTools
3767
+ });
3768
+ function isToolCapable(modelName) {
3769
+ const name = modelName.toLowerCase();
3770
+ return TOOL_CAPABLE_FAMILIES.some((family) => name.includes(family));
3420
3771
  }
3421
3772
  function toOllamaTools(tools) {
3422
3773
  return tools.map((t) => ({
@@ -3694,12 +4045,38 @@ var init_openai_compatible = __esm({
3694
4045
  OpenAICompatibleProvider = class extends OpenAIProvider {
3695
4046
  constructor(config, model) {
3696
4047
  super({ ...config, apiKey: config.apiKey ?? "not-required" }, model);
4048
+ if (!config.baseUrl?.trim()) {
4049
+ throw new Error(
4050
+ "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."
4051
+ );
4052
+ }
3697
4053
  this.client = new OpenAI__default.default({
3698
4054
  apiKey: config.apiKey ?? "not-required",
3699
4055
  baseURL: preferIpv4Host(config.baseUrl),
3700
- fetch: nodeHttpFetch
4056
+ // Bound to the configured origin, NOT bare `nodeHttpFetch`. That helper
4057
+ // follows a 3xx by reusing `init` verbatim — headers included — so an
4058
+ // endpoint that redirected to another host was handed
4059
+ // `Authorization: Bearer <apiKey>` on the second one. This is the same
4060
+ // leak the Anthropic paths were fixed for; `allowedRedirectOrigin` exists
4061
+ // precisely for callers that attach a credential, and this one attaches
4062
+ // it on every request.
4063
+ fetch: ((input, init) => nodeHttpFetch(input, init ?? {}, 0, this.redirectPolicy()))
3701
4064
  });
3702
4065
  }
4066
+ /**
4067
+ * Redirects may only stay on the host the credential was configured for.
4068
+ *
4069
+ * An unparseable `baseUrl` yields no policy, which is the pre-existing
4070
+ * behaviour — but such a config cannot address anything anyway, so the
4071
+ * request fails before a redirect is possible.
4072
+ */
4073
+ redirectPolicy() {
4074
+ try {
4075
+ return { allowedRedirectOrigin: new URL(preferIpv4Host(this.config.baseUrl) ?? "").origin };
4076
+ } catch {
4077
+ return {};
4078
+ }
4079
+ }
3703
4080
  modelsUrl() {
3704
4081
  const base = (preferIpv4Host(this.config.baseUrl) ?? "").replace(/\/+$/, "");
3705
4082
  return base + "/models";
@@ -3710,7 +4087,7 @@ var init_openai_compatible = __esm({
3710
4087
  return h;
3711
4088
  }
3712
4089
  async listModels() {
3713
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4090
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3714
4091
  if (!res.ok) throw new Error(`models endpoint ${this.modelsUrl()} returned HTTP ${res.status}`);
3715
4092
  const body = await res.json();
3716
4093
  const raw = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
@@ -3740,7 +4117,7 @@ var init_openai_compatible = __esm({
3740
4117
  }
3741
4118
  async isAvailable() {
3742
4119
  try {
3743
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4120
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3744
4121
  return res.ok;
3745
4122
  } catch {
3746
4123
  return false;
@@ -3762,12 +4139,12 @@ var init_audit_logger = __esm({
3762
4139
  constructor(workspacePath, debugMode = false) {
3763
4140
  this.workspacePath = workspacePath;
3764
4141
  this.debugMode = debugMode;
3765
- const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
3766
- if (!fs31__default.default.existsSync(cascadeDir)) {
3767
- fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
4142
+ const cascadeDir = path35__default.default.join(workspacePath, ".cascade");
4143
+ if (!fs32__default.default.existsSync(cascadeDir)) {
4144
+ fs32__default.default.mkdirSync(cascadeDir, { recursive: true });
3768
4145
  }
3769
- this.keyPath = path34__default.default.join(cascadeDir, "audit_log.key");
3770
- this.dbPath = path34__default.default.join(cascadeDir, "audit_log.db");
4146
+ this.keyPath = path35__default.default.join(cascadeDir, "audit_log.key");
4147
+ this.dbPath = path35__default.default.join(cascadeDir, "audit_log.db");
3771
4148
  this.initEncryptionKey();
3772
4149
  this.db = new Database2__default.default(this.dbPath);
3773
4150
  this.db.pragma("journal_mode = WAL");
@@ -3797,11 +4174,11 @@ var init_audit_logger = __esm({
3797
4174
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3798
4175
  }
3799
4176
  initEncryptionKey() {
3800
- if (fs31__default.default.existsSync(this.keyPath)) {
3801
- this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
4177
+ if (fs32__default.default.existsSync(this.keyPath)) {
4178
+ this.encryptionKey = fs32__default.default.readFileSync(this.keyPath);
3802
4179
  } else {
3803
4180
  this.encryptionKey = crypto__default.default.randomBytes(32);
3804
- fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
4181
+ fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3805
4182
  }
3806
4183
  }
3807
4184
  encrypt(text) {
@@ -3879,9 +4256,9 @@ var init_audit_logger = __esm({
3879
4256
  dumpDebugIfNeeded() {
3880
4257
  if (!this.debugMode) return;
3881
4258
  try {
3882
- const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
4259
+ const dumpPath = path35__default.default.join(os10__default.default.tmpdir(), "cascade_audit_logs_debug.json");
3883
4260
  const entries = this.getAllLogs();
3884
- fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4261
+ fs32__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
3885
4262
  } catch (err) {
3886
4263
  console.error("Failed to dump debug audit logs", err);
3887
4264
  }
@@ -3936,7 +4313,7 @@ var Keystore = class {
3936
4313
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
3937
4314
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
3938
4315
  this.backend = "keytar";
3939
- if (password && fs31__default.default.existsSync(this.storePath)) {
4316
+ if (password && fs32__default.default.existsSync(this.storePath)) {
3940
4317
  try {
3941
4318
  const fileEntries = this.decryptFile(password);
3942
4319
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -3955,7 +4332,7 @@ var Keystore = class {
3955
4332
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
3956
4333
  );
3957
4334
  }
3958
- if (!fs31__default.default.existsSync(this.storePath)) {
4335
+ if (!fs32__default.default.existsSync(this.storePath)) {
3959
4336
  const salt = crypto__default.default.randomBytes(SALT_LEN);
3960
4337
  this.masterKey = this.deriveKey(password, salt);
3961
4338
  this.writeWithSalt({}, salt);
@@ -3969,7 +4346,7 @@ var Keystore = class {
3969
4346
  }
3970
4347
  /** Synchronous legacy unlock kept for AES-only environments. */
3971
4348
  unlockSync(password) {
3972
- if (!fs31__default.default.existsSync(this.storePath)) {
4349
+ if (!fs32__default.default.existsSync(this.storePath)) {
3973
4350
  const salt = crypto__default.default.randomBytes(SALT_LEN);
3974
4351
  this.masterKey = this.deriveKey(password, salt);
3975
4352
  this.writeWithSalt({}, salt);
@@ -4027,7 +4404,7 @@ var Keystore = class {
4027
4404
  }
4028
4405
  }
4029
4406
  decryptFile(password, knownSalt) {
4030
- if (!fs31__default.default.existsSync(this.storePath)) return {};
4407
+ if (!fs32__default.default.existsSync(this.storePath)) return {};
4031
4408
  try {
4032
4409
  const { salt, ciphertext, iv, tag } = this.readRaw();
4033
4410
  const useSalt = knownSalt ?? salt;
@@ -4049,8 +4426,8 @@ var Keystore = class {
4049
4426
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4050
4427
  const tag = cipher.getAuthTag();
4051
4428
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4052
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4053
- fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4429
+ fs32__default.default.mkdirSync(path35__default.default.dirname(this.storePath), { recursive: true });
4430
+ fs32__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4054
4431
  }
4055
4432
  writeWithSalt(data, salt) {
4056
4433
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4060,11 +4437,11 @@ var Keystore = class {
4060
4437
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4061
4438
  const tag = cipher.getAuthTag();
4062
4439
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4063
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4064
- fs31__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4440
+ fs32__default.default.mkdirSync(path35__default.default.dirname(this.storePath), { recursive: true });
4441
+ fs32__default.default.writeFileSync(this.storePath, out, { mode: 384 });
4065
4442
  }
4066
4443
  readRaw() {
4067
- const buf = fs31__default.default.readFileSync(this.storePath);
4444
+ const buf = fs32__default.default.readFileSync(this.storePath);
4068
4445
  let offset = 0;
4069
4446
  const salt = buf.subarray(offset, offset + SALT_LEN);
4070
4447
  offset += SALT_LEN;
@@ -4097,7 +4474,7 @@ var CascadeIgnore = class {
4097
4474
  ]);
4098
4475
  }
4099
4476
  async load(workspacePath) {
4100
- const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4477
+ const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
4101
4478
  try {
4102
4479
  const content = await fs11__default.default.readFile(filePath, "utf-8");
4103
4480
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4108,7 +4485,7 @@ var CascadeIgnore = class {
4108
4485
  }
4109
4486
  isIgnored(filePath, workspacePath) {
4110
4487
  try {
4111
- const relative = workspacePath ? path34__default.default.relative(workspacePath, filePath) : filePath;
4488
+ const relative = workspacePath ? path35__default.default.relative(workspacePath, filePath) : filePath;
4112
4489
  return this.ig.ignores(relative);
4113
4490
  } catch {
4114
4491
  return false;
@@ -4119,7 +4496,7 @@ var CascadeIgnore = class {
4119
4496
  }
4120
4497
  };
4121
4498
  async function createDefaultIgnoreFile(workspacePath) {
4122
- const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4499
+ const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
4123
4500
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4124
4501
  # Syntax identical to .gitignore
4125
4502
 
@@ -4149,7 +4526,7 @@ Thumbs.db
4149
4526
  await fs11__default.default.writeFile(filePath, content, "utf-8");
4150
4527
  }
4151
4528
  async function loadCascadeMd(workspacePath) {
4152
- const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4529
+ const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
4153
4530
  try {
4154
4531
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
4155
4532
  return parseCascadeMd(raw);
@@ -4178,7 +4555,7 @@ ${raw.trim()}`;
4178
4555
  return { raw, sections, systemPrompt };
4179
4556
  }
4180
4557
  async function createDefaultCascadeMd(workspacePath) {
4181
- const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4558
+ const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
4182
4559
  const content = `# Cascade Project Instructions
4183
4560
 
4184
4561
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4213,7 +4590,7 @@ List any areas the agent should not touch.
4213
4590
  var MemoryStore = class _MemoryStore {
4214
4591
  db;
4215
4592
  constructor(dbPath) {
4216
- fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
4593
+ fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
4217
4594
  try {
4218
4595
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
4219
4596
  this.db.pragma("journal_mode = WAL");
@@ -5477,21 +5854,80 @@ function validateConfig(raw) {
5477
5854
 
5478
5855
  // src/config/global-credentials.ts
5479
5856
  init_constants();
5857
+
5858
+ // src/config/azure-endpoint.ts
5859
+ init_net();
5860
+ function normalizeAzureEndpoint(url) {
5861
+ return normalizeEndpoint(url);
5862
+ }
5863
+ function sameAzureEndpoint(a, b) {
5864
+ return normalizeAzureEndpoint(a) === normalizeAzureEndpoint(b);
5865
+ }
5866
+
5867
+ // src/config/endpoint-identity.ts
5868
+ init_net();
5869
+ var DEFAULT_ENDPOINT = {
5870
+ anthropic: "https://api.anthropic.com",
5871
+ openai: "https://api.openai.com/v1",
5872
+ gemini: "https://generativelanguage.googleapis.com"
5873
+ };
5874
+ function hasDefaultEndpoint(type) {
5875
+ return Object.hasOwn(DEFAULT_ENDPOINT, type);
5876
+ }
5877
+ function credentialEndpointIdentity(type, baseUrl) {
5878
+ const configured = baseUrl?.trim();
5879
+ const effective = configured || DEFAULT_ENDPOINT[type];
5880
+ if (!effective) return null;
5881
+ return normalizeEndpoint(clientApiRoot(type, effective));
5882
+ }
5883
+ function sameCredentialEndpoint(type, a, b) {
5884
+ const left = credentialEndpointIdentity(type, a);
5885
+ const right = credentialEndpointIdentity(type, b);
5886
+ if (left === null && right === null) return true;
5887
+ if (left === null || right === null) return false;
5888
+ return left === right;
5889
+ }
5890
+ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
5891
+ const row = rowEndpoint?.trim() ? credentialEndpointIdentity(type, rowEndpoint) : null;
5892
+ const credential = credentialEndpointIdentity(type, credentialEndpoint);
5893
+ if (row === null) return false;
5894
+ if (credential === null) return true;
5895
+ return row !== credential;
5896
+ }
5897
+
5898
+ // src/config/global-credentials.ts
5480
5899
  function credentialsPath(globalDir2) {
5481
- return path34__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5900
+ return path35__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5482
5901
  }
5483
5902
  function providerKey(p) {
5484
- if (p.type === "azure") {
5485
- return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
5486
- }
5487
5903
  return p.type;
5488
5904
  }
5905
+ function azureEndpoint(p) {
5906
+ return p.baseUrl === void 0 ? void 0 : normalizeAzureEndpoint(p.baseUrl);
5907
+ }
5908
+ function sameAzureEntry(a, b) {
5909
+ const strong = [
5910
+ [a.deploymentName, b.deploymentName],
5911
+ [azureEndpoint(a), azureEndpoint(b)]
5912
+ ];
5913
+ let shared = 0;
5914
+ for (const [x, y] of strong) {
5915
+ if (!x || !y) continue;
5916
+ if (x !== y) return false;
5917
+ shared++;
5918
+ }
5919
+ if (shared > 0) return true;
5920
+ const aNamesSomething = Boolean(a.deploymentName || azureEndpoint(a));
5921
+ const bNamesSomething = Boolean(b.deploymentName || azureEndpoint(b));
5922
+ if (aNamesSomething || bNamesSomething) return false;
5923
+ return Boolean(a.label && b.label && a.label === b.label);
5924
+ }
5489
5925
  function isPersistable(p) {
5490
5926
  return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
5491
5927
  }
5492
5928
  function loadGlobalCredentials(globalDir2) {
5493
5929
  try {
5494
- const raw = fs31__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5930
+ const raw = fs32__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5495
5931
  const parsed = JSON.parse(raw);
5496
5932
  if (!Array.isArray(parsed.providers)) return [];
5497
5933
  return parsed.providers.filter(
@@ -5503,34 +5939,93 @@ function loadGlobalCredentials(globalDir2) {
5503
5939
  }
5504
5940
  function saveGlobalCredentials(globalDir2, providers) {
5505
5941
  const filePath = credentialsPath(globalDir2);
5506
- fs31__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5942
+ fs32__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5507
5943
  const body = { version: 1, providers: providers.filter(isPersistable) };
5508
5944
  const tmp = `${filePath}.tmp`;
5509
- fs31__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5510
- fs31__default.default.renameSync(tmp, filePath);
5945
+ fs32__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5946
+ fs32__default.default.renameSync(tmp, filePath);
5511
5947
  try {
5512
- fs31__default.default.chmodSync(filePath, 384);
5948
+ fs32__default.default.chmodSync(filePath, 384);
5513
5949
  } catch {
5514
5950
  }
5515
5951
  }
5516
5952
  function mergeGlobalCredentials(workspaceProviders, globalProviders) {
5517
5953
  const merged = [...workspaceProviders];
5518
- const byKey = new Map(merged.map((p) => [providerKey(p), p]));
5954
+ const byKey = new Map(merged.filter((p) => p.type !== "azure").map((p) => [providerKey(p), p]));
5519
5955
  for (const g of globalProviders) {
5520
- const existing = byKey.get(providerKey(g));
5956
+ if (g.type === "azure" && !g.deploymentName?.trim() && azureEndpoint(g)) {
5957
+ const onResource = merged.filter((p) => p.type === "azure" && sameAzureEntry(p, g));
5958
+ if (onResource.length > 0) {
5959
+ for (const row of onResource) fillFrom(row, g);
5960
+ continue;
5961
+ }
5962
+ }
5963
+ const existing = g.type === "azure" ? merged.find((p) => p.type === "azure" && sameAzureEntry(p, g)) : byKey.get(providerKey(g));
5521
5964
  if (!existing) {
5522
5965
  merged.push({ ...g });
5523
- byKey.set(providerKey(g), merged[merged.length - 1]);
5966
+ if (g.type !== "azure") byKey.set(providerKey(g), merged[merged.length - 1]);
5524
5967
  continue;
5525
5968
  }
5526
- if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
5527
- if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
5528
- if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5529
- if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5530
- if (!existing.label && g.label) existing.label = g.label;
5969
+ fillFrom(existing, g);
5531
5970
  }
5532
5971
  return merged;
5533
5972
  }
5973
+ function fillFrom(existing, g) {
5974
+ const credentialled = Boolean(existing.apiKey || existing.authToken);
5975
+ const endpointsDiffer = credentialEndpointsConflict(existing.type, existing.baseUrl, g.baseUrl);
5976
+ if (!credentialled && !endpointsDiffer && g.apiKey) {
5977
+ existing.apiKey = g.apiKey;
5978
+ if (g.baseUrl) existing.baseUrl = g.baseUrl;
5979
+ }
5980
+ if (!credentialled && !endpointsDiffer && !existing.apiKey && g.authToken && g.baseUrl) {
5981
+ existing.authToken = g.authToken;
5982
+ existing.baseUrl = g.baseUrl;
5983
+ }
5984
+ if (!credentialled && !existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5985
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5986
+ if (!existing.label && g.label) existing.label = g.label;
5987
+ }
5988
+
5989
+ // src/config/azure-routing.ts
5990
+ function routableAzureRows(rows) {
5991
+ return rows.filter((p) => p.type === "azure" && p.baseUrl?.trim());
5992
+ }
5993
+ function resolveAzureRouting(view, write, env) {
5994
+ const rows = routableAzureRows(view);
5995
+ const resourcesOf = (list) => [...new Set(list.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
5996
+ if (rows.length === 0) return { ok: false, reason: "no-routable-rows", resources: [] };
5997
+ const endpoint = env.endpoint?.trim();
5998
+ const deployment = env.deployment?.trim();
5999
+ const named = deployment ? rows.filter((p) => (p.deploymentName?.trim() ?? "") === deployment) : [];
6000
+ let anchor;
6001
+ if (endpoint) {
6002
+ anchor = rows.find((p) => sameAzureEndpoint(p.baseUrl, endpoint));
6003
+ if (!anchor) {
6004
+ return { ok: false, reason: "endpoint-not-configured", resources: resourcesOf(rows) };
6005
+ }
6006
+ const elsewhere = named.filter((p) => !sameAzureEndpoint(p.baseUrl, endpoint));
6007
+ if (elsewhere.length > 0) {
6008
+ return { ok: false, reason: "name-on-another-resource", resources: resourcesOf(elsewhere) };
6009
+ }
6010
+ } else if (named.length > 0) {
6011
+ const owners = resourcesOf(named);
6012
+ if (owners.length > 1) return { ok: false, reason: "ambiguous-resource", resources: owners };
6013
+ anchor = named[0];
6014
+ } else {
6015
+ const all = resourcesOf(rows);
6016
+ if (all.length !== 1) return { ok: false, reason: "ambiguous-resource", resources: all };
6017
+ anchor = rows[0];
6018
+ }
6019
+ const resource = anchor.baseUrl;
6020
+ const onResource = routableAzureRows(write).filter((p) => sameAzureEndpoint(p.baseUrl, resource));
6021
+ const alreadyThere = deployment ? onResource.some((p) => (p.deploymentName?.trim() ?? "") === deployment) || named.some((p) => sameAzureEndpoint(p.baseUrl, resource)) : true;
6022
+ return {
6023
+ ok: true,
6024
+ resource,
6025
+ rows: onResource,
6026
+ ...deployment && !alreadyThere ? { createDeployment: deployment } : {}
6027
+ };
6028
+ }
5534
6029
 
5535
6030
  // src/config/retired-providers.ts
5536
6031
  var RETIRED_PROVIDER_TYPES = {
@@ -5540,7 +6035,7 @@ function isRetiredProviderType(type) {
5540
6035
  return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
5541
6036
  }
5542
6037
  function didCleanupChangeAnything(c) {
5543
- return c.removed.length > 0 || c.clearedPins.length > 0;
6038
+ return c.removed.length > 0 || c.clearedPins.length > 0 || (c.revokedCredentials ?? 0) > 0 || (c.unusableCredentials ?? 0) > 0;
5544
6039
  }
5545
6040
  function stripRetiredProviders(raw) {
5546
6041
  const cleanup = { removed: [], clearedPins: [] };
@@ -5587,11 +6082,24 @@ function describeCleanup(c) {
5587
6082
  for (const type of c.removed) {
5588
6083
  parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
5589
6084
  }
6085
+ if ((c.revokedCredentials ?? 0) > 0) {
6086
+ parts.push("discarded a linked Claude subscription token, which Anthropic no longer permits third-party tools to use");
6087
+ }
6088
+ const unusable = c.unusableCredentials ?? 0;
6089
+ if (unusable > 0) {
6090
+ parts.push(
6091
+ `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`
6092
+ );
6093
+ }
5590
6094
  if (c.clearedPins.length > 0) {
5591
6095
  parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
5592
6096
  }
6097
+ if (parts.length === 0) return "";
5593
6098
  return `Cascade config migration: ${parts.join("; ")}.`;
5594
6099
  }
6100
+
6101
+ // src/config/index.ts
6102
+ init_revoked_credentials();
5595
6103
  var MCP_TOOL_PREFIX = "mcp__";
5596
6104
  var MAX_SEGMENT_LENGTH = 24;
5597
6105
  var SEGMENT_HASH_LENGTH = 8;
@@ -5773,10 +6281,31 @@ function removeMcpServerDenials(disabledTools, serverName) {
5773
6281
  // src/config/index.ts
5774
6282
  init_constants();
5775
6283
  var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
6284
+ function hasProviderCredential(p) {
6285
+ if (!p) return false;
6286
+ if (typeof p.apiKey === "string" && p.apiKey.length > 0) return true;
6287
+ return typeof p.authToken === "string" && p.authToken.length > 0 && typeof p.baseUrl === "string" && p.baseUrl.length > 0;
6288
+ }
6289
+ function isRoutable(p) {
6290
+ if (p.type === "openai-compatible") return Boolean(p.baseUrl?.trim());
6291
+ if (p.type === "azure") return Boolean(p.baseUrl?.trim() && p.deploymentName?.trim());
6292
+ return true;
6293
+ }
5776
6294
  function hasUsableProvider(providers) {
5777
6295
  if (!providers?.length) return false;
5778
- return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
6296
+ return providers.some((p) => isRoutable(p) && (KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || hasProviderCredential(p)));
6297
+ }
6298
+ function applyProviderApiKey(providers, type, apiKey, extra = {}) {
6299
+ const existing = providers.find((p) => p.type === type);
6300
+ if (existing) {
6301
+ existing.apiKey = apiKey;
6302
+ existing.authToken = void 0;
6303
+ if (extra.baseUrl) existing.baseUrl = extra.baseUrl;
6304
+ return;
6305
+ }
6306
+ providers.push({ type, apiKey, ...extra.baseUrl ? { baseUrl: extra.baseUrl } : {} });
5779
6307
  }
6308
+ var ENV_ENDPOINT_CHANNEL = /* @__PURE__ */ new Set(["anthropic"]);
5780
6309
  var ConfigManager = class {
5781
6310
  config;
5782
6311
  keystore;
@@ -5796,6 +6325,16 @@ var ConfigManager = class {
5796
6325
  * fallback for a list that is empty for some unrelated reason.
5797
6326
  */
5798
6327
  retiredCleanup;
6328
+ /**
6329
+ * How many dead subscription credentials THIS load removed. Per-load like
6330
+ * `retiredCleanup`, for the same reason: a second load() on the same instance
6331
+ * must not re-announce a migration that already happened.
6332
+ */
6333
+ revokedCredentials = 0;
6334
+ /** Tier pins cleared by the revoked-credential migration this load. */
6335
+ revokedPins = [];
6336
+ /** This load's revoked-credential explanation, joined with any retirement one. */
6337
+ revokedNotice;
5799
6338
  /**
5800
6339
  * Human-readable migration notice held for a UI to display. `console.warn`
5801
6340
  * is not sufficient: the REPL clears the TTY immediately after load(), and
@@ -5811,7 +6350,7 @@ var ConfigManager = class {
5811
6350
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5812
6351
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5813
6352
  this.workspacePath = workspacePath;
5814
- this.globalDir = globalDirOverride ?? path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
6353
+ this.globalDir = globalDirOverride ?? path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR);
5815
6354
  }
5816
6355
  async load() {
5817
6356
  this.config = await this.loadConfig();
@@ -5825,22 +6364,38 @@ var ConfigManager = class {
5825
6364
  this.ignore = new CascadeIgnore();
5826
6365
  await this.ignore.load(this.workspacePath);
5827
6366
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5828
- this.keystore = new Keystore(path34__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5829
- this.store = new MemoryStore(path34__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5830
- const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
5831
- if (globalCreds.removed.length > 0) {
6367
+ this.keystore = new Keystore(path35__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
6368
+ this.store = new MemoryStore(path35__default.default.join(this.workspacePath, CASCADE_DB_FILE));
6369
+ const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
6370
+ const globalCreds = filterRetiredCredentials(globalRevoked.kept);
6371
+ if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
6372
+ if (globalCreds.removed.length > 0 || globalRevoked.removed > 0) {
6373
+ const retiredHere = globalCreds.removed.length > 0;
5832
6374
  try {
5833
6375
  saveGlobalCredentials(this.globalDir, globalCreds.kept);
5834
6376
  } catch (err) {
5835
6377
  console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
5836
6378
  }
5837
- this.retiredCleanup = {
5838
- removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
5839
- clearedPins: this.retiredCleanup?.clearedPins ?? []
5840
- };
6379
+ if (retiredHere) {
6380
+ this.retiredCleanup = {
6381
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
6382
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
6383
+ };
6384
+ }
5841
6385
  }
5842
- await this.injectEnvKeys();
6386
+ await this.injectEnvKeys(globalCreds.kept);
5843
6387
  this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
6388
+ if (this.revokedCredentials > 0 && !hasUsableAnthropic(this.config.providers)) {
6389
+ this.revokedPins = clearAnthropicPins(this.config.models, this.config.providers);
6390
+ if (this.revokedPins.length > 0) {
6391
+ await this.persistClearedPins(this.revokedPins.map((p) => p.tier));
6392
+ }
6393
+ }
6394
+ if (this.revokedCredentials > 0) {
6395
+ const pins = this.revokedPins.length ? ` Cleared the ${this.revokedPins.map((p) => `${p.tier.toUpperCase()} pin (${p.model})`).join(" and ")}, since it named Anthropic.` : "";
6396
+ this.revokedNotice = `Cascade config migration: ${REVOKED_CREDENTIAL_REASON}${pins}`;
6397
+ console.warn(this.revokedNotice);
6398
+ }
5844
6399
  for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
5845
6400
  try {
5846
6401
  this.store.purgeCachedModelsForRetiredProvider(type);
@@ -5849,10 +6404,14 @@ var ConfigManager = class {
5849
6404
  }
5850
6405
  await this.ensureDefaultIdentity();
5851
6406
  if (mcpNamesChanged) await this.save();
6407
+ const notices = [];
5852
6408
  if (this.retiredCleanup) {
5853
- console.warn(describeCleanup(this.retiredCleanup));
5854
- this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
6409
+ const retired = describeCleanup(this.retiredCleanup);
6410
+ console.warn(retired);
6411
+ notices.push(retired);
5855
6412
  }
6413
+ if (this.revokedNotice) notices.push(this.revokedNotice);
6414
+ if (notices.length > 0) this.pendingRetiredNotice = notices.join(" ");
5856
6415
  }
5857
6416
  getConfig() {
5858
6417
  return this.config;
@@ -5872,12 +6431,18 @@ var ConfigManager = class {
5872
6431
  getWorkspacePath() {
5873
6432
  return this.workspacePath;
5874
6433
  }
5875
- async save() {
5876
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5877
- await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
5878
- await fs11__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
6434
+ /**
6435
+ * @param config What to persist. Defaults to the manager's own, but a
6436
+ * transactional save hands in the validated copy it has NOT adopted yet —
6437
+ * writing before adopting is what stops a run picking up configuration that
6438
+ * never reached the disk (see `commitSettings`).
6439
+ */
6440
+ async save(config = this.config) {
6441
+ const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
6442
+ await fs11__default.default.mkdir(path35__default.default.dirname(configPath), { recursive: true });
6443
+ await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
5879
6444
  try {
5880
- saveGlobalCredentials(this.globalDir, this.config.providers);
6445
+ saveGlobalCredentials(this.globalDir, config.providers);
5881
6446
  } catch (err) {
5882
6447
  console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
5883
6448
  }
@@ -5908,19 +6473,35 @@ var ConfigManager = class {
5908
6473
  const configProvider = this.config.providers.find((p) => p.type === provider);
5909
6474
  return configProvider?.apiKey;
5910
6475
  }
6476
+ /**
6477
+ * The bearer token configured for a provider, if any.
6478
+ *
6479
+ * Companion to getApiKey(). A provider can be fully configured with only
6480
+ * this — `cascade link` and ANTHROPIC_AUTH_TOKEN both produce it — so any
6481
+ * surface that asks "is this provider set up" has to consult both, or it
6482
+ * reports a working install as unconfigured.
6483
+ */
6484
+ getAuthToken(provider) {
6485
+ return this.config.providers.find((p) => p.type === provider)?.authToken;
6486
+ }
5911
6487
  async loadConfig() {
5912
6488
  this.retiredCleanup = void 0;
5913
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
6489
+ this.revokedCredentials = 0;
6490
+ this.revokedPins = [];
6491
+ this.revokedNotice = void 0;
6492
+ const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5914
6493
  try {
5915
6494
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
5916
6495
  const parsed = JSON.parse(raw);
5917
6496
  const cleanup = stripRetiredProviders(parsed);
5918
- if (didCleanupChangeAnything(cleanup)) {
5919
- this.retiredCleanup = cleanup;
6497
+ const revokedHere = stripRevokedFromConfig(parsed).removed;
6498
+ if (revokedHere > 0) this.revokedCredentials += revokedHere;
6499
+ if (didCleanupChangeAnything(cleanup) || revokedHere > 0) {
6500
+ if (didCleanupChangeAnything(cleanup)) this.retiredCleanup = cleanup;
5920
6501
  try {
5921
6502
  await fs11__default.default.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
5922
6503
  } catch (err) {
5923
- console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
6504
+ console.warn(`Could not persist the config migration: ${err instanceof Error ? err.message : String(err)}`);
5924
6505
  }
5925
6506
  }
5926
6507
  return validateConfig(parsed);
@@ -5931,9 +6512,102 @@ var ConfigManager = class {
5931
6512
  throw err;
5932
6513
  }
5933
6514
  }
5934
- async injectEnvKeys() {
6515
+ /**
6516
+ * Rewrite the workspace file with those tier pins removed.
6517
+ *
6518
+ * A targeted edit of the RAW file, not save(). By this point `this.config`
6519
+ * carries environment keys and machine-global credentials, and save()
6520
+ * serializes the whole object — so persisting through it would copy secrets
6521
+ * kept 0600 in ~/.cascade-ai into a workspace file that may be 0644. Same
6522
+ * reasoning as the migration write in loadConfig(); best-effort for the same
6523
+ * reason, since a read-only config directory must not abort startup.
6524
+ */
6525
+ /**
6526
+ * The Azure entries `AZURE_OPENAI_KEY` belongs to — possibly several.
6527
+ *
6528
+ * `AZURE_OPENAI_ENDPOINT` names the resource. Without one, a single
6529
+ * configured resource is unambiguous and anything more is a guess — and
6530
+ * guessing wrong writes a key to a resource that will reject it.
6531
+ *
6532
+ * ALL the deployments on that resource, not the first. An Azure key is
6533
+ * resource-scoped, so every deployment there shares it, and Azure is
6534
+ * configured one entry per deployment — the router binds each model to its
6535
+ * own row, matching `deploymentName` against the model id, so filling only
6536
+ * the first left the rest issuing requests with no key at all.
6537
+ */
6538
+ azureEntriesForEnv(globalProviders = []) {
6539
+ const workspace = this.config.providers;
6540
+ const routing = resolveAzureRouting(
6541
+ [...workspace, ...globalProviders],
6542
+ workspace,
6543
+ {
6544
+ endpoint: process.env["AZURE_OPENAI_ENDPOINT"],
6545
+ deployment: process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"]
6546
+ }
6547
+ );
6548
+ if (!routing.ok) return [];
6549
+ const onResource = [...routing.rows];
6550
+ const claimEndpointlessRow = (name) => {
6551
+ const row = this.config.providers.find((p) => p.type === "azure" && !p.baseUrl?.trim() && (p.deploymentName?.trim() ?? "") === name);
6552
+ if (!row) return void 0;
6553
+ if (row.apiKey || row.authToken) {
6554
+ console.warn(
6555
+ `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.`
6556
+ );
6557
+ row.apiKey = void 0;
6558
+ row.authToken = void 0;
6559
+ }
6560
+ row.baseUrl = routing.resource;
6561
+ onResource.push(row);
6562
+ return row;
6563
+ };
6564
+ for (const g of globalProviders) {
6565
+ if (g.type !== "azure" || !g.baseUrl?.trim()) continue;
6566
+ if (!sameAzureEndpoint(g.baseUrl, routing.resource)) continue;
6567
+ const name = g.deploymentName?.trim() ?? "";
6568
+ if (onResource.some((p) => (p.deploymentName?.trim() ?? "") === name)) continue;
6569
+ if (claimEndpointlessRow(name)) continue;
6570
+ const row = { ...g, apiKey: void 0, authToken: void 0 };
6571
+ this.config.providers.push(row);
6572
+ onResource.push(row);
6573
+ }
6574
+ if (routing.createDeployment && !claimEndpointlessRow(routing.createDeployment)) {
6575
+ const row = {
6576
+ type: "azure",
6577
+ deploymentName: routing.createDeployment,
6578
+ baseUrl: routing.resource
6579
+ };
6580
+ this.config.providers.push(row);
6581
+ onResource.push(row);
6582
+ }
6583
+ return onResource;
6584
+ }
6585
+ async persistClearedPins(tiers) {
6586
+ const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
6587
+ try {
6588
+ const raw = JSON.parse(await fs11__default.default.readFile(configPath, "utf-8"));
6589
+ const models = raw["models"];
6590
+ if (typeof models !== "object" || models === null) return;
6591
+ for (const tier of tiers) delete models[tier];
6592
+ await fs11__default.default.writeFile(configPath, JSON.stringify(raw, null, 2), "utf-8");
6593
+ } catch (err) {
6594
+ if (err.code === "ENOENT") return;
6595
+ console.warn(`Could not persist the cleared model pin: ${err instanceof Error ? err.message : String(err)}`);
6596
+ }
6597
+ }
6598
+ /**
6599
+ * `globalProviders` is the machine-global credential store, passed in
6600
+ * READ-ONLY. This runs before mergeGlobalCredentials() on purpose — an
6601
+ * exported key should outrank a stored one, which only holds while the
6602
+ * injection goes first — but a few of the questions asked here are about the
6603
+ * user's whole configuration rather than this workspace's file, and answering
6604
+ * those from the workspace alone is what produced the gateway bug this
6605
+ * argument fixes.
6606
+ */
6607
+ async injectEnvKeys(globalProviders = []) {
5935
6608
  const wasEmpty = this.config.providers.length === 0;
5936
- const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
6609
+ const emptiedByRetirement = wasEmpty && (!!this.retiredCleanup || this.revokedCredentials > 0);
6610
+ const mayCreate = (type) => wasEmpty || type === "anthropic" && this.revokedCredentials > 0;
5937
6611
  const envProviders = [
5938
6612
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5939
6613
  { env: "OPENAI_API_KEY", type: "openai" },
@@ -5943,11 +6617,66 @@ var ConfigManager = class {
5943
6617
  for (const { env, type } of envProviders) {
5944
6618
  const key = process.env[env];
5945
6619
  if (!key) continue;
5946
- const existing = this.config.providers.find((p) => p.type === type);
5947
- if (!existing && wasEmpty) {
5948
- this.config.providers.push({ type, apiKey: key });
5949
- } else if (existing && !existing.apiKey) {
5950
- existing.apiKey = key;
6620
+ if (isSubscriptionToken(key)) continue;
6621
+ const targets = type === "azure" ? this.azureEntriesForEnv(globalProviders) : [this.config.providers.find((p) => p.type === type)].filter((p) => !!p);
6622
+ const existing = targets[0];
6623
+ const globalMatch = type === "azure" ? void 0 : globalProviders.find((p) => p.type === type);
6624
+ const anthropicGateway = type === "anthropic" ? process.env["ANTHROPIC_BASE_URL"] : void 0;
6625
+ if (!existing && (mayCreate(type) || globalMatch)) {
6626
+ if (type === "azure") {
6627
+ const endpoint = process.env["AZURE_OPENAI_ENDPOINT"];
6628
+ const deploymentName = process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"];
6629
+ if (!endpoint || !deploymentName) continue;
6630
+ const apiVersion = process.env["AZURE_OPENAI_API_VERSION"];
6631
+ this.config.providers.push({
6632
+ type,
6633
+ apiKey: key,
6634
+ baseUrl: endpoint,
6635
+ deploymentName,
6636
+ ...apiVersion ? { apiVersion } : {}
6637
+ });
6638
+ continue;
6639
+ }
6640
+ const inherited = ENV_ENDPOINT_CHANNEL.has(type) ? anthropicGateway : anthropicGateway ?? globalMatch?.baseUrl;
6641
+ this.config.providers.push({
6642
+ type,
6643
+ apiKey: key,
6644
+ ...inherited ? { baseUrl: inherited } : {}
6645
+ });
6646
+ } else if (existing) {
6647
+ for (const target of targets) {
6648
+ if (target.apiKey || target.authToken) continue;
6649
+ if (anthropicGateway) {
6650
+ target.apiKey = key;
6651
+ target.baseUrl = anthropicGateway;
6652
+ continue;
6653
+ }
6654
+ if (target.baseUrl && hasDefaultEndpoint(type) && !sameCredentialEndpoint(type, void 0, target.baseUrl)) {
6655
+ if (ENV_ENDPOINT_CHANNEL.has(type)) target.baseUrl = void 0;
6656
+ else {
6657
+ console.warn(
6658
+ `${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.`
6659
+ );
6660
+ }
6661
+ }
6662
+ target.apiKey = key;
6663
+ }
6664
+ }
6665
+ }
6666
+ const authToken = process.env["ANTHROPIC_AUTH_TOKEN"];
6667
+ if (authToken && !isSubscriptionToken(authToken)) {
6668
+ const existing = this.config.providers.find((p) => p.type === "anthropic");
6669
+ const globalAnthropic = globalProviders.find((p) => p.type === "anthropic");
6670
+ const gateway = process.env["ANTHROPIC_BASE_URL"] ?? existing?.baseUrl ?? globalAnthropic?.baseUrl;
6671
+ if (gateway) {
6672
+ if (existing) {
6673
+ if (!existing.apiKey && !existing.authToken) {
6674
+ existing.authToken = authToken;
6675
+ existing.baseUrl = gateway;
6676
+ }
6677
+ } else if (mayCreate("anthropic") || globalAnthropic) {
6678
+ this.config.providers.push({ type: "anthropic", authToken, baseUrl: gateway });
6679
+ }
5951
6680
  }
5952
6681
  }
5953
6682
  if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
@@ -6299,15 +7028,15 @@ function SafeTextInput(props) {
6299
7028
  const displayValue = mask ? mask.repeat(value.length) : value;
6300
7029
  let rendered;
6301
7030
  if (displayValue.length === 0) {
6302
- rendered = showCursor && focus ? placeholder.length > 0 ? chalk7__default.default.inverse(placeholder[0]) + chalk7__default.default.grey(placeholder.slice(1)) : chalk7__default.default.inverse(" ") : placeholder.length > 0 ? chalk7__default.default.grey(placeholder) : "";
7031
+ rendered = showCursor && focus ? placeholder.length > 0 ? chalk8__default.default.inverse(placeholder[0]) + chalk8__default.default.grey(placeholder.slice(1)) : chalk8__default.default.inverse(" ") : placeholder.length > 0 ? chalk8__default.default.grey(placeholder) : "";
6303
7032
  } else if (!showCursor || !focus) {
6304
7033
  rendered = displayValue;
6305
7034
  } else {
6306
7035
  let out = "";
6307
7036
  for (let i = 0; i < displayValue.length; i++) {
6308
- out += i === cursorOffset ? chalk7__default.default.inverse(displayValue[i]) : displayValue[i];
7037
+ out += i === cursorOffset ? chalk8__default.default.inverse(displayValue[i]) : displayValue[i];
6309
7038
  }
6310
- if (cursorOffset >= displayValue.length) out += chalk7__default.default.inverse(" ");
7039
+ if (cursorOffset >= displayValue.length) out += chalk8__default.default.inverse(" ");
6311
7040
  rendered = out;
6312
7041
  }
6313
7042
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: rendered });
@@ -6323,6 +7052,7 @@ init_gemini();
6323
7052
  init_ollama();
6324
7053
  init_openai_compatible();
6325
7054
  init_openai();
7055
+ init_base();
6326
7056
 
6327
7057
  // src/core/router/selector.ts
6328
7058
  init_constants();
@@ -6920,13 +7650,13 @@ var DeadModelStore = class {
6920
7650
  function fileDeadModelPersistence(filePath) {
6921
7651
  return {
6922
7652
  load() {
6923
- if (!fs31.existsSync(filePath)) return [];
6924
- const raw = JSON.parse(fs31.readFileSync(filePath, "utf-8"));
7653
+ if (!fs32.existsSync(filePath)) return [];
7654
+ const raw = JSON.parse(fs32.readFileSync(filePath, "utf-8"));
6925
7655
  return Array.isArray(raw) ? raw : [];
6926
7656
  },
6927
7657
  save(records) {
6928
- fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
6929
- fs31.writeFileSync(filePath, JSON.stringify(records, null, 2));
7658
+ fs32.mkdirSync(path35.dirname(filePath), { recursive: true });
7659
+ fs32.writeFileSync(filePath, JSON.stringify(records, null, 2));
6930
7660
  }
6931
7661
  };
6932
7662
  }
@@ -7350,7 +8080,7 @@ init_pricing();
7350
8080
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7351
8081
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7352
8082
  var FETCH_TIMEOUT_MS = 8e3;
7353
- var DEFAULT_CACHE_FILE = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
8083
+ var DEFAULT_CACHE_FILE = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7354
8084
  function normalizeModelId2(id) {
7355
8085
  let s = id.toLowerCase();
7356
8086
  const slash = s.lastIndexOf("/");
@@ -7497,7 +8227,7 @@ var LiveDataProvider = class {
7497
8227
  }
7498
8228
  async saveCache() {
7499
8229
  try {
7500
- await fs11__default.default.mkdir(path34__default.default.dirname(this.opts.cacheFile), { recursive: true });
8230
+ await fs11__default.default.mkdir(path35__default.default.dirname(this.opts.cacheFile), { recursive: true });
7501
8231
  const cache = {
7502
8232
  fetchedAt: this.fetchedAt,
7503
8233
  snapshot: this.snapshot ?? void 0,
@@ -7764,9 +8494,50 @@ function guardTokens(text) {
7764
8494
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
7765
8495
  var DISCOVERY_TIMEOUT_MS = 4e3;
7766
8496
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
8497
+ var IDENTITY_SWEEP_INTERVAL_MS = 60 * 1e3;
8498
+ var credentialIdentities = /* @__PURE__ */ new Map();
8499
+ var lastIdentitySweep = 0;
8500
+ var identitySweepTimer;
8501
+ function ensureIdentitySweeper() {
8502
+ if (identitySweepTimer) return;
8503
+ identitySweepTimer = setInterval(() => {
8504
+ sweepExpired(Date.now());
8505
+ if (credentialIdentities.size === 0 && identitySweepTimer) {
8506
+ clearInterval(identitySweepTimer);
8507
+ identitySweepTimer = void 0;
8508
+ }
8509
+ }, IDENTITY_SWEEP_INTERVAL_MS);
8510
+ identitySweepTimer.unref?.();
8511
+ }
8512
+ function sweepExpired(now) {
8513
+ for (const [key, held] of credentialIdentities) {
8514
+ if (now - held.at > DISCOVERY_TTL_MS) credentialIdentities.delete(key);
8515
+ }
8516
+ for (const [key, entry] of cloudDiscoveryCache) {
8517
+ if (now - entry.at > DISCOVERY_TTL_MS) cloudDiscoveryCache.delete(key);
8518
+ }
8519
+ }
8520
+ function credentialIdentity(secret) {
8521
+ if (!secret) return "-";
8522
+ const now = Date.now();
8523
+ if (now - lastIdentitySweep > IDENTITY_SWEEP_INTERVAL_MS) {
8524
+ lastIdentitySweep = now;
8525
+ sweepExpired(now);
8526
+ }
8527
+ const held = credentialIdentities.get(secret);
8528
+ if (held && now - held.at <= DISCOVERY_TTL_MS) return held.id;
8529
+ const id = crypto__default.default.randomUUID();
8530
+ credentialIdentities.set(secret, { id, at: now });
8531
+ ensureIdentitySweeper();
8532
+ return id;
8533
+ }
7767
8534
  function discoveryCacheKey(type, cfg) {
7768
- const raw = `${type}|${cfg.apiKey ?? ""}|${cfg.baseUrl ?? ""}`;
7769
- return crypto__default.default.createHash("sha256").update(raw).digest("hex").slice(0, 24);
8535
+ return [
8536
+ type,
8537
+ credentialIdentity(cfg.apiKey),
8538
+ credentialIdentity(cfg.authToken),
8539
+ cfg.baseUrl ?? ""
8540
+ ].join("|");
7770
8541
  }
7771
8542
  function applyTierLimits(options, tier, limits) {
7772
8543
  if (!limits) return options;
@@ -8065,7 +8836,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8065
8836
  await Promise.all(providers.map(async (type) => {
8066
8837
  if (!this.selector.isProviderAvailable(type)) return;
8067
8838
  const cfg = config.providers.find((p) => p.type === type);
8068
- if (!cfg?.apiKey) return;
8839
+ if (!cfg || !hasProviderCredential(cfg)) return;
8069
8840
  const cacheKey = discoveryCacheKey(type, cfg);
8070
8841
  let entry = cloudDiscoveryCache.get(cacheKey);
8071
8842
  if (!entry || Date.now() - entry.at > DISCOVERY_TTL_MS) {
@@ -8074,7 +8845,11 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8074
8845
  try {
8075
8846
  const provider = this.createProvider(cfg, seed);
8076
8847
  if (typeof provider.listModels !== "function") return;
8077
- const models = await withTimeout(provider.listModels(), DISCOVERY_TIMEOUT_MS, `${type} model discovery timed out`);
8848
+ const models = await withTimeout(
8849
+ provider.listModels({ staticFallback: false }),
8850
+ DISCOVERY_TIMEOUT_MS,
8851
+ `${type} model discovery timed out`
8852
+ );
8078
8853
  if (!models.length) return;
8079
8854
  entry = { models, ids: models.map((m) => m.id), at: Date.now() };
8080
8855
  cloudDiscoveryCache.set(cacheKey, entry);
@@ -8582,12 +9357,28 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8582
9357
  return this.selector?.getAllAvailableModels() ?? [];
8583
9358
  }
8584
9359
  // ── Private ──────────────────────────────────
9360
+ /**
9361
+ * Why each configured provider failed its availability probe, kept so the
9362
+ * surfaces can say something better than "add an API key" to a user who
9363
+ * already has one. Cleared and refilled by each detectAvailableProviders().
9364
+ */
9365
+ probeFailures = /* @__PURE__ */ new Map();
9366
+ /**
9367
+ * What stopped the configured providers from being usable, if anything.
9368
+ * Empty when every configured provider passed — or when none was configured,
9369
+ * which is a different problem with a different answer.
9370
+ */
9371
+ providerProbeFailures() {
9372
+ return [...this.probeFailures].map(([provider, reason]) => ({ provider, reason }));
9373
+ }
8585
9374
  /** Logs why a configured provider failed its availability probe. */
8586
9375
  emitProbeFailure(type, reason) {
9376
+ this.probeFailures.set(type, reason);
8587
9377
  console.warn(`[router] provider "${type}" is not available: ${reason}`);
8588
9378
  }
8589
9379
  async detectAvailableProviders(configs) {
8590
9380
  const available = /* @__PURE__ */ new Set();
9381
+ this.probeFailures.clear();
8591
9382
  const checks = configs.map(async (cfg) => {
8592
9383
  try {
8593
9384
  const testModel = this.getAnyModelForProvider(cfg.type);
@@ -8597,7 +9388,13 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8597
9388
  if (ok) available.add(cfg.type);
8598
9389
  else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
8599
9390
  } catch (err) {
8600
- this.emitProbeFailure(cfg.type, err instanceof Error ? err.message : String(err));
9391
+ const reason = err instanceof Error ? err.message : String(err);
9392
+ if (err instanceof ProviderUnreachableError) {
9393
+ available.add(cfg.type);
9394
+ console.warn(`[router] provider "${cfg.type}" probe did not complete: ${reason} \u2014 continuing with it enabled`);
9395
+ return;
9396
+ }
9397
+ this.emitProbeFailure(cfg.type, reason);
8601
9398
  }
8602
9399
  });
8603
9400
  await Promise.allSettled(checks);
@@ -10316,8 +11113,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
10316
11113
  // Media generation can run for a minute; without this a cancelled run
10317
11114
  // still pays for an image nobody will see.
10318
11115
  ...this.signal ? { signal: this.signal } : {},
10319
- saveSnapshot: async (path39, content) => {
10320
- this.store?.addFileSnapshot(this.taskId, path39, content);
11116
+ saveSnapshot: async (path40, content) => {
11117
+ this.store?.addFileSnapshot(this.taskId, path40, content);
10321
11118
  },
10322
11119
  sendPeerSync: (to, syncType, content) => {
10323
11120
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -10520,7 +11317,7 @@ ${assignment.expectedOutput}`;
10520
11317
  const { promisify: promisify4 } = await import('util');
10521
11318
  const execFileAsync2 = promisify4(execFile3);
10522
11319
  for (const artifactPath of artifactPaths) {
10523
- const absolutePath = path34__default.default.resolve(this.artifactRoot(), artifactPath);
11320
+ const absolutePath = path35__default.default.resolve(this.artifactRoot(), artifactPath);
10524
11321
  try {
10525
11322
  const stat = await fs11__default.default.stat(absolutePath);
10526
11323
  if (!stat.isFile()) {
@@ -10555,7 +11352,7 @@ ${assignment.expectedOutput}`;
10555
11352
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
10556
11353
  continue;
10557
11354
  }
10558
- const ext = path34__default.default.extname(absolutePath).toLowerCase();
11355
+ const ext = path35__default.default.extname(absolutePath).toLowerCase();
10559
11356
  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;
10560
11357
  if (check) {
10561
11358
  try {
@@ -10647,7 +11444,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10647
11444
  async checkAcceptance(assignment) {
10648
11445
  const criteria = assignment.acceptance ?? [];
10649
11446
  if (!criteria.length) return [];
10650
- const resolve2 = (target) => path34__default.default.resolve(this.artifactRoot(), target);
11447
+ const resolve2 = (target) => path35__default.default.resolve(this.artifactRoot(), target);
10651
11448
  return evaluateAcceptance(criteria, assignment.files ?? [], {
10652
11449
  stat: async (target) => {
10653
11450
  try {
@@ -13876,16 +14673,16 @@ function resolveInWorkspace(workspaceRoot, input) {
13876
14673
  if (typeof input !== "string" || input.length === 0) {
13877
14674
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
13878
14675
  }
13879
- const root = path34__default.default.resolve(workspaceRoot);
13880
- const abs = path34__default.default.isAbsolute(input) ? path34__default.default.resolve(input) : path34__default.default.resolve(root, input);
13881
- const rel = path34__default.default.relative(root, abs);
13882
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34__default.default.isAbsolute(rel)) {
14676
+ const root = path35__default.default.resolve(workspaceRoot);
14677
+ const abs = path35__default.default.isAbsolute(input) ? path35__default.default.resolve(input) : path35__default.default.resolve(root, input);
14678
+ const rel = path35__default.default.relative(root, abs);
14679
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path35__default.default.isAbsolute(rel)) {
13883
14680
  throw new WorkspaceSandboxError(input, root);
13884
14681
  }
13885
14682
  try {
13886
- const real = fs31__default.default.realpathSync(abs);
13887
- const realRel = path34__default.default.relative(root, real);
13888
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34__default.default.isAbsolute(realRel))) {
14683
+ const real = fs32__default.default.realpathSync(abs);
14684
+ const realRel = path35__default.default.relative(root, real);
14685
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path35__default.default.isAbsolute(realRel))) {
13889
14686
  throw new WorkspaceSandboxError(input, root);
13890
14687
  }
13891
14688
  } catch (e) {
@@ -14538,7 +15335,7 @@ var GenerateDocumentTool = class extends BaseTool {
14538
15335
  }
14539
15336
  }
14540
15337
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
14541
- await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
15338
+ await fs11__default.default.mkdir(path35__default.default.dirname(absPath), { recursive: true });
14542
15339
  await fs11__default.default.writeFile(absPath, bytes);
14543
15340
  const charts = countCharts(content);
14544
15341
  return [
@@ -14759,7 +15556,7 @@ var FileWriteTool = class extends BaseTool {
14759
15556
  } catch {
14760
15557
  }
14761
15558
  }
14762
- await fs11__default.default.mkdir(path34__default.default.dirname(absPath), { recursive: true });
15559
+ await fs11__default.default.mkdir(path35__default.default.dirname(absPath), { recursive: true });
14763
15560
  await fs11__default.default.writeFile(absPath, content, "utf-8");
14764
15561
  return `Written ${content.length} characters to ${filePath}`;
14765
15562
  }
@@ -15257,7 +16054,7 @@ var ImageAnalyzeTool = class extends BaseTool {
15257
16054
  };
15258
16055
  async function fileToImageAttachment(filePath) {
15259
16056
  const data = await fs11__default.default.readFile(filePath);
15260
- const ext = path34__default.default.extname(filePath).toLowerCase();
16057
+ const ext = path35__default.default.extname(filePath).toLowerCase();
15261
16058
  const mimeMap = {
15262
16059
  ".jpg": "image/jpeg",
15263
16060
  ".jpeg": "image/jpeg",
@@ -15291,14 +16088,14 @@ var PDFCreateTool = class extends BaseTool {
15291
16088
  const filePath = input["path"];
15292
16089
  const content = input["content"];
15293
16090
  const title = input["title"];
15294
- const dir = path34__default.default.dirname(filePath);
15295
- if (!fs31__default.default.existsSync(dir)) {
15296
- fs31__default.default.mkdirSync(dir, { recursive: true });
16091
+ const dir = path35__default.default.dirname(filePath);
16092
+ if (!fs32__default.default.existsSync(dir)) {
16093
+ fs32__default.default.mkdirSync(dir, { recursive: true });
15297
16094
  }
15298
16095
  return new Promise((resolve2, reject) => {
15299
16096
  try {
15300
16097
  const doc = new PDFDocument__default.default({ margin: 50 });
15301
- const stream = fs31__default.default.createWriteStream(filePath);
16098
+ const stream = fs32__default.default.createWriteStream(filePath);
15302
16099
  doc.pipe(stream);
15303
16100
  if (title) {
15304
16101
  doc.info["Title"] = title;
@@ -15376,22 +16173,22 @@ var CodeInterpreterTool = class extends BaseTool {
15376
16173
  }
15377
16174
  cmdPrefix = NODE_CMD;
15378
16175
  }
15379
- const tmpDir = path34__default.default.join(this.workspaceRoot, ".cascade", "tmp");
15380
- if (!fs31__default.default.existsSync(tmpDir)) {
15381
- fs31__default.default.mkdirSync(tmpDir, { recursive: true });
16176
+ const tmpDir = path35__default.default.join(this.workspaceRoot, ".cascade", "tmp");
16177
+ if (!fs32__default.default.existsSync(tmpDir)) {
16178
+ fs32__default.default.mkdirSync(tmpDir, { recursive: true });
15382
16179
  }
15383
16180
  const extension = language === "python" ? "py" : "js";
15384
16181
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
15385
- const filePath = path34__default.default.join(tmpDir, fileName);
15386
- fs31__default.default.writeFileSync(filePath, code, "utf-8");
16182
+ const filePath = path35__default.default.join(tmpDir, fileName);
16183
+ fs32__default.default.writeFileSync(filePath, code, "utf-8");
15387
16184
  const execArgs = [filePath, ...args];
15388
16185
  return new Promise((resolve2) => {
15389
16186
  const startMs = Date.now();
15390
16187
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
15391
16188
  const duration = Date.now() - startMs;
15392
16189
  try {
15393
- if (fs31__default.default.existsSync(filePath)) {
15394
- fs31__default.default.unlinkSync(filePath);
16190
+ if (fs32__default.default.existsSync(filePath)) {
16191
+ fs32__default.default.unlinkSync(filePath);
15395
16192
  }
15396
16193
  } catch (cleanupErr) {
15397
16194
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -15703,7 +16500,7 @@ var GlobTool = class extends BaseTool {
15703
16500
  };
15704
16501
  async execute(input, _options) {
15705
16502
  const pattern = input["pattern"];
15706
- const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16503
+ const searchPath = input["path"] ? path35__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15707
16504
  const matches = await glob.glob(pattern, {
15708
16505
  cwd: searchPath,
15709
16506
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -15716,7 +16513,7 @@ var GlobTool = class extends BaseTool {
15716
16513
  const withMtime = await Promise.all(
15717
16514
  matches.map(async (rel) => {
15718
16515
  try {
15719
- const stat = await fs11__default.default.stat(path34__default.default.join(searchPath, rel));
16516
+ const stat = await fs11__default.default.stat(path35__default.default.join(searchPath, rel));
15720
16517
  return { rel, mtime: stat.mtimeMs };
15721
16518
  } catch {
15722
16519
  return { rel, mtime: 0 };
@@ -15765,7 +16562,7 @@ var GrepTool = class extends BaseTool {
15765
16562
  };
15766
16563
  async execute(input, _options) {
15767
16564
  const pattern = input["pattern"];
15768
- const searchPath = input["path"] ? path34__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16565
+ const searchPath = input["path"] ? path35__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15769
16566
  const globPattern = input["glob"];
15770
16567
  const outputMode = input["output_mode"] ?? "content";
15771
16568
  const context = input["context"] ?? 0;
@@ -15819,12 +16616,12 @@ var GrepTool = class extends BaseTool {
15819
16616
  nodir: true
15820
16617
  });
15821
16618
  } catch {
15822
- files = [path34__default.default.relative(searchPath, searchPath) || "."];
16619
+ files = [path35__default.default.relative(searchPath, searchPath) || "."];
15823
16620
  }
15824
16621
  const results = [];
15825
16622
  let totalCount = 0;
15826
16623
  for (const rel of files) {
15827
- const abs = path34__default.default.join(searchPath, rel);
16624
+ const abs = path35__default.default.join(searchPath, rel);
15828
16625
  let content;
15829
16626
  try {
15830
16627
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -16223,10 +17020,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
16223
17020
  }
16224
17021
  isIgnored(filePath) {
16225
17022
  if (!filePath) return false;
16226
- const abs = path34__default.default.resolve(this.workspaceRoot, filePath);
16227
- const rel = path34__default.default.relative(this.workspaceRoot, abs);
16228
- if (!rel || rel.startsWith("..") || path34__default.default.isAbsolute(rel)) return true;
16229
- const posixRel = rel.split(path34__default.default.sep).join("/");
17023
+ const abs = path35__default.default.resolve(this.workspaceRoot, filePath);
17024
+ const rel = path35__default.default.relative(this.workspaceRoot, abs);
17025
+ if (!rel || rel.startsWith("..") || path35__default.default.isAbsolute(rel)) return true;
17026
+ const posixRel = rel.split(path35__default.default.sep).join("/");
16230
17027
  return this.ignoreMatcher.ignores(posixRel);
16231
17028
  }
16232
17029
  };
@@ -16460,22 +17257,22 @@ var FileMcpOAuthStore = class {
16460
17257
  filePath;
16461
17258
  load() {
16462
17259
  try {
16463
- return JSON.parse(fs31__default.default.readFileSync(this.filePath, "utf-8"));
17260
+ return JSON.parse(fs32__default.default.readFileSync(this.filePath, "utf-8"));
16464
17261
  } catch {
16465
17262
  return void 0;
16466
17263
  }
16467
17264
  }
16468
17265
  save(state) {
16469
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
16470
- fs31__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
17266
+ fs32__default.default.mkdirSync(path35__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
17267
+ fs32__default.default.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
16471
17268
  try {
16472
- fs31__default.default.chmodSync(this.filePath, 384);
17269
+ fs32__default.default.chmodSync(this.filePath, 384);
16473
17270
  } catch {
16474
17271
  }
16475
17272
  }
16476
17273
  clear() {
16477
17274
  try {
16478
- fs31__default.default.rmSync(this.filePath);
17275
+ fs32__default.default.rmSync(this.filePath);
16479
17276
  } catch {
16480
17277
  }
16481
17278
  }
@@ -17123,7 +17920,7 @@ var TaskAnalyzer = class {
17123
17920
 
17124
17921
  // src/core/router/model-performance-tracker.ts
17125
17922
  init_pricing();
17126
- var DEFAULT_STATS_FILE = path34__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
17923
+ var DEFAULT_STATS_FILE = path35__default.default.join(os10__default.default.homedir(), ".cascade", "model-perf.json");
17127
17924
  var ModelPerformanceTracker = class {
17128
17925
  stats = /* @__PURE__ */ new Map();
17129
17926
  featureStats = /* @__PURE__ */ new Map();
@@ -17163,7 +17960,7 @@ var ModelPerformanceTracker = class {
17163
17960
  async save() {
17164
17961
  if (this.readOnly) return;
17165
17962
  try {
17166
- await fs11__default.default.mkdir(path34__default.default.dirname(this.statsFile), { recursive: true });
17963
+ await fs11__default.default.mkdir(path35__default.default.dirname(this.statsFile), { recursive: true });
17167
17964
  const modelsObj = {};
17168
17965
  const featuresObj = {};
17169
17966
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -17711,7 +18508,7 @@ Required capability: ${description.slice(0, 300)}`;
17711
18508
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
17712
18509
  async loadPersistedTools() {
17713
18510
  if (!this.workspacePath || !this.persistEnabled) return;
17714
- const file = path34__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
18511
+ const file = path35__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17715
18512
  try {
17716
18513
  const raw = await fs11__default.default.readFile(file, "utf-8");
17717
18514
  const specs = JSON.parse(raw);
@@ -17735,8 +18532,8 @@ Required capability: ${description.slice(0, 300)}`;
17735
18532
  }
17736
18533
  async persist() {
17737
18534
  if (!this.workspacePath || !this.persistEnabled) return;
17738
- const dir = path34__default.default.join(this.workspacePath, ".cascade");
17739
- const file = path34__default.default.join(dir, DYNAMIC_TOOLS_FILE);
18535
+ const dir = path35__default.default.join(this.workspacePath, ".cascade");
18536
+ const file = path35__default.default.join(dir, DYNAMIC_TOOLS_FILE);
17740
18537
  try {
17741
18538
  await fs11__default.default.mkdir(dir, { recursive: true });
17742
18539
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -17756,12 +18553,12 @@ var WorldStateDB = class {
17756
18553
  constructor(workspacePath, debugMode = false) {
17757
18554
  this.workspacePath = workspacePath;
17758
18555
  this.debugMode = debugMode;
17759
- const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
17760
- if (!fs31__default.default.existsSync(cascadeDir)) {
17761
- fs31__default.default.mkdirSync(cascadeDir, { recursive: true });
18556
+ const cascadeDir = path35__default.default.join(workspacePath, ".cascade");
18557
+ if (!fs32__default.default.existsSync(cascadeDir)) {
18558
+ fs32__default.default.mkdirSync(cascadeDir, { recursive: true });
17762
18559
  }
17763
- this.keyPath = path34__default.default.join(cascadeDir, "world_state.key");
17764
- this.dbPath = path34__default.default.join(cascadeDir, "world_state.db");
18560
+ this.keyPath = path35__default.default.join(cascadeDir, "world_state.key");
18561
+ this.dbPath = path35__default.default.join(cascadeDir, "world_state.db");
17765
18562
  this.initEncryptionKey();
17766
18563
  this.db = new Database2__default.default(this.dbPath);
17767
18564
  this.db.pragma("journal_mode = WAL");
@@ -17818,11 +18615,11 @@ var WorldStateDB = class {
17818
18615
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
17819
18616
  }
17820
18617
  initEncryptionKey() {
17821
- if (fs31__default.default.existsSync(this.keyPath)) {
17822
- this.encryptionKey = fs31__default.default.readFileSync(this.keyPath);
18618
+ if (fs32__default.default.existsSync(this.keyPath)) {
18619
+ this.encryptionKey = fs32__default.default.readFileSync(this.keyPath);
17823
18620
  } else {
17824
18621
  this.encryptionKey = crypto__default.default.randomBytes(32);
17825
- fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
18622
+ fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
17826
18623
  }
17827
18624
  }
17828
18625
  encrypt(text) {
@@ -18108,9 +18905,9 @@ var WorldStateDB = class {
18108
18905
  dumpDebugIfNeeded() {
18109
18906
  if (!this.debugMode) return;
18110
18907
  try {
18111
- const dumpPath = path34__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
18908
+ const dumpPath = path35__default.default.join(os10__default.default.tmpdir(), "cascade_world_state_debug.json");
18112
18909
  const entries = this.getAllEntries();
18113
- fs31__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18910
+ fs32__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18114
18911
  } catch (err) {
18115
18912
  console.error("Failed to dump debug world state", err);
18116
18913
  }
@@ -18166,11 +18963,11 @@ var ResumeStore = class {
18166
18963
  version: RESUME_SCHEMA_VERSION,
18167
18964
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
18168
18965
  };
18169
- await fs31.promises.mkdir(this.dir, { recursive: true });
18170
- const finalPath = path34__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18966
+ await fs32.promises.mkdir(this.dir, { recursive: true });
18967
+ const finalPath = path35__default.default.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18171
18968
  const tempPath = `${finalPath}.${process.pid}.tmp`;
18172
- await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
18173
- await fs31.promises.rename(tempPath, finalPath);
18969
+ await fs32.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
18970
+ await fs32.promises.rename(tempPath, finalPath);
18174
18971
  await this.prune();
18175
18972
  return true;
18176
18973
  } catch {
@@ -18181,7 +18978,7 @@ var ResumeStore = class {
18181
18978
  async list() {
18182
18979
  let entries;
18183
18980
  try {
18184
- entries = await fs31.promises.readdir(this.dir);
18981
+ entries = await fs32.promises.readdir(this.dir);
18185
18982
  } catch {
18186
18983
  return [];
18187
18984
  }
@@ -18190,7 +18987,7 @@ var ResumeStore = class {
18190
18987
  const checkpoints = [];
18191
18988
  for (const candidate of candidates2) {
18192
18989
  try {
18193
- const raw = await fs31.promises.readFile(path34__default.default.join(this.dir, candidate.name), "utf-8");
18990
+ const raw = await fs32.promises.readFile(path35__default.default.join(this.dir, candidate.name), "utf-8");
18194
18991
  const parsed = JSON.parse(raw);
18195
18992
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
18196
18993
  } catch {
@@ -18224,12 +19021,12 @@ var ResumeStore = class {
18224
19021
  const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
18225
19022
  if (!target) return null;
18226
19023
  try {
18227
- const entries = await fs31.promises.readdir(this.dir);
19024
+ const entries = await fs32.promises.readdir(this.dir);
18228
19025
  const wanted = safeId(target.taskId);
18229
19026
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
18230
19027
  if (!source) return null;
18231
19028
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
18232
- await fs31.promises.rename(path34__default.default.join(this.dir, source), path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
19029
+ await fs32.promises.rename(path35__default.default.join(this.dir, source), path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
18233
19030
  return { checkpoint: target, claimId };
18234
19031
  } catch {
18235
19032
  return null;
@@ -18238,9 +19035,9 @@ var ResumeStore = class {
18238
19035
  /** Give a claim back, so the work stays recoverable after a failed start. */
18239
19036
  async release(claimId) {
18240
19037
  try {
18241
- await fs31.promises.rename(
18242
- path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18243
- path34__default.default.join(this.dir, `${claimId}.json`)
19038
+ await fs32.promises.rename(
19039
+ path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
19040
+ path35__default.default.join(this.dir, `${claimId}.json`)
18244
19041
  );
18245
19042
  } catch {
18246
19043
  }
@@ -18248,7 +19045,7 @@ var ResumeStore = class {
18248
19045
  /** Discard a claim for good — the resumed run has taken ownership. */
18249
19046
  async settle(claimId) {
18250
19047
  try {
18251
- await fs31.promises.rm(path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
19048
+ await fs32.promises.rm(path35__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
18252
19049
  } catch {
18253
19050
  }
18254
19051
  }
@@ -18270,15 +19067,15 @@ var ResumeStore = class {
18270
19067
  async reclaimStale() {
18271
19068
  let reclaimed = 0;
18272
19069
  try {
18273
- const entries = await fs31.promises.readdir(this.dir);
19070
+ const entries = await fs32.promises.readdir(this.dir);
18274
19071
  const cutoff = this.now() - this.claimTimeoutMs;
18275
19072
  for (const name of entries) {
18276
19073
  if (!name.endsWith(CLAIM_SUFFIX)) continue;
18277
19074
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
18278
19075
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
18279
- await fs31.promises.rename(
18280
- path34__default.default.join(this.dir, name),
18281
- path34__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
19076
+ await fs32.promises.rename(
19077
+ path35__default.default.join(this.dir, name),
19078
+ path35__default.default.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
18282
19079
  );
18283
19080
  reclaimed++;
18284
19081
  }
@@ -18289,12 +19086,12 @@ var ResumeStore = class {
18289
19086
  /** Remove every checkpoint for a task id. */
18290
19087
  async delete(taskId) {
18291
19088
  try {
18292
- const entries = await fs31.promises.readdir(this.dir);
19089
+ const entries = await fs32.promises.readdir(this.dir);
18293
19090
  const wanted = safeId(taskId);
18294
19091
  await Promise.all(entries.map(async (name) => {
18295
19092
  const match = FILE_PATTERN.exec(name);
18296
19093
  if (match?.[2] === wanted) {
18297
- await fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true });
19094
+ await fs32.promises.rm(path35__default.default.join(this.dir, name), { force: true });
18298
19095
  }
18299
19096
  }));
18300
19097
  } catch {
@@ -18303,13 +19100,13 @@ var ResumeStore = class {
18303
19100
  /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
18304
19101
  async prune() {
18305
19102
  try {
18306
- const entries = await fs31.promises.readdir(this.dir);
19103
+ const entries = await fs32.promises.readdir(this.dir);
18307
19104
  const cutoff = this.now() - this.maxAgeMs;
18308
19105
  const files = entries.map((name) => ({ name, match: FILE_PATTERN.exec(name) })).filter((e) => e.match !== null).map((e) => ({ name: e.name, timestamp: Number(e.match[1]) })).sort((a, b) => b.timestamp - a.timestamp);
18309
19106
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
18310
19107
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
18311
19108
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
18312
- (name) => fs31.promises.rm(path34__default.default.join(this.dir, name), { force: true })
19109
+ (name) => fs32.promises.rm(path35__default.default.join(this.dir, name), { force: true })
18313
19110
  ));
18314
19111
  } catch {
18315
19112
  }
@@ -18686,7 +19483,7 @@ var WorkspaceIndex = class {
18686
19483
  isIgnored;
18687
19484
  chunker;
18688
19485
  constructor(opts) {
18689
- this.root = path34__default.default.resolve(opts.root);
19486
+ this.root = path35__default.default.resolve(opts.root);
18690
19487
  this.db = opts.db;
18691
19488
  this.namespace = opts.namespace ?? "code";
18692
19489
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -18718,14 +19515,14 @@ var WorkspaceIndex = class {
18718
19515
  return;
18719
19516
  }
18720
19517
  for (const entry of entries) {
18721
- const abs = path34__default.default.join(dir, entry.name);
19518
+ const abs = path35__default.default.join(dir, entry.name);
18722
19519
  if (entry.isDirectory()) {
18723
19520
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
18724
19521
  await walk(abs);
18725
19522
  } else if (entry.isFile()) {
18726
- const ext = path34__default.default.extname(entry.name).slice(1).toLowerCase();
19523
+ const ext = path35__default.default.extname(entry.name).slice(1).toLowerCase();
18727
19524
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
18728
- out.push(path34__default.default.relative(this.root, abs));
19525
+ out.push(path35__default.default.relative(this.root, abs));
18729
19526
  }
18730
19527
  }
18731
19528
  };
@@ -18755,7 +19552,7 @@ var WorkspaceIndex = class {
18755
19552
  const entries = [];
18756
19553
  for (const rel of rels) {
18757
19554
  try {
18758
- const abs = path34__default.default.join(this.root, rel);
19555
+ const abs = path35__default.default.join(this.root, rel);
18759
19556
  const stat = await fs11__default.default.stat(abs);
18760
19557
  if (stat.size > this.maxFileBytes) continue;
18761
19558
  const buf = await fs11__default.default.readFile(abs);
@@ -19245,13 +20042,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
19245
20042
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
19246
20043
  try {
19247
20044
  this.router.setDeadModelStore(new DeadModelStore(
19248
- fileDeadModelPersistence(path34__default.default.join(workspacePath, ".cascade", "dead-models.json"))
20045
+ fileDeadModelPersistence(path35__default.default.join(workspacePath, ".cascade", "dead-models.json"))
19249
20046
  ));
19250
20047
  } catch {
19251
20048
  }
19252
20049
  try {
19253
20050
  this.resumeStore = new ResumeStore({
19254
- dir: path34__default.default.join(workspacePath, ".cascade", "resume")
20051
+ dir: path35__default.default.join(workspacePath, ".cascade", "resume")
19255
20052
  });
19256
20053
  } catch {
19257
20054
  }
@@ -19339,8 +20136,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
19339
20136
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
19340
20137
  return;
19341
20138
  }
19342
- const dbPath = ci.dbPath || path34__default.default.join(this.workspacePath, ".cascade", "code-index.db");
19343
- fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
20139
+ const dbPath = ci.dbPath || path35__default.default.join(this.workspacePath, ".cascade", "code-index.db");
20140
+ fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
19344
20141
  const db = new Database2__default.default(dbPath);
19345
20142
  db.pragma("journal_mode = WAL");
19346
20143
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -20645,7 +21442,16 @@ ${finalOutput}` : ""
20645
21442
  const selector = this.router.getSelector();
20646
21443
  const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
20647
21444
  if (!model) {
20648
- throw new Error("No model is available for a fast answer \u2014 add a provider API key first.");
21445
+ const failures2 = this.router.providerProbeFailures();
21446
+ const context = failures2.length ? ` Configured providers: ${failures2.map((f) => `${f.provider}: ${f.reason}`).join("; ")}` : "";
21447
+ if (options.fastAnswerModel) {
21448
+ throw new Error(
21449
+ `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}`
21450
+ );
21451
+ }
21452
+ throw new Error(
21453
+ 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."
21454
+ );
20649
21455
  }
20650
21456
  this.recordDecision(
20651
21457
  "model",
@@ -20779,14 +21585,14 @@ ${finalOutput}` : ""
20779
21585
  init_constants();
20780
21586
  var CLOUD_SESSION_FILE = "cloud-session.json";
20781
21587
  function globalDir(homeDir = os10__default.default.homedir()) {
20782
- return path34__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
21588
+ return path35__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20783
21589
  }
20784
21590
  function cloudSessionPath(dir = globalDir()) {
20785
- return path34__default.default.join(dir, CLOUD_SESSION_FILE);
21591
+ return path35__default.default.join(dir, CLOUD_SESSION_FILE);
20786
21592
  }
20787
21593
  function loadCloudSession(dir = globalDir()) {
20788
21594
  try {
20789
- const raw = fs31__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
21595
+ const raw = fs32__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20790
21596
  const parsed = JSON.parse(raw);
20791
21597
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20792
21598
  return parsed;
@@ -20795,17 +21601,17 @@ function loadCloudSession(dir = globalDir()) {
20795
21601
  }
20796
21602
  }
20797
21603
  function saveCloudSession(session, dir = globalDir()) {
20798
- fs31__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21604
+ fs32__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
20799
21605
  const file = cloudSessionPath(dir);
20800
- fs31__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21606
+ fs32__default.default.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20801
21607
  try {
20802
- fs31__default.default.chmodSync(file, 384);
21608
+ fs32__default.default.chmodSync(file, 384);
20803
21609
  } catch {
20804
21610
  }
20805
21611
  }
20806
21612
  function clearCloudSession(dir = globalDir()) {
20807
21613
  try {
20808
- fs31__default.default.rmSync(cloudSessionPath(dir));
21614
+ fs32__default.default.rmSync(cloudSessionPath(dir));
20809
21615
  } catch {
20810
21616
  }
20811
21617
  }
@@ -20977,16 +21783,16 @@ var CloudClient = class _CloudClient {
20977
21783
  return (await this.refresh()).accessToken;
20978
21784
  }
20979
21785
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
20980
- async authed(path39, init = {}) {
20981
- const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21786
+ async authed(path40, init = {}) {
21787
+ const call = (token) => fetch(this.url(path40), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
20982
21788
  let res = await call(await this.accessToken());
20983
21789
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
20984
21790
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
20985
21791
  const text = await res.text();
20986
21792
  return text ? JSON.parse(text) : {};
20987
21793
  }
20988
- authedGet(path39) {
20989
- return this.authed(path39);
21794
+ authedGet(path40) {
21795
+ return this.authed(path40);
20990
21796
  }
20991
21797
  async fetchMe(token) {
20992
21798
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21847,7 +22653,7 @@ function StatusBarInternal({
21847
22653
  const savedStr = savedUsd > 0 ? ` \xB7 saved $${savedUsd.toFixed(4)}` : "";
21848
22654
  const rightStr = ` ${formatTokens(tokens)} \xB7 $${costUsd.toFixed(4)}${savedStr} ${isExecuting ? "\u26A1" : "\xB7"} `;
21849
22655
  const gap = Math.max(0, width - leftStr.length - rightStr.length);
21850
- const line = chalk7__default.default.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk7__default.default.bold(leftStr) + " ".repeat(gap) + rightStr);
22656
+ const line = chalk8__default.default.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk8__default.default.bold(leftStr) + " ".repeat(gap) + rightStr);
21851
22657
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: line });
21852
22658
  }
21853
22659
  function formatTokens(n) {
@@ -22316,7 +23122,7 @@ function renderContent(content, theme) {
22316
23122
  i
22317
23123
  );
22318
23124
  }
22319
- const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk7__default.default.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk7__default.default.bgHex(theme.colors.border)(t));
23125
+ const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk8__default.default.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk8__default.default.bgHex(theme.colors.border)(t));
22320
23126
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { wrap: "wrap", children: rendered }, i);
22321
23127
  });
22322
23128
  }
@@ -22760,9 +23566,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22760
23566
  }
22761
23567
  } catch {
22762
23568
  }
22763
- const configPath = path34__default.default.join(workspacePath, ".cascade", "config.json");
23569
+ const configPath = path35__default.default.join(workspacePath, ".cascade", "config.json");
22764
23570
  try {
22765
- await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
23571
+ await fs11__default.default.mkdir(path35__default.default.dirname(configPath), { recursive: true });
22766
23572
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22767
23573
  } catch (err) {
22768
23574
  const msg = err instanceof Error ? err.message : String(err);
@@ -22814,9 +23620,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22814
23620
  if (msg.includes("non-text parts")) return;
22815
23621
  originalLog(...args);
22816
23622
  };
22817
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
23623
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
22818
23624
  storeRef.current = store;
22819
- globalStoreRef.current = new MemoryStore(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
23625
+ globalStoreRef.current = new MemoryStore(path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22820
23626
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
22821
23627
  setIdentities(identityRows);
22822
23628
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -22896,7 +23702,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22896
23702
  onThemeChange: (name) => setTheme(getTheme(name)),
22897
23703
  onExport: async (fmt) => {
22898
23704
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22899
- const exportPath = path34__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
23705
+ const exportPath = path35__default.default.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22900
23706
  if (fmt === "json") {
22901
23707
  await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
22902
23708
  } else {
@@ -23804,7 +24610,7 @@ function stringifySlashOutput(val) {
23804
24610
  }
23805
24611
  async function searchSessionsAndMessages(query, workspacePath) {
23806
24612
  if (!query) return "Usage: /search <query>";
23807
- const dbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
24613
+ const dbPath = path35__default.default.join(workspacePath, CASCADE_DB_FILE);
23808
24614
  try {
23809
24615
  await fs11__default.default.access(dbPath);
23810
24616
  } catch {
@@ -23838,7 +24644,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
23838
24644
  async function diagnoseRuntime(config, workspacePath) {
23839
24645
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
23840
24646
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
23841
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
24647
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
23842
24648
  try {
23843
24649
  const sessions2 = store.listSessions(void 0, 3);
23844
24650
  return [
@@ -23857,7 +24663,7 @@ async function diagnoseRuntime(config, workspacePath) {
23857
24663
  }
23858
24664
  async function showRecentLogs(args, workspacePath) {
23859
24665
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
23860
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
24666
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
23861
24667
  try {
23862
24668
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
23863
24669
  if (!logs.length) return "No recent runtime logs.";
@@ -23869,7 +24675,7 @@ async function showRecentLogs(args, workspacePath) {
23869
24675
  async function loadSessionSnapshot(args, workspacePath) {
23870
24676
  const sessionId = args[0];
23871
24677
  if (!sessionId) return "Usage: /resume <sessionId>";
23872
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
24678
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
23873
24679
  try {
23874
24680
  const session = store.getSession(sessionId);
23875
24681
  if (!session) return `Session not found: ${sessionId}`;
@@ -24289,9 +25095,9 @@ function SetupWizard({ workspacePath, onComplete }) {
24289
25095
  ...anyAuto ? { cascadeAuto: true } : {}
24290
25096
  };
24291
25097
  const config = CascadeConfigSchema.parse(rawConfig);
24292
- const configDir = path34__default.default.join(workspacePath, ".cascade");
25098
+ const configDir = path35__default.default.join(workspacePath, ".cascade");
24293
25099
  await fs11__default.default.mkdir(configDir, { recursive: true });
24294
- const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
25100
+ const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
24295
25101
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
24296
25102
  savedConfigRef.current = config;
24297
25103
  dispatchRef.current({ type: "GO_DONE" });
@@ -24632,25 +25438,25 @@ function printTelemetryBanner() {
24632
25438
  async function initCommand(workspacePath = process.cwd()) {
24633
25439
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
24634
25440
  try {
24635
- const configDir = path34__default.default.join(workspacePath, ".cascade");
25441
+ const configDir = path35__default.default.join(workspacePath, ".cascade");
24636
25442
  await fs11__default.default.mkdir(configDir, { recursive: true });
24637
- const mdPath = path34__default.default.join(workspacePath, "CASCADE.md");
25443
+ const mdPath = path35__default.default.join(workspacePath, "CASCADE.md");
24638
25444
  if (!await fileExists(mdPath)) {
24639
25445
  await createDefaultCascadeMd(workspacePath);
24640
- spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
25446
+ spin.succeed(chalk8__default.default.green("Created CASCADE.md"));
24641
25447
  }
24642
- const ignorePath = path34__default.default.join(workspacePath, ".cascadeignore");
25448
+ const ignorePath = path35__default.default.join(workspacePath, ".cascadeignore");
24643
25449
  if (!await fileExists(ignorePath)) {
24644
25450
  await createDefaultIgnoreFile(workspacePath);
24645
- spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
25451
+ spin.succeed(chalk8__default.default.green("Created .cascadeignore"));
24646
25452
  }
24647
25453
  spin.stop();
24648
25454
  console.log();
24649
- console.log(chalk7__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
25455
+ console.log(chalk8__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
24650
25456
  console.log();
24651
- const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
25457
+ const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
24652
25458
  if (await fileExists(configPath)) {
24653
- console.log(chalk7__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
25459
+ console.log(chalk8__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
24654
25460
  console.log();
24655
25461
  }
24656
25462
  const config = await runSetupWizard(workspacePath);
@@ -24658,10 +25464,10 @@ async function initCommand(workspacePath = process.cwd()) {
24658
25464
  await cm.load();
24659
25465
  await cm.updateConfig(config);
24660
25466
  console.log();
24661
- console.log(chalk7__default.default.green(" \u25C8 Setup complete! Run `cascade` to start."));
25467
+ console.log(chalk8__default.default.green(" \u25C8 Setup complete! Run `cascade` to start."));
24662
25468
  console.log();
24663
25469
  } catch (err) {
24664
- console.error(chalk7__default.default.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
25470
+ console.error(chalk8__default.default.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
24665
25471
  process.exit(1);
24666
25472
  }
24667
25473
  }
@@ -24676,7 +25482,20 @@ async function fileExists(p) {
24676
25482
 
24677
25483
  // src/cli/commands/doctor.ts
24678
25484
  init_constants();
25485
+
25486
+ // src/config/credential-discovery.ts
25487
+ init_revoked_credentials();
24679
25488
  var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
25489
+ 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.";
25490
+ var OPENAI_COMPATIBLE_ENV = [
25491
+ { id: "openrouter", env: "OPENROUTER_API_KEY", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1" },
25492
+ { id: "groq", env: "GROQ_API_KEY", label: "Groq", baseUrl: "https://api.groq.com/openai/v1" },
25493
+ { id: "deepseek", env: "DEEPSEEK_API_KEY", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
25494
+ { id: "xai", env: "XAI_API_KEY", label: "xAI", baseUrl: "https://api.x.ai/v1" },
25495
+ { id: "mistral", env: "MISTRAL_API_KEY", label: "Mistral", baseUrl: "https://api.mistral.ai/v1" },
25496
+ { id: "together", env: "TOGETHER_API_KEY", label: "Together AI", baseUrl: "https://api.together.xyz/v1" },
25497
+ { id: "fireworks", env: "FIREWORKS_API_KEY", label: "Fireworks AI", baseUrl: "https://api.fireworks.ai/inference/v1" }
25498
+ ];
24680
25499
  async function readJson(file) {
24681
25500
  try {
24682
25501
  const raw = await fs11__default.default.readFile(file, "utf-8");
@@ -24700,36 +25519,118 @@ function fromEnv(env) {
24700
25519
  for (const { env: name, provider } of map) {
24701
25520
  const secret = str(env[name]);
24702
25521
  if (!secret || seen.has(provider)) continue;
25522
+ if (isSubscriptionToken(secret)) {
25523
+ out.push({
25524
+ provider,
25525
+ sourceTool: `Environment (${name})`,
25526
+ kind: "oauth",
25527
+ secret,
25528
+ directlyUsable: false,
25529
+ warning: ANTHROPIC_OAUTH_WARNING
25530
+ });
25531
+ continue;
25532
+ }
24703
25533
  seen.add(provider);
24704
- out.push({ provider, sourceTool: `Environment (${name})`, kind: "api-key", secret, directlyUsable: true });
25534
+ const gateway = provider === "anthropic" ? str(env["ANTHROPIC_BASE_URL"]) : void 0;
25535
+ out.push({
25536
+ provider,
25537
+ sourceTool: `Environment (${name})`,
25538
+ kind: "api-key",
25539
+ secret,
25540
+ directlyUsable: true,
25541
+ ...gateway ? { baseUrl: gateway } : {}
25542
+ });
25543
+ }
25544
+ const authToken = str(env["ANTHROPIC_AUTH_TOKEN"]);
25545
+ if (authToken && !seen.has("anthropic")) {
25546
+ seen.add("anthropic");
25547
+ if (isSubscriptionToken(authToken)) {
25548
+ out.push({
25549
+ provider: "anthropic",
25550
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25551
+ kind: "oauth",
25552
+ secret: authToken,
25553
+ directlyUsable: false,
25554
+ warning: ANTHROPIC_OAUTH_WARNING
25555
+ });
25556
+ } else {
25557
+ const gateway = str(env["ANTHROPIC_BASE_URL"]);
25558
+ out.push({
25559
+ provider: "anthropic",
25560
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25561
+ kind: "bearer",
25562
+ secret: authToken,
25563
+ directlyUsable: Boolean(gateway),
25564
+ ...gateway ? { baseUrl: gateway } : {},
25565
+ ...gateway ? {} : {
25566
+ 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."
25567
+ }
25568
+ });
25569
+ }
25570
+ }
25571
+ const azureKey = str(env["AZURE_OPENAI_KEY"]) ?? str(env["AZURE_OPENAI_API_KEY"]);
25572
+ if (azureKey) {
25573
+ const endpoint = str(env["AZURE_OPENAI_ENDPOINT"]);
25574
+ const deployment = str(env["AZURE_OPENAI_DEPLOYMENT"]) ?? str(env["AZURE_OPENAI_DEPLOYMENT_NAME"]);
25575
+ const apiVersion = str(env["AZURE_OPENAI_API_VERSION"]);
25576
+ const missing = [
25577
+ !endpoint && "AZURE_OPENAI_ENDPOINT",
25578
+ !deployment && "AZURE_OPENAI_DEPLOYMENT"
25579
+ ].filter((x) => typeof x === "string");
25580
+ out.push({
25581
+ provider: "azure",
25582
+ sourceTool: "Environment (AZURE_OPENAI_KEY)",
25583
+ kind: "api-key",
25584
+ secret: azureKey,
25585
+ directlyUsable: missing.length === 0,
25586
+ ...endpoint ? { baseUrl: endpoint } : {},
25587
+ ...deployment ? { deploymentName: deployment } : {},
25588
+ ...apiVersion ? { apiVersion } : {},
25589
+ ...missing.length ? { warning: `Azure also needs ${missing.join(" and ")} \u2014 a key alone cannot address a deployment.` } : {}
25590
+ });
25591
+ }
25592
+ for (const { id, env: name, label, baseUrl } of OPENAI_COMPATIBLE_ENV) {
25593
+ const secret = str(env[name]);
25594
+ if (!secret) continue;
25595
+ out.push({
25596
+ provider: "openai-compatible",
25597
+ sourceTool: `Environment (${name}) \u2014 ${label}`,
25598
+ kind: "api-key",
25599
+ secret,
25600
+ directlyUsable: true,
25601
+ baseUrl,
25602
+ serviceId: id
25603
+ });
24705
25604
  }
24706
25605
  return out;
24707
25606
  }
24708
- async function fromClaudeCode(home) {
24709
- const file = path34__default.default.join(home, ".claude", ".credentials.json");
25607
+ async function fromClaudeCode(home, env) {
25608
+ const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path35__default.default.join(home, ".claude");
25609
+ const file = path35__default.default.join(dir, ".credentials.json");
24710
25610
  const data = await readJson(file);
24711
25611
  if (!data) return [];
25612
+ const found = [];
25613
+ const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
25614
+ if (apiKey) {
25615
+ found.push({ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file });
25616
+ }
24712
25617
  const oauth = data["claudeAiOauth"];
24713
25618
  const oauthToken = str(oauth?.["accessToken"]);
24714
25619
  if (oauthToken) {
24715
- return [{
25620
+ found.push({
24716
25621
  provider: "anthropic",
24717
25622
  sourceTool: "Claude Code",
24718
25623
  kind: "oauth",
24719
25624
  secret: oauthToken,
24720
- directlyUsable: true,
24721
- warning: TOS_WARNING,
25625
+ directlyUsable: false,
25626
+ warning: ANTHROPIC_OAUTH_WARNING,
24722
25627
  sourcePath: file
24723
- }];
24724
- }
24725
- const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
24726
- if (apiKey) {
24727
- return [{ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file }];
25628
+ });
24728
25629
  }
24729
- return [];
25630
+ return found;
24730
25631
  }
24731
25632
  async function fromCodex(home) {
24732
- const file = path34__default.default.join(home, ".codex", "auth.json");
25633
+ const file = path35__default.default.join(home, ".codex", "auth.json");
24733
25634
  const data = await readJson(file);
24734
25635
  if (!data) return [];
24735
25636
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -24752,7 +25653,7 @@ async function fromCodex(home) {
24752
25653
  return [];
24753
25654
  }
24754
25655
  async function fromGemini(home) {
24755
- const file = path34__default.default.join(home, ".gemini", "oauth_creds.json");
25656
+ const file = path35__default.default.join(home, ".gemini", "oauth_creds.json");
24756
25657
  const data = await readJson(file);
24757
25658
  const accessToken = str(data?.["access_token"]);
24758
25659
  if (!accessToken) return [];
@@ -24768,7 +25669,7 @@ async function fromGemini(home) {
24768
25669
  }
24769
25670
  async function fromCopilot(home) {
24770
25671
  for (const name of ["apps.json", "hosts.json"]) {
24771
- const file = path34__default.default.join(home, ".config", "github-copilot", name);
25672
+ const file = path35__default.default.join(home, ".config", "github-copilot", name);
24772
25673
  const data = await readJson(file);
24773
25674
  if (!data) continue;
24774
25675
  for (const value of Object.values(data)) {
@@ -24793,7 +25694,7 @@ async function discoverCredentials(opts = {}) {
24793
25694
  const env = opts.env ?? process.env;
24794
25695
  const groups = await Promise.all([
24795
25696
  Promise.resolve(fromEnv(env)),
24796
- fromClaudeCode(home),
25697
+ fromClaudeCode(home, env),
24797
25698
  fromCodex(home),
24798
25699
  fromGemini(home),
24799
25700
  fromCopilot(home)
@@ -24804,10 +25705,257 @@ function maskSecret(secret) {
24804
25705
  if (secret.length <= 8) return "\u2022\u2022\u2022\u2022";
24805
25706
  return `${secret.slice(0, 6)}\u2026${secret.slice(-2)}`;
24806
25707
  }
25708
+ async function linkCommand(target, options = {}) {
25709
+ const found = await discoverCredentials();
25710
+ if (found.length === 0) {
25711
+ console.log(chalk8__default.default.yellow("\n No reusable credentials found.\n"));
25712
+ console.log(chalk8__default.default.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
25713
+ console.log(chalk8__default.default.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
25714
+ return;
25715
+ }
25716
+ if (!target) {
25717
+ printDiscovered(found);
25718
+ return;
25719
+ }
25720
+ const resolved = normalizeProvider(target);
25721
+ if (!resolved) {
25722
+ const services2 = OPENAI_COMPATIBLE_ENV.map((s) => s.id).join(", ");
25723
+ console.log(chalk8__default.default.red(`
25724
+ Unknown provider "${target}".`));
25725
+ console.log(chalk8__default.default.gray(` Use one of: anthropic, openai, gemini, azure, openai-compatible`));
25726
+ console.log(chalk8__default.default.gray(` \u2026or name a compatible service directly: ${services2}
25727
+ `));
25728
+ return;
25729
+ }
25730
+ const { provider, serviceId } = resolved;
25731
+ const candidates2 = found.filter((c) => c.provider === provider && (!serviceId || c.serviceId === serviceId));
25732
+ const services = [...new Set(candidates2.map((c) => c.serviceId).filter((id) => !!id))];
25733
+ if (!serviceId && services.length > 1) {
25734
+ console.log(chalk8__default.default.yellow(`
25735
+ Keys for several OpenAI-compatible services are set, and "${target}" does not say which.`));
25736
+ console.log(chalk8__default.default.gray(" Name the one you mean:\n"));
25737
+ for (const id of services) console.log(chalk8__default.default.cyan(` cascade link ${id}`));
25738
+ console.log("");
25739
+ return;
25740
+ }
25741
+ const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
25742
+ if (!chosen) {
25743
+ console.log(chalk8__default.default.yellow(`
25744
+ No detected credential maps to "${provider}".
25745
+ `));
25746
+ return;
25747
+ }
25748
+ const cm = new ConfigManager(options.workspace ?? process.cwd());
25749
+ await cm.load();
25750
+ const configured = cm.getConfig().providers;
25751
+ const routedByConfig = isRoutedByConfig(chosen, configured);
25752
+ if (!chosen.directlyUsable && !routedByConfig) {
25753
+ console.log(chalk8__default.default.yellow(`
25754
+ Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
25755
+ if (chosen.warning) console.log(chalk8__default.default.gray(` ${chosen.warning}`));
25756
+ console.log(chalk8__default.default.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
25757
+ if (options.acceptRisk && chosen.kind === "oauth") {
25758
+ console.log(chalk8__default.default.gray(" --accept-risk no longer applies \u2014 a subscription token is refused by the"));
25759
+ console.log(chalk8__default.default.gray(" provider itself, so there is no working configuration to opt into.\n"));
25760
+ }
25761
+ console.log(chalk8__default.default.gray(` Set ${chalk8__default.default.white(envKeyFor(provider))} instead, or add a key with `) + chalk8__default.default.cyan("cascade init") + chalk8__default.default.gray(".\n"));
25762
+ return;
25763
+ }
25764
+ const adopted = await adoptCredential(chosen, cm);
25765
+ if (!adopted) return;
25766
+ console.log(chalk8__default.default.green(`
25767
+ \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
25768
+ if (chosen.kind === "bearer") {
25769
+ console.log(chalk8__default.default.gray(" Adopted as a bearer token \u2014 set `baseUrl` to the gateway that issued it."));
25770
+ }
25771
+ console.log(chalk8__default.default.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
25772
+ }
25773
+ function isRoutedByConfig(cred, configured) {
25774
+ if (cred.provider === "azure") {
25775
+ return configured.some((p) => p.type === "azure" && p.deploymentName?.trim() && p.baseUrl?.trim());
25776
+ }
25777
+ return cred.kind === "bearer" && configured.some((p) => p.type === cred.provider && p.baseUrl?.trim());
25778
+ }
25779
+ function willAdoptFromConfig(cred, configured) {
25780
+ if (cred.provider === "azure") {
25781
+ const target = cred.baseUrl?.trim();
25782
+ const deployment = cred.deploymentName?.trim();
25783
+ if (target && deployment) return !azureRoutedTarget(deployment, target, configured).collision;
25784
+ return azureDeploymentsForCredential(cred, configured).length > 0;
25785
+ }
25786
+ return cred.directlyUsable || isRoutedByConfig(cred, configured);
25787
+ }
25788
+ function azureRoutedTarget(deployment, target, configured) {
25789
+ const named = (p) => p.type === "azure" && (p.deploymentName?.trim() ?? "") === deployment;
25790
+ const existing = configured.find((p) => named(p) && (sameAzureEndpoint(p.baseUrl, target) || !p.baseUrl?.trim()));
25791
+ if (existing) return { existing, collision: false };
25792
+ return {
25793
+ collision: configured.some((p) => named(p) && !!p.baseUrl?.trim() && !sameAzureEndpoint(p.baseUrl, target))
25794
+ };
25795
+ }
25796
+ function azureDeploymentsForCredential(cred, configured) {
25797
+ const routing = resolveAzureRouting(configured, configured, {
25798
+ endpoint: cred.baseUrl,
25799
+ deployment: cred.deploymentName
25800
+ });
25801
+ return routing.ok ? routing.rows : [];
25802
+ }
25803
+ function azureRoutingForCredential(cred, configured) {
25804
+ return resolveAzureRouting(configured, configured, {
25805
+ endpoint: cred.baseUrl,
25806
+ deployment: cred.deploymentName
25807
+ });
25808
+ }
25809
+ function printDiscovered(found) {
25810
+ console.log(chalk8__default.default.magenta("\n \u25C8 Detected credentials\n"));
25811
+ for (const c of found) {
25812
+ const usable = c.directlyUsable ? chalk8__default.default.green("usable") : chalk8__default.default.yellow("needs vendor backend");
25813
+ const kind = c.kind === "oauth" ? chalk8__default.default.yellow("subscription") : c.kind === "bearer" ? chalk8__default.default.cyan("bearer") : chalk8__default.default.gray("api-key");
25814
+ console.log(` ${chalk8__default.default.white(c.provider.padEnd(18))} ${chalk8__default.default.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
25815
+ console.log(chalk8__default.default.gray(` from ${c.sourceTool}`));
25816
+ if (c.warning) console.log(chalk8__default.default.yellow(` \u26A0 ${c.warning}`));
25817
+ }
25818
+ console.log(chalk8__default.default.gray("\n Adopt one with: ") + chalk8__default.default.cyan("cascade link <provider>"));
25819
+ console.log(chalk8__default.default.gray(" Subscription tokens are listed for visibility and cannot be adopted.\n"));
25820
+ }
25821
+ function envKeyFor(provider) {
25822
+ if (provider === "openai") return "OPENAI_API_KEY";
25823
+ if (provider === "gemini") return "GEMINI_API_KEY";
25824
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
25825
+ if (provider === "azure") return "AZURE_OPENAI_KEY";
25826
+ return "the provider's API key";
25827
+ }
25828
+ function normalizeProvider(target) {
25829
+ const t = target.toLowerCase();
25830
+ if (t === "anthropic" || t === "claude" || t === "claude-code") return { provider: "anthropic" };
25831
+ if (t === "openai" || t === "codex" || t === "gpt") return { provider: "openai" };
25832
+ if (t === "gemini" || t === "google") return { provider: "gemini" };
25833
+ if (t === "azure" || t === "azure-openai") return { provider: "azure" };
25834
+ if (t === "openai-compatible" || t === "compatible") return { provider: "openai-compatible" };
25835
+ const service = OPENAI_COMPATIBLE_ENV.find((s) => s.id === t || s.label.toLowerCase() === t);
25836
+ if (service) return { provider: "openai-compatible", serviceId: service.id };
25837
+ return null;
25838
+ }
25839
+ async function adoptCredential(cred, cm) {
25840
+ const config = cm.getConfig();
25841
+ const azureDeployments = cred.provider === "azure" ? config.providers.filter((p) => p.type === "azure" && p.deploymentName?.trim()) : [];
25842
+ if (azureDeployments.length > 0) {
25843
+ const target = cred.baseUrl?.trim();
25844
+ const deployment = cred.deploymentName?.trim();
25845
+ if (target && deployment) {
25846
+ const { existing: existing2, collision: claimedElsewhere } = azureRoutedTarget(deployment, target, config.providers);
25847
+ if (claimedElsewhere) {
25848
+ console.log(chalk8__default.default.yellow(`
25849
+ A different Azure resource already has a deployment named "${deployment}".`));
25850
+ console.log(chalk8__default.default.gray(" Deployment names are model ids in Cascade, so two resources cannot share one \u2014"));
25851
+ console.log(chalk8__default.default.gray(" the second would never be selected. Rename one deployment, or remove the entry"));
25852
+ console.log(chalk8__default.default.gray(" you no longer use, then run this again.\n"));
25853
+ return false;
25854
+ }
25855
+ const patch2 = {
25856
+ apiKey: cred.secret,
25857
+ credentialSource: cred.sourceTool,
25858
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25859
+ };
25860
+ const updated = config.providers.map((p) => {
25861
+ if (p === existing2) return { ...p, baseUrl: target, ...patch2 };
25862
+ const sibling = p.type === "azure" && p.deploymentName?.trim() && sameAzureEndpoint(p.baseUrl, target);
25863
+ return sibling ? { ...p, ...patch2 } : p;
25864
+ });
25865
+ const providers3 = existing2 ? updated : [...updated, {
25866
+ type: "azure",
25867
+ baseUrl: target,
25868
+ deploymentName: deployment,
25869
+ ...patch2
25870
+ }];
25871
+ await cm.updateConfig({ providers: providers3 });
25872
+ return true;
25873
+ }
25874
+ const scoped = azureDeploymentsForCredential(cred, config.providers);
25875
+ if (scoped.length === 0) {
25876
+ const all = [...new Set(azureDeployments.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
25877
+ const onTarget = target ? azureDeployments.some((p) => sameAzureEndpoint(p.baseUrl, target)) : true;
25878
+ const anyRouted = azureDeployments.some((p) => p.baseUrl?.trim());
25879
+ if (!anyRouted) {
25880
+ console.log(chalk8__default.default.yellow("\n The configured Azure deployments have no endpoint."));
25881
+ console.log(chalk8__default.default.gray(" A deployment needs its resource URL before a key can reach it \u2014"));
25882
+ console.log(chalk8__default.default.gray(" set AZURE_OPENAI_ENDPOINT, or add `baseUrl` to the entries in"));
25883
+ console.log(chalk8__default.default.gray(" .cascade/config.json, then run this again.\n"));
25884
+ return false;
25885
+ }
25886
+ if (target && !onTarget) {
25887
+ console.log(chalk8__default.default.yellow(`
25888
+ No configured Azure deployment is on ${target}.`));
25889
+ console.log(chalk8__default.default.gray(" Set AZURE_OPENAI_DEPLOYMENT as well to add one, or point"));
25890
+ console.log(chalk8__default.default.gray(" AZURE_OPENAI_ENDPOINT at a resource you have configured:\n"));
25891
+ } else {
25892
+ console.log(chalk8__default.default.yellow("\n Several Azure resources are configured, and an Azure key belongs to one of them."));
25893
+ console.log(chalk8__default.default.gray(" Set AZURE_OPENAI_ENDPOINT to the resource this key is for, then run again:\n"));
25894
+ }
25895
+ for (const r of all) console.log(chalk8__default.default.gray(` ${r || "(no endpoint set)"}`));
25896
+ console.log("");
25897
+ return false;
25898
+ }
25899
+ const patch = {
25900
+ apiKey: cred.secret,
25901
+ credentialSource: cred.sourceTool,
25902
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25903
+ };
25904
+ const providers2 = config.providers.map((p) => scoped.includes(p) ? { ...p, ...patch } : p);
25905
+ const routing = azureRoutingForCredential(cred, config.providers);
25906
+ if (routing.ok && routing.createDeployment) {
25907
+ providers2.push({
25908
+ type: "azure",
25909
+ deploymentName: routing.createDeployment,
25910
+ baseUrl: routing.resource,
25911
+ ...patch
25912
+ });
25913
+ console.log(chalk8__default.default.gray(
25914
+ ` Added deployment "${routing.createDeployment}" on ${normalizeAzureEndpoint(routing.resource)}.`
25915
+ ));
25916
+ }
25917
+ await cm.updateConfig({ providers: providers2 });
25918
+ return true;
25919
+ }
25920
+ const existing = config.providers.find((p) => p.type === cred.provider);
25921
+ const next = {
25922
+ ...existing,
25923
+ type: cred.provider,
25924
+ credentialSource: cred.sourceTool,
25925
+ // Both cleared first: adopting a credential REPLACES the old one, and
25926
+ // leaving a stale key beside a new token makes which one is in use depend
25927
+ // on provider-internal precedence.
25928
+ apiKey: void 0,
25929
+ authToken: void 0
25930
+ };
25931
+ if (cred.kind === "bearer") {
25932
+ next.authToken = cred.secret;
25933
+ } else {
25934
+ next.apiKey = cred.secret;
25935
+ }
25936
+ if (cred.kind !== "bearer" && !cred.baseUrl && hasDefaultEndpoint(cred.provider) && next.baseUrl) {
25937
+ delete next.baseUrl;
25938
+ delete next.local;
25939
+ }
25940
+ if (cred.baseUrl) {
25941
+ next.baseUrl = cred.baseUrl;
25942
+ delete next.local;
25943
+ }
25944
+ if (cred.deploymentName) next.deploymentName = cred.deploymentName;
25945
+ if (cred.apiVersion) next.apiVersion = cred.apiVersion;
25946
+ const providers = config.providers.filter((p) => p.type !== cred.provider);
25947
+ providers.push(next);
25948
+ await cm.updateConfig({ providers });
25949
+ return true;
25950
+ }
24807
25951
 
24808
25952
  // src/cli/commands/doctor.ts
25953
+ function linkableCredentialsDetail(discovered, configured = []) {
25954
+ const usable = discovered.filter((d) => willAdoptFromConfig(d, configured)).length;
25955
+ 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`;
25956
+ }
24809
25957
  async function doctorCommand() {
24810
- console.log(chalk7__default.default.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
25958
+ console.log(chalk8__default.default.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
24811
25959
  const checks = [];
24812
25960
  const nodeVersion = process.versions.node;
24813
25961
  const [major] = nodeVersion.split(".").map(Number);
@@ -24822,7 +25970,7 @@ async function doctorCommand() {
24822
25970
  checks.push({
24823
25971
  label: "Cascade config",
24824
25972
  ok: true,
24825
- detail: `Loaded ${path34__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
25973
+ detail: `Loaded ${path35__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24826
25974
  });
24827
25975
  const providers = [
24828
25976
  { type: "anthropic", name: "Anthropic" },
@@ -24832,10 +25980,11 @@ async function doctorCommand() {
24832
25980
  ];
24833
25981
  for (const { type, name } of providers) {
24834
25982
  const key = cm.getApiKey(type);
25983
+ const token = key ? void 0 : cm.getAuthToken(type);
24835
25984
  checks.push({
24836
- label: `${name} API key`,
24837
- ok: Boolean(key),
24838
- detail: key ? "Set" : "Missing"
25985
+ label: `${name} credential`,
25986
+ ok: Boolean(key || token),
25987
+ detail: key ? "API key set" : token ? "Bearer token set" : "Missing"
24839
25988
  });
24840
25989
  }
24841
25990
  const ollamaOk = await checkEndpoint(OLLAMA_BASE_URL + "/api/tags");
@@ -24889,32 +26038,31 @@ async function doctorCommand() {
24889
26038
  try {
24890
26039
  const discovered = await discoverCredentials();
24891
26040
  if (discovered.length > 0) {
24892
- const usable = discovered.filter((d) => d.directlyUsable).length;
24893
26041
  checks.push({
24894
26042
  label: "Linkable credentials",
24895
26043
  ok: true,
24896
- detail: `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt`
26044
+ detail: linkableCredentialsDetail(discovered, config.providers ?? [])
24897
26045
  });
24898
26046
  }
24899
26047
  } catch {
24900
26048
  }
24901
26049
  for (const c of checks) {
24902
- const icon = c.ok ? chalk7__default.default.green(" \u2713") : chalk7__default.default.yellow(" \u25CB");
24903
- const label = c.ok ? chalk7__default.default.white(c.label) : chalk7__default.default.gray(c.label);
24904
- const detail = c.detail ? chalk7__default.default.gray(` \u2014 ${c.detail}`) : "";
26050
+ const icon = c.ok ? chalk8__default.default.green(" \u2713") : chalk8__default.default.yellow(" \u25CB");
26051
+ const label = c.ok ? chalk8__default.default.white(c.label) : chalk8__default.default.gray(c.label);
26052
+ const detail = c.detail ? chalk8__default.default.gray(` \u2014 ${c.detail}`) : "";
24905
26053
  console.log(`${icon} ${label}${detail}`);
24906
26054
  }
24907
26055
  const failures2 = checks.filter((c) => !c.ok);
24908
26056
  console.log();
24909
26057
  if (failures2.length === 0) {
24910
- console.log(chalk7__default.default.green(" All checks passed!\n"));
26058
+ console.log(chalk8__default.default.green(" All checks passed!\n"));
24911
26059
  } else {
24912
- const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
26060
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("credential"));
24913
26061
  if (critical.length) {
24914
- console.log(chalk7__default.default.yellow(` ${critical.length} issue(s) need attention.
26062
+ console.log(chalk8__default.default.yellow(` ${critical.length} issue(s) need attention.
24915
26063
  `));
24916
26064
  } else {
24917
- console.log(chalk7__default.default.gray(` ${failures2.length} optional item(s) not configured.
26065
+ console.log(chalk8__default.default.gray(` ${failures2.length} optional item(s) not configured.
24918
26066
  `));
24919
26067
  }
24920
26068
  }
@@ -24932,19 +26080,19 @@ async function checkEndpoint(url) {
24932
26080
  }
24933
26081
  }
24934
26082
  async function indexCommand(dirPath) {
24935
- const workspace = path34__default.default.resolve(dirPath || process.cwd());
26083
+ const workspace = path35__default.default.resolve(dirPath || process.cwd());
24936
26084
  const cm = new ConfigManager(workspace);
24937
26085
  await cm.load();
24938
26086
  const config = cm.getConfig();
24939
26087
  const embedder = embedderFromProviders(config.providers);
24940
26088
  if (!embedder) {
24941
- console.log(chalk7__default.default.red("\n No embeddings-capable provider configured."));
24942
- console.log(chalk7__default.default.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
26089
+ console.log(chalk8__default.default.red("\n No embeddings-capable provider configured."));
26090
+ console.log(chalk8__default.default.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
24943
26091
  process.exitCode = 1;
24944
26092
  return;
24945
26093
  }
24946
- const dbPath = config.codeIndex?.dbPath || path34__default.default.join(workspace, ".cascade", "code-index.db");
24947
- fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
26094
+ const dbPath = config.codeIndex?.dbPath || path35__default.default.join(workspace, ".cascade", "code-index.db");
26095
+ fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
24948
26096
  const db = new Database2__default.default(dbPath);
24949
26097
  db.pragma("journal_mode = WAL");
24950
26098
  const complete = chatCompleterFromProviders(config.providers);
@@ -24958,20 +26106,20 @@ async function indexCommand(dirPath) {
24958
26106
  reranker,
24959
26107
  isIgnored: (abs) => ignore4.isIgnored(abs, workspace)
24960
26108
  });
24961
- console.log(chalk7__default.default.magenta(`
26109
+ console.log(chalk8__default.default.magenta(`
24962
26110
  \u25C8 Indexing ${workspace}
24963
26111
  `));
24964
26112
  let seen = 0;
24965
26113
  const res = await index.refresh(() => {
24966
26114
  seen++;
24967
- if (seen % 10 === 0) process.stdout.write(chalk7__default.default.dim(` \u2026embedded ${seen} changed files\r`));
26115
+ if (seen % 10 === 0) process.stdout.write(chalk8__default.default.dim(` \u2026embedded ${seen} changed files\r`));
24968
26116
  });
24969
26117
  console.log(
24970
- chalk7__default.default.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk7__default.default.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
26118
+ chalk8__default.default.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk8__default.default.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
24971
26119
  );
24972
- console.log(chalk7__default.default.dim(` Index: ${dbPath}`));
26120
+ console.log(chalk8__default.default.dim(` Index: ${dbPath}`));
24973
26121
  if (!config.codeIndex?.enabled) {
24974
- console.log(chalk7__default.default.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
26122
+ console.log(chalk8__default.default.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
24975
26123
  }
24976
26124
  console.log("");
24977
26125
  db.close();
@@ -25020,6 +26168,7 @@ async function decryptJSON(blob, passphrase) {
25020
26168
  }
25021
26169
 
25022
26170
  // src/cloud/keysync.ts
26171
+ init_revoked_credentials();
25023
26172
  function providerSig(p) {
25024
26173
  return [p.type, p.baseUrl ?? "", p.deploymentName ?? "", p.label ?? ""].join("\0");
25025
26174
  }
@@ -25092,6 +26241,19 @@ function gatherSyncBundle(config) {
25092
26241
  function applySyncBundle(bundle, config, cleanup) {
25093
26242
  const dropped = stripRetiredProviders(bundle);
25094
26243
  if (cleanup) cleanup.removed = dropped.removed;
26244
+ let revokedFromBundle = 0;
26245
+ let unusableFromBundle = 0;
26246
+ if (Array.isArray(bundle.providers)) {
26247
+ const pass = stripRevokedCredentials(bundle.providers, { keepForEndpoint: false });
26248
+ revokedFromBundle = pass.removed;
26249
+ if (pass.removed > 0) bundle.providers = pass.kept;
26250
+ const usable = bundle.providers.filter((p) => {
26251
+ const orphanBearer = Boolean(p.authToken) && !p.apiKey && !p.baseUrl;
26252
+ if (orphanBearer) unusableFromBundle++;
26253
+ return !orphanBearer;
26254
+ });
26255
+ if (unusableFromBundle > 0) bundle.providers = usable;
26256
+ }
25095
26257
  const next = { ...config, tools: { ...config.tools } };
25096
26258
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
25097
26259
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -25109,10 +26271,15 @@ function applySyncBundle(bundle, config, cleanup) {
25109
26271
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
25110
26272
  const merged = { ...next.models };
25111
26273
  const clearedFromMerge = clearRetiredPins(merged);
25112
- if (clearedFromMerge.length > 0) next.models = merged;
26274
+ const clearedRevoked = revokedFromBundle > 0 && !hasUsableAnthropic(next.providers ?? []) ? clearAnthropicPins(merged, next.providers ?? []) : [];
26275
+ if (clearedFromMerge.length > 0 || clearedRevoked.length > 0) {
26276
+ next.models = merged;
26277
+ }
25113
26278
  if (cleanup) {
26279
+ cleanup.revokedCredentials = revokedFromBundle;
26280
+ cleanup.unusableCredentials = unusableFromBundle;
25114
26281
  cleanup.clearedPins = ["t1", "t2", "t3"].filter(
25115
- (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
26282
+ (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t) || clearedRevoked.some((c) => c.tier === t)) && next.models?.[t] === void 0
25116
26283
  );
25117
26284
  }
25118
26285
  if (bundle.prefs) {
@@ -25138,43 +26305,43 @@ function openBrowser(url) {
25138
26305
  }
25139
26306
  }
25140
26307
  function notSignedIn() {
25141
- console.log(chalk7__default.default.dim("\n Not signed in. Run `cascade login` first.\n"));
26308
+ console.log(chalk8__default.default.dim("\n Not signed in. Run `cascade login` first.\n"));
25142
26309
  process.exitCode = 1;
25143
26310
  }
25144
26311
  function promptHidden(question) {
25145
26312
  return new Promise((resolve2) => {
25146
26313
  const rl = readline__default.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
25147
- let masked = false;
26314
+ let masked2 = false;
25148
26315
  rl._writeToOutput = (s) => {
25149
- if (!masked || !process.stdin.isTTY) process.stdout.write(s);
26316
+ if (!masked2 || !process.stdin.isTTY) process.stdout.write(s);
25150
26317
  };
25151
26318
  rl.question(question, (answer) => {
25152
26319
  rl.close();
25153
26320
  process.stdout.write("\n");
25154
26321
  resolve2(answer.trim());
25155
26322
  });
25156
- masked = true;
26323
+ masked2 = true;
25157
26324
  });
25158
26325
  }
25159
26326
  async function loginCommand(opts = {}) {
25160
26327
  const serverUrl = resolveServerUrl(opts.server);
25161
26328
  const client = new CloudClient(serverUrl);
25162
- console.log(chalk7__default.default.magenta("\n \u25C8 Sign in to Cascade Cloud"));
25163
- console.log(chalk7__default.default.dim(` ${serverUrl}
26329
+ console.log(chalk8__default.default.magenta("\n \u25C8 Sign in to Cascade Cloud"));
26330
+ console.log(chalk8__default.default.dim(` ${serverUrl}
25164
26331
  `));
25165
26332
  try {
25166
26333
  const session = await client.runDeviceLogin((d) => {
25167
- console.log(` 1. Open ${chalk7__default.default.cyan(d.verificationUri)}`);
25168
- console.log(` 2. Enter ${chalk7__default.default.bold.yellow(d.userCode)}`);
25169
- console.log(chalk7__default.default.dim("\n Waiting for you to approve\u2026"));
26334
+ console.log(` 1. Open ${chalk8__default.default.cyan(d.verificationUri)}`);
26335
+ console.log(` 2. Enter ${chalk8__default.default.bold.yellow(d.userCode)}`);
26336
+ console.log(chalk8__default.default.dim("\n Waiting for you to approve\u2026"));
25170
26337
  openBrowser(`${d.verificationUri}?code=${encodeURIComponent(d.userCode)}`);
25171
26338
  });
25172
26339
  const who = session.user.name || session.user.email || session.user.id;
25173
- console.log(chalk7__default.default.green(`
26340
+ console.log(chalk8__default.default.green(`
25174
26341
  \u2713 Signed in as ${who}
25175
26342
  `));
25176
26343
  } catch (err) {
25177
- console.log(chalk7__default.default.red(`
26344
+ console.log(chalk8__default.default.red(`
25178
26345
  ${err instanceof Error ? err.message : String(err)}
25179
26346
  `));
25180
26347
  process.exitCode = 1;
@@ -25183,11 +26350,11 @@ async function loginCommand(opts = {}) {
25183
26350
  async function logoutCommand() {
25184
26351
  const client = CloudClient.fromSession();
25185
26352
  if (!client) {
25186
- console.log(chalk7__default.default.dim("\n Not signed in.\n"));
26353
+ console.log(chalk8__default.default.dim("\n Not signed in.\n"));
25187
26354
  return;
25188
26355
  }
25189
26356
  await client.logout();
25190
- console.log(chalk7__default.default.green("\n \u2713 Signed out.\n"));
26357
+ console.log(chalk8__default.default.green("\n \u2713 Signed out.\n"));
25191
26358
  }
25192
26359
  async function whoamiCommand() {
25193
26360
  const client = CloudClient.fromSession();
@@ -25195,10 +26362,10 @@ async function whoamiCommand() {
25195
26362
  try {
25196
26363
  const me = await client.me();
25197
26364
  console.log(`
25198
- ${chalk7__default.default.bold(me.name || "\u2014")} ${chalk7__default.default.dim(me.email || "")}${me.plan ? chalk7__default.default.dim(` \xB7 ${me.plan}`) : ""}
26365
+ ${chalk8__default.default.bold(me.name || "\u2014")} ${chalk8__default.default.dim(me.email || "")}${me.plan ? chalk8__default.default.dim(` \xB7 ${me.plan}`) : ""}
25199
26366
  `);
25200
26367
  } catch (err) {
25201
- console.log(chalk7__default.default.red(`
26368
+ console.log(chalk8__default.default.red(`
25202
26369
  ${err instanceof Error ? err.message : String(err)}
25203
26370
  `));
25204
26371
  process.exitCode = 1;
@@ -25210,16 +26377,16 @@ async function sessionsCommand() {
25210
26377
  try {
25211
26378
  const convos = await client.listConversations();
25212
26379
  if (convos.length === 0) {
25213
- console.log(chalk7__default.default.dim("\n No cloud chats yet.\n"));
26380
+ console.log(chalk8__default.default.dim("\n No cloud chats yet.\n"));
25214
26381
  return;
25215
26382
  }
25216
- console.log(chalk7__default.default.magenta("\n \u25C8 Your cloud chats\n"));
26383
+ console.log(chalk8__default.default.magenta("\n \u25C8 Your cloud chats\n"));
25217
26384
  for (const c of convos) {
25218
- console.log(` ${chalk7__default.default.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk7__default.default.dim("(untitled)")}`);
26385
+ console.log(` ${chalk8__default.default.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk8__default.default.dim("(untitled)")}`);
25219
26386
  }
25220
- console.log(chalk7__default.default.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
26387
+ console.log(chalk8__default.default.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
25221
26388
  } catch (err) {
25222
- console.log(chalk7__default.default.red(`
26389
+ console.log(chalk8__default.default.red(`
25223
26390
  ${err instanceof Error ? err.message : String(err)}
25224
26391
  `));
25225
26392
  process.exitCode = 1;
@@ -25233,9 +26400,9 @@ function formatCloudTranscript(messages) {
25233
26400
  for (const m of messages) {
25234
26401
  const n = m.siblingIds?.length ?? 1;
25235
26402
  const i = m.siblingIds && m.id ? m.siblingIds.indexOf(m.id) + 1 : 1;
25236
- const branch = n > 1 ? chalk7__default.default.yellow(` \u2039${i}/${n}\u203A`) : "";
25237
- const tag = m.id ? chalk7__default.default.dim(` [${shortId2(m.id)}]`) : "";
25238
- const who = m.role === "user" ? chalk7__default.default.bold.cyan("You") : chalk7__default.default.bold.magenta("Cascade");
26403
+ const branch = n > 1 ? chalk8__default.default.yellow(` \u2039${i}/${n}\u203A`) : "";
26404
+ const tag = m.id ? chalk8__default.default.dim(` [${shortId2(m.id)}]`) : "";
26405
+ const who = m.role === "user" ? chalk8__default.default.bold.cyan("You") : chalk8__default.default.bold.magenta("Cascade");
25239
26406
  lines.push(` ${who}${tag}${branch}`);
25240
26407
  lines.push(" " + m.content.replace(/\n/g, "\n ") + "\n");
25241
26408
  }
@@ -25251,23 +26418,23 @@ async function sessionShowCommand(idOrPrefix) {
25251
26418
  try {
25252
26419
  const match = await resolveConversation(client, idOrPrefix);
25253
26420
  if (!match) {
25254
- console.log(chalk7__default.default.red(`
26421
+ console.log(chalk8__default.default.red(`
25255
26422
  No chat matching "${idOrPrefix}".
25256
26423
  `));
25257
26424
  process.exitCode = 1;
25258
26425
  return;
25259
26426
  }
25260
26427
  const msgs = await client.getMessages(match.id);
25261
- console.log(chalk7__default.default.magenta(`
26428
+ console.log(chalk8__default.default.magenta(`
25262
26429
  \u25C8 ${match.title?.trim() || "(untitled)"}
25263
26430
  `));
25264
26431
  console.log(formatCloudTranscript(msgs));
25265
26432
  if (msgs.some((m) => (m.siblingIds?.length ?? 1) > 1)) {
25266
- console.log(chalk7__default.default.dim(" cascade sessions branch <chat> <message> switch to another version"));
26433
+ console.log(chalk8__default.default.dim(" cascade sessions branch <chat> <message> switch to another version"));
25267
26434
  }
25268
- console.log(chalk7__default.default.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
26435
+ console.log(chalk8__default.default.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
25269
26436
  } catch (err) {
25270
- console.log(chalk7__default.default.red(`
26437
+ console.log(chalk8__default.default.red(`
25271
26438
  ${err instanceof Error ? err.message : String(err)}
25272
26439
  `));
25273
26440
  process.exitCode = 1;
@@ -25282,7 +26449,7 @@ async function sessionBranchCommand(chat, message) {
25282
26449
  try {
25283
26450
  const match = await resolveConversation(client, chat);
25284
26451
  if (!match) {
25285
- console.log(chalk7__default.default.red(`
26452
+ console.log(chalk8__default.default.red(`
25286
26453
  No chat matching "${chat}".
25287
26454
  `));
25288
26455
  process.exitCode = 1;
@@ -25291,12 +26458,12 @@ async function sessionBranchCommand(chat, message) {
25291
26458
  const active = await client.getMessages(match.id);
25292
26459
  const targetId = resolveMessage(active, message) ?? message;
25293
26460
  const next = await client.selectBranch(match.id, targetId);
25294
- console.log(chalk7__default.default.magenta(`
26461
+ console.log(chalk8__default.default.magenta(`
25295
26462
  \u25C8 Switched branch \u2014 ${match.title?.trim() || "(untitled)"}
25296
26463
  `));
25297
26464
  console.log(formatCloudTranscript(next));
25298
26465
  } catch (err) {
25299
- console.log(chalk7__default.default.red(`
26466
+ console.log(chalk8__default.default.red(`
25300
26467
  ${err instanceof Error ? err.message : String(err)}
25301
26468
  `));
25302
26469
  process.exitCode = 1;
@@ -25308,7 +26475,7 @@ async function sessionRemoveMessageCommand(chat, message) {
25308
26475
  try {
25309
26476
  const match = await resolveConversation(client, chat);
25310
26477
  if (!match) {
25311
- console.log(chalk7__default.default.red(`
26478
+ console.log(chalk8__default.default.red(`
25312
26479
  No chat matching "${chat}".
25313
26480
  `));
25314
26481
  process.exitCode = 1;
@@ -25317,11 +26484,11 @@ async function sessionRemoveMessageCommand(chat, message) {
25317
26484
  const active = await client.getMessages(match.id);
25318
26485
  const targetId = resolveMessage(active, message) ?? message;
25319
26486
  const next = await client.deleteMessage(match.id, targetId);
25320
- console.log(chalk7__default.default.green("\n \u2713 Deleted that message and its replies.\n"));
26487
+ console.log(chalk8__default.default.green("\n \u2713 Deleted that message and its replies.\n"));
25321
26488
  if (next.length) console.log(formatCloudTranscript(next));
25322
- else console.log(chalk7__default.default.dim(" (the conversation is now empty)\n"));
26489
+ else console.log(chalk8__default.default.dim(" (the conversation is now empty)\n"));
25323
26490
  } catch (err) {
25324
- console.log(chalk7__default.default.red(`
26491
+ console.log(chalk8__default.default.red(`
25325
26492
  ${err instanceof Error ? err.message : String(err)}
25326
26493
  `));
25327
26494
  process.exitCode = 1;
@@ -25333,18 +26500,18 @@ async function sessionRenameCommand(chat, title) {
25333
26500
  try {
25334
26501
  const match = await resolveConversation(client, chat);
25335
26502
  if (!match) {
25336
- console.log(chalk7__default.default.red(`
26503
+ console.log(chalk8__default.default.red(`
25337
26504
  No chat matching "${chat}".
25338
26505
  `));
25339
26506
  process.exitCode = 1;
25340
26507
  return;
25341
26508
  }
25342
26509
  await client.renameConversation(match.id, title);
25343
- console.log(chalk7__default.default.green(`
26510
+ console.log(chalk8__default.default.green(`
25344
26511
  \u2713 Renamed to "${title}".
25345
26512
  `));
25346
26513
  } catch (err) {
25347
- console.log(chalk7__default.default.red(`
26514
+ console.log(chalk8__default.default.red(`
25348
26515
  ${err instanceof Error ? err.message : String(err)}
25349
26516
  `));
25350
26517
  process.exitCode = 1;
@@ -25356,18 +26523,18 @@ async function sessionDeleteCommand(chat) {
25356
26523
  try {
25357
26524
  const match = await resolveConversation(client, chat);
25358
26525
  if (!match) {
25359
- console.log(chalk7__default.default.red(`
26526
+ console.log(chalk8__default.default.red(`
25360
26527
  No chat matching "${chat}".
25361
26528
  `));
25362
26529
  process.exitCode = 1;
25363
26530
  return;
25364
26531
  }
25365
26532
  await client.deleteConversation(match.id);
25366
- console.log(chalk7__default.default.green(`
26533
+ console.log(chalk8__default.default.green(`
25367
26534
  \u2713 Deleted "${match.title?.trim() || "(untitled)"}".
25368
26535
  `));
25369
26536
  } catch (err) {
25370
- console.log(chalk7__default.default.red(`
26537
+ console.log(chalk8__default.default.red(`
25371
26538
  ${err instanceof Error ? err.message : String(err)}
25372
26539
  `));
25373
26540
  process.exitCode = 1;
@@ -25379,21 +26546,21 @@ async function syncPushCommand() {
25379
26546
  const cm = new ConfigManager(process.cwd());
25380
26547
  await cm.load();
25381
26548
  const bundle = gatherSyncBundle(cm.getConfig());
25382
- console.log(chalk7__default.default.magenta("\n \u25C8 Sync your settings to your account"));
25383
- console.log(chalk7__default.default.dim(" Encrypted on this device with a passphrase we never see.\n"));
26549
+ console.log(chalk8__default.default.magenta("\n \u25C8 Sync your settings to your account"));
26550
+ console.log(chalk8__default.default.dim(" Encrypted on this device with a passphrase we never see.\n"));
25384
26551
  const pass = await promptHidden(" Passphrase: ");
25385
26552
  if (!pass) {
25386
- console.log(chalk7__default.default.dim("\n Cancelled.\n"));
26553
+ console.log(chalk8__default.default.dim("\n Cancelled.\n"));
25387
26554
  return;
25388
26555
  }
25389
26556
  try {
25390
26557
  const blob = await encryptJSON(bundle, pass);
25391
26558
  const r = await client.pushSecrets(blob);
25392
- console.log(chalk7__default.default.green(`
26559
+ console.log(chalk8__default.default.green(`
25393
26560
  \u2713 Synced (v${r.version}). Run \`cascade sync pull\` on another device.
25394
26561
  `));
25395
26562
  } catch (err) {
25396
- console.log(chalk7__default.default.red(`
26563
+ console.log(chalk8__default.default.red(`
25397
26564
  ${err instanceof Error ? err.message : String(err)}
25398
26565
  `));
25399
26566
  process.exitCode = 1;
@@ -25406,27 +26573,27 @@ async function syncPullCommand() {
25406
26573
  try {
25407
26574
  ({ blob } = await client.pullSecrets());
25408
26575
  } catch (err) {
25409
- console.log(chalk7__default.default.red(`
26576
+ console.log(chalk8__default.default.red(`
25410
26577
  ${err instanceof Error ? err.message : String(err)}
25411
26578
  `));
25412
26579
  process.exitCode = 1;
25413
26580
  return;
25414
26581
  }
25415
26582
  if (!blob) {
25416
- console.log(chalk7__default.default.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
26583
+ console.log(chalk8__default.default.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
25417
26584
  return;
25418
26585
  }
25419
- console.log(chalk7__default.default.magenta("\n \u25C8 Apply your synced settings to this device\n"));
26586
+ console.log(chalk8__default.default.magenta("\n \u25C8 Apply your synced settings to this device\n"));
25420
26587
  const pass = await promptHidden(" Passphrase: ");
25421
26588
  if (!pass) {
25422
- console.log(chalk7__default.default.dim("\n Cancelled.\n"));
26589
+ console.log(chalk8__default.default.dim("\n Cancelled.\n"));
25423
26590
  return;
25424
26591
  }
25425
26592
  let bundle;
25426
26593
  try {
25427
26594
  bundle = await decryptJSON(blob, pass);
25428
26595
  } catch {
25429
- console.log(chalk7__default.default.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
26596
+ console.log(chalk8__default.default.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
25430
26597
  process.exitCode = 1;
25431
26598
  return;
25432
26599
  }
@@ -25435,13 +26602,13 @@ async function syncPullCommand() {
25435
26602
  await cm.load();
25436
26603
  const cleanup = { removed: [], clearedPins: [] };
25437
26604
  await cm.updateConfig(applySyncBundle(bundle, cm.getConfig(), cleanup));
25438
- console.log(chalk7__default.default.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
26605
+ console.log(chalk8__default.default.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
25439
26606
  if (didCleanupChangeAnything(cleanup)) {
25440
- console.log(chalk7__default.default.yellow(` ${describeCleanup(cleanup)}
26607
+ console.log(chalk8__default.default.yellow(` ${describeCleanup(cleanup)}
25441
26608
  `));
25442
26609
  }
25443
26610
  } catch (err) {
25444
- console.log(chalk7__default.default.red(`
26611
+ console.log(chalk8__default.default.red(`
25445
26612
  Could not apply your synced settings: ${err instanceof Error ? err.message : String(err)}
25446
26613
  `));
25447
26614
  process.exitCode = 1;
@@ -25457,7 +26624,7 @@ function openBrowser2(url) {
25457
26624
  }
25458
26625
  function storePathFor(name) {
25459
26626
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
25460
- return path34__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
26627
+ return path35__default.default.join(os10__default.default.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
25461
26628
  }
25462
26629
  function defaultName(url) {
25463
26630
  try {
@@ -25468,8 +26635,8 @@ function defaultName(url) {
25468
26635
  }
25469
26636
  async function mcpConnectCommand(url, opts = {}) {
25470
26637
  const desiredName = opts.name?.trim() || defaultName(url);
25471
- console.log(chalk7__default.default.magenta("\n \u25C8 Connect an MCP server via OAuth"));
25472
- console.log(chalk7__default.default.dim(` ${url}
26638
+ console.log(chalk8__default.default.magenta("\n \u25C8 Connect an MCP server via OAuth"));
26639
+ console.log(chalk8__default.default.dim(` ${url}
25473
26640
  `));
25474
26641
  try {
25475
26642
  const cm = new ConfigManager(process.cwd());
@@ -25485,7 +26652,7 @@ async function mcpConnectCommand(url, opts = {}) {
25485
26652
  store,
25486
26653
  clientName: "Cascade AI",
25487
26654
  openUrl: (u) => {
25488
- console.log(chalk7__default.default.dim(" Opening your browser to authorize\u2026"));
26655
+ console.log(chalk8__default.default.dim(" Opening your browser to authorize\u2026"));
25489
26656
  openBrowser2(u);
25490
26657
  }
25491
26658
  });
@@ -25495,11 +26662,11 @@ async function mcpConnectCommand(url, opts = {}) {
25495
26662
  config.tools.mcpServers = servers;
25496
26663
  config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
25497
26664
  await cm.updateConfig(config);
25498
- console.log(chalk7__default.default.green(`
26665
+ console.log(chalk8__default.default.green(`
25499
26666
  \u2713 Connected "${name}". Its tools are available to your runs.
25500
26667
  `));
25501
26668
  } catch (err) {
25502
- console.log(chalk7__default.default.red(`
26669
+ console.log(chalk8__default.default.red(`
25503
26670
  ${err instanceof Error ? err.message : String(err)}
25504
26671
  `));
25505
26672
  process.exitCode = 1;
@@ -25510,13 +26677,13 @@ async function mcpListCommand() {
25510
26677
  await cm.load();
25511
26678
  const servers = cm.getConfig().tools?.mcpServers ?? [];
25512
26679
  if (!servers.length) {
25513
- console.log(chalk7__default.default.dim("\n No MCP servers configured.\n"));
26680
+ console.log(chalk8__default.default.dim("\n No MCP servers configured.\n"));
25514
26681
  return;
25515
26682
  }
25516
- console.log(chalk7__default.default.magenta("\n \u25C8 MCP servers\n"));
26683
+ console.log(chalk8__default.default.magenta("\n \u25C8 MCP servers\n"));
25517
26684
  for (const s of servers) {
25518
- const kind = s.oauthStore ? chalk7__default.default.green("oauth") : s.headers ? chalk7__default.default.cyan("token") : s.command ? chalk7__default.default.dim("local") : chalk7__default.default.dim("open");
25519
- console.log(` ${chalk7__default.default.bold(s.name)} ${chalk7__default.default.dim(s.url || s.command || "")} ${kind}`);
26685
+ const kind = s.oauthStore ? chalk8__default.default.green("oauth") : s.headers ? chalk8__default.default.cyan("token") : s.command ? chalk8__default.default.dim("local") : chalk8__default.default.dim("open");
26686
+ console.log(` ${chalk8__default.default.bold(s.name)} ${chalk8__default.default.dim(s.url || s.command || "")} ${kind}`);
25520
26687
  }
25521
26688
  console.log();
25522
26689
  }
@@ -25527,7 +26694,7 @@ async function mcpRemoveCommand(name) {
25527
26694
  const servers = config.tools?.mcpServers ?? [];
25528
26695
  const match = servers.find((s) => s.name === name);
25529
26696
  if (!match) {
25530
- console.log(chalk7__default.default.red(`
26697
+ console.log(chalk8__default.default.red(`
25531
26698
  No MCP server named "${name}".
25532
26699
  `));
25533
26700
  process.exitCode = 1;
@@ -25543,7 +26710,7 @@ async function mcpRemoveCommand(name) {
25543
26710
  }
25544
26711
  }
25545
26712
  await cm.updateConfig(config);
25546
- console.log(chalk7__default.default.green(`
26713
+ console.log(chalk8__default.default.green(`
25547
26714
  \u2713 Removed "${name}".
25548
26715
  `));
25549
26716
  }
@@ -25557,16 +26724,302 @@ async function updateCommand() {
25557
26724
  const { stdout } = await execAsync2("npm show cascade-ai version", { timeout: 1e4 });
25558
26725
  const latest = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
25559
26726
  if (latest === CASCADE_VERSION) {
25560
- spin.succeed(chalk7__default.default.green(`Already up to date (v${CASCADE_VERSION})`));
26727
+ spin.succeed(chalk8__default.default.green(`Already up to date (v${CASCADE_VERSION})`));
25561
26728
  return;
25562
26729
  }
25563
26730
  spin.text = `Updating cascade-ai ${CASCADE_VERSION} \u2192 ${latest}\u2026`;
25564
26731
  await execAsync2("npm install -g cascade-ai@latest", { timeout: 6e4 });
25565
- spin.succeed(chalk7__default.default.green(`Updated to v${latest}! Restart your terminal.`));
26732
+ spin.succeed(chalk8__default.default.green(`Updated to v${latest}! Restart your terminal.`));
26733
+ } catch (err) {
26734
+ spin.fail(chalk8__default.default.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26735
+ }
26736
+ }
26737
+
26738
+ // src/config/credential-write.ts
26739
+ function endpointFromSettingsPayload(endpoints, type) {
26740
+ if (!endpoints || !Object.hasOwn(endpoints, type)) return { kind: "preserve" };
26741
+ const baseUrl = endpoints[type]?.trim();
26742
+ return baseUrl ? { kind: "at", baseUrl } : { kind: "cleared" };
26743
+ }
26744
+ function targetEndpoint(endpoint, existing) {
26745
+ switch (endpoint.kind) {
26746
+ case "at":
26747
+ return endpoint.baseUrl;
26748
+ // An explicit clear, always. Whether the row then falls back to a public
26749
+ // host or becomes unroutable is the provider's business, decided before we
26750
+ // get here.
26751
+ case "cleared":
26752
+ return void 0;
26753
+ case "preserve":
26754
+ return existing?.baseUrl;
26755
+ }
26756
+ }
26757
+ function namesCustomHost(row) {
26758
+ if (!row?.baseUrl) return false;
26759
+ return !sameCredentialEndpoint(row.type, row.baseUrl, void 0);
26760
+ }
26761
+ function applyProviderCredential(providers, type, apiKey, endpoint) {
26762
+ const existing = providers.find((p) => p.type === type);
26763
+ if (endpoint.kind === "cleared" && !hasDefaultEndpoint(type)) {
26764
+ if (existing) {
26765
+ existing.baseUrl = void 0;
26766
+ existing.apiKey = void 0;
26767
+ existing.authToken = void 0;
26768
+ delete existing.local;
26769
+ }
26770
+ return { written: false, reason: "unroutable" };
26771
+ }
26772
+ if (endpoint.kind === "preserve" && namesCustomHost(existing)) {
26773
+ return { written: false, reason: "ambiguous-scope" };
26774
+ }
26775
+ const nextBaseUrl = targetEndpoint(endpoint, existing);
26776
+ if (existing && !sameCredentialEndpoint(type, existing.baseUrl, nextBaseUrl)) {
26777
+ delete existing.local;
26778
+ }
26779
+ if (existing && nextBaseUrl === void 0) existing.baseUrl = void 0;
26780
+ applyProviderApiKey(providers, type, apiKey, nextBaseUrl ? { baseUrl: nextBaseUrl } : {});
26781
+ return { written: true };
26782
+ }
26783
+ function credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) {
26784
+ if (replacementKey) return "replaced";
26785
+ return sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl) ? "keep" : "clear";
26786
+ }
26787
+ function applyEndpointEdit(existing, nextBaseUrl, replacementKey) {
26788
+ if (credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) === "clear") {
26789
+ existing.apiKey = void 0;
26790
+ existing.authToken = void 0;
26791
+ }
26792
+ if (!sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl)) {
26793
+ delete existing.local;
26794
+ }
26795
+ existing.baseUrl = nextBaseUrl || void 0;
26796
+ }
26797
+ function explainRefusal(type, reason) {
26798
+ 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.`;
26799
+ }
26800
+ function applySettingsCredentials(providers, data) {
26801
+ const refused = [];
26802
+ if (data.keys) {
26803
+ for (const [type, apiKey] of Object.entries(data.keys)) {
26804
+ if (!apiKey) continue;
26805
+ const outcome = applyProviderCredential(
26806
+ providers,
26807
+ type,
26808
+ apiKey,
26809
+ endpointFromSettingsPayload(data.endpoints, type)
26810
+ );
26811
+ if (!outcome.written) refused.push({ type, reason: outcome.reason });
26812
+ }
26813
+ }
26814
+ if (!data.endpoints) return { refused };
26815
+ for (const [type, baseUrl] of Object.entries(data.endpoints)) {
26816
+ if (type === "azure") continue;
26817
+ if (!Object.hasOwn(data.endpoints, type)) continue;
26818
+ const existing = providers.find((p) => p.type === type);
26819
+ if (!existing) {
26820
+ if (baseUrl) providers.push({ type, baseUrl });
26821
+ continue;
26822
+ }
26823
+ applyEndpointEdit(existing, baseUrl, data.keys?.[type]);
26824
+ }
26825
+ return { refused };
26826
+ }
26827
+
26828
+ // src/config/settings-payload.ts
26829
+ function priorAzureRow(prior, incoming) {
26830
+ if (!incoming.deploymentName) return void 0;
26831
+ return prior.find((p) => p.deploymentName === incoming.deploymentName && sameAzureEndpoint(p.baseUrl, incoming.baseUrl));
26832
+ }
26833
+ function applySettingsPayload(config, data) {
26834
+ if (!Array.isArray(config.providers)) config.providers = [];
26835
+ const result = applySettingsCredentials(config.providers, data);
26836
+ if (Array.isArray(data.azureDeployments)) {
26837
+ const priorAzure = config.providers.filter((p) => p.type === "azure");
26838
+ const nonAzure = config.providers.filter((p) => p.type !== "azure");
26839
+ const nextAzure = data.azureDeployments.map((d) => {
26840
+ const prior = priorAzureRow(priorAzure, d);
26841
+ const apiKey = d.apiKey ? d.apiKey : prior?.apiKey;
26842
+ const model = d.model ?? prior?.model;
26843
+ const region = d.region ?? prior?.region;
26844
+ return {
26845
+ type: "azure",
26846
+ ...d.label ? { label: d.label } : {},
26847
+ ...d.baseUrl ? { baseUrl: d.baseUrl } : {},
26848
+ ...d.deploymentName ? { deploymentName: d.deploymentName } : {},
26849
+ ...d.apiVersion ? { apiVersion: d.apiVersion } : {},
26850
+ ...apiKey ? { apiKey } : {},
26851
+ ...model ? { model } : {},
26852
+ ...region ? { region } : {}
26853
+ };
26854
+ }).filter((d) => d.apiKey || d.baseUrl || d.deploymentName);
26855
+ config.providers = [...nonAzure, ...nextAzure];
26856
+ }
26857
+ if (data.models) {
26858
+ const models = config.models ?? {};
26859
+ for (const [tier, val] of Object.entries(data.models)) {
26860
+ if (val && val !== "auto") models[tier] = val;
26861
+ else delete models[tier];
26862
+ }
26863
+ config.models = models;
26864
+ }
26865
+ if (data.budget) {
26866
+ const b = data.budget;
26867
+ config.budget = config.budget ?? {};
26868
+ const cap = (key, target, accept, round = false) => {
26869
+ if (!Object.hasOwn(b, key)) return;
26870
+ const v = b[key];
26871
+ if (typeof v === "number" && accept(v)) config.budget[target] = round ? Math.floor(v) : v;
26872
+ else delete config.budget[target];
26873
+ };
26874
+ cap("maxCostPerRun", "maxCostPerRunUsd", (n) => n > 0);
26875
+ cap("dailyBudgetUsd", "dailyBudgetUsd", (n) => n > 0);
26876
+ cap("sessionBudgetUsd", "sessionBudgetUsd", (n) => n > 0);
26877
+ cap("maxTokensPerRun", "maxTokensPerRun", (n) => n > 0, true);
26878
+ cap("warnAtPct", "warnAtPct", (n) => n > 0 && n <= 100);
26879
+ if (b.autoBias === "balanced" || b.autoBias === "quality" || b.autoBias === "cost") {
26880
+ config.autoBias = b.autoBias;
26881
+ }
26882
+ }
26883
+ if (data.webSearch && typeof data.webSearch === "object") {
26884
+ config.tools = config.tools ?? {};
26885
+ const prior = config.tools.webSearch ?? {};
26886
+ const next = { ...prior };
26887
+ if (typeof data.webSearch.searxngUrl === "string") next.searxngUrl = data.webSearch.searxngUrl.trim() || void 0;
26888
+ if (data.webSearch.braveApiKey) next.braveApiKey = data.webSearch.braveApiKey;
26889
+ if (data.webSearch.tavilyApiKey) next.tavilyApiKey = data.webSearch.tavilyApiKey;
26890
+ config.tools.webSearch = next;
26891
+ }
26892
+ if (data.advanced && typeof data.advanced === "object") {
26893
+ const a = data.advanced;
26894
+ const num = (v, min, max) => {
26895
+ const n = Number(v);
26896
+ return Number.isFinite(n) && n >= min && n <= max ? n : void 0;
26897
+ };
26898
+ if (a["autonomy"] === "manual" || a["autonomy"] === "auto") config.autonomy = a["autonomy"];
26899
+ if (["never", "complex", "all", "always"].includes(a["planApproval"])) config.planApproval = a["planApproval"];
26900
+ {
26901
+ const n = num(a["approvalTimeoutMs"], 1e3, 864e5);
26902
+ if (n !== void 0) config.approvalTimeoutMs = Math.floor(n);
26903
+ }
26904
+ if (["auto", "parallel", "sequential"].includes(a["t3Execution"])) config.t3Execution = a["t3Execution"];
26905
+ {
26906
+ const n = num(a["localConcurrency"], 1, 16);
26907
+ if (n !== void 0) config.localConcurrency = Math.floor(n);
26908
+ }
26909
+ {
26910
+ const n = num(a["localInferenceTimeoutMs"], 1e3, 36e5);
26911
+ if (n !== void 0) config.localInferenceTimeoutMs = Math.floor(n);
26912
+ }
26913
+ {
26914
+ const n = num(a["cloudInferenceTimeoutMs"], 1e3, 36e5);
26915
+ if (n !== void 0) config.cloudInferenceTimeoutMs = Math.floor(n);
26916
+ }
26917
+ if (typeof a["reflectionEnabled"] === "boolean") config.reflection = { ...config.reflection ?? {}, enabled: a["reflectionEnabled"] };
26918
+ if (typeof a["cascadeAuto"] === "boolean") config.cascadeAuto = a["cascadeAuto"];
26919
+ if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
26920
+ if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
26921
+ if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
26922
+ if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
26923
+ if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
26924
+ if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
26925
+ if (typeof a["persistDynamicTools"] === "boolean") config.persistDynamicTools = a["persistDynamicTools"];
26926
+ if (typeof a["telemetryEnabled"] === "boolean") config.telemetry = { ...config.telemetry ?? {}, enabled: a["telemetryEnabled"] };
26927
+ }
26928
+ return result;
26929
+ }
26930
+ function settingsSnapshot(config) {
26931
+ const providers = config.providers ?? [];
26932
+ const endpoints = {};
26933
+ for (const p of providers) {
26934
+ if (p?.type && p?.baseUrl && p.type !== "azure") endpoints[p.type] = p.baseUrl;
26935
+ }
26936
+ const ws = config.tools?.webSearch ?? {};
26937
+ return {
26938
+ models: config.models ?? {},
26939
+ budget: {
26940
+ maxCostPerRun: config.budget?.maxCostPerRunUsd,
26941
+ autoBias: config.autoBias,
26942
+ dailyBudgetUsd: config.budget?.dailyBudgetUsd,
26943
+ sessionBudgetUsd: config.budget?.sessionBudgetUsd,
26944
+ maxTokensPerRun: config.budget?.maxTokensPerRun,
26945
+ warnAtPct: config.budget?.warnAtPct
26946
+ },
26947
+ // Through the shared predicate: `authToken` is a credential too, and
26948
+ // counting only `apiKey` showed a bearer-configured provider as unconfigured.
26949
+ providersWithKey: providers.filter((p) => hasProviderCredential(p)).map((p) => p.type),
26950
+ endpoints,
26951
+ azureDeployments: providers.filter((p) => p.type === "azure").map((p) => ({
26952
+ label: p.label,
26953
+ baseUrl: p.baseUrl,
26954
+ deploymentName: p.deploymentName,
26955
+ apiVersion: p.apiVersion,
26956
+ // Surfaced so a panel that round-trips the snapshot cannot drop them.
26957
+ model: p.model,
26958
+ region: p.region,
26959
+ hasKey: typeof p.apiKey === "string" && p.apiKey.length > 0
26960
+ })),
26961
+ webSearch: {
26962
+ searxngUrl: ws.searxngUrl,
26963
+ hasBraveKey: typeof ws.braveApiKey === "string" && ws.braveApiKey.length > 0,
26964
+ hasTavilyKey: typeof ws.tavilyApiKey === "string" && ws.tavilyApiKey.length > 0
26965
+ },
26966
+ advanced: {
26967
+ autonomy: config.autonomy,
26968
+ planApproval: config.planApproval,
26969
+ approvalTimeoutMs: config.approvalTimeoutMs,
26970
+ t3Execution: config.t3Execution,
26971
+ localConcurrency: config.localConcurrency,
26972
+ localInferenceTimeoutMs: config.localInferenceTimeoutMs,
26973
+ cloudInferenceTimeoutMs: config.cloudInferenceTimeoutMs,
26974
+ reflectionEnabled: config.reflection?.enabled,
26975
+ cascadeAuto: config.cascadeAuto,
26976
+ forceTier: config.routing?.forceTier,
26977
+ benchmarksLive: config.benchmarks?.live,
26978
+ dynamicToolSandbox: config.tools?.dynamicToolSandbox,
26979
+ factsExtraction: config.knowledge?.factsExtraction,
26980
+ rememberSessions: config.memory?.rememberSessions,
26981
+ enableToolCreation: config.enableToolCreation,
26982
+ persistDynamicTools: config.persistDynamicTools,
26983
+ telemetryEnabled: config.telemetry?.enabled
26984
+ }
26985
+ };
26986
+ }
26987
+ async function commitSettings(live, data, write) {
26988
+ const staged = structuredClone(live);
26989
+ const { refused } = applySettingsPayload(staged, data);
26990
+ let committed;
26991
+ try {
26992
+ committed = validateConfig(staged);
25566
26993
  } catch (err) {
25567
- spin.fail(chalk7__default.default.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26994
+ return {
26995
+ ok: false,
26996
+ refused,
26997
+ error: `Those settings are not valid: ${err instanceof Error ? err.message : String(err)}`
26998
+ };
26999
+ }
27000
+ const written = await write(committed);
27001
+ if (!written.ok) {
27002
+ return { ok: false, refused, error: `Could not write the config file: ${written.error}` };
25568
27003
  }
27004
+ replaceInPlace(live, committed);
27005
+ return { ok: true, refused };
25569
27006
  }
27007
+ function replaceInPlace(target, source) {
27008
+ const t = target;
27009
+ for (const key of Object.keys(t)) delete t[key];
27010
+ Object.assign(t, structuredClone(source));
27011
+ }
27012
+ function writeConfigFile(configPath, config) {
27013
+ try {
27014
+ fs32__default.default.mkdirSync(path35__default.default.dirname(configPath), { recursive: true });
27015
+ fs32__default.default.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
27016
+ return { ok: true };
27017
+ } catch (err) {
27018
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
27019
+ }
27020
+ }
27021
+
27022
+ // src/dashboard/server.ts
25570
27023
  init_constants();
25571
27024
  var JWT_ALGORITHM = "HS256";
25572
27025
  function createToken(user, secret) {
@@ -25814,12 +27267,22 @@ var DashboardSocket = class {
25814
27267
  });
25815
27268
  });
25816
27269
  }
27270
+ /**
27271
+ * The handler's result is ACKNOWLEDGED to the caller.
27272
+ *
27273
+ * A settings save can be partly refused — a key whose host cannot be
27274
+ * determined is deliberately not stored — and this path had no way to say so,
27275
+ * so the panel cleared the input and reported success over a key the backend
27276
+ * had dropped. Socket.IO's own ack channel carries it back, which needs no
27277
+ * new event name and cannot be missed by a client that forgot to subscribe.
27278
+ */
25817
27279
  onConfigUpdate(callback) {
25818
27280
  this.io.on("connection", (socket) => {
25819
- socket.on("config:update", (payload) => {
25820
- if (typeof payload === "object" && payload !== null) {
25821
- callback(payload);
25822
- }
27281
+ socket.on("config:update", (payload, ack) => {
27282
+ if (typeof payload !== "object" || payload === null) return;
27283
+ void callback(payload).then((result) => {
27284
+ if (typeof ack === "function") ack(result);
27285
+ });
25823
27286
  });
25824
27287
  });
25825
27288
  }
@@ -25965,8 +27428,47 @@ function aggregateCostStats(sessions2, opts = {}) {
25965
27428
  }
25966
27429
 
25967
27430
  // src/dashboard/server.ts
25968
- var __dirname$1 = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
27431
+ var __dirname$1 = path35__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
25969
27432
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
27433
+ var MASK = "***";
27434
+ function masked(key, value) {
27435
+ return { [key]: value ? MASK : void 0 };
27436
+ }
27437
+ function redactProviderSecrets(config) {
27438
+ const safe = { ...config };
27439
+ safe.providers = (config.providers ?? []).map((p) => ({
27440
+ ...p,
27441
+ ...masked("apiKey", p.apiKey),
27442
+ ...masked("authToken", p.authToken)
27443
+ }));
27444
+ if (config.tools) {
27445
+ safe.tools = { ...config.tools };
27446
+ if (config.tools.webSearch) {
27447
+ safe.tools.webSearch = {
27448
+ ...config.tools.webSearch,
27449
+ ...masked("braveApiKey", config.tools.webSearch.braveApiKey),
27450
+ ...masked("tavilyApiKey", config.tools.webSearch.tavilyApiKey)
27451
+ };
27452
+ }
27453
+ if (config.tools.mcpServers) {
27454
+ safe.tools.mcpServers = config.tools.mcpServers.map((server) => ({
27455
+ ...server,
27456
+ ...server.headers ? { headers: Object.fromEntries(Object.keys(server.headers).map((k) => [k, MASK])) } : {},
27457
+ ...server.env ? { env: Object.fromEntries(Object.keys(server.env).map((k) => [k, MASK])) } : {}
27458
+ }));
27459
+ }
27460
+ }
27461
+ if (config.dashboard) {
27462
+ safe.dashboard = { ...config.dashboard, ...masked("secret", config.dashboard.secret) };
27463
+ }
27464
+ if (config.telemetry) {
27465
+ safe.telemetry = {
27466
+ ...config.telemetry,
27467
+ ...masked("posthogApiKey", config.telemetry.posthogApiKey)
27468
+ };
27469
+ }
27470
+ return safe;
27471
+ }
25970
27472
  var DashboardServer = class {
25971
27473
  app;
25972
27474
  httpServer;
@@ -26041,40 +27543,21 @@ var DashboardServer = class {
26041
27543
  this.activeSessions.get(sessionId)?.rateLastRun(rating);
26042
27544
  });
26043
27545
  this.socket.onConfigGet((socketId) => {
26044
- this.socket.emitToSocket(socketId, "config:current", {
26045
- models: this.config.models ?? {},
26046
- budget: {
26047
- maxCostPerRun: this.config.budget?.maxCostPerRunUsd,
26048
- autoBias: this.config.autoBias
26049
- },
26050
- providersWithKey: (this.config.providers ?? []).filter((p) => typeof p.apiKey === "string" && p.apiKey.length > 0).map((p) => p.type)
26051
- });
27546
+ this.socket.emitToSocket(socketId, "config:current", settingsSnapshot(this.config));
26052
27547
  });
26053
- this.socket.onConfigUpdate((data) => {
26054
- if (data.keys) {
26055
- for (const [type, apiKey] of Object.entries(data.keys)) {
26056
- if (!apiKey) continue;
26057
- const provider = this.config.providers.find((p) => p.type === type);
26058
- if (provider) provider.apiKey = apiKey;
26059
- else this.config.providers.push({ type, apiKey });
26060
- }
26061
- }
26062
- if (data.models) {
26063
- const models = this.config.models;
26064
- for (const [tier, val] of Object.entries(data.models)) {
26065
- if (val && val !== "auto") models[tier] = val;
26066
- else delete models[tier];
26067
- }
26068
- }
26069
- if (data.budget) {
26070
- if (typeof data.budget.maxCostPerRun === "number") {
26071
- this.config.budget.maxCostPerRunUsd = data.budget.maxCostPerRun;
26072
- }
26073
- if (data.budget.autoBias === "balanced" || data.budget.autoBias === "quality" || data.budget.autoBias === "cost") {
26074
- this.config.autoBias = data.budget.autoBias;
26075
- }
27548
+ this.socket.onConfigUpdate(async (data) => {
27549
+ const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
27550
+ const refused = result.refused.map((r) => ({
27551
+ type: r.type,
27552
+ reason: r.reason,
27553
+ message: explainRefusal(r.type, r.reason)
27554
+ }));
27555
+ for (const r of refused) console.warn(r.message);
27556
+ if (!result.ok) {
27557
+ return { refused, snapshot: settingsSnapshot(this.config), error: result.error };
26076
27558
  }
26077
- this.persistConfig();
27559
+ this.syncGlobalCredentials();
27560
+ return { refused, snapshot: settingsSnapshot(this.config) };
26078
27561
  });
26079
27562
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
26080
27563
  const sessionId = requestedSessionId ?? crypto.randomUUID();
@@ -26243,16 +27726,47 @@ var DashboardServer = class {
26243
27726
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
26244
27727
  * a write failure is logged but never crashes the running dashboard.
26245
27728
  */
26246
- persistConfig() {
26247
- try {
26248
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
26249
- fs31__default.default.mkdirSync(path34__default.default.dirname(configPath), { recursive: true });
26250
- fs31__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
26251
- } catch (err) {
26252
- console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
26253
- }
27729
+ /**
27730
+ * Returns the durability of the WORKSPACE write, because a caller
27731
+ * acknowledging a save has to know it happened.
27732
+ *
27733
+ * This was `void` and swallowed every filesystem error, so a read-only
27734
+ * config path or a full disk produced: the live object mutated, the write
27735
+ * failed, the socket acknowledged success, and the panel cleared the keys the
27736
+ * user had typed. The changes then vanished at the next restart. "The handler
27737
+ * ran" is not "the save is durable".
27738
+ *
27739
+ * The global-credential sync below is genuinely best-effort — it is a
27740
+ * convenience copy, and failing it does not lose what the user just entered.
27741
+ */
27742
+ /**
27743
+ * @param config REQUIRED, with no default. `commitSettings` writes the
27744
+ * validated copy BEFORE adopting it, so a writer that reached for
27745
+ * `this.config` instead would persist the configuration being replaced — and
27746
+ * a default parameter is exactly the affordance that lets that happen
27747
+ * silently and still typecheck.
27748
+ */
27749
+ persistConfig(config) {
27750
+ const result = writeConfigFile(path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
27751
+ if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
27752
+ return result;
27753
+ }
27754
+ /**
27755
+ * Copy provider credentials to the machine-global store, so keys saved over
27756
+ * the socket survive a workspace switch — the same sync `ConfigManager.save()`
27757
+ * performs.
27758
+ *
27759
+ * Separate from the workspace write, and called only AFTER it succeeds. It
27760
+ * used to run unconditionally inside `persistConfig()`, so a failed workspace
27761
+ * write still pushed the provider half of a rejected save into the global
27762
+ * store: half the change persisted, behind an error saying none of it had.
27763
+ *
27764
+ * Best-effort on its own account: this is a convenience copy, and failing it
27765
+ * does not lose what the user just entered.
27766
+ */
27767
+ syncGlobalCredentials() {
26254
27768
  try {
26255
- saveGlobalCredentials(path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
27769
+ saveGlobalCredentials(path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
26256
27770
  } catch (err) {
26257
27771
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
26258
27772
  }
@@ -26267,15 +27781,15 @@ var DashboardServer = class {
26267
27781
  resolveDashboardSecret() {
26268
27782
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
26269
27783
  if (fromConfig) return fromConfig;
26270
- const secretPath = path34__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
27784
+ const secretPath = path35__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
26271
27785
  try {
26272
- if (fs31__default.default.existsSync(secretPath)) {
26273
- const existing = fs31__default.default.readFileSync(secretPath, "utf-8").trim();
27786
+ if (fs32__default.default.existsSync(secretPath)) {
27787
+ const existing = fs32__default.default.readFileSync(secretPath, "utf-8").trim();
26274
27788
  if (existing.length >= 16) return existing;
26275
27789
  }
26276
27790
  const generated = crypto.randomUUID();
26277
- fs31__default.default.mkdirSync(path34__default.default.dirname(secretPath), { recursive: true });
26278
- fs31__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
27791
+ fs32__default.default.mkdirSync(path35__default.default.dirname(secretPath), { recursive: true });
27792
+ fs32__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
26279
27793
  if (this.config.dashboard.auth) {
26280
27794
  console.warn(
26281
27795
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -26302,7 +27816,7 @@ var DashboardServer = class {
26302
27816
  // ── Setup ─────────────────────────────────────
26303
27817
  getGlobalStore() {
26304
27818
  if (!this.globalStore) {
26305
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
27819
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26306
27820
  this.globalStore = new MemoryStore(globalDbPath);
26307
27821
  }
26308
27822
  return this.globalStore;
@@ -26682,12 +28196,12 @@ ${prompt}`;
26682
28196
  }
26683
28197
  }
26684
28198
  watchRuntimeChanges() {
26685
- const workspaceDbPath = path34__default.default.join(this.workspacePath, CASCADE_DB_FILE);
26686
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28199
+ const workspaceDbPath = path35__default.default.join(this.workspacePath, CASCADE_DB_FILE);
28200
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26687
28201
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
26688
28202
  for (const watchPath of watchPaths) {
26689
- if (!fs31__default.default.existsSync(watchPath)) continue;
26690
- fs31__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
28203
+ if (!fs32__default.default.existsSync(watchPath)) continue;
28204
+ fs32__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
26691
28205
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
26692
28206
  });
26693
28207
  }
@@ -26809,7 +28323,7 @@ ${prompt}`;
26809
28323
  const sessionId = req.params.id;
26810
28324
  this.store.deleteSession(sessionId);
26811
28325
  this.store.deleteRuntimeSession(sessionId);
26812
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28326
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26813
28327
  const globalStore = new MemoryStore(globalDbPath);
26814
28328
  try {
26815
28329
  globalStore.deleteRuntimeSession(sessionId);
@@ -26902,7 +28416,7 @@ ${prompt}`;
26902
28416
  });
26903
28417
  this.app.delete("/api/sessions", auth2, (req, res) => {
26904
28418
  const body = req.body;
26905
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28419
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26906
28420
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
26907
28421
  const globalStore = new MemoryStore(globalDbPath);
26908
28422
  try {
@@ -26925,7 +28439,7 @@ ${prompt}`;
26925
28439
  });
26926
28440
  this.app.delete("/api/runtime", auth2, (_req, res) => {
26927
28441
  this.store.deleteAllRuntimeNodes();
26928
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28442
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26929
28443
  const globalStore = new MemoryStore(globalDbPath);
26930
28444
  try {
26931
28445
  globalStore.deleteAllRuntimeNodes();
@@ -26994,8 +28508,7 @@ ${prompt}`;
26994
28508
  res.json(log);
26995
28509
  });
26996
28510
  this.app.get("/api/config", auth2, (_req, res) => {
26997
- const safe = { ...this.config };
26998
- safe.providers = safe.providers.map((p) => ({ ...p, apiKey: p.apiKey ? "***" : void 0 }));
28511
+ const safe = redactProviderSecrets(this.config);
26999
28512
  res.json(safe);
27000
28513
  });
27001
28514
  this.app.put("/api/config", auth2, async (req, res) => {
@@ -27011,12 +28524,12 @@ ${prompt}`;
27011
28524
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
27012
28525
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
27013
28526
  try {
27014
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
27015
- const existing = fs31__default.default.existsSync(configPath) ? JSON.parse(fs31__default.default.readFileSync(configPath, "utf-8")) : {};
28527
+ const configPath = path35__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
28528
+ const existing = fs32__default.default.existsSync(configPath) ? JSON.parse(fs32__default.default.readFileSync(configPath, "utf-8")) : {};
27016
28529
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
27017
28530
  const tmp = configPath + ".tmp";
27018
- fs31__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
27019
- fs31__default.default.renameSync(tmp, configPath);
28531
+ fs32__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
28532
+ fs32__default.default.renameSync(tmp, configPath);
27020
28533
  res.json({ ok: true });
27021
28534
  } catch (err) {
27022
28535
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -27044,7 +28557,7 @@ ${prompt}`;
27044
28557
  this.app.get("/api/runtime", auth2, (req, res) => {
27045
28558
  const scope = req.query["scope"] ?? "workspace";
27046
28559
  if (scope === "global") {
27047
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28560
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
27048
28561
  const globalStore = new MemoryStore(globalDbPath);
27049
28562
  try {
27050
28563
  res.json({
@@ -27385,13 +28898,13 @@ ${prompt}`;
27385
28898
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
27386
28899
  }
27387
28900
  });
27388
- const prodPath = path34__default.default.resolve(__dirname$1, "../web/dist");
27389
- const devPath = path34__default.default.resolve(__dirname$1, "../../web/dist");
27390
- const webDistPath = fs31__default.default.existsSync(prodPath) ? prodPath : devPath;
27391
- if (fs31__default.default.existsSync(webDistPath)) {
28901
+ const prodPath = path35__default.default.resolve(__dirname$1, "../web/dist");
28902
+ const devPath = path35__default.default.resolve(__dirname$1, "../../web/dist");
28903
+ const webDistPath = fs32__default.default.existsSync(prodPath) ? prodPath : devPath;
28904
+ if (fs32__default.default.existsSync(webDistPath)) {
27392
28905
  this.app.use(express__default.default.static(webDistPath));
27393
28906
  this.app.get("*", (_req, res) => {
27394
- res.sendFile(path34__default.default.join(webDistPath, "index.html"));
28907
+ res.sendFile(path35__default.default.join(webDistPath, "index.html"));
27395
28908
  });
27396
28909
  } else {
27397
28910
  this.app.get("/", (_req, res) => {
@@ -27412,7 +28925,7 @@ init_constants();
27412
28925
  async function dashboardCommand(config, workspacePath = process.cwd()) {
27413
28926
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
27414
28927
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
27415
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
28928
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
27416
28929
  const server = new DashboardServer(config, store, workspacePath);
27417
28930
  server.watchRuntimeChanges();
27418
28931
  const gThis = globalThis;
@@ -27424,10 +28937,10 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27424
28937
  process.once("exit", onExit);
27425
28938
  try {
27426
28939
  await server.start();
27427
- spin.succeed(chalk7__default.default.green(`Dashboard running at http://localhost:${port}`));
28940
+ spin.succeed(chalk8__default.default.green(`Dashboard running at http://localhost:${port}`));
27428
28941
  server.refreshRuntime("workspace");
27429
28942
  server.refreshRuntime("global");
27430
- console.log(chalk7__default.default.gray(` Press Ctrl+C to stop
28943
+ console.log(chalk8__default.default.gray(` Press Ctrl+C to stop
27431
28944
  `));
27432
28945
  await new Promise(() => {
27433
28946
  });
@@ -27436,7 +28949,7 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27436
28949
  await server.stop().catch(() => {
27437
28950
  });
27438
28951
  onExit();
27439
- spin.fail(chalk7__default.default.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
28952
+ spin.fail(chalk8__default.default.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
27440
28953
  process.exit(1);
27441
28954
  }
27442
28955
  }
@@ -27444,18 +28957,18 @@ init_constants();
27444
28957
  function makeIdentityCommand(workspacePath = process.cwd()) {
27445
28958
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
27446
28959
  identity.command("list").description("List all available identities").action(() => {
27447
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
28960
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
27448
28961
  try {
27449
28962
  const identities = store.listIdentities();
27450
- console.log(chalk7__default.default.bold("\n Identities:"));
28963
+ console.log(chalk8__default.default.bold("\n Identities:"));
27451
28964
  if (identities.length === 0) {
27452
- console.log(chalk7__default.default.gray(" No identities found."));
28965
+ console.log(chalk8__default.default.gray(" No identities found."));
27453
28966
  } else {
27454
28967
  identities.forEach((id) => {
27455
- const defaultLabel = id.isDefault ? chalk7__default.default.green(" [Default]") : "";
27456
- console.log(` - ${chalk7__default.default.cyan(id.name)}${defaultLabel}`);
27457
- console.log(chalk7__default.default.gray(` ID: ${id.id}`));
27458
- if (id.description) console.log(chalk7__default.default.gray(` ${id.description}`));
28968
+ const defaultLabel = id.isDefault ? chalk8__default.default.green(" [Default]") : "";
28969
+ console.log(` - ${chalk8__default.default.cyan(id.name)}${defaultLabel}`);
28970
+ console.log(chalk8__default.default.gray(` ID: ${id.id}`));
28971
+ if (id.description) console.log(chalk8__default.default.gray(` ${id.description}`));
27459
28972
  });
27460
28973
  }
27461
28974
  console.log();
@@ -27464,7 +28977,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27464
28977
  }
27465
28978
  });
27466
28979
  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) => {
27467
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
28980
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
27468
28981
  try {
27469
28982
  if (options.default) {
27470
28983
  const existingDefault = store.getDefaultIdentity();
@@ -27481,21 +28994,21 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27481
28994
  isDefault: !!options.default,
27482
28995
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
27483
28996
  });
27484
- console.log(chalk7__default.default.green(`
28997
+ console.log(chalk8__default.default.green(`
27485
28998
  Successfully created identity: ${name} (${id})`));
27486
- if (options.default) console.log(chalk7__default.default.green(" Set as default."));
28999
+ if (options.default) console.log(chalk8__default.default.green(" Set as default."));
27487
29000
  console.log();
27488
29001
  } finally {
27489
29002
  store.close();
27490
29003
  }
27491
29004
  });
27492
29005
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
27493
- const store = new MemoryStore(path34__default.default.join(workspacePath, CASCADE_DB_FILE));
29006
+ const store = new MemoryStore(path35__default.default.join(workspacePath, CASCADE_DB_FILE));
27494
29007
  try {
27495
29008
  const identities = store.listIdentities();
27496
29009
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
27497
29010
  if (!match) {
27498
- console.error(chalk7__default.default.red(`
29011
+ console.error(chalk8__default.default.red(`
27499
29012
  Identity '${query}' not found.
27500
29013
  `));
27501
29014
  process.exit(1);
@@ -27505,7 +29018,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27505
29018
  store.updateIdentity(existingDefault.id, { isDefault: false });
27506
29019
  }
27507
29020
  store.updateIdentity(match.id, { isDefault: true });
27508
- console.log(chalk7__default.default.green(`
29021
+ console.log(chalk8__default.default.green(`
27509
29022
  Identity ${match.name} is now the default.
27510
29023
  `));
27511
29024
  } finally {
@@ -27516,7 +29029,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27516
29029
  }
27517
29030
  init_pricing();
27518
29031
  async function modelsCommand(options = {}) {
27519
- console.log(chalk7__default.default.magenta("\n \u25C8 Cascade Models\n"));
29032
+ console.log(chalk8__default.default.magenta("\n \u25C8 Cascade Models\n"));
27520
29033
  const cm = new ConfigManager(process.cwd());
27521
29034
  await cm.load();
27522
29035
  const config = cm.getConfig();
@@ -27524,16 +29037,16 @@ async function modelsCommand(options = {}) {
27524
29037
  try {
27525
29038
  await router.init(config);
27526
29039
  } catch (err) {
27527
- console.error(chalk7__default.default.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
29040
+ console.error(chalk8__default.default.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
27528
29041
  process.exit(1);
27529
29042
  }
27530
29043
  await withTimeout(router.refreshLiveData(), 6e3, "live data timeout").catch(() => {
27531
29044
  });
27532
29045
  const liveData = router.getLiveData();
27533
29046
  const tiers = [
27534
- { tier: "T1", label: "T1 Administrator", color: chalk7__default.default.hex("#4C8DFF") },
27535
- { tier: "T2", label: "T2 Manager", color: chalk7__default.default.hex("#38B0DE") },
27536
- { tier: "T3", label: "T3 Worker", color: chalk7__default.default.hex("#2DD4BF") }
29047
+ { tier: "T1", label: "T1 Administrator", color: chalk8__default.default.hex("#4C8DFF") },
29048
+ { tier: "T2", label: "T2 Manager", color: chalk8__default.default.hex("#38B0DE") },
29049
+ { tier: "T3", label: "T3 Worker", color: chalk8__default.default.hex("#2DD4BF") }
27537
29050
  ];
27538
29051
  let anyMissing = false;
27539
29052
  let anyUnpriced = false;
@@ -27545,76 +29058,76 @@ async function modelsCommand(options = {}) {
27545
29058
  const costIn = unpriced ? "cost not tracked" : model.inputCostPer1kTokens === 0 ? "free" : `$${model.inputCostPer1kTokens.toFixed(4)}/1K in`;
27546
29059
  const costOut = unpriced ? "" : model.outputCostPer1kTokens === 0 ? "free" : `, $${model.outputCostPer1kTokens.toFixed(4)}/1K out`;
27547
29060
  const ctx = model.contextWindow >= 1e6 ? `${(model.contextWindow / 1e6).toFixed(1)}M ctx` : `${(model.contextWindow / 1e3).toFixed(0)}K ctx`;
27548
- const local = model.isLocal ? chalk7__default.default.gray(" [local]") : "";
27549
- const vision = model.isVisionCapable ? chalk7__default.default.gray(" \u{1F441}") : "";
29061
+ const local = model.isLocal ? chalk8__default.default.gray(" [local]") : "";
29062
+ const vision = model.isVisionCapable ? chalk8__default.default.gray(" \u{1F441}") : "";
27550
29063
  const bench = Math.round(benchmarkScore01(model, "mixed") * 100);
27551
29064
  console.log(
27552
- ` ${color.bold(tier)} ${chalk7__default.default.white(col(model.name, 24))}${chalk7__default.default.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk7__default.default.gray(col(ctx, 12))}${chalk7__default.default.gray(col(`bench ${bench}/100`, 14))}${chalk7__default.default.gray(`${costIn}${costOut}`)}` : `${chalk7__default.default.gray(col(ctx, 10))}${chalk7__default.default.gray(`bench ${bench}/100`)}`) + local + vision
29065
+ ` ${color.bold(tier)} ${chalk8__default.default.white(col(model.name, 24))}${chalk8__default.default.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk8__default.default.gray(col(ctx, 12))}${chalk8__default.default.gray(col(`bench ${bench}/100`, 14))}${chalk8__default.default.gray(`${costIn}${costOut}`)}` : `${chalk8__default.default.gray(col(ctx, 10))}${chalk8__default.default.gray(`bench ${bench}/100`)}`) + local + vision
27553
29066
  );
27554
29067
  } else {
27555
- console.log(` ${color.bold(tier)} ${chalk7__default.default.red("No model available")} ${chalk7__default.default.gray(`(check provider config for ${label})`)}`);
29068
+ console.log(` ${color.bold(tier)} ${chalk8__default.default.red("No model available")} ${chalk8__default.default.gray(`(check provider config for ${label})`)}`);
27556
29069
  anyMissing = true;
27557
29070
  }
27558
29071
  }
27559
29072
  console.log();
27560
29073
  const providers = config.providers.map((p) => p.type).join(", ") || "(none)";
27561
- console.log(chalk7__default.default.gray(` Configured providers: ${providers}`));
29074
+ console.log(chalk8__default.default.gray(` Configured providers: ${providers}`));
27562
29075
  if (liveData) {
27563
29076
  const src = liveData.getDataSource();
27564
29077
  const gen = liveData.getGeneratedAt();
27565
29078
  const srcLabel = src === "live" ? "live (just fetched)" : src === "cache" ? "cached" : "bundled";
27566
29079
  const pricingMeta = pricingDatasetMeta();
27567
- console.log(chalk7__default.default.gray(
29080
+ console.log(chalk8__default.default.gray(
27568
29081
  ` Benchmark data: ${srcLabel}` + (gen ? ` \xB7 updated ${gen.slice(0, 10)}` : "") + ` \xB7 pricing: ${liveData.hasLivePricing() ? "live (OpenRouter)" : "dataset"} (baseline ${pricingMeta.generatedAt})`
27569
29082
  ));
27570
29083
  const drift = liveData.getPriceDisagreements();
27571
29084
  if (drift.length) {
27572
- console.log(chalk7__default.default.yellow(`
29085
+ console.log(chalk8__default.default.yellow(`
27573
29086
  ${drift.length} model${drift.length === 1 ? "" : "s"} priced differently by the live source than by the bundled dataset:`));
27574
29087
  for (const d of drift.slice(0, 10)) {
27575
- console.log(chalk7__default.default.gray(
29088
+ console.log(chalk8__default.default.gray(
27576
29089
  ` ${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)`
27577
29090
  ));
27578
29091
  }
27579
- console.log(chalk7__default.default.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
29092
+ console.log(chalk8__default.default.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
27580
29093
  }
27581
29094
  }
27582
29095
  if (anyUnpriced) {
27583
- console.log(chalk7__default.default.yellow(
29096
+ console.log(chalk8__default.default.yellow(
27584
29097
  '\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.'
27585
29098
  ));
27586
29099
  }
27587
29100
  if (options.verbose) {
27588
29101
  console.log();
27589
- console.log(chalk7__default.default.white(" Available models by provider:\n"));
29102
+ console.log(chalk8__default.default.white(" Available models by provider:\n"));
27590
29103
  const allProviderTypes = [...new Set(config.providers.map((p) => p.type))];
27591
29104
  for (const providerType of allProviderTypes) {
27592
29105
  const available = router.getModelsForProvider(providerType);
27593
29106
  if (available.length === 0) continue;
27594
- console.log(chalk7__default.default.gray(` ${providerType}:`));
29107
+ console.log(chalk8__default.default.gray(` ${providerType}:`));
27595
29108
  for (const m of available) {
27596
29109
  const override = config.models.t1 === m.id ? " \u2190 T1" : config.models.t2 === m.id ? " \u2190 T2" : config.models.t3 === m.id ? " \u2190 T3" : "";
27597
- console.log(` ${chalk7__default.default.white(col(m.name, 28))}${chalk7__default.default.gray(m.id)}${chalk7__default.default.yellow(override)}`);
29110
+ console.log(` ${chalk8__default.default.white(col(m.name, 28))}${chalk8__default.default.gray(m.id)}${chalk8__default.default.yellow(override)}`);
27598
29111
  }
27599
29112
  console.log();
27600
29113
  }
27601
29114
  }
27602
29115
  if (anyMissing) {
27603
- console.log(chalk7__default.default.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
29116
+ console.log(chalk8__default.default.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
27604
29117
  } else {
27605
- console.log(chalk7__default.default.green(" All tiers are configured.\n"));
29118
+ console.log(chalk8__default.default.green(" All tiers are configured.\n"));
27606
29119
  }
27607
29120
  }
27608
29121
  async function setModelCommand(action, tierArg, value) {
27609
29122
  const tier = (tierArg ?? "").toLowerCase();
27610
29123
  if (!["t1", "t2", "t3"].includes(tier)) {
27611
- console.error(chalk7__default.default.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
27612
- console.log(chalk7__default.default.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
29124
+ console.error(chalk8__default.default.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
29125
+ console.log(chalk8__default.default.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
27613
29126
  process.exit(1);
27614
29127
  }
27615
29128
  const override = action === "unset" ? "auto" : (value ?? "").trim();
27616
29129
  if (action === "set" && !override) {
27617
- console.error(chalk7__default.default.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
29130
+ console.error(chalk8__default.default.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
27618
29131
  process.exit(1);
27619
29132
  }
27620
29133
  const cm = new ConfigManager(process.cwd());
@@ -27624,8 +29137,8 @@ async function setModelCommand(action, tierArg, value) {
27624
29137
  if (override === "auto") delete models[tier];
27625
29138
  else models[tier] = override;
27626
29139
  await cm.updateConfig({ models });
27627
- const label = override === "auto" ? chalk7__default.default.gray("auto (routing decides)") : chalk7__default.default.white(override);
27628
- console.log(chalk7__default.default.green(`
29140
+ const label = override === "auto" ? chalk8__default.default.gray("auto (routing decides)") : chalk8__default.default.white(override);
29141
+ console.log(chalk8__default.default.green(`
27629
29142
  \u2713 ${tier.toUpperCase()} model set to ${label}
27630
29143
  `));
27631
29144
  }
@@ -27639,8 +29152,8 @@ async function exportCommand(options = {}) {
27639
29152
  let store;
27640
29153
  try {
27641
29154
  const workspacePath = options.workspacePath ?? process.cwd();
27642
- const workspaceDbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
27643
- const globalDbPath = path34__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
29155
+ const workspaceDbPath = path35__default.default.join(workspacePath, CASCADE_DB_FILE);
29156
+ const globalDbPath = path35__default.default.join(os10__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
27644
29157
  let dbPath = globalDbPath;
27645
29158
  try {
27646
29159
  await fs11__default.default.access(workspaceDbPath);
@@ -27649,7 +29162,7 @@ async function exportCommand(options = {}) {
27649
29162
  }
27650
29163
  store = new MemoryStore(dbPath);
27651
29164
  } catch (err) {
27652
- spin.fail(chalk7__default.default.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
29165
+ spin.fail(chalk8__default.default.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
27653
29166
  process.exit(1);
27654
29167
  }
27655
29168
  try {
@@ -27657,7 +29170,7 @@ async function exportCommand(options = {}) {
27657
29170
  if (options.sessionId) {
27658
29171
  const session2 = store.getSession(options.sessionId);
27659
29172
  if (!session2) {
27660
- spin.fail(chalk7__default.default.red(`Session "${options.sessionId}" not found.`));
29173
+ spin.fail(chalk8__default.default.red(`Session "${options.sessionId}" not found.`));
27661
29174
  process.exit(1);
27662
29175
  }
27663
29176
  sessions2 = [session2];
@@ -27665,14 +29178,14 @@ async function exportCommand(options = {}) {
27665
29178
  const limit = options.last ?? 10;
27666
29179
  sessions2 = store.listSessions(void 0, limit);
27667
29180
  if (sessions2.length === 0) {
27668
- spin.warn(chalk7__default.default.yellow("No sessions found."));
29181
+ spin.warn(chalk8__default.default.yellow("No sessions found."));
27669
29182
  return;
27670
29183
  }
27671
29184
  const latest = sessions2[0];
27672
29185
  const full = store.getSession(latest.id);
27673
29186
  sessions2 = full ? [full] : [];
27674
29187
  if (sessions2.length === 0) {
27675
- spin.fail(chalk7__default.default.red("Could not load latest session."));
29188
+ spin.fail(chalk8__default.default.red("Could not load latest session."));
27676
29189
  process.exit(1);
27677
29190
  }
27678
29191
  }
@@ -27682,17 +29195,17 @@ async function exportCommand(options = {}) {
27682
29195
  const ext = format === "json" ? ".json" : ".md";
27683
29196
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
27684
29197
  const defaultFile = `cascade-export-${safeName}${ext}`;
27685
- const outPath = options.output ? path34__default.default.resolve(options.output) : path34__default.default.join(process.cwd(), defaultFile);
29198
+ const outPath = options.output ? path35__default.default.resolve(options.output) : path35__default.default.join(process.cwd(), defaultFile);
27686
29199
  await fs11__default.default.writeFile(outPath, content, "utf-8");
27687
- spin.succeed(chalk7__default.default.green(`Exported to ${chalk7__default.default.white(outPath)}`));
29200
+ spin.succeed(chalk8__default.default.green(`Exported to ${chalk8__default.default.white(outPath)}`));
27688
29201
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
27689
29202
  console.log();
27690
- console.log(chalk7__default.default.gray(` Session: ${session.title}`));
27691
- console.log(chalk7__default.default.gray(` Messages: ${messageCount}`));
27692
- console.log(chalk7__default.default.gray(` Format: ${format}`));
29203
+ console.log(chalk8__default.default.gray(` Session: ${session.title}`));
29204
+ console.log(chalk8__default.default.gray(` Messages: ${messageCount}`));
29205
+ console.log(chalk8__default.default.gray(` Format: ${format}`));
27693
29206
  console.log();
27694
29207
  } catch (err) {
27695
- spin.fail(chalk7__default.default.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
29208
+ spin.fail(chalk8__default.default.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
27696
29209
  process.exit(1);
27697
29210
  }
27698
29211
  }
@@ -27746,93 +29259,6 @@ function buildJsonExport(session) {
27746
29259
  };
27747
29260
  return JSON.stringify(payload, null, 2);
27748
29261
  }
27749
- async function linkCommand(target, options = {}) {
27750
- const found = await discoverCredentials();
27751
- if (found.length === 0) {
27752
- console.log(chalk7__default.default.yellow("\n No reusable credentials found.\n"));
27753
- console.log(chalk7__default.default.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
27754
- console.log(chalk7__default.default.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
27755
- return;
27756
- }
27757
- if (!target) {
27758
- printDiscovered(found);
27759
- return;
27760
- }
27761
- const provider = normalizeProvider(target);
27762
- if (!provider) {
27763
- console.log(chalk7__default.default.red(`
27764
- Unknown provider "${target}". Use one of: anthropic, openai, gemini.
27765
- `));
27766
- return;
27767
- }
27768
- const candidates2 = found.filter((c) => c.provider === provider);
27769
- const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
27770
- if (!chosen) {
27771
- console.log(chalk7__default.default.yellow(`
27772
- No detected credential maps to "${provider}".
27773
- `));
27774
- return;
27775
- }
27776
- if (!chosen.directlyUsable) {
27777
- console.log(chalk7__default.default.yellow(`
27778
- Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
27779
- if (chosen.warning) console.log(chalk7__default.default.gray(` ${chosen.warning}`));
27780
- console.log(chalk7__default.default.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
27781
- return;
27782
- }
27783
- if (chosen.kind === "oauth" && !options.acceptRisk) {
27784
- console.log(chalk7__default.default.yellow(`
27785
- ${chosen.sourceTool} provides a subscription OAuth token, not an API key.`));
27786
- if (chosen.warning) console.log(chalk7__default.default.gray(` ${chosen.warning}`));
27787
- console.log(chalk7__default.default.gray(" Re-run with --accept-risk to adopt it anyway:\n"));
27788
- console.log(chalk7__default.default.cyan(` cascade link ${provider} --accept-risk
27789
- `));
27790
- return;
27791
- }
27792
- await adoptCredential(chosen, options.workspace ?? process.cwd());
27793
- console.log(chalk7__default.default.green(`
27794
- \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
27795
- if (chosen.kind === "oauth") {
27796
- console.log(chalk7__default.default.gray(" Adopted as an OAuth bearer token \u2014 revoke it in the source tool to disable."));
27797
- }
27798
- console.log(chalk7__default.default.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
27799
- }
27800
- function printDiscovered(found) {
27801
- console.log(chalk7__default.default.magenta("\n \u25C8 Detected credentials\n"));
27802
- for (const c of found) {
27803
- const usable = c.directlyUsable ? chalk7__default.default.green("usable") : chalk7__default.default.yellow("needs vendor backend");
27804
- const kind = c.kind === "oauth" ? chalk7__default.default.yellow("oauth") : chalk7__default.default.gray("api-key");
27805
- console.log(` ${chalk7__default.default.white(c.provider.padEnd(18))} ${chalk7__default.default.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
27806
- console.log(chalk7__default.default.gray(` from ${c.sourceTool}`));
27807
- if (c.warning) console.log(chalk7__default.default.yellow(` \u26A0 ${c.warning}`));
27808
- }
27809
- console.log(chalk7__default.default.gray("\n Adopt one with: ") + chalk7__default.default.cyan("cascade link <provider> [--accept-risk]"));
27810
- console.log(chalk7__default.default.gray(" --accept-risk is required for subscription OAuth tokens.\n"));
27811
- }
27812
- function normalizeProvider(target) {
27813
- const t = target.toLowerCase();
27814
- if (t === "anthropic" || t === "claude" || t === "claude-code") return "anthropic";
27815
- if (t === "openai" || t === "codex" || t === "gpt") return "openai";
27816
- if (t === "gemini" || t === "google") return "gemini";
27817
- return null;
27818
- }
27819
- async function adoptCredential(cred, workspace) {
27820
- const cm = new ConfigManager(workspace);
27821
- await cm.load();
27822
- const config = cm.getConfig();
27823
- const next = {
27824
- type: cred.provider,
27825
- credentialSource: cred.sourceTool
27826
- };
27827
- if (cred.kind === "oauth" && cred.provider === "anthropic") {
27828
- next.authToken = cred.secret;
27829
- } else {
27830
- next.apiKey = cred.secret;
27831
- }
27832
- const providers = config.providers.filter((p) => p.type !== cred.provider);
27833
- providers.push(next);
27834
- await cm.updateConfig({ providers });
27835
- }
27836
29262
  async function telemetryCommand(action) {
27837
29263
  const cm = new ConfigManager(process.cwd());
27838
29264
  await cm.load();
@@ -27840,12 +29266,12 @@ async function telemetryCommand(action) {
27840
29266
  if (action === "status") {
27841
29267
  const state = config.telemetry?.enabled ? "ON" : "OFF";
27842
29268
  console.log();
27843
- console.log(chalk7__default.default.magenta(" \u25C8 Cascade Telemetry"));
29269
+ console.log(chalk8__default.default.magenta(" \u25C8 Cascade Telemetry"));
27844
29270
  console.log();
27845
- console.log(` Status: ${config.telemetry?.enabled ? chalk7__default.default.green(state) : chalk7__default.default.gray(state)}`);
27846
- console.log(chalk7__default.default.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
29271
+ console.log(` Status: ${config.telemetry?.enabled ? chalk8__default.default.green(state) : chalk8__default.default.gray(state)}`);
29272
+ console.log(chalk8__default.default.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
27847
29273
  console.log();
27848
- console.log(chalk7__default.default.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
29274
+ console.log(chalk8__default.default.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
27849
29275
  console.log();
27850
29276
  return;
27851
29277
  }
@@ -27859,11 +29285,11 @@ async function telemetryCommand(action) {
27859
29285
  });
27860
29286
  console.log();
27861
29287
  if (enabled) {
27862
- console.log(chalk7__default.default.green(` \u2713 Telemetry enabled.`));
27863
- console.log(chalk7__default.default.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
29288
+ console.log(chalk8__default.default.green(` \u2713 Telemetry enabled.`));
29289
+ console.log(chalk8__default.default.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
27864
29290
  } else {
27865
- console.log(chalk7__default.default.yellow(` \u2713 Telemetry disabled.`));
27866
- console.log(chalk7__default.default.gray(" No events will be transmitted from this workspace."));
29291
+ console.log(chalk8__default.default.yellow(` \u2713 Telemetry disabled.`));
29292
+ console.log(chalk8__default.default.gray(" No events will be transmitted from this workspace."));
27867
29293
  }
27868
29294
  console.log();
27869
29295
  }
@@ -27873,11 +29299,11 @@ async function statsCommand() {
27873
29299
  await tracker.load();
27874
29300
  const all = tracker.getAll();
27875
29301
  if (all.size === 0) {
27876
- console.log(chalk7__default.default.dim("\n No routing history yet \u2014 run some tasks first.\n"));
29302
+ console.log(chalk8__default.default.dim("\n No routing history yet \u2014 run some tasks first.\n"));
27877
29303
  return;
27878
29304
  }
27879
- console.log(chalk7__default.default.magenta("\n \u25C8 Auto-Routing History\n"));
27880
- console.log(chalk7__default.default.dim(" Per-task-type model performance learned from past runs.\n"));
29305
+ console.log(chalk8__default.default.magenta("\n \u25C8 Auto-Routing History\n"));
29306
+ console.log(chalk8__default.default.dim(" Per-task-type model performance learned from past runs.\n"));
27881
29307
  for (const taskType of TASK_TYPES) {
27882
29308
  const entries = [];
27883
29309
  for (const [key, stat] of all) {
@@ -27889,36 +29315,36 @@ async function statsCommand() {
27889
29315
  }
27890
29316
  if (entries.length === 0) continue;
27891
29317
  entries.sort((a, b) => b.successRate - a.successRate || b.samples - a.samples);
27892
- console.log(chalk7__default.default.bold(` ${taskType.toUpperCase()}`));
29318
+ console.log(chalk8__default.default.bold(` ${taskType.toUpperCase()}`));
27893
29319
  const header = ` ${"Model".padEnd(36)} ${"Success".padEnd(9)} ${"Samples".padEnd(9)} Avg cost`;
27894
- console.log(chalk7__default.default.dim(header));
27895
- console.log(chalk7__default.default.dim(" " + "\u2500".repeat(62)));
29320
+ console.log(chalk8__default.default.dim(header));
29321
+ console.log(chalk8__default.default.dim(" " + "\u2500".repeat(62)));
27896
29322
  for (const e of entries) {
27897
29323
  const pct = `${Math.round(e.successRate * 100)}%`;
27898
29324
  const cost = e.avgCostUsd < 1e-4 ? "<$0.0001" : `$${e.avgCostUsd.toFixed(4)}`;
27899
- const color = e.successRate >= 0.8 ? chalk7__default.default.green : e.successRate >= 0.5 ? chalk7__default.default.yellow : chalk7__default.default.red;
29325
+ const color = e.successRate >= 0.8 ? chalk8__default.default.green : e.successRate >= 0.5 ? chalk8__default.default.yellow : chalk8__default.default.red;
27900
29326
  console.log(
27901
- ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk7__default.default.dim(cost)}`
29327
+ ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk8__default.default.dim(cost)}`
27902
29328
  );
27903
29329
  }
27904
29330
  console.log();
27905
29331
  }
27906
- console.log(chalk7__default.default.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
29332
+ console.log(chalk8__default.default.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
27907
29333
  }
27908
29334
 
27909
29335
  // src/cli/index.ts
27910
29336
  dotenv__default.default.config();
27911
29337
  function warnIfBuildIsStale() {
27912
29338
  try {
27913
- const here = path34__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
29339
+ const here = path35__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
27914
29340
  for (const rel of ["..", "../.."]) {
27915
- const pkgPath = path34__default.default.join(here, rel, "package.json");
27916
- if (!fs31__default.default.existsSync(pkgPath)) continue;
27917
- const pkg = JSON.parse(fs31__default.default.readFileSync(pkgPath, "utf-8"));
29341
+ const pkgPath = path35__default.default.join(here, rel, "package.json");
29342
+ if (!fs32__default.default.existsSync(pkgPath)) continue;
29343
+ const pkg = JSON.parse(fs32__default.default.readFileSync(pkgPath, "utf-8"));
27918
29344
  if (pkg.name !== "cascade-ai") continue;
27919
29345
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
27920
29346
  console.error(
27921
- chalk7__default.default.yellow(
29347
+ chalk8__default.default.yellow(
27922
29348
  `\u26A0 Stale build: compiled output is v${CASCADE_VERSION} but the source tree is v${pkg.version}.
27923
29349
  Run: npm install && npm run build`
27924
29350
  )
@@ -28017,7 +29443,7 @@ mcp.command("list").description("List configured MCP servers").action(async () =
28017
29443
  mcp.command("remove <name>").description("Remove an MCP server (and its stored OAuth tokens)").action(async (name) => {
28018
29444
  await mcpRemoveCommand(name);
28019
29445
  });
28020
- 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) => {
29446
+ 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) => {
28021
29447
  await linkCommand(provider, { acceptRisk: opts.acceptRisk, workspace: process.cwd() });
28022
29448
  });
28023
29449
  program.command("update").description("Update Cascade to the latest version").action(async () => {
@@ -28043,7 +29469,7 @@ program.command("models [action] [tier] [value]").description("List AI models pe
28043
29469
  program.command("telemetry [action]").description("Toggle anonymous usage telemetry (on | off | status). Default: status").action(async (action) => {
28044
29470
  const normalized = (action ?? "status").toLowerCase();
28045
29471
  if (normalized !== "on" && normalized !== "off" && normalized !== "status") {
28046
- console.error(chalk7__default.default.red(`Unknown action: ${action}. Use: on | off | status`));
29472
+ console.error(chalk8__default.default.red(`Unknown action: ${action}. Use: on | off | status`));
28047
29473
  process.exit(1);
28048
29474
  }
28049
29475
  await telemetryCommand(normalized);
@@ -28065,15 +29491,15 @@ async function startRepl(options) {
28065
29491
  try {
28066
29492
  await cm.load();
28067
29493
  } catch (err) {
28068
- console.error(chalk7__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28069
- console.error(chalk7__default.default.gray("Run `cascade init` to set up this directory."));
29494
+ console.error(chalk8__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29495
+ console.error(chalk8__default.default.gray("Run `cascade init` to set up this directory."));
28070
29496
  process.exit(1);
28071
29497
  }
28072
29498
  let config = cm.getConfig();
28073
29499
  const startupNotice = cm.takeRetiredNotice();
28074
29500
  const needsSetup = !hasUsableProvider(config.providers);
28075
29501
  if (needsSetup) {
28076
- console.log(chalk7__default.default.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
29502
+ console.log(chalk8__default.default.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
28077
29503
  console.log();
28078
29504
  config = await runSetupWizard(workspacePath);
28079
29505
  await cm.updateConfig(config);
@@ -28108,24 +29534,24 @@ async function runHeadless(prompt, options) {
28108
29534
  try {
28109
29535
  await cm.load();
28110
29536
  } catch (err) {
28111
- console.error(chalk7__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28112
- console.error(chalk7__default.default.gray("Run `cascade init` to set up this directory."));
29537
+ console.error(chalk8__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29538
+ console.error(chalk8__default.default.gray("Run `cascade init` to set up this directory."));
28113
29539
  process.exit(1);
28114
29540
  }
28115
29541
  const config = cm.getConfig();
28116
29542
  const needsSetup = !hasUsableProvider(config.providers);
28117
29543
  if (needsSetup) {
28118
- console.error(chalk7__default.default.red("No providers configured. Run `cascade init` first."));
29544
+ console.error(chalk8__default.default.red("No providers configured. Run `cascade init` first."));
28119
29545
  process.exit(1);
28120
29546
  }
28121
29547
  const cascade = new Cascade(config, workspacePath);
28122
29548
  try {
28123
29549
  await cascade.init();
28124
29550
  } catch (err) {
28125
- console.error(chalk7__default.default.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
29551
+ console.error(chalk8__default.default.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
28126
29552
  process.exit(1);
28127
29553
  }
28128
- console.error(chalk7__default.default.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
29554
+ console.error(chalk8__default.default.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
28129
29555
  let lastProgress = "";
28130
29556
  cascade.on("tier:status", (ev) => {
28131
29557
  const action = ev?.currentAction?.trim();
@@ -28133,7 +29559,7 @@ async function runHeadless(prompt, options) {
28133
29559
  const line = ` \xB7 ${ev.role ?? ""} ${action}`.trimEnd();
28134
29560
  if (line === lastProgress) return;
28135
29561
  lastProgress = line;
28136
- console.error(chalk7__default.default.gray(line));
29562
+ console.error(chalk8__default.default.gray(line));
28137
29563
  });
28138
29564
  try {
28139
29565
  const result = await cascade.run({
@@ -28144,7 +29570,7 @@ async function runHeadless(prompt, options) {
28144
29570
  });
28145
29571
  process.stdout.write(result.output.trimEnd() + "\n");
28146
29572
  } catch (err) {
28147
- console.error(chalk7__default.default.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
29573
+ console.error(chalk8__default.default.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
28148
29574
  await cascade.close().catch(() => {
28149
29575
  });
28150
29576
  process.exit(1);
@@ -28156,9 +29582,9 @@ async function runHeadless(prompt, options) {
28156
29582
  function printBanner() {
28157
29583
  if (process.stdout.columns < 60) return;
28158
29584
  console.log();
28159
- console.log(" " + chalk7__default.default.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
28160
- console.log(" " + chalk7__default.default.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7__default.default.white.bold("CASCADE") + chalk7__default.default.gray(" v" + CASCADE_VERSION));
28161
- console.log(" " + chalk7__default.default.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7__default.default.hex("#8A93A8")("multi-tier orchestration"));
29585
+ console.log(" " + chalk8__default.default.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
29586
+ console.log(" " + chalk8__default.default.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8__default.default.white.bold("CASCADE") + chalk8__default.default.gray(" v" + CASCADE_VERSION));
29587
+ console.log(" " + chalk8__default.default.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8__default.default.hex("#8A93A8")("multi-tier orchestration"));
28162
29588
  console.log();
28163
29589
  }
28164
29590
  program.parse(process.argv);