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.js CHANGED
@@ -1,20 +1,20 @@
1
- import Anthropic from '@anthropic-ai/sdk';
2
- import OpenAI, { AzureOpenAI } from 'openai';
3
- import { GoogleGenAI, HarmBlockThreshold, HarmCategory } from '@google/genai';
4
1
  import dns from 'dns';
5
2
  import http, { createServer } from 'http';
6
3
  import https from 'https';
7
4
  import zlib from 'zlib';
8
5
  import { Readable } from 'stream';
6
+ import Anthropic from '@anthropic-ai/sdk';
7
+ import OpenAI, { AzureOpenAI } from 'openai';
8
+ import { HarmBlockThreshold, HarmCategory, GoogleGenAI } from '@google/genai';
9
9
  import Database2 from 'better-sqlite3';
10
- import path34, { dirname } from 'path';
11
- import fs31, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
10
+ import path35, { dirname } from 'path';
11
+ import fs32, { mkdirSync, writeFileSync, existsSync, readFileSync, promises } from 'fs';
12
12
  import os10 from 'os';
13
13
  import crypto, { randomUUID, randomBytes, webcrypto, timingSafeEqual, createHash } from 'crypto';
14
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
15
15
  import { Command } from 'commander';
16
16
  import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
17
- import chalk7 from 'chalk';
17
+ import chalk8 from 'chalk';
18
18
  import dotenv from 'dotenv';
19
19
  import { fileURLToPath } from 'url';
20
20
  import fs11 from 'fs/promises';
@@ -1766,7 +1766,7 @@ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SEC
1766
1766
  var init_constants = __esm({
1767
1767
  "src/constants.ts"() {
1768
1768
  init_pricing();
1769
- CASCADE_VERSION = "0.74.0";
1769
+ CASCADE_VERSION = "0.75.0";
1770
1770
  CASCADE_CONFIG_FILE = ".cascade/config.json";
1771
1771
  CASCADE_DB_FILE = ".cascade/memory.db";
1772
1772
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -2195,6 +2195,244 @@ var init_constants = __esm({
2195
2195
  ];
2196
2196
  }
2197
2197
  });
2198
+ function preferIpv4Host(url) {
2199
+ return url;
2200
+ }
2201
+ function stripTrailingSlashes(value) {
2202
+ let end = value.length;
2203
+ while (end > 0 && value.charCodeAt(end - 1) === 47) end--;
2204
+ return end === value.length ? value : value.slice(0, end);
2205
+ }
2206
+ function normalizeEndpoint(url) {
2207
+ const raw = stripTrailingSlashes((url ?? "").trim());
2208
+ if (!raw) return "";
2209
+ try {
2210
+ const u = new URL(raw);
2211
+ const path40 = stripTrailingSlashes(`${u.pathname}${u.search}`);
2212
+ return `${u.protocol}//${u.host.toLowerCase()}${path40}`;
2213
+ } catch {
2214
+ return raw.toLowerCase();
2215
+ }
2216
+ }
2217
+ function stripPathSegment(url, segment) {
2218
+ const trimmed = stripTrailingSlashes(url.trim());
2219
+ const suffix = `/${segment}`;
2220
+ return trimmed.endsWith(suffix) ? trimmed.slice(0, -suffix.length) : trimmed;
2221
+ }
2222
+ function clientApiRoot(type, url) {
2223
+ const segment = CLIENT_VERSION_SEGMENT[type];
2224
+ return segment ? stripPathSegment(url, segment) : stripTrailingSlashes(url.trim());
2225
+ }
2226
+ function headersFrom(raw) {
2227
+ const out = new Headers();
2228
+ for (const [k, v] of Object.entries(raw)) {
2229
+ if (k === "content-encoding" || k === "content-length") continue;
2230
+ if (Array.isArray(v)) out.set(k, v.join(", "));
2231
+ else if (typeof v === "string") out.set(k, v);
2232
+ }
2233
+ return out;
2234
+ }
2235
+ async function fetchSameOrigin(url, init = {}, maxRedirects = MAX_REDIRECTS) {
2236
+ const origin = new URL(url).origin;
2237
+ let current = url;
2238
+ let options = { ...init };
2239
+ for (let hop = 0; hop <= maxRedirects; hop++) {
2240
+ const res = await fetch(current, { ...options, redirect: "manual" });
2241
+ if (![301, 302, 303, 307, 308].includes(res.status)) return res;
2242
+ const location = res.headers.get("location");
2243
+ if (!location) return res;
2244
+ await res.body?.cancel().catch(() => {
2245
+ });
2246
+ const next = new URL(location, current);
2247
+ if (next.origin !== origin) {
2248
+ throw new Error(
2249
+ `Refusing cross-origin redirect to ${next.origin}: the request carries a credential for ${origin}.`
2250
+ );
2251
+ }
2252
+ const method = (options.method ?? "GET").toUpperCase();
2253
+ const downgrades = res.status === 303 ? method !== "GET" && method !== "HEAD" : (res.status === 301 || res.status === 302) && method === "POST";
2254
+ if (downgrades) {
2255
+ const headers = new Headers(options.headers);
2256
+ for (const h of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2257
+ headers.delete(h);
2258
+ }
2259
+ options = { ...options, method: "GET", body: void 0, headers };
2260
+ }
2261
+ current = next.toString();
2262
+ }
2263
+ throw new Error(`Too many redirects from ${url}`);
2264
+ }
2265
+ async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
2266
+ const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2267
+ const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
2268
+ const lib = u.protocol === "https:" ? https : http;
2269
+ const method = (init.method ?? "GET").toUpperCase();
2270
+ const headers = {};
2271
+ const h = init.headers;
2272
+ if (h instanceof Headers) h.forEach((v, k) => {
2273
+ headers[k] = v;
2274
+ });
2275
+ else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
2276
+ else if (h) Object.assign(headers, h);
2277
+ if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
2278
+ headers["accept-encoding"] = "gzip, deflate, br";
2279
+ }
2280
+ const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
2281
+ return new Promise((resolve2, reject) => {
2282
+ const req = lib.request(
2283
+ {
2284
+ hostname: u.hostname,
2285
+ port: u.port || (u.protocol === "https:" ? 443 : 80),
2286
+ path: u.pathname + u.search,
2287
+ method,
2288
+ headers
2289
+ },
2290
+ (res) => {
2291
+ const status = res.statusCode ?? 200;
2292
+ const location = res.headers.location;
2293
+ if (REDIRECT_STATUSES.has(status) && location && redirectCount < MAX_REDIRECTS) {
2294
+ res.resume();
2295
+ const nextUrl = new URL(location, u).href;
2296
+ if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
2297
+ reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
2298
+ return;
2299
+ }
2300
+ const downgrade = status === 303 ? method !== "GET" && method !== "HEAD" : (status === 301 || status === 302) && method === "POST";
2301
+ let nextInit = init;
2302
+ if (downgrade) {
2303
+ const headers2 = new Headers(init.headers);
2304
+ for (const h2 of ["content-type", "content-length", "content-encoding", "content-language", "content-location"]) {
2305
+ headers2.delete(h2);
2306
+ }
2307
+ nextInit = { ...init, method: "GET", body: void 0, headers: headers2 };
2308
+ }
2309
+ resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
2310
+ return;
2311
+ }
2312
+ const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
2313
+ let bodyStream = res;
2314
+ if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib.createGunzip());
2315
+ else if (encoding === "deflate") bodyStream = res.pipe(zlib.createInflate());
2316
+ else if (encoding === "br") bodyStream = res.pipe(zlib.createBrotliDecompress());
2317
+ if (NULL_BODY_STATUSES.has(status)) {
2318
+ bodyStream.resume();
2319
+ resolve2(new Response(null, {
2320
+ status,
2321
+ statusText: res.statusMessage ?? "",
2322
+ headers: headersFrom(res.headers)
2323
+ }));
2324
+ return;
2325
+ }
2326
+ const stream = Readable.toWeb(bodyStream);
2327
+ resolve2(new Response(stream, {
2328
+ status,
2329
+ statusText: res.statusMessage ?? "",
2330
+ headers: headersFrom(res.headers)
2331
+ }));
2332
+ }
2333
+ );
2334
+ req.on("error", reject);
2335
+ req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
2336
+ if (body !== void 0) req.write(body);
2337
+ req.end();
2338
+ });
2339
+ }
2340
+ var CLIENT_VERSION_SEGMENT, REDIRECT_STATUSES, NULL_BODY_STATUSES, MAX_REDIRECTS;
2341
+ var init_net = __esm({
2342
+ "src/utils/net.ts"() {
2343
+ try {
2344
+ dns.setDefaultResultOrder("ipv4first");
2345
+ } catch {
2346
+ }
2347
+ CLIENT_VERSION_SEGMENT = {
2348
+ anthropic: "v1",
2349
+ gemini: "v1beta"
2350
+ };
2351
+ REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
2352
+ NULL_BODY_STATUSES = /* @__PURE__ */ new Set([204, 205, 304]);
2353
+ MAX_REDIRECTS = 5;
2354
+ }
2355
+ });
2356
+
2357
+ // src/config/revoked-credentials.ts
2358
+ function isRevokedSubscriptionCredential(p) {
2359
+ if (typeof p !== "object" || p === null) return false;
2360
+ if (p.type !== "anthropic") return false;
2361
+ if (!p.authToken && !p.apiKey) return false;
2362
+ return isSubscriptionToken(p.authToken) || isSubscriptionToken(p.apiKey) || Boolean(p.authToken) && /claude\s*code/i.test(p.credentialSource ?? "");
2363
+ }
2364
+ function isSubscriptionToken(secret) {
2365
+ return !!secret && secret.trim().startsWith("sk-ant-oat");
2366
+ }
2367
+ function stripRevokedCredentials(providers, { keepForEndpoint = true } = {}) {
2368
+ let removed = 0;
2369
+ const kept = [];
2370
+ for (const p of providers) {
2371
+ if (!isRevokedSubscriptionCredential(p)) {
2372
+ kept.push(p);
2373
+ continue;
2374
+ }
2375
+ removed++;
2376
+ const cleaned = { ...p };
2377
+ if (isSubscriptionToken(cleaned.apiKey)) delete cleaned.apiKey;
2378
+ const bearerIsDead = isSubscriptionToken(cleaned.authToken) || Boolean(cleaned.authToken) && /claude\s*code/i.test(cleaned.credentialSource ?? "");
2379
+ if (bearerIsDead) {
2380
+ delete cleaned.authToken;
2381
+ delete cleaned.credentialSource;
2382
+ }
2383
+ const worthKeeping = Boolean(
2384
+ cleaned.apiKey || cleaned.authToken || keepForEndpoint && cleaned.baseUrl
2385
+ );
2386
+ if (worthKeeping) kept.push(cleaned);
2387
+ }
2388
+ return { kept, removed };
2389
+ }
2390
+ function stripRevokedFromConfig(raw) {
2391
+ const result = { removed: 0 };
2392
+ if (typeof raw !== "object" || raw === null) return result;
2393
+ const cfg = raw;
2394
+ if (Array.isArray(cfg["providers"])) {
2395
+ const pass = stripRevokedCredentials(cfg["providers"]);
2396
+ result.removed = pass.removed;
2397
+ if (pass.removed > 0) cfg["providers"] = pass.kept;
2398
+ }
2399
+ return result;
2400
+ }
2401
+ function hasUsableAnthropic(providers) {
2402
+ return providers.some((p) => p?.type === "anthropic" && Boolean(p.apiKey || p.authToken && p.baseUrl));
2403
+ }
2404
+ function clearAnthropicPins(models, providers) {
2405
+ const cleared = [];
2406
+ if (typeof models !== "object" || models === null) return cleared;
2407
+ const tiers = models;
2408
+ for (const tier of ["t1", "t2", "t3"]) {
2409
+ const pin = tiers[tier];
2410
+ if (typeof pin !== "string" || !namesAnthropicModel(pin, providers)) continue;
2411
+ delete tiers[tier];
2412
+ cleared.push({ tier, model: pin });
2413
+ }
2414
+ return cleared;
2415
+ }
2416
+ function namesAnthropicModel(pin, providers) {
2417
+ const value = pin.trim().toLowerCase();
2418
+ if (!value) return false;
2419
+ if (value.includes(":")) return value.startsWith("anthropic:");
2420
+ const known = Object.values(MODELS).some(
2421
+ (m) => m.provider === "anthropic" && m.id.toLowerCase() === value
2422
+ );
2423
+ if (!known && !value.startsWith("claude-")) return false;
2424
+ return !canBeServedElsewhere(value, providers);
2425
+ }
2426
+ function canBeServedElsewhere(modelId, providers) {
2427
+ return providers.some((p) => p && p.type === "azure" && (p.deploymentName ?? "").trim().toLowerCase() === modelId);
2428
+ }
2429
+ var REVOKED_CREDENTIAL_REASON;
2430
+ var init_revoked_credentials = __esm({
2431
+ "src/config/revoked-credentials.ts"() {
2432
+ init_constants();
2433
+ REVOKED_CREDENTIAL_REASON = "Anthropic no longer permits third-party tools to use Claude subscription credentials and refuses them at the API, so the linked Claude Code token could not have worked. Add an API key from the Claude Console, or set ANTHROPIC_AUTH_TOKEN with a gateway.";
2434
+ }
2435
+ });
2198
2436
 
2199
2437
  // src/utils/cost.ts
2200
2438
  function resolveModelPricing(model, opts = {}) {
@@ -2347,6 +2585,8 @@ var init_model_filter = __esm({
2347
2585
  var anthropic_exports = {};
2348
2586
  __export(anthropic_exports, {
2349
2587
  AnthropicProvider: () => AnthropicProvider,
2588
+ anthropicApiRoot: () => anthropicApiRoot,
2589
+ anthropicAuth: () => anthropicAuth,
2350
2590
  toAnthropicToolUse: () => toAnthropicToolUse,
2351
2591
  toAnthropicTools: () => toAnthropicTools
2352
2592
  });
@@ -2371,25 +2611,56 @@ function toAnthropicToolUse(toolCalls) {
2371
2611
  input: tc.input
2372
2612
  }));
2373
2613
  }
2374
- var AnthropicProvider;
2614
+ function anthropicApiRoot(configured) {
2615
+ if (!configured) return void 0;
2616
+ const root = clientApiRoot("anthropic", configured);
2617
+ return root || void 0;
2618
+ }
2619
+ function anthropicAuth(config) {
2620
+ const gateway = anthropicApiRoot(config.baseUrl);
2621
+ const bearerIsSubscription = isSubscriptionToken(config.authToken);
2622
+ const keyIsSubscription = isSubscriptionToken(config.apiKey);
2623
+ if (bearerIsSubscription || keyIsSubscription) {
2624
+ if (config.apiKey && !keyIsSubscription) return { mode: "apiKey", key: config.apiKey };
2625
+ throw new Error(
2626
+ "This Anthropic credential is a Claude subscription token, which Anthropic does not permit third-party clients to use. Configure an API key from console.anthropic.com instead."
2627
+ );
2628
+ }
2629
+ if (config.authToken && gateway) return { mode: "bearer", token: config.authToken };
2630
+ if (config.apiKey) return { mode: "apiKey", key: config.apiKey };
2631
+ if (config.authToken) {
2632
+ throw new Error(
2633
+ "Anthropic bearer token configured without a gateway URL. A bearer is only valid at the gateway that issued it, so it will not be sent to the public API. Set `baseUrl` to that gateway, or configure `apiKey` instead."
2634
+ );
2635
+ }
2636
+ return { mode: "none" };
2637
+ }
2638
+ var sameOriginFetch, AnthropicProvider;
2375
2639
  var init_anthropic = __esm({
2376
2640
  "src/providers/anthropic.ts"() {
2377
2641
  init_constants();
2378
2642
  init_base();
2379
2643
  init_pricing();
2380
2644
  init_model_filter();
2645
+ init_net();
2646
+ init_revoked_credentials();
2647
+ sameOriginFetch = ((input, init) => {
2648
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2649
+ return fetchSameOrigin(url, init);
2650
+ });
2381
2651
  AnthropicProvider = class extends BaseProvider {
2382
2652
  client;
2383
2653
  constructor(config, model) {
2384
2654
  super(config, model);
2385
- if (config.authToken) {
2386
- this.client = new Anthropic({
2387
- authToken: config.authToken,
2388
- defaultHeaders: { "anthropic-beta": "oauth-2025-04-20" }
2389
- });
2655
+ const baseURL = anthropicApiRoot(config.baseUrl);
2656
+ const auth2 = anthropicAuth(config);
2657
+ if (auth2.mode === "bearer") {
2658
+ this.client = new Anthropic({ authToken: auth2.token, fetch: sameOriginFetch, baseURL });
2390
2659
  } else {
2391
2660
  this.client = new Anthropic({
2392
- apiKey: config.apiKey
2661
+ apiKey: auth2.mode === "apiKey" ? auth2.key : void 0,
2662
+ fetch: sameOriginFetch,
2663
+ ...baseURL ? { baseURL } : {}
2393
2664
  });
2394
2665
  }
2395
2666
  }
@@ -2465,21 +2736,24 @@ var init_anthropic = __esm({
2465
2736
  async countTokens(text) {
2466
2737
  return Math.ceil(text.length / 4);
2467
2738
  }
2468
- async listModels() {
2739
+ async listModels(options = {}) {
2740
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "anthropic");
2469
2741
  try {
2470
- const resp = await fetch("https://api.anthropic.com/v1/models", {
2742
+ const base = anthropicApiRoot(this.config.baseUrl) ?? "https://api.anthropic.com";
2743
+ const modelsUrl = `${base}/v1/models`;
2744
+ const auth2 = anthropicAuth(this.config);
2745
+ const resp = await fetchSameOrigin(modelsUrl, {
2471
2746
  headers: {
2472
- "x-api-key": this.config.apiKey ?? "",
2747
+ // Same decision the constructor makes — see anthropicAuth(). Read
2748
+ // straight off `config`, this branch sent a bearer to the public
2749
+ // default host whenever no gateway was configured.
2750
+ ...auth2.mode === "bearer" ? { authorization: `Bearer ${auth2.token}` } : { "x-api-key": auth2.mode === "apiKey" ? auth2.key : "" },
2473
2751
  "anthropic-version": "2023-06-01"
2474
2752
  }
2475
2753
  });
2476
- if (!resp.ok) {
2477
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2478
- }
2754
+ if (!resp.ok) return staticCatalog();
2479
2755
  const data = await resp.json();
2480
- if (!Array.isArray(data?.data)) {
2481
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2482
- }
2756
+ if (!Array.isArray(data?.data)) return staticCatalog();
2483
2757
  return data.data.filter((m) => isChatModel(m.id)).map((m) => {
2484
2758
  const known = Object.values(MODELS).find((km) => km.id === m.id && km.provider === "anthropic");
2485
2759
  if (known) return known;
@@ -2497,7 +2771,7 @@ var init_anthropic = __esm({
2497
2771
  });
2498
2772
  });
2499
2773
  } catch {
2500
- return Object.values(MODELS).filter((m) => m.provider === "anthropic");
2774
+ return staticCatalog();
2501
2775
  }
2502
2776
  }
2503
2777
  async isAvailable() {
@@ -2717,7 +2991,8 @@ var init_openai = __esm({
2717
2991
  async countTokens(text) {
2718
2992
  return Math.ceil(text.length / 4);
2719
2993
  }
2720
- async listModels() {
2994
+ async listModels(options = {}) {
2995
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "openai");
2721
2996
  try {
2722
2997
  const response = await this.client.models.list();
2723
2998
  return response.data.filter((m) => isChatModel(m.id)).map((m) => {
@@ -2737,7 +3012,7 @@ var init_openai = __esm({
2737
3012
  });
2738
3013
  });
2739
3014
  } catch {
2740
- return Object.values(MODELS).filter((m) => m.provider === "openai");
3015
+ return staticCatalog();
2741
3016
  }
2742
3017
  }
2743
3018
  async isAvailable() {
@@ -3039,9 +3314,46 @@ var init_gemini_schema = __esm({
3039
3314
  var gemini_exports = {};
3040
3315
  __export(gemini_exports, {
3041
3316
  GeminiProvider: () => GeminiProvider,
3317
+ geminiApiRoot: () => geminiApiRoot,
3042
3318
  toGeminiFunctionCalls: () => toGeminiFunctionCalls,
3043
3319
  toGeminiTools: () => toGeminiTools
3044
3320
  });
3321
+ async function* parseSseChunks(body) {
3322
+ const reader = body.getReader();
3323
+ const decoder = new TextDecoder();
3324
+ let buffered = "";
3325
+ try {
3326
+ for (; ; ) {
3327
+ const { done, value } = await reader.read();
3328
+ if (done) break;
3329
+ buffered += decoder.decode(value, { stream: true });
3330
+ let cut = buffered.indexOf("\n");
3331
+ while (cut !== -1) {
3332
+ const line = buffered.slice(0, cut).trim();
3333
+ buffered = buffered.slice(cut + 1);
3334
+ if (line.startsWith("data:")) {
3335
+ const payload = line.slice(5).trim();
3336
+ if (payload && payload !== "[DONE]") {
3337
+ try {
3338
+ yield JSON.parse(payload);
3339
+ } catch {
3340
+ }
3341
+ }
3342
+ }
3343
+ cut = buffered.indexOf("\n");
3344
+ }
3345
+ }
3346
+ } finally {
3347
+ reader.releaseLock();
3348
+ await body.cancel().catch(() => {
3349
+ });
3350
+ }
3351
+ }
3352
+ function geminiApiRoot(baseUrl) {
3353
+ const configured = baseUrl?.trim();
3354
+ if (!configured) return GEMINI_PUBLIC_ROOT;
3355
+ return clientApiRoot("gemini", configured) || GEMINI_PUBLIC_ROOT;
3356
+ }
3045
3357
  function toGeminiTools(tools) {
3046
3358
  return tools.map((tool) => ({
3047
3359
  name: tool.name,
@@ -3074,7 +3386,7 @@ async function describeGeminiError(resp) {
3074
3386
  return "";
3075
3387
  }
3076
3388
  }
3077
- var GeminiProvider;
3389
+ var GEMINI_API_VERSION, GEMINI_PUBLIC_ROOT, SAFETY_SETTINGS, GeminiProvider;
3078
3390
  var init_gemini = __esm({
3079
3391
  "src/providers/gemini.ts"() {
3080
3392
  init_constants();
@@ -3082,11 +3394,21 @@ var init_gemini = __esm({
3082
3394
  init_pricing();
3083
3395
  init_model_filter();
3084
3396
  init_gemini_schema();
3397
+ init_net();
3398
+ GEMINI_API_VERSION = "v1beta";
3399
+ GEMINI_PUBLIC_ROOT = "https://generativelanguage.googleapis.com";
3400
+ SAFETY_SETTINGS = [
3401
+ { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
3402
+ { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
3403
+ ];
3085
3404
  GeminiProvider = class extends BaseProvider {
3086
3405
  client;
3087
3406
  constructor(config, model) {
3088
3407
  super(config, model);
3089
- this.client = new GoogleGenAI({ apiKey: config.apiKey ?? "" });
3408
+ this.client = new GoogleGenAI({
3409
+ apiKey: config.apiKey ?? "",
3410
+ ...config.baseUrl ? { httpOptions: { baseUrl: geminiApiRoot(config.baseUrl) } } : {}
3411
+ });
3090
3412
  }
3091
3413
  async generate(options) {
3092
3414
  const chunks = [];
@@ -3094,15 +3416,12 @@ var init_gemini = __esm({
3094
3416
  }
3095
3417
  async generateStream(options, onChunk) {
3096
3418
  const contents = this.buildContents(options.messages, options.images);
3097
- const stream = await this.client.models.generateContentStream({
3419
+ const stream = this.config.baseUrl ? await this.streamOverRest(contents, options) : await this.client.models.generateContentStream({
3098
3420
  model: this.model.id,
3099
3421
  contents,
3100
3422
  config: {
3101
3423
  systemInstruction: options.systemPrompt,
3102
- safetySettings: [
3103
- { category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold: HarmBlockThreshold.BLOCK_NONE },
3104
- { category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold: HarmBlockThreshold.BLOCK_NONE }
3105
- ],
3424
+ safetySettings: SAFETY_SETTINGS,
3106
3425
  tools: options.tools?.length ? [{ functionDeclarations: toGeminiTools(options.tools) }] : void 0,
3107
3426
  abortSignal: options.signal
3108
3427
  }
@@ -3153,25 +3472,40 @@ var init_gemini = __esm({
3153
3472
  };
3154
3473
  }
3155
3474
  async countTokens(text) {
3475
+ const contents = [{ role: "user", parts: [{ text }] }];
3156
3476
  try {
3157
- const result = await this.client.models.countTokens({
3158
- model: this.model.id,
3159
- contents: [{ role: "user", parts: [{ text }] }]
3160
- });
3477
+ if (this.config.baseUrl) {
3478
+ const resp = await fetchSameOrigin(
3479
+ `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:countTokens`,
3480
+ {
3481
+ method: "POST",
3482
+ headers: {
3483
+ "x-goog-api-key": this.config.apiKey ?? "",
3484
+ "content-type": "application/json"
3485
+ },
3486
+ body: JSON.stringify({ contents })
3487
+ }
3488
+ );
3489
+ if (!resp.ok) return Math.ceil(text.length / 4);
3490
+ const data = await resp.json();
3491
+ return data.totalTokens ?? 0;
3492
+ }
3493
+ const result = await this.client.models.countTokens({ model: this.model.id, contents });
3161
3494
  return result.totalTokens ?? 0;
3162
3495
  } catch {
3163
3496
  return Math.ceil(text.length / 4);
3164
3497
  }
3165
3498
  }
3166
- async listModels() {
3499
+ async listModels(options = {}) {
3500
+ const staticCatalog = () => options.staticFallback === false ? [] : Object.values(MODELS).filter((m) => m.provider === "gemini");
3167
3501
  try {
3168
3502
  const resp = await this.fetchModelList();
3169
3503
  if (!resp.ok) {
3170
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3504
+ return staticCatalog();
3171
3505
  }
3172
3506
  const data = await resp.json();
3173
3507
  if (!Array.isArray(data?.models)) {
3174
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3508
+ return staticCatalog();
3175
3509
  }
3176
3510
  return data.models.filter((m) => isChatModel(m.name.replace("models/", ""), m.supportedGenerationMethods)).map((m) => {
3177
3511
  const id = m.name.replace("models/", "");
@@ -3193,7 +3527,7 @@ var init_gemini = __esm({
3193
3527
  });
3194
3528
  });
3195
3529
  } catch {
3196
- return Object.values(MODELS).filter((m) => m.provider === "gemini");
3530
+ return staticCatalog();
3197
3531
  }
3198
3532
  }
3199
3533
  /**
@@ -3246,8 +3580,43 @@ var init_gemini = __esm({
3246
3580
  * logs, error reports and shell history, and this one would carry the key
3247
3581
  * with it.
3248
3582
  */
3583
+ /**
3584
+ * Generation against a configured endpoint, issued by hand.
3585
+ *
3586
+ * Same wire protocol the SDK speaks — `:streamGenerateContent?alt=sse` on
3587
+ * `/v1beta`, with the same body — so the chunk shapes the caller walks are
3588
+ * identical and neither transport needs its own parsing. The difference that
3589
+ * matters is `fetchSameOrigin`: the key is never replayed on a host the
3590
+ * configured origin redirects to.
3591
+ */
3592
+ async streamOverRest(contents, options) {
3593
+ const url = `${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models/${encodeURIComponent(this.model.id)}:streamGenerateContent?alt=sse`;
3594
+ const resp = await fetchSameOrigin(url, {
3595
+ method: "POST",
3596
+ headers: {
3597
+ "x-goog-api-key": this.config.apiKey ?? "",
3598
+ "content-type": "application/json"
3599
+ },
3600
+ body: JSON.stringify({
3601
+ contents,
3602
+ // REST wants the object form; the SDK accepts a bare string and wraps
3603
+ // it itself.
3604
+ ...options.systemPrompt ? { systemInstruction: { parts: [{ text: options.systemPrompt }] } } : {},
3605
+ safetySettings: SAFETY_SETTINGS,
3606
+ ...options.tools?.length ? { tools: [{ functionDeclarations: toGeminiTools(options.tools) }] } : {}
3607
+ }),
3608
+ signal: options.signal
3609
+ });
3610
+ if (!resp.ok || !resp.body) {
3611
+ const detail = await resp.text().catch(() => "");
3612
+ throw new ProviderUnreachableError(
3613
+ `Gemini generation failed at ${geminiApiRoot(this.config.baseUrl)}: ${resp.status}${detail ? ` \u2014 ${detail.slice(0, 200)}` : ""}`
3614
+ );
3615
+ }
3616
+ return parseSseChunks(resp.body);
3617
+ }
3249
3618
  fetchModelList() {
3250
- return fetch("https://generativelanguage.googleapis.com/v1beta/models", {
3619
+ return fetchSameOrigin(`${geminiApiRoot(this.config.baseUrl)}/${GEMINI_API_VERSION}/models`, {
3251
3620
  headers: { "x-goog-api-key": this.config.apiKey ?? "" }
3252
3621
  });
3253
3622
  }
@@ -3335,84 +3704,6 @@ var init_gemini = __esm({
3335
3704
  };
3336
3705
  }
3337
3706
  });
3338
- function preferIpv4Host(url) {
3339
- return url;
3340
- }
3341
- async function nodeHttpFetch(input, init = {}, redirectCount = 0, opts = {}) {
3342
- const urlStr = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
3343
- const u = new URL(preferIpv4Host(urlStr) ?? urlStr);
3344
- const lib = u.protocol === "https:" ? https : http;
3345
- const method = (init.method ?? "GET").toUpperCase();
3346
- const headers = {};
3347
- const h = init.headers;
3348
- if (h instanceof Headers) h.forEach((v, k) => {
3349
- headers[k] = v;
3350
- });
3351
- else if (Array.isArray(h)) for (const [k, v] of h) headers[k] = v;
3352
- else if (h) Object.assign(headers, h);
3353
- if (!Object.keys(headers).some((k) => k.toLowerCase() === "accept-encoding")) {
3354
- headers["accept-encoding"] = "gzip, deflate, br";
3355
- }
3356
- const body = init.body == null ? void 0 : typeof init.body === "string" ? init.body : Buffer.from(init.body);
3357
- return new Promise((resolve2, reject) => {
3358
- const req = lib.request(
3359
- {
3360
- hostname: u.hostname,
3361
- port: u.port || (u.protocol === "https:" ? 443 : 80),
3362
- path: u.pathname + u.search,
3363
- method,
3364
- headers
3365
- },
3366
- (res) => {
3367
- const status = res.statusCode ?? 200;
3368
- const location = res.headers.location;
3369
- if (status >= 300 && status < 400 && location && redirectCount < MAX_REDIRECTS) {
3370
- res.resume();
3371
- const nextUrl = new URL(location, u).href;
3372
- if (opts.allowedRedirectOrigin && new URL(nextUrl).origin !== opts.allowedRedirectOrigin) {
3373
- reject(new Error(`Refusing cross-origin redirect to ${new URL(nextUrl).origin}`));
3374
- return;
3375
- }
3376
- const downgrade = status === 303 || (status === 301 || status === 302) && method !== "GET" && method !== "HEAD";
3377
- const nextInit = downgrade ? { ...init, method: "GET", body: void 0 } : init;
3378
- resolve2(nodeHttpFetch(nextUrl, nextInit, redirectCount + 1, opts));
3379
- return;
3380
- }
3381
- const encoding = (res.headers["content-encoding"] ?? "").toLowerCase();
3382
- let bodyStream = res;
3383
- if (encoding === "gzip" || encoding === "x-gzip") bodyStream = res.pipe(zlib.createGunzip());
3384
- else if (encoding === "deflate") bodyStream = res.pipe(zlib.createInflate());
3385
- else if (encoding === "br") bodyStream = res.pipe(zlib.createBrotliDecompress());
3386
- const stream = Readable.toWeb(bodyStream);
3387
- const respHeaders = new Headers();
3388
- for (const [k, v] of Object.entries(res.headers)) {
3389
- if (k === "content-encoding" || k === "content-length") continue;
3390
- if (Array.isArray(v)) respHeaders.set(k, v.join(", "));
3391
- else if (typeof v === "string") respHeaders.set(k, v);
3392
- }
3393
- resolve2(new Response(stream, {
3394
- status,
3395
- statusText: res.statusMessage ?? "",
3396
- headers: respHeaders
3397
- }));
3398
- }
3399
- );
3400
- req.on("error", reject);
3401
- req.setTimeout(15e3, () => req.destroy(new Error("request timed out")));
3402
- if (body !== void 0) req.write(body);
3403
- req.end();
3404
- });
3405
- }
3406
- var MAX_REDIRECTS;
3407
- var init_net = __esm({
3408
- "src/utils/net.ts"() {
3409
- try {
3410
- dns.setDefaultResultOrder("ipv4first");
3411
- } catch {
3412
- }
3413
- MAX_REDIRECTS = 5;
3414
- }
3415
- });
3416
3707
 
3417
3708
  // src/providers/ollama.ts
3418
3709
  var ollama_exports = {};
@@ -3701,12 +3992,38 @@ var init_openai_compatible = __esm({
3701
3992
  OpenAICompatibleProvider = class extends OpenAIProvider {
3702
3993
  constructor(config, model) {
3703
3994
  super({ ...config, apiKey: config.apiKey ?? "not-required" }, model);
3995
+ if (!config.baseUrl?.trim()) {
3996
+ throw new Error(
3997
+ "An OpenAI-compatible provider requires `baseUrl`: without one the OpenAI SDK sends to api.openai.com, so a key issued by another service would go to OpenAI."
3998
+ );
3999
+ }
3704
4000
  this.client = new OpenAI({
3705
4001
  apiKey: config.apiKey ?? "not-required",
3706
4002
  baseURL: preferIpv4Host(config.baseUrl),
3707
- fetch: nodeHttpFetch
4003
+ // Bound to the configured origin, NOT bare `nodeHttpFetch`. That helper
4004
+ // follows a 3xx by reusing `init` verbatim — headers included — so an
4005
+ // endpoint that redirected to another host was handed
4006
+ // `Authorization: Bearer <apiKey>` on the second one. This is the same
4007
+ // leak the Anthropic paths were fixed for; `allowedRedirectOrigin` exists
4008
+ // precisely for callers that attach a credential, and this one attaches
4009
+ // it on every request.
4010
+ fetch: ((input, init) => nodeHttpFetch(input, init ?? {}, 0, this.redirectPolicy()))
3708
4011
  });
3709
4012
  }
4013
+ /**
4014
+ * Redirects may only stay on the host the credential was configured for.
4015
+ *
4016
+ * An unparseable `baseUrl` yields no policy, which is the pre-existing
4017
+ * behaviour — but such a config cannot address anything anyway, so the
4018
+ * request fails before a redirect is possible.
4019
+ */
4020
+ redirectPolicy() {
4021
+ try {
4022
+ return { allowedRedirectOrigin: new URL(preferIpv4Host(this.config.baseUrl) ?? "").origin };
4023
+ } catch {
4024
+ return {};
4025
+ }
4026
+ }
3710
4027
  modelsUrl() {
3711
4028
  const base = (preferIpv4Host(this.config.baseUrl) ?? "").replace(/\/+$/, "");
3712
4029
  return base + "/models";
@@ -3717,7 +4034,7 @@ var init_openai_compatible = __esm({
3717
4034
  return h;
3718
4035
  }
3719
4036
  async listModels() {
3720
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4037
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3721
4038
  if (!res.ok) throw new Error(`models endpoint ${this.modelsUrl()} returned HTTP ${res.status}`);
3722
4039
  const body = await res.json();
3723
4040
  const raw = Array.isArray(body?.data) ? body.data : Array.isArray(body?.models) ? body.models : [];
@@ -3747,7 +4064,7 @@ var init_openai_compatible = __esm({
3747
4064
  }
3748
4065
  async isAvailable() {
3749
4066
  try {
3750
- const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() });
4067
+ const res = await nodeHttpFetch(this.modelsUrl(), { headers: this.authHeaders() }, 0, this.redirectPolicy());
3751
4068
  return res.ok;
3752
4069
  } catch {
3753
4070
  return false;
@@ -3769,12 +4086,12 @@ var init_audit_logger = __esm({
3769
4086
  constructor(workspacePath, debugMode = false) {
3770
4087
  this.workspacePath = workspacePath;
3771
4088
  this.debugMode = debugMode;
3772
- const cascadeDir = path34.join(workspacePath, ".cascade");
3773
- if (!fs31.existsSync(cascadeDir)) {
3774
- fs31.mkdirSync(cascadeDir, { recursive: true });
4089
+ const cascadeDir = path35.join(workspacePath, ".cascade");
4090
+ if (!fs32.existsSync(cascadeDir)) {
4091
+ fs32.mkdirSync(cascadeDir, { recursive: true });
3775
4092
  }
3776
- this.keyPath = path34.join(cascadeDir, "audit_log.key");
3777
- this.dbPath = path34.join(cascadeDir, "audit_log.db");
4093
+ this.keyPath = path35.join(cascadeDir, "audit_log.key");
4094
+ this.dbPath = path35.join(cascadeDir, "audit_log.db");
3778
4095
  this.initEncryptionKey();
3779
4096
  this.db = new Database2(this.dbPath);
3780
4097
  this.db.pragma("journal_mode = WAL");
@@ -3804,11 +4121,11 @@ var init_audit_logger = __esm({
3804
4121
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
3805
4122
  }
3806
4123
  initEncryptionKey() {
3807
- if (fs31.existsSync(this.keyPath)) {
3808
- this.encryptionKey = fs31.readFileSync(this.keyPath);
4124
+ if (fs32.existsSync(this.keyPath)) {
4125
+ this.encryptionKey = fs32.readFileSync(this.keyPath);
3809
4126
  } else {
3810
4127
  this.encryptionKey = crypto.randomBytes(32);
3811
- fs31.writeFileSync(this.keyPath, this.encryptionKey);
4128
+ fs32.writeFileSync(this.keyPath, this.encryptionKey);
3812
4129
  }
3813
4130
  }
3814
4131
  encrypt(text) {
@@ -3886,9 +4203,9 @@ var init_audit_logger = __esm({
3886
4203
  dumpDebugIfNeeded() {
3887
4204
  if (!this.debugMode) return;
3888
4205
  try {
3889
- const dumpPath = path34.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
4206
+ const dumpPath = path35.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
3890
4207
  const entries = this.getAllLogs();
3891
- fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
4208
+ fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
3892
4209
  } catch (err) {
3893
4210
  console.error("Failed to dump debug audit logs", err);
3894
4211
  }
@@ -3943,7 +4260,7 @@ var Keystore = class {
3943
4260
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
3944
4261
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
3945
4262
  this.backend = "keytar";
3946
- if (password && fs31.existsSync(this.storePath)) {
4263
+ if (password && fs32.existsSync(this.storePath)) {
3947
4264
  try {
3948
4265
  const fileEntries = this.decryptFile(password);
3949
4266
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -3962,7 +4279,7 @@ var Keystore = class {
3962
4279
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
3963
4280
  );
3964
4281
  }
3965
- if (!fs31.existsSync(this.storePath)) {
4282
+ if (!fs32.existsSync(this.storePath)) {
3966
4283
  const salt = crypto.randomBytes(SALT_LEN);
3967
4284
  this.masterKey = this.deriveKey(password, salt);
3968
4285
  this.writeWithSalt({}, salt);
@@ -3976,7 +4293,7 @@ var Keystore = class {
3976
4293
  }
3977
4294
  /** Synchronous legacy unlock kept for AES-only environments. */
3978
4295
  unlockSync(password) {
3979
- if (!fs31.existsSync(this.storePath)) {
4296
+ if (!fs32.existsSync(this.storePath)) {
3980
4297
  const salt = crypto.randomBytes(SALT_LEN);
3981
4298
  this.masterKey = this.deriveKey(password, salt);
3982
4299
  this.writeWithSalt({}, salt);
@@ -4034,7 +4351,7 @@ var Keystore = class {
4034
4351
  }
4035
4352
  }
4036
4353
  decryptFile(password, knownSalt) {
4037
- if (!fs31.existsSync(this.storePath)) return {};
4354
+ if (!fs32.existsSync(this.storePath)) return {};
4038
4355
  try {
4039
4356
  const { salt, ciphertext, iv, tag } = this.readRaw();
4040
4357
  const useSalt = knownSalt ?? salt;
@@ -4056,8 +4373,8 @@ var Keystore = class {
4056
4373
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4057
4374
  const tag = cipher.getAuthTag();
4058
4375
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
4059
- fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4060
- fs31.writeFileSync(this.storePath, out, { mode: 384 });
4376
+ fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
4377
+ fs32.writeFileSync(this.storePath, out, { mode: 384 });
4061
4378
  }
4062
4379
  writeWithSalt(data, salt) {
4063
4380
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -4067,11 +4384,11 @@ var Keystore = class {
4067
4384
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
4068
4385
  const tag = cipher.getAuthTag();
4069
4386
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
4070
- fs31.mkdirSync(path34.dirname(this.storePath), { recursive: true });
4071
- fs31.writeFileSync(this.storePath, out, { mode: 384 });
4387
+ fs32.mkdirSync(path35.dirname(this.storePath), { recursive: true });
4388
+ fs32.writeFileSync(this.storePath, out, { mode: 384 });
4072
4389
  }
4073
4390
  readRaw() {
4074
- const buf = fs31.readFileSync(this.storePath);
4391
+ const buf = fs32.readFileSync(this.storePath);
4075
4392
  let offset = 0;
4076
4393
  const salt = buf.subarray(offset, offset + SALT_LEN);
4077
4394
  offset += SALT_LEN;
@@ -4104,7 +4421,7 @@ var CascadeIgnore = class {
4104
4421
  ]);
4105
4422
  }
4106
4423
  async load(workspacePath) {
4107
- const filePath = path34.join(workspacePath, ".cascadeignore");
4424
+ const filePath = path35.join(workspacePath, ".cascadeignore");
4108
4425
  try {
4109
4426
  const content = await fs11.readFile(filePath, "utf-8");
4110
4427
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -4115,7 +4432,7 @@ var CascadeIgnore = class {
4115
4432
  }
4116
4433
  isIgnored(filePath, workspacePath) {
4117
4434
  try {
4118
- const relative = workspacePath ? path34.relative(workspacePath, filePath) : filePath;
4435
+ const relative = workspacePath ? path35.relative(workspacePath, filePath) : filePath;
4119
4436
  return this.ig.ignores(relative);
4120
4437
  } catch {
4121
4438
  return false;
@@ -4126,7 +4443,7 @@ var CascadeIgnore = class {
4126
4443
  }
4127
4444
  };
4128
4445
  async function createDefaultIgnoreFile(workspacePath) {
4129
- const filePath = path34.join(workspacePath, ".cascadeignore");
4446
+ const filePath = path35.join(workspacePath, ".cascadeignore");
4130
4447
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
4131
4448
  # Syntax identical to .gitignore
4132
4449
 
@@ -4156,7 +4473,7 @@ Thumbs.db
4156
4473
  await fs11.writeFile(filePath, content, "utf-8");
4157
4474
  }
4158
4475
  async function loadCascadeMd(workspacePath) {
4159
- const filePath = path34.join(workspacePath, "CASCADE.md");
4476
+ const filePath = path35.join(workspacePath, "CASCADE.md");
4160
4477
  try {
4161
4478
  const raw = await fs11.readFile(filePath, "utf-8");
4162
4479
  return parseCascadeMd(raw);
@@ -4185,7 +4502,7 @@ ${raw.trim()}`;
4185
4502
  return { raw, sections, systemPrompt };
4186
4503
  }
4187
4504
  async function createDefaultCascadeMd(workspacePath) {
4188
- const filePath = path34.join(workspacePath, "CASCADE.md");
4505
+ const filePath = path35.join(workspacePath, "CASCADE.md");
4189
4506
  const content = `# Cascade Project Instructions
4190
4507
 
4191
4508
  This file contains project-specific instructions for the Cascade AI agent.
@@ -4220,7 +4537,7 @@ List any areas the agent should not touch.
4220
4537
  var MemoryStore = class _MemoryStore {
4221
4538
  db;
4222
4539
  constructor(dbPath) {
4223
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
4540
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
4224
4541
  try {
4225
4542
  this.db = new Database2(dbPath, { timeout: 5e3 });
4226
4543
  this.db.pragma("journal_mode = WAL");
@@ -5484,21 +5801,80 @@ function validateConfig(raw) {
5484
5801
 
5485
5802
  // src/config/global-credentials.ts
5486
5803
  init_constants();
5487
- function credentialsPath(globalDir2) {
5488
- return path34.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5804
+
5805
+ // src/config/azure-endpoint.ts
5806
+ init_net();
5807
+ function normalizeAzureEndpoint(url) {
5808
+ return normalizeEndpoint(url);
5489
5809
  }
5490
- function providerKey(p) {
5491
- if (p.type === "azure") {
5492
- return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
5493
- }
5810
+ function sameAzureEndpoint(a, b) {
5811
+ return normalizeAzureEndpoint(a) === normalizeAzureEndpoint(b);
5812
+ }
5813
+
5814
+ // src/config/endpoint-identity.ts
5815
+ init_net();
5816
+ var DEFAULT_ENDPOINT = {
5817
+ anthropic: "https://api.anthropic.com",
5818
+ openai: "https://api.openai.com/v1",
5819
+ gemini: "https://generativelanguage.googleapis.com"
5820
+ };
5821
+ function hasDefaultEndpoint(type) {
5822
+ return Object.hasOwn(DEFAULT_ENDPOINT, type);
5823
+ }
5824
+ function credentialEndpointIdentity(type, baseUrl) {
5825
+ const configured = baseUrl?.trim();
5826
+ const effective = configured || DEFAULT_ENDPOINT[type];
5827
+ if (!effective) return null;
5828
+ return normalizeEndpoint(clientApiRoot(type, effective));
5829
+ }
5830
+ function sameCredentialEndpoint(type, a, b) {
5831
+ const left = credentialEndpointIdentity(type, a);
5832
+ const right = credentialEndpointIdentity(type, b);
5833
+ if (left === null && right === null) return true;
5834
+ if (left === null || right === null) return false;
5835
+ return left === right;
5836
+ }
5837
+ function credentialEndpointsConflict(type, rowEndpoint, credentialEndpoint) {
5838
+ const row = rowEndpoint?.trim() ? credentialEndpointIdentity(type, rowEndpoint) : null;
5839
+ const credential = credentialEndpointIdentity(type, credentialEndpoint);
5840
+ if (row === null) return false;
5841
+ if (credential === null) return true;
5842
+ return row !== credential;
5843
+ }
5844
+
5845
+ // src/config/global-credentials.ts
5846
+ function credentialsPath(globalDir2) {
5847
+ return path35.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
5848
+ }
5849
+ function providerKey(p) {
5494
5850
  return p.type;
5495
5851
  }
5852
+ function azureEndpoint(p) {
5853
+ return p.baseUrl === void 0 ? void 0 : normalizeAzureEndpoint(p.baseUrl);
5854
+ }
5855
+ function sameAzureEntry(a, b) {
5856
+ const strong = [
5857
+ [a.deploymentName, b.deploymentName],
5858
+ [azureEndpoint(a), azureEndpoint(b)]
5859
+ ];
5860
+ let shared = 0;
5861
+ for (const [x, y] of strong) {
5862
+ if (!x || !y) continue;
5863
+ if (x !== y) return false;
5864
+ shared++;
5865
+ }
5866
+ if (shared > 0) return true;
5867
+ const aNamesSomething = Boolean(a.deploymentName || azureEndpoint(a));
5868
+ const bNamesSomething = Boolean(b.deploymentName || azureEndpoint(b));
5869
+ if (aNamesSomething || bNamesSomething) return false;
5870
+ return Boolean(a.label && b.label && a.label === b.label);
5871
+ }
5496
5872
  function isPersistable(p) {
5497
5873
  return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
5498
5874
  }
5499
5875
  function loadGlobalCredentials(globalDir2) {
5500
5876
  try {
5501
- const raw = fs31.readFileSync(credentialsPath(globalDir2), "utf-8");
5877
+ const raw = fs32.readFileSync(credentialsPath(globalDir2), "utf-8");
5502
5878
  const parsed = JSON.parse(raw);
5503
5879
  if (!Array.isArray(parsed.providers)) return [];
5504
5880
  return parsed.providers.filter(
@@ -5510,34 +5886,93 @@ function loadGlobalCredentials(globalDir2) {
5510
5886
  }
5511
5887
  function saveGlobalCredentials(globalDir2, providers) {
5512
5888
  const filePath = credentialsPath(globalDir2);
5513
- fs31.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5889
+ fs32.mkdirSync(globalDir2, { recursive: true, mode: 448 });
5514
5890
  const body = { version: 1, providers: providers.filter(isPersistable) };
5515
5891
  const tmp = `${filePath}.tmp`;
5516
- fs31.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5517
- fs31.renameSync(tmp, filePath);
5892
+ fs32.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
5893
+ fs32.renameSync(tmp, filePath);
5518
5894
  try {
5519
- fs31.chmodSync(filePath, 384);
5895
+ fs32.chmodSync(filePath, 384);
5520
5896
  } catch {
5521
5897
  }
5522
5898
  }
5523
5899
  function mergeGlobalCredentials(workspaceProviders, globalProviders) {
5524
5900
  const merged = [...workspaceProviders];
5525
- const byKey = new Map(merged.map((p) => [providerKey(p), p]));
5901
+ const byKey = new Map(merged.filter((p) => p.type !== "azure").map((p) => [providerKey(p), p]));
5526
5902
  for (const g of globalProviders) {
5527
- const existing = byKey.get(providerKey(g));
5903
+ if (g.type === "azure" && !g.deploymentName?.trim() && azureEndpoint(g)) {
5904
+ const onResource = merged.filter((p) => p.type === "azure" && sameAzureEntry(p, g));
5905
+ if (onResource.length > 0) {
5906
+ for (const row of onResource) fillFrom(row, g);
5907
+ continue;
5908
+ }
5909
+ }
5910
+ const existing = g.type === "azure" ? merged.find((p) => p.type === "azure" && sameAzureEntry(p, g)) : byKey.get(providerKey(g));
5528
5911
  if (!existing) {
5529
5912
  merged.push({ ...g });
5530
- byKey.set(providerKey(g), merged[merged.length - 1]);
5913
+ if (g.type !== "azure") byKey.set(providerKey(g), merged[merged.length - 1]);
5531
5914
  continue;
5532
5915
  }
5533
- if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
5534
- if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
5535
- if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5536
- if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5537
- if (!existing.label && g.label) existing.label = g.label;
5916
+ fillFrom(existing, g);
5538
5917
  }
5539
5918
  return merged;
5540
5919
  }
5920
+ function fillFrom(existing, g) {
5921
+ const credentialled = Boolean(existing.apiKey || existing.authToken);
5922
+ const endpointsDiffer = credentialEndpointsConflict(existing.type, existing.baseUrl, g.baseUrl);
5923
+ if (!credentialled && !endpointsDiffer && g.apiKey) {
5924
+ existing.apiKey = g.apiKey;
5925
+ if (g.baseUrl) existing.baseUrl = g.baseUrl;
5926
+ }
5927
+ if (!credentialled && !endpointsDiffer && !existing.apiKey && g.authToken && g.baseUrl) {
5928
+ existing.authToken = g.authToken;
5929
+ existing.baseUrl = g.baseUrl;
5930
+ }
5931
+ if (!credentialled && !existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
5932
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
5933
+ if (!existing.label && g.label) existing.label = g.label;
5934
+ }
5935
+
5936
+ // src/config/azure-routing.ts
5937
+ function routableAzureRows(rows) {
5938
+ return rows.filter((p) => p.type === "azure" && p.baseUrl?.trim());
5939
+ }
5940
+ function resolveAzureRouting(view, write, env) {
5941
+ const rows = routableAzureRows(view);
5942
+ const resourcesOf = (list) => [...new Set(list.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
5943
+ if (rows.length === 0) return { ok: false, reason: "no-routable-rows", resources: [] };
5944
+ const endpoint = env.endpoint?.trim();
5945
+ const deployment = env.deployment?.trim();
5946
+ const named = deployment ? rows.filter((p) => (p.deploymentName?.trim() ?? "") === deployment) : [];
5947
+ let anchor;
5948
+ if (endpoint) {
5949
+ anchor = rows.find((p) => sameAzureEndpoint(p.baseUrl, endpoint));
5950
+ if (!anchor) {
5951
+ return { ok: false, reason: "endpoint-not-configured", resources: resourcesOf(rows) };
5952
+ }
5953
+ const elsewhere = named.filter((p) => !sameAzureEndpoint(p.baseUrl, endpoint));
5954
+ if (elsewhere.length > 0) {
5955
+ return { ok: false, reason: "name-on-another-resource", resources: resourcesOf(elsewhere) };
5956
+ }
5957
+ } else if (named.length > 0) {
5958
+ const owners = resourcesOf(named);
5959
+ if (owners.length > 1) return { ok: false, reason: "ambiguous-resource", resources: owners };
5960
+ anchor = named[0];
5961
+ } else {
5962
+ const all = resourcesOf(rows);
5963
+ if (all.length !== 1) return { ok: false, reason: "ambiguous-resource", resources: all };
5964
+ anchor = rows[0];
5965
+ }
5966
+ const resource = anchor.baseUrl;
5967
+ const onResource = routableAzureRows(write).filter((p) => sameAzureEndpoint(p.baseUrl, resource));
5968
+ const alreadyThere = deployment ? onResource.some((p) => (p.deploymentName?.trim() ?? "") === deployment) || named.some((p) => sameAzureEndpoint(p.baseUrl, resource)) : true;
5969
+ return {
5970
+ ok: true,
5971
+ resource,
5972
+ rows: onResource,
5973
+ ...deployment && !alreadyThere ? { createDeployment: deployment } : {}
5974
+ };
5975
+ }
5541
5976
 
5542
5977
  // src/config/retired-providers.ts
5543
5978
  var RETIRED_PROVIDER_TYPES = {
@@ -5547,7 +5982,7 @@ function isRetiredProviderType(type) {
5547
5982
  return typeof type === "string" && Object.hasOwn(RETIRED_PROVIDER_TYPES, type);
5548
5983
  }
5549
5984
  function didCleanupChangeAnything(c) {
5550
- return c.removed.length > 0 || c.clearedPins.length > 0;
5985
+ return c.removed.length > 0 || c.clearedPins.length > 0 || (c.revokedCredentials ?? 0) > 0 || (c.unusableCredentials ?? 0) > 0;
5551
5986
  }
5552
5987
  function stripRetiredProviders(raw) {
5553
5988
  const cleanup = { removed: [], clearedPins: [] };
@@ -5594,11 +6029,24 @@ function describeCleanup(c) {
5594
6029
  for (const type of c.removed) {
5595
6030
  parts.push(`removed the retired "${type}" provider (${RETIRED_PROVIDER_TYPES[type]})`);
5596
6031
  }
6032
+ if ((c.revokedCredentials ?? 0) > 0) {
6033
+ parts.push("discarded a linked Claude subscription token, which Anthropic no longer permits third-party tools to use");
6034
+ }
6035
+ const unusable = c.unusableCredentials ?? 0;
6036
+ if (unusable > 0) {
6037
+ parts.push(
6038
+ `discarded ${unusable} synced gateway token${unusable === 1 ? "" : "s"} that named no gateway URL \u2014 a bearer is only valid at the gateway that issued it`
6039
+ );
6040
+ }
5597
6041
  if (c.clearedPins.length > 0) {
5598
6042
  parts.push(`reset ${c.clearedPins.map((t) => t.toUpperCase()).join("/")} to Auto, since the pin named it`);
5599
6043
  }
6044
+ if (parts.length === 0) return "";
5600
6045
  return `Cascade config migration: ${parts.join("; ")}.`;
5601
6046
  }
6047
+
6048
+ // src/config/index.ts
6049
+ init_revoked_credentials();
5602
6050
  var MCP_TOOL_PREFIX = "mcp__";
5603
6051
  var MAX_SEGMENT_LENGTH = 24;
5604
6052
  var SEGMENT_HASH_LENGTH = 8;
@@ -5780,10 +6228,31 @@ function removeMcpServerDenials(disabledTools, serverName) {
5780
6228
  // src/config/index.ts
5781
6229
  init_constants();
5782
6230
  var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
6231
+ function hasProviderCredential(p) {
6232
+ if (!p) return false;
6233
+ if (typeof p.apiKey === "string" && p.apiKey.length > 0) return true;
6234
+ return typeof p.authToken === "string" && p.authToken.length > 0 && typeof p.baseUrl === "string" && p.baseUrl.length > 0;
6235
+ }
6236
+ function isRoutable(p) {
6237
+ if (p.type === "openai-compatible") return Boolean(p.baseUrl?.trim());
6238
+ if (p.type === "azure") return Boolean(p.baseUrl?.trim() && p.deploymentName?.trim());
6239
+ return true;
6240
+ }
5783
6241
  function hasUsableProvider(providers) {
5784
6242
  if (!providers?.length) return false;
5785
- return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey || !!p.authToken);
6243
+ return providers.some((p) => isRoutable(p) && (KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || hasProviderCredential(p)));
6244
+ }
6245
+ function applyProviderApiKey(providers, type, apiKey, extra = {}) {
6246
+ const existing = providers.find((p) => p.type === type);
6247
+ if (existing) {
6248
+ existing.apiKey = apiKey;
6249
+ existing.authToken = void 0;
6250
+ if (extra.baseUrl) existing.baseUrl = extra.baseUrl;
6251
+ return;
6252
+ }
6253
+ providers.push({ type, apiKey, ...extra.baseUrl ? { baseUrl: extra.baseUrl } : {} });
5786
6254
  }
6255
+ var ENV_ENDPOINT_CHANNEL = /* @__PURE__ */ new Set(["anthropic"]);
5787
6256
  var ConfigManager = class {
5788
6257
  config;
5789
6258
  keystore;
@@ -5803,6 +6272,16 @@ var ConfigManager = class {
5803
6272
  * fallback for a list that is empty for some unrelated reason.
5804
6273
  */
5805
6274
  retiredCleanup;
6275
+ /**
6276
+ * How many dead subscription credentials THIS load removed. Per-load like
6277
+ * `retiredCleanup`, for the same reason: a second load() on the same instance
6278
+ * must not re-announce a migration that already happened.
6279
+ */
6280
+ revokedCredentials = 0;
6281
+ /** Tier pins cleared by the revoked-credential migration this load. */
6282
+ revokedPins = [];
6283
+ /** This load's revoked-credential explanation, joined with any retirement one. */
6284
+ revokedNotice;
5806
6285
  /**
5807
6286
  * Human-readable migration notice held for a UI to display. `console.warn`
5808
6287
  * is not sufficient: the REPL clears the TTY immediately after load(), and
@@ -5818,7 +6297,7 @@ var ConfigManager = class {
5818
6297
  /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
5819
6298
  constructor(workspacePath = process.cwd(), globalDirOverride) {
5820
6299
  this.workspacePath = workspacePath;
5821
- this.globalDir = globalDirOverride ?? path34.join(os10.homedir(), GLOBAL_CONFIG_DIR);
6300
+ this.globalDir = globalDirOverride ?? path35.join(os10.homedir(), GLOBAL_CONFIG_DIR);
5822
6301
  }
5823
6302
  async load() {
5824
6303
  this.config = await this.loadConfig();
@@ -5832,22 +6311,38 @@ var ConfigManager = class {
5832
6311
  this.ignore = new CascadeIgnore();
5833
6312
  await this.ignore.load(this.workspacePath);
5834
6313
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
5835
- this.keystore = new Keystore(path34.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
5836
- this.store = new MemoryStore(path34.join(this.workspacePath, CASCADE_DB_FILE));
5837
- const globalCreds = filterRetiredCredentials(loadGlobalCredentials(this.globalDir));
5838
- if (globalCreds.removed.length > 0) {
6314
+ this.keystore = new Keystore(path35.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
6315
+ this.store = new MemoryStore(path35.join(this.workspacePath, CASCADE_DB_FILE));
6316
+ const globalRevoked = stripRevokedCredentials(loadGlobalCredentials(this.globalDir));
6317
+ const globalCreds = filterRetiredCredentials(globalRevoked.kept);
6318
+ if (globalRevoked.removed > 0) this.revokedCredentials += globalRevoked.removed;
6319
+ if (globalCreds.removed.length > 0 || globalRevoked.removed > 0) {
6320
+ const retiredHere = globalCreds.removed.length > 0;
5839
6321
  try {
5840
6322
  saveGlobalCredentials(this.globalDir, globalCreds.kept);
5841
6323
  } catch (err) {
5842
6324
  console.warn(`Could not rewrite the global credential store: ${err instanceof Error ? err.message : String(err)}`);
5843
6325
  }
5844
- this.retiredCleanup = {
5845
- removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
5846
- clearedPins: this.retiredCleanup?.clearedPins ?? []
5847
- };
6326
+ if (retiredHere) {
6327
+ this.retiredCleanup = {
6328
+ removed: [.../* @__PURE__ */ new Set([...this.retiredCleanup?.removed ?? [], ...globalCreds.removed])],
6329
+ clearedPins: this.retiredCleanup?.clearedPins ?? []
6330
+ };
6331
+ }
5848
6332
  }
5849
- await this.injectEnvKeys();
6333
+ await this.injectEnvKeys(globalCreds.kept);
5850
6334
  this.config.providers = mergeGlobalCredentials(this.config.providers, globalCreds.kept);
6335
+ if (this.revokedCredentials > 0 && !hasUsableAnthropic(this.config.providers)) {
6336
+ this.revokedPins = clearAnthropicPins(this.config.models, this.config.providers);
6337
+ if (this.revokedPins.length > 0) {
6338
+ await this.persistClearedPins(this.revokedPins.map((p) => p.tier));
6339
+ }
6340
+ }
6341
+ if (this.revokedCredentials > 0) {
6342
+ const pins = this.revokedPins.length ? ` Cleared the ${this.revokedPins.map((p) => `${p.tier.toUpperCase()} pin (${p.model})`).join(" and ")}, since it named Anthropic.` : "";
6343
+ this.revokedNotice = `Cascade config migration: ${REVOKED_CREDENTIAL_REASON}${pins}`;
6344
+ console.warn(this.revokedNotice);
6345
+ }
5851
6346
  for (const type of Object.keys(RETIRED_PROVIDER_TYPES)) {
5852
6347
  try {
5853
6348
  this.store.purgeCachedModelsForRetiredProvider(type);
@@ -5856,10 +6351,14 @@ var ConfigManager = class {
5856
6351
  }
5857
6352
  await this.ensureDefaultIdentity();
5858
6353
  if (mcpNamesChanged) await this.save();
6354
+ const notices = [];
5859
6355
  if (this.retiredCleanup) {
5860
- console.warn(describeCleanup(this.retiredCleanup));
5861
- this.pendingRetiredNotice = describeCleanup(this.retiredCleanup);
6356
+ const retired = describeCleanup(this.retiredCleanup);
6357
+ console.warn(retired);
6358
+ notices.push(retired);
5862
6359
  }
6360
+ if (this.revokedNotice) notices.push(this.revokedNotice);
6361
+ if (notices.length > 0) this.pendingRetiredNotice = notices.join(" ");
5863
6362
  }
5864
6363
  getConfig() {
5865
6364
  return this.config;
@@ -5879,12 +6378,18 @@ var ConfigManager = class {
5879
6378
  getWorkspacePath() {
5880
6379
  return this.workspacePath;
5881
6380
  }
5882
- async save() {
5883
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
5884
- await fs11.mkdir(path34.dirname(configPath), { recursive: true });
5885
- await fs11.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
6381
+ /**
6382
+ * @param config What to persist. Defaults to the manager's own, but a
6383
+ * transactional save hands in the validated copy it has NOT adopted yet —
6384
+ * writing before adopting is what stops a run picking up configuration that
6385
+ * never reached the disk (see `commitSettings`).
6386
+ */
6387
+ async save(config = this.config) {
6388
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
6389
+ await fs11.mkdir(path35.dirname(configPath), { recursive: true });
6390
+ await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
5886
6391
  try {
5887
- saveGlobalCredentials(this.globalDir, this.config.providers);
6392
+ saveGlobalCredentials(this.globalDir, config.providers);
5888
6393
  } catch (err) {
5889
6394
  console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
5890
6395
  }
@@ -5915,19 +6420,35 @@ var ConfigManager = class {
5915
6420
  const configProvider = this.config.providers.find((p) => p.type === provider);
5916
6421
  return configProvider?.apiKey;
5917
6422
  }
6423
+ /**
6424
+ * The bearer token configured for a provider, if any.
6425
+ *
6426
+ * Companion to getApiKey(). A provider can be fully configured with only
6427
+ * this — `cascade link` and ANTHROPIC_AUTH_TOKEN both produce it — so any
6428
+ * surface that asks "is this provider set up" has to consult both, or it
6429
+ * reports a working install as unconfigured.
6430
+ */
6431
+ getAuthToken(provider) {
6432
+ return this.config.providers.find((p) => p.type === provider)?.authToken;
6433
+ }
5918
6434
  async loadConfig() {
5919
6435
  this.retiredCleanup = void 0;
5920
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
6436
+ this.revokedCredentials = 0;
6437
+ this.revokedPins = [];
6438
+ this.revokedNotice = void 0;
6439
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
5921
6440
  try {
5922
6441
  const raw = await fs11.readFile(configPath, "utf-8");
5923
6442
  const parsed = JSON.parse(raw);
5924
6443
  const cleanup = stripRetiredProviders(parsed);
5925
- if (didCleanupChangeAnything(cleanup)) {
5926
- this.retiredCleanup = cleanup;
6444
+ const revokedHere = stripRevokedFromConfig(parsed).removed;
6445
+ if (revokedHere > 0) this.revokedCredentials += revokedHere;
6446
+ if (didCleanupChangeAnything(cleanup) || revokedHere > 0) {
6447
+ if (didCleanupChangeAnything(cleanup)) this.retiredCleanup = cleanup;
5927
6448
  try {
5928
6449
  await fs11.writeFile(configPath, JSON.stringify(parsed, null, 2), "utf-8");
5929
6450
  } catch (err) {
5930
- console.warn(`Could not persist the retired-provider migration: ${err instanceof Error ? err.message : String(err)}`);
6451
+ console.warn(`Could not persist the config migration: ${err instanceof Error ? err.message : String(err)}`);
5931
6452
  }
5932
6453
  }
5933
6454
  return validateConfig(parsed);
@@ -5938,9 +6459,102 @@ var ConfigManager = class {
5938
6459
  throw err;
5939
6460
  }
5940
6461
  }
5941
- async injectEnvKeys() {
6462
+ /**
6463
+ * Rewrite the workspace file with those tier pins removed.
6464
+ *
6465
+ * A targeted edit of the RAW file, not save(). By this point `this.config`
6466
+ * carries environment keys and machine-global credentials, and save()
6467
+ * serializes the whole object — so persisting through it would copy secrets
6468
+ * kept 0600 in ~/.cascade-ai into a workspace file that may be 0644. Same
6469
+ * reasoning as the migration write in loadConfig(); best-effort for the same
6470
+ * reason, since a read-only config directory must not abort startup.
6471
+ */
6472
+ /**
6473
+ * The Azure entries `AZURE_OPENAI_KEY` belongs to — possibly several.
6474
+ *
6475
+ * `AZURE_OPENAI_ENDPOINT` names the resource. Without one, a single
6476
+ * configured resource is unambiguous and anything more is a guess — and
6477
+ * guessing wrong writes a key to a resource that will reject it.
6478
+ *
6479
+ * ALL the deployments on that resource, not the first. An Azure key is
6480
+ * resource-scoped, so every deployment there shares it, and Azure is
6481
+ * configured one entry per deployment — the router binds each model to its
6482
+ * own row, matching `deploymentName` against the model id, so filling only
6483
+ * the first left the rest issuing requests with no key at all.
6484
+ */
6485
+ azureEntriesForEnv(globalProviders = []) {
6486
+ const workspace = this.config.providers;
6487
+ const routing = resolveAzureRouting(
6488
+ [...workspace, ...globalProviders],
6489
+ workspace,
6490
+ {
6491
+ endpoint: process.env["AZURE_OPENAI_ENDPOINT"],
6492
+ deployment: process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"]
6493
+ }
6494
+ );
6495
+ if (!routing.ok) return [];
6496
+ const onResource = [...routing.rows];
6497
+ const claimEndpointlessRow = (name) => {
6498
+ const row = this.config.providers.find((p) => p.type === "azure" && !p.baseUrl?.trim() && (p.deploymentName?.trim() ?? "") === name);
6499
+ if (!row) return void 0;
6500
+ if (row.apiKey || row.authToken) {
6501
+ console.warn(
6502
+ `Ignoring the stored key on Azure deployment "${name}": it was saved without an endpoint, so there is nothing to show it belongs to ${routing.resource}. AZURE_OPENAI_KEY is being used instead.`
6503
+ );
6504
+ row.apiKey = void 0;
6505
+ row.authToken = void 0;
6506
+ }
6507
+ row.baseUrl = routing.resource;
6508
+ onResource.push(row);
6509
+ return row;
6510
+ };
6511
+ for (const g of globalProviders) {
6512
+ if (g.type !== "azure" || !g.baseUrl?.trim()) continue;
6513
+ if (!sameAzureEndpoint(g.baseUrl, routing.resource)) continue;
6514
+ const name = g.deploymentName?.trim() ?? "";
6515
+ if (onResource.some((p) => (p.deploymentName?.trim() ?? "") === name)) continue;
6516
+ if (claimEndpointlessRow(name)) continue;
6517
+ const row = { ...g, apiKey: void 0, authToken: void 0 };
6518
+ this.config.providers.push(row);
6519
+ onResource.push(row);
6520
+ }
6521
+ if (routing.createDeployment && !claimEndpointlessRow(routing.createDeployment)) {
6522
+ const row = {
6523
+ type: "azure",
6524
+ deploymentName: routing.createDeployment,
6525
+ baseUrl: routing.resource
6526
+ };
6527
+ this.config.providers.push(row);
6528
+ onResource.push(row);
6529
+ }
6530
+ return onResource;
6531
+ }
6532
+ async persistClearedPins(tiers) {
6533
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
6534
+ try {
6535
+ const raw = JSON.parse(await fs11.readFile(configPath, "utf-8"));
6536
+ const models = raw["models"];
6537
+ if (typeof models !== "object" || models === null) return;
6538
+ for (const tier of tiers) delete models[tier];
6539
+ await fs11.writeFile(configPath, JSON.stringify(raw, null, 2), "utf-8");
6540
+ } catch (err) {
6541
+ if (err.code === "ENOENT") return;
6542
+ console.warn(`Could not persist the cleared model pin: ${err instanceof Error ? err.message : String(err)}`);
6543
+ }
6544
+ }
6545
+ /**
6546
+ * `globalProviders` is the machine-global credential store, passed in
6547
+ * READ-ONLY. This runs before mergeGlobalCredentials() on purpose — an
6548
+ * exported key should outrank a stored one, which only holds while the
6549
+ * injection goes first — but a few of the questions asked here are about the
6550
+ * user's whole configuration rather than this workspace's file, and answering
6551
+ * those from the workspace alone is what produced the gateway bug this
6552
+ * argument fixes.
6553
+ */
6554
+ async injectEnvKeys(globalProviders = []) {
5942
6555
  const wasEmpty = this.config.providers.length === 0;
5943
- const emptiedByRetirement = wasEmpty && !!this.retiredCleanup;
6556
+ const emptiedByRetirement = wasEmpty && (!!this.retiredCleanup || this.revokedCredentials > 0);
6557
+ const mayCreate = (type) => wasEmpty || type === "anthropic" && this.revokedCredentials > 0;
5944
6558
  const envProviders = [
5945
6559
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
5946
6560
  { env: "OPENAI_API_KEY", type: "openai" },
@@ -5950,11 +6564,66 @@ var ConfigManager = class {
5950
6564
  for (const { env, type } of envProviders) {
5951
6565
  const key = process.env[env];
5952
6566
  if (!key) continue;
5953
- const existing = this.config.providers.find((p) => p.type === type);
5954
- if (!existing && wasEmpty) {
5955
- this.config.providers.push({ type, apiKey: key });
5956
- } else if (existing && !existing.apiKey) {
5957
- existing.apiKey = key;
6567
+ if (isSubscriptionToken(key)) continue;
6568
+ const targets = type === "azure" ? this.azureEntriesForEnv(globalProviders) : [this.config.providers.find((p) => p.type === type)].filter((p) => !!p);
6569
+ const existing = targets[0];
6570
+ const globalMatch = type === "azure" ? void 0 : globalProviders.find((p) => p.type === type);
6571
+ const anthropicGateway = type === "anthropic" ? process.env["ANTHROPIC_BASE_URL"] : void 0;
6572
+ if (!existing && (mayCreate(type) || globalMatch)) {
6573
+ if (type === "azure") {
6574
+ const endpoint = process.env["AZURE_OPENAI_ENDPOINT"];
6575
+ const deploymentName = process.env["AZURE_OPENAI_DEPLOYMENT"] ?? process.env["AZURE_OPENAI_DEPLOYMENT_NAME"];
6576
+ if (!endpoint || !deploymentName) continue;
6577
+ const apiVersion = process.env["AZURE_OPENAI_API_VERSION"];
6578
+ this.config.providers.push({
6579
+ type,
6580
+ apiKey: key,
6581
+ baseUrl: endpoint,
6582
+ deploymentName,
6583
+ ...apiVersion ? { apiVersion } : {}
6584
+ });
6585
+ continue;
6586
+ }
6587
+ const inherited = ENV_ENDPOINT_CHANNEL.has(type) ? anthropicGateway : anthropicGateway ?? globalMatch?.baseUrl;
6588
+ this.config.providers.push({
6589
+ type,
6590
+ apiKey: key,
6591
+ ...inherited ? { baseUrl: inherited } : {}
6592
+ });
6593
+ } else if (existing) {
6594
+ for (const target of targets) {
6595
+ if (target.apiKey || target.authToken) continue;
6596
+ if (anthropicGateway) {
6597
+ target.apiKey = key;
6598
+ target.baseUrl = anthropicGateway;
6599
+ continue;
6600
+ }
6601
+ if (target.baseUrl && hasDefaultEndpoint(type) && !sameCredentialEndpoint(type, void 0, target.baseUrl)) {
6602
+ if (ENV_ENDPOINT_CHANNEL.has(type)) target.baseUrl = void 0;
6603
+ else {
6604
+ console.warn(
6605
+ `${env} will be used at the ${type} endpoint configured in this workspace (${target.baseUrl}), not at the provider's public host. Remove that \`baseUrl\` if the key was issued by ${type} directly.`
6606
+ );
6607
+ }
6608
+ }
6609
+ target.apiKey = key;
6610
+ }
6611
+ }
6612
+ }
6613
+ const authToken = process.env["ANTHROPIC_AUTH_TOKEN"];
6614
+ if (authToken && !isSubscriptionToken(authToken)) {
6615
+ const existing = this.config.providers.find((p) => p.type === "anthropic");
6616
+ const globalAnthropic = globalProviders.find((p) => p.type === "anthropic");
6617
+ const gateway = process.env["ANTHROPIC_BASE_URL"] ?? existing?.baseUrl ?? globalAnthropic?.baseUrl;
6618
+ if (gateway) {
6619
+ if (existing) {
6620
+ if (!existing.apiKey && !existing.authToken) {
6621
+ existing.authToken = authToken;
6622
+ existing.baseUrl = gateway;
6623
+ }
6624
+ } else if (mayCreate("anthropic") || globalAnthropic) {
6625
+ this.config.providers.push({ type: "anthropic", authToken, baseUrl: gateway });
6626
+ }
5958
6627
  }
5959
6628
  }
5960
6629
  if (wasEmpty && !emptiedByRetirement && !this.config.providers.find((p) => p.type === "ollama")) {
@@ -6306,15 +6975,15 @@ function SafeTextInput(props) {
6306
6975
  const displayValue = mask ? mask.repeat(value.length) : value;
6307
6976
  let rendered;
6308
6977
  if (displayValue.length === 0) {
6309
- rendered = showCursor && focus ? placeholder.length > 0 ? chalk7.inverse(placeholder[0]) + chalk7.grey(placeholder.slice(1)) : chalk7.inverse(" ") : placeholder.length > 0 ? chalk7.grey(placeholder) : "";
6978
+ rendered = showCursor && focus ? placeholder.length > 0 ? chalk8.inverse(placeholder[0]) + chalk8.grey(placeholder.slice(1)) : chalk8.inverse(" ") : placeholder.length > 0 ? chalk8.grey(placeholder) : "";
6310
6979
  } else if (!showCursor || !focus) {
6311
6980
  rendered = displayValue;
6312
6981
  } else {
6313
6982
  let out = "";
6314
6983
  for (let i = 0; i < displayValue.length; i++) {
6315
- out += i === cursorOffset ? chalk7.inverse(displayValue[i]) : displayValue[i];
6984
+ out += i === cursorOffset ? chalk8.inverse(displayValue[i]) : displayValue[i];
6316
6985
  }
6317
- if (cursorOffset >= displayValue.length) out += chalk7.inverse(" ");
6986
+ if (cursorOffset >= displayValue.length) out += chalk8.inverse(" ");
6318
6987
  rendered = out;
6319
6988
  }
6320
6989
  return /* @__PURE__ */ jsx(Text, { children: rendered });
@@ -7358,7 +8027,7 @@ init_pricing();
7358
8027
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
7359
8028
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
7360
8029
  var FETCH_TIMEOUT_MS = 8e3;
7361
- var DEFAULT_CACHE_FILE = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
8030
+ var DEFAULT_CACHE_FILE = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
7362
8031
  function normalizeModelId2(id) {
7363
8032
  let s = id.toLowerCase();
7364
8033
  const slash = s.lastIndexOf("/");
@@ -7505,7 +8174,7 @@ var LiveDataProvider = class {
7505
8174
  }
7506
8175
  async saveCache() {
7507
8176
  try {
7508
- await fs11.mkdir(path34.dirname(this.opts.cacheFile), { recursive: true });
8177
+ await fs11.mkdir(path35.dirname(this.opts.cacheFile), { recursive: true });
7509
8178
  const cache = {
7510
8179
  fetchedAt: this.fetchedAt,
7511
8180
  snapshot: this.snapshot ?? void 0,
@@ -7772,9 +8441,50 @@ function guardTokens(text) {
7772
8441
  var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
7773
8442
  var DISCOVERY_TIMEOUT_MS = 4e3;
7774
8443
  var cloudDiscoveryCache = /* @__PURE__ */ new Map();
8444
+ var IDENTITY_SWEEP_INTERVAL_MS = 60 * 1e3;
8445
+ var credentialIdentities = /* @__PURE__ */ new Map();
8446
+ var lastIdentitySweep = 0;
8447
+ var identitySweepTimer;
8448
+ function ensureIdentitySweeper() {
8449
+ if (identitySweepTimer) return;
8450
+ identitySweepTimer = setInterval(() => {
8451
+ sweepExpired(Date.now());
8452
+ if (credentialIdentities.size === 0 && identitySweepTimer) {
8453
+ clearInterval(identitySweepTimer);
8454
+ identitySweepTimer = void 0;
8455
+ }
8456
+ }, IDENTITY_SWEEP_INTERVAL_MS);
8457
+ identitySweepTimer.unref?.();
8458
+ }
8459
+ function sweepExpired(now) {
8460
+ for (const [key, held] of credentialIdentities) {
8461
+ if (now - held.at > DISCOVERY_TTL_MS) credentialIdentities.delete(key);
8462
+ }
8463
+ for (const [key, entry] of cloudDiscoveryCache) {
8464
+ if (now - entry.at > DISCOVERY_TTL_MS) cloudDiscoveryCache.delete(key);
8465
+ }
8466
+ }
8467
+ function credentialIdentity(secret) {
8468
+ if (!secret) return "-";
8469
+ const now = Date.now();
8470
+ if (now - lastIdentitySweep > IDENTITY_SWEEP_INTERVAL_MS) {
8471
+ lastIdentitySweep = now;
8472
+ sweepExpired(now);
8473
+ }
8474
+ const held = credentialIdentities.get(secret);
8475
+ if (held && now - held.at <= DISCOVERY_TTL_MS) return held.id;
8476
+ const id = crypto.randomUUID();
8477
+ credentialIdentities.set(secret, { id, at: now });
8478
+ ensureIdentitySweeper();
8479
+ return id;
8480
+ }
7775
8481
  function discoveryCacheKey(type, cfg) {
7776
- const raw = `${type}|${cfg.apiKey ?? ""}|${cfg.baseUrl ?? ""}`;
7777
- return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 24);
8482
+ return [
8483
+ type,
8484
+ credentialIdentity(cfg.apiKey),
8485
+ credentialIdentity(cfg.authToken),
8486
+ cfg.baseUrl ?? ""
8487
+ ].join("|");
7778
8488
  }
7779
8489
  function applyTierLimits(options, tier, limits) {
7780
8490
  if (!limits) return options;
@@ -8073,7 +8783,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8073
8783
  await Promise.all(providers.map(async (type) => {
8074
8784
  if (!this.selector.isProviderAvailable(type)) return;
8075
8785
  const cfg = config.providers.find((p) => p.type === type);
8076
- if (!cfg?.apiKey) return;
8786
+ if (!cfg || !hasProviderCredential(cfg)) return;
8077
8787
  const cacheKey = discoveryCacheKey(type, cfg);
8078
8788
  let entry = cloudDiscoveryCache.get(cacheKey);
8079
8789
  if (!entry || Date.now() - entry.at > DISCOVERY_TTL_MS) {
@@ -8082,7 +8792,11 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
8082
8792
  try {
8083
8793
  const provider = this.createProvider(cfg, seed);
8084
8794
  if (typeof provider.listModels !== "function") return;
8085
- const models = await withTimeout(provider.listModels(), DISCOVERY_TIMEOUT_MS, `${type} model discovery timed out`);
8795
+ const models = await withTimeout(
8796
+ provider.listModels({ staticFallback: false }),
8797
+ DISCOVERY_TIMEOUT_MS,
8798
+ `${type} model discovery timed out`
8799
+ );
8086
8800
  if (!models.length) return;
8087
8801
  entry = { models, ids: models.map((m) => m.id), at: Date.now() };
8088
8802
  cloudDiscoveryCache.set(cacheKey, entry);
@@ -10346,8 +11060,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
10346
11060
  // Media generation can run for a minute; without this a cancelled run
10347
11061
  // still pays for an image nobody will see.
10348
11062
  ...this.signal ? { signal: this.signal } : {},
10349
- saveSnapshot: async (path39, content) => {
10350
- this.store?.addFileSnapshot(this.taskId, path39, content);
11063
+ saveSnapshot: async (path40, content) => {
11064
+ this.store?.addFileSnapshot(this.taskId, path40, content);
10351
11065
  },
10352
11066
  sendPeerSync: (to, syncType, content) => {
10353
11067
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -10550,7 +11264,7 @@ ${assignment.expectedOutput}`;
10550
11264
  const { promisify: promisify4 } = await import('util');
10551
11265
  const execFileAsync2 = promisify4(execFile3);
10552
11266
  for (const artifactPath of artifactPaths) {
10553
- const absolutePath = path34.resolve(this.artifactRoot(), artifactPath);
11267
+ const absolutePath = path35.resolve(this.artifactRoot(), artifactPath);
10554
11268
  try {
10555
11269
  const stat = await fs11.stat(absolutePath);
10556
11270
  if (!stat.isFile()) {
@@ -10585,7 +11299,7 @@ ${assignment.expectedOutput}`;
10585
11299
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
10586
11300
  continue;
10587
11301
  }
10588
- const ext = path34.extname(absolutePath).toLowerCase();
11302
+ const ext = path35.extname(absolutePath).toLowerCase();
10589
11303
  const check = ext === ".ts" || ext === ".tsx" ? { cmd: "npx", args: ["tsc", "--noEmit", absolutePath] } : ext === ".js" || ext === ".jsx" ? { cmd: process.execPath, args: ["--check", absolutePath] } : ext === ".py" ? { cmd: "python", args: ["-m", "py_compile", absolutePath] } : null;
10590
11304
  if (check) {
10591
11305
  try {
@@ -10677,7 +11391,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
10677
11391
  async checkAcceptance(assignment) {
10678
11392
  const criteria = assignment.acceptance ?? [];
10679
11393
  if (!criteria.length) return [];
10680
- const resolve2 = (target) => path34.resolve(this.artifactRoot(), target);
11394
+ const resolve2 = (target) => path35.resolve(this.artifactRoot(), target);
10681
11395
  return evaluateAcceptance(criteria, assignment.files ?? [], {
10682
11396
  stat: async (target) => {
10683
11397
  try {
@@ -13906,16 +14620,16 @@ function resolveInWorkspace(workspaceRoot, input) {
13906
14620
  if (typeof input !== "string" || input.length === 0) {
13907
14621
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
13908
14622
  }
13909
- const root = path34.resolve(workspaceRoot);
13910
- const abs = path34.isAbsolute(input) ? path34.resolve(input) : path34.resolve(root, input);
13911
- const rel = path34.relative(root, abs);
13912
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path34.isAbsolute(rel)) {
14623
+ const root = path35.resolve(workspaceRoot);
14624
+ const abs = path35.isAbsolute(input) ? path35.resolve(input) : path35.resolve(root, input);
14625
+ const rel = path35.relative(root, abs);
14626
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path35.isAbsolute(rel)) {
13913
14627
  throw new WorkspaceSandboxError(input, root);
13914
14628
  }
13915
14629
  try {
13916
- const real = fs31.realpathSync(abs);
13917
- const realRel = path34.relative(root, real);
13918
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path34.isAbsolute(realRel))) {
14630
+ const real = fs32.realpathSync(abs);
14631
+ const realRel = path35.relative(root, real);
14632
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path35.isAbsolute(realRel))) {
13919
14633
  throw new WorkspaceSandboxError(input, root);
13920
14634
  }
13921
14635
  } catch (e) {
@@ -14568,7 +15282,7 @@ var GenerateDocumentTool = class extends BaseTool {
14568
15282
  }
14569
15283
  }
14570
15284
  const bytes = await renderDocument(ext, content, { name: filePath, loadImageBytes });
14571
- await fs11.mkdir(path34.dirname(absPath), { recursive: true });
15285
+ await fs11.mkdir(path35.dirname(absPath), { recursive: true });
14572
15286
  await fs11.writeFile(absPath, bytes);
14573
15287
  const charts = countCharts(content);
14574
15288
  return [
@@ -14789,7 +15503,7 @@ var FileWriteTool = class extends BaseTool {
14789
15503
  } catch {
14790
15504
  }
14791
15505
  }
14792
- await fs11.mkdir(path34.dirname(absPath), { recursive: true });
15506
+ await fs11.mkdir(path35.dirname(absPath), { recursive: true });
14793
15507
  await fs11.writeFile(absPath, content, "utf-8");
14794
15508
  return `Written ${content.length} characters to ${filePath}`;
14795
15509
  }
@@ -15287,7 +16001,7 @@ var ImageAnalyzeTool = class extends BaseTool {
15287
16001
  };
15288
16002
  async function fileToImageAttachment(filePath) {
15289
16003
  const data = await fs11.readFile(filePath);
15290
- const ext = path34.extname(filePath).toLowerCase();
16004
+ const ext = path35.extname(filePath).toLowerCase();
15291
16005
  const mimeMap = {
15292
16006
  ".jpg": "image/jpeg",
15293
16007
  ".jpeg": "image/jpeg",
@@ -15321,14 +16035,14 @@ var PDFCreateTool = class extends BaseTool {
15321
16035
  const filePath = input["path"];
15322
16036
  const content = input["content"];
15323
16037
  const title = input["title"];
15324
- const dir = path34.dirname(filePath);
15325
- if (!fs31.existsSync(dir)) {
15326
- fs31.mkdirSync(dir, { recursive: true });
16038
+ const dir = path35.dirname(filePath);
16039
+ if (!fs32.existsSync(dir)) {
16040
+ fs32.mkdirSync(dir, { recursive: true });
15327
16041
  }
15328
16042
  return new Promise((resolve2, reject) => {
15329
16043
  try {
15330
16044
  const doc = new PDFDocument({ margin: 50 });
15331
- const stream = fs31.createWriteStream(filePath);
16045
+ const stream = fs32.createWriteStream(filePath);
15332
16046
  doc.pipe(stream);
15333
16047
  if (title) {
15334
16048
  doc.info["Title"] = title;
@@ -15406,22 +16120,22 @@ var CodeInterpreterTool = class extends BaseTool {
15406
16120
  }
15407
16121
  cmdPrefix = NODE_CMD;
15408
16122
  }
15409
- const tmpDir = path34.join(this.workspaceRoot, ".cascade", "tmp");
15410
- if (!fs31.existsSync(tmpDir)) {
15411
- fs31.mkdirSync(tmpDir, { recursive: true });
16123
+ const tmpDir = path35.join(this.workspaceRoot, ".cascade", "tmp");
16124
+ if (!fs32.existsSync(tmpDir)) {
16125
+ fs32.mkdirSync(tmpDir, { recursive: true });
15412
16126
  }
15413
16127
  const extension = language === "python" ? "py" : "js";
15414
16128
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
15415
- const filePath = path34.join(tmpDir, fileName);
15416
- fs31.writeFileSync(filePath, code, "utf-8");
16129
+ const filePath = path35.join(tmpDir, fileName);
16130
+ fs32.writeFileSync(filePath, code, "utf-8");
15417
16131
  const execArgs = [filePath, ...args];
15418
16132
  return new Promise((resolve2) => {
15419
16133
  const startMs = Date.now();
15420
16134
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
15421
16135
  const duration = Date.now() - startMs;
15422
16136
  try {
15423
- if (fs31.existsSync(filePath)) {
15424
- fs31.unlinkSync(filePath);
16137
+ if (fs32.existsSync(filePath)) {
16138
+ fs32.unlinkSync(filePath);
15425
16139
  }
15426
16140
  } catch (cleanupErr) {
15427
16141
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -15733,7 +16447,7 @@ var GlobTool = class extends BaseTool {
15733
16447
  };
15734
16448
  async execute(input, _options) {
15735
16449
  const pattern = input["pattern"];
15736
- const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16450
+ const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15737
16451
  const matches = await glob(pattern, {
15738
16452
  cwd: searchPath,
15739
16453
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -15746,7 +16460,7 @@ var GlobTool = class extends BaseTool {
15746
16460
  const withMtime = await Promise.all(
15747
16461
  matches.map(async (rel) => {
15748
16462
  try {
15749
- const stat = await fs11.stat(path34.join(searchPath, rel));
16463
+ const stat = await fs11.stat(path35.join(searchPath, rel));
15750
16464
  return { rel, mtime: stat.mtimeMs };
15751
16465
  } catch {
15752
16466
  return { rel, mtime: 0 };
@@ -15795,7 +16509,7 @@ var GrepTool = class extends BaseTool {
15795
16509
  };
15796
16510
  async execute(input, _options) {
15797
16511
  const pattern = input["pattern"];
15798
- const searchPath = input["path"] ? path34.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
16512
+ const searchPath = input["path"] ? path35.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
15799
16513
  const globPattern = input["glob"];
15800
16514
  const outputMode = input["output_mode"] ?? "content";
15801
16515
  const context = input["context"] ?? 0;
@@ -15849,12 +16563,12 @@ var GrepTool = class extends BaseTool {
15849
16563
  nodir: true
15850
16564
  });
15851
16565
  } catch {
15852
- files = [path34.relative(searchPath, searchPath) || "."];
16566
+ files = [path35.relative(searchPath, searchPath) || "."];
15853
16567
  }
15854
16568
  const results = [];
15855
16569
  let totalCount = 0;
15856
16570
  for (const rel of files) {
15857
- const abs = path34.join(searchPath, rel);
16571
+ const abs = path35.join(searchPath, rel);
15858
16572
  let content;
15859
16573
  try {
15860
16574
  content = await fs11.readFile(abs, "utf-8");
@@ -16253,10 +16967,10 @@ var ToolRegistry = class extends EventEmitter {
16253
16967
  }
16254
16968
  isIgnored(filePath) {
16255
16969
  if (!filePath) return false;
16256
- const abs = path34.resolve(this.workspaceRoot, filePath);
16257
- const rel = path34.relative(this.workspaceRoot, abs);
16258
- if (!rel || rel.startsWith("..") || path34.isAbsolute(rel)) return true;
16259
- const posixRel = rel.split(path34.sep).join("/");
16970
+ const abs = path35.resolve(this.workspaceRoot, filePath);
16971
+ const rel = path35.relative(this.workspaceRoot, abs);
16972
+ if (!rel || rel.startsWith("..") || path35.isAbsolute(rel)) return true;
16973
+ const posixRel = rel.split(path35.sep).join("/");
16260
16974
  return this.ignoreMatcher.ignores(posixRel);
16261
16975
  }
16262
16976
  };
@@ -16490,22 +17204,22 @@ var FileMcpOAuthStore = class {
16490
17204
  filePath;
16491
17205
  load() {
16492
17206
  try {
16493
- return JSON.parse(fs31.readFileSync(this.filePath, "utf-8"));
17207
+ return JSON.parse(fs32.readFileSync(this.filePath, "utf-8"));
16494
17208
  } catch {
16495
17209
  return void 0;
16496
17210
  }
16497
17211
  }
16498
17212
  save(state) {
16499
- fs31.mkdirSync(path34.dirname(this.filePath), { recursive: true, mode: 448 });
16500
- fs31.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
17213
+ fs32.mkdirSync(path35.dirname(this.filePath), { recursive: true, mode: 448 });
17214
+ fs32.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
16501
17215
  try {
16502
- fs31.chmodSync(this.filePath, 384);
17216
+ fs32.chmodSync(this.filePath, 384);
16503
17217
  } catch {
16504
17218
  }
16505
17219
  }
16506
17220
  clear() {
16507
17221
  try {
16508
- fs31.rmSync(this.filePath);
17222
+ fs32.rmSync(this.filePath);
16509
17223
  } catch {
16510
17224
  }
16511
17225
  }
@@ -17153,7 +17867,7 @@ var TaskAnalyzer = class {
17153
17867
 
17154
17868
  // src/core/router/model-performance-tracker.ts
17155
17869
  init_pricing();
17156
- var DEFAULT_STATS_FILE = path34.join(os10.homedir(), ".cascade", "model-perf.json");
17870
+ var DEFAULT_STATS_FILE = path35.join(os10.homedir(), ".cascade", "model-perf.json");
17157
17871
  var ModelPerformanceTracker = class {
17158
17872
  stats = /* @__PURE__ */ new Map();
17159
17873
  featureStats = /* @__PURE__ */ new Map();
@@ -17193,7 +17907,7 @@ var ModelPerformanceTracker = class {
17193
17907
  async save() {
17194
17908
  if (this.readOnly) return;
17195
17909
  try {
17196
- await fs11.mkdir(path34.dirname(this.statsFile), { recursive: true });
17910
+ await fs11.mkdir(path35.dirname(this.statsFile), { recursive: true });
17197
17911
  const modelsObj = {};
17198
17912
  const featuresObj = {};
17199
17913
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -17741,7 +18455,7 @@ Required capability: ${description.slice(0, 300)}`;
17741
18455
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
17742
18456
  async loadPersistedTools() {
17743
18457
  if (!this.workspacePath || !this.persistEnabled) return;
17744
- const file = path34.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
18458
+ const file = path35.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
17745
18459
  try {
17746
18460
  const raw = await fs11.readFile(file, "utf-8");
17747
18461
  const specs = JSON.parse(raw);
@@ -17765,8 +18479,8 @@ Required capability: ${description.slice(0, 300)}`;
17765
18479
  }
17766
18480
  async persist() {
17767
18481
  if (!this.workspacePath || !this.persistEnabled) return;
17768
- const dir = path34.join(this.workspacePath, ".cascade");
17769
- const file = path34.join(dir, DYNAMIC_TOOLS_FILE);
18482
+ const dir = path35.join(this.workspacePath, ".cascade");
18483
+ const file = path35.join(dir, DYNAMIC_TOOLS_FILE);
17770
18484
  try {
17771
18485
  await fs11.mkdir(dir, { recursive: true });
17772
18486
  await fs11.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -17786,12 +18500,12 @@ var WorldStateDB = class {
17786
18500
  constructor(workspacePath, debugMode = false) {
17787
18501
  this.workspacePath = workspacePath;
17788
18502
  this.debugMode = debugMode;
17789
- const cascadeDir = path34.join(workspacePath, ".cascade");
17790
- if (!fs31.existsSync(cascadeDir)) {
17791
- fs31.mkdirSync(cascadeDir, { recursive: true });
18503
+ const cascadeDir = path35.join(workspacePath, ".cascade");
18504
+ if (!fs32.existsSync(cascadeDir)) {
18505
+ fs32.mkdirSync(cascadeDir, { recursive: true });
17792
18506
  }
17793
- this.keyPath = path34.join(cascadeDir, "world_state.key");
17794
- this.dbPath = path34.join(cascadeDir, "world_state.db");
18507
+ this.keyPath = path35.join(cascadeDir, "world_state.key");
18508
+ this.dbPath = path35.join(cascadeDir, "world_state.db");
17795
18509
  this.initEncryptionKey();
17796
18510
  this.db = new Database2(this.dbPath);
17797
18511
  this.db.pragma("journal_mode = WAL");
@@ -17848,11 +18562,11 @@ var WorldStateDB = class {
17848
18562
  ).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
17849
18563
  }
17850
18564
  initEncryptionKey() {
17851
- if (fs31.existsSync(this.keyPath)) {
17852
- this.encryptionKey = fs31.readFileSync(this.keyPath);
18565
+ if (fs32.existsSync(this.keyPath)) {
18566
+ this.encryptionKey = fs32.readFileSync(this.keyPath);
17853
18567
  } else {
17854
18568
  this.encryptionKey = crypto.randomBytes(32);
17855
- fs31.writeFileSync(this.keyPath, this.encryptionKey);
18569
+ fs32.writeFileSync(this.keyPath, this.encryptionKey);
17856
18570
  }
17857
18571
  }
17858
18572
  encrypt(text) {
@@ -18138,9 +18852,9 @@ var WorldStateDB = class {
18138
18852
  dumpDebugIfNeeded() {
18139
18853
  if (!this.debugMode) return;
18140
18854
  try {
18141
- const dumpPath = path34.join(os10.tmpdir(), "cascade_world_state_debug.json");
18855
+ const dumpPath = path35.join(os10.tmpdir(), "cascade_world_state_debug.json");
18142
18856
  const entries = this.getAllEntries();
18143
- fs31.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18857
+ fs32.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
18144
18858
  } catch (err) {
18145
18859
  console.error("Failed to dump debug world state", err);
18146
18860
  }
@@ -18197,7 +18911,7 @@ var ResumeStore = class {
18197
18911
  createdAt: checkpoint.createdAt ?? new Date(timestamp).toISOString()
18198
18912
  };
18199
18913
  await promises.mkdir(this.dir, { recursive: true });
18200
- const finalPath = path34.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18914
+ const finalPath = path35.join(this.dir, `${timestamp}-${safeId(record.taskId)}.json`);
18201
18915
  const tempPath = `${finalPath}.${process.pid}.tmp`;
18202
18916
  await promises.writeFile(tempPath, JSON.stringify(record, null, 2), "utf-8");
18203
18917
  await promises.rename(tempPath, finalPath);
@@ -18220,7 +18934,7 @@ var ResumeStore = class {
18220
18934
  const checkpoints = [];
18221
18935
  for (const candidate of candidates2) {
18222
18936
  try {
18223
- const raw = await promises.readFile(path34.join(this.dir, candidate.name), "utf-8");
18937
+ const raw = await promises.readFile(path35.join(this.dir, candidate.name), "utf-8");
18224
18938
  const parsed = JSON.parse(raw);
18225
18939
  if (isCheckpoint(parsed)) checkpoints.push(parsed);
18226
18940
  } catch {
@@ -18259,7 +18973,7 @@ var ResumeStore = class {
18259
18973
  const source = entries.find((name) => FILE_PATTERN.exec(name)?.[2] === wanted);
18260
18974
  if (!source) return null;
18261
18975
  const claimId = `${this.now()}-${safeId(target.taskId)}`;
18262
- await promises.rename(path34.join(this.dir, source), path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
18976
+ await promises.rename(path35.join(this.dir, source), path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`));
18263
18977
  return { checkpoint: target, claimId };
18264
18978
  } catch {
18265
18979
  return null;
@@ -18269,8 +18983,8 @@ var ResumeStore = class {
18269
18983
  async release(claimId) {
18270
18984
  try {
18271
18985
  await promises.rename(
18272
- path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18273
- path34.join(this.dir, `${claimId}.json`)
18986
+ path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`),
18987
+ path35.join(this.dir, `${claimId}.json`)
18274
18988
  );
18275
18989
  } catch {
18276
18990
  }
@@ -18278,7 +18992,7 @@ var ResumeStore = class {
18278
18992
  /** Discard a claim for good — the resumed run has taken ownership. */
18279
18993
  async settle(claimId) {
18280
18994
  try {
18281
- await promises.rm(path34.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
18995
+ await promises.rm(path35.join(this.dir, `${claimId}${CLAIM_SUFFIX}`), { force: true });
18282
18996
  } catch {
18283
18997
  }
18284
18998
  }
@@ -18307,8 +19021,8 @@ var ResumeStore = class {
18307
19021
  const stamp2 = Number(/^(\d+)-/.exec(name)?.[1]);
18308
19022
  if (!Number.isFinite(stamp2) || stamp2 >= cutoff) continue;
18309
19023
  await promises.rename(
18310
- path34.join(this.dir, name),
18311
- path34.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
19024
+ path35.join(this.dir, name),
19025
+ path35.join(this.dir, `${name.slice(0, -CLAIM_SUFFIX.length)}.json`)
18312
19026
  );
18313
19027
  reclaimed++;
18314
19028
  }
@@ -18324,7 +19038,7 @@ var ResumeStore = class {
18324
19038
  await Promise.all(entries.map(async (name) => {
18325
19039
  const match = FILE_PATTERN.exec(name);
18326
19040
  if (match?.[2] === wanted) {
18327
- await promises.rm(path34.join(this.dir, name), { force: true });
19041
+ await promises.rm(path35.join(this.dir, name), { force: true });
18328
19042
  }
18329
19043
  }));
18330
19044
  } catch {
@@ -18339,7 +19053,7 @@ var ResumeStore = class {
18339
19053
  const doomed = files.filter((f, index) => f.timestamp < cutoff || index >= this.maxCheckpoints);
18340
19054
  const staleTemps = entries.filter((name) => name.endsWith(".tmp")).filter((name) => (Number(FILE_PATTERN.exec(name.replace(/\.\d+\.tmp$/, ".json"))?.[1]) || 0) < cutoff);
18341
19055
  await Promise.all([...doomed.map((f) => f.name), ...staleTemps].map(
18342
- (name) => promises.rm(path34.join(this.dir, name), { force: true })
19056
+ (name) => promises.rm(path35.join(this.dir, name), { force: true })
18343
19057
  ));
18344
19058
  } catch {
18345
19059
  }
@@ -18716,7 +19430,7 @@ var WorkspaceIndex = class {
18716
19430
  isIgnored;
18717
19431
  chunker;
18718
19432
  constructor(opts) {
18719
- this.root = path34.resolve(opts.root);
19433
+ this.root = path35.resolve(opts.root);
18720
19434
  this.db = opts.db;
18721
19435
  this.namespace = opts.namespace ?? "code";
18722
19436
  this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
@@ -18748,14 +19462,14 @@ var WorkspaceIndex = class {
18748
19462
  return;
18749
19463
  }
18750
19464
  for (const entry of entries) {
18751
- const abs = path34.join(dir, entry.name);
19465
+ const abs = path35.join(dir, entry.name);
18752
19466
  if (entry.isDirectory()) {
18753
19467
  if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
18754
19468
  await walk(abs);
18755
19469
  } else if (entry.isFile()) {
18756
- const ext = path34.extname(entry.name).slice(1).toLowerCase();
19470
+ const ext = path35.extname(entry.name).slice(1).toLowerCase();
18757
19471
  if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
18758
- out.push(path34.relative(this.root, abs));
19472
+ out.push(path35.relative(this.root, abs));
18759
19473
  }
18760
19474
  }
18761
19475
  };
@@ -18785,7 +19499,7 @@ var WorkspaceIndex = class {
18785
19499
  const entries = [];
18786
19500
  for (const rel of rels) {
18787
19501
  try {
18788
- const abs = path34.join(this.root, rel);
19502
+ const abs = path35.join(this.root, rel);
18789
19503
  const stat = await fs11.stat(abs);
18790
19504
  if (stat.size > this.maxFileBytes) continue;
18791
19505
  const buf = await fs11.readFile(abs);
@@ -19275,13 +19989,13 @@ var Cascade = class _Cascade extends EventEmitter {
19275
19989
  this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
19276
19990
  try {
19277
19991
  this.router.setDeadModelStore(new DeadModelStore(
19278
- fileDeadModelPersistence(path34.join(workspacePath, ".cascade", "dead-models.json"))
19992
+ fileDeadModelPersistence(path35.join(workspacePath, ".cascade", "dead-models.json"))
19279
19993
  ));
19280
19994
  } catch {
19281
19995
  }
19282
19996
  try {
19283
19997
  this.resumeStore = new ResumeStore({
19284
- dir: path34.join(workspacePath, ".cascade", "resume")
19998
+ dir: path35.join(workspacePath, ".cascade", "resume")
19285
19999
  });
19286
20000
  } catch {
19287
20001
  }
@@ -19369,8 +20083,8 @@ var Cascade = class _Cascade extends EventEmitter {
19369
20083
  this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
19370
20084
  return;
19371
20085
  }
19372
- const dbPath = ci.dbPath || path34.join(this.workspacePath, ".cascade", "code-index.db");
19373
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
20086
+ const dbPath = ci.dbPath || path35.join(this.workspacePath, ".cascade", "code-index.db");
20087
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
19374
20088
  const db = new Database2(dbPath);
19375
20089
  db.pragma("journal_mode = WAL");
19376
20090
  const complete = chatCompleterFromProviders(this.config.providers);
@@ -20818,14 +21532,14 @@ ${finalOutput}` : ""
20818
21532
  init_constants();
20819
21533
  var CLOUD_SESSION_FILE = "cloud-session.json";
20820
21534
  function globalDir(homeDir = os10.homedir()) {
20821
- return path34.join(homeDir, GLOBAL_CONFIG_DIR);
21535
+ return path35.join(homeDir, GLOBAL_CONFIG_DIR);
20822
21536
  }
20823
21537
  function cloudSessionPath(dir = globalDir()) {
20824
- return path34.join(dir, CLOUD_SESSION_FILE);
21538
+ return path35.join(dir, CLOUD_SESSION_FILE);
20825
21539
  }
20826
21540
  function loadCloudSession(dir = globalDir()) {
20827
21541
  try {
20828
- const raw = fs31.readFileSync(cloudSessionPath(dir), "utf-8");
21542
+ const raw = fs32.readFileSync(cloudSessionPath(dir), "utf-8");
20829
21543
  const parsed = JSON.parse(raw);
20830
21544
  if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
20831
21545
  return parsed;
@@ -20834,17 +21548,17 @@ function loadCloudSession(dir = globalDir()) {
20834
21548
  }
20835
21549
  }
20836
21550
  function saveCloudSession(session, dir = globalDir()) {
20837
- fs31.mkdirSync(dir, { recursive: true, mode: 448 });
21551
+ fs32.mkdirSync(dir, { recursive: true, mode: 448 });
20838
21552
  const file = cloudSessionPath(dir);
20839
- fs31.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
21553
+ fs32.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
20840
21554
  try {
20841
- fs31.chmodSync(file, 384);
21555
+ fs32.chmodSync(file, 384);
20842
21556
  } catch {
20843
21557
  }
20844
21558
  }
20845
21559
  function clearCloudSession(dir = globalDir()) {
20846
21560
  try {
20847
- fs31.rmSync(cloudSessionPath(dir));
21561
+ fs32.rmSync(cloudSessionPath(dir));
20848
21562
  } catch {
20849
21563
  }
20850
21564
  }
@@ -21016,16 +21730,16 @@ var CloudClient = class _CloudClient {
21016
21730
  return (await this.refresh()).accessToken;
21017
21731
  }
21018
21732
  /** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
21019
- async authed(path39, init = {}) {
21020
- const call = (token) => fetch(this.url(path39), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21733
+ async authed(path40, init = {}) {
21734
+ const call = (token) => fetch(this.url(path40), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
21021
21735
  let res = await call(await this.accessToken());
21022
21736
  if (res.status === 401) res = await call((await this.refresh()).accessToken);
21023
21737
  if (!res.ok) throw new Error(`Request failed (${res.status}).`);
21024
21738
  const text = await res.text();
21025
21739
  return text ? JSON.parse(text) : {};
21026
21740
  }
21027
- authedGet(path39) {
21028
- return this.authed(path39);
21741
+ authedGet(path40) {
21742
+ return this.authed(path40);
21029
21743
  }
21030
21744
  async fetchMe(token) {
21031
21745
  const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
@@ -21886,7 +22600,7 @@ function StatusBarInternal({
21886
22600
  const savedStr = savedUsd > 0 ? ` \xB7 saved $${savedUsd.toFixed(4)}` : "";
21887
22601
  const rightStr = ` ${formatTokens(tokens)} \xB7 $${costUsd.toFixed(4)}${savedStr} ${isExecuting ? "\u26A1" : "\xB7"} `;
21888
22602
  const gap = Math.max(0, width - leftStr.length - rightStr.length);
21889
- const line = chalk7.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk7.bold(leftStr) + " ".repeat(gap) + rightStr);
22603
+ const line = chalk8.bgHex(theme.colors.primary).hex(theme.colors.background)(chalk8.bold(leftStr) + " ".repeat(gap) + rightStr);
21890
22604
  return /* @__PURE__ */ jsx(Text, { children: line });
21891
22605
  }
21892
22606
  function formatTokens(n) {
@@ -22355,7 +23069,7 @@ function renderContent(content, theme) {
22355
23069
  i
22356
23070
  );
22357
23071
  }
22358
- const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk7.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk7.bgHex(theme.colors.border)(t));
23072
+ const rendered = part.replace(/\*\*(.*?)\*\*/g, (_, t) => chalk8.bold(t)).replace(/`(.*?)`/g, (_, t) => chalk8.bgHex(theme.colors.border)(t));
22359
23073
  return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: rendered }, i);
22360
23074
  });
22361
23075
  }
@@ -22799,9 +23513,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22799
23513
  }
22800
23514
  } catch {
22801
23515
  }
22802
- const configPath = path34.join(workspacePath, ".cascade", "config.json");
23516
+ const configPath = path35.join(workspacePath, ".cascade", "config.json");
22803
23517
  try {
22804
- await fs11.mkdir(path34.dirname(configPath), { recursive: true });
23518
+ await fs11.mkdir(path35.dirname(configPath), { recursive: true });
22805
23519
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
22806
23520
  } catch (err) {
22807
23521
  const msg = err instanceof Error ? err.message : String(err);
@@ -22853,9 +23567,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22853
23567
  if (msg.includes("non-text parts")) return;
22854
23568
  originalLog(...args);
22855
23569
  };
22856
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
23570
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
22857
23571
  storeRef.current = store;
22858
- globalStoreRef.current = new MemoryStore(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
23572
+ globalStoreRef.current = new MemoryStore(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
22859
23573
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
22860
23574
  setIdentities(identityRows);
22861
23575
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -22935,7 +23649,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, s
22935
23649
  onThemeChange: (name) => setTheme(getTheme(name)),
22936
23650
  onExport: async (fmt) => {
22937
23651
  const stamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
22938
- const exportPath = path34.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
23652
+ const exportPath = path35.join(workspacePath, `cascade-export-${stamp2}.${fmt === "json" ? "json" : "md"}`);
22939
23653
  if (fmt === "json") {
22940
23654
  await fs11.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
22941
23655
  } else {
@@ -23843,7 +24557,7 @@ function stringifySlashOutput(val) {
23843
24557
  }
23844
24558
  async function searchSessionsAndMessages(query, workspacePath) {
23845
24559
  if (!query) return "Usage: /search <query>";
23846
- const dbPath = path34.join(workspacePath, CASCADE_DB_FILE);
24560
+ const dbPath = path35.join(workspacePath, CASCADE_DB_FILE);
23847
24561
  try {
23848
24562
  await fs11.access(dbPath);
23849
24563
  } catch {
@@ -23877,7 +24591,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
23877
24591
  async function diagnoseRuntime(config, workspacePath) {
23878
24592
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
23879
24593
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
23880
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24594
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23881
24595
  try {
23882
24596
  const sessions2 = store.listSessions(void 0, 3);
23883
24597
  return [
@@ -23896,7 +24610,7 @@ async function diagnoseRuntime(config, workspacePath) {
23896
24610
  }
23897
24611
  async function showRecentLogs(args, workspacePath) {
23898
24612
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
23899
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24613
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23900
24614
  try {
23901
24615
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
23902
24616
  if (!logs.length) return "No recent runtime logs.";
@@ -23908,7 +24622,7 @@ async function showRecentLogs(args, workspacePath) {
23908
24622
  async function loadSessionSnapshot(args, workspacePath) {
23909
24623
  const sessionId = args[0];
23910
24624
  if (!sessionId) return "Usage: /resume <sessionId>";
23911
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
24625
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
23912
24626
  try {
23913
24627
  const session = store.getSession(sessionId);
23914
24628
  if (!session) return `Session not found: ${sessionId}`;
@@ -24328,9 +25042,9 @@ function SetupWizard({ workspacePath, onComplete }) {
24328
25042
  ...anyAuto ? { cascadeAuto: true } : {}
24329
25043
  };
24330
25044
  const config = CascadeConfigSchema.parse(rawConfig);
24331
- const configDir = path34.join(workspacePath, ".cascade");
25045
+ const configDir = path35.join(workspacePath, ".cascade");
24332
25046
  await fs11.mkdir(configDir, { recursive: true });
24333
- const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
25047
+ const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
24334
25048
  await fs11.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
24335
25049
  savedConfigRef.current = config;
24336
25050
  dispatchRef.current({ type: "GO_DONE" });
@@ -24671,25 +25385,25 @@ function printTelemetryBanner() {
24671
25385
  async function initCommand(workspacePath = process.cwd()) {
24672
25386
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
24673
25387
  try {
24674
- const configDir = path34.join(workspacePath, ".cascade");
25388
+ const configDir = path35.join(workspacePath, ".cascade");
24675
25389
  await fs11.mkdir(configDir, { recursive: true });
24676
- const mdPath = path34.join(workspacePath, "CASCADE.md");
25390
+ const mdPath = path35.join(workspacePath, "CASCADE.md");
24677
25391
  if (!await fileExists(mdPath)) {
24678
25392
  await createDefaultCascadeMd(workspacePath);
24679
- spin.succeed(chalk7.green("Created CASCADE.md"));
25393
+ spin.succeed(chalk8.green("Created CASCADE.md"));
24680
25394
  }
24681
- const ignorePath = path34.join(workspacePath, ".cascadeignore");
25395
+ const ignorePath = path35.join(workspacePath, ".cascadeignore");
24682
25396
  if (!await fileExists(ignorePath)) {
24683
25397
  await createDefaultIgnoreFile(workspacePath);
24684
- spin.succeed(chalk7.green("Created .cascadeignore"));
25398
+ spin.succeed(chalk8.green("Created .cascadeignore"));
24685
25399
  }
24686
25400
  spin.stop();
24687
25401
  console.log();
24688
- console.log(chalk7.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
25402
+ console.log(chalk8.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
24689
25403
  console.log();
24690
- const configPath = path34.join(workspacePath, CASCADE_CONFIG_FILE);
25404
+ const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
24691
25405
  if (await fileExists(configPath)) {
24692
- console.log(chalk7.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
25406
+ console.log(chalk8.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
24693
25407
  console.log();
24694
25408
  }
24695
25409
  const config = await runSetupWizard(workspacePath);
@@ -24697,10 +25411,10 @@ async function initCommand(workspacePath = process.cwd()) {
24697
25411
  await cm.load();
24698
25412
  await cm.updateConfig(config);
24699
25413
  console.log();
24700
- console.log(chalk7.green(" \u25C8 Setup complete! Run `cascade` to start."));
25414
+ console.log(chalk8.green(" \u25C8 Setup complete! Run `cascade` to start."));
24701
25415
  console.log();
24702
25416
  } catch (err) {
24703
- console.error(chalk7.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
25417
+ console.error(chalk8.red(`Init failed: ${err instanceof Error ? err.message : String(err)}`));
24704
25418
  process.exit(1);
24705
25419
  }
24706
25420
  }
@@ -24715,7 +25429,20 @@ async function fileExists(p) {
24715
25429
 
24716
25430
  // src/cli/commands/doctor.ts
24717
25431
  init_constants();
25432
+
25433
+ // src/config/credential-discovery.ts
25434
+ init_revoked_credentials();
24718
25435
  var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
25436
+ var ANTHROPIC_OAUTH_WARNING = "Anthropic does not permit third-party tools to route requests through Claude subscription credentials, and blocks them server-side. Use an API key from the Claude Console, or ANTHROPIC_AUTH_TOKEN with a gateway. See code.claude.com/docs/en/legal-and-compliance.";
25437
+ var OPENAI_COMPATIBLE_ENV = [
25438
+ { id: "openrouter", env: "OPENROUTER_API_KEY", label: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1" },
25439
+ { id: "groq", env: "GROQ_API_KEY", label: "Groq", baseUrl: "https://api.groq.com/openai/v1" },
25440
+ { id: "deepseek", env: "DEEPSEEK_API_KEY", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
25441
+ { id: "xai", env: "XAI_API_KEY", label: "xAI", baseUrl: "https://api.x.ai/v1" },
25442
+ { id: "mistral", env: "MISTRAL_API_KEY", label: "Mistral", baseUrl: "https://api.mistral.ai/v1" },
25443
+ { id: "together", env: "TOGETHER_API_KEY", label: "Together AI", baseUrl: "https://api.together.xyz/v1" },
25444
+ { id: "fireworks", env: "FIREWORKS_API_KEY", label: "Fireworks AI", baseUrl: "https://api.fireworks.ai/inference/v1" }
25445
+ ];
24719
25446
  async function readJson(file) {
24720
25447
  try {
24721
25448
  const raw = await fs11.readFile(file, "utf-8");
@@ -24739,36 +25466,118 @@ function fromEnv(env) {
24739
25466
  for (const { env: name, provider } of map) {
24740
25467
  const secret = str(env[name]);
24741
25468
  if (!secret || seen.has(provider)) continue;
25469
+ if (isSubscriptionToken(secret)) {
25470
+ out.push({
25471
+ provider,
25472
+ sourceTool: `Environment (${name})`,
25473
+ kind: "oauth",
25474
+ secret,
25475
+ directlyUsable: false,
25476
+ warning: ANTHROPIC_OAUTH_WARNING
25477
+ });
25478
+ continue;
25479
+ }
24742
25480
  seen.add(provider);
24743
- out.push({ provider, sourceTool: `Environment (${name})`, kind: "api-key", secret, directlyUsable: true });
25481
+ const gateway = provider === "anthropic" ? str(env["ANTHROPIC_BASE_URL"]) : void 0;
25482
+ out.push({
25483
+ provider,
25484
+ sourceTool: `Environment (${name})`,
25485
+ kind: "api-key",
25486
+ secret,
25487
+ directlyUsable: true,
25488
+ ...gateway ? { baseUrl: gateway } : {}
25489
+ });
25490
+ }
25491
+ const authToken = str(env["ANTHROPIC_AUTH_TOKEN"]);
25492
+ if (authToken && !seen.has("anthropic")) {
25493
+ seen.add("anthropic");
25494
+ if (isSubscriptionToken(authToken)) {
25495
+ out.push({
25496
+ provider: "anthropic",
25497
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25498
+ kind: "oauth",
25499
+ secret: authToken,
25500
+ directlyUsable: false,
25501
+ warning: ANTHROPIC_OAUTH_WARNING
25502
+ });
25503
+ } else {
25504
+ const gateway = str(env["ANTHROPIC_BASE_URL"]);
25505
+ out.push({
25506
+ provider: "anthropic",
25507
+ sourceTool: "Environment (ANTHROPIC_AUTH_TOKEN)",
25508
+ kind: "bearer",
25509
+ secret: authToken,
25510
+ directlyUsable: Boolean(gateway),
25511
+ ...gateway ? { baseUrl: gateway } : {},
25512
+ ...gateway ? {} : {
25513
+ warning: "A bearer token is only valid at the gateway that issued it. Set ANTHROPIC_BASE_URL to that gateway, or configure `baseUrl` for the anthropic provider, before linking it."
25514
+ }
25515
+ });
25516
+ }
25517
+ }
25518
+ const azureKey = str(env["AZURE_OPENAI_KEY"]) ?? str(env["AZURE_OPENAI_API_KEY"]);
25519
+ if (azureKey) {
25520
+ const endpoint = str(env["AZURE_OPENAI_ENDPOINT"]);
25521
+ const deployment = str(env["AZURE_OPENAI_DEPLOYMENT"]) ?? str(env["AZURE_OPENAI_DEPLOYMENT_NAME"]);
25522
+ const apiVersion = str(env["AZURE_OPENAI_API_VERSION"]);
25523
+ const missing = [
25524
+ !endpoint && "AZURE_OPENAI_ENDPOINT",
25525
+ !deployment && "AZURE_OPENAI_DEPLOYMENT"
25526
+ ].filter((x) => typeof x === "string");
25527
+ out.push({
25528
+ provider: "azure",
25529
+ sourceTool: "Environment (AZURE_OPENAI_KEY)",
25530
+ kind: "api-key",
25531
+ secret: azureKey,
25532
+ directlyUsable: missing.length === 0,
25533
+ ...endpoint ? { baseUrl: endpoint } : {},
25534
+ ...deployment ? { deploymentName: deployment } : {},
25535
+ ...apiVersion ? { apiVersion } : {},
25536
+ ...missing.length ? { warning: `Azure also needs ${missing.join(" and ")} \u2014 a key alone cannot address a deployment.` } : {}
25537
+ });
25538
+ }
25539
+ for (const { id, env: name, label, baseUrl } of OPENAI_COMPATIBLE_ENV) {
25540
+ const secret = str(env[name]);
25541
+ if (!secret) continue;
25542
+ out.push({
25543
+ provider: "openai-compatible",
25544
+ sourceTool: `Environment (${name}) \u2014 ${label}`,
25545
+ kind: "api-key",
25546
+ secret,
25547
+ directlyUsable: true,
25548
+ baseUrl,
25549
+ serviceId: id
25550
+ });
24744
25551
  }
24745
25552
  return out;
24746
25553
  }
24747
- async function fromClaudeCode(home) {
24748
- const file = path34.join(home, ".claude", ".credentials.json");
25554
+ async function fromClaudeCode(home, env) {
25555
+ const dir = str(env["CLAUDE_CONFIG_DIR"]) ?? path35.join(home, ".claude");
25556
+ const file = path35.join(dir, ".credentials.json");
24749
25557
  const data = await readJson(file);
24750
25558
  if (!data) return [];
25559
+ const found = [];
25560
+ const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
25561
+ if (apiKey) {
25562
+ found.push({ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file });
25563
+ }
24751
25564
  const oauth = data["claudeAiOauth"];
24752
25565
  const oauthToken = str(oauth?.["accessToken"]);
24753
25566
  if (oauthToken) {
24754
- return [{
25567
+ found.push({
24755
25568
  provider: "anthropic",
24756
25569
  sourceTool: "Claude Code",
24757
25570
  kind: "oauth",
24758
25571
  secret: oauthToken,
24759
- directlyUsable: true,
24760
- warning: TOS_WARNING,
25572
+ directlyUsable: false,
25573
+ warning: ANTHROPIC_OAUTH_WARNING,
24761
25574
  sourcePath: file
24762
- }];
24763
- }
24764
- const apiKey = str(data["apiKey"]) ?? str(data["anthropicApiKey"]);
24765
- if (apiKey) {
24766
- return [{ provider: "anthropic", sourceTool: "Claude Code", kind: "api-key", secret: apiKey, directlyUsable: true, sourcePath: file }];
25575
+ });
24767
25576
  }
24768
- return [];
25577
+ return found;
24769
25578
  }
24770
25579
  async function fromCodex(home) {
24771
- const file = path34.join(home, ".codex", "auth.json");
25580
+ const file = path35.join(home, ".codex", "auth.json");
24772
25581
  const data = await readJson(file);
24773
25582
  if (!data) return [];
24774
25583
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -24791,7 +25600,7 @@ async function fromCodex(home) {
24791
25600
  return [];
24792
25601
  }
24793
25602
  async function fromGemini(home) {
24794
- const file = path34.join(home, ".gemini", "oauth_creds.json");
25603
+ const file = path35.join(home, ".gemini", "oauth_creds.json");
24795
25604
  const data = await readJson(file);
24796
25605
  const accessToken = str(data?.["access_token"]);
24797
25606
  if (!accessToken) return [];
@@ -24807,7 +25616,7 @@ async function fromGemini(home) {
24807
25616
  }
24808
25617
  async function fromCopilot(home) {
24809
25618
  for (const name of ["apps.json", "hosts.json"]) {
24810
- const file = path34.join(home, ".config", "github-copilot", name);
25619
+ const file = path35.join(home, ".config", "github-copilot", name);
24811
25620
  const data = await readJson(file);
24812
25621
  if (!data) continue;
24813
25622
  for (const value of Object.values(data)) {
@@ -24832,7 +25641,7 @@ async function discoverCredentials(opts = {}) {
24832
25641
  const env = opts.env ?? process.env;
24833
25642
  const groups = await Promise.all([
24834
25643
  Promise.resolve(fromEnv(env)),
24835
- fromClaudeCode(home),
25644
+ fromClaudeCode(home, env),
24836
25645
  fromCodex(home),
24837
25646
  fromGemini(home),
24838
25647
  fromCopilot(home)
@@ -24843,10 +25652,257 @@ function maskSecret(secret) {
24843
25652
  if (secret.length <= 8) return "\u2022\u2022\u2022\u2022";
24844
25653
  return `${secret.slice(0, 6)}\u2026${secret.slice(-2)}`;
24845
25654
  }
25655
+ async function linkCommand(target, options = {}) {
25656
+ const found = await discoverCredentials();
25657
+ if (found.length === 0) {
25658
+ console.log(chalk8.yellow("\n No reusable credentials found.\n"));
25659
+ console.log(chalk8.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
25660
+ console.log(chalk8.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
25661
+ return;
25662
+ }
25663
+ if (!target) {
25664
+ printDiscovered(found);
25665
+ return;
25666
+ }
25667
+ const resolved = normalizeProvider(target);
25668
+ if (!resolved) {
25669
+ const services2 = OPENAI_COMPATIBLE_ENV.map((s) => s.id).join(", ");
25670
+ console.log(chalk8.red(`
25671
+ Unknown provider "${target}".`));
25672
+ console.log(chalk8.gray(` Use one of: anthropic, openai, gemini, azure, openai-compatible`));
25673
+ console.log(chalk8.gray(` \u2026or name a compatible service directly: ${services2}
25674
+ `));
25675
+ return;
25676
+ }
25677
+ const { provider, serviceId } = resolved;
25678
+ const candidates2 = found.filter((c) => c.provider === provider && (!serviceId || c.serviceId === serviceId));
25679
+ const services = [...new Set(candidates2.map((c) => c.serviceId).filter((id) => !!id))];
25680
+ if (!serviceId && services.length > 1) {
25681
+ console.log(chalk8.yellow(`
25682
+ Keys for several OpenAI-compatible services are set, and "${target}" does not say which.`));
25683
+ console.log(chalk8.gray(" Name the one you mean:\n"));
25684
+ for (const id of services) console.log(chalk8.cyan(` cascade link ${id}`));
25685
+ console.log("");
25686
+ return;
25687
+ }
25688
+ const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
25689
+ if (!chosen) {
25690
+ console.log(chalk8.yellow(`
25691
+ No detected credential maps to "${provider}".
25692
+ `));
25693
+ return;
25694
+ }
25695
+ const cm = new ConfigManager(options.workspace ?? process.cwd());
25696
+ await cm.load();
25697
+ const configured = cm.getConfig().providers;
25698
+ const routedByConfig = isRoutedByConfig(chosen, configured);
25699
+ if (!chosen.directlyUsable && !routedByConfig) {
25700
+ console.log(chalk8.yellow(`
25701
+ Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
25702
+ if (chosen.warning) console.log(chalk8.gray(` ${chosen.warning}`));
25703
+ console.log(chalk8.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
25704
+ if (options.acceptRisk && chosen.kind === "oauth") {
25705
+ console.log(chalk8.gray(" --accept-risk no longer applies \u2014 a subscription token is refused by the"));
25706
+ console.log(chalk8.gray(" provider itself, so there is no working configuration to opt into.\n"));
25707
+ }
25708
+ console.log(chalk8.gray(` Set ${chalk8.white(envKeyFor(provider))} instead, or add a key with `) + chalk8.cyan("cascade init") + chalk8.gray(".\n"));
25709
+ return;
25710
+ }
25711
+ const adopted = await adoptCredential(chosen, cm);
25712
+ if (!adopted) return;
25713
+ console.log(chalk8.green(`
25714
+ \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
25715
+ if (chosen.kind === "bearer") {
25716
+ console.log(chalk8.gray(" Adopted as a bearer token \u2014 set `baseUrl` to the gateway that issued it."));
25717
+ }
25718
+ console.log(chalk8.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
25719
+ }
25720
+ function isRoutedByConfig(cred, configured) {
25721
+ if (cred.provider === "azure") {
25722
+ return configured.some((p) => p.type === "azure" && p.deploymentName?.trim() && p.baseUrl?.trim());
25723
+ }
25724
+ return cred.kind === "bearer" && configured.some((p) => p.type === cred.provider && p.baseUrl?.trim());
25725
+ }
25726
+ function willAdoptFromConfig(cred, configured) {
25727
+ if (cred.provider === "azure") {
25728
+ const target = cred.baseUrl?.trim();
25729
+ const deployment = cred.deploymentName?.trim();
25730
+ if (target && deployment) return !azureRoutedTarget(deployment, target, configured).collision;
25731
+ return azureDeploymentsForCredential(cred, configured).length > 0;
25732
+ }
25733
+ return cred.directlyUsable || isRoutedByConfig(cred, configured);
25734
+ }
25735
+ function azureRoutedTarget(deployment, target, configured) {
25736
+ const named = (p) => p.type === "azure" && (p.deploymentName?.trim() ?? "") === deployment;
25737
+ const existing = configured.find((p) => named(p) && (sameAzureEndpoint(p.baseUrl, target) || !p.baseUrl?.trim()));
25738
+ if (existing) return { existing, collision: false };
25739
+ return {
25740
+ collision: configured.some((p) => named(p) && !!p.baseUrl?.trim() && !sameAzureEndpoint(p.baseUrl, target))
25741
+ };
25742
+ }
25743
+ function azureDeploymentsForCredential(cred, configured) {
25744
+ const routing = resolveAzureRouting(configured, configured, {
25745
+ endpoint: cred.baseUrl,
25746
+ deployment: cred.deploymentName
25747
+ });
25748
+ return routing.ok ? routing.rows : [];
25749
+ }
25750
+ function azureRoutingForCredential(cred, configured) {
25751
+ return resolveAzureRouting(configured, configured, {
25752
+ endpoint: cred.baseUrl,
25753
+ deployment: cred.deploymentName
25754
+ });
25755
+ }
25756
+ function printDiscovered(found) {
25757
+ console.log(chalk8.magenta("\n \u25C8 Detected credentials\n"));
25758
+ for (const c of found) {
25759
+ const usable = c.directlyUsable ? chalk8.green("usable") : chalk8.yellow("needs vendor backend");
25760
+ const kind = c.kind === "oauth" ? chalk8.yellow("subscription") : c.kind === "bearer" ? chalk8.cyan("bearer") : chalk8.gray("api-key");
25761
+ console.log(` ${chalk8.white(c.provider.padEnd(18))} ${chalk8.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
25762
+ console.log(chalk8.gray(` from ${c.sourceTool}`));
25763
+ if (c.warning) console.log(chalk8.yellow(` \u26A0 ${c.warning}`));
25764
+ }
25765
+ console.log(chalk8.gray("\n Adopt one with: ") + chalk8.cyan("cascade link <provider>"));
25766
+ console.log(chalk8.gray(" Subscription tokens are listed for visibility and cannot be adopted.\n"));
25767
+ }
25768
+ function envKeyFor(provider) {
25769
+ if (provider === "openai") return "OPENAI_API_KEY";
25770
+ if (provider === "gemini") return "GEMINI_API_KEY";
25771
+ if (provider === "anthropic") return "ANTHROPIC_API_KEY";
25772
+ if (provider === "azure") return "AZURE_OPENAI_KEY";
25773
+ return "the provider's API key";
25774
+ }
25775
+ function normalizeProvider(target) {
25776
+ const t = target.toLowerCase();
25777
+ if (t === "anthropic" || t === "claude" || t === "claude-code") return { provider: "anthropic" };
25778
+ if (t === "openai" || t === "codex" || t === "gpt") return { provider: "openai" };
25779
+ if (t === "gemini" || t === "google") return { provider: "gemini" };
25780
+ if (t === "azure" || t === "azure-openai") return { provider: "azure" };
25781
+ if (t === "openai-compatible" || t === "compatible") return { provider: "openai-compatible" };
25782
+ const service = OPENAI_COMPATIBLE_ENV.find((s) => s.id === t || s.label.toLowerCase() === t);
25783
+ if (service) return { provider: "openai-compatible", serviceId: service.id };
25784
+ return null;
25785
+ }
25786
+ async function adoptCredential(cred, cm) {
25787
+ const config = cm.getConfig();
25788
+ const azureDeployments = cred.provider === "azure" ? config.providers.filter((p) => p.type === "azure" && p.deploymentName?.trim()) : [];
25789
+ if (azureDeployments.length > 0) {
25790
+ const target = cred.baseUrl?.trim();
25791
+ const deployment = cred.deploymentName?.trim();
25792
+ if (target && deployment) {
25793
+ const { existing: existing2, collision: claimedElsewhere } = azureRoutedTarget(deployment, target, config.providers);
25794
+ if (claimedElsewhere) {
25795
+ console.log(chalk8.yellow(`
25796
+ A different Azure resource already has a deployment named "${deployment}".`));
25797
+ console.log(chalk8.gray(" Deployment names are model ids in Cascade, so two resources cannot share one \u2014"));
25798
+ console.log(chalk8.gray(" the second would never be selected. Rename one deployment, or remove the entry"));
25799
+ console.log(chalk8.gray(" you no longer use, then run this again.\n"));
25800
+ return false;
25801
+ }
25802
+ const patch2 = {
25803
+ apiKey: cred.secret,
25804
+ credentialSource: cred.sourceTool,
25805
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25806
+ };
25807
+ const updated = config.providers.map((p) => {
25808
+ if (p === existing2) return { ...p, baseUrl: target, ...patch2 };
25809
+ const sibling = p.type === "azure" && p.deploymentName?.trim() && sameAzureEndpoint(p.baseUrl, target);
25810
+ return sibling ? { ...p, ...patch2 } : p;
25811
+ });
25812
+ const providers3 = existing2 ? updated : [...updated, {
25813
+ type: "azure",
25814
+ baseUrl: target,
25815
+ deploymentName: deployment,
25816
+ ...patch2
25817
+ }];
25818
+ await cm.updateConfig({ providers: providers3 });
25819
+ return true;
25820
+ }
25821
+ const scoped = azureDeploymentsForCredential(cred, config.providers);
25822
+ if (scoped.length === 0) {
25823
+ const all = [...new Set(azureDeployments.map((p) => normalizeAzureEndpoint(p.baseUrl)))];
25824
+ const onTarget = target ? azureDeployments.some((p) => sameAzureEndpoint(p.baseUrl, target)) : true;
25825
+ const anyRouted = azureDeployments.some((p) => p.baseUrl?.trim());
25826
+ if (!anyRouted) {
25827
+ console.log(chalk8.yellow("\n The configured Azure deployments have no endpoint."));
25828
+ console.log(chalk8.gray(" A deployment needs its resource URL before a key can reach it \u2014"));
25829
+ console.log(chalk8.gray(" set AZURE_OPENAI_ENDPOINT, or add `baseUrl` to the entries in"));
25830
+ console.log(chalk8.gray(" .cascade/config.json, then run this again.\n"));
25831
+ return false;
25832
+ }
25833
+ if (target && !onTarget) {
25834
+ console.log(chalk8.yellow(`
25835
+ No configured Azure deployment is on ${target}.`));
25836
+ console.log(chalk8.gray(" Set AZURE_OPENAI_DEPLOYMENT as well to add one, or point"));
25837
+ console.log(chalk8.gray(" AZURE_OPENAI_ENDPOINT at a resource you have configured:\n"));
25838
+ } else {
25839
+ console.log(chalk8.yellow("\n Several Azure resources are configured, and an Azure key belongs to one of them."));
25840
+ console.log(chalk8.gray(" Set AZURE_OPENAI_ENDPOINT to the resource this key is for, then run again:\n"));
25841
+ }
25842
+ for (const r of all) console.log(chalk8.gray(` ${r || "(no endpoint set)"}`));
25843
+ console.log("");
25844
+ return false;
25845
+ }
25846
+ const patch = {
25847
+ apiKey: cred.secret,
25848
+ credentialSource: cred.sourceTool,
25849
+ ...cred.apiVersion ? { apiVersion: cred.apiVersion } : {}
25850
+ };
25851
+ const providers2 = config.providers.map((p) => scoped.includes(p) ? { ...p, ...patch } : p);
25852
+ const routing = azureRoutingForCredential(cred, config.providers);
25853
+ if (routing.ok && routing.createDeployment) {
25854
+ providers2.push({
25855
+ type: "azure",
25856
+ deploymentName: routing.createDeployment,
25857
+ baseUrl: routing.resource,
25858
+ ...patch
25859
+ });
25860
+ console.log(chalk8.gray(
25861
+ ` Added deployment "${routing.createDeployment}" on ${normalizeAzureEndpoint(routing.resource)}.`
25862
+ ));
25863
+ }
25864
+ await cm.updateConfig({ providers: providers2 });
25865
+ return true;
25866
+ }
25867
+ const existing = config.providers.find((p) => p.type === cred.provider);
25868
+ const next = {
25869
+ ...existing,
25870
+ type: cred.provider,
25871
+ credentialSource: cred.sourceTool,
25872
+ // Both cleared first: adopting a credential REPLACES the old one, and
25873
+ // leaving a stale key beside a new token makes which one is in use depend
25874
+ // on provider-internal precedence.
25875
+ apiKey: void 0,
25876
+ authToken: void 0
25877
+ };
25878
+ if (cred.kind === "bearer") {
25879
+ next.authToken = cred.secret;
25880
+ } else {
25881
+ next.apiKey = cred.secret;
25882
+ }
25883
+ if (cred.kind !== "bearer" && !cred.baseUrl && hasDefaultEndpoint(cred.provider) && next.baseUrl) {
25884
+ delete next.baseUrl;
25885
+ delete next.local;
25886
+ }
25887
+ if (cred.baseUrl) {
25888
+ next.baseUrl = cred.baseUrl;
25889
+ delete next.local;
25890
+ }
25891
+ if (cred.deploymentName) next.deploymentName = cred.deploymentName;
25892
+ if (cred.apiVersion) next.apiVersion = cred.apiVersion;
25893
+ const providers = config.providers.filter((p) => p.type !== cred.provider);
25894
+ providers.push(next);
25895
+ await cm.updateConfig({ providers });
25896
+ return true;
25897
+ }
24846
25898
 
24847
25899
  // src/cli/commands/doctor.ts
25900
+ function linkableCredentialsDetail(discovered, configured = []) {
25901
+ const usable = discovered.filter((d) => willAdoptFromConfig(d, configured)).length;
25902
+ return usable > 0 ? `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt` : `${discovered.length} found, none usable \u2014 run \`cascade link\` to see why`;
25903
+ }
24848
25904
  async function doctorCommand() {
24849
- console.log(chalk7.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
25905
+ console.log(chalk8.magenta("\n \u25C8 Cascade Doctor \u2014 System Diagnostics\n"));
24850
25906
  const checks = [];
24851
25907
  const nodeVersion = process.versions.node;
24852
25908
  const [major] = nodeVersion.split(".").map(Number);
@@ -24861,7 +25917,7 @@ async function doctorCommand() {
24861
25917
  checks.push({
24862
25918
  label: "Cascade config",
24863
25919
  ok: true,
24864
- detail: `Loaded ${path34.join(process.cwd(), CASCADE_CONFIG_FILE)}`
25920
+ detail: `Loaded ${path35.join(process.cwd(), CASCADE_CONFIG_FILE)}`
24865
25921
  });
24866
25922
  const providers = [
24867
25923
  { type: "anthropic", name: "Anthropic" },
@@ -24871,10 +25927,11 @@ async function doctorCommand() {
24871
25927
  ];
24872
25928
  for (const { type, name } of providers) {
24873
25929
  const key = cm.getApiKey(type);
25930
+ const token = key ? void 0 : cm.getAuthToken(type);
24874
25931
  checks.push({
24875
- label: `${name} API key`,
24876
- ok: Boolean(key),
24877
- detail: key ? "Set" : "Missing"
25932
+ label: `${name} credential`,
25933
+ ok: Boolean(key || token),
25934
+ detail: key ? "API key set" : token ? "Bearer token set" : "Missing"
24878
25935
  });
24879
25936
  }
24880
25937
  const ollamaOk = await checkEndpoint(OLLAMA_BASE_URL + "/api/tags");
@@ -24928,32 +25985,31 @@ async function doctorCommand() {
24928
25985
  try {
24929
25986
  const discovered = await discoverCredentials();
24930
25987
  if (discovered.length > 0) {
24931
- const usable = discovered.filter((d) => d.directlyUsable).length;
24932
25988
  checks.push({
24933
25989
  label: "Linkable credentials",
24934
25990
  ok: true,
24935
- detail: `${discovered.length} found (${usable} usable) \u2014 run \`cascade link\` to adopt`
25991
+ detail: linkableCredentialsDetail(discovered, config.providers ?? [])
24936
25992
  });
24937
25993
  }
24938
25994
  } catch {
24939
25995
  }
24940
25996
  for (const c of checks) {
24941
- const icon = c.ok ? chalk7.green(" \u2713") : chalk7.yellow(" \u25CB");
24942
- const label = c.ok ? chalk7.white(c.label) : chalk7.gray(c.label);
24943
- const detail = c.detail ? chalk7.gray(` \u2014 ${c.detail}`) : "";
25997
+ const icon = c.ok ? chalk8.green(" \u2713") : chalk8.yellow(" \u25CB");
25998
+ const label = c.ok ? chalk8.white(c.label) : chalk8.gray(c.label);
25999
+ const detail = c.detail ? chalk8.gray(` \u2014 ${c.detail}`) : "";
24944
26000
  console.log(`${icon} ${label}${detail}`);
24945
26001
  }
24946
26002
  const failures2 = checks.filter((c) => !c.ok);
24947
26003
  console.log();
24948
26004
  if (failures2.length === 0) {
24949
- console.log(chalk7.green(" All checks passed!\n"));
26005
+ console.log(chalk8.green(" All checks passed!\n"));
24950
26006
  } else {
24951
- const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("API key"));
26007
+ const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("credential"));
24952
26008
  if (critical.length) {
24953
- console.log(chalk7.yellow(` ${critical.length} issue(s) need attention.
26009
+ console.log(chalk8.yellow(` ${critical.length} issue(s) need attention.
24954
26010
  `));
24955
26011
  } else {
24956
- console.log(chalk7.gray(` ${failures2.length} optional item(s) not configured.
26012
+ console.log(chalk8.gray(` ${failures2.length} optional item(s) not configured.
24957
26013
  `));
24958
26014
  }
24959
26015
  }
@@ -24971,19 +26027,19 @@ async function checkEndpoint(url) {
24971
26027
  }
24972
26028
  }
24973
26029
  async function indexCommand(dirPath) {
24974
- const workspace = path34.resolve(dirPath || process.cwd());
26030
+ const workspace = path35.resolve(dirPath || process.cwd());
24975
26031
  const cm = new ConfigManager(workspace);
24976
26032
  await cm.load();
24977
26033
  const config = cm.getConfig();
24978
26034
  const embedder = embedderFromProviders(config.providers);
24979
26035
  if (!embedder) {
24980
- console.log(chalk7.red("\n No embeddings-capable provider configured."));
24981
- console.log(chalk7.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
26036
+ console.log(chalk8.red("\n No embeddings-capable provider configured."));
26037
+ console.log(chalk8.dim(" Add an OpenAI, OpenAI-compatible, or Ollama key, then re-run `cascade index`.\n"));
24982
26038
  process.exitCode = 1;
24983
26039
  return;
24984
26040
  }
24985
- const dbPath = config.codeIndex?.dbPath || path34.join(workspace, ".cascade", "code-index.db");
24986
- fs31.mkdirSync(path34.dirname(dbPath), { recursive: true });
26041
+ const dbPath = config.codeIndex?.dbPath || path35.join(workspace, ".cascade", "code-index.db");
26042
+ fs32.mkdirSync(path35.dirname(dbPath), { recursive: true });
24987
26043
  const db = new Database2(dbPath);
24988
26044
  db.pragma("journal_mode = WAL");
24989
26045
  const complete = chatCompleterFromProviders(config.providers);
@@ -24997,20 +26053,20 @@ async function indexCommand(dirPath) {
24997
26053
  reranker,
24998
26054
  isIgnored: (abs) => ignore4.isIgnored(abs, workspace)
24999
26055
  });
25000
- console.log(chalk7.magenta(`
26056
+ console.log(chalk8.magenta(`
25001
26057
  \u25C8 Indexing ${workspace}
25002
26058
  `));
25003
26059
  let seen = 0;
25004
26060
  const res = await index.refresh(() => {
25005
26061
  seen++;
25006
- if (seen % 10 === 0) process.stdout.write(chalk7.dim(` \u2026embedded ${seen} changed files\r`));
26062
+ if (seen % 10 === 0) process.stdout.write(chalk8.dim(` \u2026embedded ${seen} changed files\r`));
25007
26063
  });
25008
26064
  console.log(
25009
- chalk7.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk7.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
26065
+ chalk8.green(` \u2713 ${res.filesIndexed} file(s) indexed`) + chalk8.dim(` \xB7 ${res.chunks} chunks \xB7 ${res.filesUnchanged} unchanged \xB7 ${res.filesRemoved} removed`)
25010
26066
  );
25011
- console.log(chalk7.dim(` Index: ${dbPath}`));
26067
+ console.log(chalk8.dim(` Index: ${dbPath}`));
25012
26068
  if (!config.codeIndex?.enabled) {
25013
- console.log(chalk7.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
26069
+ console.log(chalk8.dim(" Tip: set codeIndex.enabled = true in your config to give runs the code_search tool."));
25014
26070
  }
25015
26071
  console.log("");
25016
26072
  db.close();
@@ -25059,6 +26115,7 @@ async function decryptJSON(blob, passphrase) {
25059
26115
  }
25060
26116
 
25061
26117
  // src/cloud/keysync.ts
26118
+ init_revoked_credentials();
25062
26119
  function providerSig(p) {
25063
26120
  return [p.type, p.baseUrl ?? "", p.deploymentName ?? "", p.label ?? ""].join("\0");
25064
26121
  }
@@ -25131,6 +26188,19 @@ function gatherSyncBundle(config) {
25131
26188
  function applySyncBundle(bundle, config, cleanup) {
25132
26189
  const dropped = stripRetiredProviders(bundle);
25133
26190
  if (cleanup) cleanup.removed = dropped.removed;
26191
+ let revokedFromBundle = 0;
26192
+ let unusableFromBundle = 0;
26193
+ if (Array.isArray(bundle.providers)) {
26194
+ const pass = stripRevokedCredentials(bundle.providers, { keepForEndpoint: false });
26195
+ revokedFromBundle = pass.removed;
26196
+ if (pass.removed > 0) bundle.providers = pass.kept;
26197
+ const usable = bundle.providers.filter((p) => {
26198
+ const orphanBearer = Boolean(p.authToken) && !p.apiKey && !p.baseUrl;
26199
+ if (orphanBearer) unusableFromBundle++;
26200
+ return !orphanBearer;
26201
+ });
26202
+ if (unusableFromBundle > 0) bundle.providers = usable;
26203
+ }
25134
26204
  const next = { ...config, tools: { ...config.tools } };
25135
26205
  if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
25136
26206
  if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
@@ -25148,10 +26218,15 @@ function applySyncBundle(bundle, config, cleanup) {
25148
26218
  if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
25149
26219
  const merged = { ...next.models };
25150
26220
  const clearedFromMerge = clearRetiredPins(merged);
25151
- if (clearedFromMerge.length > 0) next.models = merged;
26221
+ const clearedRevoked = revokedFromBundle > 0 && !hasUsableAnthropic(next.providers ?? []) ? clearAnthropicPins(merged, next.providers ?? []) : [];
26222
+ if (clearedFromMerge.length > 0 || clearedRevoked.length > 0) {
26223
+ next.models = merged;
26224
+ }
25152
26225
  if (cleanup) {
26226
+ cleanup.revokedCredentials = revokedFromBundle;
26227
+ cleanup.unusableCredentials = unusableFromBundle;
25153
26228
  cleanup.clearedPins = ["t1", "t2", "t3"].filter(
25154
- (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t)) && next.models?.[t] === void 0
26229
+ (t) => (dropped.clearedPins.includes(t) || clearedFromMerge.includes(t) || clearedRevoked.some((c) => c.tier === t)) && next.models?.[t] === void 0
25155
26230
  );
25156
26231
  }
25157
26232
  if (bundle.prefs) {
@@ -25177,43 +26252,43 @@ function openBrowser(url) {
25177
26252
  }
25178
26253
  }
25179
26254
  function notSignedIn() {
25180
- console.log(chalk7.dim("\n Not signed in. Run `cascade login` first.\n"));
26255
+ console.log(chalk8.dim("\n Not signed in. Run `cascade login` first.\n"));
25181
26256
  process.exitCode = 1;
25182
26257
  }
25183
26258
  function promptHidden(question) {
25184
26259
  return new Promise((resolve2) => {
25185
26260
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true });
25186
- let masked = false;
26261
+ let masked2 = false;
25187
26262
  rl._writeToOutput = (s) => {
25188
- if (!masked || !process.stdin.isTTY) process.stdout.write(s);
26263
+ if (!masked2 || !process.stdin.isTTY) process.stdout.write(s);
25189
26264
  };
25190
26265
  rl.question(question, (answer) => {
25191
26266
  rl.close();
25192
26267
  process.stdout.write("\n");
25193
26268
  resolve2(answer.trim());
25194
26269
  });
25195
- masked = true;
26270
+ masked2 = true;
25196
26271
  });
25197
26272
  }
25198
26273
  async function loginCommand(opts = {}) {
25199
26274
  const serverUrl = resolveServerUrl(opts.server);
25200
26275
  const client = new CloudClient(serverUrl);
25201
- console.log(chalk7.magenta("\n \u25C8 Sign in to Cascade Cloud"));
25202
- console.log(chalk7.dim(` ${serverUrl}
26276
+ console.log(chalk8.magenta("\n \u25C8 Sign in to Cascade Cloud"));
26277
+ console.log(chalk8.dim(` ${serverUrl}
25203
26278
  `));
25204
26279
  try {
25205
26280
  const session = await client.runDeviceLogin((d) => {
25206
- console.log(` 1. Open ${chalk7.cyan(d.verificationUri)}`);
25207
- console.log(` 2. Enter ${chalk7.bold.yellow(d.userCode)}`);
25208
- console.log(chalk7.dim("\n Waiting for you to approve\u2026"));
26281
+ console.log(` 1. Open ${chalk8.cyan(d.verificationUri)}`);
26282
+ console.log(` 2. Enter ${chalk8.bold.yellow(d.userCode)}`);
26283
+ console.log(chalk8.dim("\n Waiting for you to approve\u2026"));
25209
26284
  openBrowser(`${d.verificationUri}?code=${encodeURIComponent(d.userCode)}`);
25210
26285
  });
25211
26286
  const who = session.user.name || session.user.email || session.user.id;
25212
- console.log(chalk7.green(`
26287
+ console.log(chalk8.green(`
25213
26288
  \u2713 Signed in as ${who}
25214
26289
  `));
25215
26290
  } catch (err) {
25216
- console.log(chalk7.red(`
26291
+ console.log(chalk8.red(`
25217
26292
  ${err instanceof Error ? err.message : String(err)}
25218
26293
  `));
25219
26294
  process.exitCode = 1;
@@ -25222,11 +26297,11 @@ async function loginCommand(opts = {}) {
25222
26297
  async function logoutCommand() {
25223
26298
  const client = CloudClient.fromSession();
25224
26299
  if (!client) {
25225
- console.log(chalk7.dim("\n Not signed in.\n"));
26300
+ console.log(chalk8.dim("\n Not signed in.\n"));
25226
26301
  return;
25227
26302
  }
25228
26303
  await client.logout();
25229
- console.log(chalk7.green("\n \u2713 Signed out.\n"));
26304
+ console.log(chalk8.green("\n \u2713 Signed out.\n"));
25230
26305
  }
25231
26306
  async function whoamiCommand() {
25232
26307
  const client = CloudClient.fromSession();
@@ -25234,10 +26309,10 @@ async function whoamiCommand() {
25234
26309
  try {
25235
26310
  const me = await client.me();
25236
26311
  console.log(`
25237
- ${chalk7.bold(me.name || "\u2014")} ${chalk7.dim(me.email || "")}${me.plan ? chalk7.dim(` \xB7 ${me.plan}`) : ""}
26312
+ ${chalk8.bold(me.name || "\u2014")} ${chalk8.dim(me.email || "")}${me.plan ? chalk8.dim(` \xB7 ${me.plan}`) : ""}
25238
26313
  `);
25239
26314
  } catch (err) {
25240
- console.log(chalk7.red(`
26315
+ console.log(chalk8.red(`
25241
26316
  ${err instanceof Error ? err.message : String(err)}
25242
26317
  `));
25243
26318
  process.exitCode = 1;
@@ -25249,16 +26324,16 @@ async function sessionsCommand() {
25249
26324
  try {
25250
26325
  const convos = await client.listConversations();
25251
26326
  if (convos.length === 0) {
25252
- console.log(chalk7.dim("\n No cloud chats yet.\n"));
26327
+ console.log(chalk8.dim("\n No cloud chats yet.\n"));
25253
26328
  return;
25254
26329
  }
25255
- console.log(chalk7.magenta("\n \u25C8 Your cloud chats\n"));
26330
+ console.log(chalk8.magenta("\n \u25C8 Your cloud chats\n"));
25256
26331
  for (const c of convos) {
25257
- console.log(` ${chalk7.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk7.dim("(untitled)")}`);
26332
+ console.log(` ${chalk8.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk8.dim("(untitled)")}`);
25258
26333
  }
25259
- console.log(chalk7.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
26334
+ console.log(chalk8.dim("\n cascade sessions show <id> read a chat (id or its first characters)\n"));
25260
26335
  } catch (err) {
25261
- console.log(chalk7.red(`
26336
+ console.log(chalk8.red(`
25262
26337
  ${err instanceof Error ? err.message : String(err)}
25263
26338
  `));
25264
26339
  process.exitCode = 1;
@@ -25272,9 +26347,9 @@ function formatCloudTranscript(messages) {
25272
26347
  for (const m of messages) {
25273
26348
  const n = m.siblingIds?.length ?? 1;
25274
26349
  const i = m.siblingIds && m.id ? m.siblingIds.indexOf(m.id) + 1 : 1;
25275
- const branch = n > 1 ? chalk7.yellow(` \u2039${i}/${n}\u203A`) : "";
25276
- const tag = m.id ? chalk7.dim(` [${shortId2(m.id)}]`) : "";
25277
- const who = m.role === "user" ? chalk7.bold.cyan("You") : chalk7.bold.magenta("Cascade");
26350
+ const branch = n > 1 ? chalk8.yellow(` \u2039${i}/${n}\u203A`) : "";
26351
+ const tag = m.id ? chalk8.dim(` [${shortId2(m.id)}]`) : "";
26352
+ const who = m.role === "user" ? chalk8.bold.cyan("You") : chalk8.bold.magenta("Cascade");
25278
26353
  lines.push(` ${who}${tag}${branch}`);
25279
26354
  lines.push(" " + m.content.replace(/\n/g, "\n ") + "\n");
25280
26355
  }
@@ -25290,23 +26365,23 @@ async function sessionShowCommand(idOrPrefix) {
25290
26365
  try {
25291
26366
  const match = await resolveConversation(client, idOrPrefix);
25292
26367
  if (!match) {
25293
- console.log(chalk7.red(`
26368
+ console.log(chalk8.red(`
25294
26369
  No chat matching "${idOrPrefix}".
25295
26370
  `));
25296
26371
  process.exitCode = 1;
25297
26372
  return;
25298
26373
  }
25299
26374
  const msgs = await client.getMessages(match.id);
25300
- console.log(chalk7.magenta(`
26375
+ console.log(chalk8.magenta(`
25301
26376
  \u25C8 ${match.title?.trim() || "(untitled)"}
25302
26377
  `));
25303
26378
  console.log(formatCloudTranscript(msgs));
25304
26379
  if (msgs.some((m) => (m.siblingIds?.length ?? 1) > 1)) {
25305
- console.log(chalk7.dim(" cascade sessions branch <chat> <message> switch to another version"));
26380
+ console.log(chalk8.dim(" cascade sessions branch <chat> <message> switch to another version"));
25306
26381
  }
25307
- console.log(chalk7.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
26382
+ console.log(chalk8.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
25308
26383
  } catch (err) {
25309
- console.log(chalk7.red(`
26384
+ console.log(chalk8.red(`
25310
26385
  ${err instanceof Error ? err.message : String(err)}
25311
26386
  `));
25312
26387
  process.exitCode = 1;
@@ -25321,7 +26396,7 @@ async function sessionBranchCommand(chat, message) {
25321
26396
  try {
25322
26397
  const match = await resolveConversation(client, chat);
25323
26398
  if (!match) {
25324
- console.log(chalk7.red(`
26399
+ console.log(chalk8.red(`
25325
26400
  No chat matching "${chat}".
25326
26401
  `));
25327
26402
  process.exitCode = 1;
@@ -25330,12 +26405,12 @@ async function sessionBranchCommand(chat, message) {
25330
26405
  const active = await client.getMessages(match.id);
25331
26406
  const targetId = resolveMessage(active, message) ?? message;
25332
26407
  const next = await client.selectBranch(match.id, targetId);
25333
- console.log(chalk7.magenta(`
26408
+ console.log(chalk8.magenta(`
25334
26409
  \u25C8 Switched branch \u2014 ${match.title?.trim() || "(untitled)"}
25335
26410
  `));
25336
26411
  console.log(formatCloudTranscript(next));
25337
26412
  } catch (err) {
25338
- console.log(chalk7.red(`
26413
+ console.log(chalk8.red(`
25339
26414
  ${err instanceof Error ? err.message : String(err)}
25340
26415
  `));
25341
26416
  process.exitCode = 1;
@@ -25347,7 +26422,7 @@ async function sessionRemoveMessageCommand(chat, message) {
25347
26422
  try {
25348
26423
  const match = await resolveConversation(client, chat);
25349
26424
  if (!match) {
25350
- console.log(chalk7.red(`
26425
+ console.log(chalk8.red(`
25351
26426
  No chat matching "${chat}".
25352
26427
  `));
25353
26428
  process.exitCode = 1;
@@ -25356,11 +26431,11 @@ async function sessionRemoveMessageCommand(chat, message) {
25356
26431
  const active = await client.getMessages(match.id);
25357
26432
  const targetId = resolveMessage(active, message) ?? message;
25358
26433
  const next = await client.deleteMessage(match.id, targetId);
25359
- console.log(chalk7.green("\n \u2713 Deleted that message and its replies.\n"));
26434
+ console.log(chalk8.green("\n \u2713 Deleted that message and its replies.\n"));
25360
26435
  if (next.length) console.log(formatCloudTranscript(next));
25361
- else console.log(chalk7.dim(" (the conversation is now empty)\n"));
26436
+ else console.log(chalk8.dim(" (the conversation is now empty)\n"));
25362
26437
  } catch (err) {
25363
- console.log(chalk7.red(`
26438
+ console.log(chalk8.red(`
25364
26439
  ${err instanceof Error ? err.message : String(err)}
25365
26440
  `));
25366
26441
  process.exitCode = 1;
@@ -25372,18 +26447,18 @@ async function sessionRenameCommand(chat, title) {
25372
26447
  try {
25373
26448
  const match = await resolveConversation(client, chat);
25374
26449
  if (!match) {
25375
- console.log(chalk7.red(`
26450
+ console.log(chalk8.red(`
25376
26451
  No chat matching "${chat}".
25377
26452
  `));
25378
26453
  process.exitCode = 1;
25379
26454
  return;
25380
26455
  }
25381
26456
  await client.renameConversation(match.id, title);
25382
- console.log(chalk7.green(`
26457
+ console.log(chalk8.green(`
25383
26458
  \u2713 Renamed to "${title}".
25384
26459
  `));
25385
26460
  } catch (err) {
25386
- console.log(chalk7.red(`
26461
+ console.log(chalk8.red(`
25387
26462
  ${err instanceof Error ? err.message : String(err)}
25388
26463
  `));
25389
26464
  process.exitCode = 1;
@@ -25395,18 +26470,18 @@ async function sessionDeleteCommand(chat) {
25395
26470
  try {
25396
26471
  const match = await resolveConversation(client, chat);
25397
26472
  if (!match) {
25398
- console.log(chalk7.red(`
26473
+ console.log(chalk8.red(`
25399
26474
  No chat matching "${chat}".
25400
26475
  `));
25401
26476
  process.exitCode = 1;
25402
26477
  return;
25403
26478
  }
25404
26479
  await client.deleteConversation(match.id);
25405
- console.log(chalk7.green(`
26480
+ console.log(chalk8.green(`
25406
26481
  \u2713 Deleted "${match.title?.trim() || "(untitled)"}".
25407
26482
  `));
25408
26483
  } catch (err) {
25409
- console.log(chalk7.red(`
26484
+ console.log(chalk8.red(`
25410
26485
  ${err instanceof Error ? err.message : String(err)}
25411
26486
  `));
25412
26487
  process.exitCode = 1;
@@ -25418,21 +26493,21 @@ async function syncPushCommand() {
25418
26493
  const cm = new ConfigManager(process.cwd());
25419
26494
  await cm.load();
25420
26495
  const bundle = gatherSyncBundle(cm.getConfig());
25421
- console.log(chalk7.magenta("\n \u25C8 Sync your settings to your account"));
25422
- console.log(chalk7.dim(" Encrypted on this device with a passphrase we never see.\n"));
26496
+ console.log(chalk8.magenta("\n \u25C8 Sync your settings to your account"));
26497
+ console.log(chalk8.dim(" Encrypted on this device with a passphrase we never see.\n"));
25423
26498
  const pass = await promptHidden(" Passphrase: ");
25424
26499
  if (!pass) {
25425
- console.log(chalk7.dim("\n Cancelled.\n"));
26500
+ console.log(chalk8.dim("\n Cancelled.\n"));
25426
26501
  return;
25427
26502
  }
25428
26503
  try {
25429
26504
  const blob = await encryptJSON(bundle, pass);
25430
26505
  const r = await client.pushSecrets(blob);
25431
- console.log(chalk7.green(`
26506
+ console.log(chalk8.green(`
25432
26507
  \u2713 Synced (v${r.version}). Run \`cascade sync pull\` on another device.
25433
26508
  `));
25434
26509
  } catch (err) {
25435
- console.log(chalk7.red(`
26510
+ console.log(chalk8.red(`
25436
26511
  ${err instanceof Error ? err.message : String(err)}
25437
26512
  `));
25438
26513
  process.exitCode = 1;
@@ -25445,27 +26520,27 @@ async function syncPullCommand() {
25445
26520
  try {
25446
26521
  ({ blob } = await client.pullSecrets());
25447
26522
  } catch (err) {
25448
- console.log(chalk7.red(`
26523
+ console.log(chalk8.red(`
25449
26524
  ${err instanceof Error ? err.message : String(err)}
25450
26525
  `));
25451
26526
  process.exitCode = 1;
25452
26527
  return;
25453
26528
  }
25454
26529
  if (!blob) {
25455
- console.log(chalk7.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
26530
+ console.log(chalk8.dim("\n Nothing synced to your account yet. Run `cascade sync push` first.\n"));
25456
26531
  return;
25457
26532
  }
25458
- console.log(chalk7.magenta("\n \u25C8 Apply your synced settings to this device\n"));
26533
+ console.log(chalk8.magenta("\n \u25C8 Apply your synced settings to this device\n"));
25459
26534
  const pass = await promptHidden(" Passphrase: ");
25460
26535
  if (!pass) {
25461
- console.log(chalk7.dim("\n Cancelled.\n"));
26536
+ console.log(chalk8.dim("\n Cancelled.\n"));
25462
26537
  return;
25463
26538
  }
25464
26539
  let bundle;
25465
26540
  try {
25466
26541
  bundle = await decryptJSON(blob, pass);
25467
26542
  } catch {
25468
- console.log(chalk7.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
26543
+ console.log(chalk8.red("\n Could not decrypt \u2014 check your passphrase and try again.\n"));
25469
26544
  process.exitCode = 1;
25470
26545
  return;
25471
26546
  }
@@ -25474,13 +26549,13 @@ async function syncPullCommand() {
25474
26549
  await cm.load();
25475
26550
  const cleanup = { removed: [], clearedPins: [] };
25476
26551
  await cm.updateConfig(applySyncBundle(bundle, cm.getConfig(), cleanup));
25477
- console.log(chalk7.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
26552
+ console.log(chalk8.green("\n \u2713 Applied your synced settings. Your keys are ready here.\n"));
25478
26553
  if (didCleanupChangeAnything(cleanup)) {
25479
- console.log(chalk7.yellow(` ${describeCleanup(cleanup)}
26554
+ console.log(chalk8.yellow(` ${describeCleanup(cleanup)}
25480
26555
  `));
25481
26556
  }
25482
26557
  } catch (err) {
25483
- console.log(chalk7.red(`
26558
+ console.log(chalk8.red(`
25484
26559
  Could not apply your synced settings: ${err instanceof Error ? err.message : String(err)}
25485
26560
  `));
25486
26561
  process.exitCode = 1;
@@ -25496,7 +26571,7 @@ function openBrowser2(url) {
25496
26571
  }
25497
26572
  function storePathFor(name) {
25498
26573
  const safe = name.replace(/[^a-z0-9._-]/gi, "_").slice(0, 64) || "server";
25499
- return path34.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
26574
+ return path35.join(os10.homedir(), ".cascade-ai", "mcp-oauth", `${safe}.json`);
25500
26575
  }
25501
26576
  function defaultName(url) {
25502
26577
  try {
@@ -25507,8 +26582,8 @@ function defaultName(url) {
25507
26582
  }
25508
26583
  async function mcpConnectCommand(url, opts = {}) {
25509
26584
  const desiredName = opts.name?.trim() || defaultName(url);
25510
- console.log(chalk7.magenta("\n \u25C8 Connect an MCP server via OAuth"));
25511
- console.log(chalk7.dim(` ${url}
26585
+ console.log(chalk8.magenta("\n \u25C8 Connect an MCP server via OAuth"));
26586
+ console.log(chalk8.dim(` ${url}
25512
26587
  `));
25513
26588
  try {
25514
26589
  const cm = new ConfigManager(process.cwd());
@@ -25524,7 +26599,7 @@ async function mcpConnectCommand(url, opts = {}) {
25524
26599
  store,
25525
26600
  clientName: "Cascade AI",
25526
26601
  openUrl: (u) => {
25527
- console.log(chalk7.dim(" Opening your browser to authorize\u2026"));
26602
+ console.log(chalk8.dim(" Opening your browser to authorize\u2026"));
25528
26603
  openBrowser2(u);
25529
26604
  }
25530
26605
  });
@@ -25534,11 +26609,11 @@ async function mcpConnectCommand(url, opts = {}) {
25534
26609
  config.tools.mcpServers = servers;
25535
26610
  config.tools.mcpTrusted = Array.from(/* @__PURE__ */ new Set([...config.tools.mcpTrusted ?? [], name]));
25536
26611
  await cm.updateConfig(config);
25537
- console.log(chalk7.green(`
26612
+ console.log(chalk8.green(`
25538
26613
  \u2713 Connected "${name}". Its tools are available to your runs.
25539
26614
  `));
25540
26615
  } catch (err) {
25541
- console.log(chalk7.red(`
26616
+ console.log(chalk8.red(`
25542
26617
  ${err instanceof Error ? err.message : String(err)}
25543
26618
  `));
25544
26619
  process.exitCode = 1;
@@ -25549,13 +26624,13 @@ async function mcpListCommand() {
25549
26624
  await cm.load();
25550
26625
  const servers = cm.getConfig().tools?.mcpServers ?? [];
25551
26626
  if (!servers.length) {
25552
- console.log(chalk7.dim("\n No MCP servers configured.\n"));
26627
+ console.log(chalk8.dim("\n No MCP servers configured.\n"));
25553
26628
  return;
25554
26629
  }
25555
- console.log(chalk7.magenta("\n \u25C8 MCP servers\n"));
26630
+ console.log(chalk8.magenta("\n \u25C8 MCP servers\n"));
25556
26631
  for (const s of servers) {
25557
- const kind = s.oauthStore ? chalk7.green("oauth") : s.headers ? chalk7.cyan("token") : s.command ? chalk7.dim("local") : chalk7.dim("open");
25558
- console.log(` ${chalk7.bold(s.name)} ${chalk7.dim(s.url || s.command || "")} ${kind}`);
26632
+ const kind = s.oauthStore ? chalk8.green("oauth") : s.headers ? chalk8.cyan("token") : s.command ? chalk8.dim("local") : chalk8.dim("open");
26633
+ console.log(` ${chalk8.bold(s.name)} ${chalk8.dim(s.url || s.command || "")} ${kind}`);
25559
26634
  }
25560
26635
  console.log();
25561
26636
  }
@@ -25566,7 +26641,7 @@ async function mcpRemoveCommand(name) {
25566
26641
  const servers = config.tools?.mcpServers ?? [];
25567
26642
  const match = servers.find((s) => s.name === name);
25568
26643
  if (!match) {
25569
- console.log(chalk7.red(`
26644
+ console.log(chalk8.red(`
25570
26645
  No MCP server named "${name}".
25571
26646
  `));
25572
26647
  process.exitCode = 1;
@@ -25582,7 +26657,7 @@ async function mcpRemoveCommand(name) {
25582
26657
  }
25583
26658
  }
25584
26659
  await cm.updateConfig(config);
25585
- console.log(chalk7.green(`
26660
+ console.log(chalk8.green(`
25586
26661
  \u2713 Removed "${name}".
25587
26662
  `));
25588
26663
  }
@@ -25596,16 +26671,302 @@ async function updateCommand() {
25596
26671
  const { stdout } = await execAsync2("npm show cascade-ai version", { timeout: 1e4 });
25597
26672
  const latest = stdout.trim().split("\n").filter(Boolean).pop() ?? "";
25598
26673
  if (latest === CASCADE_VERSION) {
25599
- spin.succeed(chalk7.green(`Already up to date (v${CASCADE_VERSION})`));
26674
+ spin.succeed(chalk8.green(`Already up to date (v${CASCADE_VERSION})`));
25600
26675
  return;
25601
26676
  }
25602
26677
  spin.text = `Updating cascade-ai ${CASCADE_VERSION} \u2192 ${latest}\u2026`;
25603
26678
  await execAsync2("npm install -g cascade-ai@latest", { timeout: 6e4 });
25604
- spin.succeed(chalk7.green(`Updated to v${latest}! Restart your terminal.`));
26679
+ spin.succeed(chalk8.green(`Updated to v${latest}! Restart your terminal.`));
25605
26680
  } catch (err) {
25606
- spin.fail(chalk7.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26681
+ spin.fail(chalk8.red(`Update failed: ${err instanceof Error ? err.message : String(err)}`));
26682
+ }
26683
+ }
26684
+
26685
+ // src/config/credential-write.ts
26686
+ function endpointFromSettingsPayload(endpoints, type) {
26687
+ if (!endpoints || !Object.hasOwn(endpoints, type)) return { kind: "preserve" };
26688
+ const baseUrl = endpoints[type]?.trim();
26689
+ return baseUrl ? { kind: "at", baseUrl } : { kind: "cleared" };
26690
+ }
26691
+ function targetEndpoint(endpoint, existing) {
26692
+ switch (endpoint.kind) {
26693
+ case "at":
26694
+ return endpoint.baseUrl;
26695
+ // An explicit clear, always. Whether the row then falls back to a public
26696
+ // host or becomes unroutable is the provider's business, decided before we
26697
+ // get here.
26698
+ case "cleared":
26699
+ return void 0;
26700
+ case "preserve":
26701
+ return existing?.baseUrl;
25607
26702
  }
25608
26703
  }
26704
+ function namesCustomHost(row) {
26705
+ if (!row?.baseUrl) return false;
26706
+ return !sameCredentialEndpoint(row.type, row.baseUrl, void 0);
26707
+ }
26708
+ function applyProviderCredential(providers, type, apiKey, endpoint) {
26709
+ const existing = providers.find((p) => p.type === type);
26710
+ if (endpoint.kind === "cleared" && !hasDefaultEndpoint(type)) {
26711
+ if (existing) {
26712
+ existing.baseUrl = void 0;
26713
+ existing.apiKey = void 0;
26714
+ existing.authToken = void 0;
26715
+ delete existing.local;
26716
+ }
26717
+ return { written: false, reason: "unroutable" };
26718
+ }
26719
+ if (endpoint.kind === "preserve" && namesCustomHost(existing)) {
26720
+ return { written: false, reason: "ambiguous-scope" };
26721
+ }
26722
+ const nextBaseUrl = targetEndpoint(endpoint, existing);
26723
+ if (existing && !sameCredentialEndpoint(type, existing.baseUrl, nextBaseUrl)) {
26724
+ delete existing.local;
26725
+ }
26726
+ if (existing && nextBaseUrl === void 0) existing.baseUrl = void 0;
26727
+ applyProviderApiKey(providers, type, apiKey, nextBaseUrl ? { baseUrl: nextBaseUrl } : {});
26728
+ return { written: true };
26729
+ }
26730
+ function credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) {
26731
+ if (replacementKey) return "replaced";
26732
+ return sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl) ? "keep" : "clear";
26733
+ }
26734
+ function applyEndpointEdit(existing, nextBaseUrl, replacementKey) {
26735
+ if (credentialDispositionForEdit(existing, nextBaseUrl, replacementKey) === "clear") {
26736
+ existing.apiKey = void 0;
26737
+ existing.authToken = void 0;
26738
+ }
26739
+ if (!sameCredentialEndpoint(existing.type, existing.baseUrl, nextBaseUrl)) {
26740
+ delete existing.local;
26741
+ }
26742
+ existing.baseUrl = nextBaseUrl || void 0;
26743
+ }
26744
+ function explainRefusal(type, reason) {
26745
+ return reason === "unroutable" ? `The ${type} key was not saved: its endpoint was cleared, and ${type} has no default host, so there is nowhere the key could be sent. Enter the endpoint URL alongside the key.` : `The ${type} key was not saved: this provider is configured with a custom endpoint, and Cascade cannot tell whether the new key was issued by that endpoint or by the provider directly. Set the endpoint field alongside the key \u2014 clear it if the key is for the provider itself.`;
26746
+ }
26747
+ function applySettingsCredentials(providers, data) {
26748
+ const refused = [];
26749
+ if (data.keys) {
26750
+ for (const [type, apiKey] of Object.entries(data.keys)) {
26751
+ if (!apiKey) continue;
26752
+ const outcome = applyProviderCredential(
26753
+ providers,
26754
+ type,
26755
+ apiKey,
26756
+ endpointFromSettingsPayload(data.endpoints, type)
26757
+ );
26758
+ if (!outcome.written) refused.push({ type, reason: outcome.reason });
26759
+ }
26760
+ }
26761
+ if (!data.endpoints) return { refused };
26762
+ for (const [type, baseUrl] of Object.entries(data.endpoints)) {
26763
+ if (type === "azure") continue;
26764
+ if (!Object.hasOwn(data.endpoints, type)) continue;
26765
+ const existing = providers.find((p) => p.type === type);
26766
+ if (!existing) {
26767
+ if (baseUrl) providers.push({ type, baseUrl });
26768
+ continue;
26769
+ }
26770
+ applyEndpointEdit(existing, baseUrl, data.keys?.[type]);
26771
+ }
26772
+ return { refused };
26773
+ }
26774
+
26775
+ // src/config/settings-payload.ts
26776
+ function priorAzureRow(prior, incoming) {
26777
+ if (!incoming.deploymentName) return void 0;
26778
+ return prior.find((p) => p.deploymentName === incoming.deploymentName && sameAzureEndpoint(p.baseUrl, incoming.baseUrl));
26779
+ }
26780
+ function applySettingsPayload(config, data) {
26781
+ if (!Array.isArray(config.providers)) config.providers = [];
26782
+ const result = applySettingsCredentials(config.providers, data);
26783
+ if (Array.isArray(data.azureDeployments)) {
26784
+ const priorAzure = config.providers.filter((p) => p.type === "azure");
26785
+ const nonAzure = config.providers.filter((p) => p.type !== "azure");
26786
+ const nextAzure = data.azureDeployments.map((d) => {
26787
+ const prior = priorAzureRow(priorAzure, d);
26788
+ const apiKey = d.apiKey ? d.apiKey : prior?.apiKey;
26789
+ const model = d.model ?? prior?.model;
26790
+ const region = d.region ?? prior?.region;
26791
+ return {
26792
+ type: "azure",
26793
+ ...d.label ? { label: d.label } : {},
26794
+ ...d.baseUrl ? { baseUrl: d.baseUrl } : {},
26795
+ ...d.deploymentName ? { deploymentName: d.deploymentName } : {},
26796
+ ...d.apiVersion ? { apiVersion: d.apiVersion } : {},
26797
+ ...apiKey ? { apiKey } : {},
26798
+ ...model ? { model } : {},
26799
+ ...region ? { region } : {}
26800
+ };
26801
+ }).filter((d) => d.apiKey || d.baseUrl || d.deploymentName);
26802
+ config.providers = [...nonAzure, ...nextAzure];
26803
+ }
26804
+ if (data.models) {
26805
+ const models = config.models ?? {};
26806
+ for (const [tier, val] of Object.entries(data.models)) {
26807
+ if (val && val !== "auto") models[tier] = val;
26808
+ else delete models[tier];
26809
+ }
26810
+ config.models = models;
26811
+ }
26812
+ if (data.budget) {
26813
+ const b = data.budget;
26814
+ config.budget = config.budget ?? {};
26815
+ const cap = (key, target, accept, round = false) => {
26816
+ if (!Object.hasOwn(b, key)) return;
26817
+ const v = b[key];
26818
+ if (typeof v === "number" && accept(v)) config.budget[target] = round ? Math.floor(v) : v;
26819
+ else delete config.budget[target];
26820
+ };
26821
+ cap("maxCostPerRun", "maxCostPerRunUsd", (n) => n > 0);
26822
+ cap("dailyBudgetUsd", "dailyBudgetUsd", (n) => n > 0);
26823
+ cap("sessionBudgetUsd", "sessionBudgetUsd", (n) => n > 0);
26824
+ cap("maxTokensPerRun", "maxTokensPerRun", (n) => n > 0, true);
26825
+ cap("warnAtPct", "warnAtPct", (n) => n > 0 && n <= 100);
26826
+ if (b.autoBias === "balanced" || b.autoBias === "quality" || b.autoBias === "cost") {
26827
+ config.autoBias = b.autoBias;
26828
+ }
26829
+ }
26830
+ if (data.webSearch && typeof data.webSearch === "object") {
26831
+ config.tools = config.tools ?? {};
26832
+ const prior = config.tools.webSearch ?? {};
26833
+ const next = { ...prior };
26834
+ if (typeof data.webSearch.searxngUrl === "string") next.searxngUrl = data.webSearch.searxngUrl.trim() || void 0;
26835
+ if (data.webSearch.braveApiKey) next.braveApiKey = data.webSearch.braveApiKey;
26836
+ if (data.webSearch.tavilyApiKey) next.tavilyApiKey = data.webSearch.tavilyApiKey;
26837
+ config.tools.webSearch = next;
26838
+ }
26839
+ if (data.advanced && typeof data.advanced === "object") {
26840
+ const a = data.advanced;
26841
+ const num = (v, min, max) => {
26842
+ const n = Number(v);
26843
+ return Number.isFinite(n) && n >= min && n <= max ? n : void 0;
26844
+ };
26845
+ if (a["autonomy"] === "manual" || a["autonomy"] === "auto") config.autonomy = a["autonomy"];
26846
+ if (["never", "complex", "all", "always"].includes(a["planApproval"])) config.planApproval = a["planApproval"];
26847
+ {
26848
+ const n = num(a["approvalTimeoutMs"], 1e3, 864e5);
26849
+ if (n !== void 0) config.approvalTimeoutMs = Math.floor(n);
26850
+ }
26851
+ if (["auto", "parallel", "sequential"].includes(a["t3Execution"])) config.t3Execution = a["t3Execution"];
26852
+ {
26853
+ const n = num(a["localConcurrency"], 1, 16);
26854
+ if (n !== void 0) config.localConcurrency = Math.floor(n);
26855
+ }
26856
+ {
26857
+ const n = num(a["localInferenceTimeoutMs"], 1e3, 36e5);
26858
+ if (n !== void 0) config.localInferenceTimeoutMs = Math.floor(n);
26859
+ }
26860
+ {
26861
+ const n = num(a["cloudInferenceTimeoutMs"], 1e3, 36e5);
26862
+ if (n !== void 0) config.cloudInferenceTimeoutMs = Math.floor(n);
26863
+ }
26864
+ if (typeof a["reflectionEnabled"] === "boolean") config.reflection = { ...config.reflection ?? {}, enabled: a["reflectionEnabled"] };
26865
+ if (typeof a["cascadeAuto"] === "boolean") config.cascadeAuto = a["cascadeAuto"];
26866
+ if (["auto", "T1", "T2", "T3"].includes(a["forceTier"])) config.routing = { ...config.routing ?? {}, forceTier: a["forceTier"] };
26867
+ if (typeof a["benchmarksLive"] === "boolean") config.benchmarks = { ...config.benchmarks ?? {}, live: a["benchmarksLive"] };
26868
+ if (["isolate", "worker", "auto"].includes(a["dynamicToolSandbox"])) config.tools = { ...config.tools ?? {}, dynamicToolSandbox: a["dynamicToolSandbox"] };
26869
+ if (typeof a["factsExtraction"] === "boolean") config.knowledge = { ...config.knowledge ?? {}, factsExtraction: a["factsExtraction"] };
26870
+ if (typeof a["rememberSessions"] === "boolean") config.memory = { ...config.memory ?? {}, rememberSessions: a["rememberSessions"] };
26871
+ if (typeof a["enableToolCreation"] === "boolean") config.enableToolCreation = a["enableToolCreation"];
26872
+ if (typeof a["persistDynamicTools"] === "boolean") config.persistDynamicTools = a["persistDynamicTools"];
26873
+ if (typeof a["telemetryEnabled"] === "boolean") config.telemetry = { ...config.telemetry ?? {}, enabled: a["telemetryEnabled"] };
26874
+ }
26875
+ return result;
26876
+ }
26877
+ function settingsSnapshot(config) {
26878
+ const providers = config.providers ?? [];
26879
+ const endpoints = {};
26880
+ for (const p of providers) {
26881
+ if (p?.type && p?.baseUrl && p.type !== "azure") endpoints[p.type] = p.baseUrl;
26882
+ }
26883
+ const ws = config.tools?.webSearch ?? {};
26884
+ return {
26885
+ models: config.models ?? {},
26886
+ budget: {
26887
+ maxCostPerRun: config.budget?.maxCostPerRunUsd,
26888
+ autoBias: config.autoBias,
26889
+ dailyBudgetUsd: config.budget?.dailyBudgetUsd,
26890
+ sessionBudgetUsd: config.budget?.sessionBudgetUsd,
26891
+ maxTokensPerRun: config.budget?.maxTokensPerRun,
26892
+ warnAtPct: config.budget?.warnAtPct
26893
+ },
26894
+ // Through the shared predicate: `authToken` is a credential too, and
26895
+ // counting only `apiKey` showed a bearer-configured provider as unconfigured.
26896
+ providersWithKey: providers.filter((p) => hasProviderCredential(p)).map((p) => p.type),
26897
+ endpoints,
26898
+ azureDeployments: providers.filter((p) => p.type === "azure").map((p) => ({
26899
+ label: p.label,
26900
+ baseUrl: p.baseUrl,
26901
+ deploymentName: p.deploymentName,
26902
+ apiVersion: p.apiVersion,
26903
+ // Surfaced so a panel that round-trips the snapshot cannot drop them.
26904
+ model: p.model,
26905
+ region: p.region,
26906
+ hasKey: typeof p.apiKey === "string" && p.apiKey.length > 0
26907
+ })),
26908
+ webSearch: {
26909
+ searxngUrl: ws.searxngUrl,
26910
+ hasBraveKey: typeof ws.braveApiKey === "string" && ws.braveApiKey.length > 0,
26911
+ hasTavilyKey: typeof ws.tavilyApiKey === "string" && ws.tavilyApiKey.length > 0
26912
+ },
26913
+ advanced: {
26914
+ autonomy: config.autonomy,
26915
+ planApproval: config.planApproval,
26916
+ approvalTimeoutMs: config.approvalTimeoutMs,
26917
+ t3Execution: config.t3Execution,
26918
+ localConcurrency: config.localConcurrency,
26919
+ localInferenceTimeoutMs: config.localInferenceTimeoutMs,
26920
+ cloudInferenceTimeoutMs: config.cloudInferenceTimeoutMs,
26921
+ reflectionEnabled: config.reflection?.enabled,
26922
+ cascadeAuto: config.cascadeAuto,
26923
+ forceTier: config.routing?.forceTier,
26924
+ benchmarksLive: config.benchmarks?.live,
26925
+ dynamicToolSandbox: config.tools?.dynamicToolSandbox,
26926
+ factsExtraction: config.knowledge?.factsExtraction,
26927
+ rememberSessions: config.memory?.rememberSessions,
26928
+ enableToolCreation: config.enableToolCreation,
26929
+ persistDynamicTools: config.persistDynamicTools,
26930
+ telemetryEnabled: config.telemetry?.enabled
26931
+ }
26932
+ };
26933
+ }
26934
+ async function commitSettings(live, data, write) {
26935
+ const staged = structuredClone(live);
26936
+ const { refused } = applySettingsPayload(staged, data);
26937
+ let committed;
26938
+ try {
26939
+ committed = validateConfig(staged);
26940
+ } catch (err) {
26941
+ return {
26942
+ ok: false,
26943
+ refused,
26944
+ error: `Those settings are not valid: ${err instanceof Error ? err.message : String(err)}`
26945
+ };
26946
+ }
26947
+ const written = await write(committed);
26948
+ if (!written.ok) {
26949
+ return { ok: false, refused, error: `Could not write the config file: ${written.error}` };
26950
+ }
26951
+ replaceInPlace(live, committed);
26952
+ return { ok: true, refused };
26953
+ }
26954
+ function replaceInPlace(target, source) {
26955
+ const t = target;
26956
+ for (const key of Object.keys(t)) delete t[key];
26957
+ Object.assign(t, structuredClone(source));
26958
+ }
26959
+ function writeConfigFile(configPath, config) {
26960
+ try {
26961
+ fs32.mkdirSync(path35.dirname(configPath), { recursive: true });
26962
+ fs32.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
26963
+ return { ok: true };
26964
+ } catch (err) {
26965
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
26966
+ }
26967
+ }
26968
+
26969
+ // src/dashboard/server.ts
25609
26970
  init_constants();
25610
26971
  var JWT_ALGORITHM = "HS256";
25611
26972
  function createToken(user, secret) {
@@ -25853,12 +27214,22 @@ var DashboardSocket = class {
25853
27214
  });
25854
27215
  });
25855
27216
  }
27217
+ /**
27218
+ * The handler's result is ACKNOWLEDGED to the caller.
27219
+ *
27220
+ * A settings save can be partly refused — a key whose host cannot be
27221
+ * determined is deliberately not stored — and this path had no way to say so,
27222
+ * so the panel cleared the input and reported success over a key the backend
27223
+ * had dropped. Socket.IO's own ack channel carries it back, which needs no
27224
+ * new event name and cannot be missed by a client that forgot to subscribe.
27225
+ */
25856
27226
  onConfigUpdate(callback) {
25857
27227
  this.io.on("connection", (socket) => {
25858
- socket.on("config:update", (payload) => {
25859
- if (typeof payload === "object" && payload !== null) {
25860
- callback(payload);
25861
- }
27228
+ socket.on("config:update", (payload, ack) => {
27229
+ if (typeof payload !== "object" || payload === null) return;
27230
+ void callback(payload).then((result) => {
27231
+ if (typeof ack === "function") ack(result);
27232
+ });
25862
27233
  });
25863
27234
  });
25864
27235
  }
@@ -26004,8 +27375,47 @@ function aggregateCostStats(sessions2, opts = {}) {
26004
27375
  }
26005
27376
 
26006
27377
  // src/dashboard/server.ts
26007
- var __dirname$1 = path34.dirname(fileURLToPath(import.meta.url));
27378
+ var __dirname$1 = path35.dirname(fileURLToPath(import.meta.url));
26008
27379
  var ORPHANED_ESCALATION_GRACE_MS = 3e4;
27380
+ var MASK = "***";
27381
+ function masked(key, value) {
27382
+ return { [key]: value ? MASK : void 0 };
27383
+ }
27384
+ function redactProviderSecrets(config) {
27385
+ const safe = { ...config };
27386
+ safe.providers = (config.providers ?? []).map((p) => ({
27387
+ ...p,
27388
+ ...masked("apiKey", p.apiKey),
27389
+ ...masked("authToken", p.authToken)
27390
+ }));
27391
+ if (config.tools) {
27392
+ safe.tools = { ...config.tools };
27393
+ if (config.tools.webSearch) {
27394
+ safe.tools.webSearch = {
27395
+ ...config.tools.webSearch,
27396
+ ...masked("braveApiKey", config.tools.webSearch.braveApiKey),
27397
+ ...masked("tavilyApiKey", config.tools.webSearch.tavilyApiKey)
27398
+ };
27399
+ }
27400
+ if (config.tools.mcpServers) {
27401
+ safe.tools.mcpServers = config.tools.mcpServers.map((server) => ({
27402
+ ...server,
27403
+ ...server.headers ? { headers: Object.fromEntries(Object.keys(server.headers).map((k) => [k, MASK])) } : {},
27404
+ ...server.env ? { env: Object.fromEntries(Object.keys(server.env).map((k) => [k, MASK])) } : {}
27405
+ }));
27406
+ }
27407
+ }
27408
+ if (config.dashboard) {
27409
+ safe.dashboard = { ...config.dashboard, ...masked("secret", config.dashboard.secret) };
27410
+ }
27411
+ if (config.telemetry) {
27412
+ safe.telemetry = {
27413
+ ...config.telemetry,
27414
+ ...masked("posthogApiKey", config.telemetry.posthogApiKey)
27415
+ };
27416
+ }
27417
+ return safe;
27418
+ }
26009
27419
  var DashboardServer = class {
26010
27420
  app;
26011
27421
  httpServer;
@@ -26080,40 +27490,21 @@ var DashboardServer = class {
26080
27490
  this.activeSessions.get(sessionId)?.rateLastRun(rating);
26081
27491
  });
26082
27492
  this.socket.onConfigGet((socketId) => {
26083
- this.socket.emitToSocket(socketId, "config:current", {
26084
- models: this.config.models ?? {},
26085
- budget: {
26086
- maxCostPerRun: this.config.budget?.maxCostPerRunUsd,
26087
- autoBias: this.config.autoBias
26088
- },
26089
- providersWithKey: (this.config.providers ?? []).filter((p) => typeof p.apiKey === "string" && p.apiKey.length > 0).map((p) => p.type)
26090
- });
27493
+ this.socket.emitToSocket(socketId, "config:current", settingsSnapshot(this.config));
26091
27494
  });
26092
- this.socket.onConfigUpdate((data) => {
26093
- if (data.keys) {
26094
- for (const [type, apiKey] of Object.entries(data.keys)) {
26095
- if (!apiKey) continue;
26096
- const provider = this.config.providers.find((p) => p.type === type);
26097
- if (provider) provider.apiKey = apiKey;
26098
- else this.config.providers.push({ type, apiKey });
26099
- }
26100
- }
26101
- if (data.models) {
26102
- const models = this.config.models;
26103
- for (const [tier, val] of Object.entries(data.models)) {
26104
- if (val && val !== "auto") models[tier] = val;
26105
- else delete models[tier];
26106
- }
26107
- }
26108
- if (data.budget) {
26109
- if (typeof data.budget.maxCostPerRun === "number") {
26110
- this.config.budget.maxCostPerRunUsd = data.budget.maxCostPerRun;
26111
- }
26112
- if (data.budget.autoBias === "balanced" || data.budget.autoBias === "quality" || data.budget.autoBias === "cost") {
26113
- this.config.autoBias = data.budget.autoBias;
26114
- }
27495
+ this.socket.onConfigUpdate(async (data) => {
27496
+ const result = await commitSettings(this.config, data, (committed) => this.persistConfig(committed));
27497
+ const refused = result.refused.map((r) => ({
27498
+ type: r.type,
27499
+ reason: r.reason,
27500
+ message: explainRefusal(r.type, r.reason)
27501
+ }));
27502
+ for (const r of refused) console.warn(r.message);
27503
+ if (!result.ok) {
27504
+ return { refused, snapshot: settingsSnapshot(this.config), error: result.error };
26115
27505
  }
26116
- this.persistConfig();
27506
+ this.syncGlobalCredentials();
27507
+ return { refused, snapshot: settingsSnapshot(this.config) };
26117
27508
  });
26118
27509
  this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
26119
27510
  const sessionId = requestedSessionId ?? randomUUID();
@@ -26282,16 +27673,47 @@ var DashboardServer = class {
26282
27673
  * made over the socket (Settings → Save) persist across restarts. Best-effort:
26283
27674
  * a write failure is logged but never crashes the running dashboard.
26284
27675
  */
26285
- persistConfig() {
26286
- try {
26287
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
26288
- fs31.mkdirSync(path34.dirname(configPath), { recursive: true });
26289
- fs31.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
26290
- } catch (err) {
26291
- console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
26292
- }
27676
+ /**
27677
+ * Returns the durability of the WORKSPACE write, because a caller
27678
+ * acknowledging a save has to know it happened.
27679
+ *
27680
+ * This was `void` and swallowed every filesystem error, so a read-only
27681
+ * config path or a full disk produced: the live object mutated, the write
27682
+ * failed, the socket acknowledged success, and the panel cleared the keys the
27683
+ * user had typed. The changes then vanished at the next restart. "The handler
27684
+ * ran" is not "the save is durable".
27685
+ *
27686
+ * The global-credential sync below is genuinely best-effort — it is a
27687
+ * convenience copy, and failing it does not lose what the user just entered.
27688
+ */
27689
+ /**
27690
+ * @param config REQUIRED, with no default. `commitSettings` writes the
27691
+ * validated copy BEFORE adopting it, so a writer that reached for
27692
+ * `this.config` instead would persist the configuration being replaced — and
27693
+ * a default parameter is exactly the affordance that lets that happen
27694
+ * silently and still typecheck.
27695
+ */
27696
+ persistConfig(config) {
27697
+ const result = writeConfigFile(path35.join(this.workspacePath, CASCADE_CONFIG_FILE), config);
27698
+ if (!result.ok) console.warn(`[dashboard] Failed to persist config: ${result.error}`);
27699
+ return result;
27700
+ }
27701
+ /**
27702
+ * Copy provider credentials to the machine-global store, so keys saved over
27703
+ * the socket survive a workspace switch — the same sync `ConfigManager.save()`
27704
+ * performs.
27705
+ *
27706
+ * Separate from the workspace write, and called only AFTER it succeeds. It
27707
+ * used to run unconditionally inside `persistConfig()`, so a failed workspace
27708
+ * write still pushed the provider half of a rejected save into the global
27709
+ * store: half the change persisted, behind an error saying none of it had.
27710
+ *
27711
+ * Best-effort on its own account: this is a convenience copy, and failing it
27712
+ * does not lose what the user just entered.
27713
+ */
27714
+ syncGlobalCredentials() {
26293
27715
  try {
26294
- saveGlobalCredentials(path34.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
27716
+ saveGlobalCredentials(path35.join(os10.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
26295
27717
  } catch (err) {
26296
27718
  console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
26297
27719
  }
@@ -26306,15 +27728,15 @@ var DashboardServer = class {
26306
27728
  resolveDashboardSecret() {
26307
27729
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
26308
27730
  if (fromConfig) return fromConfig;
26309
- const secretPath = path34.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
27731
+ const secretPath = path35.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
26310
27732
  try {
26311
- if (fs31.existsSync(secretPath)) {
26312
- const existing = fs31.readFileSync(secretPath, "utf-8").trim();
27733
+ if (fs32.existsSync(secretPath)) {
27734
+ const existing = fs32.readFileSync(secretPath, "utf-8").trim();
26313
27735
  if (existing.length >= 16) return existing;
26314
27736
  }
26315
27737
  const generated = randomUUID();
26316
- fs31.mkdirSync(path34.dirname(secretPath), { recursive: true });
26317
- fs31.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
27738
+ fs32.mkdirSync(path35.dirname(secretPath), { recursive: true });
27739
+ fs32.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
26318
27740
  if (this.config.dashboard.auth) {
26319
27741
  console.warn(
26320
27742
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -26341,7 +27763,7 @@ var DashboardServer = class {
26341
27763
  // ── Setup ─────────────────────────────────────
26342
27764
  getGlobalStore() {
26343
27765
  if (!this.globalStore) {
26344
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
27766
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26345
27767
  this.globalStore = new MemoryStore(globalDbPath);
26346
27768
  }
26347
27769
  return this.globalStore;
@@ -26721,12 +28143,12 @@ ${prompt}`;
26721
28143
  }
26722
28144
  }
26723
28145
  watchRuntimeChanges() {
26724
- const workspaceDbPath = path34.join(this.workspacePath, CASCADE_DB_FILE);
26725
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28146
+ const workspaceDbPath = path35.join(this.workspacePath, CASCADE_DB_FILE);
28147
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26726
28148
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
26727
28149
  for (const watchPath of watchPaths) {
26728
- if (!fs31.existsSync(watchPath)) continue;
26729
- fs31.watchFile(watchPath, { interval: 3e3 }, () => {
28150
+ if (!fs32.existsSync(watchPath)) continue;
28151
+ fs32.watchFile(watchPath, { interval: 3e3 }, () => {
26730
28152
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
26731
28153
  });
26732
28154
  }
@@ -26848,7 +28270,7 @@ ${prompt}`;
26848
28270
  const sessionId = req.params.id;
26849
28271
  this.store.deleteSession(sessionId);
26850
28272
  this.store.deleteRuntimeSession(sessionId);
26851
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28273
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26852
28274
  const globalStore = new MemoryStore(globalDbPath);
26853
28275
  try {
26854
28276
  globalStore.deleteRuntimeSession(sessionId);
@@ -26941,7 +28363,7 @@ ${prompt}`;
26941
28363
  });
26942
28364
  this.app.delete("/api/sessions", auth2, (req, res) => {
26943
28365
  const body = req.body;
26944
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28366
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26945
28367
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
26946
28368
  const globalStore = new MemoryStore(globalDbPath);
26947
28369
  try {
@@ -26964,7 +28386,7 @@ ${prompt}`;
26964
28386
  });
26965
28387
  this.app.delete("/api/runtime", auth2, (_req, res) => {
26966
28388
  this.store.deleteAllRuntimeNodes();
26967
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28389
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
26968
28390
  const globalStore = new MemoryStore(globalDbPath);
26969
28391
  try {
26970
28392
  globalStore.deleteAllRuntimeNodes();
@@ -27033,8 +28455,7 @@ ${prompt}`;
27033
28455
  res.json(log);
27034
28456
  });
27035
28457
  this.app.get("/api/config", auth2, (_req, res) => {
27036
- const safe = { ...this.config };
27037
- safe.providers = safe.providers.map((p) => ({ ...p, apiKey: p.apiKey ? "***" : void 0 }));
28458
+ const safe = redactProviderSecrets(this.config);
27038
28459
  res.json(safe);
27039
28460
  });
27040
28461
  this.app.put("/api/config", auth2, async (req, res) => {
@@ -27050,12 +28471,12 @@ ${prompt}`;
27050
28471
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
27051
28472
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
27052
28473
  try {
27053
- const configPath = path34.join(this.workspacePath, CASCADE_CONFIG_FILE);
27054
- const existing = fs31.existsSync(configPath) ? JSON.parse(fs31.readFileSync(configPath, "utf-8")) : {};
28474
+ const configPath = path35.join(this.workspacePath, CASCADE_CONFIG_FILE);
28475
+ const existing = fs32.existsSync(configPath) ? JSON.parse(fs32.readFileSync(configPath, "utf-8")) : {};
27055
28476
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
27056
28477
  const tmp = configPath + ".tmp";
27057
- fs31.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
27058
- fs31.renameSync(tmp, configPath);
28478
+ fs32.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
28479
+ fs32.renameSync(tmp, configPath);
27059
28480
  res.json({ ok: true });
27060
28481
  } catch (err) {
27061
28482
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -27083,7 +28504,7 @@ ${prompt}`;
27083
28504
  this.app.get("/api/runtime", auth2, (req, res) => {
27084
28505
  const scope = req.query["scope"] ?? "workspace";
27085
28506
  if (scope === "global") {
27086
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
28507
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
27087
28508
  const globalStore = new MemoryStore(globalDbPath);
27088
28509
  try {
27089
28510
  res.json({
@@ -27424,13 +28845,13 @@ ${prompt}`;
27424
28845
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
27425
28846
  }
27426
28847
  });
27427
- const prodPath = path34.resolve(__dirname$1, "../web/dist");
27428
- const devPath = path34.resolve(__dirname$1, "../../web/dist");
27429
- const webDistPath = fs31.existsSync(prodPath) ? prodPath : devPath;
27430
- if (fs31.existsSync(webDistPath)) {
28848
+ const prodPath = path35.resolve(__dirname$1, "../web/dist");
28849
+ const devPath = path35.resolve(__dirname$1, "../../web/dist");
28850
+ const webDistPath = fs32.existsSync(prodPath) ? prodPath : devPath;
28851
+ if (fs32.existsSync(webDistPath)) {
27431
28852
  this.app.use(express.static(webDistPath));
27432
28853
  this.app.get("*", (_req, res) => {
27433
- res.sendFile(path34.join(webDistPath, "index.html"));
28854
+ res.sendFile(path35.join(webDistPath, "index.html"));
27434
28855
  });
27435
28856
  } else {
27436
28857
  this.app.get("/", (_req, res) => {
@@ -27451,7 +28872,7 @@ init_constants();
27451
28872
  async function dashboardCommand(config, workspacePath = process.cwd()) {
27452
28873
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
27453
28874
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
27454
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28875
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27455
28876
  const server = new DashboardServer(config, store, workspacePath);
27456
28877
  server.watchRuntimeChanges();
27457
28878
  const gThis = globalThis;
@@ -27463,10 +28884,10 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27463
28884
  process.once("exit", onExit);
27464
28885
  try {
27465
28886
  await server.start();
27466
- spin.succeed(chalk7.green(`Dashboard running at http://localhost:${port}`));
28887
+ spin.succeed(chalk8.green(`Dashboard running at http://localhost:${port}`));
27467
28888
  server.refreshRuntime("workspace");
27468
28889
  server.refreshRuntime("global");
27469
- console.log(chalk7.gray(` Press Ctrl+C to stop
28890
+ console.log(chalk8.gray(` Press Ctrl+C to stop
27470
28891
  `));
27471
28892
  await new Promise(() => {
27472
28893
  });
@@ -27475,7 +28896,7 @@ async function dashboardCommand(config, workspacePath = process.cwd()) {
27475
28896
  await server.stop().catch(() => {
27476
28897
  });
27477
28898
  onExit();
27478
- spin.fail(chalk7.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
28899
+ spin.fail(chalk8.red(`Dashboard failed: ${err instanceof Error ? err.message : String(err)}`));
27479
28900
  process.exit(1);
27480
28901
  }
27481
28902
  }
@@ -27483,18 +28904,18 @@ init_constants();
27483
28904
  function makeIdentityCommand(workspacePath = process.cwd()) {
27484
28905
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
27485
28906
  identity.command("list").description("List all available identities").action(() => {
27486
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28907
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27487
28908
  try {
27488
28909
  const identities = store.listIdentities();
27489
- console.log(chalk7.bold("\n Identities:"));
28910
+ console.log(chalk8.bold("\n Identities:"));
27490
28911
  if (identities.length === 0) {
27491
- console.log(chalk7.gray(" No identities found."));
28912
+ console.log(chalk8.gray(" No identities found."));
27492
28913
  } else {
27493
28914
  identities.forEach((id) => {
27494
- const defaultLabel = id.isDefault ? chalk7.green(" [Default]") : "";
27495
- console.log(` - ${chalk7.cyan(id.name)}${defaultLabel}`);
27496
- console.log(chalk7.gray(` ID: ${id.id}`));
27497
- if (id.description) console.log(chalk7.gray(` ${id.description}`));
28915
+ const defaultLabel = id.isDefault ? chalk8.green(" [Default]") : "";
28916
+ console.log(` - ${chalk8.cyan(id.name)}${defaultLabel}`);
28917
+ console.log(chalk8.gray(` ID: ${id.id}`));
28918
+ if (id.description) console.log(chalk8.gray(` ${id.description}`));
27498
28919
  });
27499
28920
  }
27500
28921
  console.log();
@@ -27503,7 +28924,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27503
28924
  }
27504
28925
  });
27505
28926
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
27506
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28927
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27507
28928
  try {
27508
28929
  if (options.default) {
27509
28930
  const existingDefault = store.getDefaultIdentity();
@@ -27520,21 +28941,21 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27520
28941
  isDefault: !!options.default,
27521
28942
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
27522
28943
  });
27523
- console.log(chalk7.green(`
28944
+ console.log(chalk8.green(`
27524
28945
  Successfully created identity: ${name} (${id})`));
27525
- if (options.default) console.log(chalk7.green(" Set as default."));
28946
+ if (options.default) console.log(chalk8.green(" Set as default."));
27526
28947
  console.log();
27527
28948
  } finally {
27528
28949
  store.close();
27529
28950
  }
27530
28951
  });
27531
28952
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
27532
- const store = new MemoryStore(path34.join(workspacePath, CASCADE_DB_FILE));
28953
+ const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
27533
28954
  try {
27534
28955
  const identities = store.listIdentities();
27535
28956
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
27536
28957
  if (!match) {
27537
- console.error(chalk7.red(`
28958
+ console.error(chalk8.red(`
27538
28959
  Identity '${query}' not found.
27539
28960
  `));
27540
28961
  process.exit(1);
@@ -27544,7 +28965,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27544
28965
  store.updateIdentity(existingDefault.id, { isDefault: false });
27545
28966
  }
27546
28967
  store.updateIdentity(match.id, { isDefault: true });
27547
- console.log(chalk7.green(`
28968
+ console.log(chalk8.green(`
27548
28969
  Identity ${match.name} is now the default.
27549
28970
  `));
27550
28971
  } finally {
@@ -27555,7 +28976,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
27555
28976
  }
27556
28977
  init_pricing();
27557
28978
  async function modelsCommand(options = {}) {
27558
- console.log(chalk7.magenta("\n \u25C8 Cascade Models\n"));
28979
+ console.log(chalk8.magenta("\n \u25C8 Cascade Models\n"));
27559
28980
  const cm = new ConfigManager(process.cwd());
27560
28981
  await cm.load();
27561
28982
  const config = cm.getConfig();
@@ -27563,16 +28984,16 @@ async function modelsCommand(options = {}) {
27563
28984
  try {
27564
28985
  await router.init(config);
27565
28986
  } catch (err) {
27566
- console.error(chalk7.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
28987
+ console.error(chalk8.red(` Failed to initialize router: ${err instanceof Error ? err.message : String(err)}`));
27567
28988
  process.exit(1);
27568
28989
  }
27569
28990
  await withTimeout(router.refreshLiveData(), 6e3, "live data timeout").catch(() => {
27570
28991
  });
27571
28992
  const liveData = router.getLiveData();
27572
28993
  const tiers = [
27573
- { tier: "T1", label: "T1 Administrator", color: chalk7.hex("#4C8DFF") },
27574
- { tier: "T2", label: "T2 Manager", color: chalk7.hex("#38B0DE") },
27575
- { tier: "T3", label: "T3 Worker", color: chalk7.hex("#2DD4BF") }
28994
+ { tier: "T1", label: "T1 Administrator", color: chalk8.hex("#4C8DFF") },
28995
+ { tier: "T2", label: "T2 Manager", color: chalk8.hex("#38B0DE") },
28996
+ { tier: "T3", label: "T3 Worker", color: chalk8.hex("#2DD4BF") }
27576
28997
  ];
27577
28998
  let anyMissing = false;
27578
28999
  let anyUnpriced = false;
@@ -27584,76 +29005,76 @@ async function modelsCommand(options = {}) {
27584
29005
  const costIn = unpriced ? "cost not tracked" : model.inputCostPer1kTokens === 0 ? "free" : `$${model.inputCostPer1kTokens.toFixed(4)}/1K in`;
27585
29006
  const costOut = unpriced ? "" : model.outputCostPer1kTokens === 0 ? "free" : `, $${model.outputCostPer1kTokens.toFixed(4)}/1K out`;
27586
29007
  const ctx = model.contextWindow >= 1e6 ? `${(model.contextWindow / 1e6).toFixed(1)}M ctx` : `${(model.contextWindow / 1e3).toFixed(0)}K ctx`;
27587
- const local = model.isLocal ? chalk7.gray(" [local]") : "";
27588
- const vision = model.isVisionCapable ? chalk7.gray(" \u{1F441}") : "";
29008
+ const local = model.isLocal ? chalk8.gray(" [local]") : "";
29009
+ const vision = model.isVisionCapable ? chalk8.gray(" \u{1F441}") : "";
27589
29010
  const bench = Math.round(benchmarkScore01(model, "mixed") * 100);
27590
29011
  console.log(
27591
- ` ${color.bold(tier)} ${chalk7.white(col(model.name, 24))}${chalk7.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk7.gray(col(ctx, 12))}${chalk7.gray(col(`bench ${bench}/100`, 14))}${chalk7.gray(`${costIn}${costOut}`)}` : `${chalk7.gray(col(ctx, 10))}${chalk7.gray(`bench ${bench}/100`)}`) + local + vision
29012
+ ` ${color.bold(tier)} ${chalk8.white(col(model.name, 24))}${chalk8.gray(col(model.provider, 16))}` + (options.verbose ? `${chalk8.gray(col(ctx, 12))}${chalk8.gray(col(`bench ${bench}/100`, 14))}${chalk8.gray(`${costIn}${costOut}`)}` : `${chalk8.gray(col(ctx, 10))}${chalk8.gray(`bench ${bench}/100`)}`) + local + vision
27592
29013
  );
27593
29014
  } else {
27594
- console.log(` ${color.bold(tier)} ${chalk7.red("No model available")} ${chalk7.gray(`(check provider config for ${label})`)}`);
29015
+ console.log(` ${color.bold(tier)} ${chalk8.red("No model available")} ${chalk8.gray(`(check provider config for ${label})`)}`);
27595
29016
  anyMissing = true;
27596
29017
  }
27597
29018
  }
27598
29019
  console.log();
27599
29020
  const providers = config.providers.map((p) => p.type).join(", ") || "(none)";
27600
- console.log(chalk7.gray(` Configured providers: ${providers}`));
29021
+ console.log(chalk8.gray(` Configured providers: ${providers}`));
27601
29022
  if (liveData) {
27602
29023
  const src = liveData.getDataSource();
27603
29024
  const gen = liveData.getGeneratedAt();
27604
29025
  const srcLabel = src === "live" ? "live (just fetched)" : src === "cache" ? "cached" : "bundled";
27605
29026
  const pricingMeta = pricingDatasetMeta();
27606
- console.log(chalk7.gray(
29027
+ console.log(chalk8.gray(
27607
29028
  ` Benchmark data: ${srcLabel}` + (gen ? ` \xB7 updated ${gen.slice(0, 10)}` : "") + ` \xB7 pricing: ${liveData.hasLivePricing() ? "live (OpenRouter)" : "dataset"} (baseline ${pricingMeta.generatedAt})`
27608
29029
  ));
27609
29030
  const drift = liveData.getPriceDisagreements();
27610
29031
  if (drift.length) {
27611
- console.log(chalk7.yellow(`
29032
+ console.log(chalk8.yellow(`
27612
29033
  ${drift.length} model${drift.length === 1 ? "" : "s"} priced differently by the live source than by the bundled dataset:`));
27613
29034
  for (const d of drift.slice(0, 10)) {
27614
- console.log(chalk7.gray(
29035
+ console.log(chalk8.gray(
27615
29036
  ` ${d.provider}:${d.modelId} dataset $${d.dataset.input.toFixed(5)}/$${d.dataset.output.toFixed(5)} per 1K (as of ${d.datasetAsOf}) \u2192 live $${d.live.input.toFixed(5)}/$${d.live.output.toFixed(5)} (${d.ratio}\xD7)`
27616
29037
  ));
27617
29038
  }
27618
- console.log(chalk7.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
29039
+ console.log(chalk8.gray(" Live pricing is used. Refresh src/core/router/pricing-data.json to clear this."));
27619
29040
  }
27620
29041
  }
27621
29042
  if (anyUnpriced) {
27622
- console.log(chalk7.yellow(
29043
+ console.log(chalk8.yellow(
27623
29044
  '\n Some tiers use a model with no published price \u2014 their spend is reported as "cost not tracked", never $0.00,\n and is not counted toward a cost budget. Add the model to the pricing dataset, or set `local: true` on the\n provider if the endpoint genuinely costs nothing to run.'
27624
29045
  ));
27625
29046
  }
27626
29047
  if (options.verbose) {
27627
29048
  console.log();
27628
- console.log(chalk7.white(" Available models by provider:\n"));
29049
+ console.log(chalk8.white(" Available models by provider:\n"));
27629
29050
  const allProviderTypes = [...new Set(config.providers.map((p) => p.type))];
27630
29051
  for (const providerType of allProviderTypes) {
27631
29052
  const available = router.getModelsForProvider(providerType);
27632
29053
  if (available.length === 0) continue;
27633
- console.log(chalk7.gray(` ${providerType}:`));
29054
+ console.log(chalk8.gray(` ${providerType}:`));
27634
29055
  for (const m of available) {
27635
29056
  const override = config.models.t1 === m.id ? " \u2190 T1" : config.models.t2 === m.id ? " \u2190 T2" : config.models.t3 === m.id ? " \u2190 T3" : "";
27636
- console.log(` ${chalk7.white(col(m.name, 28))}${chalk7.gray(m.id)}${chalk7.yellow(override)}`);
29057
+ console.log(` ${chalk8.white(col(m.name, 28))}${chalk8.gray(m.id)}${chalk8.yellow(override)}`);
27637
29058
  }
27638
29059
  console.log();
27639
29060
  }
27640
29061
  }
27641
29062
  if (anyMissing) {
27642
- console.log(chalk7.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
29063
+ console.log(chalk8.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
27643
29064
  } else {
27644
- console.log(chalk7.green(" All tiers are configured.\n"));
29065
+ console.log(chalk8.green(" All tiers are configured.\n"));
27645
29066
  }
27646
29067
  }
27647
29068
  async function setModelCommand(action, tierArg, value) {
27648
29069
  const tier = (tierArg ?? "").toLowerCase();
27649
29070
  if (!["t1", "t2", "t3"].includes(tier)) {
27650
- console.error(chalk7.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
27651
- console.log(chalk7.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
29071
+ console.error(chalk8.red(` Invalid tier "${tierArg ?? ""}". Use t1, t2, or t3.`));
29072
+ console.log(chalk8.gray(" e.g. cascade models set t1 anthropic:claude-opus-4-8"));
27652
29073
  process.exit(1);
27653
29074
  }
27654
29075
  const override = action === "unset" ? "auto" : (value ?? "").trim();
27655
29076
  if (action === "set" && !override) {
27656
- console.error(chalk7.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
29077
+ console.error(chalk8.red(' Missing value. e.g. cascade models set t3 openai:gpt-4o-mini (or "auto")'));
27657
29078
  process.exit(1);
27658
29079
  }
27659
29080
  const cm = new ConfigManager(process.cwd());
@@ -27663,8 +29084,8 @@ async function setModelCommand(action, tierArg, value) {
27663
29084
  if (override === "auto") delete models[tier];
27664
29085
  else models[tier] = override;
27665
29086
  await cm.updateConfig({ models });
27666
- const label = override === "auto" ? chalk7.gray("auto (routing decides)") : chalk7.white(override);
27667
- console.log(chalk7.green(`
29087
+ const label = override === "auto" ? chalk8.gray("auto (routing decides)") : chalk8.white(override);
29088
+ console.log(chalk8.green(`
27668
29089
  \u2713 ${tier.toUpperCase()} model set to ${label}
27669
29090
  `));
27670
29091
  }
@@ -27678,8 +29099,8 @@ async function exportCommand(options = {}) {
27678
29099
  let store;
27679
29100
  try {
27680
29101
  const workspacePath = options.workspacePath ?? process.cwd();
27681
- const workspaceDbPath = path34.join(workspacePath, CASCADE_DB_FILE);
27682
- const globalDbPath = path34.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
29102
+ const workspaceDbPath = path35.join(workspacePath, CASCADE_DB_FILE);
29103
+ const globalDbPath = path35.join(os10.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
27683
29104
  let dbPath = globalDbPath;
27684
29105
  try {
27685
29106
  await fs11.access(workspaceDbPath);
@@ -27688,7 +29109,7 @@ async function exportCommand(options = {}) {
27688
29109
  }
27689
29110
  store = new MemoryStore(dbPath);
27690
29111
  } catch (err) {
27691
- spin.fail(chalk7.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
29112
+ spin.fail(chalk8.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
27692
29113
  process.exit(1);
27693
29114
  }
27694
29115
  try {
@@ -27696,7 +29117,7 @@ async function exportCommand(options = {}) {
27696
29117
  if (options.sessionId) {
27697
29118
  const session2 = store.getSession(options.sessionId);
27698
29119
  if (!session2) {
27699
- spin.fail(chalk7.red(`Session "${options.sessionId}" not found.`));
29120
+ spin.fail(chalk8.red(`Session "${options.sessionId}" not found.`));
27700
29121
  process.exit(1);
27701
29122
  }
27702
29123
  sessions2 = [session2];
@@ -27704,14 +29125,14 @@ async function exportCommand(options = {}) {
27704
29125
  const limit = options.last ?? 10;
27705
29126
  sessions2 = store.listSessions(void 0, limit);
27706
29127
  if (sessions2.length === 0) {
27707
- spin.warn(chalk7.yellow("No sessions found."));
29128
+ spin.warn(chalk8.yellow("No sessions found."));
27708
29129
  return;
27709
29130
  }
27710
29131
  const latest = sessions2[0];
27711
29132
  const full = store.getSession(latest.id);
27712
29133
  sessions2 = full ? [full] : [];
27713
29134
  if (sessions2.length === 0) {
27714
- spin.fail(chalk7.red("Could not load latest session."));
29135
+ spin.fail(chalk8.red("Could not load latest session."));
27715
29136
  process.exit(1);
27716
29137
  }
27717
29138
  }
@@ -27721,17 +29142,17 @@ async function exportCommand(options = {}) {
27721
29142
  const ext = format === "json" ? ".json" : ".md";
27722
29143
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
27723
29144
  const defaultFile = `cascade-export-${safeName}${ext}`;
27724
- const outPath = options.output ? path34.resolve(options.output) : path34.join(process.cwd(), defaultFile);
29145
+ const outPath = options.output ? path35.resolve(options.output) : path35.join(process.cwd(), defaultFile);
27725
29146
  await fs11.writeFile(outPath, content, "utf-8");
27726
- spin.succeed(chalk7.green(`Exported to ${chalk7.white(outPath)}`));
29147
+ spin.succeed(chalk8.green(`Exported to ${chalk8.white(outPath)}`));
27727
29148
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
27728
29149
  console.log();
27729
- console.log(chalk7.gray(` Session: ${session.title}`));
27730
- console.log(chalk7.gray(` Messages: ${messageCount}`));
27731
- console.log(chalk7.gray(` Format: ${format}`));
29150
+ console.log(chalk8.gray(` Session: ${session.title}`));
29151
+ console.log(chalk8.gray(` Messages: ${messageCount}`));
29152
+ console.log(chalk8.gray(` Format: ${format}`));
27732
29153
  console.log();
27733
29154
  } catch (err) {
27734
- spin.fail(chalk7.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
29155
+ spin.fail(chalk8.red(`Export failed: ${err instanceof Error ? err.message : String(err)}`));
27735
29156
  process.exit(1);
27736
29157
  }
27737
29158
  }
@@ -27785,93 +29206,6 @@ function buildJsonExport(session) {
27785
29206
  };
27786
29207
  return JSON.stringify(payload, null, 2);
27787
29208
  }
27788
- async function linkCommand(target, options = {}) {
27789
- const found = await discoverCredentials();
27790
- if (found.length === 0) {
27791
- console.log(chalk7.yellow("\n No reusable credentials found.\n"));
27792
- console.log(chalk7.gray(" Cascade looks for Claude Code, Codex, Gemini CLI, and GitHub Copilot logins,"));
27793
- console.log(chalk7.gray(" plus ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY in your environment.\n"));
27794
- return;
27795
- }
27796
- if (!target) {
27797
- printDiscovered(found);
27798
- return;
27799
- }
27800
- const provider = normalizeProvider(target);
27801
- if (!provider) {
27802
- console.log(chalk7.red(`
27803
- Unknown provider "${target}". Use one of: anthropic, openai, gemini.
27804
- `));
27805
- return;
27806
- }
27807
- const candidates2 = found.filter((c) => c.provider === provider);
27808
- const chosen = candidates2.find((c) => c.directlyUsable) ?? candidates2[0];
27809
- if (!chosen) {
27810
- console.log(chalk7.yellow(`
27811
- No detected credential maps to "${provider}".
27812
- `));
27813
- return;
27814
- }
27815
- if (!chosen.directlyUsable) {
27816
- console.log(chalk7.yellow(`
27817
- Found a ${chosen.sourceTool} credential, but it can't be used against the standard ${provider} API.`));
27818
- if (chosen.warning) console.log(chalk7.gray(` ${chosen.warning}`));
27819
- console.log(chalk7.gray(" Cascade won't adopt it because it would create a non-working provider.\n"));
27820
- return;
27821
- }
27822
- if (chosen.kind === "oauth" && !options.acceptRisk) {
27823
- console.log(chalk7.yellow(`
27824
- ${chosen.sourceTool} provides a subscription OAuth token, not an API key.`));
27825
- if (chosen.warning) console.log(chalk7.gray(` ${chosen.warning}`));
27826
- console.log(chalk7.gray(" Re-run with --accept-risk to adopt it anyway:\n"));
27827
- console.log(chalk7.cyan(` cascade link ${provider} --accept-risk
27828
- `));
27829
- return;
27830
- }
27831
- await adoptCredential(chosen, options.workspace ?? process.cwd());
27832
- console.log(chalk7.green(`
27833
- \u2713 Linked ${provider} using your ${chosen.sourceTool} credential (${maskSecret(chosen.secret)}).`));
27834
- if (chosen.kind === "oauth") {
27835
- console.log(chalk7.gray(" Adopted as an OAuth bearer token \u2014 revoke it in the source tool to disable."));
27836
- }
27837
- console.log(chalk7.gray(" Run `cascade doctor` to verify, or `cascade` to start.\n"));
27838
- }
27839
- function printDiscovered(found) {
27840
- console.log(chalk7.magenta("\n \u25C8 Detected credentials\n"));
27841
- for (const c of found) {
27842
- const usable = c.directlyUsable ? chalk7.green("usable") : chalk7.yellow("needs vendor backend");
27843
- const kind = c.kind === "oauth" ? chalk7.yellow("oauth") : chalk7.gray("api-key");
27844
- console.log(` ${chalk7.white(c.provider.padEnd(18))} ${chalk7.gray(maskSecret(c.secret).padEnd(12))} ${kind} ${usable}`);
27845
- console.log(chalk7.gray(` from ${c.sourceTool}`));
27846
- if (c.warning) console.log(chalk7.yellow(` \u26A0 ${c.warning}`));
27847
- }
27848
- console.log(chalk7.gray("\n Adopt one with: ") + chalk7.cyan("cascade link <provider> [--accept-risk]"));
27849
- console.log(chalk7.gray(" --accept-risk is required for subscription OAuth tokens.\n"));
27850
- }
27851
- function normalizeProvider(target) {
27852
- const t = target.toLowerCase();
27853
- if (t === "anthropic" || t === "claude" || t === "claude-code") return "anthropic";
27854
- if (t === "openai" || t === "codex" || t === "gpt") return "openai";
27855
- if (t === "gemini" || t === "google") return "gemini";
27856
- return null;
27857
- }
27858
- async function adoptCredential(cred, workspace) {
27859
- const cm = new ConfigManager(workspace);
27860
- await cm.load();
27861
- const config = cm.getConfig();
27862
- const next = {
27863
- type: cred.provider,
27864
- credentialSource: cred.sourceTool
27865
- };
27866
- if (cred.kind === "oauth" && cred.provider === "anthropic") {
27867
- next.authToken = cred.secret;
27868
- } else {
27869
- next.apiKey = cred.secret;
27870
- }
27871
- const providers = config.providers.filter((p) => p.type !== cred.provider);
27872
- providers.push(next);
27873
- await cm.updateConfig({ providers });
27874
- }
27875
29209
  async function telemetryCommand(action) {
27876
29210
  const cm = new ConfigManager(process.cwd());
27877
29211
  await cm.load();
@@ -27879,12 +29213,12 @@ async function telemetryCommand(action) {
27879
29213
  if (action === "status") {
27880
29214
  const state = config.telemetry?.enabled ? "ON" : "OFF";
27881
29215
  console.log();
27882
- console.log(chalk7.magenta(" \u25C8 Cascade Telemetry"));
29216
+ console.log(chalk8.magenta(" \u25C8 Cascade Telemetry"));
27883
29217
  console.log();
27884
- console.log(` Status: ${config.telemetry?.enabled ? chalk7.green(state) : chalk7.gray(state)}`);
27885
- console.log(chalk7.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
29218
+ console.log(` Status: ${config.telemetry?.enabled ? chalk8.green(state) : chalk8.gray(state)}`);
29219
+ console.log(chalk8.gray(" Scope: anonymous session metadata only (no prompts/outputs)"));
27886
29220
  console.log();
27887
- console.log(chalk7.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
29221
+ console.log(chalk8.gray(" Toggle with: cascade telemetry on | cascade telemetry off"));
27888
29222
  console.log();
27889
29223
  return;
27890
29224
  }
@@ -27898,11 +29232,11 @@ async function telemetryCommand(action) {
27898
29232
  });
27899
29233
  console.log();
27900
29234
  if (enabled) {
27901
- console.log(chalk7.green(` \u2713 Telemetry enabled.`));
27902
- console.log(chalk7.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
29235
+ console.log(chalk8.green(` \u2713 Telemetry enabled.`));
29236
+ console.log(chalk8.gray(" Anonymous session metadata (no prompts, no outputs) will be sent."));
27903
29237
  } else {
27904
- console.log(chalk7.yellow(` \u2713 Telemetry disabled.`));
27905
- console.log(chalk7.gray(" No events will be transmitted from this workspace."));
29238
+ console.log(chalk8.yellow(` \u2713 Telemetry disabled.`));
29239
+ console.log(chalk8.gray(" No events will be transmitted from this workspace."));
27906
29240
  }
27907
29241
  console.log();
27908
29242
  }
@@ -27912,11 +29246,11 @@ async function statsCommand() {
27912
29246
  await tracker.load();
27913
29247
  const all = tracker.getAll();
27914
29248
  if (all.size === 0) {
27915
- console.log(chalk7.dim("\n No routing history yet \u2014 run some tasks first.\n"));
29249
+ console.log(chalk8.dim("\n No routing history yet \u2014 run some tasks first.\n"));
27916
29250
  return;
27917
29251
  }
27918
- console.log(chalk7.magenta("\n \u25C8 Auto-Routing History\n"));
27919
- console.log(chalk7.dim(" Per-task-type model performance learned from past runs.\n"));
29252
+ console.log(chalk8.magenta("\n \u25C8 Auto-Routing History\n"));
29253
+ console.log(chalk8.dim(" Per-task-type model performance learned from past runs.\n"));
27920
29254
  for (const taskType of TASK_TYPES) {
27921
29255
  const entries = [];
27922
29256
  for (const [key, stat] of all) {
@@ -27928,36 +29262,36 @@ async function statsCommand() {
27928
29262
  }
27929
29263
  if (entries.length === 0) continue;
27930
29264
  entries.sort((a, b) => b.successRate - a.successRate || b.samples - a.samples);
27931
- console.log(chalk7.bold(` ${taskType.toUpperCase()}`));
29265
+ console.log(chalk8.bold(` ${taskType.toUpperCase()}`));
27932
29266
  const header = ` ${"Model".padEnd(36)} ${"Success".padEnd(9)} ${"Samples".padEnd(9)} Avg cost`;
27933
- console.log(chalk7.dim(header));
27934
- console.log(chalk7.dim(" " + "\u2500".repeat(62)));
29267
+ console.log(chalk8.dim(header));
29268
+ console.log(chalk8.dim(" " + "\u2500".repeat(62)));
27935
29269
  for (const e of entries) {
27936
29270
  const pct = `${Math.round(e.successRate * 100)}%`;
27937
29271
  const cost = e.avgCostUsd < 1e-4 ? "<$0.0001" : `$${e.avgCostUsd.toFixed(4)}`;
27938
- const color = e.successRate >= 0.8 ? chalk7.green : e.successRate >= 0.5 ? chalk7.yellow : chalk7.red;
29272
+ const color = e.successRate >= 0.8 ? chalk8.green : e.successRate >= 0.5 ? chalk8.yellow : chalk8.red;
27939
29273
  console.log(
27940
- ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk7.dim(cost)}`
29274
+ ` ${e.modelId.padEnd(36)} ${color(pct.padEnd(9))} ${String(e.samples).padEnd(9)} ${chalk8.dim(cost)}`
27941
29275
  );
27942
29276
  }
27943
29277
  console.log();
27944
29278
  }
27945
- console.log(chalk7.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
29279
+ console.log(chalk8.dim(" tip: use /rate good | bad after a task to improve these scores.\n"));
27946
29280
  }
27947
29281
 
27948
29282
  // src/cli/index.ts
27949
29283
  dotenv.config();
27950
29284
  function warnIfBuildIsStale() {
27951
29285
  try {
27952
- const here = path34.dirname(fileURLToPath(import.meta.url));
29286
+ const here = path35.dirname(fileURLToPath(import.meta.url));
27953
29287
  for (const rel of ["..", "../.."]) {
27954
- const pkgPath = path34.join(here, rel, "package.json");
27955
- if (!fs31.existsSync(pkgPath)) continue;
27956
- const pkg = JSON.parse(fs31.readFileSync(pkgPath, "utf-8"));
29288
+ const pkgPath = path35.join(here, rel, "package.json");
29289
+ if (!fs32.existsSync(pkgPath)) continue;
29290
+ const pkg = JSON.parse(fs32.readFileSync(pkgPath, "utf-8"));
27957
29291
  if (pkg.name !== "cascade-ai") continue;
27958
29292
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
27959
29293
  console.error(
27960
- chalk7.yellow(
29294
+ chalk8.yellow(
27961
29295
  `\u26A0 Stale build: compiled output is v${CASCADE_VERSION} but the source tree is v${pkg.version}.
27962
29296
  Run: npm install && npm run build`
27963
29297
  )
@@ -28056,7 +29390,7 @@ mcp.command("list").description("List configured MCP servers").action(async () =
28056
29390
  mcp.command("remove <name>").description("Remove an MCP server (and its stored OAuth tokens)").action(async (name) => {
28057
29391
  await mcpRemoveCommand(name);
28058
29392
  });
28059
- program.command("link [provider]").description("Reuse credentials from other AI CLIs (Claude Code, Codex, Gemini CLI, Copilot)").option("--accept-risk", "Adopt a subscription OAuth token despite the ToS warning").action(async (provider, opts) => {
29393
+ program.command("link [provider]").description("Reuse credentials from other AI CLIs (Claude Code, Codex, Gemini CLI, Copilot)").option("--accept-risk", "Deprecated \u2014 subscription tokens can no longer be adopted").action(async (provider, opts) => {
28060
29394
  await linkCommand(provider, { acceptRisk: opts.acceptRisk, workspace: process.cwd() });
28061
29395
  });
28062
29396
  program.command("update").description("Update Cascade to the latest version").action(async () => {
@@ -28082,7 +29416,7 @@ program.command("models [action] [tier] [value]").description("List AI models pe
28082
29416
  program.command("telemetry [action]").description("Toggle anonymous usage telemetry (on | off | status). Default: status").action(async (action) => {
28083
29417
  const normalized = (action ?? "status").toLowerCase();
28084
29418
  if (normalized !== "on" && normalized !== "off" && normalized !== "status") {
28085
- console.error(chalk7.red(`Unknown action: ${action}. Use: on | off | status`));
29419
+ console.error(chalk8.red(`Unknown action: ${action}. Use: on | off | status`));
28086
29420
  process.exit(1);
28087
29421
  }
28088
29422
  await telemetryCommand(normalized);
@@ -28104,15 +29438,15 @@ async function startRepl(options) {
28104
29438
  try {
28105
29439
  await cm.load();
28106
29440
  } catch (err) {
28107
- console.error(chalk7.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28108
- console.error(chalk7.gray("Run `cascade init` to set up this directory."));
29441
+ console.error(chalk8.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29442
+ console.error(chalk8.gray("Run `cascade init` to set up this directory."));
28109
29443
  process.exit(1);
28110
29444
  }
28111
29445
  let config = cm.getConfig();
28112
29446
  const startupNotice = cm.takeRetiredNotice();
28113
29447
  const needsSetup = !hasUsableProvider(config.providers);
28114
29448
  if (needsSetup) {
28115
- console.log(chalk7.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
29449
+ console.log(chalk8.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
28116
29450
  console.log();
28117
29451
  config = await runSetupWizard(workspacePath);
28118
29452
  await cm.updateConfig(config);
@@ -28147,24 +29481,24 @@ async function runHeadless(prompt, options) {
28147
29481
  try {
28148
29482
  await cm.load();
28149
29483
  } catch (err) {
28150
- console.error(chalk7.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
28151
- console.error(chalk7.gray("Run `cascade init` to set up this directory."));
29484
+ console.error(chalk8.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
29485
+ console.error(chalk8.gray("Run `cascade init` to set up this directory."));
28152
29486
  process.exit(1);
28153
29487
  }
28154
29488
  const config = cm.getConfig();
28155
29489
  const needsSetup = !hasUsableProvider(config.providers);
28156
29490
  if (needsSetup) {
28157
- console.error(chalk7.red("No providers configured. Run `cascade init` first."));
29491
+ console.error(chalk8.red("No providers configured. Run `cascade init` first."));
28158
29492
  process.exit(1);
28159
29493
  }
28160
29494
  const cascade = new Cascade(config, workspacePath);
28161
29495
  try {
28162
29496
  await cascade.init();
28163
29497
  } catch (err) {
28164
- console.error(chalk7.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
29498
+ console.error(chalk8.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
28165
29499
  process.exit(1);
28166
29500
  }
28167
- console.error(chalk7.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
29501
+ console.error(chalk8.gray(" \u25C8 Running headlessly \u2014 tool approvals are auto-granted."));
28168
29502
  let lastProgress = "";
28169
29503
  cascade.on("tier:status", (ev) => {
28170
29504
  const action = ev?.currentAction?.trim();
@@ -28172,7 +29506,7 @@ async function runHeadless(prompt, options) {
28172
29506
  const line = ` \xB7 ${ev.role ?? ""} ${action}`.trimEnd();
28173
29507
  if (line === lastProgress) return;
28174
29508
  lastProgress = line;
28175
- console.error(chalk7.gray(line));
29509
+ console.error(chalk8.gray(line));
28176
29510
  });
28177
29511
  try {
28178
29512
  const result = await cascade.run({
@@ -28183,7 +29517,7 @@ async function runHeadless(prompt, options) {
28183
29517
  });
28184
29518
  process.stdout.write(result.output.trimEnd() + "\n");
28185
29519
  } catch (err) {
28186
- console.error(chalk7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
29520
+ console.error(chalk8.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
28187
29521
  await cascade.close().catch(() => {
28188
29522
  });
28189
29523
  process.exit(1);
@@ -28195,9 +29529,9 @@ async function runHeadless(prompt, options) {
28195
29529
  function printBanner() {
28196
29530
  if (process.stdout.columns < 60) return;
28197
29531
  console.log();
28198
- console.log(" " + chalk7.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
28199
- console.log(" " + chalk7.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7.white.bold("CASCADE") + chalk7.gray(" v" + CASCADE_VERSION));
28200
- console.log(" " + chalk7.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk7.hex("#8A93A8")("multi-tier orchestration"));
29532
+ console.log(" " + chalk8.hex("#4C8DFF").bold("\u2588\u2588\u2588\u2588\u2588\u2588\u2588"));
29533
+ console.log(" " + chalk8.hex("#38B0DE").bold(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8.white.bold("CASCADE") + chalk8.gray(" v" + CASCADE_VERSION));
29534
+ console.log(" " + chalk8.hex("#2DD4BF").bold(" \u2588\u2588\u2588\u2588\u2588\u2588") + " " + chalk8.hex("#8A93A8")("multi-tier orchestration"));
28201
29535
  console.log();
28202
29536
  }
28203
29537
  program.parse(process.argv);