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