cascade-ai 0.74.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.74.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 = {}) {
@@ -2400,6 +2638,8 @@ var init_model_filter = __esm({
2400
2638
  var anthropic_exports = {};
2401
2639
  __export(anthropic_exports, {
2402
2640
  AnthropicProvider: () => AnthropicProvider,
2641
+ anthropicApiRoot: () => anthropicApiRoot,
2642
+ anthropicAuth: () => anthropicAuth,
2403
2643
  toAnthropicToolUse: () => toAnthropicToolUse,
2404
2644
  toAnthropicTools: () => toAnthropicTools
2405
2645
  });
@@ -2424,25 +2664,56 @@ function toAnthropicToolUse(toolCalls) {
2424
2664
  input: tc.input
2425
2665
  }));
2426
2666
  }
2427
- 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;
2428
2692
  var init_anthropic = __esm({
2429
2693
  "src/providers/anthropic.ts"() {
2430
2694
  init_constants();
2431
2695
  init_base();
2432
2696
  init_pricing();
2433
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
+ });
2434
2704
  AnthropicProvider = class extends BaseProvider {
2435
2705
  client;
2436
2706
  constructor(config, model) {
2437
2707
  super(config, model);
2438
- if (config.authToken) {
2439
- this.client = new Anthropic__default.default({
2440
- authToken: config.authToken,
2441
- defaultHeaders: { "anthropic-beta": "oauth-2025-04-20" }
2442
- });
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 });
2443
2712
  } else {
2444
2713
  this.client = new Anthropic__default.default({
2445
- apiKey: config.apiKey
2714
+ apiKey: auth2.mode === "apiKey" ? auth2.key : void 0,
2715
+ fetch: sameOriginFetch,
2716
+ ...baseURL ? { baseURL } : {}
2446
2717
  });
2447
2718
  }
2448
2719
  }
@@ -2518,21 +2789,24 @@ var init_anthropic = __esm({
2518
2789
  async countTokens(text) {
2519
2790
  return Math.ceil(text.length / 4);
2520
2791
  }
2521
- async listModels() {
2792
+ async listModels(options = {}) {
2793
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "anthropic");
2522
2794
  try {
2523
- 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, {
2524
2799
  headers: {
2525
- "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 : "" },
2526
2804
  "anthropic-version": "2023-06-01"
2527
2805
  }
2528
2806
  });
2529
- if (!resp.ok) {
2530
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2531
- }
2807
+ if (!resp.ok) return staticCatalog();
2532
2808
  const data = await resp.json();
2533
- if (!Array.isArray(data?.data)) {
2534
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2535
- }
2809
+ if (!Array.isArray(data?.data)) return staticCatalog();
2536
2810
  return data.data.filter((m) => isChatModel(m.id)).map((m) => {
2537
2811
  const known = Object.values(MODELS).find((km) => km.id === m.id && km.provider === "anthropic");
2538
2812
  if (known) return known;
@@ -2550,7 +2824,7 @@ var init_anthropic = __esm({
2550
2824
  });
2551
2825
  });
2552
2826
  } catch {
2553
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2827
+ return staticCatalog();
2554
2828
  }
2555
2829
  }
2556
2830
  async isAvailable() {
@@ -2770,7 +3044,8 @@ var init_openai = __esm({
2770
3044
  async countTokens(text) {
2771
3045
  return Math.ceil(text.length / 4);
2772
3046
  }
2773
- async listModels() {
3047
+ async listModels(options = {}) {
3048
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "openai");
2774
3049
  try {
2775
3050
  const response = await this.client.models.list();
2776
3051
  return response.data.filter((m) => isChatModel(m.id)).map((m) => {
@@ -2790,7 +3065,7 @@ var init_openai = __esm({
2790
3065
  });
2791
3066
  });
2792
3067
  } catch {
2793
- return Object.values(MODELS).filter((m) => m.provider === "openai");
3068
+ return staticCatalog();
2794
3069
  }
2795
3070
  }
2796
3071
  async isAvailable() {
@@ -3092,9 +3367,46 @@ var init_gemini_schema = __esm({
3092
3367
  var gemini_exports = {};
3093
3368
  __export(gemini_exports, {
3094
3369
  GeminiProvider: () => GeminiProvider,
3370
+ geminiApiRoot: () => geminiApiRoot,
3095
3371
  toGeminiFunctionCalls: () => toGeminiFunctionCalls,
3096
3372
  toGeminiTools: () => toGeminiTools
3097
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
+ }
3098
3410
  function toGeminiTools(tools) {
3099
3411
  return tools.map((tool) => ({
3100
3412
  name: tool.name,
@@ -3127,7 +3439,7 @@ async function describeGeminiError(resp) {
3127
3439
  return "";
3128
3440
  }
3129
3441
  }
3130
- var GeminiProvider;
3442
+ var GEMINI_API_VERSION, GEMINI_PUBLIC_ROOT, SAFETY_SETTINGS, GeminiProvider;
3131
3443
  var init_gemini = __esm({
3132
3444
  "src/providers/gemini.ts"() {
3133
3445
  init_constants();
@@ -3135,11 +3447,21 @@ var init_gemini = __esm({
3135
3447
  init_pricing();
3136
3448
  init_model_filter();
3137
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
+ ];
3138
3457
  GeminiProvider = class extends BaseProvider {
3139
3458
  client;
3140
3459
  constructor(config, model) {
3141
3460
  super(config, model);
3142
- 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
+ });
3143
3465
  }
3144
3466
  async generate(options) {
3145
3467
  const chunks = [];
@@ -3147,15 +3469,12 @@ var init_gemini = __esm({
3147
3469
  }
3148
3470
  async generateStream(options, onChunk) {
3149
3471
  const contents = this.buildContents(options.messages, options.images);
3150
- const stream = await this.client.models.generateContentStream({
3472
+ const stream = this.config.baseUrl ? await this.streamOverRest(contents, options) : await this.client.models.generateContentStream({
3151
3473
  model: this.model.id,
3152
3474
  contents,
3153
3475
  config: {
3154
3476
  systemInstruction: options.systemPrompt,
3155
- safetySettings: [
3156
- { category: genai.HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: genai.HarmBlockThreshold.BLOCK_NONE },
3157
- { category: genai.HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: genai.HarmBlockThreshold.BLOCK_NONE }
3158
- ],
3477
+ safetySettings: SAFETY_SETTINGS,
3159
3478
  tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
3160
3479
  abortSignal: options.signal
3161
3480
  }
@@ -3206,25 +3525,40 @@ var init_gemini = __esm({
3206
3525
  };
3207
3526
  }
3208
3527
  async countTokens(text) {
3528
+ const contents = [{ role: "user", parts: [{ text }] }];
3209
3529
  try {
3210
- const result = await this.client.models.countTokens({
3211
- model: this.model.id,
3212
- contents: [{ role: "user", parts: [{ text }] }]
3213
- });
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 });
3214
3547
  return result.totalTokens ?? 0;
3215
3548
  } catch {
3216
3549
  return Math.ceil(text.length / 4);
3217
3550
  }
3218
3551
  }
3219
- async listModels() {
3552
+ async listModels(options = {}) {
3553
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "gemini");
3220
3554
  try {
3221
3555
  const resp = await this.fetchModelList();
3222
3556
  if (!resp.ok) {
3223
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3557
+ return staticCatalog();
3224
3558
  }
3225
3559
  const data = await resp.json();
3226
3560
  if (!Array.isArray(data?.models)) {
3227
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3561
+ return staticCatalog();
3228
3562
  }
3229
3563
  return data.models.filter((m) => isChatModel(m.name.replace("models/", ""), m.supportedGenerationMethods)).map((m) => {
3230
3564
  const id = m.name.replace("models/", "");
@@ -3246,7 +3580,7 @@ var init_gemini = __esm({
3246
3580
  });
3247
3581
  });
3248
3582
  } catch {
3249
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3583
+ return staticCatalog();
3250
3584
  }
3251
3585
  }
3252
3586
  /**
@@ -3299,8 +3633,43 @@ var init_gemini = __esm({
3299
3633
  * logs, error reports and shell history, and this one would carry the key
3300
3634
  * with it.
3301
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
+ }
3302
3671
  fetchModelList() {
3303
- return fetch("https://generativelanguage.googleapis.com/v1beta/models", {
3672
+ return fetchSameOrigin(`${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models`, {
3304
3673
  headers: { "x-goog-api-key": this.config.apiKey ?? "" }
3305
3674
  });
3306
3675
  }
@@ -3388,84 +3757,6 @@ var init_gemini = __esm({
3388
3757
  };
3389
3758
  }
3390
3759
  });
3391
- function preferIpv4Host(url) {
3392
- return url;
3393
- }
3394
- async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3395
- const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3396
- const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3397
- const lib = u.protocol === "https:" ? https__default.default : http__default.default;
3398
- const method = (init.method ?? "GET").toUpperCase();
3399
- const headers = {};
3400
- const h = init.headers;
3401
- if (h instanceof Headers) h.forEach((v, k) => {
3402
- headers[k] = v;
3403
- });
3404
- else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
3405
- else if (h) Object.assign(headers, h);
3406
- if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
3407
- headers["accept-encoding"] = "gzip, deflate, br";
3408
- }
3409
- const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3410
- return new Promise((resolve2, reject) => {
3411
- const req = lib.request(
3412
- {
3413
- hostname: u.hostname,
3414
- port: u.port || (u.protocol === "https:" ? 443 : 80),
3415
- path: u.pathname + u.search,
3416
- method,
3417
- headers
3418
- },
3419
- (res) => {
3420
- const status = res.statusCode ?? 200;
3421
- const location = res.headers.location;
3422
- if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3423
- res.resume();
3424
- const nextUrl = new URL(location, u).href;
3425
- if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3426
- reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3427
- return;
3428
- }
3429
- const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3430
- const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3431
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3432
- return;
3433
- }
3434
- const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
3435
- let bodyStream = res;
3436
- if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib__default.default.createGunzip());
3437
- else if (encoding === "deflate") bodyStream = res.pipe(zlib__default.default.createInflate());
3438
- else if (encoding === "br") bodyStream = res.pipe(zlib__default.default.createBrotliDecompress());
3439
- const stream$1 = stream.Readable.toWeb(bodyStream);
3440
- const respHeaders = new Headers();
3441
- for (const [k, v] of Object.entries(res.headers)) {
3442
- if (k === "content-encoding" || k === "content-length") continue;
3443
- if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3444
- else if (typeof v === "string") respHeaders.set(k, v);
3445
- }
3446
- resolve2(new Response(stream$1, {
3447
- status,
3448
- statusText: res.statusMessage ?? "",
3449
- headers: respHeaders
3450
- }));
3451
- }
3452
- );
3453
- req.on("error", reject);
3454
- req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
3455
- if (body !== void 0) req.write(body);
3456
- req.end();
3457
- });
3458
- }
3459
- var MAX_REDIRECTS;
3460
- var init_net = __esm({
3461
- "src/utils/net.ts"() {
3462
- try {
3463
- dns__default.default.setDefaultResultOrder("ipv4first");
3464
- } catch {
3465
- }
3466
- MAX_REDIRECTS = 5;
3467
- }
3468
- });
3469
3760
 
3470
3761
  // src/providers/ollama.ts
3471
3762
  var ollama_exports = {};
@@ -3754,23 +4045,49 @@ var init_openai_compatible = __esm({
3754
4045
  OpenAICompatibleProvider = class extends OpenAIProvider {
3755
4046
  constructor(config, model) {
3756
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
+ }
3757
4053
  this.client = new OpenAI__default.default({
3758
4054
  apiKey: config.apiKey ?? "not-required",
3759
4055
  baseURL: preferIpv4Host(config.baseUrl),
3760
- 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()))
3761
4064
  });
3762
4065
  }
3763
- modelsUrl() {
3764
- const base = (preferIpv4Host(this.config.baseUrl) ?? "").replace(/\/+$/, "");
3765
- return base + "/models";
3766
- }
3767
- authHeaders() {
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
+ }
4080
+ modelsUrl() {
4081
+ const base = (preferIpv4Host(this.config.baseUrl) ?? "").replace(/\/+$/, "");
4082
+ return base + "/models";
4083
+ }
4084
+ authHeaders() {
3768
4085
  const h = { Accept: "application/json" };
3769
4086
  if (this.config.apiKey) h["Authorization"] = `Bearer ${this.config.apiKey}`;
3770
4087
  return h;
3771
4088
  }
3772
4089
  async listModels() {
3773
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4090
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3774
4091
  if (!res.ok) throw new Error(`models endpoint ${this.modelsUrl()} returned HTTP ${res.status}`);
3775
4092
  const body = await res.json();
3776
4093
  const raw = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
@@ -3800,7 +4117,7 @@ var init_openai_compatible = __esm({
3800
4117
  }
3801
4118
  async isAvailable() {
3802
4119
  try {
3803
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4120
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3804
4121
  return res.ok;
3805
4122
  } catch {
3806
4123
  return false;
@@ -3822,12 +4139,12 @@ var init_audit_logger = __esm({
3822
4139
  constructor(workspacePath, debugMode = false) {
3823
4140
  this.workspacePath = workspacePath;
3824
4141
  this.debugMode = debugMode;
3825
- const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
3826
- if (!fs31__default.default.existsSync(cascadeDir)) {
3827
- 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 });
3828
4145
  }
3829
- this.keyPath = path34__default.default.join(cascadeDir, "audit_log.key");
3830
- 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");
3831
4148
  this.initEncryptionKey();
3832
4149
  this.db = new Database2__default.default(this.dbPath);
3833
4150
  this.db.pragma("journal_mode = WAL");
@@ -3857,11 +4174,11 @@ var init_audit_logger = __esm({
3857
4174
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3858
4175
  }
3859
4176
  initEncryptionKey() {
3860
- if (fs31__default.default.existsSync(this.keyPath)) {
3861
- 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);
3862
4179
  } else {
3863
4180
  this.encryptionKey = crypto__default.default.randomBytes(32);
3864
- fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
4181
+ fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
3865
4182
  }
3866
4183
  }
3867
4184
  encrypt(text) {
@@ -3939,9 +4256,9 @@ var init_audit_logger = __esm({
3939
4256
  dumpDebugIfNeeded() {
3940
4257
  if (!this.debugMode) return;
3941
4258
  try {
3942
- 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");
3943
4260
  const entries = this.getAllLogs();
3944
- 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");
3945
4262
  } catch (err) {
3946
4263
  console.error("Failed to dump debug audit logs", err);
3947
4264
  }
@@ -3996,7 +4313,7 @@ var Keystore = class {
3996
4313
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
3997
4314
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
3998
4315
  this.backend = "keytar";
3999
- if (password && fs31__default.default.existsSync(this.storePath)) {
4316
+ if (password && fs32__default.default.existsSync(this.storePath)) {
4000
4317
  try {
4001
4318
  const fileEntries = this.decryptFile(password);
4002
4319
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -4015,7 +4332,7 @@ var Keystore = class {
4015
4332
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
4016
4333
  );
4017
4334
  }
4018
- if (!fs31__default.default.existsSync(this.storePath)) {
4335
+ if (!fs32__default.default.existsSync(this.storePath)) {
4019
4336
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4020
4337
  this.masterKey = this.deriveKey(password, salt);
4021
4338
  this.writeWithSalt({}, salt);
@@ -4029,7 +4346,7 @@ var Keystore = class {
4029
4346
  }
4030
4347
  /** Synchronous legacy unlock kept for AES-only environments. */
4031
4348
  unlockSync(password) {
4032
- if (!fs31__default.default.existsSync(this.storePath)) {
4349
+ if (!fs32__default.default.existsSync(this.storePath)) {
4033
4350
  const salt = crypto__default.default.randomBytes(SALT_LEN);
4034
4351
  this.masterKey = this.deriveKey(password, salt);
4035
4352
  this.writeWithSalt({}, salt);
@@ -4087,7 +4404,7 @@ var Keystore = class {
4087
4404
  }
4088
4405
  }
4089
4406
  decryptFile(password, knownSalt) {
4090
- if (!fs31__default.default.existsSync(this.storePath)) return {};
4407
+ if (!fs32__default.default.existsSync(this.storePath)) return {};
4091
4408
  try {
4092
4409
  const { salt, ciphertext, iv, tag } = this.readRaw();
4093
4410
  const useSalt = knownSalt ?? salt;
@@ -4109,8 +4426,8 @@ var Keystore = class {
4109
4426
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4110
4427
  const tag = cipher.getAuthTag();
4111
4428
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4112
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4113
- 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 });
4114
4431
  }
4115
4432
  writeWithSalt(data, salt) {
4116
4433
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4120,11 +4437,11 @@ var Keystore = class {
4120
4437
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4121
4438
  const tag = cipher.getAuthTag();
4122
4439
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4123
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.storePath), { recursive: true });
4124
- 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 });
4125
4442
  }
4126
4443
  readRaw() {
4127
- const buf = fs31__default.default.readFileSync(this.storePath);
4444
+ const buf = fs32__default.default.readFileSync(this.storePath);
4128
4445
  let offset = 0;
4129
4446
  const salt = buf.subarray(offset, offset + SALT_LEN);
4130
4447
  offset += SALT_LEN;
@@ -4157,7 +4474,7 @@ var CascadeIgnore = class {
4157
4474
  ]);
4158
4475
  }
4159
4476
  async load(workspacePath) {
4160
- const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4477
+ const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
4161
4478
  try {
4162
4479
  const content = await fs11__default.default.readFile(filePath, "utf-8");
4163
4480
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4168,7 +4485,7 @@ var CascadeIgnore = class {
4168
4485
  }
4169
4486
  isIgnored(filePath, workspacePath) {
4170
4487
  try {
4171
- const relative = workspacePath ? path34__default.default.relative(workspacePath, filePath) : filePath;
4488
+ const relative = workspacePath ? path35__default.default.relative(workspacePath, filePath) : filePath;
4172
4489
  return this.ig.ignores(relative);
4173
4490
  } catch {
4174
4491
  return false;
@@ -4179,7 +4496,7 @@ var CascadeIgnore = class {
4179
4496
  }
4180
4497
  };
4181
4498
  async function createDefaultIgnoreFile(workspacePath) {
4182
- const filePath = path34__default.default.join(workspacePath, ".cascadeignore");
4499
+ const filePath = path35__default.default.join(workspacePath, ".cascadeignore");
4183
4500
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4184
4501
  # Syntax identical to .gitignore
4185
4502
 
@@ -4209,7 +4526,7 @@ Thumbs.db
4209
4526
  await fs11__default.default.writeFile(filePath, content, "utf-8");
4210
4527
  }
4211
4528
  async function loadCascadeMd(workspacePath) {
4212
- const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4529
+ const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
4213
4530
  try {
4214
4531
  const raw = await fs11__default.default.readFile(filePath, "utf-8");
4215
4532
  return parseCascadeMd(raw);
@@ -4238,7 +4555,7 @@ ${raw.trim()}`;
4238
4555
  return { raw, sections, systemPrompt };
4239
4556
  }
4240
4557
  async function createDefaultCascadeMd(workspacePath) {
4241
- const filePath = path34__default.default.join(workspacePath, "CASCADE.md");
4558
+ const filePath = path35__default.default.join(workspacePath, "CASCADE.md");
4242
4559
  const content = `# Cascade Project Instructions
4243
4560
 
4244
4561
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4273,7 +4590,7 @@ List any areas the agent should not touch.
4273
4590
  var MemoryStore = class _MemoryStore {
4274
4591
  db;
4275
4592
  constructor(dbPath) {
4276
- fs31__default.default.mkdirSync(path34__default.default.dirname(dbPath), { recursive: true });
4593
+ fs32__default.default.mkdirSync(path35__default.default.dirname(dbPath), { recursive: true });
4277
4594
  try {
4278
4595
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
4279
4596
  this.db.pragma("journal_mode = WAL");
@@ -5537,21 +5854,80 @@ function validateConfig(raw) {
5537
5854
 
5538
5855
  // src/config/global-credentials.ts
5539
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
5540
5899
  function credentialsPath(globalDir2) {
5541
- return path34__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5900
+ return path35__default.default.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5542
5901
  }
5543
5902
  function providerKey(p) {
5544
- if (p.type === "azure") {
5545
- return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
5546
- }
5547
5903
  return p.type;
5548
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
+ }
5549
5925
  function isPersistable(p) {
5550
5926
  return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
5551
5927
  }
5552
5928
  function loadGlobalCredentials(globalDir2) {
5553
5929
  try {
5554
- const raw = fs31__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5930
+ const raw = fs32__default.default.readFileSync(credentialsPath(globalDir2), "utf-8");
5555
5931
  const parsed = JSON.parse(raw);
5556
5932
  if (!Array.isArray(parsed.providers)) return [];
5557
5933
  return parsed.providers.filter(
@@ -5563,34 +5939,93 @@ function loadGlobalCredentials(globalDir2) {
5563
5939
  }
5564
5940
  function saveGlobalCredentials(globalDir2, providers) {
5565
5941
  const filePath = credentialsPath(globalDir2);
5566
- fs31__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5942
+ fs32__default.default.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5567
5943
  const body = { version: 1, providers: providers.filter(isPersistable) };
5568
5944
  const tmp = `${filePath}.tmp`;
5569
- fs31__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5570
- 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);
5571
5947
  try {
5572
- fs31__default.default.chmodSync(filePath, 384);
5948
+ fs32__default.default.chmodSync(filePath, 384);
5573
5949
  } catch {
5574
5950
  }
5575
5951
  }
5576
5952
  function mergeGlobalCredentials(workspaceProviders, globalProviders) {
5577
5953
  const merged = [...workspaceProviders];
5578
- 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]));
5579
5955
  for (const g of globalProviders) {
5580
- 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));
5581
5964
  if (!existing) {
5582
5965
  merged.push({ ...g });
5583
- byKey.set(providerKey(g), merged[merged.length - 1]);
5966
+ if (g.type !== "azure") byKey.set(providerKey(g), merged[merged.length - 1]);
5584
5967
  continue;
5585
5968
  }
5586
- if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
5587
- if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
5588
- if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5589
- if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5590
- if (!existing.label && g.label) existing.label = g.label;
5969
+ fillFrom(existing, g);
5591
5970
  }
5592
5971
  return merged;
5593
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
+ }
5594
6029
 
5595
6030
  // src/config/retired-providers.ts
5596
6031
  var RETIRED_PROVIDER_TYPES = {
@@ -5600,7 +6035,7 @@ function isRetiredProviderType(type) {
5600
6035
  return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
5601
6036
  }
5602
6037
  function didCleanupChangeAnything(c) {
5603
- 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;
5604
6039
  }
5605
6040
  function stripRetiredProviders(raw) {
5606
6041
  const cleanup = { removed: [], clearedPins: [] };
@@ -5647,11 +6082,24 @@ function describeCleanup(c) {
5647
6082
  for (const type of c.removed) {
5648
6083
  parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
5649
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
+ }
5650
6094
  if (c.clearedPins.length > 0) {
5651
6095
  parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
5652
6096
  }
6097
+ if (parts.length === 0) return "";
5653
6098
  return `Cascade config migration: ${parts.join("; ")}.`;
5654
6099
  }
6100
+
6101
+ // src/config/index.ts
6102
+ init_revoked_credentials();
5655
6103
  var MCP_TOOL_PREFIX = "mcp__";
5656
6104
  var MAX_SEGMENT_LENGTH = 24;
5657
6105
  var SEGMENT_HASH_LENGTH = 8;
@@ -5833,10 +6281,31 @@ function removeMcpServerDenials(disabledTools, serverName) {
5833
6281
  // src/config/index.ts
5834
6282
  init_constants();
5835
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
+ }
5836
6294
  function hasUsableProvider(providers) {
5837
6295
  if (!providers?.length) return false;
5838
- 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 } : {} });
5839
6307
  }
6308
+ var ENV_ENDPOINT_CHANNEL = /* @__PURE__ */ new Set(["anthropic"]);
5840
6309
  var ConfigManager = class {
5841
6310
  config;
5842
6311
  keystore;
@@ -5856,6 +6325,16 @@ var ConfigManager = class {
5856
6325
  * fallback for a list that is empty for some unrelated reason.
5857
6326
  */
5858
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;
5859
6338
  /**
5860
6339
  * Human-readable migration notice held for a UI to display. `console.warn`
5861
6340
  * is not sufficient: the REPL clears the TTY immediately after load(), and
@@ -5871,7 +6350,7 @@ var ConfigManager = class {
5871
6350
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5872
6351
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5873
6352
  this.workspacePath = workspacePath;
5874
- 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);
5875
6354
  }
5876
6355
  async load() {
5877
6356
  this.config = await this.loadConfig();
@@ -5885,22 +6364,38 @@ var ConfigManager = class {
5885
6364
  this.ignore = new CascadeIgnore();
5886
6365
  await this.ignore.load(this.workspacePath);
5887
6366
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5888
- this.keystore = new Keystore(path34__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5889
- this.store = new MemoryStore(path34__default.default.join(this.workspacePath, CASCADE_DB_FILE));
5890
- const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
5891
- 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;
5892
6374
  try {
5893
6375
  saveGlobalCredentials(this.globalDir, globalCreds.kept);
5894
6376
  } catch (err) {
5895
6377
  console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
5896
6378
  }
5897
- this.retiredCleanup = {
5898
- removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
5899
- clearedPins: this.retiredCleanup?.clearedPins ?? []
5900
- };
6379
+ if (retiredHere) {
6380
+ this.retiredCleanup = {
6381
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
6382
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
6383
+ };
6384
+ }
5901
6385
  }
5902
- await this.injectEnvKeys();
6386
+ await this.injectEnvKeys(globalCreds.kept);
5903
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
+ }
5904
6399
  for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
5905
6400
  try {
5906
6401
  this.store.purgeCachedModelsForRetiredProvider(type);
@@ -5909,10 +6404,14 @@ var ConfigManager = class {
5909
6404
  }
5910
6405
  await this.ensureDefaultIdentity();
5911
6406
  if (mcpNamesChanged) await this.save();
6407
+ const notices = [];
5912
6408
  if (this.retiredCleanup) {
5913
- console.warn(describeCleanup(this.retiredCleanup));
5914
- this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
6409
+ const retired = describeCleanup(this.retiredCleanup);
6410
+ console.warn(retired);
6411
+ notices.push(retired);
5915
6412
  }
6413
+ if (this.revokedNotice) notices.push(this.revokedNotice);
6414
+ if (notices.length > 0) this.pendingRetiredNotice = notices.join(" ");
5916
6415
  }
5917
6416
  getConfig() {
5918
6417
  return this.config;
@@ -5932,12 +6431,18 @@ var ConfigManager = class {
5932
6431
  getWorkspacePath() {
5933
6432
  return this.workspacePath;
5934
6433
  }
5935
- async save() {
5936
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
5937
- await fs11__default.default.mkdir(path34__default.default.dirname(configPath), { recursive: true });
5938
- 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");
5939
6444
  try {
5940
- saveGlobalCredentials(this.globalDir, this.config.providers);
6445
+ saveGlobalCredentials(this.globalDir, config.providers);
5941
6446
  } catch (err) {
5942
6447
  console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
5943
6448
  }
@@ -5968,19 +6473,35 @@ var ConfigManager = class {
5968
6473
  const configProvider = this.config.providers.find((p) => p.type === provider);
5969
6474
  return configProvider?.apiKey;
5970
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
+ }
5971
6487
  async loadConfig() {
5972
6488
  this.retiredCleanup = void 0;
5973
- 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);
5974
6493
  try {
5975
6494
  const raw = await fs11__default.default.readFile(configPath, "utf-8");
5976
6495
  const parsed = JSON.parse(raw);
5977
6496
  const cleanup = stripRetiredProviders(parsed);
5978
- if (didCleanupChangeAnything(cleanup)) {
5979
- 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;
5980
6501
  try {
5981
6502
  await fs11__default.default.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
5982
6503
  } catch (err) {
5983
- 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)}`);
5984
6505
  }
5985
6506
  }
5986
6507
  return validateConfig(parsed);
@@ -5991,9 +6512,102 @@ var ConfigManager = class {
5991
6512
  throw err;
5992
6513
  }
5993
6514
  }
5994
- 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 = []) {
5995
6608
  const wasEmpty = this.config.providers.length === 0;
5996
- const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
6609
+ const emptiedByRetirement = wasEmpty && (!!this.retiredCleanup || this.revokedCredentials > 0);
6610
+ const mayCreate = (type) => wasEmpty || type === "anthropic" && this.revokedCredentials > 0;
5997
6611
  const envProviders = [
5998
6612
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5999
6613
  { env: "OPENAI_API_KEY", type: "openai" },
@@ -6003,11 +6617,66 @@ var ConfigManager = class {
6003
6617
  for (const { env, type } of envProviders) {
6004
6618
  const key = process.env[env];
6005
6619
  if (!key) continue;
6006
- const existing = this.config.providers.find((p) => p.type === type);
6007
- if (!existing && wasEmpty) {
6008
- this.config.providers.push({ type, apiKey: key });
6009
- } else if (existing && !existing.apiKey) {
6010
- 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
+ }
6011
6680
  }
6012
6681
  }
6013
6682
  if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
@@ -6359,15 +7028,15 @@ function SafeTextInput(props) {
6359
7028
  const displayValue = mask ? mask.repeat(value.length) : value;
6360
7029
  let rendered;
6361
7030
  if (displayValue.length === 0) {
6362
- 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) : "";
6363
7032
  } else if (!showCursor || !focus) {
6364
7033
  rendered = displayValue;
6365
7034
  } else {
6366
7035
  let out = "";
6367
7036
  for (let i = 0; i < displayValue.length; i++) {
6368
- out += i === cursorOffset ? chalk7__default.default.inverse(displayValue[i]) : displayValue[i];
7037
+ out += i === cursorOffset ? chalk8__default.default.inverse(displayValue[i]) : displayValue[i];
6369
7038
  }
6370
- if (cursorOffset >= displayValue.length) out += chalk7__default.default.inverse(" ");
7039
+ if (cursorOffset >= displayValue.length) out += chalk8__default.default.inverse(" ");
6371
7040
  rendered = out;
6372
7041
  }
6373
7042
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: rendered });
@@ -6981,13 +7650,13 @@ var DeadModelStore = class {
6981
7650
  function fileDeadModelPersistence(filePath) {
6982
7651
  return {
6983
7652
  load() {
6984
- if (!fs31.existsSync(filePath)) return [];
6985
- 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"));
6986
7655
  return Array.isArray(raw) ? raw : [];
6987
7656
  },
6988
7657
  save(records) {
6989
- fs31.mkdirSync(path34.dirname(filePath), { recursive: true });
6990
- 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));
6991
7660
  }
6992
7661
  };
6993
7662
  }
@@ -7411,7 +8080,7 @@ init_pricing();
7411
8080
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7412
8081
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7413
8082
  var FETCH_TIMEOUT_MS = 8e3;
7414
- 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");
7415
8084
  function normalizeModelId2(id) {
7416
8085
  let s = id.toLowerCase();
7417
8086
  const slash = s.lastIndexOf("/");
@@ -7558,7 +8227,7 @@ var LiveDataProvider = class {
7558
8227
  }
7559
8228
  async saveCache() {
7560
8229
  try {
7561
- 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 });
7562
8231
  const cache = {
7563
8232
  fetchedAt: this.fetchedAt,
7564
8233
  snapshot: this.snapshot ?? void 0,
@@ -7825,9 +8494,50 @@ function guardTokens(text) {
7825
8494
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
7826
8495
  var DISCOVERY_TIMEOUT_MS = 4e3;
7827
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
+ }
7828
8534
  function discoveryCacheKey(type, cfg) {
7829
- const raw = `${type}|${cfg.apiKey ?? ""}|${cfg.baseUrl ?? ""}`;
7830
- 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("|");
7831
8541
  }
7832
8542
  function applyTierLimits(options, tier, limits) {
7833
8543
  if (!limits) return options;
@@ -8126,7 +8836,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8126
8836
  await Promise.all(providers.map(async (type) => {
8127
8837
  if (!this.selector.isProviderAvailable(type)) return;
8128
8838
  const cfg = config.providers.find((p) => p.type === type);
8129
- if (!cfg?.apiKey) return;
8839
+ if (!cfg || !hasProviderCredential(cfg)) return;
8130
8840
  const cacheKey = discoveryCacheKey(type, cfg);
8131
8841
  let entry = cloudDiscoveryCache.get(cacheKey);
8132
8842
  if (!entry || Date.now() - entry.at > DISCOVERY_TTL_MS) {
@@ -8135,7 +8845,11 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
8135
8845
  try {
8136
8846
  const provider = this.createProvider(cfg, seed);
8137
8847
  if (typeof provider.listModels !== "function") return;
8138
- 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
+ );
8139
8853
  if (!models.length) return;
8140
8854
  entry = { models, ids: models.map((m) => m.id), at: Date.now() };
8141
8855
  cloudDiscoveryCache.set(cacheKey, entry);
@@ -10399,8 +11113,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
10399
11113
  // Media generation can run for a minute; without this a cancelled run
10400
11114
  // still pays for an image nobody will see.
10401
11115
  ...this.signal ? { signal: this.signal } : {},
10402
- saveSnapshot: async (path39, content) => {
10403
- this.store?.addFileSnapshot(this.taskId, path39, content);
11116
+ saveSnapshot: async (path40, content) => {
11117
+ this.store?.addFileSnapshot(this.taskId, path40, content);
10404
11118
  },
10405
11119
  sendPeerSync: (to, syncType, content) => {
10406
11120
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -10603,7 +11317,7 @@ ${assignment.expectedOutput}`;
10603
11317
  const { promisify: promisify4 } = await import('util');
10604
11318
  const execFileAsync2 = promisify4(execFile3);
10605
11319
  for (const artifactPath of artifactPaths) {
10606
- const absolutePath = path34__default.default.resolve(this.artifactRoot(), artifactPath);
11320
+ const absolutePath = path35__default.default.resolve(this.artifactRoot(), artifactPath);
10607
11321
  try {
10608
11322
  const stat = await fs11__default.default.stat(absolutePath);
10609
11323
  if (!stat.isFile()) {
@@ -10638,7 +11352,7 @@ ${assignment.expectedOutput}`;
10638
11352
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
10639
11353
  continue;
10640
11354
  }
10641
- const ext = path34__default.default.extname(absolutePath).toLowerCase();
11355
+ const ext = path35__default.default.extname(absolutePath).toLowerCase();
10642
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;
10643
11357
  if (check) {
10644
11358
  try {
@@ -10730,7 +11444,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10730
11444
  async checkAcceptance(assignment) {
10731
11445
  const criteria = assignment.acceptance ?? [];
10732
11446
  if (!criteria.length) return [];
10733
- const resolve2 = (target) => path34__default.default.resolve(this.artifactRoot(), target);
11447
+ const resolve2 = (target) => path35__default.default.resolve(this.artifactRoot(), target);
10734
11448
  return evaluateAcceptance(criteria, assignment.files ?? [], {
10735
11449
  stat: async (target) => {
10736
11450
  try {
@@ -13959,16 +14673,16 @@ function resolveInWorkspace(workspaceRoot, input) {
13959
14673
  if (typeof input !== "string" || input.length === 0) {
13960
14674
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
13961
14675
  }
13962
- const root = path34__default.default.resolve(workspaceRoot);
13963
- const abs = path34__default.default.isAbsolute(input) ? path34__default.default.resolve(input) : path34__default.default.resolve(root, input);
13964
- const rel = path34__default.default.relative(root, abs);
13965
- 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)) {
13966
14680
  throw new WorkspaceSandboxError(input, root);
13967
14681
  }
13968
14682
  try {
13969
- const real = fs31__default.default.realpathSync(abs);
13970
- const realRel = path34__default.default.relative(root, real);
13971
- 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))) {
13972
14686
  throw new WorkspaceSandboxError(input, root);
13973
14687
  }
13974
14688
  } catch (e) {
@@ -14621,7 +15335,7 @@ var GenerateDocumentTool = class extends BaseTool {
14621
15335
  }
14622
15336
  }
14623
15337
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
14624
- 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 });
14625
15339
  await fs11__default.default.writeFile(absPath, bytes);
14626
15340
  const charts = countCharts(content);
14627
15341
  return [
@@ -14842,7 +15556,7 @@ var FileWriteTool = class extends BaseTool {
14842
15556
  } catch {
14843
15557
  }
14844
15558
  }
14845
- 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 });
14846
15560
  await fs11__default.default.writeFile(absPath, content, "utf-8");
14847
15561
  return `Written ${content.length} characters to ${filePath}`;
14848
15562
  }
@@ -15340,7 +16054,7 @@ var ImageAnalyzeTool = class extends BaseTool {
15340
16054
  };
15341
16055
  async function fileToImageAttachment(filePath) {
15342
16056
  const data = await fs11__default.default.readFile(filePath);
15343
- const ext = path34__default.default.extname(filePath).toLowerCase();
16057
+ const ext = path35__default.default.extname(filePath).toLowerCase();
15344
16058
  const mimeMap = {
15345
16059
  ".jpg": "image/jpeg",
15346
16060
  ".jpeg": "image/jpeg",
@@ -15374,14 +16088,14 @@ var PDFCreateTool = class extends BaseTool {
15374
16088
  const filePath = input["path"];
15375
16089
  const content = input["content"];
15376
16090
  const title = input["title"];
15377
- const dir = path34__default.default.dirname(filePath);
15378
- if (!fs31__default.default.existsSync(dir)) {
15379
- 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 });
15380
16094
  }
15381
16095
  return new Promise((resolve2, reject) => {
15382
16096
  try {
15383
16097
  const doc = new PDFDocument__default.default({ margin: 50 });
15384
- const stream = fs31__default.default.createWriteStream(filePath);
16098
+ const stream = fs32__default.default.createWriteStream(filePath);
15385
16099
  doc.pipe(stream);
15386
16100
  if (title) {
15387
16101
  doc.info["Title"] = title;
@@ -15459,22 +16173,22 @@ var CodeInterpreterTool = class extends BaseTool {
15459
16173
  }
15460
16174
  cmdPrefix = NODE_CMD;
15461
16175
  }
15462
- const tmpDir = path34__default.default.join(this.workspaceRoot, ".cascade", "tmp");
15463
- if (!fs31__default.default.existsSync(tmpDir)) {
15464
- 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 });
15465
16179
  }
15466
16180
  const extension = language === "python" ? "py" : "js";
15467
16181
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
15468
- const filePath = path34__default.default.join(tmpDir, fileName);
15469
- 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");
15470
16184
  const execArgs = [filePath, ...args];
15471
16185
  return new Promise((resolve2) => {
15472
16186
  const startMs = Date.now();
15473
16187
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
15474
16188
  const duration = Date.now() - startMs;
15475
16189
  try {
15476
- if (fs31__default.default.existsSync(filePath)) {
15477
- fs31__default.default.unlinkSync(filePath);
16190
+ if (fs32__default.default.existsSync(filePath)) {
16191
+ fs32__default.default.unlinkSync(filePath);
15478
16192
  }
15479
16193
  } catch (cleanupErr) {
15480
16194
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -15786,7 +16500,7 @@ var GlobTool = class extends BaseTool {
15786
16500
  };
15787
16501
  async execute(input, _options) {
15788
16502
  const pattern = input["pattern"];
15789
- 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;
15790
16504
  const matches = await glob.glob(pattern, {
15791
16505
  cwd: searchPath,
15792
16506
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -15799,7 +16513,7 @@ var GlobTool = class extends BaseTool {
15799
16513
  const withMtime = await Promise.all(
15800
16514
  matches.map(async (rel) => {
15801
16515
  try {
15802
- 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));
15803
16517
  return { rel, mtime: stat.mtimeMs };
15804
16518
  } catch {
15805
16519
  return { rel, mtime: 0 };
@@ -15848,7 +16562,7 @@ var GrepTool = class extends BaseTool {
15848
16562
  };
15849
16563
  async execute(input, _options) {
15850
16564
  const pattern = input["pattern"];
15851
- 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;
15852
16566
  const globPattern = input["glob"];
15853
16567
  const outputMode = input["output_mode"] ?? "content";
15854
16568
  const context = input["context"] ?? 0;
@@ -15902,12 +16616,12 @@ var GrepTool = class extends BaseTool {
15902
16616
  nodir: true
15903
16617
  });
15904
16618
  } catch {
15905
- files = [path34__default.default.relative(searchPath, searchPath) || "."];
16619
+ files = [path35__default.default.relative(searchPath, searchPath) || "."];
15906
16620
  }
15907
16621
  const results = [];
15908
16622
  let totalCount = 0;
15909
16623
  for (const rel of files) {
15910
- const abs = path34__default.default.join(searchPath, rel);
16624
+ const abs = path35__default.default.join(searchPath, rel);
15911
16625
  let content;
15912
16626
  try {
15913
16627
  content = await fs11__default.default.readFile(abs, "utf-8");
@@ -16306,10 +17020,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
16306
17020
  }
16307
17021
  isIgnored(filePath) {
16308
17022
  if (!filePath) return false;
16309
- const abs = path34__default.default.resolve(this.workspaceRoot, filePath);
16310
- const rel = path34__default.default.relative(this.workspaceRoot, abs);
16311
- if (!rel || rel.startsWith("..") || path34__default.default.isAbsolute(rel)) return true;
16312
- 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("/");
16313
17027
  return this.ignoreMatcher.ignores(posixRel);
16314
17028
  }
16315
17029
  };
@@ -16543,22 +17257,22 @@ var FileMcpOAuthStore = class {
16543
17257
  filePath;
16544
17258
  load() {
16545
17259
  try {
16546
- return JSON.parse(fs31__default.default.readFileSync(this.filePath, "utf-8"));
17260
+ return JSON.parse(fs32__default.default.readFileSync(this.filePath, "utf-8"));
16547
17261
  } catch {
16548
17262
  return void 0;
16549
17263
  }
16550
17264
  }
16551
17265
  save(state) {
16552
- fs31__default.default.mkdirSync(path34__default.default.dirname(this.filePath), { recursive: true, mode: 448 });
16553
- 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 });
16554
17268
  try {
16555
- fs31__default.default.chmodSync(this.filePath, 384);
17269
+ fs32__default.default.chmodSync(this.filePath, 384);
16556
17270
  } catch {
16557
17271
  }
16558
17272
  }
16559
17273
  clear() {
16560
17274
  try {
16561
- fs31__default.default.rmSync(this.filePath);
17275
+ fs32__default.default.rmSync(this.filePath);
16562
17276
  } catch {
16563
17277
  }
16564
17278
  }
@@ -17206,7 +17920,7 @@ var TaskAnalyzer = class {
17206
17920
 
17207
17921
  // src/core/router/model-performance-tracker.ts
17208
17922
  init_pricing();
17209
- 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");
17210
17924
  var ModelPerformanceTracker = class {
17211
17925
  stats = /* @__PURE__ */ new Map();
17212
17926
  featureStats = /* @__PURE__ */ new Map();
@@ -17246,7 +17960,7 @@ var ModelPerformanceTracker = class {
17246
17960
  async save() {
17247
17961
  if (this.readOnly) return;
17248
17962
  try {
17249
- 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 });
17250
17964
  const modelsObj = {};
17251
17965
  const featuresObj = {};
17252
17966
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -17794,7 +18508,7 @@ Required capability: ${description.slice(0, 300)}`;
17794
18508
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
17795
18509
  async loadPersistedTools() {
17796
18510
  if (!this.workspacePath || !this.persistEnabled) return;
17797
- 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);
17798
18512
  try {
17799
18513
  const raw = await fs11__default.default.readFile(file, "utf-8");
17800
18514
  const specs = JSON.parse(raw);
@@ -17818,8 +18532,8 @@ Required capability: ${description.slice(0, 300)}`;
17818
18532
  }
17819
18533
  async persist() {
17820
18534
  if (!this.workspacePath || !this.persistEnabled) return;
17821
- const dir = path34__default.default.join(this.workspacePath, ".cascade");
17822
- 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);
17823
18537
  try {
17824
18538
  await fs11__default.default.mkdir(dir, { recursive: true });
17825
18539
  await fs11__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -17839,12 +18553,12 @@ var WorldStateDB = class {
17839
18553
  constructor(workspacePath, debugMode = false) {
17840
18554
  this.workspacePath = workspacePath;
17841
18555
  this.debugMode = debugMode;
17842
- const cascadeDir = path34__default.default.join(workspacePath, ".cascade");
17843
- if (!fs31__default.default.existsSync(cascadeDir)) {
17844
- 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 });
17845
18559
  }
17846
- this.keyPath = path34__default.default.join(cascadeDir, "world_state.key");
17847
- 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");
17848
18562
  this.initEncryptionKey();
17849
18563
  this.db = new Database2__default.default(this.dbPath);
17850
18564
  this.db.pragma("journal_mode = WAL");
@@ -17901,11 +18615,11 @@ var WorldStateDB = class {
17901
18615
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
17902
18616
  }
17903
18617
  initEncryptionKey() {
17904
- if (fs31__default.default.existsSync(this.keyPath)) {
17905
- 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);
17906
18620
  } else {
17907
18621
  this.encryptionKey = crypto__default.default.randomBytes(32);
17908
- fs31__default.default.writeFileSync(this.keyPath, this.encryptionKey);
18622
+ fs32__default.default.writeFileSync(this.keyPath, this.encryptionKey);
17909
18623
  }
17910
18624
  }
17911
18625
  encrypt(text) {
@@ -18191,9 +18905,9 @@ var WorldStateDB = class {
18191
18905
  dumpDebugIfNeeded() {
18192
18906
  if (!this.debugMode) return;
18193
18907
  try {
18194
- 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");
18195
18909
  const entries = this.getAllEntries();
18196
- 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");
18197
18911
  } catch (err) {
18198
18912
  console.error("Failed to dump debug world state", err);
18199
18913
  }
@@ -18249,11 +18963,11 @@ var ResumeStore = class {
18249
18963
  version: RESUME_SCHEMA_VERSION,
18250
18964
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
18251
18965
  };
18252
- await fs31.promises.mkdir(this.dir, { recursive: true });
18253
- 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`);
18254
18968
  const tempPath = `${finalPath}.${process.pid}.tmp`;
18255
- await fs31.promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
18256
- 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);
18257
18971
  await this.prune();
18258
18972
  return true;
18259
18973
  } catch {
@@ -18264,7 +18978,7 @@ var ResumeStore = class {
18264
18978
  async list() {
18265
18979
  let entries;
18266
18980
  try {
18267
- entries = await fs31.promises.readdir(this.dir);
18981
+ entries = await fs32.promises.readdir(this.dir);
18268
18982
  } catch {
18269
18983
  return [];
18270
18984
  }
@@ -18273,7 +18987,7 @@ var ResumeStore = class {
18273
18987
  const checkpoints = [];
18274
18988
  for (const candidate of candidates2) {
18275
18989
  try {
18276
- 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");
18277
18991
  const parsed = JSON.parse(raw);
18278
18992
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
18279
18993
  } catch {
@@ -18307,12 +19021,12 @@ var ResumeStore = class {
18307
19021
  const target = taskId ? all.find((c) => c.taskId === taskId) : all[0];
18308
19022
  if (!target) return null;
18309
19023
  try {
18310
- const entries = await fs31.promises.readdir(this.dir);
19024
+ const entries = await fs32.promises.readdir(this.dir);
18311
19025
  const wanted = safeId(target.taskId);
18312
19026
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
18313
19027
  if (!source) return null;
18314
19028
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
18315
- 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}`));
18316
19030
  return { checkpoint: target, claimId };
18317
19031
  } catch {
18318
19032
  return null;
@@ -18321,9 +19035,9 @@ var ResumeStore = class {
18321
19035
  /** Give a claim back, so the work stays recoverable after a failed start. */
18322
19036
  async release(claimId) {
18323
19037
  try {
18324
- await fs31.promises.rename(
18325
- path34__default.default.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18326
- 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`)
18327
19041
  );
18328
19042
  } catch {
18329
19043
  }
@@ -18331,7 +19045,7 @@ var ResumeStore = class {
18331
19045
  /** Discard a claim for good — the resumed run has taken ownership. */
18332
19046
  async settle(claimId) {
18333
19047
  try {
18334
- 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 });
18335
19049
  } catch {
18336
19050
  }
18337
19051
  }
@@ -18353,15 +19067,15 @@ var ResumeStore = class {
18353
19067
  async reclaimStale() {
18354
19068
  let reclaimed = 0;
18355
19069
  try {
18356
- const entries = await fs31.promises.readdir(this.dir);
19070
+ const entries = await fs32.promises.readdir(this.dir);
18357
19071
  const cutoff = this.now() - this.claimTimeoutMs;
18358
19072
  for (const name of entries) {
18359
19073
  if (!name.endsWith(CLAIM_SUFFIX)) continue;
18360
19074
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
18361
19075
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
18362
- await fs31.promises.rename(
18363
- path34__default.default.join(this.dir, name),
18364
- 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`)
18365
19079
  );
18366
19080
  reclaimed++;
18367
19081
  }
@@ -18372,12 +19086,12 @@ var ResumeStore = class {
18372
19086
  /** Remove every checkpoint for a task id. */
18373
19087
  async delete(taskId) {
18374
19088
  try {
18375
- const entries = await fs31.promises.readdir(this.dir);
19089
+ const entries = await fs32.promises.readdir(this.dir);
18376
19090
  const wanted = safeId(taskId);
18377
19091
  await Promise.all(entries.map(async (name) => {
18378
19092
  const match = FILE_PATTERN.exec(name);
18379
19093
  if (match?.[2] === wanted) {
18380
- 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 });
18381
19095
  }
18382
19096
  }));
18383
19097
  } catch {
@@ -18386,13 +19100,13 @@ var ResumeStore = class {
18386
19100
  /** Drop expired checkpoints, then any beyond the newest `maxCheckpoints`. */
18387
19101
  async prune() {
18388
19102
  try {
18389
- const entries = await fs31.promises.readdir(this.dir);
19103
+ const entries = await fs32.promises.readdir(this.dir);
18390
19104
  const cutoff = this.now() - this.maxAgeMs;
18391
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);
18392
19106
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
18393
19107
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
18394
19108
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
18395
- (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 })
18396
19110
  ));
18397
19111
  } catch {
18398
19112
  }
@@ -18769,7 +19483,7 @@ var WorkspaceIndex = class {
18769
19483
  isIgnored;
18770
19484
  chunker;
18771
19485
  constructor(opts) {
18772
- this.root = path34__default.default.resolve(opts.root);
19486
+ this.root = path35__default.default.resolve(opts.root);
18773
19487
  this.db = opts.db;
18774
19488
  this.namespace = opts.namespace ?? "code";
18775
19489
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -18801,14 +19515,14 @@ var WorkspaceIndex = class {
18801
19515
  return;
18802
19516
  }
18803
19517
  for (const entry of entries) {
18804
- const abs = path34__default.default.join(dir, entry.name);
19518
+ const abs = path35__default.default.join(dir, entry.name);
18805
19519
  if (entry.isDirectory()) {
18806
19520
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
18807
19521
  await walk(abs);
18808
19522
  } else if (entry.isFile()) {
18809
- const ext = path34__default.default.extname(entry.name).slice(1).toLowerCase();
19523
+ const ext = path35__default.default.extname(entry.name).slice(1).toLowerCase();
18810
19524
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
18811
- out.push(path34__default.default.relative(this.root, abs));
19525
+ out.push(path35__default.default.relative(this.root, abs));
18812
19526
  }
18813
19527
  }
18814
19528
  };
@@ -18838,7 +19552,7 @@ var WorkspaceIndex = class {
18838
19552
  const entries = [];
18839
19553
  for (const rel of rels) {
18840
19554
  try {
18841
- const abs = path34__default.default.join(this.root, rel);
19555
+ const abs = path35__default.default.join(this.root, rel);
18842
19556
  const stat = await fs11__default.default.stat(abs);
18843
19557
  if (stat.size > this.maxFileBytes) continue;
18844
19558
  const buf = await fs11__default.default.readFile(abs);
@@ -19328,13 +20042,13 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
19328
20042
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
19329
20043
  try {
19330
20044
  this.router.setDeadModelStore(new DeadModelStore(
19331
- fileDeadModelPersistence(path34__default.default.join(workspacePath, ".cascade", "dead-models.json"))
20045
+ fileDeadModelPersistence(path35__default.default.join(workspacePath, ".cascade", "dead-models.json"))
19332
20046
  ));
19333
20047
  } catch {
19334
20048
  }
19335
20049
  try {
19336
20050
  this.resumeStore = new ResumeStore({
19337
- dir: path34__default.default.join(workspacePath, ".cascade", "resume")
20051
+ dir: path35__default.default.join(workspacePath, ".cascade", "resume")
19338
20052
  });
19339
20053
  } catch {
19340
20054
  }
@@ -19422,8 +20136,8 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
19422
20136
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
19423
20137
  return;
19424
20138
  }
19425
- const dbPath = ci.dbPath || path34__default.default.join(this.workspacePath, ".cascade", "code-index.db");
19426
- 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 });
19427
20141
  const db = new Database2__default.default(dbPath);
19428
20142
  db.pragma("journal_mode = WAL");
19429
20143
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -20871,14 +21585,14 @@ ${finalOutput}` : ""
20871
21585
  init_constants();
20872
21586
  var CLOUD_SESSION_FILE = "cloud-session.json";
20873
21587
  function globalDir(homeDir = os10__default.default.homedir()) {
20874
- return path34__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
21588
+ return path35__default.default.join(homeDir, GLOBAL_CONFIG_DIR);
20875
21589
  }
20876
21590
  function cloudSessionPath(dir = globalDir()) {
20877
- return path34__default.default.join(dir, CLOUD_SESSION_FILE);
21591
+ return path35__default.default.join(dir, CLOUD_SESSION_FILE);
20878
21592
  }
20879
21593
  function loadCloudSession(dir = globalDir()) {
20880
21594
  try {
20881
- const raw = fs31__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
21595
+ const raw = fs32__default.default.readFileSync(cloudSessionPath(dir), "utf-8");
20882
21596
  const parsed = JSON.parse(raw);
20883
21597
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20884
21598
  return parsed;
@@ -20887,17 +21601,17 @@ function loadCloudSession(dir = globalDir()) {
20887
21601
  }
20888
21602
  }
20889
21603
  function saveCloudSession(session, dir = globalDir()) {
20890
- fs31__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
21604
+ fs32__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
20891
21605
  const file = cloudSessionPath(dir);
20892
- 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 });
20893
21607
  try {
20894
- fs31__default.default.chmodSync(file, 384);
21608
+ fs32__default.default.chmodSync(file, 384);
20895
21609
  } catch {
20896
21610
  }
20897
21611
  }
20898
21612
  function clearCloudSession(dir = globalDir()) {
20899
21613
  try {
20900
- fs31__default.default.rmSync(cloudSessionPath(dir));
21614
+ fs32__default.default.rmSync(cloudSessionPath(dir));
20901
21615
  } catch {
20902
21616
  }
20903
21617
  }
@@ -21069,16 +21783,16 @@ var CloudClient = class _CloudClient {
21069
21783
  return (await this.refresh()).accessToken;
21070
21784
  }
21071
21785
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
21072
- async authed(path39, init = {}) {
21073
- 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}` } });
21074
21788
  let res = await call(await this.accessToken());
21075
21789
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21076
21790
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21077
21791
  const text = await res.text();
21078
21792
  return text ? JSON.parse(text) : {};
21079
21793
  }
21080
- authedGet(path39) {
21081
- return this.authed(path39);
21794
+ authedGet(path40) {
21795
+ return this.authed(path40);
21082
21796
  }
21083
21797
  async fetchMe(token) {
21084
21798
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21939,7 +22653,7 @@ function StatusBarInternal({
21939
22653
  const savedStr = savedUsd > 0 ? ` \xB7 saved $${savedUsd.toFixed(4)}` : "";
21940
22654
  const rightStr = ` ${formatTokens(tokens)} \xB7 $${costUsd.toFixed(4)}${savedStr} ${isExecuting ? "\u26A1" : "\xB7"} `;
21941
22655
  const gap = Math.max(0, width - leftStr.length - rightStr.length);
21942
- 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);
21943
22657
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { children: line });
21944
22658
  }
21945
22659
  function formatTokens(n) {
@@ -22408,7 +23122,7 @@ function renderContent(content, theme) {
22408
23122
  i
22409
23123
  );
22410
23124
  }
22411
- 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));
22412
23126
  return /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { wrap: "wrap", children: rendered }, i);
22413
23127
  });
22414
23128
  }
@@ -22852,9 +23566,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22852
23566
  }
22853
23567
  } catch {
22854
23568
  }
22855
- const configPath = path34__default.default.join(workspacePath, ".cascade", "config.json");
23569
+ const configPath = path35__default.default.join(workspacePath, ".cascade", "config.json");
22856
23570
  try {
22857
- 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 });
22858
23572
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22859
23573
  } catch (err) {
22860
23574
  const msg = err instanceof Error ? err.message : String(err);
@@ -22906,9 +23620,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22906
23620
  if (msg.includes("non-text parts")) return;
22907
23621
  originalLog(...args);
22908
23622
  };
22909
- 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));
22910
23624
  storeRef.current = store;
22911
- 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));
22912
23626
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
22913
23627
  setIdentities(identityRows);
22914
23628
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -22988,7 +23702,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22988
23702
  onThemeChange: (name) => setTheme(getTheme(name)),
22989
23703
  onExport: async (fmt) => {
22990
23704
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22991
- 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"}`);
22992
23706
  if (fmt === "json") {
22993
23707
  await fs11__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
22994
23708
  } else {
@@ -23896,7 +24610,7 @@ function stringifySlashOutput(val) {
23896
24610
  }
23897
24611
  async function searchSessionsAndMessages(query, workspacePath) {
23898
24612
  if (!query) return "Usage: /search <query>";
23899
- const dbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
24613
+ const dbPath = path35__default.default.join(workspacePath, CASCADE_DB_FILE);
23900
24614
  try {
23901
24615
  await fs11__default.default.access(dbPath);
23902
24616
  } catch {
@@ -23930,7 +24644,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
23930
24644
  async function diagnoseRuntime(config, workspacePath) {
23931
24645
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
23932
24646
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
23933
- 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));
23934
24648
  try {
23935
24649
  const sessions2 = store.listSessions(void 0, 3);
23936
24650
  return [
@@ -23949,7 +24663,7 @@ async function diagnoseRuntime(config, workspacePath) {
23949
24663
  }
23950
24664
  async function showRecentLogs(args, workspacePath) {
23951
24665
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
23952
- 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));
23953
24667
  try {
23954
24668
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
23955
24669
  if (!logs.length) return "No recent runtime logs.";
@@ -23961,7 +24675,7 @@ async function showRecentLogs(args, workspacePath) {
23961
24675
  async function loadSessionSnapshot(args, workspacePath) {
23962
24676
  const sessionId = args[0];
23963
24677
  if (!sessionId) return "Usage: /resume <sessionId>";
23964
- 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));
23965
24679
  try {
23966
24680
  const session = store.getSession(sessionId);
23967
24681
  if (!session) return `Session not found: ${sessionId}`;
@@ -24381,9 +25095,9 @@ function SetupWizard({ workspacePath, onComplete }) {
24381
25095
  ...anyAuto ? { cascadeAuto: true } : {}
24382
25096
  };
24383
25097
  const config = CascadeConfigSchema.parse(rawConfig);
24384
- const configDir = path34__default.default.join(workspacePath, ".cascade");
25098
+ const configDir = path35__default.default.join(workspacePath, ".cascade");
24385
25099
  await fs11__default.default.mkdir(configDir, { recursive: true });
24386
- const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
25100
+ const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
24387
25101
  await fs11__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
24388
25102
  savedConfigRef.current = config;
24389
25103
  dispatchRef.current({ type: "GO_DONE" });
@@ -24724,25 +25438,25 @@ function printTelemetryBanner() {
24724
25438
  async function initCommand(workspacePath = process.cwd()) {
24725
25439
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
24726
25440
  try {
24727
- const configDir = path34__default.default.join(workspacePath, ".cascade");
25441
+ const configDir = path35__default.default.join(workspacePath, ".cascade");
24728
25442
  await fs11__default.default.mkdir(configDir, { recursive: true });
24729
- const mdPath = path34__default.default.join(workspacePath, "CASCADE.md");
25443
+ const mdPath = path35__default.default.join(workspacePath, "CASCADE.md");
24730
25444
  if (!await fileExists(mdPath)) {
24731
25445
  await createDefaultCascadeMd(workspacePath);
24732
- spin.succeed(chalk7__default.default.green("Created CASCADE.md"));
25446
+ spin.succeed(chalk8__default.default.green("Created CASCADE.md"));
24733
25447
  }
24734
- const ignorePath = path34__default.default.join(workspacePath, ".cascadeignore");
25448
+ const ignorePath = path35__default.default.join(workspacePath, ".cascadeignore");
24735
25449
  if (!await fileExists(ignorePath)) {
24736
25450
  await createDefaultIgnoreFile(workspacePath);
24737
- spin.succeed(chalk7__default.default.green("Created .cascadeignore"));
25451
+ spin.succeed(chalk8__default.default.green("Created .cascadeignore"));
24738
25452
  }
24739
25453
  spin.stop();
24740
25454
  console.log();
24741
- 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"));
24742
25456
  console.log();
24743
- const configPath = path34__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
25457
+ const configPath = path35__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
24744
25458
  if (await fileExists(configPath)) {
24745
- 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."));
24746
25460
  console.log();
24747
25461
  }
24748
25462
  const config = await runSetupWizard(workspacePath);
@@ -24750,10 +25464,10 @@ async function initCommand(workspacePath = process.cwd()) {
24750
25464
  await cm.load();
24751
25465
  await cm.updateConfig(config);
24752
25466
  console.log();
24753
- 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."));
24754
25468
  console.log();
24755
25469
  } catch (err) {
24756
- 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)}`));
24757
25471
  process.exit(1);
24758
25472
  }
24759
25473
  }
@@ -24768,7 +25482,20 @@ async function fileExists(p) {
24768
25482
 
24769
25483
  // src/cli/commands/doctor.ts
24770
25484
  init_constants();
25485
+
25486
+ // src/config/credential-discovery.ts
25487
+ init_revoked_credentials();
24771
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
+ ];
24772
25499
  async function readJson(file) {
24773
25500
  try {
24774
25501
  const raw = await fs11__default.default.readFile(file, "utf-8");
@@ -24792,36 +25519,118 @@ function fromEnv(env) {
24792
25519
  for (const { env: name, provider } of map) {
24793
25520
  const secret = str(env[name]);
24794
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
+ }
24795
25533
  seen.add(provider);
24796
- 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
+ });
24797
25604
  }
24798
25605
  return out;
24799
25606
  }
24800
- async function fromClaudeCode(home) {
24801
- 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");
24802
25610
  const data = await readJson(file);
24803
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
+ }
24804
25617
  const oauth = data["claudeAiOauth"];
24805
25618
  const oauthToken = str(oauth?.["accessToken"]);
24806
25619
  if (oauthToken) {
24807
- return [{
25620
+ found.push({
24808
25621
  provider: "anthropic",
24809
25622
  sourceTool: "Claude Code",
24810
25623
  kind: "oauth",
24811
25624
  secret: oauthToken,
24812
- directlyUsable: true,
24813
- warning: TOS_WARNING,
25625
+ directlyUsable: false,
25626
+ warning: ANTHROPIC_OAUTH_WARNING,
24814
25627
  sourcePath: file
24815
- }];
24816
- }
24817
- const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
24818
- if (apiKey) {
24819
- return [{ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file }];
25628
+ });
24820
25629
  }
24821
- return [];
25630
+ return found;
24822
25631
  }
24823
25632
  async function fromCodex(home) {
24824
- const file = path34__default.default.join(home, ".codex", "auth.json");
25633
+ const file = path35__default.default.join(home, ".codex", "auth.json");
24825
25634
  const data = await readJson(file);
24826
25635
  if (!data) return [];
24827
25636
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -24844,7 +25653,7 @@ async function fromCodex(home) {
24844
25653
  return [];
24845
25654
  }
24846
25655
  async function fromGemini(home) {
24847
- const file = path34__default.default.join(home, ".gemini", "oauth_creds.json");
25656
+ const file = path35__default.default.join(home, ".gemini", "oauth_creds.json");
24848
25657
  const data = await readJson(file);
24849
25658
  const accessToken = str(data?.["access_token"]);
24850
25659
  if (!accessToken) return [];
@@ -24860,7 +25669,7 @@ async function fromGemini(home) {
24860
25669
  }
24861
25670
  async function fromCopilot(home) {
24862
25671
  for (const name of ["apps.json", "hosts.json"]) {
24863
- const file = path34__default.default.join(home, ".config", "github-copilot", name);
25672
+ const file = path35__default.default.join(home, ".config", "github-copilot", name);
24864
25673
  const data = await readJson(file);
24865
25674
  if (!data) continue;
24866
25675
  for (const value of Object.values(data)) {
@@ -24885,7 +25694,7 @@ async function discoverCredentials(opts = {}) {
24885
25694
  const env = opts.env ?? process.env;
24886
25695
  const groups = await Promise.all([
24887
25696
  Promise.resolve(fromEnv(env)),
24888
- fromClaudeCode(home),
25697
+ fromClaudeCode(home, env),
24889
25698
  fromCodex(home),
24890
25699
  fromGemini(home),
24891
25700
  fromCopilot(home)
@@ -24896,10 +25705,257 @@ function maskSecret(secret) {
24896
25705
  if (secret.length <= 8) return "\u2022\u2022\u2022\u2022";
24897
25706
  return `${secret.slice(0, 6)}\u2026${secret.slice(-2)}`;
24898
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
+ }
24899
25951
 
24900
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
+ }
24901
25957
  async function doctorCommand() {
24902
- 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"));
24903
25959
  const checks = [];
24904
25960
  const nodeVersion = process.versions.node;
24905
25961
  const [major] = nodeVersion.split(".").map(Number);
@@ -24914,7 +25970,7 @@ async function doctorCommand() {
24914
25970
  checks.push({
24915
25971
  label: "Cascade config",
24916
25972
  ok: true,
24917
- detail: `Loaded ${path34__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
25973
+ detail: `Loaded ${path35__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24918
25974
  });
24919
25975
  const providers = [
24920
25976
  { type: "anthropic", name: "Anthropic" },
@@ -24924,10 +25980,11 @@ async function doctorCommand() {
24924
25980
  ];
24925
25981
  for (const { type, name } of providers) {
24926
25982
  const key = cm.getApiKey(type);
25983
+ const token = key ? void 0 : cm.getAuthToken(type);
24927
25984
  checks.push({
24928
- label: `${name} API key`,
24929
- ok: Boolean(key),
24930
- detail: key ? "Set" : "Missing"
25985
+ label: `${name} credential`,
25986
+ ok: Boolean(key || token),
25987
+ detail: key ? "API key set" : token ? "Bearer token set" : "Missing"
24931
25988
  });
24932
25989
  }
24933
25990
  const ollamaOk = await checkEndpoint(OLLAMA_BASE_URL + "/api/tags");
@@ -24981,32 +26038,31 @@ async function doctorCommand() {
24981
26038
  try {
24982
26039
  const discovered = await discoverCredentials();
24983
26040
  if (discovered.length > 0) {
24984
- const usable = discovered.filter((d) => d.directlyUsable).length;
24985
26041
  checks.push({
24986
26042
  label: "Linkable credentials",
24987
26043
  ok: true,
24988
- detail: `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt`
26044
+ detail: linkableCredentialsDetail(discovered, config.providers ?? [])
24989
26045
  });
24990
26046
  }
24991
26047
  } catch {
24992
26048
  }
24993
26049
  for (const c of checks) {
24994
- const icon = c.ok ? chalk7__default.default.green(" \u2713") : chalk7__default.default.yellow(" \u25CB");
24995
- const label = c.ok ? chalk7__default.default.white(c.label) : chalk7__default.default.gray(c.label);
24996
- 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}`) : "";
24997
26053
  console.log(`${icon} ${label}${detail}`);
24998
26054
  }
24999
26055
  const failures2 = checks.filter((c) => !c.ok);
25000
26056
  console.log();
25001
26057
  if (failures2.length === 0) {
25002
- console.log(chalk7__default.default.green(" All checks passed!\n"));
26058
+ console.log(chalk8__default.default.green(" All checks passed!\n"));
25003
26059
  } else {
25004
- 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"));
25005
26061
  if (critical.length) {
25006
- 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.
25007
26063
  `));
25008
26064
  } else {
25009
- 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.
25010
26066
  `));
25011
26067
  }
25012
26068
  }
@@ -25024,19 +26080,19 @@ async function checkEndpoint(url) {
25024
26080
  }
25025
26081
  }
25026
26082
  async function indexCommand(dirPath) {
25027
- const workspace = path34__default.default.resolve(dirPath || process.cwd());
26083
+ const workspace = path35__default.default.resolve(dirPath || process.cwd());
25028
26084
  const cm = new ConfigManager(workspace);
25029
26085
  await cm.load();
25030
26086
  const config = cm.getConfig();
25031
26087
  const embedder = embedderFromProviders(config.providers);
25032
26088
  if (!embedder) {
25033
- console.log(chalk7__default.default.red("\n No embeddings-capable provider configured."));
25034
- 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"));
25035
26091
  process.exitCode = 1;
25036
26092
  return;
25037
26093
  }
25038
- const dbPath = config.codeIndex?.dbPath || path34__default.default.join(workspace, ".cascade", "code-index.db");
25039
- 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 });
25040
26096
  const db = new Database2__default.default(dbPath);
25041
26097
  db.pragma("journal_mode = WAL");
25042
26098
  const complete = chatCompleterFromProviders(config.providers);
@@ -25050,20 +26106,20 @@ async function indexCommand(dirPath) {
25050
26106
  reranker,
25051
26107
  isIgnored: (abs) => ignore4.isIgnored(abs, workspace)
25052
26108
  });
25053
- console.log(chalk7__default.default.magenta(`
26109
+ console.log(chalk8__default.default.magenta(`
25054
26110
  \u25C8 Indexing ${workspace}
25055
26111
  `));
25056
26112
  let seen = 0;
25057
26113
  const res = await index.refresh(() => {
25058
26114
  seen++;
25059
- 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`));
25060
26116
  });
25061
26117
  console.log(
25062
- 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`)
25063
26119
  );
25064
- console.log(chalk7__default.default.dim(` Index: ${dbPath}`));
26120
+ console.log(chalk8__default.default.dim(` Index: ${dbPath}`));
25065
26121
  if (!config.codeIndex?.enabled) {
25066
- 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."));
25067
26123
  }
25068
26124
  console.log("");
25069
26125
  db.close();
@@ -25112,6 +26168,7 @@ async function decryptJSON(blob, passphrase) {
25112
26168
  }
25113
26169
 
25114
26170
  // src/cloud/keysync.ts
26171
+ init_revoked_credentials();
25115
26172
  function providerSig(p) {
25116
26173
  return [p.type, p.baseUrl ?? "", p.deploymentName ?? "", p.label ?? ""].join("\0");
25117
26174
  }
@@ -25184,6 +26241,19 @@ function gatherSyncBundle(config) {
25184
26241
  function applySyncBundle(bundle, config, cleanup) {
25185
26242
  const dropped = stripRetiredProviders(bundle);
25186
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
+ }
25187
26257
  const next = { ...config, tools: { ...config.tools } };
25188
26258
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
25189
26259
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -25201,10 +26271,15 @@ function applySyncBundle(bundle, config, cleanup) {
25201
26271
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
25202
26272
  const merged = { ...next.models };
25203
26273
  const clearedFromMerge = clearRetiredPins(merged);
25204
- 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
+ }
25205
26278
  if (cleanup) {
26279
+ cleanup.revokedCredentials = revokedFromBundle;
26280
+ cleanup.unusableCredentials = unusableFromBundle;
25206
26281
  cleanup.clearedPins = ["t1", "t2", "t3"].filter(
25207
- (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
25208
26283
  );
25209
26284
  }
25210
26285
  if (bundle.prefs) {
@@ -25230,43 +26305,43 @@ function openBrowser(url) {
25230
26305
  }
25231
26306
  }
25232
26307
  function notSignedIn() {
25233
- 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"));
25234
26309
  process.exitCode = 1;
25235
26310
  }
25236
26311
  function promptHidden(question) {
25237
26312
  return new Promise((resolve2) => {
25238
26313
  const rl = readline__default.default.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
25239
- let masked = false;
26314
+ let masked2 = false;
25240
26315
  rl._writeToOutput = (s) => {
25241
- if (!masked || !process.stdin.isTTY) process.stdout.write(s);
26316
+ if (!masked2 || !process.stdin.isTTY) process.stdout.write(s);
25242
26317
  };
25243
26318
  rl.question(question, (answer) => {
25244
26319
  rl.close();
25245
26320
  process.stdout.write("\n");
25246
26321
  resolve2(answer.trim());
25247
26322
  });
25248
- masked = true;
26323
+ masked2 = true;
25249
26324
  });
25250
26325
  }
25251
26326
  async function loginCommand(opts = {}) {
25252
26327
  const serverUrl = resolveServerUrl(opts.server);
25253
26328
  const client = new CloudClient(serverUrl);
25254
- console.log(chalk7__default.default.magenta("\n \u25C8 Sign in to Cascade Cloud"));
25255
- 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}
25256
26331
  `));
25257
26332
  try {
25258
26333
  const session = await client.runDeviceLogin((d) => {
25259
- console.log(` 1. Open ${chalk7__default.default.cyan(d.verificationUri)}`);
25260
- console.log(` 2. Enter ${chalk7__default.default.bold.yellow(d.userCode)}`);
25261
- 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"));
25262
26337
  openBrowser(`${d.verificationUri}?code=${encodeURIComponent(d.userCode)}`);
25263
26338
  });
25264
26339
  const who = session.user.name || session.user.email || session.user.id;
25265
- console.log(chalk7__default.default.green(`
26340
+ console.log(chalk8__default.default.green(`
25266
26341
  \u2713 Signed in as ${who}
25267
26342
  `));
25268
26343
  } catch (err) {
25269
- console.log(chalk7__default.default.red(`
26344
+ console.log(chalk8__default.default.red(`
25270
26345
  ${err instanceof Error ? err.message : String(err)}
25271
26346
  `));
25272
26347
  process.exitCode = 1;
@@ -25275,11 +26350,11 @@ async function loginCommand(opts = {}) {
25275
26350
  async function logoutCommand() {
25276
26351
  const client = CloudClient.fromSession();
25277
26352
  if (!client) {
25278
- console.log(chalk7__default.default.dim("\n Not signed in.\n"));
26353
+ console.log(chalk8__default.default.dim("\n Not signed in.\n"));
25279
26354
  return;
25280
26355
  }
25281
26356
  await client.logout();
25282
- console.log(chalk7__default.default.green("\n \u2713 Signed out.\n"));
26357
+ console.log(chalk8__default.default.green("\n \u2713 Signed out.\n"));
25283
26358
  }
25284
26359
  async function whoamiCommand() {
25285
26360
  const client = CloudClient.fromSession();
@@ -25287,10 +26362,10 @@ async function whoamiCommand() {
25287
26362
  try {
25288
26363
  const me = await client.me();
25289
26364
  console.log(`
25290
- ${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}`) : ""}
25291
26366
  `);
25292
26367
  } catch (err) {
25293
- console.log(chalk7__default.default.red(`
26368
+ console.log(chalk8__default.default.red(`
25294
26369
  ${err instanceof Error ? err.message : String(err)}
25295
26370
  `));
25296
26371
  process.exitCode = 1;
@@ -25302,16 +26377,16 @@ async function sessionsCommand() {
25302
26377
  try {
25303
26378
  const convos = await client.listConversations();
25304
26379
  if (convos.length === 0) {
25305
- 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"));
25306
26381
  return;
25307
26382
  }
25308
- 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"));
25309
26384
  for (const c of convos) {
25310
- 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)")}`);
25311
26386
  }
25312
- 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"));
25313
26388
  } catch (err) {
25314
- console.log(chalk7__default.default.red(`
26389
+ console.log(chalk8__default.default.red(`
25315
26390
  ${err instanceof Error ? err.message : String(err)}
25316
26391
  `));
25317
26392
  process.exitCode = 1;
@@ -25325,9 +26400,9 @@ function formatCloudTranscript(messages) {
25325
26400
  for (const m of messages) {
25326
26401
  const n = m.siblingIds?.length ?? 1;
25327
26402
  const i = m.siblingIds && m.id ? m.siblingIds.indexOf(m.id) + 1 : 1;
25328
- const branch = n > 1 ? chalk7__default.default.yellow(` \u2039${i}/${n}\u203A`) : "";
25329
- const tag = m.id ? chalk7__default.default.dim(` [${shortId2(m.id)}]`) : "";
25330
- 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");
25331
26406
  lines.push(` ${who}${tag}${branch}`);
25332
26407
  lines.push(" " + m.content.replace(/\n/g, "\n ") + "\n");
25333
26408
  }
@@ -25343,23 +26418,23 @@ async function sessionShowCommand(idOrPrefix) {
25343
26418
  try {
25344
26419
  const match = await resolveConversation(client, idOrPrefix);
25345
26420
  if (!match) {
25346
- console.log(chalk7__default.default.red(`
26421
+ console.log(chalk8__default.default.red(`
25347
26422
  No chat matching "${idOrPrefix}".
25348
26423
  `));
25349
26424
  process.exitCode = 1;
25350
26425
  return;
25351
26426
  }
25352
26427
  const msgs = await client.getMessages(match.id);
25353
- console.log(chalk7__default.default.magenta(`
26428
+ console.log(chalk8__default.default.magenta(`
25354
26429
  \u25C8 ${match.title?.trim() || "(untitled)"}
25355
26430
  `));
25356
26431
  console.log(formatCloudTranscript(msgs));
25357
26432
  if (msgs.some((m) => (m.siblingIds?.length ?? 1) > 1)) {
25358
- 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"));
25359
26434
  }
25360
- 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"));
25361
26436
  } catch (err) {
25362
- console.log(chalk7__default.default.red(`
26437
+ console.log(chalk8__default.default.red(`
25363
26438
  ${err instanceof Error ? err.message : String(err)}
25364
26439
  `));
25365
26440
  process.exitCode = 1;
@@ -25374,7 +26449,7 @@ async function sessionBranchCommand(chat, message) {
25374
26449
  try {
25375
26450
  const match = await resolveConversation(client, chat);
25376
26451
  if (!match) {
25377
- console.log(chalk7__default.default.red(`
26452
+ console.log(chalk8__default.default.red(`
25378
26453
  No chat matching "${chat}".
25379
26454
  `));
25380
26455
  process.exitCode = 1;
@@ -25383,12 +26458,12 @@ async function sessionBranchCommand(chat, message) {
25383
26458
  const active = await client.getMessages(match.id);
25384
26459
  const targetId = resolveMessage(active, message) ?? message;
25385
26460
  const next = await client.selectBranch(match.id, targetId);
25386
- console.log(chalk7__default.default.magenta(`
26461
+ console.log(chalk8__default.default.magenta(`
25387
26462
  \u25C8 Switched branch \u2014 ${match.title?.trim() || "(untitled)"}
25388
26463
  `));
25389
26464
  console.log(formatCloudTranscript(next));
25390
26465
  } catch (err) {
25391
- console.log(chalk7__default.default.red(`
26466
+ console.log(chalk8__default.default.red(`
25392
26467
  ${err instanceof Error ? err.message : String(err)}
25393
26468
  `));
25394
26469
  process.exitCode = 1;
@@ -25400,7 +26475,7 @@ async function sessionRemoveMessageCommand(chat, message) {
25400
26475
  try {
25401
26476
  const match = await resolveConversation(client, chat);
25402
26477
  if (!match) {
25403
- console.log(chalk7__default.default.red(`
26478
+ console.log(chalk8__default.default.red(`
25404
26479
  No chat matching "${chat}".
25405
26480
  `));
25406
26481
  process.exitCode = 1;
@@ -25409,11 +26484,11 @@ async function sessionRemoveMessageCommand(chat, message) {
25409
26484
  const active = await client.getMessages(match.id);
25410
26485
  const targetId = resolveMessage(active, message) ?? message;
25411
26486
  const next = await client.deleteMessage(match.id, targetId);
25412
- 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"));
25413
26488
  if (next.length) console.log(formatCloudTranscript(next));
25414
- 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"));
25415
26490
  } catch (err) {
25416
- console.log(chalk7__default.default.red(`
26491
+ console.log(chalk8__default.default.red(`
25417
26492
  ${err instanceof Error ? err.message : String(err)}
25418
26493
  `));
25419
26494
  process.exitCode = 1;
@@ -25425,18 +26500,18 @@ async function sessionRenameCommand(chat, title) {
25425
26500
  try {
25426
26501
  const match = await resolveConversation(client, chat);
25427
26502
  if (!match) {
25428
- console.log(chalk7__default.default.red(`
26503
+ console.log(chalk8__default.default.red(`
25429
26504
  No chat matching "${chat}".
25430
26505
  `));
25431
26506
  process.exitCode = 1;
25432
26507
  return;
25433
26508
  }
25434
26509
  await client.renameConversation(match.id, title);
25435
- console.log(chalk7__default.default.green(`
26510
+ console.log(chalk8__default.default.green(`
25436
26511
  \u2713 Renamed to "${title}".
25437
26512
  `));
25438
26513
  } catch (err) {
25439
- console.log(chalk7__default.default.red(`
26514
+ console.log(chalk8__default.default.red(`
25440
26515
  ${err instanceof Error ? err.message : String(err)}
25441
26516
  `));
25442
26517
  process.exitCode = 1;
@@ -25448,18 +26523,18 @@ async function sessionDeleteCommand(chat) {
25448
26523
  try {
25449
26524
  const match = await resolveConversation(client, chat);
25450
26525
  if (!match) {
25451
- console.log(chalk7__default.default.red(`
26526
+ console.log(chalk8__default.default.red(`
25452
26527
  No chat matching "${chat}".
25453
26528
  `));
25454
26529
  process.exitCode = 1;
25455
26530
  return;
25456
26531
  }
25457
26532
  await client.deleteConversation(match.id);
25458
- console.log(chalk7__default.default.green(`
26533
+ console.log(chalk8__default.default.green(`
25459
26534
  \u2713 Deleted "${match.title?.trim() || "(untitled)"}".
25460
26535
  `));
25461
26536
  } catch (err) {
25462
- console.log(chalk7__default.default.red(`
26537
+ console.log(chalk8__default.default.red(`
25463
26538
  ${err instanceof Error ? err.message : String(err)}
25464
26539
  `));
25465
26540
  process.exitCode = 1;
@@ -25471,21 +26546,21 @@ async function syncPushCommand() {
25471
26546
  const cm = new ConfigManager(process.cwd());
25472
26547
  await cm.load();
25473
26548
  const bundle = gatherSyncBundle(cm.getConfig());
25474
- console.log(chalk7__default.default.magenta("\n \u25C8 Sync your settings to your account"));
25475
- 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"));
25476
26551
  const pass = await promptHidden(" Passphrase: ");
25477
26552
  if (!pass) {
25478
- console.log(chalk7__default.default.dim("\n Cancelled.\n"));
26553
+ console.log(chalk8__default.default.dim("\n Cancelled.\n"));
25479
26554
  return;
25480
26555
  }
25481
26556
  try {
25482
26557
  const blob = await encryptJSON(bundle, pass);
25483
26558
  const r = await client.pushSecrets(blob);
25484
- console.log(chalk7__default.default.green(`
26559
+ console.log(chalk8__default.default.green(`
25485
26560
  \u2713 Synced (v${r.version}). Run \`cascade sync pull\` on another device.
25486
26561
  `));
25487
26562
  } catch (err) {
25488
- console.log(chalk7__default.default.red(`
26563
+ console.log(chalk8__default.default.red(`
25489
26564
  ${err instanceof Error ? err.message : String(err)}
25490
26565
  `));
25491
26566
  process.exitCode = 1;
@@ -25498,27 +26573,27 @@ async function syncPullCommand() {
25498
26573
  try {
25499
26574
  ({ blob } = await client.pullSecrets());
25500
26575
  } catch (err) {
25501
- console.log(chalk7__default.default.red(`
26576
+ console.log(chalk8__default.default.red(`
25502
26577
  ${err instanceof Error ? err.message : String(err)}
25503
26578
  `));
25504
26579
  process.exitCode = 1;
25505
26580
  return;
25506
26581
  }
25507
26582
  if (!blob) {
25508
- 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"));
25509
26584
  return;
25510
26585
  }
25511
- 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"));
25512
26587
  const pass = await promptHidden(" Passphrase: ");
25513
26588
  if (!pass) {
25514
- console.log(chalk7__default.default.dim("\n Cancelled.\n"));
26589
+ console.log(chalk8__default.default.dim("\n Cancelled.\n"));
25515
26590
  return;
25516
26591
  }
25517
26592
  let bundle;
25518
26593
  try {
25519
26594
  bundle = await decryptJSON(blob, pass);
25520
26595
  } catch {
25521
- 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"));
25522
26597
  process.exitCode = 1;
25523
26598
  return;
25524
26599
  }
@@ -25527,13 +26602,13 @@ async function syncPullCommand() {
25527
26602
  await cm.load();
25528
26603
  const cleanup = { removed: [], clearedPins: [] };
25529
26604
  await cm.updateConfig(applySyncBundle(bundle, cm.getConfig(), cleanup));
25530
- 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"));
25531
26606
  if (didCleanupChangeAnything(cleanup)) {
25532
- console.log(chalk7__default.default.yellow(` ${describeCleanup(cleanup)}
26607
+ console.log(chalk8__default.default.yellow(` ${describeCleanup(cleanup)}
25533
26608
  `));
25534
26609
  }
25535
26610
  } catch (err) {
25536
- console.log(chalk7__default.default.red(`
26611
+ console.log(chalk8__default.default.red(`
25537
26612
  Could not apply your synced settings: ${err instanceof Error ? err.message : String(err)}
25538
26613
  `));
25539
26614
  process.exitCode = 1;
@@ -25549,7 +26624,7 @@ function openBrowser2(url) {
25549
26624
  }
25550
26625
  function storePathFor(name) {
25551
26626
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
25552
- 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`);
25553
26628
  }
25554
26629
  function defaultName(url) {
25555
26630
  try {
@@ -25560,8 +26635,8 @@ function defaultName(url) {
25560
26635
  }
25561
26636
  async function mcpConnectCommand(url, opts = {}) {
25562
26637
  const desiredName = opts.name?.trim() || defaultName(url);
25563
- console.log(chalk7__default.default.magenta("\n \u25C8 Connect an MCP server via OAuth"));
25564
- 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}
25565
26640
  `));
25566
26641
  try {
25567
26642
  const cm = new ConfigManager(process.cwd());
@@ -25577,7 +26652,7 @@ async function mcpConnectCommand(url, opts = {}) {
25577
26652
  store,
25578
26653
  clientName: "Cascade AI",
25579
26654
  openUrl: (u) => {
25580
- 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"));
25581
26656
  openBrowser2(u);
25582
26657
  }
25583
26658
  });
@@ -25587,11 +26662,11 @@ async function mcpConnectCommand(url, opts = {}) {
25587
26662
  config.tools.mcpServers = servers;
25588
26663
  config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
25589
26664
  await cm.updateConfig(config);
25590
- console.log(chalk7__default.default.green(`
26665
+ console.log(chalk8__default.default.green(`
25591
26666
  \u2713 Connected "${name}". Its tools are available to your runs.
25592
26667
  `));
25593
26668
  } catch (err) {
25594
- console.log(chalk7__default.default.red(`
26669
+ console.log(chalk8__default.default.red(`
25595
26670
  ${err instanceof Error ? err.message : String(err)}
25596
26671
  `));
25597
26672
  process.exitCode = 1;
@@ -25602,13 +26677,13 @@ async function mcpListCommand() {
25602
26677
  await cm.load();
25603
26678
  const servers = cm.getConfig().tools?.mcpServers ?? [];
25604
26679
  if (!servers.length) {
25605
- 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"));
25606
26681
  return;
25607
26682
  }
25608
- console.log(chalk7__default.default.magenta("\n \u25C8 MCP servers\n"));
26683
+ console.log(chalk8__default.default.magenta("\n \u25C8 MCP servers\n"));
25609
26684
  for (const s of servers) {
25610
- 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");
25611
- 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}`);
25612
26687
  }
25613
26688
  console.log();
25614
26689
  }
@@ -25619,7 +26694,7 @@ async function mcpRemoveCommand(name) {
25619
26694
  const servers = config.tools?.mcpServers ?? [];
25620
26695
  const match = servers.find((s) => s.name === name);
25621
26696
  if (!match) {
25622
- console.log(chalk7__default.default.red(`
26697
+ console.log(chalk8__default.default.red(`
25623
26698
  No MCP server named "${name}".
25624
26699
  `));
25625
26700
  process.exitCode = 1;
@@ -25635,7 +26710,7 @@ async function mcpRemoveCommand(name) {
25635
26710
  }
25636
26711
  }
25637
26712
  await cm.updateConfig(config);
25638
- console.log(chalk7__default.default.green(`
26713
+ console.log(chalk8__default.default.green(`
25639
26714
  \u2713 Removed "${name}".
25640
26715
  `));
25641
26716
  }
@@ -25649,16 +26724,302 @@ async function updateCommand() {
25649
26724
  const { stdout } = await execAsync2("npm show cascade-ai version", { timeout: 1e4 });
25650
26725
  const latest = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
25651
26726
  if (latest === CASCADE_VERSION) {
25652
- 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})`));
25653
26728
  return;
25654
26729
  }
25655
26730
  spin.text = `Updating cascade-ai ${CASCADE_VERSION} \u2192 ${latest}\u2026`;
25656
26731
  await execAsync2("npm install -g cascade-ai@latest", { timeout: 6e4 });
25657
- 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);
26993
+ } catch (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}` };
27003
+ }
27004
+ replaceInPlace(live, committed);
27005
+ return { ok: true, refused };
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 };
25658
27017
  } catch (err) {
25659
- spin.fail(chalk7__default.default.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
27018
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
25660
27019
  }
25661
27020
  }
27021
+
27022
+ // src/dashboard/server.ts
25662
27023
  init_constants();
25663
27024
  var JWT_ALGORITHM = "HS256";
25664
27025
  function createToken(user, secret) {
@@ -25906,12 +27267,22 @@ var DashboardSocket = class {
25906
27267
  });
25907
27268
  });
25908
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
+ */
25909
27279
  onConfigUpdate(callback) {
25910
27280
  this.io.on("connection", (socket) => {
25911
- socket.on("config:update", (payload) => {
25912
- if (typeof payload === "object" && payload !== null) {
25913
- callback(payload);
25914
- }
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
+ });
25915
27286
  });
25916
27287
  });
25917
27288
  }
@@ -26057,8 +27428,47 @@ function aggregateCostStats(sessions2, opts = {}) {
26057
27428
  }
26058
27429
 
26059
27430
  // src/dashboard/server.ts
26060
- 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))));
26061
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
+ }
26062
27472
  var DashboardServer = class {
26063
27473
  app;
26064
27474
  httpServer;
@@ -26133,40 +27543,21 @@ var DashboardServer = class {
26133
27543
  this.activeSessions.get(sessionId)?.rateLastRun(rating);
26134
27544
  });
26135
27545
  this.socket.onConfigGet((socketId) => {
26136
- this.socket.emitToSocket(socketId, "config:current", {
26137
- models: this.config.models ?? {},
26138
- budget: {
26139
- maxCostPerRun: this.config.budget?.maxCostPerRunUsd,
26140
- autoBias: this.config.autoBias
26141
- },
26142
- providersWithKey: (this.config.providers ?? []).filter((p) => typeof p.apiKey === "string" && p.apiKey.length > 0).map((p) => p.type)
26143
- });
27546
+ this.socket.emitToSocket(socketId, "config:current", settingsSnapshot(this.config));
26144
27547
  });
26145
- this.socket.onConfigUpdate((data) => {
26146
- if (data.keys) {
26147
- for (const [type, apiKey] of Object.entries(data.keys)) {
26148
- if (!apiKey) continue;
26149
- const provider = this.config.providers.find((p) => p.type === type);
26150
- if (provider) provider.apiKey = apiKey;
26151
- else this.config.providers.push({ type, apiKey });
26152
- }
26153
- }
26154
- if (data.models) {
26155
- const models = this.config.models;
26156
- for (const [tier, val] of Object.entries(data.models)) {
26157
- if (val && val !== "auto") models[tier] = val;
26158
- else delete models[tier];
26159
- }
26160
- }
26161
- if (data.budget) {
26162
- if (typeof data.budget.maxCostPerRun === "number") {
26163
- this.config.budget.maxCostPerRunUsd = data.budget.maxCostPerRun;
26164
- }
26165
- if (data.budget.autoBias === "balanced" || data.budget.autoBias === "quality" || data.budget.autoBias === "cost") {
26166
- this.config.autoBias = data.budget.autoBias;
26167
- }
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 };
26168
27558
  }
26169
- this.persistConfig();
27559
+ this.syncGlobalCredentials();
27560
+ return { refused, snapshot: settingsSnapshot(this.config) };
26170
27561
  });
26171
27562
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
26172
27563
  const sessionId = requestedSessionId ?? crypto.randomUUID();
@@ -26335,16 +27726,47 @@ var DashboardServer = class {
26335
27726
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
26336
27727
  * a write failure is logged but never crashes the running dashboard.
26337
27728
  */
26338
- persistConfig() {
26339
- try {
26340
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
26341
- fs31__default.default.mkdirSync(path34__default.default.dirname(configPath), { recursive: true });
26342
- fs31__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
26343
- } catch (err) {
26344
- console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
26345
- }
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() {
26346
27768
  try {
26347
- 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 ?? []);
26348
27770
  } catch (err) {
26349
27771
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
26350
27772
  }
@@ -26359,15 +27781,15 @@ var DashboardServer = class {
26359
27781
  resolveDashboardSecret() {
26360
27782
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
26361
27783
  if (fromConfig) return fromConfig;
26362
- 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);
26363
27785
  try {
26364
- if (fs31__default.default.existsSync(secretPath)) {
26365
- 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();
26366
27788
  if (existing.length >= 16) return existing;
26367
27789
  }
26368
27790
  const generated = crypto.randomUUID();
26369
- fs31__default.default.mkdirSync(path34__default.default.dirname(secretPath), { recursive: true });
26370
- 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 });
26371
27793
  if (this.config.dashboard.auth) {
26372
27794
  console.warn(
26373
27795
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -26394,7 +27816,7 @@ var DashboardServer = class {
26394
27816
  // ── Setup ─────────────────────────────────────
26395
27817
  getGlobalStore() {
26396
27818
  if (!this.globalStore) {
26397
- 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);
26398
27820
  this.globalStore = new MemoryStore(globalDbPath);
26399
27821
  }
26400
27822
  return this.globalStore;
@@ -26774,12 +28196,12 @@ ${prompt}`;
26774
28196
  }
26775
28197
  }
26776
28198
  watchRuntimeChanges() {
26777
- const workspaceDbPath = path34__default.default.join(this.workspacePath, CASCADE_DB_FILE);
26778
- 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);
26779
28201
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
26780
28202
  for (const watchPath of watchPaths) {
26781
- if (!fs31__default.default.existsSync(watchPath)) continue;
26782
- fs31__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
28203
+ if (!fs32__default.default.existsSync(watchPath)) continue;
28204
+ fs32__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
26783
28205
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
26784
28206
  });
26785
28207
  }
@@ -26901,7 +28323,7 @@ ${prompt}`;
26901
28323
  const sessionId = req.params.id;
26902
28324
  this.store.deleteSession(sessionId);
26903
28325
  this.store.deleteRuntimeSession(sessionId);
26904
- 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);
26905
28327
  const globalStore = new MemoryStore(globalDbPath);
26906
28328
  try {
26907
28329
  globalStore.deleteRuntimeSession(sessionId);
@@ -26994,7 +28416,7 @@ ${prompt}`;
26994
28416
  });
26995
28417
  this.app.delete("/api/sessions", auth2, (req, res) => {
26996
28418
  const body = req.body;
26997
- 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);
26998
28420
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
26999
28421
  const globalStore = new MemoryStore(globalDbPath);
27000
28422
  try {
@@ -27017,7 +28439,7 @@ ${prompt}`;
27017
28439
  });
27018
28440
  this.app.delete("/api/runtime", auth2, (_req, res) => {
27019
28441
  this.store.deleteAllRuntimeNodes();
27020
- 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);
27021
28443
  const globalStore = new MemoryStore(globalDbPath);
27022
28444
  try {
27023
28445
  globalStore.deleteAllRuntimeNodes();
@@ -27086,8 +28508,7 @@ ${prompt}`;
27086
28508
  res.json(log);
27087
28509
  });
27088
28510
  this.app.get("/api/config", auth2, (_req, res) => {
27089
- const safe = { ...this.config };
27090
- safe.providers = safe.providers.map((p) => ({ ...p, apiKey: p.apiKey ? "***" : void 0 }));
28511
+ const safe = redactProviderSecrets(this.config);
27091
28512
  res.json(safe);
27092
28513
  });
27093
28514
  this.app.put("/api/config", auth2, async (req, res) => {
@@ -27103,12 +28524,12 @@ ${prompt}`;
27103
28524
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
27104
28525
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
27105
28526
  try {
27106
- const configPath = path34__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
27107
- 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")) : {};
27108
28529
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
27109
28530
  const tmp = configPath + ".tmp";
27110
- fs31__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
27111
- 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);
27112
28533
  res.json({ ok: true });
27113
28534
  } catch (err) {
27114
28535
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -27136,7 +28557,7 @@ ${prompt}`;
27136
28557
  this.app.get("/api/runtime", auth2, (req, res) => {
27137
28558
  const scope = req.query["scope"] ?? "workspace";
27138
28559
  if (scope === "global") {
27139
- 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);
27140
28561
  const globalStore = new MemoryStore(globalDbPath);
27141
28562
  try {
27142
28563
  res.json({
@@ -27477,13 +28898,13 @@ ${prompt}`;
27477
28898
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
27478
28899
  }
27479
28900
  });
27480
- const prodPath = path34__default.default.resolve(__dirname$1, "../web/dist");
27481
- const devPath = path34__default.default.resolve(__dirname$1, "../../web/dist");
27482
- const webDistPath = fs31__default.default.existsSync(prodPath) ? prodPath : devPath;
27483
- 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)) {
27484
28905
  this.app.use(express__default.default.static(webDistPath));
27485
28906
  this.app.get("*", (_req, res) => {
27486
- res.sendFile(path34__default.default.join(webDistPath, "index.html"));
28907
+ res.sendFile(path35__default.default.join(webDistPath, "index.html"));
27487
28908
  });
27488
28909
  } else {
27489
28910
  this.app.get("/", (_req, res) => {
@@ -27504,7 +28925,7 @@ init_constants();
27504
28925
  async function dashboardCommand(config, workspacePath = process.cwd()) {
27505
28926
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
27506
28927
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
27507
- 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));
27508
28929
  const server = new DashboardServer(config, store, workspacePath);
27509
28930
  server.watchRuntimeChanges();
27510
28931
  const gThis = globalThis;
@@ -27516,10 +28937,10 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27516
28937
  process.once("exit", onExit);
27517
28938
  try {
27518
28939
  await server.start();
27519
- 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}`));
27520
28941
  server.refreshRuntime("workspace");
27521
28942
  server.refreshRuntime("global");
27522
- console.log(chalk7__default.default.gray(` Press Ctrl+C to stop
28943
+ console.log(chalk8__default.default.gray(` Press Ctrl+C to stop
27523
28944
  `));
27524
28945
  await new Promise(() => {
27525
28946
  });
@@ -27528,7 +28949,7 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27528
28949
  await server.stop().catch(() => {
27529
28950
  });
27530
28951
  onExit();
27531
- 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)}`));
27532
28953
  process.exit(1);
27533
28954
  }
27534
28955
  }
@@ -27536,18 +28957,18 @@ init_constants();
27536
28957
  function makeIdentityCommand(workspacePath = process.cwd()) {
27537
28958
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
27538
28959
  identity.command("list").description("List all available identities").action(() => {
27539
- 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));
27540
28961
  try {
27541
28962
  const identities = store.listIdentities();
27542
- console.log(chalk7__default.default.bold("\n Identities:"));
28963
+ console.log(chalk8__default.default.bold("\n Identities:"));
27543
28964
  if (identities.length === 0) {
27544
- console.log(chalk7__default.default.gray(" No identities found."));
28965
+ console.log(chalk8__default.default.gray(" No identities found."));
27545
28966
  } else {
27546
28967
  identities.forEach((id) => {
27547
- const defaultLabel = id.isDefault ? chalk7__default.default.green(" [Default]") : "";
27548
- console.log(` - ${chalk7__default.default.cyan(id.name)}${defaultLabel}`);
27549
- console.log(chalk7__default.default.gray(` ID: ${id.id}`));
27550
- 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}`));
27551
28972
  });
27552
28973
  }
27553
28974
  console.log();
@@ -27556,7 +28977,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27556
28977
  }
27557
28978
  });
27558
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) => {
27559
- 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));
27560
28981
  try {
27561
28982
  if (options.default) {
27562
28983
  const existingDefault = store.getDefaultIdentity();
@@ -27573,21 +28994,21 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27573
28994
  isDefault: !!options.default,
27574
28995
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
27575
28996
  });
27576
- console.log(chalk7__default.default.green(`
28997
+ console.log(chalk8__default.default.green(`
27577
28998
  Successfully created identity: ${name} (${id})`));
27578
- 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."));
27579
29000
  console.log();
27580
29001
  } finally {
27581
29002
  store.close();
27582
29003
  }
27583
29004
  });
27584
29005
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
27585
- 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));
27586
29007
  try {
27587
29008
  const identities = store.listIdentities();
27588
29009
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
27589
29010
  if (!match) {
27590
- console.error(chalk7__default.default.red(`
29011
+ console.error(chalk8__default.default.red(`
27591
29012
  Identity '${query}' not found.
27592
29013
  `));
27593
29014
  process.exit(1);
@@ -27597,7 +29018,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27597
29018
  store.updateIdentity(existingDefault.id, { isDefault: false });
27598
29019
  }
27599
29020
  store.updateIdentity(match.id, { isDefault: true });
27600
- console.log(chalk7__default.default.green(`
29021
+ console.log(chalk8__default.default.green(`
27601
29022
  Identity ${match.name} is now the default.
27602
29023
  `));
27603
29024
  } finally {
@@ -27608,7 +29029,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27608
29029
  }
27609
29030
  init_pricing();
27610
29031
  async function modelsCommand(options = {}) {
27611
- console.log(chalk7__default.default.magenta("\n \u25C8 Cascade Models\n"));
29032
+ console.log(chalk8__default.default.magenta("\n \u25C8 Cascade Models\n"));
27612
29033
  const cm = new ConfigManager(process.cwd());
27613
29034
  await cm.load();
27614
29035
  const config = cm.getConfig();
@@ -27616,16 +29037,16 @@ async function modelsCommand(options = {}) {
27616
29037
  try {
27617
29038
  await router.init(config);
27618
29039
  } catch (err) {
27619
- 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)}`));
27620
29041
  process.exit(1);
27621
29042
  }
27622
29043
  await withTimeout(router.refreshLiveData(), 6e3, "live data timeout").catch(() => {
27623
29044
  });
27624
29045
  const liveData = router.getLiveData();
27625
29046
  const tiers = [
27626
- { tier: "T1", label: "T1 Administrator", color: chalk7__default.default.hex("#4C8DFF") },
27627
- { tier: "T2", label: "T2 Manager", color: chalk7__default.default.hex("#38B0DE") },
27628
- { 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") }
27629
29050
  ];
27630
29051
  let anyMissing = false;
27631
29052
  let anyUnpriced = false;
@@ -27637,76 +29058,76 @@ async function modelsCommand(options = {}) {
27637
29058
  const costIn = unpriced ? "cost not tracked" : model.inputCostPer1kTokens === 0 ? "free" : `$${model.inputCostPer1kTokens.toFixed(4)}/1K in`;
27638
29059
  const costOut = unpriced ? "" : model.outputCostPer1kTokens === 0 ? "free" : `, $${model.outputCostPer1kTokens.toFixed(4)}/1K out`;
27639
29060
  const ctx = model.contextWindow >= 1e6 ? `${(model.contextWindow / 1e6).toFixed(1)}M ctx` : `${(model.contextWindow / 1e3).toFixed(0)}K ctx`;
27640
- const local = model.isLocal ? chalk7__default.default.gray(" [local]") : "";
27641
- 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}") : "";
27642
29063
  const bench = Math.round(benchmarkScore01(model, "mixed") * 100);
27643
29064
  console.log(
27644
- ` ${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
27645
29066
  );
27646
29067
  } else {
27647
- 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})`)}`);
27648
29069
  anyMissing = true;
27649
29070
  }
27650
29071
  }
27651
29072
  console.log();
27652
29073
  const providers = config.providers.map((p) => p.type).join(", ") || "(none)";
27653
- console.log(chalk7__default.default.gray(` Configured providers: ${providers}`));
29074
+ console.log(chalk8__default.default.gray(` Configured providers: ${providers}`));
27654
29075
  if (liveData) {
27655
29076
  const src = liveData.getDataSource();
27656
29077
  const gen = liveData.getGeneratedAt();
27657
29078
  const srcLabel = src === "live" ? "live (just fetched)" : src === "cache" ? "cached" : "bundled";
27658
29079
  const pricingMeta = pricingDatasetMeta();
27659
- console.log(chalk7__default.default.gray(
29080
+ console.log(chalk8__default.default.gray(
27660
29081
  ` Benchmark data: ${srcLabel}` + (gen ? ` \xB7 updated ${gen.slice(0, 10)}` : "") + ` \xB7 pricing: ${liveData.hasLivePricing() ? "live (OpenRouter)" : "dataset"} (baseline ${pricingMeta.generatedAt})`
27661
29082
  ));
27662
29083
  const drift = liveData.getPriceDisagreements();
27663
29084
  if (drift.length) {
27664
- console.log(chalk7__default.default.yellow(`
29085
+ console.log(chalk8__default.default.yellow(`
27665
29086
  ${drift.length} model${drift.length === 1 ? "" : "s"} priced differently by the live source than by the bundled dataset:`));
27666
29087
  for (const d of drift.slice(0, 10)) {
27667
- console.log(chalk7__default.default.gray(
29088
+ console.log(chalk8__default.default.gray(
27668
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)`
27669
29090
  ));
27670
29091
  }
27671
- 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."));
27672
29093
  }
27673
29094
  }
27674
29095
  if (anyUnpriced) {
27675
- console.log(chalk7__default.default.yellow(
29096
+ console.log(chalk8__default.default.yellow(
27676
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.'
27677
29098
  ));
27678
29099
  }
27679
29100
  if (options.verbose) {
27680
29101
  console.log();
27681
- console.log(chalk7__default.default.white(" Available models by provider:\n"));
29102
+ console.log(chalk8__default.default.white(" Available models by provider:\n"));
27682
29103
  const allProviderTypes = [...new Set(config.providers.map((p) => p.type))];
27683
29104
  for (const providerType of allProviderTypes) {
27684
29105
  const available = router.getModelsForProvider(providerType);
27685
29106
  if (available.length === 0) continue;
27686
- console.log(chalk7__default.default.gray(` ${providerType}:`));
29107
+ console.log(chalk8__default.default.gray(` ${providerType}:`));
27687
29108
  for (const m of available) {
27688
29109
  const override = config.models.t1 === m.id ? " \u2190 T1" : config.models.t2 === m.id ? " \u2190 T2" : config.models.t3 === m.id ? " \u2190 T3" : "";
27689
- 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)}`);
27690
29111
  }
27691
29112
  console.log();
27692
29113
  }
27693
29114
  }
27694
29115
  if (anyMissing) {
27695
- 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"));
27696
29117
  } else {
27697
- console.log(chalk7__default.default.green(" All tiers are configured.\n"));
29118
+ console.log(chalk8__default.default.green(" All tiers are configured.\n"));
27698
29119
  }
27699
29120
  }
27700
29121
  async function setModelCommand(action, tierArg, value) {
27701
29122
  const tier = (tierArg ?? "").toLowerCase();
27702
29123
  if (!["t1", "t2", "t3"].includes(tier)) {
27703
- console.error(chalk7__default.default.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
27704
- 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"));
27705
29126
  process.exit(1);
27706
29127
  }
27707
29128
  const override = action === "unset" ? "auto" : (value ?? "").trim();
27708
29129
  if (action === "set" && !override) {
27709
- 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")'));
27710
29131
  process.exit(1);
27711
29132
  }
27712
29133
  const cm = new ConfigManager(process.cwd());
@@ -27716,8 +29137,8 @@ async function setModelCommand(action, tierArg, value) {
27716
29137
  if (override === "auto") delete models[tier];
27717
29138
  else models[tier] = override;
27718
29139
  await cm.updateConfig({ models });
27719
- const label = override === "auto" ? chalk7__default.default.gray("auto (routing decides)") : chalk7__default.default.white(override);
27720
- 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(`
27721
29142
  \u2713 ${tier.toUpperCase()} model set to ${label}
27722
29143
  `));
27723
29144
  }
@@ -27731,8 +29152,8 @@ async function exportCommand(options = {}) {
27731
29152
  let store;
27732
29153
  try {
27733
29154
  const workspacePath = options.workspacePath ?? process.cwd();
27734
- const workspaceDbPath = path34__default.default.join(workspacePath, CASCADE_DB_FILE);
27735
- 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);
27736
29157
  let dbPath = globalDbPath;
27737
29158
  try {
27738
29159
  await fs11__default.default.access(workspaceDbPath);
@@ -27741,7 +29162,7 @@ async function exportCommand(options = {}) {
27741
29162
  }
27742
29163
  store = new MemoryStore(dbPath);
27743
29164
  } catch (err) {
27744
- 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)}`));
27745
29166
  process.exit(1);
27746
29167
  }
27747
29168
  try {
@@ -27749,7 +29170,7 @@ async function exportCommand(options = {}) {
27749
29170
  if (options.sessionId) {
27750
29171
  const session2 = store.getSession(options.sessionId);
27751
29172
  if (!session2) {
27752
- spin.fail(chalk7__default.default.red(`Session "${options.sessionId}" not found.`));
29173
+ spin.fail(chalk8__default.default.red(`Session "${options.sessionId}" not found.`));
27753
29174
  process.exit(1);
27754
29175
  }
27755
29176
  sessions2 = [session2];
@@ -27757,14 +29178,14 @@ async function exportCommand(options = {}) {
27757
29178
  const limit = options.last ?? 10;
27758
29179
  sessions2 = store.listSessions(void 0, limit);
27759
29180
  if (sessions2.length === 0) {
27760
- spin.warn(chalk7__default.default.yellow("No sessions found."));
29181
+ spin.warn(chalk8__default.default.yellow("No sessions found."));
27761
29182
  return;
27762
29183
  }
27763
29184
  const latest = sessions2[0];
27764
29185
  const full = store.getSession(latest.id);
27765
29186
  sessions2 = full ? [full] : [];
27766
29187
  if (sessions2.length === 0) {
27767
- spin.fail(chalk7__default.default.red("Could not load latest session."));
29188
+ spin.fail(chalk8__default.default.red("Could not load latest session."));
27768
29189
  process.exit(1);
27769
29190
  }
27770
29191
  }
@@ -27774,17 +29195,17 @@ async function exportCommand(options = {}) {
27774
29195
  const ext = format === "json" ? ".json" : ".md";
27775
29196
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
27776
29197
  const defaultFile = `cascade-export-${safeName}${ext}`;
27777
- 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);
27778
29199
  await fs11__default.default.writeFile(outPath, content, "utf-8");
27779
- 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)}`));
27780
29201
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
27781
29202
  console.log();
27782
- console.log(chalk7__default.default.gray(` Session: ${session.title}`));
27783
- console.log(chalk7__default.default.gray(` Messages: ${messageCount}`));
27784
- 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}`));
27785
29206
  console.log();
27786
29207
  } catch (err) {
27787
- 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)}`));
27788
29209
  process.exit(1);
27789
29210
  }
27790
29211
  }
@@ -27838,93 +29259,6 @@ function buildJsonExport(session) {
27838
29259
  };
27839
29260
  return JSON.stringify(payload, null, 2);
27840
29261
  }
27841
- async function linkCommand(target, options = {}) {
27842
- const found = await discoverCredentials();
27843
- if (found.length === 0) {
27844
- console.log(chalk7__default.default.yellow("\n No reusable credentials found.\n"));
27845
- console.log(chalk7__default.default.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
27846
- console.log(chalk7__default.default.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
27847
- return;
27848
- }
27849
- if (!target) {
27850
- printDiscovered(found);
27851
- return;
27852
- }
27853
- const provider = normalizeProvider(target);
27854
- if (!provider) {
27855
- console.log(chalk7__default.default.red(`
27856
- Unknown provider "${target}". Use one of: anthropic, openai, gemini.
27857
- `));
27858
- return;
27859
- }
27860
- const candidates2 = found.filter((c) => c.provider === provider);
27861
- const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
27862
- if (!chosen) {
27863
- console.log(chalk7__default.default.yellow(`
27864
- No detected credential maps to "${provider}".
27865
- `));
27866
- return;
27867
- }
27868
- if (!chosen.directlyUsable) {
27869
- console.log(chalk7__default.default.yellow(`
27870
- Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
27871
- if (chosen.warning) console.log(chalk7__default.default.gray(` ${chosen.warning}`));
27872
- console.log(chalk7__default.default.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
27873
- return;
27874
- }
27875
- if (chosen.kind === "oauth" && !options.acceptRisk) {
27876
- console.log(chalk7__default.default.yellow(`
27877
- ${chosen.sourceTool} provides a subscription OAuth token, not an API key.`));
27878
- if (chosen.warning) console.log(chalk7__default.default.gray(` ${chosen.warning}`));
27879
- console.log(chalk7__default.default.gray(" Re-run with --accept-risk to adopt it anyway:\n"));
27880
- console.log(chalk7__default.default.cyan(` cascade link ${provider} --accept-risk
27881
- `));
27882
- return;
27883
- }
27884
- await adoptCredential(chosen, options.workspace ?? process.cwd());
27885
- console.log(chalk7__default.default.green(`
27886
- \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
27887
- if (chosen.kind === "oauth") {
27888
- console.log(chalk7__default.default.gray(" Adopted as an OAuth bearer token \u2014 revoke it in the source tool to disable."));
27889
- }
27890
- console.log(chalk7__default.default.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
27891
- }
27892
- function printDiscovered(found) {
27893
- console.log(chalk7__default.default.magenta("\n \u25C8 Detected credentials\n"));
27894
- for (const c of found) {
27895
- const usable = c.directlyUsable ? chalk7__default.default.green("usable") : chalk7__default.default.yellow("needs vendor backend");
27896
- const kind = c.kind === "oauth" ? chalk7__default.default.yellow("oauth") : chalk7__default.default.gray("api-key");
27897
- console.log(` ${chalk7__default.default.white(c.provider.padEnd(18))} ${chalk7__default.default.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
27898
- console.log(chalk7__default.default.gray(` from ${c.sourceTool}`));
27899
- if (c.warning) console.log(chalk7__default.default.yellow(` \u26A0 ${c.warning}`));
27900
- }
27901
- console.log(chalk7__default.default.gray("\n Adopt one with: ") + chalk7__default.default.cyan("cascade link <provider> [--accept-risk]"));
27902
- console.log(chalk7__default.default.gray(" --accept-risk is required for subscription OAuth tokens.\n"));
27903
- }
27904
- function normalizeProvider(target) {
27905
- const t = target.toLowerCase();
27906
- if (t === "anthropic" || t === "claude" || t === "claude-code") return "anthropic";
27907
- if (t === "openai" || t === "codex" || t === "gpt") return "openai";
27908
- if (t === "gemini" || t === "google") return "gemini";
27909
- return null;
27910
- }
27911
- async function adoptCredential(cred, workspace) {
27912
- const cm = new ConfigManager(workspace);
27913
- await cm.load();
27914
- const config = cm.getConfig();
27915
- const next = {
27916
- type: cred.provider,
27917
- credentialSource: cred.sourceTool
27918
- };
27919
- if (cred.kind === "oauth" && cred.provider === "anthropic") {
27920
- next.authToken = cred.secret;
27921
- } else {
27922
- next.apiKey = cred.secret;
27923
- }
27924
- const providers = config.providers.filter((p) => p.type !== cred.provider);
27925
- providers.push(next);
27926
- await cm.updateConfig({ providers });
27927
- }
27928
29262
  async function telemetryCommand(action) {
27929
29263
  const cm = new ConfigManager(process.cwd());
27930
29264
  await cm.load();
@@ -27932,12 +29266,12 @@ async function telemetryCommand(action) {
27932
29266
  if (action === "status") {
27933
29267
  const state = config.telemetry?.enabled ? "ON" : "OFF";
27934
29268
  console.log();
27935
- console.log(chalk7__default.default.magenta(" \u25C8 Cascade Telemetry"));
29269
+ console.log(chalk8__default.default.magenta(" \u25C8 Cascade Telemetry"));
27936
29270
  console.log();
27937
- console.log(` Status: ${config.telemetry?.enabled ? chalk7__default.default.green(state) : chalk7__default.default.gray(state)}`);
27938
- 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)"));
27939
29273
  console.log();
27940
- 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"));
27941
29275
  console.log();
27942
29276
  return;
27943
29277
  }
@@ -27951,11 +29285,11 @@ async function telemetryCommand(action) {
27951
29285
  });
27952
29286
  console.log();
27953
29287
  if (enabled) {
27954
- console.log(chalk7__default.default.green(` \u2713 Telemetry enabled.`));
27955
- 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."));
27956
29290
  } else {
27957
- console.log(chalk7__default.default.yellow(` \u2713 Telemetry disabled.`));
27958
- 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."));
27959
29293
  }
27960
29294
  console.log();
27961
29295
  }
@@ -27965,11 +29299,11 @@ async function statsCommand() {
27965
29299
  await tracker.load();
27966
29300
  const all = tracker.getAll();
27967
29301
  if (all.size === 0) {
27968
- 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"));
27969
29303
  return;
27970
29304
  }
27971
- console.log(chalk7__default.default.magenta("\n \u25C8 Auto-Routing History\n"));
27972
- 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"));
27973
29307
  for (const taskType of TASK_TYPES) {
27974
29308
  const entries = [];
27975
29309
  for (const [key, stat] of all) {
@@ -27981,36 +29315,36 @@ async function statsCommand() {
27981
29315
  }
27982
29316
  if (entries.length === 0) continue;
27983
29317
  entries.sort((a, b) => b.successRate - a.successRate || b.samples - a.samples);
27984
- console.log(chalk7__default.default.bold(` ${taskType.toUpperCase()}`));
29318
+ console.log(chalk8__default.default.bold(` ${taskType.toUpperCase()}`));
27985
29319
  const header = ` ${"Model".padEnd(36)} ${"Success".padEnd(9)} ${"Samples".padEnd(9)} Avg cost`;
27986
- console.log(chalk7__default.default.dim(header));
27987
- 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)));
27988
29322
  for (const e of entries) {
27989
29323
  const pct = `${Math.round(e.successRate * 100)}%`;
27990
29324
  const cost = e.avgCostUsd < 1e-4 ? "<$0.0001" : `$${e.avgCostUsd.toFixed(4)}`;
27991
- 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;
27992
29326
  console.log(
27993
- ` ${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)}`
27994
29328
  );
27995
29329
  }
27996
29330
  console.log();
27997
29331
  }
27998
- 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"));
27999
29333
  }
28000
29334
 
28001
29335
  // src/cli/index.ts
28002
29336
  dotenv__default.default.config();
28003
29337
  function warnIfBuildIsStale() {
28004
29338
  try {
28005
- 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))));
28006
29340
  for (const rel of ["..", "../.."]) {
28007
- const pkgPath = path34__default.default.join(here, rel, "package.json");
28008
- if (!fs31__default.default.existsSync(pkgPath)) continue;
28009
- 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"));
28010
29344
  if (pkg.name !== "cascade-ai") continue;
28011
29345
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
28012
29346
  console.error(
28013
- chalk7__default.default.yellow(
29347
+ chalk8__default.default.yellow(
28014
29348
  `\u26A0 Stale build: compiled output is v${CASCADE_VERSION} but the source tree is v${pkg.version}.
28015
29349
  Run: npm install && npm run build`
28016
29350
  )
@@ -28109,7 +29443,7 @@ mcp.command("list").description("List configured MCP servers").action(async () =
28109
29443
  mcp.command("remove <name>").description("Remove an MCP server (and its stored OAuth tokens)").action(async (name) => {
28110
29444
  await mcpRemoveCommand(name);
28111
29445
  });
28112
- 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) => {
28113
29447
  await linkCommand(provider, { acceptRisk: opts.acceptRisk, workspace: process.cwd() });
28114
29448
  });
28115
29449
  program.command("update").description("Update Cascade to the latest version").action(async () => {
@@ -28135,7 +29469,7 @@ program.command("models [action] [tier] [value]").description("List AI models pe
28135
29469
  program.command("telemetry [action]").description("Toggle anonymous usage telemetry (on | off | status). Default: status").action(async (action) => {
28136
29470
  const normalized = (action ?? "status").toLowerCase();
28137
29471
  if (normalized !== "on" && normalized !== "off" && normalized !== "status") {
28138
- 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`));
28139
29473
  process.exit(1);
28140
29474
  }
28141
29475
  await telemetryCommand(normalized);
@@ -28157,15 +29491,15 @@ async function startRepl(options) {
28157
29491
  try {
28158
29492
  await cm.load();
28159
29493
  } catch (err) {
28160
- console.error(chalk7__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28161
- 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."));
28162
29496
  process.exit(1);
28163
29497
  }
28164
29498
  let config = cm.getConfig();
28165
29499
  const startupNotice = cm.takeRetiredNotice();
28166
29500
  const needsSetup = !hasUsableProvider(config.providers);
28167
29501
  if (needsSetup) {
28168
- 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"));
28169
29503
  console.log();
28170
29504
  config = await runSetupWizard(workspacePath);
28171
29505
  await cm.updateConfig(config);
@@ -28200,24 +29534,24 @@ async function runHeadless(prompt, options) {
28200
29534
  try {
28201
29535
  await cm.load();
28202
29536
  } catch (err) {
28203
- console.error(chalk7__default.default.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28204
- 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."));
28205
29539
  process.exit(1);
28206
29540
  }
28207
29541
  const config = cm.getConfig();
28208
29542
  const needsSetup = !hasUsableProvider(config.providers);
28209
29543
  if (needsSetup) {
28210
- 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."));
28211
29545
  process.exit(1);
28212
29546
  }
28213
29547
  const cascade = new Cascade(config, workspacePath);
28214
29548
  try {
28215
29549
  await cascade.init();
28216
29550
  } catch (err) {
28217
- 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)}`));
28218
29552
  process.exit(1);
28219
29553
  }
28220
- 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."));
28221
29555
  let lastProgress = "";
28222
29556
  cascade.on("tier:status", (ev) => {
28223
29557
  const action = ev?.currentAction?.trim();
@@ -28225,7 +29559,7 @@ async function runHeadless(prompt, options) {
28225
29559
  const line = ` \xB7 ${ev.role ?? ""} ${action}`.trimEnd();
28226
29560
  if (line === lastProgress) return;
28227
29561
  lastProgress = line;
28228
- console.error(chalk7__default.default.gray(line));
29562
+ console.error(chalk8__default.default.gray(line));
28229
29563
  });
28230
29564
  try {
28231
29565
  const result = await cascade.run({
@@ -28236,7 +29570,7 @@ async function runHeadless(prompt, options) {
28236
29570
  });
28237
29571
  process.stdout.write(result.output.trimEnd() + "\n");
28238
29572
  } catch (err) {
28239
- 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)}`));
28240
29574
  await cascade.close().catch(() => {
28241
29575
  });
28242
29576
  process.exit(1);
@@ -28248,9 +29582,9 @@ async function runHeadless(prompt, options) {
28248
29582
  function printBanner() {
28249
29583
  if (process.stdout.columns < 60) return;
28250
29584
  console.log();
28251
- console.log(" " + chalk7__default.default.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
28252
- 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));
28253
- 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"));
28254
29588
  console.log();
28255
29589
  }
28256
29590
  program.parse(process.argv);