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/README.md +6 -3
- package/dist/cli.cjs +2030 -696
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +2008 -674
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +50734 -49654
- package/dist/index.cjs +16114 -15034
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +554 -34
- package/dist/index.d.ts +554 -34
- package/dist/index.js +16115 -15053
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
11
|
-
import
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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({
|
|
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
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
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
|
|
3504
|
+
return staticCatalog();
|
|
3171
3505
|
}
|
|
3172
3506
|
const data = await resp.json();
|
|
3173
3507
|
if (!Array.isArray(data?.models)) {
|
|
3174
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
3773
|
-
if (!
|
|
3774
|
-
|
|
4089
|
+
const cascadeDir = path35.join(workspacePath, ".cascade");
|
|
4090
|
+
if (!fs32.existsSync(cascadeDir)) {
|
|
4091
|
+
fs32.mkdirSync(cascadeDir, { recursive: true });
|
|
3775
4092
|
}
|
|
3776
|
-
this.keyPath =
|
|
3777
|
-
this.dbPath =
|
|
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 (
|
|
3808
|
-
this.encryptionKey =
|
|
4124
|
+
if (fs32.existsSync(this.keyPath)) {
|
|
4125
|
+
this.encryptionKey = fs32.readFileSync(this.keyPath);
|
|
3809
4126
|
} else {
|
|
3810
4127
|
this.encryptionKey = crypto.randomBytes(32);
|
|
3811
|
-
|
|
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 =
|
|
4206
|
+
const dumpPath = path35.join(os10.tmpdir(), "cascade_audit_logs_debug.json");
|
|
3890
4207
|
const entries = this.getAllLogs();
|
|
3891
|
-
|
|
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 &&
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
4060
|
-
|
|
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
|
-
|
|
4071
|
-
|
|
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 =
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
5488
|
-
|
|
5804
|
+
|
|
5805
|
+
// src/config/azure-endpoint.ts
|
|
5806
|
+
init_net();
|
|
5807
|
+
function normalizeAzureEndpoint(url) {
|
|
5808
|
+
return normalizeEndpoint(url);
|
|
5489
5809
|
}
|
|
5490
|
-
function
|
|
5491
|
-
|
|
5492
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
5517
|
-
|
|
5892
|
+
fs32.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
5893
|
+
fs32.renameSync(tmp, filePath);
|
|
5518
5894
|
try {
|
|
5519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) ||
|
|
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 ??
|
|
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(
|
|
5836
|
-
this.store = new MemoryStore(
|
|
5837
|
-
const
|
|
5838
|
-
|
|
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
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
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
|
-
|
|
5861
|
-
|
|
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
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
5926
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
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 ?
|
|
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 ?
|
|
6984
|
+
out += i === cursorOffset ? chalk8.inverse(displayValue[i]) : displayValue[i];
|
|
6316
6985
|
}
|
|
6317
|
-
if (cursorOffset >= displayValue.length) out +=
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
7777
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
10350
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
13910
|
-
const abs =
|
|
13911
|
-
const rel =
|
|
13912
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
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 =
|
|
13917
|
-
const realRel =
|
|
13918
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
15325
|
-
if (!
|
|
15326
|
-
|
|
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 =
|
|
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 =
|
|
15410
|
-
if (!
|
|
15411
|
-
|
|
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 =
|
|
15416
|
-
|
|
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 (
|
|
15424
|
-
|
|
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"] ?
|
|
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(
|
|
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"] ?
|
|
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 = [
|
|
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 =
|
|
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 =
|
|
16257
|
-
const rel =
|
|
16258
|
-
if (!rel || rel.startsWith("..") ||
|
|
16259
|
-
const posixRel = rel.split(
|
|
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(
|
|
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
|
-
|
|
16500
|
-
|
|
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
|
-
|
|
17216
|
+
fs32.chmodSync(this.filePath, 384);
|
|
16503
17217
|
} catch {
|
|
16504
17218
|
}
|
|
16505
17219
|
}
|
|
16506
17220
|
clear() {
|
|
16507
17221
|
try {
|
|
16508
|
-
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
17769
|
-
const 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 =
|
|
17790
|
-
if (!
|
|
17791
|
-
|
|
18503
|
+
const cascadeDir = path35.join(workspacePath, ".cascade");
|
|
18504
|
+
if (!fs32.existsSync(cascadeDir)) {
|
|
18505
|
+
fs32.mkdirSync(cascadeDir, { recursive: true });
|
|
17792
18506
|
}
|
|
17793
|
-
this.keyPath =
|
|
17794
|
-
this.dbPath =
|
|
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 (
|
|
17852
|
-
this.encryptionKey =
|
|
18565
|
+
if (fs32.existsSync(this.keyPath)) {
|
|
18566
|
+
this.encryptionKey = fs32.readFileSync(this.keyPath);
|
|
17853
18567
|
} else {
|
|
17854
18568
|
this.encryptionKey = crypto.randomBytes(32);
|
|
17855
|
-
|
|
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 =
|
|
18855
|
+
const dumpPath = path35.join(os10.tmpdir(), "cascade_world_state_debug.json");
|
|
18142
18856
|
const entries = this.getAllEntries();
|
|
18143
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
18273
|
-
|
|
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(
|
|
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
|
-
|
|
18311
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
19470
|
+
const ext = path35.extname(entry.name).slice(1).toLowerCase();
|
|
18757
19471
|
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
18758
|
-
out.push(
|
|
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 =
|
|
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(
|
|
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:
|
|
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 ||
|
|
19373
|
-
|
|
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
|
|
21535
|
+
return path35.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
20822
21536
|
}
|
|
20823
21537
|
function cloudSessionPath(dir = globalDir()) {
|
|
20824
|
-
return
|
|
21538
|
+
return path35.join(dir, CLOUD_SESSION_FILE);
|
|
20825
21539
|
}
|
|
20826
21540
|
function loadCloudSession(dir = globalDir()) {
|
|
20827
21541
|
try {
|
|
20828
|
-
const raw =
|
|
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
|
-
|
|
21551
|
+
fs32.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
20838
21552
|
const file = cloudSessionPath(dir);
|
|
20839
|
-
|
|
21553
|
+
fs32.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
20840
21554
|
try {
|
|
20841
|
-
|
|
21555
|
+
fs32.chmodSync(file, 384);
|
|
20842
21556
|
} catch {
|
|
20843
21557
|
}
|
|
20844
21558
|
}
|
|
20845
21559
|
function clearCloudSession(dir = globalDir()) {
|
|
20846
21560
|
try {
|
|
20847
|
-
|
|
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(
|
|
21020
|
-
const call = (token) => fetch(this.url(
|
|
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(
|
|
21028
|
-
return this.authed(
|
|
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 =
|
|
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) =>
|
|
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 =
|
|
23516
|
+
const configPath = path35.join(workspacePath, ".cascade", "config.json");
|
|
22803
23517
|
try {
|
|
22804
|
-
await fs11.mkdir(
|
|
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(
|
|
23570
|
+
const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
|
|
22857
23571
|
storeRef.current = store;
|
|
22858
|
-
globalStoreRef.current = new MemoryStore(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
25045
|
+
const configDir = path35.join(workspacePath, ".cascade");
|
|
24332
25046
|
await fs11.mkdir(configDir, { recursive: true });
|
|
24333
|
-
const configPath =
|
|
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 =
|
|
25388
|
+
const configDir = path35.join(workspacePath, ".cascade");
|
|
24675
25389
|
await fs11.mkdir(configDir, { recursive: true });
|
|
24676
|
-
const mdPath =
|
|
25390
|
+
const mdPath = path35.join(workspacePath, "CASCADE.md");
|
|
24677
25391
|
if (!await fileExists(mdPath)) {
|
|
24678
25392
|
await createDefaultCascadeMd(workspacePath);
|
|
24679
|
-
spin.succeed(
|
|
25393
|
+
spin.succeed(chalk8.green("Created CASCADE.md"));
|
|
24680
25394
|
}
|
|
24681
|
-
const ignorePath =
|
|
25395
|
+
const ignorePath = path35.join(workspacePath, ".cascadeignore");
|
|
24682
25396
|
if (!await fileExists(ignorePath)) {
|
|
24683
25397
|
await createDefaultIgnoreFile(workspacePath);
|
|
24684
|
-
spin.succeed(
|
|
25398
|
+
spin.succeed(chalk8.green("Created .cascadeignore"));
|
|
24685
25399
|
}
|
|
24686
25400
|
spin.stop();
|
|
24687
25401
|
console.log();
|
|
24688
|
-
console.log(
|
|
25402
|
+
console.log(chalk8.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
24689
25403
|
console.log();
|
|
24690
|
-
const configPath =
|
|
25404
|
+
const configPath = path35.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
24691
25405
|
if (await fileExists(configPath)) {
|
|
24692
|
-
console.log(
|
|
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(
|
|
25414
|
+
console.log(chalk8.green(" \u25C8 Setup complete! Run `cascade` to start."));
|
|
24701
25415
|
console.log();
|
|
24702
25416
|
} catch (err) {
|
|
24703
|
-
console.error(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
25567
|
+
found.push({
|
|
24755
25568
|
provider: "anthropic",
|
|
24756
25569
|
sourceTool: "Claude Code",
|
|
24757
25570
|
kind: "oauth",
|
|
24758
25571
|
secret: oauthToken,
|
|
24759
|
-
directlyUsable:
|
|
24760
|
-
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 ${
|
|
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}
|
|
24876
|
-
ok: Boolean(key),
|
|
24877
|
-
detail: key ? "
|
|
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:
|
|
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 ?
|
|
24942
|
-
const label = c.ok ?
|
|
24943
|
-
const detail = 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(
|
|
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("
|
|
26007
|
+
const critical = failures2.filter((c) => c.label.includes("Node") || c.label.includes("credential"));
|
|
24952
26008
|
if (critical.length) {
|
|
24953
|
-
console.log(
|
|
26009
|
+
console.log(chalk8.yellow(` ${critical.length} issue(s) need attention.
|
|
24954
26010
|
`));
|
|
24955
26011
|
} else {
|
|
24956
|
-
console.log(
|
|
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 =
|
|
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(
|
|
24981
|
-
console.log(
|
|
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 ||
|
|
24986
|
-
|
|
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(
|
|
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(
|
|
26062
|
+
if (seen % 10 === 0) process.stdout.write(chalk8.dim(` \u2026embedded ${seen} changed files\r`));
|
|
25007
26063
|
});
|
|
25008
26064
|
console.log(
|
|
25009
|
-
|
|
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(
|
|
26067
|
+
console.log(chalk8.dim(` Index: ${dbPath}`));
|
|
25012
26068
|
if (!config.codeIndex?.enabled) {
|
|
25013
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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
|
|
26261
|
+
let masked2 = false;
|
|
25187
26262
|
rl._writeToOutput = (s) => {
|
|
25188
|
-
if (!
|
|
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
|
-
|
|
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(
|
|
25202
|
-
console.log(
|
|
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 ${
|
|
25207
|
-
console.log(` 2. Enter ${
|
|
25208
|
-
console.log(
|
|
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(
|
|
26287
|
+
console.log(chalk8.green(`
|
|
25213
26288
|
\u2713 Signed in as ${who}
|
|
25214
26289
|
`));
|
|
25215
26290
|
} catch (err) {
|
|
25216
|
-
console.log(
|
|
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(
|
|
26300
|
+
console.log(chalk8.dim("\n Not signed in.\n"));
|
|
25226
26301
|
return;
|
|
25227
26302
|
}
|
|
25228
26303
|
await client.logout();
|
|
25229
|
-
console.log(
|
|
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
|
-
${
|
|
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(
|
|
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(
|
|
26327
|
+
console.log(chalk8.dim("\n No cloud chats yet.\n"));
|
|
25253
26328
|
return;
|
|
25254
26329
|
}
|
|
25255
|
-
console.log(
|
|
26330
|
+
console.log(chalk8.magenta("\n \u25C8 Your cloud chats\n"));
|
|
25256
26331
|
for (const c of convos) {
|
|
25257
|
-
console.log(` ${
|
|
26332
|
+
console.log(` ${chalk8.dim(c.id.slice(0, 8))} ${c.title?.trim() || chalk8.dim("(untitled)")}`);
|
|
25258
26333
|
}
|
|
25259
|
-
console.log(
|
|
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(
|
|
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 ?
|
|
25276
|
-
const tag = m.id ?
|
|
25277
|
-
const who = m.role === "user" ?
|
|
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(
|
|
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(
|
|
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(
|
|
26380
|
+
console.log(chalk8.dim(" cascade sessions branch <chat> <message> switch to another version"));
|
|
25306
26381
|
}
|
|
25307
|
-
console.log(
|
|
26382
|
+
console.log(chalk8.dim(" cascade sessions rm <chat> <message> delete a message + its replies\n"));
|
|
25308
26383
|
} catch (err) {
|
|
25309
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
26436
|
+
else console.log(chalk8.dim(" (the conversation is now empty)\n"));
|
|
25362
26437
|
} catch (err) {
|
|
25363
|
-
console.log(
|
|
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(
|
|
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(
|
|
26457
|
+
console.log(chalk8.green(`
|
|
25383
26458
|
\u2713 Renamed to "${title}".
|
|
25384
26459
|
`));
|
|
25385
26460
|
} catch (err) {
|
|
25386
|
-
console.log(
|
|
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(
|
|
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(
|
|
26480
|
+
console.log(chalk8.green(`
|
|
25406
26481
|
\u2713 Deleted "${match.title?.trim() || "(untitled)"}".
|
|
25407
26482
|
`));
|
|
25408
26483
|
} catch (err) {
|
|
25409
|
-
console.log(
|
|
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(
|
|
25422
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
26554
|
+
console.log(chalk8.yellow(` ${describeCleanup(cleanup)}
|
|
25480
26555
|
`));
|
|
25481
26556
|
}
|
|
25482
26557
|
} catch (err) {
|
|
25483
|
-
console.log(
|
|
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
|
|
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(
|
|
25511
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
26627
|
+
console.log(chalk8.dim("\n No MCP servers configured.\n"));
|
|
25553
26628
|
return;
|
|
25554
26629
|
}
|
|
25555
|
-
console.log(
|
|
26630
|
+
console.log(chalk8.magenta("\n \u25C8 MCP servers\n"));
|
|
25556
26631
|
for (const s of servers) {
|
|
25557
|
-
const kind = s.oauthStore ?
|
|
25558
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
26679
|
+
spin.succeed(chalk8.green(`Updated to v${latest}! Restart your terminal.`));
|
|
25605
26680
|
} catch (err) {
|
|
25606
|
-
spin.fail(
|
|
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
|
|
25860
|
-
|
|
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 =
|
|
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
|
-
|
|
26094
|
-
|
|
26095
|
-
|
|
26096
|
-
|
|
26097
|
-
|
|
26098
|
-
|
|
26099
|
-
|
|
26100
|
-
|
|
26101
|
-
|
|
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.
|
|
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
|
-
|
|
26286
|
-
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
|
|
26290
|
-
|
|
26291
|
-
|
|
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(
|
|
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 =
|
|
27731
|
+
const secretPath = path35.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
26310
27732
|
try {
|
|
26311
|
-
if (
|
|
26312
|
-
const existing =
|
|
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
|
-
|
|
26317
|
-
|
|
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 =
|
|
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 =
|
|
26725
|
-
const globalDbPath =
|
|
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 (!
|
|
26729
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
27054
|
-
const existing =
|
|
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
|
-
|
|
27058
|
-
|
|
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 =
|
|
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 =
|
|
27428
|
-
const devPath =
|
|
27429
|
-
const webDistPath =
|
|
27430
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
28887
|
+
spin.succeed(chalk8.green(`Dashboard running at http://localhost:${port}`));
|
|
27467
28888
|
server.refreshRuntime("workspace");
|
|
27468
28889
|
server.refreshRuntime("global");
|
|
27469
|
-
console.log(
|
|
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(
|
|
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(
|
|
28907
|
+
const store = new MemoryStore(path35.join(workspacePath, CASCADE_DB_FILE));
|
|
27487
28908
|
try {
|
|
27488
28909
|
const identities = store.listIdentities();
|
|
27489
|
-
console.log(
|
|
28910
|
+
console.log(chalk8.bold("\n Identities:"));
|
|
27490
28911
|
if (identities.length === 0) {
|
|
27491
|
-
console.log(
|
|
28912
|
+
console.log(chalk8.gray(" No identities found."));
|
|
27492
28913
|
} else {
|
|
27493
28914
|
identities.forEach((id) => {
|
|
27494
|
-
const defaultLabel = id.isDefault ?
|
|
27495
|
-
console.log(` - ${
|
|
27496
|
-
console.log(
|
|
27497
|
-
if (id.description) console.log(
|
|
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(
|
|
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(
|
|
28944
|
+
console.log(chalk8.green(`
|
|
27524
28945
|
Successfully created identity: ${name} (${id})`));
|
|
27525
|
-
if (options.default) console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
27574
|
-
{ tier: "T2", label: "T2 Manager", color:
|
|
27575
|
-
{ tier: "T3", label: "T3 Worker", color:
|
|
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 ?
|
|
27588
|
-
const vision = model.isVisionCapable ?
|
|
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)} ${
|
|
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)} ${
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(` ${
|
|
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(
|
|
29063
|
+
console.log(chalk8.yellow(" Some tiers have no available model. Run `cascade doctor` for details.\n"));
|
|
27643
29064
|
} else {
|
|
27644
|
-
console.log(
|
|
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(
|
|
27651
|
-
console.log(
|
|
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(
|
|
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" ?
|
|
27667
|
-
console.log(
|
|
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 =
|
|
27682
|
-
const globalDbPath =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
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(
|
|
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(
|
|
27730
|
-
console.log(
|
|
27731
|
-
console.log(
|
|
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(
|
|
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(
|
|
29216
|
+
console.log(chalk8.magenta(" \u25C8 Cascade Telemetry"));
|
|
27883
29217
|
console.log();
|
|
27884
|
-
console.log(` Status: ${config.telemetry?.enabled ?
|
|
27885
|
-
console.log(
|
|
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(
|
|
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(
|
|
27902
|
-
console.log(
|
|
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(
|
|
27905
|
-
console.log(
|
|
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(
|
|
29249
|
+
console.log(chalk8.dim("\n No routing history yet \u2014 run some tasks first.\n"));
|
|
27916
29250
|
return;
|
|
27917
29251
|
}
|
|
27918
|
-
console.log(
|
|
27919
|
-
console.log(
|
|
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(
|
|
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(
|
|
27934
|
-
console.log(
|
|
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 ?
|
|
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)} ${
|
|
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(
|
|
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 =
|
|
29286
|
+
const here = path35.dirname(fileURLToPath(import.meta.url));
|
|
27953
29287
|
for (const rel of ["..", "../.."]) {
|
|
27954
|
-
const pkgPath =
|
|
27955
|
-
if (!
|
|
27956
|
-
const pkg = JSON.parse(
|
|
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
|
-
|
|
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", "
|
|
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(
|
|
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(
|
|
28108
|
-
console.error(
|
|
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(
|
|
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(
|
|
28151
|
-
console.error(
|
|
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(
|
|
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(
|
|
29498
|
+
console.error(chalk8.red(`Initialization failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
28165
29499
|
process.exit(1);
|
|
28166
29500
|
}
|
|
28167
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(" " +
|
|
28199
|
-
console.log(" " +
|
|
28200
|
-
console.log(" " +
|
|
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);
|