cascade-ai 0.20.0 → 0.20.2
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/bin/cascade.js +0 -0
- package/dist/cli.cjs +3511 -662
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3509 -661
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +6684 -1950
- package/dist/index.cjs +3023 -410
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +955 -10
- package/dist/index.d.ts +955 -10
- package/dist/index.js +2959 -386
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import path24 from 'path';
|
|
3
|
+
import fs23 from 'fs';
|
|
4
4
|
import os6 from 'os';
|
|
5
|
-
import
|
|
6
|
-
import EventEmitter from 'events';
|
|
5
|
+
import crypto4, { randomUUID, randomBytes, createHash, timingSafeEqual, webcrypto } from 'crypto';
|
|
6
|
+
import EventEmitter, { setMaxListeners } from 'events';
|
|
7
7
|
import { glob } from 'glob';
|
|
8
8
|
import Anthropic from '@anthropic-ai/sdk';
|
|
9
9
|
import OpenAI, { AzureOpenAI } from 'openai';
|
|
@@ -24,6 +24,9 @@ import dns2 from 'dns/promises';
|
|
|
24
24
|
import net from 'net';
|
|
25
25
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
26
26
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
27
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
28
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
29
|
+
import { auth, discoverOAuthProtectedResourceMetadata, discoverAuthorizationServerMetadata, refreshAuthorization, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
27
30
|
import { z } from 'zod';
|
|
28
31
|
import { Worker } from 'worker_threads';
|
|
29
32
|
import { fileURLToPath } from 'url';
|
|
@@ -58,12 +61,12 @@ var init_audit_logger = __esm({
|
|
|
58
61
|
constructor(workspacePath, debugMode = false) {
|
|
59
62
|
this.workspacePath = workspacePath;
|
|
60
63
|
this.debugMode = debugMode;
|
|
61
|
-
const cascadeDir =
|
|
62
|
-
if (!
|
|
63
|
-
|
|
64
|
+
const cascadeDir = path24.join(workspacePath, ".cascade");
|
|
65
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
66
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
64
67
|
}
|
|
65
|
-
this.keyPath =
|
|
66
|
-
this.dbPath =
|
|
68
|
+
this.keyPath = path24.join(cascadeDir, "audit_log.key");
|
|
69
|
+
this.dbPath = path24.join(cascadeDir, "audit_log.db");
|
|
67
70
|
this.initEncryptionKey();
|
|
68
71
|
this.db = new Database(this.dbPath);
|
|
69
72
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -90,19 +93,19 @@ var init_audit_logger = __esm({
|
|
|
90
93
|
encryptionKey;
|
|
91
94
|
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
92
95
|
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
93
|
-
return
|
|
96
|
+
return crypto4.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
94
97
|
}
|
|
95
98
|
initEncryptionKey() {
|
|
96
|
-
if (
|
|
97
|
-
this.encryptionKey =
|
|
99
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
100
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
98
101
|
} else {
|
|
99
|
-
this.encryptionKey =
|
|
100
|
-
|
|
102
|
+
this.encryptionKey = crypto4.randomBytes(32);
|
|
103
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
101
104
|
}
|
|
102
105
|
}
|
|
103
106
|
encrypt(text) {
|
|
104
|
-
const iv =
|
|
105
|
-
const cipher =
|
|
107
|
+
const iv = crypto4.randomBytes(16);
|
|
108
|
+
const cipher = crypto4.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
106
109
|
let encrypted = cipher.update(text, "utf8", "hex");
|
|
107
110
|
encrypted += cipher.final("hex");
|
|
108
111
|
const authTag = cipher.getAuthTag().toString("hex");
|
|
@@ -114,14 +117,14 @@ var init_audit_logger = __esm({
|
|
|
114
117
|
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
115
118
|
const iv = Buffer.from(ivHex, "hex");
|
|
116
119
|
const authTag = Buffer.from(authTagHex, "hex");
|
|
117
|
-
const decipher =
|
|
120
|
+
const decipher = crypto4.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
118
121
|
decipher.setAuthTag(authTag);
|
|
119
122
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
120
123
|
decrypted += decipher.final("utf8");
|
|
121
124
|
return decrypted;
|
|
122
125
|
}
|
|
123
126
|
logEvent(eventType, tierId, payloadObj) {
|
|
124
|
-
const id =
|
|
127
|
+
const id = crypto4.randomUUID();
|
|
125
128
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
126
129
|
const payload = JSON.stringify(payloadObj);
|
|
127
130
|
const encryptedPayload = this.encrypt(payload);
|
|
@@ -175,9 +178,9 @@ var init_audit_logger = __esm({
|
|
|
175
178
|
dumpDebugIfNeeded() {
|
|
176
179
|
if (!this.debugMode) return;
|
|
177
180
|
try {
|
|
178
|
-
const dumpPath =
|
|
181
|
+
const dumpPath = path24.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
|
|
179
182
|
const entries = this.getAllLogs();
|
|
180
|
-
|
|
183
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
181
184
|
} catch (err) {
|
|
182
185
|
console.error("Failed to dump debug audit logs", err);
|
|
183
186
|
}
|
|
@@ -190,7 +193,7 @@ var init_audit_logger = __esm({
|
|
|
190
193
|
});
|
|
191
194
|
|
|
192
195
|
// src/constants.ts
|
|
193
|
-
var CASCADE_VERSION = "0.20.
|
|
196
|
+
var CASCADE_VERSION = "0.20.2";
|
|
194
197
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
195
198
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
196
199
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -345,6 +348,82 @@ var MODELS = {
|
|
|
345
348
|
supportsStreaming: true,
|
|
346
349
|
isLocal: false
|
|
347
350
|
},
|
|
351
|
+
// GPT-5 family (offline baseline pricing; the live fetch keeps it current).
|
|
352
|
+
"gpt-5": {
|
|
353
|
+
id: "gpt-5",
|
|
354
|
+
name: "GPT-5",
|
|
355
|
+
provider: "openai",
|
|
356
|
+
contextWindow: 4e5,
|
|
357
|
+
isVisionCapable: true,
|
|
358
|
+
inputCostPer1kTokens: 125e-5,
|
|
359
|
+
outputCostPer1kTokens: 0.01,
|
|
360
|
+
maxOutputTokens: 128e3,
|
|
361
|
+
supportsStreaming: true,
|
|
362
|
+
isLocal: false
|
|
363
|
+
},
|
|
364
|
+
"gpt-5-mini": {
|
|
365
|
+
id: "gpt-5-mini",
|
|
366
|
+
name: "GPT-5 Mini",
|
|
367
|
+
provider: "openai",
|
|
368
|
+
contextWindow: 4e5,
|
|
369
|
+
isVisionCapable: true,
|
|
370
|
+
inputCostPer1kTokens: 25e-5,
|
|
371
|
+
outputCostPer1kTokens: 2e-3,
|
|
372
|
+
maxOutputTokens: 128e3,
|
|
373
|
+
supportsStreaming: true,
|
|
374
|
+
isLocal: false
|
|
375
|
+
},
|
|
376
|
+
"gpt-5-nano": {
|
|
377
|
+
id: "gpt-5-nano",
|
|
378
|
+
name: "GPT-5 Nano",
|
|
379
|
+
provider: "openai",
|
|
380
|
+
contextWindow: 4e5,
|
|
381
|
+
isVisionCapable: true,
|
|
382
|
+
inputCostPer1kTokens: 5e-5,
|
|
383
|
+
outputCostPer1kTokens: 4e-4,
|
|
384
|
+
maxOutputTokens: 128e3,
|
|
385
|
+
supportsStreaming: true,
|
|
386
|
+
isLocal: false
|
|
387
|
+
},
|
|
388
|
+
// GPT-5 point releases — genuinely distinct capability, so they route as their
|
|
389
|
+
// own families (not folded into gpt-5). Offline baseline pricing; live fetch
|
|
390
|
+
// keeps it current. gpt-5.5 "Spud" (Apr 2026) is the current SWE-bench leader.
|
|
391
|
+
"gpt-5.5": {
|
|
392
|
+
id: "gpt-5.5",
|
|
393
|
+
name: "GPT-5.5",
|
|
394
|
+
provider: "openai",
|
|
395
|
+
contextWindow: 4e5,
|
|
396
|
+
isVisionCapable: true,
|
|
397
|
+
inputCostPer1kTokens: 25e-4,
|
|
398
|
+
outputCostPer1kTokens: 0.02,
|
|
399
|
+
maxOutputTokens: 128e3,
|
|
400
|
+
supportsStreaming: true,
|
|
401
|
+
isLocal: false
|
|
402
|
+
},
|
|
403
|
+
"gpt-5.4": {
|
|
404
|
+
id: "gpt-5.4",
|
|
405
|
+
name: "GPT-5.4",
|
|
406
|
+
provider: "openai",
|
|
407
|
+
contextWindow: 4e5,
|
|
408
|
+
isVisionCapable: true,
|
|
409
|
+
inputCostPer1kTokens: 125e-5,
|
|
410
|
+
outputCostPer1kTokens: 0.01,
|
|
411
|
+
maxOutputTokens: 128e3,
|
|
412
|
+
supportsStreaming: true,
|
|
413
|
+
isLocal: false
|
|
414
|
+
},
|
|
415
|
+
"gpt-5.4-mini": {
|
|
416
|
+
id: "gpt-5.4-mini",
|
|
417
|
+
name: "GPT-5.4 Mini",
|
|
418
|
+
provider: "openai",
|
|
419
|
+
contextWindow: 4e5,
|
|
420
|
+
isVisionCapable: true,
|
|
421
|
+
inputCostPer1kTokens: 25e-5,
|
|
422
|
+
outputCostPer1kTokens: 2e-3,
|
|
423
|
+
maxOutputTokens: 128e3,
|
|
424
|
+
supportsStreaming: true,
|
|
425
|
+
isLocal: false
|
|
426
|
+
},
|
|
348
427
|
// Google
|
|
349
428
|
"gemini-2.0-flash": {
|
|
350
429
|
id: "gemini-2.0-flash",
|
|
@@ -464,6 +543,9 @@ for (const _m of Object.values(MODELS)) {
|
|
|
464
543
|
if (_m.supportsToolUse === void 0) _m.supportsToolUse = !_m.isLocal;
|
|
465
544
|
}
|
|
466
545
|
var T1_MODEL_PRIORITY = [
|
|
546
|
+
"gpt-5.5",
|
|
547
|
+
"gpt-5.4",
|
|
548
|
+
"gpt-5",
|
|
467
549
|
"claude-opus-4",
|
|
468
550
|
"claude-sonnet-4",
|
|
469
551
|
"gemini-2.5-pro",
|
|
@@ -472,6 +554,8 @@ var T1_MODEL_PRIORITY = [
|
|
|
472
554
|
"gemini-1.5-pro"
|
|
473
555
|
];
|
|
474
556
|
var T2_MODEL_PRIORITY = [
|
|
557
|
+
"gpt-5.4-mini",
|
|
558
|
+
"gpt-5-mini",
|
|
475
559
|
"claude-sonnet-4",
|
|
476
560
|
"claude-haiku-4-5",
|
|
477
561
|
"claude-haiku-3-5",
|
|
@@ -482,6 +566,7 @@ var T2_MODEL_PRIORITY = [
|
|
|
482
566
|
"llama3:70b"
|
|
483
567
|
];
|
|
484
568
|
var T3_MODEL_PRIORITY = [
|
|
569
|
+
"gpt-5-nano",
|
|
485
570
|
"llama3.2:3b",
|
|
486
571
|
"mistral:7b",
|
|
487
572
|
"claude-haiku-4-5",
|
|
@@ -788,10 +873,23 @@ var AnthropicProvider = class extends BaseProvider {
|
|
|
788
873
|
return result;
|
|
789
874
|
}
|
|
790
875
|
};
|
|
876
|
+
function isReasoningModel(id) {
|
|
877
|
+
const m = (id || "").toLowerCase();
|
|
878
|
+
return /(^o[1345]\b)|(^o[1345]-)|gpt-?5/.test(m);
|
|
879
|
+
}
|
|
880
|
+
function isParamShapeError(err) {
|
|
881
|
+
const e = err;
|
|
882
|
+
const msg = String(e?.message ?? e?.error?.message ?? "").toLowerCase();
|
|
883
|
+
return msg.includes("max_completion_tokens") || msg.includes("max_tokens") && (msg.includes("unsupported") || msg.includes("not supported") || msg.includes("use")) || msg.includes("temperature") && (msg.includes("unsupported") || msg.includes("does not support") || msg.includes("only the default"));
|
|
884
|
+
}
|
|
791
885
|
var OpenAIProvider = class extends BaseProvider {
|
|
792
886
|
client;
|
|
887
|
+
/** Once we learn (from the model id or an API error) that this deployment
|
|
888
|
+
* needs max_completion_tokens, remember it so we don't re-fail every call. */
|
|
889
|
+
useMaxCompletionTokens;
|
|
793
890
|
constructor(config, model) {
|
|
794
891
|
super(config, model);
|
|
892
|
+
this.useMaxCompletionTokens = isReasoningModel(model.id);
|
|
795
893
|
this.client = new OpenAI({
|
|
796
894
|
apiKey: config.apiKey,
|
|
797
895
|
baseURL: config.baseUrl
|
|
@@ -815,27 +913,23 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
815
913
|
let inputTokens = 0;
|
|
816
914
|
let outputTokens = 0;
|
|
817
915
|
let finishReason = "stop";
|
|
818
|
-
const
|
|
916
|
+
const budget = options.maxTokens ?? this.model.maxOutputTokens;
|
|
917
|
+
const buildParams = (useCompletionTokens) => ({
|
|
819
918
|
model: this.model.id,
|
|
820
919
|
messages,
|
|
821
|
-
max_tokens:
|
|
822
|
-
temperature: options.temperature ?? 0.7,
|
|
920
|
+
...useCompletionTokens ? { max_completion_tokens: budget } : { max_tokens: budget },
|
|
921
|
+
...useCompletionTokens ? {} : { temperature: options.temperature ?? 0.7 },
|
|
823
922
|
tools: tools?.length ? tools : void 0,
|
|
824
923
|
stream: true,
|
|
825
924
|
stream_options: { include_usage: true }
|
|
826
|
-
};
|
|
925
|
+
});
|
|
827
926
|
let stream;
|
|
828
927
|
try {
|
|
829
|
-
stream = await this.client.chat.completions.create(
|
|
928
|
+
stream = await this.client.chat.completions.create(buildParams(this.useMaxCompletionTokens), { signal: options.signal });
|
|
830
929
|
} catch (err) {
|
|
831
|
-
if (
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
fallbackParams.max_completion_tokens = options.maxTokens ?? this.model.maxOutputTokens;
|
|
835
|
-
if (this.model.id.includes("o1") || this.model.id.includes("o3")) {
|
|
836
|
-
fallbackParams.temperature = 1;
|
|
837
|
-
}
|
|
838
|
-
stream = await this.client.chat.completions.create(fallbackParams, { signal: options.signal });
|
|
930
|
+
if (!this.useMaxCompletionTokens && isParamShapeError(err)) {
|
|
931
|
+
this.useMaxCompletionTokens = true;
|
|
932
|
+
stream = await this.client.chat.completions.create(buildParams(true), { signal: options.signal });
|
|
839
933
|
} else {
|
|
840
934
|
throw err;
|
|
841
935
|
}
|
|
@@ -993,13 +1087,48 @@ var OpenAIProvider = class extends BaseProvider {
|
|
|
993
1087
|
};
|
|
994
1088
|
|
|
995
1089
|
// src/providers/azure.ts
|
|
1090
|
+
var DEFAULT_AZURE_API_VERSION = "2024-12-01-preview";
|
|
1091
|
+
function inferAzureBaseModel(deploymentName) {
|
|
1092
|
+
const n = deploymentName.toLowerCase();
|
|
1093
|
+
const rules = [
|
|
1094
|
+
[/gpt-?5\.5/, "gpt-5.5"],
|
|
1095
|
+
[/gpt-?5\.4.*mini/, "gpt-5.4-mini"],
|
|
1096
|
+
[/gpt-?5\.4/, "gpt-5.4"],
|
|
1097
|
+
[/gpt-?5.*nano/, "gpt-5-nano"],
|
|
1098
|
+
[/gpt-?5.*mini/, "gpt-5-mini"],
|
|
1099
|
+
[/gpt-?5/, "gpt-5"],
|
|
1100
|
+
[/gpt-?4\.1-nano/, "gpt-4.1-nano"],
|
|
1101
|
+
[/gpt-?4\.1-mini/, "gpt-4.1-mini"],
|
|
1102
|
+
[/gpt-?4\.1/, "gpt-4.1"],
|
|
1103
|
+
[/gpt-?4o-mini/, "gpt-4o-mini"],
|
|
1104
|
+
[/gpt-?4o/, "gpt-4o"]
|
|
1105
|
+
];
|
|
1106
|
+
for (const [re, base] of rules) if (re.test(n)) return base;
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
996
1109
|
function azureModelForDeployment(cfg) {
|
|
997
1110
|
if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
|
|
998
1111
|
const id = cfg.deploymentName.trim();
|
|
1112
|
+
const name = cfg.label?.trim() || id;
|
|
1113
|
+
const baseModelId = cfg.model?.trim() || inferAzureBaseModel(id) || void 0;
|
|
1114
|
+
const base = baseModelId ? MODELS[baseModelId] : void 0;
|
|
1115
|
+
if (base) {
|
|
1116
|
+
return {
|
|
1117
|
+
...base,
|
|
1118
|
+
id,
|
|
1119
|
+
// callable deployment name
|
|
1120
|
+
name,
|
|
1121
|
+
provider: "azure",
|
|
1122
|
+
baseModelId,
|
|
1123
|
+
// real identity for benchmark + live pricing
|
|
1124
|
+
supportsToolUse: base.supportsToolUse ?? true
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
999
1127
|
return {
|
|
1000
1128
|
id,
|
|
1001
|
-
name
|
|
1129
|
+
name,
|
|
1002
1130
|
provider: "azure",
|
|
1131
|
+
...baseModelId ? { baseModelId } : {},
|
|
1003
1132
|
contextWindow: 128e3,
|
|
1004
1133
|
isVisionCapable: false,
|
|
1005
1134
|
inputCostPer1kTokens: 25e-4,
|
|
@@ -1026,7 +1155,7 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
1026
1155
|
apiKey: config.apiKey,
|
|
1027
1156
|
endpoint,
|
|
1028
1157
|
deployment: config.deploymentName ?? model.id,
|
|
1029
|
-
apiVersion: config.apiVersion ??
|
|
1158
|
+
apiVersion: config.apiVersion ?? DEFAULT_AZURE_API_VERSION
|
|
1030
1159
|
});
|
|
1031
1160
|
}
|
|
1032
1161
|
async listModels() {
|
|
@@ -1034,25 +1163,22 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
|
|
|
1034
1163
|
return [fromDeployment ?? this.model];
|
|
1035
1164
|
}
|
|
1036
1165
|
async isAvailable() {
|
|
1037
|
-
const
|
|
1166
|
+
const ping = (extra) => this.client.chat.completions.create({
|
|
1038
1167
|
model: this.model.id,
|
|
1039
1168
|
messages: [{ role: "user", content: "ping" }],
|
|
1040
|
-
|
|
1041
|
-
};
|
|
1169
|
+
...extra
|
|
1170
|
+
});
|
|
1171
|
+
const reasoning = isReasoningModel(this.model.id);
|
|
1042
1172
|
try {
|
|
1043
|
-
await
|
|
1173
|
+
await ping(reasoning ? { max_completion_tokens: 16 } : { max_tokens: 1 });
|
|
1044
1174
|
return true;
|
|
1045
1175
|
} catch (err) {
|
|
1046
|
-
if (
|
|
1176
|
+
if (isParamShapeError(err)) {
|
|
1047
1177
|
try {
|
|
1048
|
-
await
|
|
1049
|
-
model: this.model.id,
|
|
1050
|
-
messages: [{ role: "user", content: "ping" }],
|
|
1051
|
-
max_completion_tokens: 1
|
|
1052
|
-
});
|
|
1178
|
+
await ping({ max_completion_tokens: 16 });
|
|
1053
1179
|
return true;
|
|
1054
|
-
} catch {
|
|
1055
|
-
return
|
|
1180
|
+
} catch (err2) {
|
|
1181
|
+
return isParamShapeError(err2);
|
|
1056
1182
|
}
|
|
1057
1183
|
}
|
|
1058
1184
|
return false;
|
|
@@ -1090,14 +1216,14 @@ var GeminiProvider = class extends BaseProvider {
|
|
|
1090
1216
|
const toolCalls = [];
|
|
1091
1217
|
let finishReason = "stop";
|
|
1092
1218
|
for await (const chunk of stream) {
|
|
1093
|
-
const text = chunk.text ?? "";
|
|
1094
|
-
if (text) {
|
|
1095
|
-
fullContent += text;
|
|
1096
|
-
onChunk({ text, finishReason: null });
|
|
1097
|
-
}
|
|
1098
1219
|
const candidates = chunk.candidates ?? [];
|
|
1099
1220
|
for (const candidate of candidates) {
|
|
1100
1221
|
for (const part of candidate?.content?.parts ?? []) {
|
|
1222
|
+
if (part.thought) continue;
|
|
1223
|
+
if (typeof part.text === "string" && part.text) {
|
|
1224
|
+
fullContent += part.text;
|
|
1225
|
+
onChunk({ text: part.text, finishReason: null });
|
|
1226
|
+
}
|
|
1101
1227
|
if (part.functionCall) {
|
|
1102
1228
|
toolCalls.push({
|
|
1103
1229
|
id: part.functionCall.name,
|
|
@@ -1657,9 +1783,20 @@ var OpenAICompatibleProvider = class extends OpenAIProvider {
|
|
|
1657
1783
|
};
|
|
1658
1784
|
|
|
1659
1785
|
// src/core/router/selector.ts
|
|
1786
|
+
function normalizeModelId(id) {
|
|
1787
|
+
return id.replace(/^models\//, "").toLowerCase();
|
|
1788
|
+
}
|
|
1660
1789
|
var ModelSelector = class {
|
|
1661
1790
|
availableProviders;
|
|
1662
1791
|
availableModels;
|
|
1792
|
+
/**
|
|
1793
|
+
* Per-provider set of model ids the provider's own API confirmed it serves
|
|
1794
|
+
* (from listModels discovery). When present for a provider, AUTO selection
|
|
1795
|
+
* skips that provider's bundled catalog ids that aren't in the set — so a
|
|
1796
|
+
* stale/access-gated catalog id is never picked, then 404'd, then failed over.
|
|
1797
|
+
* Absent for a provider ⇒ no validation ran ⇒ behave exactly as before.
|
|
1798
|
+
*/
|
|
1799
|
+
validatedIds = /* @__PURE__ */ new Map();
|
|
1663
1800
|
constructor(availableProviders) {
|
|
1664
1801
|
this.availableProviders = availableProviders;
|
|
1665
1802
|
this.availableModels = new Map(Object.entries(MODELS));
|
|
@@ -1667,6 +1804,20 @@ var ModelSelector = class {
|
|
|
1667
1804
|
addDynamicModel(model) {
|
|
1668
1805
|
this.availableModels.set(model.id, model);
|
|
1669
1806
|
}
|
|
1807
|
+
/** Record the ids a provider's API actually serves (discovery). Empty ⇒ ignored. */
|
|
1808
|
+
setValidatedModels(provider, ids) {
|
|
1809
|
+
if (!ids.length) return;
|
|
1810
|
+
this.validatedIds.set(provider, new Set(ids.map(normalizeModelId)));
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* A model is usable for AUTO selection when its provider is available AND —
|
|
1814
|
+
* if that provider was validated — the id is one the provider confirmed.
|
|
1815
|
+
*/
|
|
1816
|
+
isUsable(model) {
|
|
1817
|
+
if (!this.availableProviders.has(model.provider)) return false;
|
|
1818
|
+
const valid = this.validatedIds.get(model.provider);
|
|
1819
|
+
return !valid || valid.has(normalizeModelId(model.id));
|
|
1820
|
+
}
|
|
1670
1821
|
/**
|
|
1671
1822
|
* Permanently drop a model from the available set for this session. Used by
|
|
1672
1823
|
* the router's 404 / "model not found" self-heal so a dead id is never
|
|
@@ -1704,10 +1855,10 @@ var ModelSelector = class {
|
|
|
1704
1855
|
const priority = this.getPriorityList(tier);
|
|
1705
1856
|
for (const key of priority) {
|
|
1706
1857
|
const model = this.availableModels.get(key);
|
|
1707
|
-
if (model && this.
|
|
1858
|
+
if (model && this.isUsable(model)) return model;
|
|
1708
1859
|
}
|
|
1709
1860
|
for (const [, model] of this.availableModels) {
|
|
1710
|
-
if (this.
|
|
1861
|
+
if (this.isUsable(model)) return model;
|
|
1711
1862
|
}
|
|
1712
1863
|
return null;
|
|
1713
1864
|
}
|
|
@@ -1727,7 +1878,7 @@ var ModelSelector = class {
|
|
|
1727
1878
|
for (let i = currentIdx + 1; i < priority.length; i++) {
|
|
1728
1879
|
const key = priority[i];
|
|
1729
1880
|
const model = this.availableModels.get(key);
|
|
1730
|
-
if (model && this.
|
|
1881
|
+
if (model && this.isUsable(model)) return model;
|
|
1731
1882
|
}
|
|
1732
1883
|
return null;
|
|
1733
1884
|
}
|
|
@@ -1755,7 +1906,7 @@ var ModelSelector = class {
|
|
|
1755
1906
|
const candidates = [];
|
|
1756
1907
|
for (const key of priority) {
|
|
1757
1908
|
const model = this.availableModels.get(key);
|
|
1758
|
-
if (model && this.
|
|
1909
|
+
if (model && this.isUsable(model)) {
|
|
1759
1910
|
candidates.push(model);
|
|
1760
1911
|
}
|
|
1761
1912
|
}
|
|
@@ -2200,8 +2351,8 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
2200
2351
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
2201
2352
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
2202
2353
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
2203
|
-
var DEFAULT_CACHE_FILE =
|
|
2204
|
-
function
|
|
2354
|
+
var DEFAULT_CACHE_FILE = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
2355
|
+
function normalizeModelId2(id) {
|
|
2205
2356
|
let s = id.toLowerCase();
|
|
2206
2357
|
const slash = s.lastIndexOf("/");
|
|
2207
2358
|
if (slash !== -1) s = s.slice(slash + 1);
|
|
@@ -2318,7 +2469,7 @@ var LiveDataProvider = class {
|
|
|
2318
2469
|
const capabilities = /* @__PURE__ */ new Map();
|
|
2319
2470
|
for (const m of data.data) {
|
|
2320
2471
|
if (!m?.id) continue;
|
|
2321
|
-
const key =
|
|
2472
|
+
const key = normalizeModelId2(m.id);
|
|
2322
2473
|
if (m.pricing) {
|
|
2323
2474
|
const input = Number(m.pricing.prompt) * 1e3;
|
|
2324
2475
|
const output = Number(m.pricing.completion) * 1e3;
|
|
@@ -2345,7 +2496,7 @@ var LiveDataProvider = class {
|
|
|
2345
2496
|
}
|
|
2346
2497
|
async saveCache() {
|
|
2347
2498
|
try {
|
|
2348
|
-
await fs4.mkdir(
|
|
2499
|
+
await fs4.mkdir(path24.dirname(this.opts.cacheFile), { recursive: true });
|
|
2349
2500
|
const cache = {
|
|
2350
2501
|
fetchedAt: this.fetchedAt,
|
|
2351
2502
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -2362,7 +2513,7 @@ var LiveDataProvider = class {
|
|
|
2362
2513
|
}
|
|
2363
2514
|
/** Current per-1k price for a model id, or null when unknown. */
|
|
2364
2515
|
getLivePrice(modelId) {
|
|
2365
|
-
return this.prices.get(
|
|
2516
|
+
return this.prices.get(normalizeModelId2(modelId)) ?? null;
|
|
2366
2517
|
}
|
|
2367
2518
|
/**
|
|
2368
2519
|
* Returns a price-corrected copy of each model when live pricing is known,
|
|
@@ -2370,14 +2521,14 @@ var LiveDataProvider = class {
|
|
|
2370
2521
|
*/
|
|
2371
2522
|
applyLivePricing(models) {
|
|
2372
2523
|
return models.map((m) => {
|
|
2373
|
-
const p = this.getLivePrice(m.id);
|
|
2524
|
+
const p = this.getLivePrice(m.baseModelId ?? m.id);
|
|
2374
2525
|
if (!p) return m;
|
|
2375
2526
|
return { ...m, inputCostPer1kTokens: p.input, outputCostPer1kTokens: p.output };
|
|
2376
2527
|
});
|
|
2377
2528
|
}
|
|
2378
2529
|
/** Current capability facts for a model id, or null when unknown. */
|
|
2379
2530
|
getCapability(modelId) {
|
|
2380
|
-
return this.capabilities.get(
|
|
2531
|
+
return this.capabilities.get(normalizeModelId2(modelId)) ?? null;
|
|
2381
2532
|
}
|
|
2382
2533
|
/**
|
|
2383
2534
|
* Returns capability-corrected copies of each model (originals untouched):
|
|
@@ -2387,7 +2538,7 @@ var LiveDataProvider = class {
|
|
|
2387
2538
|
*/
|
|
2388
2539
|
applyLiveCapabilities(models) {
|
|
2389
2540
|
return models.map((m) => {
|
|
2390
|
-
const c = this.getCapability(m.id);
|
|
2541
|
+
const c = this.getCapability(m.baseModelId ?? m.id);
|
|
2391
2542
|
if (!c) return m;
|
|
2392
2543
|
const next = { ...m };
|
|
2393
2544
|
if (c.contextWindow) next.contextWindow = c.contextWindow;
|
|
@@ -2421,6 +2572,15 @@ var FAMILY_BENCHMARKS = {
|
|
|
2421
2572
|
"claude-opus": { code: 95, analysis: 92, creative: 90, data: 88 },
|
|
2422
2573
|
"claude-sonnet": { code: 93, analysis: 88, creative: 87, data: 85 },
|
|
2423
2574
|
"claude-haiku": { code: 80, analysis: 75, creative: 76, data: 72 },
|
|
2575
|
+
// OpenAI GPT-5 family — current flagship; strongest all-round. Offline
|
|
2576
|
+
// baseline (the live fetch refreshes these). Distinct point releases carry
|
|
2577
|
+
// their own scores: gpt-5.5 "Spud" leads SWE-bench (88.7%); gpt-5.4 ~80%.
|
|
2578
|
+
"gpt-5.5": { code: 97, analysis: 96, creative: 94, data: 95 },
|
|
2579
|
+
"gpt-5.4": { code: 94, analysis: 94, creative: 92, data: 92 },
|
|
2580
|
+
"gpt-5.4-mini": { code: 86, analysis: 85, creative: 85, data: 83 },
|
|
2581
|
+
"gpt-5": { code: 96, analysis: 95, creative: 93, data: 93 },
|
|
2582
|
+
"gpt-5-mini": { code: 88, analysis: 86, creative: 86, data: 84 },
|
|
2583
|
+
"gpt-5-nano": { code: 78, analysis: 75, creative: 78, data: 73 },
|
|
2424
2584
|
// OpenAI — strong all-round, particularly creative/writing.
|
|
2425
2585
|
"gpt-4.1": { code: 90, analysis: 89, creative: 91, data: 87 },
|
|
2426
2586
|
"gpt-4.1-mini": { code: 82, analysis: 80, creative: 83, data: 79 },
|
|
@@ -2447,6 +2607,15 @@ var FAMILY_MATCHERS = [
|
|
|
2447
2607
|
[/opus/i, "claude-opus"],
|
|
2448
2608
|
[/sonnet/i, "claude-sonnet"],
|
|
2449
2609
|
[/haiku/i, "claude-haiku"],
|
|
2610
|
+
// GPT-5 family — ordered most-specific first. Distinct point releases
|
|
2611
|
+
// (gpt-5.5, gpt-5.4, gpt-5.4-mini) match their own family; nano/mini before
|
|
2612
|
+
// the base; unrecognised gpt-5.x still fold into the gpt-5 base.
|
|
2613
|
+
[/gpt-?5\.5/i, "gpt-5.5"],
|
|
2614
|
+
[/gpt-?5\.4.*mini/i, "gpt-5.4-mini"],
|
|
2615
|
+
[/gpt-?5\.4/i, "gpt-5.4"],
|
|
2616
|
+
[/gpt-?5.*nano/i, "gpt-5-nano"],
|
|
2617
|
+
[/gpt-?5.*mini/i, "gpt-5-mini"],
|
|
2618
|
+
[/gpt-?5/i, "gpt-5"],
|
|
2450
2619
|
[/gpt-?4\.1-nano/i, "gpt-4.1-nano"],
|
|
2451
2620
|
[/gpt-?4\.1-mini/i, "gpt-4.1-mini"],
|
|
2452
2621
|
[/gpt-?4\.1/i, "gpt-4.1"],
|
|
@@ -2466,7 +2635,7 @@ var FAMILY_MATCHERS = [
|
|
|
2466
2635
|
[/gemma/i, "gemma"]
|
|
2467
2636
|
];
|
|
2468
2637
|
function resolveFamily(model) {
|
|
2469
|
-
const hay = `${model.id} ${model.name}`;
|
|
2638
|
+
const hay = `${model.baseModelId ?? ""} ${model.id} ${model.name}`;
|
|
2470
2639
|
for (const [re, fam] of FAMILY_MATCHERS) {
|
|
2471
2640
|
if (re.test(hay)) return fam;
|
|
2472
2641
|
}
|
|
@@ -2488,6 +2657,50 @@ function benchmarkScore01(model, taskType) {
|
|
|
2488
2657
|
}
|
|
2489
2658
|
|
|
2490
2659
|
// src/core/router/index.ts
|
|
2660
|
+
function inferModelCapability(id) {
|
|
2661
|
+
const s = (id || "").toLowerCase();
|
|
2662
|
+
let score = 0;
|
|
2663
|
+
if (/\b(mini|nano|small|lite|flash|instant|tiny|micro|0\.5b|1b|1\.5b|3b|7b|8b)\b/.test(s)) score -= 30;
|
|
2664
|
+
else if (/\b(pro|max|opus|ultra|large|xl|xxl|70b|405b|405)\b/.test(s)) score += 30;
|
|
2665
|
+
const m = s.match(/(\d{1,3})(?:\.(\d))?/);
|
|
2666
|
+
if (m) {
|
|
2667
|
+
let major = parseInt(m[1], 10);
|
|
2668
|
+
let minor = m[2] ? parseInt(m[2], 10) : 0;
|
|
2669
|
+
if (major >= 30 && major < 100) {
|
|
2670
|
+
minor = major % 10;
|
|
2671
|
+
major = Math.floor(major / 10);
|
|
2672
|
+
}
|
|
2673
|
+
score += major + minor / 10;
|
|
2674
|
+
}
|
|
2675
|
+
return score;
|
|
2676
|
+
}
|
|
2677
|
+
function azureModelForTier(tier, ranked) {
|
|
2678
|
+
if (ranked.length === 0) return void 0;
|
|
2679
|
+
if (tier === "T1") return ranked[0];
|
|
2680
|
+
if (tier === "T3") return ranked[ranked.length - 1];
|
|
2681
|
+
return ranked[Math.min(1, ranked.length - 1)];
|
|
2682
|
+
}
|
|
2683
|
+
var DISCOVERY_TTL_MS = 15 * 60 * 1e3;
|
|
2684
|
+
var DISCOVERY_TIMEOUT_MS = 4e3;
|
|
2685
|
+
var cloudDiscoveryCache = /* @__PURE__ */ new Map();
|
|
2686
|
+
function discoveryCacheKey(type, cfg) {
|
|
2687
|
+
const raw = `${type}|${cfg.apiKey ?? ""}|${cfg.baseUrl ?? ""}`;
|
|
2688
|
+
return crypto4.createHash("sha256").update(raw).digest("hex").slice(0, 24);
|
|
2689
|
+
}
|
|
2690
|
+
function applyTierLimits(options, tier, limits) {
|
|
2691
|
+
if (!limits) return options;
|
|
2692
|
+
const key = tier.toLowerCase();
|
|
2693
|
+
const maxTokens = limits[`${key}MaxTokens`];
|
|
2694
|
+
const temperature = limits[`${key}Temperature`];
|
|
2695
|
+
let next = options;
|
|
2696
|
+
if (typeof maxTokens === "number" && maxTokens > 0 && (!next.maxTokens || next.maxTokens > maxTokens)) {
|
|
2697
|
+
next = { ...next, maxTokens };
|
|
2698
|
+
}
|
|
2699
|
+
if (typeof temperature === "number" && next.temperature === void 0) {
|
|
2700
|
+
next = { ...next, temperature };
|
|
2701
|
+
}
|
|
2702
|
+
return next;
|
|
2703
|
+
}
|
|
2491
2704
|
var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
2492
2705
|
selector;
|
|
2493
2706
|
failover;
|
|
@@ -2557,12 +2770,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2557
2770
|
const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
|
|
2558
2771
|
if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
|
|
2559
2772
|
}
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
}
|
|
2773
|
+
const azureDeployments = config.providers.map((cfg) => azureModelForDeployment(cfg)).filter((m) => m !== null);
|
|
2774
|
+
if (azureDeployments.length > 0) {
|
|
2775
|
+
for (const model of azureDeployments) this.selector.addDynamicModel(model);
|
|
2776
|
+
availableProviders.add("azure");
|
|
2565
2777
|
}
|
|
2778
|
+
await this.validateCloudProviderModels(config);
|
|
2566
2779
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
2567
2780
|
const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
|
|
2568
2781
|
if (!override || override === "auto") continue;
|
|
@@ -2583,9 +2796,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2583
2796
|
this.tierModels.set(tier, model);
|
|
2584
2797
|
this.ensureProvider(model, config.providers);
|
|
2585
2798
|
}
|
|
2799
|
+
const azureRanked = [...azureDeployments].sort((a, b) => inferModelCapability(b.id) - inferModelCapability(a.id));
|
|
2586
2800
|
for (const tier of ["T1", "T2", "T3"]) {
|
|
2587
2801
|
if (this.tierModels.has(tier)) continue;
|
|
2588
|
-
const model = this.selector.selectForTier(tier);
|
|
2802
|
+
const model = this.selector.getCandidatesForTier(tier)[0] ?? (azureRanked.length ? azureModelForTier(tier, azureRanked) : void 0) ?? this.selector.selectForTier(tier);
|
|
2589
2803
|
if (model) {
|
|
2590
2804
|
this.tierModels.set(tier, model);
|
|
2591
2805
|
this.ensureProvider(model, config.providers);
|
|
@@ -2667,7 +2881,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2667
2881
|
live: benchCfg.live,
|
|
2668
2882
|
pricingLive: benchCfg.pricingLive,
|
|
2669
2883
|
refreshHours: benchCfg.refreshHours,
|
|
2670
|
-
sourceUrl: benchCfg.sourceUrl
|
|
2884
|
+
sourceUrl: benchCfg.sourceUrl,
|
|
2885
|
+
cacheFile: benchCfg.cacheFile
|
|
2671
2886
|
});
|
|
2672
2887
|
setBenchmarkLiveProvider(this.liveData);
|
|
2673
2888
|
}
|
|
@@ -2680,6 +2895,40 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2680
2895
|
getLiveData() {
|
|
2681
2896
|
return this.liveData;
|
|
2682
2897
|
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Validate the official cloud providers (anthropic/openai/gemini) against
|
|
2900
|
+
* their own /models list at init time so AUTO selection only picks a model the
|
|
2901
|
+
* key actually serves. This is the fix for "a non-existent catalog model gets
|
|
2902
|
+
* selected, then the run fails over through several models before one works".
|
|
2903
|
+
* Cached per key (TTL) so a per-request router doesn't re-discover; fully
|
|
2904
|
+
* best-effort + time-boxed, so offline/error simply keeps the bundled catalog.
|
|
2905
|
+
*/
|
|
2906
|
+
async validateCloudProviderModels(config) {
|
|
2907
|
+
const providers = ["anthropic", "openai", "gemini"];
|
|
2908
|
+
await Promise.all(providers.map(async (type) => {
|
|
2909
|
+
if (!this.selector.isProviderAvailable(type)) return;
|
|
2910
|
+
const cfg = config.providers.find((p) => p.type === type);
|
|
2911
|
+
if (!cfg?.apiKey) return;
|
|
2912
|
+
const cacheKey = discoveryCacheKey(type, cfg);
|
|
2913
|
+
let entry = cloudDiscoveryCache.get(cacheKey);
|
|
2914
|
+
if (!entry || Date.now() - entry.at > DISCOVERY_TTL_MS) {
|
|
2915
|
+
const seed = this.getAnyModelForProvider(type);
|
|
2916
|
+
if (!seed) return;
|
|
2917
|
+
try {
|
|
2918
|
+
const provider = this.createProvider(cfg, seed);
|
|
2919
|
+
if (typeof provider.listModels !== "function") return;
|
|
2920
|
+
const models = await withTimeout(provider.listModels(), DISCOVERY_TIMEOUT_MS, `${type} model discovery timed out`);
|
|
2921
|
+
if (!models.length) return;
|
|
2922
|
+
entry = { models, ids: models.map((m) => m.id), at: Date.now() };
|
|
2923
|
+
cloudDiscoveryCache.set(cacheKey, entry);
|
|
2924
|
+
} catch {
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
for (const m of entry.models) this.selector.addDynamicModel(m);
|
|
2929
|
+
this.selector.setValidatedModels(type, entry.ids);
|
|
2930
|
+
}));
|
|
2931
|
+
}
|
|
2683
2932
|
/**
|
|
2684
2933
|
* Query each available cloud provider's live model list and register the
|
|
2685
2934
|
* results. Confirms catalog ids still exist and surfaces newly released
|
|
@@ -2733,19 +2982,14 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2733
2982
|
this.runBudgetExceededReason ?? "Per-task budget exceeded."
|
|
2734
2983
|
);
|
|
2735
2984
|
}
|
|
2736
|
-
|
|
2737
|
-
const tierKey = tier.toLowerCase();
|
|
2738
|
-
const tierMaxTokens = limits?.[`${tierKey}MaxTokens`];
|
|
2739
|
-
if (tierMaxTokens && (!options.maxTokens || options.maxTokens > tierMaxTokens)) {
|
|
2740
|
-
options = { ...options, maxTokens: tierMaxTokens };
|
|
2741
|
-
}
|
|
2985
|
+
options = applyTierLimits(options, tier, this.config?.tierLimits);
|
|
2742
2986
|
if (this.runSignal && !options.signal) {
|
|
2743
2987
|
options = { ...options, signal: this.runSignal };
|
|
2744
2988
|
}
|
|
2745
2989
|
if (options.model && !requireVision) {
|
|
2746
2990
|
this.ensureProvider(options.model, this.config.providers);
|
|
2747
2991
|
}
|
|
2748
|
-
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
2992
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier) ?? this.selector.selectForTier(tier) ?? void 0;
|
|
2749
2993
|
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
2750
2994
|
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
2751
2995
|
if (!localModel) {
|
|
@@ -2854,7 +3098,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2854
3098
|
}
|
|
2855
3099
|
if (isModelNotFoundError(errMsg)) {
|
|
2856
3100
|
this.selector.removeModel(model.id);
|
|
2857
|
-
const
|
|
3101
|
+
const depth = (options._notFoundDepth ?? 0) + 1;
|
|
3102
|
+
const next = depth <= 3 ? this.selector.selectForTier(tier) : null;
|
|
2858
3103
|
if (next && next.id !== model.id) {
|
|
2859
3104
|
this.tierModels.set(tier, next);
|
|
2860
3105
|
this.ensureProvider(next, this.config.providers);
|
|
@@ -2866,7 +3111,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2866
3111
|
});
|
|
2867
3112
|
releaseLocalSlot?.();
|
|
2868
3113
|
releaseLocalSlot = void 0;
|
|
2869
|
-
const
|
|
3114
|
+
const base = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
|
|
3115
|
+
const retryOpts = { ...base, _notFoundDepth: depth };
|
|
2870
3116
|
return this.generate(tier, retryOpts, onChunk, requireVision);
|
|
2871
3117
|
}
|
|
2872
3118
|
}
|
|
@@ -2932,6 +3178,15 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
2932
3178
|
getSelector() {
|
|
2933
3179
|
return this.selector;
|
|
2934
3180
|
}
|
|
3181
|
+
/**
|
|
3182
|
+
* The tightest context window (in tokens) across the resolved tier models, or
|
|
3183
|
+
* undefined if none are resolved yet. Extended-context compaction budgets
|
|
3184
|
+
* against this so the compacted context fits whichever tier handles the run.
|
|
3185
|
+
*/
|
|
3186
|
+
getReferenceContextWindow() {
|
|
3187
|
+
const windows = [...this.tierModels.values()].map((m) => m.contextWindow).filter((n) => typeof n === "number" && n > 0);
|
|
3188
|
+
return windows.length ? Math.min(...windows) : void 0;
|
|
3189
|
+
}
|
|
2935
3190
|
/** Wire the Cascade Auto task analyzer used for per-subtask model routing. */
|
|
2936
3191
|
setTaskAnalyzer(analyzer) {
|
|
2937
3192
|
this.taskAnalyzer = analyzer;
|
|
@@ -3111,6 +3366,10 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
3111
3366
|
return this.selector?.getAllAvailableModels() ?? [];
|
|
3112
3367
|
}
|
|
3113
3368
|
// ── Private ──────────────────────────────────
|
|
3369
|
+
/** Logs why a configured provider failed its availability probe. */
|
|
3370
|
+
emitProbeFailure(type, reason) {
|
|
3371
|
+
console.warn(`[router] provider "${type}" is not available: ${reason}`);
|
|
3372
|
+
}
|
|
3114
3373
|
async detectAvailableProviders(configs) {
|
|
3115
3374
|
const available = /* @__PURE__ */ new Set();
|
|
3116
3375
|
const checks = configs.map(async (cfg) => {
|
|
@@ -3120,7 +3379,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
3120
3379
|
const provider = this.createProvider(cfg, testModel);
|
|
3121
3380
|
const ok = await provider.isAvailable();
|
|
3122
3381
|
if (ok) available.add(cfg.type);
|
|
3123
|
-
|
|
3382
|
+
else this.emitProbeFailure(cfg.type, "availability check returned false (bad key, wrong endpoint/deployment, or unreachable)");
|
|
3383
|
+
} catch (err) {
|
|
3384
|
+
this.emitProbeFailure(cfg.type, err instanceof Error ? err.message : String(err));
|
|
3124
3385
|
}
|
|
3125
3386
|
});
|
|
3126
3387
|
await Promise.allSettled(checks);
|
|
@@ -3790,18 +4051,51 @@ var WorkerStallError = class extends Error {
|
|
|
3790
4051
|
}
|
|
3791
4052
|
partialOutput;
|
|
3792
4053
|
};
|
|
3793
|
-
var
|
|
4054
|
+
var KNOWN_TOOLS = [
|
|
4055
|
+
"shell",
|
|
4056
|
+
"file_read",
|
|
4057
|
+
"file_write",
|
|
4058
|
+
"file_edit",
|
|
4059
|
+
"file_delete",
|
|
4060
|
+
"file_list",
|
|
4061
|
+
"git",
|
|
4062
|
+
"github",
|
|
4063
|
+
"image_analyze",
|
|
4064
|
+
"pdf_create",
|
|
4065
|
+
"run_code",
|
|
4066
|
+
"peer_message",
|
|
4067
|
+
"web_search",
|
|
4068
|
+
"glob",
|
|
4069
|
+
"grep",
|
|
4070
|
+
"web_fetch"
|
|
4071
|
+
];
|
|
4072
|
+
function buildWorkerRules(has) {
|
|
4073
|
+
const canWriteFiles = has("file_write") || has("file_edit") || has("run_code");
|
|
4074
|
+
const hasAnyTool = KNOWN_TOOLS.some(has);
|
|
4075
|
+
const rules = [
|
|
4076
|
+
"- Execute the subtask completely \u2014 do not stop partway through.",
|
|
4077
|
+
hasAnyTool && "- Use tools when needed. Ask for approval only when the tool registry requires it.",
|
|
4078
|
+
canWriteFiles && "- If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.",
|
|
4079
|
+
has("web_search") && '- Use the "web_search" tool to find current information, documentation, news, or general web data.',
|
|
4080
|
+
has("pdf_create") && '- Use the "pdf_create" tool for PDF requests.',
|
|
4081
|
+
has("run_code") && '- Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.',
|
|
4082
|
+
"- If you are not making meaningful progress, stop and escalate rather than looping or padding the response.",
|
|
4083
|
+
has("peer_message") && '- Use the "peer_message" tool to communicate with other T3 workers if your tasks have dependencies or shared state. You can send updates or wait for signals.',
|
|
4084
|
+
"- Return structured output that directly addresses the expected output specification."
|
|
4085
|
+
];
|
|
4086
|
+
return `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
|
|
3794
4087
|
|
|
3795
4088
|
Rules:
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
4089
|
+
${rules.filter((r) => r !== false).join("\n")}`;
|
|
4090
|
+
}
|
|
4091
|
+
var ARTIFACT_TOOLS = /* @__PURE__ */ new Set(["file_write", "file_edit", "shell"]);
|
|
4092
|
+
function shouldRequireArtifact(assignment, toolNames) {
|
|
4093
|
+
if (!toolNames.some((n) => ARTIFACT_TOOLS.has(n))) return false;
|
|
4094
|
+
if (assignment?.files?.length) return true;
|
|
4095
|
+
const haystack = `${assignment?.description ?? ""}
|
|
4096
|
+
${assignment?.expectedOutput ?? ""}`;
|
|
4097
|
+
return /\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/i.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
4098
|
+
}
|
|
3805
4099
|
var T3Worker = class extends BaseTier {
|
|
3806
4100
|
router;
|
|
3807
4101
|
toolRegistry;
|
|
@@ -4282,8 +4576,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
4282
4576
|
tierId: this.id,
|
|
4283
4577
|
sessionId: this.taskId,
|
|
4284
4578
|
requireApproval: false,
|
|
4285
|
-
saveSnapshot: async (
|
|
4286
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4579
|
+
saveSnapshot: async (path26, content) => {
|
|
4580
|
+
this.store?.addFileSnapshot(this.taskId, path26, content);
|
|
4287
4581
|
},
|
|
4288
4582
|
sendPeerSync: (to, syncType, content) => {
|
|
4289
4583
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -4435,10 +4729,7 @@ ${assignment.expectedOutput}`;
|
|
|
4435
4729
|
};
|
|
4436
4730
|
}
|
|
4437
4731
|
requiresArtifact() {
|
|
4438
|
-
|
|
4439
|
-
const haystack = `${this.assignment?.description ?? ""}
|
|
4440
|
-
${this.assignment?.expectedOutput ?? ""}`;
|
|
4441
|
-
return /\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/i.test(haystack) || /save (?:a|the)? file|create (?:a|the)? file|write (?:a|the)? file/i.test(haystack);
|
|
4732
|
+
return shouldRequireArtifact(this.assignment, this.tools.map((t) => t.name));
|
|
4442
4733
|
}
|
|
4443
4734
|
extractArtifactPaths(assignment) {
|
|
4444
4735
|
const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
|
|
@@ -4455,7 +4746,7 @@ ${assignment.expectedOutput}`;
|
|
|
4455
4746
|
const { promisify: promisify4 } = await import('util');
|
|
4456
4747
|
const execAsync2 = promisify4(exec2);
|
|
4457
4748
|
for (const artifactPath of artifactPaths) {
|
|
4458
|
-
const absolutePath =
|
|
4749
|
+
const absolutePath = path24.resolve(process.cwd(), artifactPath);
|
|
4459
4750
|
try {
|
|
4460
4751
|
const stat = await fs4.stat(absolutePath);
|
|
4461
4752
|
if (!stat.isFile()) {
|
|
@@ -4476,7 +4767,7 @@ ${assignment.expectedOutput}`;
|
|
|
4476
4767
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
4477
4768
|
continue;
|
|
4478
4769
|
}
|
|
4479
|
-
const ext =
|
|
4770
|
+
const ext = path24.extname(absolutePath).toLowerCase();
|
|
4480
4771
|
try {
|
|
4481
4772
|
if (ext === ".ts" || ext === ".tsx") {
|
|
4482
4773
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -4653,7 +4944,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
4653
4944
|
return result.output;
|
|
4654
4945
|
}
|
|
4655
4946
|
buildSystemPrompt(assignment) {
|
|
4656
|
-
|
|
4947
|
+
const available = new Set(this.toolRegistry.getToolDefinitions().map((t) => t.name));
|
|
4948
|
+
return `${buildWorkerRules((name) => available.has(name))}
|
|
4657
4949
|
|
|
4658
4950
|
Your subtask:
|
|
4659
4951
|
- Title: ${assignment.subtaskTitle}
|
|
@@ -5052,11 +5344,15 @@ var RedactionLayer = class {
|
|
|
5052
5344
|
};
|
|
5053
5345
|
|
|
5054
5346
|
// src/core/tiers/t2-manager.ts
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5347
|
+
function buildT2SystemPrompt(hasPeerMessage) {
|
|
5348
|
+
return [
|
|
5349
|
+
"You are a T2 Manager agent in the Cascade AI system.",
|
|
5350
|
+
"Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.",
|
|
5351
|
+
'If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.',
|
|
5352
|
+
hasPeerMessage && 'Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.',
|
|
5353
|
+
"Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text."
|
|
5354
|
+
].filter((l) => l !== false).join("\n");
|
|
5355
|
+
}
|
|
5060
5356
|
var T2Manager = class extends BaseTier {
|
|
5061
5357
|
router;
|
|
5062
5358
|
toolRegistry;
|
|
@@ -5275,7 +5571,7 @@ Return ONLY the JSON array.`;
|
|
|
5275
5571
|
const messages = [{ role: "user", content: prompt }];
|
|
5276
5572
|
const result = await this.router.generate("T2", {
|
|
5277
5573
|
messages,
|
|
5278
|
-
systemPrompt: this.systemPromptOverride +
|
|
5574
|
+
systemPrompt: this.systemPromptOverride + buildT2SystemPrompt(this.toolRegistry.hasTool("peer_message")) + (this.hierarchyContext ? `
|
|
5279
5575
|
|
|
5280
5576
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
5281
5577
|
maxTokens: 2e3,
|
|
@@ -5630,16 +5926,16 @@ ${peerOutputs}` : "";
|
|
|
5630
5926
|
let currentSummary = "";
|
|
5631
5927
|
let i = 0;
|
|
5632
5928
|
while (i < completed.length) {
|
|
5633
|
-
let
|
|
5929
|
+
let chunkText3 = "";
|
|
5634
5930
|
let chunkEnd = i;
|
|
5635
5931
|
while (chunkEnd < completed.length) {
|
|
5636
5932
|
const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
|
|
5637
5933
|
|
|
5638
5934
|
`;
|
|
5639
|
-
if (
|
|
5935
|
+
if (chunkText3.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
|
|
5640
5936
|
break;
|
|
5641
5937
|
}
|
|
5642
|
-
|
|
5938
|
+
chunkText3 += nextOutput;
|
|
5643
5939
|
chunkEnd++;
|
|
5644
5940
|
}
|
|
5645
5941
|
i = chunkEnd;
|
|
@@ -5650,7 +5946,7 @@ PREVIOUS SUMMARY SO FAR:
|
|
|
5650
5946
|
${currentSummary}
|
|
5651
5947
|
|
|
5652
5948
|
NEW OUTPUTS TO INTEGRATE:
|
|
5653
|
-
` : "\nOUTPUTS:\n"}${
|
|
5949
|
+
` : "\nOUTPUTS:\n"}${chunkText3}${peerContext}`;
|
|
5654
5950
|
const messages = [{ role: "user", content: prompt }];
|
|
5655
5951
|
try {
|
|
5656
5952
|
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
@@ -5665,7 +5961,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
5665
5961
|
currentSummary = result.content;
|
|
5666
5962
|
} catch (err) {
|
|
5667
5963
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
5668
|
-
return currentSummary + "\n\n" +
|
|
5964
|
+
return currentSummary + "\n\n" + chunkText3;
|
|
5669
5965
|
}
|
|
5670
5966
|
}
|
|
5671
5967
|
return currentSummary;
|
|
@@ -5779,7 +6075,16 @@ function isStrongKeywordOverlap(a = [], b = []) {
|
|
|
5779
6075
|
const ratio = shared.length / Math.min(a.length, b.length);
|
|
5780
6076
|
return shared.length >= 3 && ratio >= 0.6;
|
|
5781
6077
|
}
|
|
5782
|
-
|
|
6078
|
+
function buildT1SystemPrompt(has) {
|
|
6079
|
+
const canWriteArtifacts = has("file_write") || has("file_edit") || has("run_code") || has("pdf_create");
|
|
6080
|
+
const rules = [
|
|
6081
|
+
"- Simple \u2192 1 T3, Moderate \u2192 2-3 T2s, Complex \u2192 3-5 T2s, Highly Complex \u2192 5+ T2s",
|
|
6082
|
+
"- Return ONLY valid JSON \u2014 no other text",
|
|
6083
|
+
has("pdf_create") && '- If the user asks for a PDF, explicitly use the "pdf_create" tool',
|
|
6084
|
+
has("run_code") && '- If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js',
|
|
6085
|
+
canWriteArtifacts && "- Ensure every plan includes explicit creation and verification steps for requested artifacts"
|
|
6086
|
+
];
|
|
6087
|
+
return `You are T1, the Administrator in the Cascade AI orchestration system.
|
|
5783
6088
|
|
|
5784
6089
|
Your responsibilities:
|
|
5785
6090
|
1. Analyze task complexity: Simple | Moderate | Complex | Highly Complex
|
|
@@ -5795,11 +6100,7 @@ The directory must appear verbatim in every subtask that creates or reads a file
|
|
|
5795
6100
|
NEVER omit the directory prefix when decomposing into subtasks.
|
|
5796
6101
|
|
|
5797
6102
|
Rules:
|
|
5798
|
-
|
|
5799
|
-
- Return ONLY valid JSON \u2014 no other text
|
|
5800
|
-
- If the user asks for a PDF, explicitly use the "pdf_create" tool
|
|
5801
|
-
- If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
|
|
5802
|
-
- Ensure every plan includes explicit creation and verification steps for requested artifacts
|
|
6103
|
+
${rules.filter((r) => r !== false).join("\n")}
|
|
5803
6104
|
|
|
5804
6105
|
DEPENDENCY GUIDANCE:
|
|
5805
6106
|
- Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
|
|
@@ -5811,6 +6112,7 @@ QUALITY RULES:
|
|
|
5811
6112
|
- Each section must have a clear, testable "expectedOutput" so T2 knows when it is done.
|
|
5812
6113
|
- Do NOT create trivial sections that only move files or print summaries \u2014 fold those into adjacent sections.
|
|
5813
6114
|
- If the plan would naturally produce fewer than 2 independent sections, prefer Moderate routing (single T2).`;
|
|
6115
|
+
}
|
|
5814
6116
|
var T1Administrator = class extends BaseTier {
|
|
5815
6117
|
router;
|
|
5816
6118
|
toolRegistry;
|
|
@@ -6138,10 +6440,11 @@ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute f
|
|
|
6138
6440
|
- "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
|
|
6139
6441
|
- "contextBrief": 1-3 short sentences with the ONLY background the worker needs. It sees nothing else about the task, so make the brief self-sufficient \u2014 but never pad it.
|
|
6140
6442
|
- RIGHT-SIZE the plan: use the FEWEST sections and workers that fully cover the task. One section with 1-2 subtasks is the CORRECT plan for a small task; padding a plan with filler sections wastes the user's money.`;
|
|
6443
|
+
const available = new Set(this.toolRegistry.getToolDefinitions().map((t) => t.name));
|
|
6141
6444
|
const messages = [{ role: "user", content: decompositionPrompt }];
|
|
6142
6445
|
const result = await this.router.generate("T1", {
|
|
6143
6446
|
messages,
|
|
6144
|
-
systemPrompt: this.systemPromptOverride +
|
|
6447
|
+
systemPrompt: this.systemPromptOverride + buildT1SystemPrompt((name) => available.has(name)),
|
|
6145
6448
|
maxTokens: 4e3
|
|
6146
6449
|
});
|
|
6147
6450
|
try {
|
|
@@ -6586,16 +6889,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
6586
6889
|
if (typeof input !== "string" || input.length === 0) {
|
|
6587
6890
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
6588
6891
|
}
|
|
6589
|
-
const root =
|
|
6590
|
-
const abs =
|
|
6591
|
-
const rel =
|
|
6592
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6892
|
+
const root = path24.resolve(workspaceRoot);
|
|
6893
|
+
const abs = path24.isAbsolute(input) ? path24.resolve(input) : path24.resolve(root, input);
|
|
6894
|
+
const rel = path24.relative(root, abs);
|
|
6895
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path24.isAbsolute(rel)) {
|
|
6593
6896
|
throw new WorkspaceSandboxError(input, root);
|
|
6594
6897
|
}
|
|
6595
6898
|
try {
|
|
6596
|
-
const real =
|
|
6597
|
-
const realRel =
|
|
6598
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6899
|
+
const real = fs23.realpathSync(abs);
|
|
6900
|
+
const realRel = path24.relative(root, real);
|
|
6901
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path24.isAbsolute(realRel))) {
|
|
6599
6902
|
throw new WorkspaceSandboxError(input, root);
|
|
6600
6903
|
}
|
|
6601
6904
|
} catch (e) {
|
|
@@ -6656,7 +6959,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6656
6959
|
} catch {
|
|
6657
6960
|
}
|
|
6658
6961
|
}
|
|
6659
|
-
await fs4.mkdir(
|
|
6962
|
+
await fs4.mkdir(path24.dirname(absPath), { recursive: true });
|
|
6660
6963
|
await fs4.writeFile(absPath, content, "utf-8");
|
|
6661
6964
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6662
6965
|
}
|
|
@@ -7068,36 +7371,36 @@ var BrowserTool = class extends BaseTool {
|
|
|
7068
7371
|
return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
7069
7372
|
}
|
|
7070
7373
|
}
|
|
7071
|
-
const
|
|
7374
|
+
const page2 = this.page;
|
|
7072
7375
|
try {
|
|
7073
7376
|
switch (action) {
|
|
7074
7377
|
case "navigate": {
|
|
7075
|
-
await
|
|
7076
|
-
const title = await
|
|
7378
|
+
await page2.goto(input["url"], { timeout });
|
|
7379
|
+
const title = await page2.title();
|
|
7077
7380
|
return `Navigated to ${input["url"]} (title: "${title}")`;
|
|
7078
7381
|
}
|
|
7079
7382
|
case "click": {
|
|
7080
|
-
await
|
|
7383
|
+
await page2.click(input["selector"], { timeout });
|
|
7081
7384
|
return `Clicked ${input["selector"]}`;
|
|
7082
7385
|
}
|
|
7083
7386
|
case "fill": {
|
|
7084
|
-
await
|
|
7387
|
+
await page2.fill(input["selector"], input["value"]);
|
|
7085
7388
|
return `Filled ${input["selector"]} with value`;
|
|
7086
7389
|
}
|
|
7087
7390
|
case "screenshot": {
|
|
7088
|
-
const buf = await
|
|
7391
|
+
const buf = await page2.screenshot({ type: "png" });
|
|
7089
7392
|
return `data:image/png;base64,${buf.toString("base64")}`;
|
|
7090
7393
|
}
|
|
7091
7394
|
case "evaluate": {
|
|
7092
|
-
const result = await
|
|
7395
|
+
const result = await page2.evaluate(input["script"]);
|
|
7093
7396
|
return JSON.stringify(result);
|
|
7094
7397
|
}
|
|
7095
7398
|
case "extract_text": {
|
|
7096
|
-
const text = await
|
|
7399
|
+
const text = await page2.locator("body").innerText();
|
|
7097
7400
|
return text.slice(0, 1e4);
|
|
7098
7401
|
}
|
|
7099
7402
|
case "wait": {
|
|
7100
|
-
await
|
|
7403
|
+
await page2.waitForTimeout(timeout);
|
|
7101
7404
|
return `Waited ${timeout}ms`;
|
|
7102
7405
|
}
|
|
7103
7406
|
default:
|
|
@@ -7154,7 +7457,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
7154
7457
|
};
|
|
7155
7458
|
async function fileToImageAttachment(filePath) {
|
|
7156
7459
|
const data = await fs4.readFile(filePath);
|
|
7157
|
-
const ext =
|
|
7460
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
7158
7461
|
const mimeMap = {
|
|
7159
7462
|
".jpg": "image/jpeg",
|
|
7160
7463
|
".jpeg": "image/jpeg",
|
|
@@ -7188,14 +7491,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
7188
7491
|
const filePath = input["path"];
|
|
7189
7492
|
const content = input["content"];
|
|
7190
7493
|
const title = input["title"];
|
|
7191
|
-
const dir =
|
|
7192
|
-
if (!
|
|
7193
|
-
|
|
7494
|
+
const dir = path24.dirname(filePath);
|
|
7495
|
+
if (!fs23.existsSync(dir)) {
|
|
7496
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
7194
7497
|
}
|
|
7195
7498
|
return new Promise((resolve, reject) => {
|
|
7196
7499
|
try {
|
|
7197
7500
|
const doc = new PDFDocument({ margin: 50 });
|
|
7198
|
-
const stream =
|
|
7501
|
+
const stream = fs23.createWriteStream(filePath);
|
|
7199
7502
|
doc.pipe(stream);
|
|
7200
7503
|
if (title) {
|
|
7201
7504
|
doc.info["Title"] = title;
|
|
@@ -7273,22 +7576,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
7273
7576
|
}
|
|
7274
7577
|
cmdPrefix = NODE_CMD;
|
|
7275
7578
|
}
|
|
7276
|
-
const tmpDir =
|
|
7277
|
-
if (!
|
|
7278
|
-
|
|
7579
|
+
const tmpDir = path24.join(this.workspaceRoot, ".cascade", "tmp");
|
|
7580
|
+
if (!fs23.existsSync(tmpDir)) {
|
|
7581
|
+
fs23.mkdirSync(tmpDir, { recursive: true });
|
|
7279
7582
|
}
|
|
7280
7583
|
const extension = language === "python" ? "py" : "js";
|
|
7281
7584
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
7282
|
-
const filePath =
|
|
7283
|
-
|
|
7585
|
+
const filePath = path24.join(tmpDir, fileName);
|
|
7586
|
+
fs23.writeFileSync(filePath, code, "utf-8");
|
|
7284
7587
|
const execArgs = [filePath, ...args];
|
|
7285
7588
|
return new Promise((resolve) => {
|
|
7286
7589
|
const startMs = Date.now();
|
|
7287
7590
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
7288
7591
|
const duration = Date.now() - startMs;
|
|
7289
7592
|
try {
|
|
7290
|
-
if (
|
|
7291
|
-
|
|
7593
|
+
if (fs23.existsSync(filePath)) {
|
|
7594
|
+
fs23.unlinkSync(filePath);
|
|
7292
7595
|
}
|
|
7293
7596
|
} catch (cleanupErr) {
|
|
7294
7597
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -7595,7 +7898,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7595
7898
|
};
|
|
7596
7899
|
async execute(input, _options) {
|
|
7597
7900
|
const pattern = input["pattern"];
|
|
7598
|
-
const searchPath = input["path"] ?
|
|
7901
|
+
const searchPath = input["path"] ? path24.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7599
7902
|
const matches = await glob(pattern, {
|
|
7600
7903
|
cwd: searchPath,
|
|
7601
7904
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -7608,7 +7911,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7608
7911
|
const withMtime = await Promise.all(
|
|
7609
7912
|
matches.map(async (rel) => {
|
|
7610
7913
|
try {
|
|
7611
|
-
const stat = await fs4.stat(
|
|
7914
|
+
const stat = await fs4.stat(path24.join(searchPath, rel));
|
|
7612
7915
|
return { rel, mtime: stat.mtimeMs };
|
|
7613
7916
|
} catch {
|
|
7614
7917
|
return { rel, mtime: 0 };
|
|
@@ -7657,7 +7960,7 @@ var GrepTool = class extends BaseTool {
|
|
|
7657
7960
|
};
|
|
7658
7961
|
async execute(input, _options) {
|
|
7659
7962
|
const pattern = input["pattern"];
|
|
7660
|
-
const searchPath = input["path"] ?
|
|
7963
|
+
const searchPath = input["path"] ? path24.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7661
7964
|
const globPattern = input["glob"];
|
|
7662
7965
|
const outputMode = input["output_mode"] ?? "content";
|
|
7663
7966
|
const context = input["context"] ?? 0;
|
|
@@ -7711,12 +8014,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7711
8014
|
nodir: true
|
|
7712
8015
|
});
|
|
7713
8016
|
} catch {
|
|
7714
|
-
files = [
|
|
8017
|
+
files = [path24.relative(searchPath, searchPath) || "."];
|
|
7715
8018
|
}
|
|
7716
8019
|
const results = [];
|
|
7717
8020
|
let totalCount = 0;
|
|
7718
8021
|
for (const rel of files) {
|
|
7719
|
-
const abs =
|
|
8022
|
+
const abs = path24.join(searchPath, rel);
|
|
7720
8023
|
let content;
|
|
7721
8024
|
try {
|
|
7722
8025
|
content = await fs4.readFile(abs, "utf-8");
|
|
@@ -8081,13 +8384,16 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
8081
8384
|
}
|
|
8082
8385
|
isIgnored(filePath) {
|
|
8083
8386
|
if (!filePath) return false;
|
|
8084
|
-
const abs =
|
|
8085
|
-
const rel =
|
|
8086
|
-
if (!rel || rel.startsWith("..") ||
|
|
8087
|
-
const posixRel = rel.split(
|
|
8387
|
+
const abs = path24.resolve(this.workspaceRoot, filePath);
|
|
8388
|
+
const rel = path24.relative(this.workspaceRoot, abs);
|
|
8389
|
+
if (!rel || rel.startsWith("..") || path24.isAbsolute(rel)) return true;
|
|
8390
|
+
const posixRel = rel.split(path24.sep).join("/");
|
|
8088
8391
|
return this.ignoreMatcher.ignores(posixRel);
|
|
8089
8392
|
}
|
|
8090
8393
|
};
|
|
8394
|
+
function isRemoteMcpServer(server) {
|
|
8395
|
+
return typeof server.url === "string" && server.url.length > 0;
|
|
8396
|
+
}
|
|
8091
8397
|
var McpClient = class _McpClient {
|
|
8092
8398
|
static activeProcessPids = /* @__PURE__ */ new Set();
|
|
8093
8399
|
/**
|
|
@@ -8114,7 +8420,8 @@ var McpClient = class _McpClient {
|
|
|
8114
8420
|
this.approvalCallback = options.approvalCallback;
|
|
8115
8421
|
this.onWarn = options.onWarn ?? ((message) => console.warn(message));
|
|
8116
8422
|
}
|
|
8117
|
-
async connect(server) {
|
|
8423
|
+
async connect(server, opts = {}) {
|
|
8424
|
+
const remote = isRemoteMcpServer(server);
|
|
8118
8425
|
if (!this.trustedServers.has(server.name)) {
|
|
8119
8426
|
const approved = this.approvalCallback ? await this.approvalCallback(server) : false;
|
|
8120
8427
|
if (!approved) {
|
|
@@ -8124,19 +8431,29 @@ var McpClient = class _McpClient {
|
|
|
8124
8431
|
}
|
|
8125
8432
|
this.trustedServers.add(server.name);
|
|
8126
8433
|
}
|
|
8127
|
-
const transport = new StdioClientTransport({
|
|
8128
|
-
command: server.command,
|
|
8129
|
-
args: server.args ?? [],
|
|
8130
|
-
env: { ...process.env, ...server.env ?? {} }
|
|
8131
|
-
});
|
|
8132
8434
|
const client = new Client(
|
|
8133
8435
|
{ name: "cascade-ai", version: "0.1.0" },
|
|
8134
8436
|
{ capabilities: {} }
|
|
8135
8437
|
);
|
|
8136
|
-
|
|
8438
|
+
let transport;
|
|
8439
|
+
if (remote) {
|
|
8440
|
+
transport = await this.connectRemote(server, client, opts.authProvider);
|
|
8441
|
+
} else {
|
|
8442
|
+
if (!server.command) {
|
|
8443
|
+
throw new Error(`MCP server "${server.name}" has neither a url nor a command.`);
|
|
8444
|
+
}
|
|
8445
|
+
transport = new StdioClientTransport({
|
|
8446
|
+
command: server.command,
|
|
8447
|
+
args: server.args ?? [],
|
|
8448
|
+
env: { ...process.env, ...server.env ?? {} }
|
|
8449
|
+
});
|
|
8450
|
+
await client.connect(transport);
|
|
8451
|
+
}
|
|
8137
8452
|
this.clients.set(server.name, client);
|
|
8138
8453
|
this.transports.set(server.name, transport);
|
|
8139
|
-
if (transport
|
|
8454
|
+
if (transport instanceof StdioClientTransport && transport.pid) {
|
|
8455
|
+
_McpClient.activeProcessPids.add(transport.pid);
|
|
8456
|
+
}
|
|
8140
8457
|
const toolsResult = await client.listTools();
|
|
8141
8458
|
for (const tool of toolsResult.tools) {
|
|
8142
8459
|
for (const existing of this.tools.values()) {
|
|
@@ -8155,11 +8472,37 @@ var McpClient = class _McpClient {
|
|
|
8155
8472
|
});
|
|
8156
8473
|
}
|
|
8157
8474
|
}
|
|
8475
|
+
/**
|
|
8476
|
+
* Connect to a remote MCP server. Tries Streamable HTTP first (the current
|
|
8477
|
+
* transport); if that fails (a legacy server that only speaks SSE), retries
|
|
8478
|
+
* once over SSE. Auth headers ride on every request. Returns the live
|
|
8479
|
+
* transport so the caller can register it.
|
|
8480
|
+
*/
|
|
8481
|
+
async connectRemote(server, client, authProvider) {
|
|
8482
|
+
const url = new URL(server.url);
|
|
8483
|
+
const headers = server.headers && Object.keys(server.headers).length ? server.headers : void 0;
|
|
8484
|
+
const opts = {
|
|
8485
|
+
...authProvider ? { authProvider } : {},
|
|
8486
|
+
...headers ? { requestInit: { headers } } : {}
|
|
8487
|
+
};
|
|
8488
|
+
try {
|
|
8489
|
+
const http4 = new StreamableHTTPClientTransport(url, opts);
|
|
8490
|
+
await client.connect(http4);
|
|
8491
|
+
return http4;
|
|
8492
|
+
} catch (httpErr) {
|
|
8493
|
+
this.onWarn(`[mcp] Streamable HTTP failed for "${server.name}", falling back to SSE: ${String(httpErr)}`);
|
|
8494
|
+
const sse = new SSEClientTransport(url, opts);
|
|
8495
|
+
await client.connect(sse);
|
|
8496
|
+
return sse;
|
|
8497
|
+
}
|
|
8498
|
+
}
|
|
8158
8499
|
async disconnect(serverName) {
|
|
8159
8500
|
const client = this.clients.get(serverName);
|
|
8160
8501
|
if (client) {
|
|
8161
8502
|
const transport = this.transports.get(serverName);
|
|
8162
|
-
if (transport
|
|
8503
|
+
if (transport instanceof StdioClientTransport && transport.pid) {
|
|
8504
|
+
_McpClient.activeProcessPids.delete(transport.pid);
|
|
8505
|
+
}
|
|
8163
8506
|
await client.close();
|
|
8164
8507
|
this.clients.delete(serverName);
|
|
8165
8508
|
this.transports.delete(serverName);
|
|
@@ -8193,7 +8536,7 @@ var McpClient = class _McpClient {
|
|
|
8193
8536
|
getActivePids() {
|
|
8194
8537
|
const pids = [];
|
|
8195
8538
|
for (const transport of this.transports.values()) {
|
|
8196
|
-
if (transport.pid) pids.push(transport.pid);
|
|
8539
|
+
if (transport instanceof StdioClientTransport && transport.pid) pids.push(transport.pid);
|
|
8197
8540
|
}
|
|
8198
8541
|
return pids;
|
|
8199
8542
|
}
|
|
@@ -8201,6 +8544,277 @@ var McpClient = class _McpClient {
|
|
|
8201
8544
|
return this.clients.has(serverName);
|
|
8202
8545
|
}
|
|
8203
8546
|
};
|
|
8547
|
+
var McpOAuthProvider = class {
|
|
8548
|
+
constructor(opts) {
|
|
8549
|
+
this.opts = opts;
|
|
8550
|
+
this._state = opts.state ?? randomBytes(16).toString("hex");
|
|
8551
|
+
}
|
|
8552
|
+
opts;
|
|
8553
|
+
cache;
|
|
8554
|
+
_state;
|
|
8555
|
+
/** The `state` this provider uses — callers validate it on the callback. */
|
|
8556
|
+
get oauthState() {
|
|
8557
|
+
return this._state;
|
|
8558
|
+
}
|
|
8559
|
+
async loaded() {
|
|
8560
|
+
if (!this.cache) this.cache = await this.opts.store.load() ?? {};
|
|
8561
|
+
return this.cache;
|
|
8562
|
+
}
|
|
8563
|
+
async persist() {
|
|
8564
|
+
await this.opts.store.save(this.cache ?? {});
|
|
8565
|
+
}
|
|
8566
|
+
get redirectUrl() {
|
|
8567
|
+
return this.opts.redirectUrl;
|
|
8568
|
+
}
|
|
8569
|
+
get clientMetadata() {
|
|
8570
|
+
return {
|
|
8571
|
+
redirect_uris: [this.opts.redirectUrl],
|
|
8572
|
+
client_name: this.opts.clientName ?? "Cascade AI",
|
|
8573
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
8574
|
+
response_types: ["code"],
|
|
8575
|
+
token_endpoint_auth_method: "none",
|
|
8576
|
+
// public client — PKCE, no secret
|
|
8577
|
+
...this.opts.scope ? { scope: this.opts.scope } : {}
|
|
8578
|
+
};
|
|
8579
|
+
}
|
|
8580
|
+
state() {
|
|
8581
|
+
return this._state;
|
|
8582
|
+
}
|
|
8583
|
+
async clientInformation() {
|
|
8584
|
+
return (await this.loaded()).clientInformation;
|
|
8585
|
+
}
|
|
8586
|
+
async saveClientInformation(info) {
|
|
8587
|
+
(await this.loaded()).clientInformation = info;
|
|
8588
|
+
await this.persist();
|
|
8589
|
+
}
|
|
8590
|
+
async tokens() {
|
|
8591
|
+
return (await this.loaded()).tokens;
|
|
8592
|
+
}
|
|
8593
|
+
async saveTokens(tokens) {
|
|
8594
|
+
(await this.loaded()).tokens = tokens;
|
|
8595
|
+
await this.persist();
|
|
8596
|
+
}
|
|
8597
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
8598
|
+
await this.opts.redirect(authorizationUrl);
|
|
8599
|
+
}
|
|
8600
|
+
async saveCodeVerifier(codeVerifier) {
|
|
8601
|
+
(await this.loaded()).codeVerifier = codeVerifier;
|
|
8602
|
+
await this.persist();
|
|
8603
|
+
}
|
|
8604
|
+
async codeVerifier() {
|
|
8605
|
+
const v = (await this.loaded()).codeVerifier;
|
|
8606
|
+
if (!v) throw new Error("No PKCE verifier saved for this MCP OAuth flow.");
|
|
8607
|
+
return v;
|
|
8608
|
+
}
|
|
8609
|
+
async invalidateCredentials(scope) {
|
|
8610
|
+
const s = await this.loaded();
|
|
8611
|
+
if (scope === "all" || scope === "client") s.clientInformation = void 0;
|
|
8612
|
+
if (scope === "all" || scope === "tokens") s.tokens = void 0;
|
|
8613
|
+
if (scope === "all" || scope === "verifier") s.codeVerifier = void 0;
|
|
8614
|
+
await this.persist();
|
|
8615
|
+
}
|
|
8616
|
+
};
|
|
8617
|
+
var FileMcpOAuthStore = class {
|
|
8618
|
+
constructor(filePath) {
|
|
8619
|
+
this.filePath = filePath;
|
|
8620
|
+
}
|
|
8621
|
+
filePath;
|
|
8622
|
+
load() {
|
|
8623
|
+
try {
|
|
8624
|
+
return JSON.parse(fs23.readFileSync(this.filePath, "utf-8"));
|
|
8625
|
+
} catch {
|
|
8626
|
+
return void 0;
|
|
8627
|
+
}
|
|
8628
|
+
}
|
|
8629
|
+
save(state) {
|
|
8630
|
+
fs23.mkdirSync(path24.dirname(this.filePath), { recursive: true, mode: 448 });
|
|
8631
|
+
fs23.writeFileSync(this.filePath, JSON.stringify(state, null, 2), { mode: 384 });
|
|
8632
|
+
try {
|
|
8633
|
+
fs23.chmodSync(this.filePath, 384);
|
|
8634
|
+
} catch {
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
clear() {
|
|
8638
|
+
try {
|
|
8639
|
+
fs23.rmSync(this.filePath);
|
|
8640
|
+
} catch {
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
};
|
|
8644
|
+
function fileOAuthProvider(storePath) {
|
|
8645
|
+
return new McpOAuthProvider({
|
|
8646
|
+
redirectUrl: "http://127.0.0.1/callback",
|
|
8647
|
+
// unused by refresh
|
|
8648
|
+
store: new FileMcpOAuthStore(storePath),
|
|
8649
|
+
redirect: () => {
|
|
8650
|
+
throw new Error("This MCP server needs re-authorization \u2014 run `cascade mcp connect` again.");
|
|
8651
|
+
}
|
|
8652
|
+
});
|
|
8653
|
+
}
|
|
8654
|
+
function beginMcpOAuth(provider, serverUrl) {
|
|
8655
|
+
return auth(provider, { serverUrl });
|
|
8656
|
+
}
|
|
8657
|
+
async function completeMcpOAuth(provider, serverUrl, authorizationCode) {
|
|
8658
|
+
await auth(provider, { serverUrl, authorizationCode });
|
|
8659
|
+
}
|
|
8660
|
+
async function discoverMcpAuthServer(serverUrl) {
|
|
8661
|
+
try {
|
|
8662
|
+
const prm = await discoverOAuthProtectedResourceMetadata(serverUrl);
|
|
8663
|
+
return prm.authorization_servers?.[0];
|
|
8664
|
+
} catch {
|
|
8665
|
+
return void 0;
|
|
8666
|
+
}
|
|
8667
|
+
}
|
|
8668
|
+
async function refreshMcpToken(input) {
|
|
8669
|
+
const metadata = await discoverAuthorizationServerMetadata(input.authorizationServerUrl);
|
|
8670
|
+
return refreshAuthorization(input.authorizationServerUrl, {
|
|
8671
|
+
metadata,
|
|
8672
|
+
clientInformation: input.clientInformation,
|
|
8673
|
+
refreshToken: input.refreshToken
|
|
8674
|
+
});
|
|
8675
|
+
}
|
|
8676
|
+
async function connectMcpWithLoopbackOAuth(opts) {
|
|
8677
|
+
const listener = await startCallbackListener(opts);
|
|
8678
|
+
const transport = new StreamableHTTPClientTransport(new URL(opts.serverUrl), {
|
|
8679
|
+
authProvider: new McpOAuthProvider({
|
|
8680
|
+
redirectUrl: listener.redirectUri,
|
|
8681
|
+
store: opts.store,
|
|
8682
|
+
redirect: (url) => opts.openUrl(url.toString()),
|
|
8683
|
+
clientName: opts.clientName,
|
|
8684
|
+
scope: opts.scope
|
|
8685
|
+
})
|
|
8686
|
+
});
|
|
8687
|
+
const client = new Client({ name: "cascade-ai", version: "0.1.0" }, { capabilities: {} });
|
|
8688
|
+
try {
|
|
8689
|
+
try {
|
|
8690
|
+
await client.connect(transport);
|
|
8691
|
+
const state2 = await opts.store.load();
|
|
8692
|
+
if (!state2?.tokens) throw new Error("MCP server did not require authorization.");
|
|
8693
|
+
return { tokens: state2.tokens, clientInformation: state2.clientInformation };
|
|
8694
|
+
} catch (err) {
|
|
8695
|
+
if (!(err instanceof UnauthorizedError)) throw err;
|
|
8696
|
+
}
|
|
8697
|
+
const code = await listener.waitForCode;
|
|
8698
|
+
await transport.finishAuth(code);
|
|
8699
|
+
const state = await opts.store.load();
|
|
8700
|
+
if (!state?.tokens) throw new Error("Authorization completed but no tokens were issued.");
|
|
8701
|
+
return { tokens: state.tokens, clientInformation: state.clientInformation };
|
|
8702
|
+
} finally {
|
|
8703
|
+
listener.close();
|
|
8704
|
+
await transport.close().catch(() => {
|
|
8705
|
+
});
|
|
8706
|
+
}
|
|
8707
|
+
}
|
|
8708
|
+
function startCallbackListener(opts) {
|
|
8709
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
8710
|
+
let resolveCode;
|
|
8711
|
+
let rejectCode;
|
|
8712
|
+
const waitForCode = new Promise((res, rej) => {
|
|
8713
|
+
resolveCode = res;
|
|
8714
|
+
rejectCode = rej;
|
|
8715
|
+
});
|
|
8716
|
+
let server;
|
|
8717
|
+
const close = () => {
|
|
8718
|
+
try {
|
|
8719
|
+
server?.close();
|
|
8720
|
+
} catch {
|
|
8721
|
+
}
|
|
8722
|
+
};
|
|
8723
|
+
const timer = setTimeout(() => {
|
|
8724
|
+
rejectCode(new Error("Authorization timed out."));
|
|
8725
|
+
close();
|
|
8726
|
+
}, opts.timeoutMs ?? 5 * 6e4);
|
|
8727
|
+
timer.unref?.();
|
|
8728
|
+
const settle = (code) => {
|
|
8729
|
+
clearTimeout(timer);
|
|
8730
|
+
resolveCode(code);
|
|
8731
|
+
};
|
|
8732
|
+
const fail = (err) => {
|
|
8733
|
+
clearTimeout(timer);
|
|
8734
|
+
rejectCode(err);
|
|
8735
|
+
};
|
|
8736
|
+
server = http.createServer((req, res) => {
|
|
8737
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
8738
|
+
if (reqUrl.pathname !== "/callback") {
|
|
8739
|
+
res.writeHead(404).end();
|
|
8740
|
+
return;
|
|
8741
|
+
}
|
|
8742
|
+
const code = reqUrl.searchParams.get("code");
|
|
8743
|
+
const err = reqUrl.searchParams.get("error");
|
|
8744
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
8745
|
+
if (err || !code) {
|
|
8746
|
+
res.end(page("Authorization failed", "You can close this tab and try again."));
|
|
8747
|
+
fail(new Error(err ? `Authorization failed: ${err}` : "No authorization code returned."));
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8750
|
+
res.end(page("Connected", "You can close this tab and return to Cascade."));
|
|
8751
|
+
settle(code);
|
|
8752
|
+
});
|
|
8753
|
+
opts.signal?.addEventListener("abort", () => {
|
|
8754
|
+
fail(new Error("Authorization cancelled."));
|
|
8755
|
+
close();
|
|
8756
|
+
}, { once: true });
|
|
8757
|
+
server.on("error", (e) => {
|
|
8758
|
+
clearTimeout(timer);
|
|
8759
|
+
rejectListener(e);
|
|
8760
|
+
});
|
|
8761
|
+
server.listen(0, "127.0.0.1", () => {
|
|
8762
|
+
const port = server.address().port;
|
|
8763
|
+
resolveListener({ redirectUri: `http://127.0.0.1:${port}/callback`, waitForCode, close });
|
|
8764
|
+
});
|
|
8765
|
+
});
|
|
8766
|
+
}
|
|
8767
|
+
function page(title, body) {
|
|
8768
|
+
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} \xB7 Cascade</title><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0d12;color:#e6e8ee"><div style="text-align:center;padding:40px"><div style="font-size:15px;font-weight:600;margin-bottom:8px">${title}</div><div style="font-size:13px;color:#9aa3b2">${body}</div></div></body>`;
|
|
8769
|
+
}
|
|
8770
|
+
|
|
8771
|
+
// src/core/knowledge/session-memory.ts
|
|
8772
|
+
function sessionWorthRemembering(history, latestPrompt, output) {
|
|
8773
|
+
const turns = history.length + (latestPrompt ? 1 : 0);
|
|
8774
|
+
const historyChars = history.reduce((n, m) => n + messageText(m.content).trim().length, 0);
|
|
8775
|
+
const substance = historyChars + (output?.trim().length ?? 0) + (latestPrompt?.trim().length ?? 0);
|
|
8776
|
+
return turns >= 2 && substance >= 200;
|
|
8777
|
+
}
|
|
8778
|
+
function messageText(content) {
|
|
8779
|
+
if (typeof content === "string") return content;
|
|
8780
|
+
return content.map((block) => block.type === "text" ? block.text : block.type === "image" ? "[image]" : "").join(" ");
|
|
8781
|
+
}
|
|
8782
|
+
function buildSessionTranscript(history, latestPrompt, output, maxChars = 6e3) {
|
|
8783
|
+
const lines = [
|
|
8784
|
+
...history.slice(-12).map((m) => `${m.role}: ${messageText(m.content)}`),
|
|
8785
|
+
`user: ${latestPrompt}`,
|
|
8786
|
+
`assistant: ${output}`
|
|
8787
|
+
];
|
|
8788
|
+
const joined = lines.join("\n");
|
|
8789
|
+
return joined.length > maxChars ? joined.slice(joined.length - maxChars) : joined;
|
|
8790
|
+
}
|
|
8791
|
+
async function distillSessionFacts(transcript, generate) {
|
|
8792
|
+
const prompt = `From this conversation, extract durable facts worth remembering about the USER and their PROJECT for future sessions.
|
|
8793
|
+
Return ONLY a JSON array of {"entity","relation","value"} triples \u2014 e.g.
|
|
8794
|
+
{"entity":"user","relation":"prefers","value":"TypeScript"} or
|
|
8795
|
+
{"entity":"project","relation":"deploys_to","value":"Railway"}.
|
|
8796
|
+
Capture stable preferences, decisions, and project facts. Ignore transient chit-chat,
|
|
8797
|
+
one-off questions, and anything already obvious. At most 6 triples. If nothing durable, return [].
|
|
8798
|
+
|
|
8799
|
+
Conversation:
|
|
8800
|
+
${transcript}`;
|
|
8801
|
+
try {
|
|
8802
|
+
const raw = await generate(prompt);
|
|
8803
|
+
const match = /\[[\s\S]*\]/.exec(raw);
|
|
8804
|
+
if (!match) return [];
|
|
8805
|
+
const parsed = JSON.parse(match[0]);
|
|
8806
|
+
if (!Array.isArray(parsed)) return [];
|
|
8807
|
+
const out = [];
|
|
8808
|
+
for (const f of parsed.slice(0, 6)) {
|
|
8809
|
+
if (f && typeof f.entity === "string" && typeof f.relation === "string" && typeof f.value === "string" && f.entity.trim() && f.relation.trim() && f.value.trim()) {
|
|
8810
|
+
out.push({ entity: f.entity.trim(), relation: f.relation.trim(), value: f.value.trim() });
|
|
8811
|
+
}
|
|
8812
|
+
}
|
|
8813
|
+
return out;
|
|
8814
|
+
} catch {
|
|
8815
|
+
return [];
|
|
8816
|
+
}
|
|
8817
|
+
}
|
|
8204
8818
|
|
|
8205
8819
|
// src/core/cascade.ts
|
|
8206
8820
|
init_audit_logger();
|
|
@@ -8390,9 +9004,17 @@ var ModelOverridesSchema = z.object({
|
|
|
8390
9004
|
});
|
|
8391
9005
|
var McpServerConfigSchema = z.object({
|
|
8392
9006
|
name: z.string(),
|
|
8393
|
-
|
|
9007
|
+
// Local (stdio) transport.
|
|
9008
|
+
command: z.string().optional(),
|
|
8394
9009
|
args: z.array(z.string()).optional(),
|
|
8395
|
-
env: z.record(z.string()).optional()
|
|
9010
|
+
env: z.record(z.string()).optional(),
|
|
9011
|
+
// Remote (hosted-safe) transport.
|
|
9012
|
+
url: z.string().url().optional(),
|
|
9013
|
+
headers: z.record(z.string()).optional(),
|
|
9014
|
+
// Path to this server's OAuth token store (set by `cascade mcp connect`).
|
|
9015
|
+
oauthStore: z.string().optional()
|
|
9016
|
+
}).refine((s) => !!s.command || !!s.url, {
|
|
9017
|
+
message: "MCP server needs either a command (local) or a url (remote)."
|
|
8396
9018
|
});
|
|
8397
9019
|
var WebSearchConfigSchema = z.object({
|
|
8398
9020
|
/** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
|
|
@@ -8461,12 +9083,26 @@ var TelemetryConfigSchema = z.object({
|
|
|
8461
9083
|
var MemoryConfigSchema = z.object({
|
|
8462
9084
|
maxSessionMessages: z.number().default(1e3),
|
|
8463
9085
|
autoSummarizeAt: z.number().default(15e4),
|
|
8464
|
-
retentionDays: z.number().default(90)
|
|
9086
|
+
retentionDays: z.number().default(90),
|
|
9087
|
+
/** Opt-in session→knowledge distillation after each run. Default: false. */
|
|
9088
|
+
rememberSessions: z.boolean().default(false)
|
|
8465
9089
|
});
|
|
8466
9090
|
var TierLimitsSchema = z.object({
|
|
8467
9091
|
t1MaxTokens: z.number().optional(),
|
|
8468
9092
|
t2MaxTokens: z.number().optional(),
|
|
8469
|
-
t3MaxTokens: z.number().optional()
|
|
9093
|
+
t3MaxTokens: z.number().optional(),
|
|
9094
|
+
// Per-tier sampling temperature (0–2). Applied only to calls that don't set
|
|
9095
|
+
// their own temperature — internal deterministic calls (classification,
|
|
9096
|
+
// routing) pin temperature: 0 explicitly and are never overridden.
|
|
9097
|
+
t1Temperature: z.number().min(0).max(2).optional(),
|
|
9098
|
+
t2Temperature: z.number().min(0).max(2).optional(),
|
|
9099
|
+
t3Temperature: z.number().min(0).max(2).optional()
|
|
9100
|
+
});
|
|
9101
|
+
var ExtendedContextConfigSchema = z.object({
|
|
9102
|
+
enabled: z.boolean().default(false),
|
|
9103
|
+
// 1 = no headroom past the window (compact-to-fit only); 2–3 = allow chunking
|
|
9104
|
+
// an input up to N× the window before truncating. Bounded to keep cost sane.
|
|
9105
|
+
maxMultiplier: z.number().min(1).max(5).default(2)
|
|
8470
9106
|
});
|
|
8471
9107
|
var BudgetConfigSchema = z.object({
|
|
8472
9108
|
dailyBudgetUsd: z.number().optional(),
|
|
@@ -8500,6 +9136,7 @@ var CascadeConfigSchema = z.object({
|
|
|
8500
9136
|
telemetry: TelemetryConfigSchema.default({}),
|
|
8501
9137
|
memory: MemoryConfigSchema.default({}),
|
|
8502
9138
|
tierLimits: TierLimitsSchema.default({}),
|
|
9139
|
+
extendedContext: ExtendedContextConfigSchema.default({}),
|
|
8503
9140
|
budget: BudgetConfigSchema.default({}),
|
|
8504
9141
|
theme: z.string().default("cascade"),
|
|
8505
9142
|
workspace: WorkspaceConfigSchema.default({}),
|
|
@@ -8536,7 +9173,9 @@ var CascadeConfigSchema = z.object({
|
|
|
8536
9173
|
*/
|
|
8537
9174
|
sourceUrl: z.string().url().optional(),
|
|
8538
9175
|
/** Fetch current per-token prices from OpenRouter (free, no key). */
|
|
8539
|
-
pricingLive: z.boolean().default(true)
|
|
9176
|
+
pricingLive: z.boolean().default(true),
|
|
9177
|
+
/** Cache location for the fetched snapshot (cloud → persistent volume). */
|
|
9178
|
+
cacheFile: z.string().optional()
|
|
8540
9179
|
}).default({}),
|
|
8541
9180
|
/**
|
|
8542
9181
|
* Runtime Tool Creation: when true, T3 workers can generate and register new tools
|
|
@@ -8555,6 +9194,27 @@ var CascadeConfigSchema = z.object({
|
|
|
8555
9194
|
knowledge: z.object({
|
|
8556
9195
|
factsExtraction: z.boolean().default(true)
|
|
8557
9196
|
}).default({}),
|
|
9197
|
+
/**
|
|
9198
|
+
* Fast answer behaviour. `autoSimple`: pure small talk ("hi", "who are you")
|
|
9199
|
+
* skips the T1/T2/T3 machinery for a direct single-model reply — same cost
|
|
9200
|
+
* shape as the explicit fast-answer toggle, applied automatically.
|
|
9201
|
+
*/
|
|
9202
|
+
fastAnswer: z.object({
|
|
9203
|
+
autoSimple: z.boolean().default(true)
|
|
9204
|
+
}).default({}),
|
|
9205
|
+
/**
|
|
9206
|
+
* Workspace code index (Phase 3). Opt-in. When enabled, Cascade registers a
|
|
9207
|
+
* `code_search` tool backed by a hybrid + reranked index of the workspace so
|
|
9208
|
+
* workers can find code by meaning. Build/refresh the index with
|
|
9209
|
+
* `cascade index`; set autoRefresh to update it at run start (incremental —
|
|
9210
|
+
* only changed files are re-embedded).
|
|
9211
|
+
*/
|
|
9212
|
+
codeIndex: z.object({
|
|
9213
|
+
enabled: z.boolean().default(false),
|
|
9214
|
+
autoRefresh: z.boolean().default(false),
|
|
9215
|
+
/** SQLite file for the index. Defaults to <workspace>/.cascade/code-index.db. */
|
|
9216
|
+
dbPath: z.string().optional()
|
|
9217
|
+
}).default({}),
|
|
8558
9218
|
/**
|
|
8559
9219
|
* Persist runtime-generated tools to .cascade/dynamic-tools.json and reload them
|
|
8560
9220
|
* on startup for cross-run dedup. Reloaded (and peer-received) tools are always
|
|
@@ -8654,7 +9314,16 @@ var CascadeConfigSchema = z.object({
|
|
|
8654
9314
|
paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
|
|
8655
9315
|
}).optional(),
|
|
8656
9316
|
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
8657
|
-
routing: z.object({
|
|
9317
|
+
routing: z.object({
|
|
9318
|
+
forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto"),
|
|
9319
|
+
// Where the model-performance stats live. Defaults to ~/.cascade/model-perf.json
|
|
9320
|
+
// when unset; the cloud points it at the persistent volume so learning survives
|
|
9321
|
+
// redeploys and is shared across users.
|
|
9322
|
+
perfStatsPath: z.string().optional(),
|
|
9323
|
+
// When false, Cascade still READS the shared performance scores to route, but
|
|
9324
|
+
// does NOT record this run's outcomes (opt-out). Default true.
|
|
9325
|
+
learnFromOutcomes: z.boolean().default(true)
|
|
9326
|
+
}).optional(),
|
|
8658
9327
|
/**
|
|
8659
9328
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
8660
9329
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -8871,12 +9540,12 @@ var TaskAnalyzer = class {
|
|
|
8871
9540
|
* Record the outcome of a completed run across all tiers that were selected
|
|
8872
9541
|
* during this session and persist stats to disk.
|
|
8873
9542
|
*/
|
|
8874
|
-
recordRunOutcome(outcome, costByTier) {
|
|
9543
|
+
recordRunOutcome(outcome, costByTier, contextTokens = 0) {
|
|
8875
9544
|
if (!this.tracker || !this.lastProfile) return;
|
|
8876
9545
|
const taskType = this.lastProfile.type;
|
|
8877
9546
|
for (const [tier, model] of this.lastSelectedModels) {
|
|
8878
9547
|
const cost = costByTier[tier] ?? 0;
|
|
8879
|
-
this.tracker.record(model.id, taskType, outcome, 0, cost);
|
|
9548
|
+
this.tracker.record(model.id, taskType, outcome, 0, cost, contextTokens);
|
|
8880
9549
|
}
|
|
8881
9550
|
this.lastSelectedModels.clear();
|
|
8882
9551
|
void this.tracker.save();
|
|
@@ -8927,14 +9596,21 @@ var TaskAnalyzer = class {
|
|
|
8927
9596
|
analysisCache.clear();
|
|
8928
9597
|
}
|
|
8929
9598
|
};
|
|
8930
|
-
var DEFAULT_STATS_FILE =
|
|
9599
|
+
var DEFAULT_STATS_FILE = path24.join(os6.homedir(), ".cascade", "model-perf.json");
|
|
8931
9600
|
var ModelPerformanceTracker = class {
|
|
8932
9601
|
stats = /* @__PURE__ */ new Map();
|
|
8933
9602
|
featureStats = /* @__PURE__ */ new Map();
|
|
8934
9603
|
statsFile;
|
|
9604
|
+
readOnly;
|
|
8935
9605
|
loaded = false;
|
|
8936
|
-
|
|
9606
|
+
/**
|
|
9607
|
+
* @param statsFile where stats persist (cloud → the persistent volume).
|
|
9608
|
+
* @param options.readOnly consume the shared scores but don't record/save this
|
|
9609
|
+
* run's outcomes — the opt-out path for users who declined to contribute.
|
|
9610
|
+
*/
|
|
9611
|
+
constructor(statsFile = DEFAULT_STATS_FILE, options = {}) {
|
|
8937
9612
|
this.statsFile = statsFile;
|
|
9613
|
+
this.readOnly = options.readOnly ?? false;
|
|
8938
9614
|
}
|
|
8939
9615
|
async load() {
|
|
8940
9616
|
if (this.loaded) return;
|
|
@@ -8958,34 +9634,44 @@ var ModelPerformanceTracker = class {
|
|
|
8958
9634
|
}
|
|
8959
9635
|
}
|
|
8960
9636
|
async save() {
|
|
9637
|
+
if (this.readOnly) return;
|
|
8961
9638
|
try {
|
|
8962
|
-
await fs4.mkdir(
|
|
9639
|
+
await fs4.mkdir(path24.dirname(this.statsFile), { recursive: true });
|
|
8963
9640
|
const modelsObj = {};
|
|
8964
9641
|
const featuresObj = {};
|
|
8965
9642
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
8966
9643
|
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
8967
|
-
|
|
9644
|
+
const json = JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2);
|
|
9645
|
+
const tmp = `${this.statsFile}.${process.pid}.${Date.now()}.tmp`;
|
|
9646
|
+
await fs4.writeFile(tmp, json, "utf-8");
|
|
9647
|
+
await fs4.rename(tmp, this.statsFile);
|
|
8968
9648
|
} catch {
|
|
8969
9649
|
}
|
|
8970
9650
|
}
|
|
8971
|
-
record(modelId, taskType, outcome, retries = 0, costUsd = 0) {
|
|
9651
|
+
record(modelId, taskType, outcome, retries = 0, costUsd = 0, contextTokens = 0) {
|
|
9652
|
+
if (this.readOnly) return;
|
|
8972
9653
|
const key = `${modelId}:${taskType}`;
|
|
8973
9654
|
const s = this.stats.get(key) ?? {
|
|
8974
9655
|
successCount: 0,
|
|
8975
9656
|
failureCount: 0,
|
|
8976
9657
|
totalRetries: 0,
|
|
8977
9658
|
totalCostUsd: 0,
|
|
8978
|
-
sampleCount: 0
|
|
9659
|
+
sampleCount: 0,
|
|
9660
|
+
totalContextTokens: 0,
|
|
9661
|
+
failureContextTokens: 0
|
|
8979
9662
|
};
|
|
8980
9663
|
this.stats.set(key, {
|
|
8981
9664
|
successCount: s.successCount + (outcome === "success" ? 1 : 0),
|
|
8982
9665
|
failureCount: s.failureCount + (outcome === "failure" ? 1 : 0),
|
|
8983
9666
|
totalRetries: s.totalRetries + retries,
|
|
8984
9667
|
totalCostUsd: s.totalCostUsd + costUsd,
|
|
8985
|
-
sampleCount: s.sampleCount + 1
|
|
9668
|
+
sampleCount: s.sampleCount + 1,
|
|
9669
|
+
totalContextTokens: (s.totalContextTokens ?? 0) + Math.max(0, contextTokens),
|
|
9670
|
+
failureContextTokens: (s.failureContextTokens ?? 0) + (outcome === "failure" ? Math.max(0, contextTokens) : 0)
|
|
8986
9671
|
});
|
|
8987
9672
|
}
|
|
8988
9673
|
recordFeatureCost(featureTag, costUsd) {
|
|
9674
|
+
if (this.readOnly) return;
|
|
8989
9675
|
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
8990
9676
|
this.featureStats.set(featureTag, {
|
|
8991
9677
|
totalCostUsd: s.totalCostUsd + costUsd,
|
|
@@ -9498,7 +10184,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9498
10184
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
9499
10185
|
async loadPersistedTools() {
|
|
9500
10186
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9501
|
-
const file =
|
|
10187
|
+
const file = path24.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
9502
10188
|
try {
|
|
9503
10189
|
const raw = await fs4.readFile(file, "utf-8");
|
|
9504
10190
|
const specs = JSON.parse(raw);
|
|
@@ -9522,8 +10208,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9522
10208
|
}
|
|
9523
10209
|
async persist() {
|
|
9524
10210
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9525
|
-
const dir =
|
|
9526
|
-
const file =
|
|
10211
|
+
const dir = path24.join(this.workspacePath, ".cascade");
|
|
10212
|
+
const file = path24.join(dir, DYNAMIC_TOOLS_FILE);
|
|
9527
10213
|
try {
|
|
9528
10214
|
await fs4.mkdir(dir, { recursive: true });
|
|
9529
10215
|
await fs4.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -9543,12 +10229,12 @@ var WorldStateDB = class {
|
|
|
9543
10229
|
constructor(workspacePath, debugMode = false) {
|
|
9544
10230
|
this.workspacePath = workspacePath;
|
|
9545
10231
|
this.debugMode = debugMode;
|
|
9546
|
-
const cascadeDir =
|
|
9547
|
-
if (!
|
|
9548
|
-
|
|
10232
|
+
const cascadeDir = path24.join(workspacePath, ".cascade");
|
|
10233
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
10234
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
9549
10235
|
}
|
|
9550
|
-
this.keyPath =
|
|
9551
|
-
this.dbPath =
|
|
10236
|
+
this.keyPath = path24.join(cascadeDir, "world_state.key");
|
|
10237
|
+
this.dbPath = path24.join(cascadeDir, "world_state.db");
|
|
9552
10238
|
this.initEncryptionKey();
|
|
9553
10239
|
this.db = new Database(this.dbPath);
|
|
9554
10240
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -9570,6 +10256,20 @@ var WorldStateDB = class {
|
|
|
9570
10256
|
PRIMARY KEY (entity, relation)
|
|
9571
10257
|
)
|
|
9572
10258
|
`);
|
|
10259
|
+
this.db.exec(`
|
|
10260
|
+
CREATE TABLE IF NOT EXISTS facts_history (
|
|
10261
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10262
|
+
entity TEXT NOT NULL,
|
|
10263
|
+
relation TEXT NOT NULL,
|
|
10264
|
+
encrypted_value TEXT NOT NULL,
|
|
10265
|
+
source_worker TEXT NOT NULL,
|
|
10266
|
+
valid_from TEXT NOT NULL,
|
|
10267
|
+
valid_to TEXT NOT NULL,
|
|
10268
|
+
change TEXT NOT NULL
|
|
10269
|
+
);
|
|
10270
|
+
CREATE INDEX IF NOT EXISTS facts_history_key
|
|
10271
|
+
ON facts_history(entity, relation, valid_to DESC);
|
|
10272
|
+
`);
|
|
9573
10273
|
}
|
|
9574
10274
|
workspacePath;
|
|
9575
10275
|
debugMode;
|
|
@@ -9577,17 +10277,30 @@ var WorldStateDB = class {
|
|
|
9577
10277
|
keyPath;
|
|
9578
10278
|
dbPath;
|
|
9579
10279
|
encryptionKey;
|
|
10280
|
+
/**
|
|
10281
|
+
* Append the CURRENTLY-stored fact for (e, r) to the history ledger before it
|
|
10282
|
+
* is overwritten/deleted, timestamped [its recorded time → now]. No-op when
|
|
10283
|
+
* there is no current row. The value stays encrypted with the same key.
|
|
10284
|
+
*/
|
|
10285
|
+
archiveCurrentFact(e, r, change, validTo) {
|
|
10286
|
+
const row = this.db.prepare("SELECT encrypted_value, source_worker, timestamp FROM facts WHERE entity = ? AND relation = ?").get(e, r);
|
|
10287
|
+
if (!row) return;
|
|
10288
|
+
this.db.prepare(
|
|
10289
|
+
`INSERT INTO facts_history (entity, relation, encrypted_value, source_worker, valid_from, valid_to, change)
|
|
10290
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
10291
|
+
).run(e, r, row.encrypted_value, row.source_worker, row.timestamp, validTo, change);
|
|
10292
|
+
}
|
|
9580
10293
|
initEncryptionKey() {
|
|
9581
|
-
if (
|
|
9582
|
-
this.encryptionKey =
|
|
10294
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
10295
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
9583
10296
|
} else {
|
|
9584
|
-
this.encryptionKey =
|
|
9585
|
-
|
|
10297
|
+
this.encryptionKey = crypto4.randomBytes(32);
|
|
10298
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9586
10299
|
}
|
|
9587
10300
|
}
|
|
9588
10301
|
encrypt(text) {
|
|
9589
|
-
const iv =
|
|
9590
|
-
const cipher =
|
|
10302
|
+
const iv = crypto4.randomBytes(16);
|
|
10303
|
+
const cipher = crypto4.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9591
10304
|
let encrypted = cipher.update(text, "utf8", "hex");
|
|
9592
10305
|
encrypted += cipher.final("hex");
|
|
9593
10306
|
const authTag = cipher.getAuthTag().toString("hex");
|
|
@@ -9599,14 +10312,14 @@ var WorldStateDB = class {
|
|
|
9599
10312
|
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
9600
10313
|
const iv = Buffer.from(ivHex, "hex");
|
|
9601
10314
|
const authTag = Buffer.from(authTagHex, "hex");
|
|
9602
|
-
const decipher =
|
|
10315
|
+
const decipher = crypto4.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
9603
10316
|
decipher.setAuthTag(authTag);
|
|
9604
10317
|
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
9605
10318
|
decrypted += decipher.final("utf8");
|
|
9606
10319
|
return decrypted;
|
|
9607
10320
|
}
|
|
9608
10321
|
addEntry(workerId, summary) {
|
|
9609
|
-
const id =
|
|
10322
|
+
const id = crypto4.randomUUID();
|
|
9610
10323
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
9611
10324
|
const payload = JSON.stringify({ summary });
|
|
9612
10325
|
const encryptedPayload = this.encrypt(payload);
|
|
@@ -9654,8 +10367,9 @@ var WorldStateDB = class {
|
|
|
9654
10367
|
const r = normalizeKey(relation);
|
|
9655
10368
|
const v = value.trim();
|
|
9656
10369
|
if (!e || !r || !v) return;
|
|
10370
|
+
const now = timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9657
10371
|
const encrypted = this.encrypt(JSON.stringify({ value: v }));
|
|
9658
|
-
const
|
|
10372
|
+
const upsert = this.db.prepare(`
|
|
9659
10373
|
INSERT INTO facts (entity, relation, encrypted_value, source_worker, timestamp)
|
|
9660
10374
|
VALUES (?, ?, ?, ?, ?)
|
|
9661
10375
|
ON CONFLICT(entity, relation) DO UPDATE SET
|
|
@@ -9663,7 +10377,20 @@ var WorldStateDB = class {
|
|
|
9663
10377
|
source_worker = excluded.source_worker,
|
|
9664
10378
|
timestamp = excluded.timestamp
|
|
9665
10379
|
`);
|
|
9666
|
-
|
|
10380
|
+
const tx = this.db.transaction(() => {
|
|
10381
|
+
const current = this.db.prepare("SELECT encrypted_value FROM facts WHERE entity = ? AND relation = ?").get(e, r);
|
|
10382
|
+
if (current) {
|
|
10383
|
+
let prevValue = null;
|
|
10384
|
+
try {
|
|
10385
|
+
prevValue = JSON.parse(this.decrypt(current.encrypted_value)).value;
|
|
10386
|
+
} catch {
|
|
10387
|
+
prevValue = null;
|
|
10388
|
+
}
|
|
10389
|
+
if (prevValue !== v) this.archiveCurrentFact(e, r, "update", now);
|
|
10390
|
+
}
|
|
10391
|
+
upsert.run(e, r, encrypted, sourceWorker, now);
|
|
10392
|
+
});
|
|
10393
|
+
tx();
|
|
9667
10394
|
this.dumpDebugIfNeeded();
|
|
9668
10395
|
}
|
|
9669
10396
|
rowToFact(row) {
|
|
@@ -9732,32 +10459,99 @@ var WorldStateDB = class {
|
|
|
9732
10459
|
const e = normalizeKey(entity);
|
|
9733
10460
|
const r = normalizeKey(relation);
|
|
9734
10461
|
if (!e || !r) return false;
|
|
9735
|
-
const
|
|
9736
|
-
|
|
10462
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10463
|
+
const tx = this.db.transaction(() => {
|
|
10464
|
+
this.archiveCurrentFact(e, r, "delete", now);
|
|
10465
|
+
return this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
|
|
10466
|
+
});
|
|
10467
|
+
return tx().changes > 0;
|
|
9737
10468
|
}
|
|
9738
|
-
/** Delete every fact. Returns how many were removed. */
|
|
10469
|
+
/** Delete every fact. Returns how many were removed. History is preserved. */
|
|
9739
10470
|
clearFacts() {
|
|
9740
|
-
|
|
9741
|
-
|
|
9742
|
-
|
|
9743
|
-
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
10471
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10472
|
+
const tx = this.db.transaction(() => {
|
|
10473
|
+
const rows = this.db.prepare("SELECT entity, relation, encrypted_value, source_worker, timestamp FROM facts").all();
|
|
10474
|
+
const archive = this.db.prepare(
|
|
10475
|
+
`INSERT INTO facts_history (entity, relation, encrypted_value, source_worker, valid_from, valid_to, change)
|
|
10476
|
+
VALUES (?, ?, ?, ?, ?, ?, 'clear')`
|
|
10477
|
+
);
|
|
10478
|
+
for (const row of rows) {
|
|
10479
|
+
archive.run(row.entity, row.relation, row.encrypted_value, row.source_worker, row.timestamp, now);
|
|
10480
|
+
}
|
|
10481
|
+
return this.db.prepare("DELETE FROM facts").run().changes;
|
|
10482
|
+
});
|
|
10483
|
+
return tx();
|
|
9749
10484
|
}
|
|
10485
|
+
// ── History + restore (undo) ──────────────────
|
|
9750
10486
|
/**
|
|
9751
|
-
*
|
|
9752
|
-
*
|
|
9753
|
-
*
|
|
9754
|
-
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
10487
|
+
* Prior values for one (entity, relation), newest-first — the durable trail of
|
|
10488
|
+
* what this fact used to be before it was overwritten or deleted. Decrypted
|
|
10489
|
+
* here (the ciphertext never leaves this process).
|
|
9755
10490
|
*/
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
if (
|
|
9760
|
-
|
|
10491
|
+
getFactHistory(entity, relation) {
|
|
10492
|
+
const e = normalizeKey(entity);
|
|
10493
|
+
const r = normalizeKey(relation);
|
|
10494
|
+
if (!e || !r) return [];
|
|
10495
|
+
const rows = this.db.prepare(
|
|
10496
|
+
`SELECT encrypted_value, source_worker, valid_from, valid_to, change
|
|
10497
|
+
FROM facts_history WHERE entity = ? AND relation = ? ORDER BY valid_to DESC, id DESC`
|
|
10498
|
+
).all(e, r);
|
|
10499
|
+
return rows.map((row) => {
|
|
10500
|
+
let value;
|
|
10501
|
+
try {
|
|
10502
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
10503
|
+
} catch {
|
|
10504
|
+
value = "[Decryption Failed]";
|
|
10505
|
+
}
|
|
10506
|
+
return { value, sourceWorker: row.source_worker, validFrom: row.valid_from, validTo: row.valid_to, change: row.change };
|
|
10507
|
+
});
|
|
10508
|
+
}
|
|
10509
|
+
/**
|
|
10510
|
+
* Restore a historical value for (entity, relation) as the current fact. The
|
|
10511
|
+
* value currently in place (if any) is itself archived first, so restore is
|
|
10512
|
+
* undoable. When `validFrom` is given, restores that exact historical entry;
|
|
10513
|
+
* otherwise restores the most recent one. Returns whether anything was restored.
|
|
10514
|
+
*/
|
|
10515
|
+
restoreFact(entity, relation, validFrom) {
|
|
10516
|
+
const e = normalizeKey(entity);
|
|
10517
|
+
const r = normalizeKey(relation);
|
|
10518
|
+
if (!e || !r) return false;
|
|
10519
|
+
const row = validFrom ? this.db.prepare(
|
|
10520
|
+
`SELECT encrypted_value, source_worker FROM facts_history
|
|
10521
|
+
WHERE entity = ? AND relation = ? AND valid_from = ? ORDER BY id DESC LIMIT 1`
|
|
10522
|
+
).get(e, r, validFrom) : this.db.prepare(
|
|
10523
|
+
`SELECT encrypted_value, source_worker FROM facts_history
|
|
10524
|
+
WHERE entity = ? AND relation = ? ORDER BY valid_to DESC, id DESC LIMIT 1`
|
|
10525
|
+
).get(e, r);
|
|
10526
|
+
if (!row) return false;
|
|
10527
|
+
let value;
|
|
10528
|
+
try {
|
|
10529
|
+
value = JSON.parse(this.decrypt(row.encrypted_value)).value;
|
|
10530
|
+
} catch {
|
|
10531
|
+
return false;
|
|
10532
|
+
}
|
|
10533
|
+
this.upsertFact(e, r, value, `${row.source_worker} (restored)`);
|
|
10534
|
+
return true;
|
|
10535
|
+
}
|
|
10536
|
+
// ── Export / Import ──────────────────────────
|
|
10537
|
+
//
|
|
10538
|
+
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
10539
|
+
// JSON file — the encryption key never leaves this machine) and re-encrypts
|
|
10540
|
+
// with the LOCAL key on import.
|
|
10541
|
+
exportKnowledge() {
|
|
10542
|
+
return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
|
|
10543
|
+
}
|
|
10544
|
+
/**
|
|
10545
|
+
* Merge imported knowledge. Facts upsert on (entity, relation) with
|
|
10546
|
+
* newer-timestamp-wins (a local fact newer than the imported one is kept);
|
|
10547
|
+
* log entries append with fresh ids, skipping exact duplicates
|
|
10548
|
+
* (worker + timestamp + summary). Returns counts of what actually landed.
|
|
10549
|
+
*/
|
|
10550
|
+
importKnowledge(data) {
|
|
10551
|
+
let facts = 0;
|
|
10552
|
+
let logEntries = 0;
|
|
10553
|
+
if (Array.isArray(data.facts)) {
|
|
10554
|
+
const local = new Map(this.getAllFacts().map((f) => [`${f.entity}\0${f.relation}`, f.timestamp]));
|
|
9761
10555
|
for (const f of data.facts) {
|
|
9762
10556
|
if (!f || typeof f.entity !== "string" || typeof f.relation !== "string" || typeof f.value !== "string") continue;
|
|
9763
10557
|
const key = `${normalizeKey(f.entity)}\0${normalizeKey(f.relation)}`;
|
|
@@ -9777,7 +10571,7 @@ var WorldStateDB = class {
|
|
|
9777
10571
|
const timestamp = e.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9778
10572
|
const key = `${workerId}\0${timestamp}\0${e.summary}`;
|
|
9779
10573
|
if (seen.has(key)) continue;
|
|
9780
|
-
stmt.run(
|
|
10574
|
+
stmt.run(crypto4.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e.summary })));
|
|
9781
10575
|
seen.add(key);
|
|
9782
10576
|
logEntries++;
|
|
9783
10577
|
}
|
|
@@ -9787,9 +10581,9 @@ var WorldStateDB = class {
|
|
|
9787
10581
|
dumpDebugIfNeeded() {
|
|
9788
10582
|
if (!this.debugMode) return;
|
|
9789
10583
|
try {
|
|
9790
|
-
const dumpPath =
|
|
10584
|
+
const dumpPath = path24.join(os6.tmpdir(), "cascade_world_state_debug.json");
|
|
9791
10585
|
const entries = this.getAllEntries();
|
|
9792
|
-
|
|
10586
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9793
10587
|
} catch (err) {
|
|
9794
10588
|
console.error("Failed to dump debug world state", err);
|
|
9795
10589
|
}
|
|
@@ -9826,6 +10620,894 @@ var PrivacyPaths = class {
|
|
|
9826
10620
|
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
9827
10621
|
}
|
|
9828
10622
|
};
|
|
10623
|
+
var ignore3 = ignoreFactory.default ?? ignoreFactory;
|
|
10624
|
+
var CascadeIgnore = class {
|
|
10625
|
+
ig;
|
|
10626
|
+
loaded = false;
|
|
10627
|
+
constructor() {
|
|
10628
|
+
this.ig = ignore3();
|
|
10629
|
+
this.ig.add([
|
|
10630
|
+
".cascade/keystore.enc",
|
|
10631
|
+
".cascade/memory.db",
|
|
10632
|
+
".env",
|
|
10633
|
+
".env.*",
|
|
10634
|
+
"*.pem",
|
|
10635
|
+
"*.key",
|
|
10636
|
+
"id_rsa",
|
|
10637
|
+
"id_ed25519"
|
|
10638
|
+
]);
|
|
10639
|
+
}
|
|
10640
|
+
async load(workspacePath) {
|
|
10641
|
+
const filePath = path24.join(workspacePath, ".cascadeignore");
|
|
10642
|
+
try {
|
|
10643
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
10644
|
+
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
10645
|
+
this.ig.add(lines);
|
|
10646
|
+
this.loaded = true;
|
|
10647
|
+
} catch {
|
|
10648
|
+
}
|
|
10649
|
+
}
|
|
10650
|
+
isIgnored(filePath, workspacePath) {
|
|
10651
|
+
try {
|
|
10652
|
+
const relative = workspacePath ? path24.relative(workspacePath, filePath) : filePath;
|
|
10653
|
+
return this.ig.ignores(relative);
|
|
10654
|
+
} catch {
|
|
10655
|
+
return false;
|
|
10656
|
+
}
|
|
10657
|
+
}
|
|
10658
|
+
getPatterns() {
|
|
10659
|
+
return this.ig._rules?.map((r) => r.pattern) ?? [];
|
|
10660
|
+
}
|
|
10661
|
+
};
|
|
10662
|
+
|
|
10663
|
+
// src/retrieval/retriever.ts
|
|
10664
|
+
function reciprocalRankFusion(lists, rrfK = 60) {
|
|
10665
|
+
const scores = /* @__PURE__ */ new Map();
|
|
10666
|
+
const items = /* @__PURE__ */ new Map();
|
|
10667
|
+
for (const list of lists) {
|
|
10668
|
+
list.forEach((item, rank3) => {
|
|
10669
|
+
scores.set(item.id, (scores.get(item.id) ?? 0) + 1 / (rrfK + rank3));
|
|
10670
|
+
if (!items.has(item.id)) items.set(item.id, item);
|
|
10671
|
+
});
|
|
10672
|
+
}
|
|
10673
|
+
return [...items.values()].map((item) => ({ ...item, score: scores.get(item.id) ?? 0 })).sort((a, b) => b.score - a.score);
|
|
10674
|
+
}
|
|
10675
|
+
var Retriever = class {
|
|
10676
|
+
constructor(embedder, store, reranker) {
|
|
10677
|
+
this.embedder = embedder;
|
|
10678
|
+
this.store = store;
|
|
10679
|
+
this.reranker = reranker;
|
|
10680
|
+
}
|
|
10681
|
+
embedder;
|
|
10682
|
+
store;
|
|
10683
|
+
reranker;
|
|
10684
|
+
/** Chunk model this retriever embeds with (for cache/version keys). */
|
|
10685
|
+
get embedModel() {
|
|
10686
|
+
return this.embedder.model;
|
|
10687
|
+
}
|
|
10688
|
+
/** True when this source is already indexed under the current embed model. */
|
|
10689
|
+
isIndexed(namespace, sourceId) {
|
|
10690
|
+
return this.store.hasSource(namespace, sourceId, this.embedder.model);
|
|
10691
|
+
}
|
|
10692
|
+
/**
|
|
10693
|
+
* Embed and store a source's chunks. Skips work when the source is already
|
|
10694
|
+
* indexed under the current embed model (so re-runs don't re-embed). Returns
|
|
10695
|
+
* the number of chunks newly indexed.
|
|
10696
|
+
*/
|
|
10697
|
+
async index(namespace, sourceId, chunks) {
|
|
10698
|
+
if (chunks.length === 0 || this.isIndexed(namespace, sourceId)) return 0;
|
|
10699
|
+
const vectors = await this.embedder.embed(chunks.map((c) => c.text));
|
|
10700
|
+
const records = chunks.map((c, i) => ({
|
|
10701
|
+
chunk: { id: `${namespace}:${sourceId}:${c.ord}`, text: c.text, sourceId, ord: c.ord, meta: { namespace } },
|
|
10702
|
+
vector: vectors[i] ?? []
|
|
10703
|
+
})).filter((r) => r.vector.length > 0);
|
|
10704
|
+
this.store.upsert(records, this.embedder.model);
|
|
10705
|
+
return records.length;
|
|
10706
|
+
}
|
|
10707
|
+
/** Hybrid search: lexical ∪ dense, fused with RRF, then optionally reranked. */
|
|
10708
|
+
async search(query, opts) {
|
|
10709
|
+
const candidates = opts.candidates ?? 30;
|
|
10710
|
+
const k = opts.k ?? 6;
|
|
10711
|
+
const base = { namespace: opts.namespace, k: candidates, sourceIds: opts.sourceIds };
|
|
10712
|
+
const lexical = this.store.lexicalSearch(query, base);
|
|
10713
|
+
let dense = [];
|
|
10714
|
+
try {
|
|
10715
|
+
const [qvec] = await this.embedder.embed([query]);
|
|
10716
|
+
if (qvec && qvec.length) dense = this.store.denseSearch(qvec, base);
|
|
10717
|
+
} catch {
|
|
10718
|
+
}
|
|
10719
|
+
const fused = reciprocalRankFusion([lexical, dense], opts.rrfK ?? 60);
|
|
10720
|
+
if (this.reranker && opts.rerank !== false && fused.length > 1) {
|
|
10721
|
+
return this.reranker.rerank(query, fused, k);
|
|
10722
|
+
}
|
|
10723
|
+
return fused.slice(0, k);
|
|
10724
|
+
}
|
|
10725
|
+
};
|
|
10726
|
+
|
|
10727
|
+
// src/retrieval/sqlite-store.ts
|
|
10728
|
+
function normalize(vec) {
|
|
10729
|
+
const f = new Float32Array(vec);
|
|
10730
|
+
let sum = 0;
|
|
10731
|
+
for (let i = 0; i < f.length; i++) sum += f[i] * f[i];
|
|
10732
|
+
const norm = Math.sqrt(sum) || 1;
|
|
10733
|
+
for (let i = 0; i < f.length; i++) f[i] /= norm;
|
|
10734
|
+
return f;
|
|
10735
|
+
}
|
|
10736
|
+
function toBlob(f) {
|
|
10737
|
+
return Buffer.from(f.buffer, f.byteOffset, f.byteLength);
|
|
10738
|
+
}
|
|
10739
|
+
function fromBlob(buf) {
|
|
10740
|
+
return new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
10741
|
+
}
|
|
10742
|
+
function ftsMatchExpr(query) {
|
|
10743
|
+
const terms = (query.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length > 1).slice(0, 24);
|
|
10744
|
+
if (terms.length === 0) return null;
|
|
10745
|
+
return terms.map((t) => `"${t}"`).join(" OR ");
|
|
10746
|
+
}
|
|
10747
|
+
var SqliteVectorStore = class {
|
|
10748
|
+
constructor(db) {
|
|
10749
|
+
this.db = db;
|
|
10750
|
+
this.migrate();
|
|
10751
|
+
}
|
|
10752
|
+
db;
|
|
10753
|
+
migrate() {
|
|
10754
|
+
this.db.exec(`
|
|
10755
|
+
CREATE TABLE IF NOT EXISTS kb_chunks (
|
|
10756
|
+
chunk_id TEXT PRIMARY KEY,
|
|
10757
|
+
namespace TEXT NOT NULL,
|
|
10758
|
+
source_id TEXT NOT NULL,
|
|
10759
|
+
ord INTEGER NOT NULL,
|
|
10760
|
+
text TEXT NOT NULL,
|
|
10761
|
+
embed_model TEXT NOT NULL,
|
|
10762
|
+
dims INTEGER NOT NULL,
|
|
10763
|
+
vector BLOB NOT NULL,
|
|
10764
|
+
created_at INTEGER NOT NULL
|
|
10765
|
+
);
|
|
10766
|
+
CREATE INDEX IF NOT EXISTS idx_kb_chunks_ns_src ON kb_chunks(namespace, source_id);
|
|
10767
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS kb_fts USING fts5(
|
|
10768
|
+
text, chunk_id UNINDEXED, namespace UNINDEXED, source_id UNINDEXED
|
|
10769
|
+
);
|
|
10770
|
+
`);
|
|
10771
|
+
}
|
|
10772
|
+
upsert(records, embedModel) {
|
|
10773
|
+
if (records.length === 0) return;
|
|
10774
|
+
const insChunk = this.db.prepare(
|
|
10775
|
+
`INSERT OR REPLACE INTO kb_chunks (chunk_id, namespace, source_id, ord, text, embed_model, dims, vector, created_at)
|
|
10776
|
+
VALUES (@chunk_id, @namespace, @source_id, @ord, @text, @embed_model, @dims, @vector, @created_at)`
|
|
10777
|
+
);
|
|
10778
|
+
const delFts = this.db.prepare("DELETE FROM kb_fts WHERE chunk_id = ?");
|
|
10779
|
+
const insFts = this.db.prepare("INSERT INTO kb_fts (text, chunk_id, namespace, source_id) VALUES (?, ?, ?, ?)");
|
|
10780
|
+
const now = Date.now();
|
|
10781
|
+
const tx = this.db.transaction((rows) => {
|
|
10782
|
+
for (const { chunk, vector } of rows) {
|
|
10783
|
+
const f = normalize(vector);
|
|
10784
|
+
insChunk.run({
|
|
10785
|
+
chunk_id: chunk.id,
|
|
10786
|
+
namespace: chunk.meta?.["namespace"] ?? "",
|
|
10787
|
+
source_id: chunk.sourceId,
|
|
10788
|
+
ord: chunk.ord,
|
|
10789
|
+
text: chunk.text,
|
|
10790
|
+
embed_model: embedModel,
|
|
10791
|
+
dims: f.length,
|
|
10792
|
+
vector: toBlob(f),
|
|
10793
|
+
created_at: now
|
|
10794
|
+
});
|
|
10795
|
+
delFts.run(chunk.id);
|
|
10796
|
+
insFts.run(chunk.text, chunk.id, chunk.meta?.["namespace"] ?? "", chunk.sourceId);
|
|
10797
|
+
}
|
|
10798
|
+
});
|
|
10799
|
+
tx(records);
|
|
10800
|
+
}
|
|
10801
|
+
hasSource(namespace, sourceId, embedModel) {
|
|
10802
|
+
const row = this.db.prepare("SELECT 1 FROM kb_chunks WHERE namespace = ? AND source_id = ? AND embed_model = ? LIMIT 1").get(namespace, sourceId, embedModel);
|
|
10803
|
+
return !!row;
|
|
10804
|
+
}
|
|
10805
|
+
deleteSource(namespace, sourceId) {
|
|
10806
|
+
this.db.prepare("DELETE FROM kb_chunks WHERE namespace = ? AND source_id = ?").run(namespace, sourceId);
|
|
10807
|
+
this.db.prepare("DELETE FROM kb_fts WHERE namespace = ? AND source_id = ?").run(namespace, sourceId);
|
|
10808
|
+
}
|
|
10809
|
+
lexicalSearch(query, opts) {
|
|
10810
|
+
const expr = ftsMatchExpr(query);
|
|
10811
|
+
if (!expr) return [];
|
|
10812
|
+
const k = opts.k ?? 10;
|
|
10813
|
+
const srcFilter = this.sourceFilter(opts.sourceIds, "kb_fts");
|
|
10814
|
+
const rows = this.db.prepare(
|
|
10815
|
+
`SELECT chunk_id, source_id, text, bm25(kb_fts) AS score
|
|
10816
|
+
FROM kb_fts
|
|
10817
|
+
WHERE kb_fts MATCH ? AND namespace = ? ${srcFilter.clause}
|
|
10818
|
+
ORDER BY score ASC LIMIT ?`
|
|
10819
|
+
).all(expr, opts.namespace, ...srcFilter.params, k);
|
|
10820
|
+
return rows.map((r, i) => ({ id: r.chunk_id, sourceId: r.source_id, ord: i, text: r.text, score: -r.score }));
|
|
10821
|
+
}
|
|
10822
|
+
denseSearch(queryVector, opts) {
|
|
10823
|
+
const q = normalize(queryVector);
|
|
10824
|
+
const k = opts.k ?? 10;
|
|
10825
|
+
const srcFilter = this.sourceFilter(opts.sourceIds, "kb_chunks");
|
|
10826
|
+
const rows = this.db.prepare(
|
|
10827
|
+
`SELECT chunk_id, source_id, ord, text, vector FROM kb_chunks
|
|
10828
|
+
WHERE namespace = ? ${srcFilter.clause}`
|
|
10829
|
+
).all(opts.namespace, ...srcFilter.params);
|
|
10830
|
+
const scored = rows.map((r) => {
|
|
10831
|
+
const v = fromBlob(r.vector);
|
|
10832
|
+
let dot = 0;
|
|
10833
|
+
const n = Math.min(v.length, q.length);
|
|
10834
|
+
for (let i = 0; i < n; i++) dot += v[i] * q[i];
|
|
10835
|
+
return { id: r.chunk_id, sourceId: r.source_id, ord: r.ord, text: r.text, score: dot };
|
|
10836
|
+
});
|
|
10837
|
+
scored.sort((a, b) => b.score - a.score);
|
|
10838
|
+
return scored.slice(0, k);
|
|
10839
|
+
}
|
|
10840
|
+
sourceFilter(sourceIds, table) {
|
|
10841
|
+
if (!sourceIds || sourceIds.length === 0) return { clause: "", params: [] };
|
|
10842
|
+
const placeholders = sourceIds.map(() => "?").join(", ");
|
|
10843
|
+
return { clause: `AND ${table}.source_id IN (${placeholders})`, params: sourceIds };
|
|
10844
|
+
}
|
|
10845
|
+
};
|
|
10846
|
+
|
|
10847
|
+
// src/retrieval/code-chunk.ts
|
|
10848
|
+
var DEFAULTS = { targetChars: 1600, overlapLines: 2 };
|
|
10849
|
+
var BOUNDARY_RE = new RegExp(
|
|
10850
|
+
[
|
|
10851
|
+
/^\s*(export\s+)?(default\s+)?(async\s+)?function\s+/,
|
|
10852
|
+
// JS/TS function
|
|
10853
|
+
/^\s*(export\s+)?(abstract\s+)?class\s+/,
|
|
10854
|
+
// class (JS/TS/PY/…)
|
|
10855
|
+
/^\s*(export\s+)?(interface|type|enum|namespace|module)\s+\w/,
|
|
10856
|
+
// TS declarations
|
|
10857
|
+
/^\s*(export\s+)?(const|let|var)\s+[\w$]+\s*[:=][^\n]*=>/,
|
|
10858
|
+
// arrow-fn const
|
|
10859
|
+
/^\s*(async\s+)?def\s+\w/,
|
|
10860
|
+
// Python def
|
|
10861
|
+
/^\s*(pub\s+)?(async\s+)?fn\s+\w/,
|
|
10862
|
+
// Rust fn
|
|
10863
|
+
/^\s*(pub\s+)?(struct|enum|trait|impl)\b/,
|
|
10864
|
+
// Rust items
|
|
10865
|
+
/^func\s+/,
|
|
10866
|
+
// Go func
|
|
10867
|
+
/^\s*(public|private|protected|internal|static|final|abstract)[\w\s<>,\[\]]*\s[\w$]+\s*\(/
|
|
10868
|
+
// Java/C# member
|
|
10869
|
+
].map((r) => r.source).join("|")
|
|
10870
|
+
);
|
|
10871
|
+
function hardSplitByLines(lines, targetChars, overlapLines) {
|
|
10872
|
+
const out = [];
|
|
10873
|
+
let cur = [];
|
|
10874
|
+
let curLen = 0;
|
|
10875
|
+
for (const line of lines) {
|
|
10876
|
+
if (curLen + line.length > targetChars && cur.length) {
|
|
10877
|
+
out.push(cur.join("\n"));
|
|
10878
|
+
cur = overlapLines > 0 ? cur.slice(-overlapLines) : [];
|
|
10879
|
+
curLen = cur.reduce((n, l) => n + l.length + 1, 0);
|
|
10880
|
+
}
|
|
10881
|
+
cur.push(line);
|
|
10882
|
+
curLen += line.length + 1;
|
|
10883
|
+
}
|
|
10884
|
+
if (cur.length) out.push(cur.join("\n"));
|
|
10885
|
+
return out;
|
|
10886
|
+
}
|
|
10887
|
+
function chunkCode(text, opts = {}) {
|
|
10888
|
+
const { targetChars, overlapLines } = { ...DEFAULTS, ...opts };
|
|
10889
|
+
const normalized = text.replace(/\r\n/g, "\n").replace(/\s+$/, "");
|
|
10890
|
+
if (!normalized.trim()) return [];
|
|
10891
|
+
const lines = normalized.split("\n");
|
|
10892
|
+
const boundaries = [0];
|
|
10893
|
+
for (let i = 1; i < lines.length; i++) {
|
|
10894
|
+
if (BOUNDARY_RE.test(lines[i])) boundaries.push(i);
|
|
10895
|
+
}
|
|
10896
|
+
boundaries.push(lines.length);
|
|
10897
|
+
const segments = [];
|
|
10898
|
+
for (let b = 0; b < boundaries.length - 1; b++) {
|
|
10899
|
+
const seg = lines.slice(boundaries[b], boundaries[b + 1]).join("\n").replace(/^\n+|\n+$/g, "");
|
|
10900
|
+
if (seg.trim()) segments.push(seg);
|
|
10901
|
+
}
|
|
10902
|
+
const chunks = [];
|
|
10903
|
+
let cur = "";
|
|
10904
|
+
for (const seg of segments) {
|
|
10905
|
+
if (seg.length > targetChars) {
|
|
10906
|
+
if (cur) {
|
|
10907
|
+
chunks.push(cur);
|
|
10908
|
+
cur = "";
|
|
10909
|
+
}
|
|
10910
|
+
chunks.push(...hardSplitByLines(seg.split("\n"), targetChars, overlapLines));
|
|
10911
|
+
continue;
|
|
10912
|
+
}
|
|
10913
|
+
const candidate = cur ? `${cur}
|
|
10914
|
+
|
|
10915
|
+
${seg}` : seg;
|
|
10916
|
+
if (candidate.length > targetChars && cur) {
|
|
10917
|
+
chunks.push(cur);
|
|
10918
|
+
cur = seg;
|
|
10919
|
+
} else cur = candidate;
|
|
10920
|
+
}
|
|
10921
|
+
if (cur.trim()) chunks.push(cur);
|
|
10922
|
+
return chunks.map((text2, ord) => ({ text: text2.replace(/^\n+|\n+$/g, ""), ord }));
|
|
10923
|
+
}
|
|
10924
|
+
var heuristicCodeChunker = {
|
|
10925
|
+
chunk: (text, opts) => chunkCode(text, opts)
|
|
10926
|
+
};
|
|
10927
|
+
function hashContent(content) {
|
|
10928
|
+
return createHash("sha256").update(content).digest("hex");
|
|
10929
|
+
}
|
|
10930
|
+
function buildManifest(entries) {
|
|
10931
|
+
const files = {};
|
|
10932
|
+
for (const e of entries) files[e.path] = e.hash;
|
|
10933
|
+
const root = createHash("sha256").update(Object.keys(files).sort().map((p) => `${p}:${files[p]}`).join("\n")).digest("hex");
|
|
10934
|
+
return { files, root };
|
|
10935
|
+
}
|
|
10936
|
+
function diffManifest(oldManifest, next) {
|
|
10937
|
+
const added = [];
|
|
10938
|
+
const changed = [];
|
|
10939
|
+
const removed = [];
|
|
10940
|
+
const unchanged = [];
|
|
10941
|
+
const old = oldManifest?.files ?? {};
|
|
10942
|
+
for (const [p, h] of Object.entries(next.files)) {
|
|
10943
|
+
if (!(p in old)) added.push(p);
|
|
10944
|
+
else if (old[p] !== h) changed.push(p);
|
|
10945
|
+
else unchanged.push(p);
|
|
10946
|
+
}
|
|
10947
|
+
for (const p of Object.keys(old)) if (!(p in next.files)) removed.push(p);
|
|
10948
|
+
return { added, changed, removed, unchanged };
|
|
10949
|
+
}
|
|
10950
|
+
|
|
10951
|
+
// src/retrieval/workspace-index.ts
|
|
10952
|
+
var DEFAULT_EXTENSIONS = [
|
|
10953
|
+
"ts",
|
|
10954
|
+
"tsx",
|
|
10955
|
+
"js",
|
|
10956
|
+
"jsx",
|
|
10957
|
+
"mjs",
|
|
10958
|
+
"cjs",
|
|
10959
|
+
"py",
|
|
10960
|
+
"go",
|
|
10961
|
+
"rs",
|
|
10962
|
+
"java",
|
|
10963
|
+
"rb",
|
|
10964
|
+
"php",
|
|
10965
|
+
"c",
|
|
10966
|
+
"h",
|
|
10967
|
+
"cpp",
|
|
10968
|
+
"hpp",
|
|
10969
|
+
"cc",
|
|
10970
|
+
"cs",
|
|
10971
|
+
"swift",
|
|
10972
|
+
"kt",
|
|
10973
|
+
"scala",
|
|
10974
|
+
"sh",
|
|
10975
|
+
"sql",
|
|
10976
|
+
"md",
|
|
10977
|
+
"json",
|
|
10978
|
+
"yaml",
|
|
10979
|
+
"yml",
|
|
10980
|
+
"toml"
|
|
10981
|
+
];
|
|
10982
|
+
var DEFAULT_MAX_FILE_BYTES = 512 * 1024;
|
|
10983
|
+
var HARD_SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "out", "target", "vendor", ".cache", "coverage"]);
|
|
10984
|
+
var WorkspaceIndex = class {
|
|
10985
|
+
root;
|
|
10986
|
+
db;
|
|
10987
|
+
store;
|
|
10988
|
+
retriever;
|
|
10989
|
+
namespace;
|
|
10990
|
+
extensions;
|
|
10991
|
+
maxFileBytes;
|
|
10992
|
+
isIgnored;
|
|
10993
|
+
chunker;
|
|
10994
|
+
constructor(opts) {
|
|
10995
|
+
this.root = path24.resolve(opts.root);
|
|
10996
|
+
this.db = opts.db;
|
|
10997
|
+
this.namespace = opts.namespace ?? "code";
|
|
10998
|
+
this.extensions = new Set((opts.extensions ?? DEFAULT_EXTENSIONS).map((e) => e.replace(/^\./, "").toLowerCase()));
|
|
10999
|
+
this.maxFileBytes = opts.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
|
|
11000
|
+
this.isIgnored = opts.isIgnored ?? (() => false);
|
|
11001
|
+
this.chunker = opts.chunker ?? heuristicCodeChunker;
|
|
11002
|
+
this.store = new SqliteVectorStore(this.db);
|
|
11003
|
+
this.retriever = new Retriever(opts.embedder, this.store, opts.reranker);
|
|
11004
|
+
this.migrate();
|
|
11005
|
+
}
|
|
11006
|
+
migrate() {
|
|
11007
|
+
this.db.exec(`
|
|
11008
|
+
CREATE TABLE IF NOT EXISTS code_manifest (
|
|
11009
|
+
namespace TEXT NOT NULL,
|
|
11010
|
+
path TEXT NOT NULL,
|
|
11011
|
+
hash TEXT NOT NULL,
|
|
11012
|
+
PRIMARY KEY (namespace, path)
|
|
11013
|
+
);
|
|
11014
|
+
`);
|
|
11015
|
+
}
|
|
11016
|
+
/** Recursively collect indexable files under root (relative paths). */
|
|
11017
|
+
async collectFiles() {
|
|
11018
|
+
const out = [];
|
|
11019
|
+
const walk = async (dir) => {
|
|
11020
|
+
let entries;
|
|
11021
|
+
try {
|
|
11022
|
+
entries = await fs4.readdir(dir, { withFileTypes: true });
|
|
11023
|
+
} catch {
|
|
11024
|
+
return;
|
|
11025
|
+
}
|
|
11026
|
+
for (const entry of entries) {
|
|
11027
|
+
const abs = path24.join(dir, entry.name);
|
|
11028
|
+
if (entry.isDirectory()) {
|
|
11029
|
+
if (HARD_SKIP_DIRS.has(entry.name) || this.isIgnored(abs)) continue;
|
|
11030
|
+
await walk(abs);
|
|
11031
|
+
} else if (entry.isFile()) {
|
|
11032
|
+
const ext = path24.extname(entry.name).slice(1).toLowerCase();
|
|
11033
|
+
if (!this.extensions.has(ext) || this.isIgnored(abs)) continue;
|
|
11034
|
+
out.push(path24.relative(this.root, abs));
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
};
|
|
11038
|
+
await walk(this.root);
|
|
11039
|
+
return out;
|
|
11040
|
+
}
|
|
11041
|
+
loadStoredManifest() {
|
|
11042
|
+
const rows = this.db.prepare("SELECT path, hash FROM code_manifest WHERE namespace = ?").all(this.namespace);
|
|
11043
|
+
if (rows.length === 0) return null;
|
|
11044
|
+
return buildManifest(rows);
|
|
11045
|
+
}
|
|
11046
|
+
saveManifest(manifest) {
|
|
11047
|
+
const del = this.db.prepare("DELETE FROM code_manifest WHERE namespace = ?");
|
|
11048
|
+
const ins = this.db.prepare("INSERT INTO code_manifest (namespace, path, hash) VALUES (?, ?, ?)");
|
|
11049
|
+
const tx = this.db.transaction(() => {
|
|
11050
|
+
del.run(this.namespace);
|
|
11051
|
+
for (const [p, h] of Object.entries(manifest.files)) ins.run(this.namespace, p, h);
|
|
11052
|
+
});
|
|
11053
|
+
tx();
|
|
11054
|
+
}
|
|
11055
|
+
/**
|
|
11056
|
+
* Scan the workspace and bring the index up to date, re-embedding only files
|
|
11057
|
+
* whose contents changed since the last refresh. Returns what changed.
|
|
11058
|
+
*/
|
|
11059
|
+
async refresh(onFile) {
|
|
11060
|
+
const rels = await this.collectFiles();
|
|
11061
|
+
const entries = [];
|
|
11062
|
+
for (const rel of rels) {
|
|
11063
|
+
try {
|
|
11064
|
+
const abs = path24.join(this.root, rel);
|
|
11065
|
+
const stat = await fs4.stat(abs);
|
|
11066
|
+
if (stat.size > this.maxFileBytes) continue;
|
|
11067
|
+
const buf = await fs4.readFile(abs);
|
|
11068
|
+
if (buf.includes(0)) continue;
|
|
11069
|
+
entries.push({ path: rel, hash: hashContent(buf), text: buf.toString("utf8") });
|
|
11070
|
+
} catch {
|
|
11071
|
+
}
|
|
11072
|
+
}
|
|
11073
|
+
const next = buildManifest(entries.map((e) => ({ path: e.path, hash: e.hash })));
|
|
11074
|
+
const diff = diffManifest(this.loadStoredManifest(), next);
|
|
11075
|
+
const byPath = new Map(entries.map((e) => [e.path, e.text]));
|
|
11076
|
+
let chunks = 0;
|
|
11077
|
+
for (const rel of [...diff.added, ...diff.changed]) {
|
|
11078
|
+
onFile?.(rel);
|
|
11079
|
+
this.store.deleteSource(this.namespace, rel);
|
|
11080
|
+
const pieces = this.chunker.chunk(byPath.get(rel) ?? "", { filename: rel });
|
|
11081
|
+
chunks += await this.retriever.index(this.namespace, rel, pieces);
|
|
11082
|
+
}
|
|
11083
|
+
for (const rel of diff.removed) this.store.deleteSource(this.namespace, rel);
|
|
11084
|
+
this.saveManifest(next);
|
|
11085
|
+
return {
|
|
11086
|
+
filesIndexed: diff.added.length + diff.changed.length,
|
|
11087
|
+
filesRemoved: diff.removed.length,
|
|
11088
|
+
filesUnchanged: diff.unchanged.length,
|
|
11089
|
+
chunks,
|
|
11090
|
+
root: next.root
|
|
11091
|
+
};
|
|
11092
|
+
}
|
|
11093
|
+
/** Hybrid + reranked search over the indexed codebase. */
|
|
11094
|
+
async search(query, k = 8) {
|
|
11095
|
+
return this.retriever.search(query, { namespace: this.namespace, k, candidates: 40 });
|
|
11096
|
+
}
|
|
11097
|
+
};
|
|
11098
|
+
var DEFAULT_OPENAI_EMBED_MODEL = "text-embedding-3-small";
|
|
11099
|
+
var DEFAULT_OLLAMA_EMBED_MODEL = "nomic-embed-text";
|
|
11100
|
+
var BATCH = 96;
|
|
11101
|
+
var OpenAICompatibleEmbedder = class {
|
|
11102
|
+
model;
|
|
11103
|
+
client;
|
|
11104
|
+
dimensions;
|
|
11105
|
+
resolvedDims = 0;
|
|
11106
|
+
constructor(opts) {
|
|
11107
|
+
this.model = opts.model;
|
|
11108
|
+
this.dimensions = opts.dimensions;
|
|
11109
|
+
this.client = new OpenAI({ apiKey: opts.apiKey || "sk-none", baseURL: opts.baseURL });
|
|
11110
|
+
}
|
|
11111
|
+
get dims() {
|
|
11112
|
+
return this.dimensions ?? this.resolvedDims;
|
|
11113
|
+
}
|
|
11114
|
+
async embed(texts) {
|
|
11115
|
+
if (texts.length === 0) return [];
|
|
11116
|
+
const out = [];
|
|
11117
|
+
for (let i = 0; i < texts.length; i += BATCH) {
|
|
11118
|
+
const batch = texts.slice(i, i + BATCH).map((t) => t.trim() ? t : " ");
|
|
11119
|
+
const res = await this.client.embeddings.create({
|
|
11120
|
+
model: this.model,
|
|
11121
|
+
input: batch,
|
|
11122
|
+
...this.dimensions ? { dimensions: this.dimensions } : {}
|
|
11123
|
+
});
|
|
11124
|
+
const rows = [...res.data].sort((a, b) => a.index - b.index);
|
|
11125
|
+
for (const row of rows) {
|
|
11126
|
+
const vec = row.embedding;
|
|
11127
|
+
if (!this.resolvedDims) this.resolvedDims = vec.length;
|
|
11128
|
+
out.push(vec);
|
|
11129
|
+
}
|
|
11130
|
+
}
|
|
11131
|
+
return out;
|
|
11132
|
+
}
|
|
11133
|
+
};
|
|
11134
|
+
var EMBED_CAPABLE = /* @__PURE__ */ new Set(["openai", "openai-compatible", "ollama"]);
|
|
11135
|
+
function embedderFromProviders(providers, opts = {}) {
|
|
11136
|
+
const ranked = [...providers].sort((a, b) => rank(a.type) - rank(b.type));
|
|
11137
|
+
const chosen = ranked.find((p) => EMBED_CAPABLE.has(p.type) && (p.apiKey || p.type === "ollama" || p.baseUrl));
|
|
11138
|
+
if (!chosen) return null;
|
|
11139
|
+
const model = opts.model || (chosen.type === "ollama" ? DEFAULT_OLLAMA_EMBED_MODEL : DEFAULT_OPENAI_EMBED_MODEL);
|
|
11140
|
+
return new OpenAICompatibleEmbedder({
|
|
11141
|
+
apiKey: chosen.apiKey,
|
|
11142
|
+
baseURL: chosen.baseUrl,
|
|
11143
|
+
model,
|
|
11144
|
+
dimensions: opts.dimensions
|
|
11145
|
+
});
|
|
11146
|
+
}
|
|
11147
|
+
function rank(type) {
|
|
11148
|
+
if (type === "openai") return 0;
|
|
11149
|
+
if (type === "openai-compatible") return 1;
|
|
11150
|
+
if (type === "ollama") return 2;
|
|
11151
|
+
return 9;
|
|
11152
|
+
}
|
|
11153
|
+
var RERANK_INSTRUCTIONS = 'You are a search reranker. Given a query and numbered passages, return the passage numbers ordered from MOST to LEAST relevant to the query. Respond with ONLY a comma-separated list of numbers (e.g. "3,1,4"). Include only clearly relevant passages; omit irrelevant ones.';
|
|
11154
|
+
function parseRankOrder(reply, count) {
|
|
11155
|
+
const nums = (reply.match(/\d+/g) ?? []).map((n) => Number(n) - 1);
|
|
11156
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11157
|
+
const order = [];
|
|
11158
|
+
for (const i of nums) {
|
|
11159
|
+
if (i >= 0 && i < count && !seen.has(i)) {
|
|
11160
|
+
seen.add(i);
|
|
11161
|
+
order.push(i);
|
|
11162
|
+
}
|
|
11163
|
+
}
|
|
11164
|
+
return order.length ? order : null;
|
|
11165
|
+
}
|
|
11166
|
+
var LLMReranker = class {
|
|
11167
|
+
name = "llm-listwise";
|
|
11168
|
+
complete;
|
|
11169
|
+
/** Cap on candidates sent to the model (keeps the prompt + cost bounded). */
|
|
11170
|
+
maxCandidates;
|
|
11171
|
+
constructor(opts) {
|
|
11172
|
+
this.complete = opts.complete;
|
|
11173
|
+
this.maxCandidates = opts.maxCandidates ?? 20;
|
|
11174
|
+
}
|
|
11175
|
+
async rerank(query, candidates, topK) {
|
|
11176
|
+
if (candidates.length <= 1) return candidates.slice(0, topK);
|
|
11177
|
+
const pool = candidates.slice(0, this.maxCandidates);
|
|
11178
|
+
const numbered = pool.map((c, i) => `[${i + 1}] ${c.text.slice(0, 600).replace(/\s+/g, " ").trim()}`).join("\n");
|
|
11179
|
+
const prompt = `${RERANK_INSTRUCTIONS}
|
|
11180
|
+
|
|
11181
|
+
Query: ${query}
|
|
11182
|
+
|
|
11183
|
+
Passages:
|
|
11184
|
+
${numbered}
|
|
11185
|
+
|
|
11186
|
+
Ranked passage numbers:`;
|
|
11187
|
+
let order = null;
|
|
11188
|
+
try {
|
|
11189
|
+
order = parseRankOrder(await this.complete(prompt), pool.length);
|
|
11190
|
+
} catch {
|
|
11191
|
+
order = null;
|
|
11192
|
+
}
|
|
11193
|
+
if (!order) return candidates.slice(0, topK);
|
|
11194
|
+
const ranked = order.map((i, rank3) => ({ ...pool[i], score: 1 / (rank3 + 1) }));
|
|
11195
|
+
if (ranked.length < topK) {
|
|
11196
|
+
const chosen = new Set(order);
|
|
11197
|
+
for (let i = 0; i < pool.length && ranked.length < topK; i++) {
|
|
11198
|
+
if (!chosen.has(i)) ranked.push({ ...pool[i], score: 0 });
|
|
11199
|
+
}
|
|
11200
|
+
}
|
|
11201
|
+
return ranked.slice(0, topK);
|
|
11202
|
+
}
|
|
11203
|
+
};
|
|
11204
|
+
function chatCompleterFromProviders(providers, opts = {}) {
|
|
11205
|
+
const capable = /* @__PURE__ */ new Set(["openai", "openai-compatible", "ollama"]);
|
|
11206
|
+
const ranked = [...providers].sort((a, b) => rank2(a.type) - rank2(b.type));
|
|
11207
|
+
const chosen = ranked.find((p) => capable.has(p.type) && (p.apiKey || p.type === "ollama" || p.baseUrl));
|
|
11208
|
+
if (!chosen) return null;
|
|
11209
|
+
const model = opts.model || chosen.model || (chosen.type === "ollama" ? "llama3" : "gpt-4o-mini");
|
|
11210
|
+
const client = new OpenAI({ apiKey: chosen.apiKey || "sk-none", baseURL: chosen.baseUrl });
|
|
11211
|
+
return async (prompt) => {
|
|
11212
|
+
const res = await client.chat.completions.create({
|
|
11213
|
+
model,
|
|
11214
|
+
messages: [{ role: "user", content: prompt }],
|
|
11215
|
+
temperature: 0,
|
|
11216
|
+
max_tokens: 60
|
|
11217
|
+
});
|
|
11218
|
+
return res.choices[0]?.message?.content ?? "";
|
|
11219
|
+
};
|
|
11220
|
+
}
|
|
11221
|
+
function rank2(type) {
|
|
11222
|
+
if (type === "openai") return 0;
|
|
11223
|
+
if (type === "openai-compatible") return 1;
|
|
11224
|
+
if (type === "ollama") return 2;
|
|
11225
|
+
return 9;
|
|
11226
|
+
}
|
|
11227
|
+
|
|
11228
|
+
// src/tools/code-search.ts
|
|
11229
|
+
var MAX_CHARS2 = 12e3;
|
|
11230
|
+
var CodeSearchTool = class extends BaseTool {
|
|
11231
|
+
constructor(index) {
|
|
11232
|
+
super();
|
|
11233
|
+
this.index = index;
|
|
11234
|
+
}
|
|
11235
|
+
index;
|
|
11236
|
+
name = "code_search";
|
|
11237
|
+
description = "Search the indexed workspace codebase for code relevant to a query (semantic + keyword). Returns the most relevant snippets with their file paths. Use to find where something is implemented or how an API is used.";
|
|
11238
|
+
inputSchema = {
|
|
11239
|
+
type: "object",
|
|
11240
|
+
properties: {
|
|
11241
|
+
query: { type: "string", description: "What to look for (natural language or code terms)." },
|
|
11242
|
+
k: { type: "number", description: "Max snippets to return (default 6)." }
|
|
11243
|
+
},
|
|
11244
|
+
required: ["query"]
|
|
11245
|
+
};
|
|
11246
|
+
async execute(input, _options) {
|
|
11247
|
+
const query = String(input["query"] ?? "").trim();
|
|
11248
|
+
if (!query) return 'Provide a "query" to search the codebase.';
|
|
11249
|
+
const k = typeof input["k"] === "number" ? Math.max(1, Math.min(20, input["k"])) : 6;
|
|
11250
|
+
let hits;
|
|
11251
|
+
try {
|
|
11252
|
+
hits = await this.index.search(query, k);
|
|
11253
|
+
} catch (err) {
|
|
11254
|
+
return `Code search failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
11255
|
+
}
|
|
11256
|
+
if (hits.length === 0) return "No relevant code found in the workspace index.";
|
|
11257
|
+
let out = "";
|
|
11258
|
+
for (const h of hits) {
|
|
11259
|
+
const block = `# ${h.sourceId}
|
|
11260
|
+
${h.text}
|
|
11261
|
+
`;
|
|
11262
|
+
if (out.length + block.length > MAX_CHARS2) break;
|
|
11263
|
+
out += (out ? "\n---\n\n" : "") + block;
|
|
11264
|
+
}
|
|
11265
|
+
return out;
|
|
11266
|
+
}
|
|
11267
|
+
};
|
|
11268
|
+
|
|
11269
|
+
// src/retrieval/graph.ts
|
|
11270
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
11271
|
+
"the",
|
|
11272
|
+
"a",
|
|
11273
|
+
"an",
|
|
11274
|
+
"and",
|
|
11275
|
+
"or",
|
|
11276
|
+
"of",
|
|
11277
|
+
"to",
|
|
11278
|
+
"in",
|
|
11279
|
+
"on",
|
|
11280
|
+
"for",
|
|
11281
|
+
"is",
|
|
11282
|
+
"are",
|
|
11283
|
+
"was",
|
|
11284
|
+
"were",
|
|
11285
|
+
"what",
|
|
11286
|
+
"which",
|
|
11287
|
+
"who",
|
|
11288
|
+
"how",
|
|
11289
|
+
"why",
|
|
11290
|
+
"does",
|
|
11291
|
+
"do",
|
|
11292
|
+
"did",
|
|
11293
|
+
"with",
|
|
11294
|
+
"that",
|
|
11295
|
+
"this",
|
|
11296
|
+
"it",
|
|
11297
|
+
"its",
|
|
11298
|
+
"by",
|
|
11299
|
+
"from",
|
|
11300
|
+
"as",
|
|
11301
|
+
"at",
|
|
11302
|
+
"be",
|
|
11303
|
+
"has",
|
|
11304
|
+
"have",
|
|
11305
|
+
"had",
|
|
11306
|
+
"will"
|
|
11307
|
+
]);
|
|
11308
|
+
function tokenize(text) {
|
|
11309
|
+
return (text.toLowerCase().match(/[a-z0-9_$.]+/g) ?? []).filter((t) => t.length > 1 && !STOPWORDS.has(t));
|
|
11310
|
+
}
|
|
11311
|
+
function entityTokens(entity) {
|
|
11312
|
+
return entity.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1);
|
|
11313
|
+
}
|
|
11314
|
+
var GraphRetriever = class {
|
|
11315
|
+
constructor(source) {
|
|
11316
|
+
this.source = source;
|
|
11317
|
+
}
|
|
11318
|
+
source;
|
|
11319
|
+
/**
|
|
11320
|
+
* Retrieve facts relevant to a query, expanding the graph a few hops from the
|
|
11321
|
+
* entities the query mentions. Returns facts ranked by relevance.
|
|
11322
|
+
*/
|
|
11323
|
+
search(query, opts = {}) {
|
|
11324
|
+
const hops = opts.hops ?? 1;
|
|
11325
|
+
const limit = opts.limit ?? 30;
|
|
11326
|
+
const qTokens = new Set(tokenize(query));
|
|
11327
|
+
if (qTokens.size === 0) return [];
|
|
11328
|
+
const all = this.source.getAllFacts();
|
|
11329
|
+
if (all.length === 0) return [];
|
|
11330
|
+
const entityNames = [...new Set(all.map((f) => f.entity))];
|
|
11331
|
+
const seeds = entityNames.filter((e) => {
|
|
11332
|
+
const et = entityTokens(e);
|
|
11333
|
+
return et.some((t) => qTokens.has(t)) || qTokens.has(e.toLowerCase());
|
|
11334
|
+
});
|
|
11335
|
+
if (seeds.length === 0) return [];
|
|
11336
|
+
const visited = new Set(seeds);
|
|
11337
|
+
let frontier = seeds;
|
|
11338
|
+
const collected = /* @__PURE__ */ new Map();
|
|
11339
|
+
for (let hop = 0; hop <= hops && frontier.length; hop++) {
|
|
11340
|
+
const facts = this.source.getFactsForEntities(frontier);
|
|
11341
|
+
for (const f of facts) {
|
|
11342
|
+
const key = `${f.entity}\0${f.relation}`;
|
|
11343
|
+
if (!collected.has(key)) collected.set(key, { ...f, score: 0, hop });
|
|
11344
|
+
}
|
|
11345
|
+
const next = [];
|
|
11346
|
+
for (const f of facts) {
|
|
11347
|
+
const valLower = f.value.toLowerCase();
|
|
11348
|
+
for (const e of entityNames) {
|
|
11349
|
+
if (visited.has(e)) continue;
|
|
11350
|
+
if (valLower.includes(e.toLowerCase())) {
|
|
11351
|
+
visited.add(e);
|
|
11352
|
+
next.push(e);
|
|
11353
|
+
}
|
|
11354
|
+
}
|
|
11355
|
+
}
|
|
11356
|
+
frontier = next;
|
|
11357
|
+
}
|
|
11358
|
+
const ranked = [...collected.values()].map((f) => {
|
|
11359
|
+
const hay = new Set(tokenize(`${f.entity} ${f.relation} ${f.value}`));
|
|
11360
|
+
let overlap = 0;
|
|
11361
|
+
for (const t of qTokens) if (hay.has(t)) overlap++;
|
|
11362
|
+
return { ...f, score: overlap };
|
|
11363
|
+
});
|
|
11364
|
+
ranked.sort((a, b) => b.score - a.score || a.hop - b.hop || a.entity.localeCompare(b.entity));
|
|
11365
|
+
return ranked.slice(0, limit);
|
|
11366
|
+
}
|
|
11367
|
+
/** Human-readable "- entity relation value" lines for injection into a run. */
|
|
11368
|
+
format(facts) {
|
|
11369
|
+
return facts.map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11370
|
+
}
|
|
11371
|
+
};
|
|
11372
|
+
|
|
11373
|
+
// src/tools/graph-search.ts
|
|
11374
|
+
var MAX_CHARS3 = 8e3;
|
|
11375
|
+
var GraphSearchTool = class extends BaseTool {
|
|
11376
|
+
name = "knowledge_graph_search";
|
|
11377
|
+
description = "Search the project knowledge graph (learned entity\u2192relation\u2192value facts) for information related to a query, expanding a couple of hops across relationships. Use for relational or multi-hop questions about the project.";
|
|
11378
|
+
inputSchema = {
|
|
11379
|
+
type: "object",
|
|
11380
|
+
properties: {
|
|
11381
|
+
query: { type: "string", description: "What to look up (mention the entities of interest)." },
|
|
11382
|
+
hops: { type: "number", description: "Relationship hops to expand (default 1, max 3)." }
|
|
11383
|
+
},
|
|
11384
|
+
required: ["query"]
|
|
11385
|
+
};
|
|
11386
|
+
retriever;
|
|
11387
|
+
constructor(source) {
|
|
11388
|
+
super();
|
|
11389
|
+
this.retriever = new GraphRetriever(source);
|
|
11390
|
+
}
|
|
11391
|
+
async execute(input, _options) {
|
|
11392
|
+
const query = String(input["query"] ?? "").trim();
|
|
11393
|
+
if (!query) return 'Provide a "query" naming the entities to look up.';
|
|
11394
|
+
const hops = typeof input["hops"] === "number" ? Math.max(0, Math.min(3, input["hops"])) : 1;
|
|
11395
|
+
const facts = this.retriever.search(query, { hops, limit: 40 });
|
|
11396
|
+
if (facts.length === 0) return "No related facts found in the project knowledge graph.";
|
|
11397
|
+
const formatted = this.retriever.format(facts);
|
|
11398
|
+
return formatted.length > MAX_CHARS3 ? `${formatted.slice(0, MAX_CHARS3)}
|
|
11399
|
+
\u2026` : formatted;
|
|
11400
|
+
}
|
|
11401
|
+
};
|
|
11402
|
+
|
|
11403
|
+
// src/core/context/compaction.ts
|
|
11404
|
+
function estimateTokens(text) {
|
|
11405
|
+
if (!text) return 0;
|
|
11406
|
+
return Math.max(1, Math.ceil(text.length / 4));
|
|
11407
|
+
}
|
|
11408
|
+
function contentToText(content) {
|
|
11409
|
+
if (typeof content === "string") return content;
|
|
11410
|
+
return content.map((c) => c.type === "text" ? c.text : c.type === "tool_result" ? c.content : `[${c.type}]`).join(" ");
|
|
11411
|
+
}
|
|
11412
|
+
function messagesTokens(messages) {
|
|
11413
|
+
return messages.reduce((n, m) => n + estimateTokens(contentToText(m.content)), 0);
|
|
11414
|
+
}
|
|
11415
|
+
function atomize(text, maxChars) {
|
|
11416
|
+
const seps = ["\n\n", "\n", ". ", " "];
|
|
11417
|
+
let units = [text];
|
|
11418
|
+
for (const sep of seps) {
|
|
11419
|
+
if (units.every((u) => u.length <= maxChars)) break;
|
|
11420
|
+
const next = [];
|
|
11421
|
+
for (const u of units) {
|
|
11422
|
+
if (u.length <= maxChars) {
|
|
11423
|
+
next.push(u);
|
|
11424
|
+
continue;
|
|
11425
|
+
}
|
|
11426
|
+
const parts = u.split(sep);
|
|
11427
|
+
parts.forEach((p, i) => next.push(i < parts.length - 1 ? p + sep : p));
|
|
11428
|
+
}
|
|
11429
|
+
units = next;
|
|
11430
|
+
}
|
|
11431
|
+
const out = [];
|
|
11432
|
+
for (const u of units) {
|
|
11433
|
+
if (u.length <= maxChars) out.push(u);
|
|
11434
|
+
else for (let i = 0; i < u.length; i += maxChars) out.push(u.slice(i, i + maxChars));
|
|
11435
|
+
}
|
|
11436
|
+
return out;
|
|
11437
|
+
}
|
|
11438
|
+
function chunkText(text, opts) {
|
|
11439
|
+
const maxChars = Math.max(1, Math.floor(opts.maxTokens * 4));
|
|
11440
|
+
const overlap = Math.min(maxChars - 1, Math.max(0, Math.floor(maxChars * (opts.overlapRatio ?? 0.1))));
|
|
11441
|
+
const trimmed = text.trim();
|
|
11442
|
+
if (!trimmed) return [];
|
|
11443
|
+
if (trimmed.length <= maxChars) return [trimmed];
|
|
11444
|
+
const units = atomize(trimmed, maxChars);
|
|
11445
|
+
const chunks = [];
|
|
11446
|
+
let cur = "";
|
|
11447
|
+
for (const u of units) {
|
|
11448
|
+
if (cur && cur.length + u.length > maxChars) {
|
|
11449
|
+
chunks.push(cur.trim());
|
|
11450
|
+
cur = overlap > 0 ? cur.slice(-overlap) : "";
|
|
11451
|
+
}
|
|
11452
|
+
cur += u;
|
|
11453
|
+
}
|
|
11454
|
+
if (cur.trim()) chunks.push(cur.trim());
|
|
11455
|
+
return chunks;
|
|
11456
|
+
}
|
|
11457
|
+
function needsCompaction(totalTokens, contextWindow, reserve = 0.2) {
|
|
11458
|
+
if (!contextWindow || contextWindow <= 0) return false;
|
|
11459
|
+
return totalTokens > contextWindow * (1 - reserve);
|
|
11460
|
+
}
|
|
11461
|
+
async function mapReduceCompact(text, opts) {
|
|
11462
|
+
const capChars = opts.capTokens * 4;
|
|
11463
|
+
let working = text;
|
|
11464
|
+
let truncated = false;
|
|
11465
|
+
if (working.length > capChars) {
|
|
11466
|
+
working = working.slice(0, capChars);
|
|
11467
|
+
truncated = true;
|
|
11468
|
+
}
|
|
11469
|
+
const instruction = opts.instruction ?? "Summarize this content, preserving key facts, names, numbers, and decisions.";
|
|
11470
|
+
let chunks = chunkText(working, { maxTokens: opts.chunkTokens });
|
|
11471
|
+
if (chunks.length <= 1) return { text: working, chunks: chunks.length, calls: 0, truncated };
|
|
11472
|
+
let calls = 0;
|
|
11473
|
+
let parts = await Promise.all(
|
|
11474
|
+
chunks.map((c, i) => {
|
|
11475
|
+
calls++;
|
|
11476
|
+
return opts.summarize(c, `${instruction} (part ${i + 1} of ${chunks.length})`);
|
|
11477
|
+
})
|
|
11478
|
+
);
|
|
11479
|
+
let combined = parts.join("\n\n");
|
|
11480
|
+
const maxPasses = opts.maxReducePasses ?? 3;
|
|
11481
|
+
for (let pass = 0; pass < maxPasses && estimateTokens(combined) > opts.targetTokens; pass++) {
|
|
11482
|
+
const groups = chunkText(combined, { maxTokens: opts.chunkTokens });
|
|
11483
|
+
if (groups.length <= 1) break;
|
|
11484
|
+
parts = await Promise.all(
|
|
11485
|
+
groups.map((g) => {
|
|
11486
|
+
calls++;
|
|
11487
|
+
return opts.summarize(g, "Merge these partial summaries into one cohesive summary; keep all distinct facts.");
|
|
11488
|
+
})
|
|
11489
|
+
);
|
|
11490
|
+
combined = parts.join("\n\n");
|
|
11491
|
+
}
|
|
11492
|
+
const note = truncated ? "\n\n[Note: input exceeded the extended-context cap and was truncated.]" : "";
|
|
11493
|
+
return { text: combined + note, chunks: chunks.length, calls, truncated };
|
|
11494
|
+
}
|
|
11495
|
+
async function rollingSummary(messages, opts) {
|
|
11496
|
+
if (messages.length <= opts.keepRecent) return messages;
|
|
11497
|
+
const older = messages.slice(0, messages.length - opts.keepRecent);
|
|
11498
|
+
const recent = messages.slice(messages.length - opts.keepRecent);
|
|
11499
|
+
const transcript = older.map((m) => `${m.role}: ${contentToText(m.content)}`).join("\n");
|
|
11500
|
+
const summary = await opts.summarize(
|
|
11501
|
+
transcript,
|
|
11502
|
+
"Summarize this earlier conversation. Preserve facts, decisions, names, numbers, and any open threads or todos."
|
|
11503
|
+
);
|
|
11504
|
+
const summaryMessage = {
|
|
11505
|
+
role: "system",
|
|
11506
|
+
content: `Summary of earlier conversation (folded to fit the context window):
|
|
11507
|
+
${summary}`
|
|
11508
|
+
};
|
|
11509
|
+
return [summaryMessage, ...recent];
|
|
11510
|
+
}
|
|
9829
11511
|
|
|
9830
11512
|
// src/core/steering/guidance.ts
|
|
9831
11513
|
var GuidanceQueue = class {
|
|
@@ -9852,10 +11534,22 @@ var GuidanceQueue = class {
|
|
|
9852
11534
|
};
|
|
9853
11535
|
|
|
9854
11536
|
// src/core/cascade.ts
|
|
11537
|
+
function buildContextualPrompt(prompt, history = []) {
|
|
11538
|
+
const recent = history.slice(-6);
|
|
11539
|
+
if (recent.length === 0) return prompt;
|
|
11540
|
+
const asText = (content) => typeof content === "string" ? content : content.map((b) => b.type === "text" ? b.text : "[non-text]").join(" ");
|
|
11541
|
+
const block = recent.map((m) => `${m.role === "assistant" ? "Assistant" : "User"}: ${asText(m.content).slice(0, 2e3)}`).join("\n");
|
|
11542
|
+
return `Recent conversation (for context \u2014 the user is continuing it; resolve their latest message with this in mind):
|
|
11543
|
+
${block}
|
|
11544
|
+
|
|
11545
|
+
Latest user message:
|
|
11546
|
+
${prompt}`;
|
|
11547
|
+
}
|
|
9855
11548
|
var Cascade = class _Cascade extends EventEmitter {
|
|
9856
11549
|
router;
|
|
9857
11550
|
toolRegistry;
|
|
9858
11551
|
mcpClient;
|
|
11552
|
+
codeIndex;
|
|
9859
11553
|
config;
|
|
9860
11554
|
/** Orchestration decisions for the CURRENT run — cleared on each run(). */
|
|
9861
11555
|
decisionLog = [];
|
|
@@ -9913,7 +11607,10 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
9913
11607
|
}
|
|
9914
11608
|
initOptionalFeatures() {
|
|
9915
11609
|
if (this.config.cascadeAuto === true) {
|
|
9916
|
-
this.perfTracker = new ModelPerformanceTracker(
|
|
11610
|
+
this.perfTracker = new ModelPerformanceTracker(
|
|
11611
|
+
this.config.routing?.perfStatsPath,
|
|
11612
|
+
{ readOnly: this.config.routing?.learnFromOutcomes === false }
|
|
11613
|
+
);
|
|
9917
11614
|
void this.perfTracker.load();
|
|
9918
11615
|
this.taskAnalyzer = new TaskAnalyzer(this.perfTracker, this.config.autoBias ?? "balanced");
|
|
9919
11616
|
this.router.setTaskAnalyzer(this.taskAnalyzer);
|
|
@@ -9955,6 +11652,41 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
9955
11652
|
this.emit("mcp:approval-required", { server });
|
|
9956
11653
|
});
|
|
9957
11654
|
}
|
|
11655
|
+
/**
|
|
11656
|
+
* Build the workspace code index and register the `code_search` tool. Uses the
|
|
11657
|
+
* user's own provider key for embeddings (and, if chat-capable, reranking).
|
|
11658
|
+
* The index persists to a per-workspace SQLite file; `autoRefresh` brings it
|
|
11659
|
+
* up to date at init (incremental — only changed files re-embed).
|
|
11660
|
+
*/
|
|
11661
|
+
async initCodeIndex() {
|
|
11662
|
+
const ci = this.config.codeIndex;
|
|
11663
|
+
const embedder = embedderFromProviders(this.config.providers);
|
|
11664
|
+
if (!embedder) {
|
|
11665
|
+
this.emit("log", { level: "warn", message: "Code index enabled but no embeddings-capable provider is configured \u2014 skipping." });
|
|
11666
|
+
return;
|
|
11667
|
+
}
|
|
11668
|
+
const dbPath = ci.dbPath || path24.join(this.workspacePath, ".cascade", "code-index.db");
|
|
11669
|
+
fs23.mkdirSync(path24.dirname(dbPath), { recursive: true });
|
|
11670
|
+
const db = new Database(dbPath);
|
|
11671
|
+
db.pragma("journal_mode = WAL");
|
|
11672
|
+
const complete = chatCompleterFromProviders(this.config.providers);
|
|
11673
|
+
const reranker = complete ? new LLMReranker({ complete }) : void 0;
|
|
11674
|
+
const ignore4 = new CascadeIgnore();
|
|
11675
|
+
await ignore4.load(this.workspacePath);
|
|
11676
|
+
const index = new WorkspaceIndex({
|
|
11677
|
+
root: this.workspacePath,
|
|
11678
|
+
db,
|
|
11679
|
+
embedder,
|
|
11680
|
+
reranker,
|
|
11681
|
+
isIgnored: (abs) => ignore4.isIgnored(abs, this.workspacePath)
|
|
11682
|
+
});
|
|
11683
|
+
if (ci.autoRefresh) {
|
|
11684
|
+
const res = await index.refresh();
|
|
11685
|
+
this.emit("log", { level: "info", message: `Code index refreshed: ${res.filesIndexed} indexed, ${res.filesUnchanged} unchanged, ${res.chunks} chunks.` });
|
|
11686
|
+
}
|
|
11687
|
+
this.codeIndex = index;
|
|
11688
|
+
this.toolRegistry.register(new CodeSearchTool(index));
|
|
11689
|
+
}
|
|
9958
11690
|
recordDecision(kind, detail) {
|
|
9959
11691
|
this.decisionLog.push({ at: (/* @__PURE__ */ new Date()).toISOString(), kind, detail });
|
|
9960
11692
|
}
|
|
@@ -10019,6 +11751,87 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10019
11751
|
resolvePlanApproval(approved, note, editedPlan) {
|
|
10020
11752
|
this.pendingPlanApproval?.({ approved, note, editedPlan });
|
|
10021
11753
|
}
|
|
11754
|
+
// ── Extended-context approval ───────────────────────────────────────
|
|
11755
|
+
// Same gate pattern: an oversized single input costs extra model calls to
|
|
11756
|
+
// chunk + map-reduce, so we confirm before spending. No listener (headless)
|
|
11757
|
+
// or a timeout means PROCEED — the feature is opt-in, and the run's budget
|
|
11758
|
+
// cap is the real guardrail against runaway cost.
|
|
11759
|
+
pendingContextApproval;
|
|
11760
|
+
/** Resolve a pending extended-context confirmation from a UI listener. */
|
|
11761
|
+
resolveContextApproval(approved) {
|
|
11762
|
+
this.pendingContextApproval?.(approved);
|
|
11763
|
+
}
|
|
11764
|
+
async requestContextApproval(info) {
|
|
11765
|
+
if (this.listenerCount("context:approval-required") === 0) return true;
|
|
11766
|
+
return await new Promise((resolve) => {
|
|
11767
|
+
const timeout = setTimeout(() => {
|
|
11768
|
+
if (this.pendingContextApproval) {
|
|
11769
|
+
this.pendingContextApproval = void 0;
|
|
11770
|
+
resolve(true);
|
|
11771
|
+
}
|
|
11772
|
+
}, 12e4);
|
|
11773
|
+
this.pendingContextApproval = (approved) => {
|
|
11774
|
+
clearTimeout(timeout);
|
|
11775
|
+
this.pendingContextApproval = void 0;
|
|
11776
|
+
resolve(approved);
|
|
11777
|
+
};
|
|
11778
|
+
this.emit("context:approval-required", info);
|
|
11779
|
+
});
|
|
11780
|
+
}
|
|
11781
|
+
/**
|
|
11782
|
+
* Extended context: when enabled, compact an over-budget conversation history
|
|
11783
|
+
* (rolling summary — cheap, automatic) and/or a single oversized input (chunk
|
|
11784
|
+
* + map-reduce — confirm-gated) so the run fits the model's window. Returns a
|
|
11785
|
+
* new options object with the compacted prompt/history; a no-op when disabled,
|
|
11786
|
+
* when nothing overflows, or when the model window isn't known yet.
|
|
11787
|
+
*/
|
|
11788
|
+
async applyExtendedContext(options) {
|
|
11789
|
+
const ec = this.config.extendedContext;
|
|
11790
|
+
if (!ec?.enabled) return options;
|
|
11791
|
+
const windowTokens = this.router.getReferenceContextWindow();
|
|
11792
|
+
if (!windowTokens) return options;
|
|
11793
|
+
const reserve = 0.2;
|
|
11794
|
+
const budget = Math.floor(windowTokens * (1 - reserve));
|
|
11795
|
+
const multiplier = ec.maxMultiplier ?? 2;
|
|
11796
|
+
const capTokens = windowTokens * multiplier;
|
|
11797
|
+
const summarize = async (input, instruction) => {
|
|
11798
|
+
const res = await this.router.generate("T3", {
|
|
11799
|
+
messages: [{ role: "user", content: `${instruction}
|
|
11800
|
+
|
|
11801
|
+
${input}` }],
|
|
11802
|
+
maxTokens: 1024,
|
|
11803
|
+
temperature: 0
|
|
11804
|
+
});
|
|
11805
|
+
return res.content;
|
|
11806
|
+
};
|
|
11807
|
+
let history = options.conversationHistory ?? [];
|
|
11808
|
+
let prompt = options.prompt;
|
|
11809
|
+
if (history.length > 4 && needsCompaction(messagesTokens(history), windowTokens, reserve)) {
|
|
11810
|
+
try {
|
|
11811
|
+
const before = history.length;
|
|
11812
|
+
history = await rollingSummary(history, { summarize, keepRecent: 4, targetTokens: budget });
|
|
11813
|
+
this.recordDecision("context", `Extended context: folded ${before - history.length + 1} earlier turns into a summary to fit the window`);
|
|
11814
|
+
this.emit("context:compacted", { kind: "history", foldedTurns: before - (history.length - 1) });
|
|
11815
|
+
} catch {
|
|
11816
|
+
}
|
|
11817
|
+
}
|
|
11818
|
+
const inputTokens = estimateTokens(prompt);
|
|
11819
|
+
if (inputTokens > windowTokens) {
|
|
11820
|
+
const chunkTokens = Math.max(256, Math.floor(budget / 3));
|
|
11821
|
+
const estChunks = Math.ceil(inputTokens / chunkTokens);
|
|
11822
|
+
const approved = await this.requestContextApproval({ inputTokens, windowTokens, multiplier, estChunks });
|
|
11823
|
+
if (approved) {
|
|
11824
|
+
try {
|
|
11825
|
+
const r = await mapReduceCompact(prompt, { summarize, chunkTokens, targetTokens: budget, capTokens });
|
|
11826
|
+
prompt = r.text;
|
|
11827
|
+
this.recordDecision("context", `Extended context: compacted a ${inputTokens}-token input via ${r.chunks} chunks (${r.calls} calls${r.truncated ? ", truncated at cap" : ""})`);
|
|
11828
|
+
this.emit("context:compacted", { kind: "input", chunks: r.chunks, calls: r.calls, truncated: r.truncated });
|
|
11829
|
+
} catch {
|
|
11830
|
+
}
|
|
11831
|
+
}
|
|
11832
|
+
}
|
|
11833
|
+
return { ...options, prompt, conversationHistory: history };
|
|
11834
|
+
}
|
|
10022
11835
|
/**
|
|
10023
11836
|
* Autonomy control (used by the /auto command). 'auto' makes the next run
|
|
10024
11837
|
* hands-off: the plan gate auto-approves and non-dangerous tools auto-approve,
|
|
@@ -10132,13 +11945,31 @@ ${last.partialOutput}` : "");
|
|
|
10132
11945
|
if (this.config.tools.mcpServers?.length) {
|
|
10133
11946
|
for (const server of this.config.tools.mcpServers) {
|
|
10134
11947
|
try {
|
|
10135
|
-
|
|
11948
|
+
const authProvider = server.oauthStore ? fileOAuthProvider(server.oauthStore) : void 0;
|
|
11949
|
+
await this.mcpClient.connect(server, authProvider ? { authProvider } : {});
|
|
10136
11950
|
this.toolRegistry.registerMcpTools(this.mcpClient);
|
|
10137
11951
|
} catch (err) {
|
|
10138
11952
|
console.error(`Failed to connect to MCP server "${server.name}":`, err);
|
|
10139
11953
|
}
|
|
10140
11954
|
}
|
|
10141
11955
|
}
|
|
11956
|
+
if (this.config.codeIndex?.enabled) {
|
|
11957
|
+
try {
|
|
11958
|
+
await this.initCodeIndex();
|
|
11959
|
+
} catch (err) {
|
|
11960
|
+
console.error("Failed to initialize workspace code index:", err);
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
if (this.config.knowledge?.factsExtraction !== false) {
|
|
11964
|
+
try {
|
|
11965
|
+
const ws = this.worldStateDB;
|
|
11966
|
+
if (ws && ws.getAllFacts().length > 0) {
|
|
11967
|
+
this.toolRegistry.register(new GraphSearchTool(ws));
|
|
11968
|
+
}
|
|
11969
|
+
} catch (err) {
|
|
11970
|
+
console.error("Failed to register knowledge graph search:", err);
|
|
11971
|
+
}
|
|
11972
|
+
}
|
|
10142
11973
|
const pluginPaths = this.config["plugins"];
|
|
10143
11974
|
if (pluginPaths?.length) {
|
|
10144
11975
|
for (const pluginPath of pluginPaths) {
|
|
@@ -10187,6 +12018,33 @@ ${last.partialOutput}` : "");
|
|
|
10187
12018
|
const casual = /^(hi|hello|hey|greetings|thanks|thank you|thx|bye|goodbye|cya)$/i.test(prompt.trim().replace(/[!?.]+$/, ""));
|
|
10188
12019
|
return casual;
|
|
10189
12020
|
}
|
|
12021
|
+
/**
|
|
12022
|
+
* Pure small talk that a single tool-less model answers best: greetings,
|
|
12023
|
+
* acknowledgements and self-identity questions. Deliberately NARROWER than
|
|
12024
|
+
* looksLikeConversational — "what is X" / "show me Y" / "list Z" need tools
|
|
12025
|
+
* or real lookup and stay on the worker path. Bare confirmations ("yes",
|
|
12026
|
+
* "ok") in an ongoing conversation are task input, not small talk.
|
|
12027
|
+
*/
|
|
12028
|
+
looksLikeSmallTalk(prompt, history = []) {
|
|
12029
|
+
const p = prompt.trim();
|
|
12030
|
+
if (this.isCasualGreeting(p)) return true;
|
|
12031
|
+
if (/^(?:who|what)\b.*\byou\b/i.test(p) || /^what can you\b/i.test(p)) {
|
|
12032
|
+
return p.split(/\s+/).length <= 12;
|
|
12033
|
+
}
|
|
12034
|
+
if (history.length === 0 && /^(?:hi|hello|hey|greetings)\b[\s,!.]*\w*[!.]*$/i.test(p)) return true;
|
|
12035
|
+
return false;
|
|
12036
|
+
}
|
|
12037
|
+
/**
|
|
12038
|
+
* A terse option/menu selection ("3", "b)", "option 2") replying to an
|
|
12039
|
+
* ongoing conversation. These inherit the prior turn's intent — they are
|
|
12040
|
+
* usually an ACTION choice, so they must not be short-circuited to a
|
|
12041
|
+
* tool-less direct answer, and a context-free on-device hint about them
|
|
12042
|
+
* is noise (the hint classifier never saw the conversation).
|
|
12043
|
+
*/
|
|
12044
|
+
looksLikeTerseOptionReply(prompt) {
|
|
12045
|
+
const p = prompt.trim();
|
|
12046
|
+
return /^\(?(?:\d{1,2}|[a-d])[.)]?$/i.test(p) || /^(?:option|choice)\s+\d{1,2}$/i.test(p);
|
|
12047
|
+
}
|
|
10190
12048
|
looksLikeSimpleArtifactTask(prompt) {
|
|
10191
12049
|
return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
|
|
10192
12050
|
}
|
|
@@ -10267,7 +12125,28 @@ ${last.partialOutput}` : "");
|
|
|
10267
12125
|
return 0;
|
|
10268
12126
|
}
|
|
10269
12127
|
}
|
|
10270
|
-
|
|
12128
|
+
/**
|
|
12129
|
+
* Raises a raw complexity verdict (from the classifier LLM or a caller hint)
|
|
12130
|
+
* to the tier the prompt's signals demand. A small model that under-rates
|
|
12131
|
+
* clearly multi-step build work can't strand it below where it belongs:
|
|
12132
|
+
* explicit build+scale signals floor it to Complex (full T1→T2→T3), and a
|
|
12133
|
+
* single-deliverable build floors a "Simple" up to Moderate (one manager).
|
|
12134
|
+
* Conservative on purpose — the source is free to go higher, and a manual
|
|
12135
|
+
* tier override is the escape hatch. Records the decision it makes.
|
|
12136
|
+
*/
|
|
12137
|
+
floorComplexity(prompt, verdict, source, reason) {
|
|
12138
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
12139
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over ${source} "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
12140
|
+
return "Complex";
|
|
12141
|
+
}
|
|
12142
|
+
if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
12143
|
+
this.recordDecision("complexity", `Moderate \u2014 heuristic floor over ${source} "Simple": build signals without multi-system scale (single manager)`);
|
|
12144
|
+
return "Moderate";
|
|
12145
|
+
}
|
|
12146
|
+
this.recordDecision("complexity", `${verdict} \u2014 ${source}${reason ? `: ${reason}` : ""}`);
|
|
12147
|
+
return verdict;
|
|
12148
|
+
}
|
|
12149
|
+
async determineComplexity(prompt, workspacePath, conversationHistory = [], hint) {
|
|
10271
12150
|
if (this.isCasualGreeting(prompt)) {
|
|
10272
12151
|
this.recordDecision("complexity", "Simple \u2014 heuristic: casual greeting (no classifier call)");
|
|
10273
12152
|
return "Simple";
|
|
@@ -10284,6 +12163,16 @@ ${last.partialOutput}` : "");
|
|
|
10284
12163
|
this.recordDecision("complexity", "Simple \u2014 heuristic: read-only inquiry over existing content (single agent, no classifier call)");
|
|
10285
12164
|
return "Simple";
|
|
10286
12165
|
}
|
|
12166
|
+
if (hint) {
|
|
12167
|
+
if (this.looksLikeTerseOptionReply(prompt) && conversationHistory.length > 0) {
|
|
12168
|
+
this.recordDecision(
|
|
12169
|
+
"complexity",
|
|
12170
|
+
`terse option reply \u2014 on-device hint "${hint}" ignored; classifying with conversation context`
|
|
12171
|
+
);
|
|
12172
|
+
} else {
|
|
12173
|
+
return this.floorComplexity(prompt, hint, "on-device hint");
|
|
12174
|
+
}
|
|
12175
|
+
}
|
|
10287
12176
|
let workspaceContext = "";
|
|
10288
12177
|
try {
|
|
10289
12178
|
const count = await this.countWorkspaceFiles(workspacePath);
|
|
@@ -10331,39 +12220,45 @@ ${prompt}` : prompt;
|
|
|
10331
12220
|
const reason = content.replace(/^\S+\s*[—–-]*\s*/, "").trim();
|
|
10332
12221
|
let verdict;
|
|
10333
12222
|
if (match) {
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
10337
|
-
verdict = "Complex";
|
|
10338
|
-
} else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
|
|
10339
|
-
this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
|
|
10340
|
-
verdict = "Moderate";
|
|
10341
|
-
} else {
|
|
10342
|
-
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
10343
|
-
}
|
|
12223
|
+
const raw = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
12224
|
+
verdict = this.floorComplexity(prompt, raw, "classifier", reason || "no reason given");
|
|
10344
12225
|
} else {
|
|
10345
12226
|
const words = prompt.trim().split(/\s+/).length;
|
|
10346
12227
|
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
10347
12228
|
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
10348
12229
|
}
|
|
10349
12230
|
return verdict;
|
|
10350
|
-
} catch {
|
|
12231
|
+
} catch (err) {
|
|
12232
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
12233
|
+
if (this.listenerCount("log") > 0) this.emit("log", { level: "warn", message: `Complexity classifier failed: ${detail}` });
|
|
12234
|
+
else console.warn(`[cascade] Complexity classifier failed: ${detail}`);
|
|
10351
12235
|
const followUpPrompt = /^(proceed|continue|go ahead|do it|yes|yep|ok|okay|carry on)$/i.test(prompt.trim());
|
|
10352
12236
|
if (followUpPrompt && recentHistory.length > 0) {
|
|
10353
12237
|
this.recordDecision("complexity", "Complex \u2014 classifier unavailable; short follow-up inherits prior context");
|
|
10354
12238
|
return "Complex";
|
|
10355
12239
|
}
|
|
10356
|
-
this.recordDecision("complexity",
|
|
12240
|
+
this.recordDecision("complexity", `Moderate \u2014 classifier unavailable (${detail}); defaulting to the mid-cost route`);
|
|
10357
12241
|
return "Moderate";
|
|
10358
12242
|
}
|
|
10359
12243
|
}
|
|
10360
12244
|
async run(options) {
|
|
10361
12245
|
await this.init();
|
|
10362
12246
|
this.router.beginRun();
|
|
12247
|
+
if (options.signal) setMaxListeners(64, options.signal);
|
|
10363
12248
|
this.router.setRunSignal(options.signal);
|
|
10364
12249
|
const startMs = Date.now();
|
|
10365
12250
|
const taskId = randomUUID();
|
|
10366
12251
|
this.decisionLog = [];
|
|
12252
|
+
if (options.fastAnswer) {
|
|
12253
|
+
return this.runFastAnswer(options, startMs, taskId);
|
|
12254
|
+
}
|
|
12255
|
+
const routingPrompt = options.routingPrompt ?? options.prompt;
|
|
12256
|
+
const pinnedTier = ["T1", "T2", "T3"].includes(this.config.routing?.forceTier ?? "");
|
|
12257
|
+
if (this.config.fastAnswer?.autoSimple !== false && !pinnedTier && this.looksLikeSmallTalk(routingPrompt, options.conversationHistory)) {
|
|
12258
|
+
this.recordDecision("complexity", "Simple \u2014 heuristic: small talk \u2192 direct answer (no worker, no classifier call)");
|
|
12259
|
+
return this.runFastAnswer(options, startMs, taskId, { auto: true });
|
|
12260
|
+
}
|
|
12261
|
+
options = await this.applyExtendedContext(options);
|
|
10367
12262
|
const escalator = new PermissionEscalator(this.config.approvalTimeoutMs ?? 6e5, this.config.autonomy === "auto");
|
|
10368
12263
|
escalator.on("permission:user-required", async (req) => {
|
|
10369
12264
|
this.emit("permission:user-required", req);
|
|
@@ -10403,7 +12298,12 @@ ${prompt}` : prompt;
|
|
|
10403
12298
|
complexity = forced;
|
|
10404
12299
|
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
10405
12300
|
} else {
|
|
10406
|
-
complexity = await this.determineComplexity(
|
|
12301
|
+
complexity = await this.determineComplexity(
|
|
12302
|
+
routingPrompt,
|
|
12303
|
+
options.workspacePath || process.cwd(),
|
|
12304
|
+
options.conversationHistory,
|
|
12305
|
+
options.complexityHint
|
|
12306
|
+
);
|
|
10407
12307
|
}
|
|
10408
12308
|
this.telemetry.capture("cascade:session_start", {
|
|
10409
12309
|
complexity,
|
|
@@ -10418,7 +12318,7 @@ ${prompt}` : prompt;
|
|
|
10418
12318
|
const tierKey = tier.toLowerCase();
|
|
10419
12319
|
if (this.config.models?.[tierKey]) return;
|
|
10420
12320
|
try {
|
|
10421
|
-
const model = await this.taskAnalyzer.selectModel(
|
|
12321
|
+
const model = await this.taskAnalyzer.selectModel(routingPrompt, tier, this.router.getSelector());
|
|
10422
12322
|
if (model) {
|
|
10423
12323
|
this.router.overrideTierModel(tier, model);
|
|
10424
12324
|
const taskType = this.taskAnalyzer.getLastProfile()?.type ?? "mixed";
|
|
@@ -10440,6 +12340,7 @@ ${prompt}` : prompt;
|
|
|
10440
12340
|
}).join(" \xB7 "));
|
|
10441
12341
|
const toolCreator = this.toolCreator;
|
|
10442
12342
|
if (toolCreator) toolCreator.setPermissionEscalator(escalator);
|
|
12343
|
+
const rootPrompt = buildContextualPrompt(options.prompt, options.conversationHistory);
|
|
10443
12344
|
let finalOutput = "";
|
|
10444
12345
|
let t2Results = [];
|
|
10445
12346
|
let runError = null;
|
|
@@ -10498,7 +12399,7 @@ ${prompt}` : prompt;
|
|
|
10498
12399
|
const assignment = {
|
|
10499
12400
|
subtaskId: taskId,
|
|
10500
12401
|
subtaskTitle: "Direct Request",
|
|
10501
|
-
description:
|
|
12402
|
+
description: rootPrompt,
|
|
10502
12403
|
expectedOutput: "A complete and direct answer.",
|
|
10503
12404
|
constraints: [],
|
|
10504
12405
|
peerT3Ids: [],
|
|
@@ -10543,7 +12444,7 @@ ${prompt}` : prompt;
|
|
|
10543
12444
|
const assignment = {
|
|
10544
12445
|
sectionId: taskId,
|
|
10545
12446
|
sectionTitle: "Direct Task",
|
|
10546
|
-
description:
|
|
12447
|
+
description: rootPrompt,
|
|
10547
12448
|
expectedOutput: "A complete resolution of the task.",
|
|
10548
12449
|
constraints: [],
|
|
10549
12450
|
t3Subtasks: []
|
|
@@ -10555,7 +12456,17 @@ ${prompt}` : prompt;
|
|
|
10555
12456
|
if (completed.length > 0) {
|
|
10556
12457
|
finalOutput = t2Result.sectionSummary + "\n\n" + completed.map((r) => r.output).join("\n\n");
|
|
10557
12458
|
} else {
|
|
10558
|
-
|
|
12459
|
+
const partial = t2Result.t3Results.map((r) => typeof r.output === "string" ? r.output : "").filter((o) => o.trim()).sort((a, b) => b.length - a.length)[0];
|
|
12460
|
+
const reasons = Array.from(
|
|
12461
|
+
new Set(t2Result.t3Results.flatMap((r) => r.issues ?? []).filter(Boolean))
|
|
12462
|
+
);
|
|
12463
|
+
if (partial) {
|
|
12464
|
+
finalOutput = reasons.length ? `${partial}
|
|
12465
|
+
|
|
12466
|
+
_(incomplete: ${reasons.join("; ")})_` : partial;
|
|
12467
|
+
} else {
|
|
12468
|
+
finalOutput = reasons.length ? `The task could not be completed: ${reasons.join("; ")}` : "The task could not be completed \u2014 the model returned no usable output.";
|
|
12469
|
+
}
|
|
10559
12470
|
}
|
|
10560
12471
|
} else {
|
|
10561
12472
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
@@ -10579,7 +12490,7 @@ ${prompt}` : prompt;
|
|
|
10579
12490
|
return decision;
|
|
10580
12491
|
});
|
|
10581
12492
|
}
|
|
10582
|
-
const result = await t1.execute(
|
|
12493
|
+
const result = await t1.execute(rootPrompt, options.images, void 0, options.signal);
|
|
10583
12494
|
finalOutput = result.output;
|
|
10584
12495
|
t2Results = result.t2Results;
|
|
10585
12496
|
}
|
|
@@ -10614,7 +12525,7 @@ ${prompt}` : prompt;
|
|
|
10614
12525
|
if (this.taskAnalyzer) {
|
|
10615
12526
|
try {
|
|
10616
12527
|
const stats2 = this.router.getStats();
|
|
10617
|
-
this.taskAnalyzer.recordRunOutcome(runError ? "failure" : "success", stats2.costByTier);
|
|
12528
|
+
this.taskAnalyzer.recordRunOutcome(runError ? "failure" : "success", stats2.costByTier, stats2.totalTokens);
|
|
10618
12529
|
} catch {
|
|
10619
12530
|
}
|
|
10620
12531
|
}
|
|
@@ -10637,6 +12548,10 @@ ${prompt}` : prompt;
|
|
|
10637
12548
|
}
|
|
10638
12549
|
const stats = this.router.getStats();
|
|
10639
12550
|
const durationMs = Date.now() - startMs;
|
|
12551
|
+
if (!runError && this.config.memory?.rememberSessions) {
|
|
12552
|
+
void this.rememberSession(options, finalOutput).catch(() => {
|
|
12553
|
+
});
|
|
12554
|
+
}
|
|
10640
12555
|
return {
|
|
10641
12556
|
output: finalOutput,
|
|
10642
12557
|
sessionId: options.sessionId ?? "",
|
|
@@ -10655,6 +12570,121 @@ ${prompt}` : prompt;
|
|
|
10655
12570
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
10656
12571
|
};
|
|
10657
12572
|
}
|
|
12573
|
+
/**
|
|
12574
|
+
* Direct single-model reply — the "fast answer" path. No planning, no workers,
|
|
12575
|
+
* no tools, no artifact verification: the user's message (with recent history)
|
|
12576
|
+
* answered by one mid-tier model, streamed as the primary output and shaped
|
|
12577
|
+
* into the normal CascadeRunResult so persistence + the run receipt still work.
|
|
12578
|
+
*/
|
|
12579
|
+
async runFastAnswer(options, startMs, taskId, { auto = false } = {}) {
|
|
12580
|
+
const tier = "T2";
|
|
12581
|
+
const selector = this.router.getSelector();
|
|
12582
|
+
const model = options.fastAnswerModel ? selector.selectForTier(tier, options.fastAnswerModel) : selector.getCandidatesForTier(tier)[0] ?? selector.selectForTier(tier);
|
|
12583
|
+
if (!model) {
|
|
12584
|
+
throw new Error("No model is available for a fast answer \u2014 add a provider API key first.");
|
|
12585
|
+
}
|
|
12586
|
+
this.recordDecision(
|
|
12587
|
+
"model",
|
|
12588
|
+
`Fast answer${auto ? " (auto)" : ""} \u2192 ${model.provider}:${model.id} \u2014 direct single-model reply (no orchestration)`
|
|
12589
|
+
);
|
|
12590
|
+
this.emit("tier:status", { tierId: "fast", role: tier, status: "ACTIVE", model: `${model.provider}:${model.id}` });
|
|
12591
|
+
let identityPrompt = "";
|
|
12592
|
+
if (this.store) {
|
|
12593
|
+
const identityId = options.identityId || this.config.defaultIdentityId;
|
|
12594
|
+
if (identityId) {
|
|
12595
|
+
const identity = this.store.listIdentities().find((i) => i.id === identityId);
|
|
12596
|
+
if (identity?.systemPrompt) identityPrompt = identity.systemPrompt + "\n\n";
|
|
12597
|
+
}
|
|
12598
|
+
}
|
|
12599
|
+
const history = (options.conversationHistory ?? []).slice(-10);
|
|
12600
|
+
const userContent = options.images?.length ? [
|
|
12601
|
+
{ type: "text", text: options.prompt },
|
|
12602
|
+
...options.images.map((image) => ({ type: "image", image }))
|
|
12603
|
+
] : options.prompt;
|
|
12604
|
+
const messages = [...history, { role: "user", content: userContent }];
|
|
12605
|
+
const systemPrompt = identityPrompt + "You are Cascade in fast mode. Answer the user directly, accurately and concisely. You have no tools and cannot run code, browse, or create files \u2014 do not claim to.";
|
|
12606
|
+
let streamed = "";
|
|
12607
|
+
const requireVision = !!options.images?.length;
|
|
12608
|
+
let result;
|
|
12609
|
+
try {
|
|
12610
|
+
result = await this.router.generate(
|
|
12611
|
+
tier,
|
|
12612
|
+
{ messages, systemPrompt, maxTokens: 2048, ...requireVision ? {} : { model } },
|
|
12613
|
+
(chunk) => {
|
|
12614
|
+
streamed += chunk.text;
|
|
12615
|
+
this.emit("stream:token", { tierId: "fast", text: chunk.text, primary: true });
|
|
12616
|
+
options.streamCallback?.({ text: chunk.text, finishReason: null });
|
|
12617
|
+
},
|
|
12618
|
+
requireVision
|
|
12619
|
+
);
|
|
12620
|
+
} catch (err) {
|
|
12621
|
+
if (err instanceof CascadeCancelledError || options.signal?.aborted) {
|
|
12622
|
+
this.emit("run:cancelled", { taskId, reason: err instanceof Error ? err.message : "Task cancelled" });
|
|
12623
|
+
this.emit("tier:status", { tierId: "fast", role: tier, status: "COMPLETED", model: `${model.provider}:${model.id}` });
|
|
12624
|
+
const stats2 = this.router.getStats();
|
|
12625
|
+
return {
|
|
12626
|
+
output: streamed,
|
|
12627
|
+
sessionId: options.sessionId ?? "",
|
|
12628
|
+
taskId,
|
|
12629
|
+
usage: {
|
|
12630
|
+
inputTokens: 0,
|
|
12631
|
+
outputTokens: 0,
|
|
12632
|
+
totalTokens: stats2.totalTokens,
|
|
12633
|
+
estimatedCostUsd: stats2.totalCostUsd
|
|
12634
|
+
},
|
|
12635
|
+
t2Results: [],
|
|
12636
|
+
durationMs: Date.now() - startMs,
|
|
12637
|
+
costByTier: stats2.costByTier,
|
|
12638
|
+
tokensByTier: stats2.tokensByTier,
|
|
12639
|
+
costByFeature: stats2.costByFeature,
|
|
12640
|
+
costPercentByTier: this.router.getTierCostPercentages()
|
|
12641
|
+
};
|
|
12642
|
+
}
|
|
12643
|
+
throw err;
|
|
12644
|
+
} finally {
|
|
12645
|
+
this.router.setRunSignal(void 0);
|
|
12646
|
+
}
|
|
12647
|
+
this.emit("tier:status", { tierId: "fast", role: tier, status: "COMPLETED", model: `${model.provider}:${model.id}` });
|
|
12648
|
+
const stats = this.router.getStats();
|
|
12649
|
+
return {
|
|
12650
|
+
output: result.content || streamed,
|
|
12651
|
+
sessionId: options.sessionId ?? "",
|
|
12652
|
+
taskId,
|
|
12653
|
+
usage: {
|
|
12654
|
+
inputTokens: result.usage?.inputTokens ?? 0,
|
|
12655
|
+
outputTokens: result.usage?.outputTokens ?? 0,
|
|
12656
|
+
totalTokens: stats.totalTokens,
|
|
12657
|
+
estimatedCostUsd: stats.totalCostUsd
|
|
12658
|
+
},
|
|
12659
|
+
t2Results: [],
|
|
12660
|
+
durationMs: Date.now() - startMs,
|
|
12661
|
+
costByTier: stats.costByTier,
|
|
12662
|
+
tokensByTier: stats.tokensByTier,
|
|
12663
|
+
costByFeature: stats.costByFeature,
|
|
12664
|
+
costPercentByTier: this.router.getTierCostPercentages()
|
|
12665
|
+
};
|
|
12666
|
+
}
|
|
12667
|
+
/**
|
|
12668
|
+
* Distill a completed session into durable facts and store them in the
|
|
12669
|
+
* project knowledge graph (opt-in via memory.rememberSessions). Best-effort:
|
|
12670
|
+
* skips trivial exchanges, uses one cheap T3 call, and swallows all errors.
|
|
12671
|
+
*/
|
|
12672
|
+
async rememberSession(options, output) {
|
|
12673
|
+
const db = this.router.getWorldStateDB?.();
|
|
12674
|
+
if (!db) return;
|
|
12675
|
+
const history = options.conversationHistory ?? [];
|
|
12676
|
+
if (!sessionWorthRemembering(history, options.prompt, output)) return;
|
|
12677
|
+
const transcript = buildSessionTranscript(history, options.prompt, output);
|
|
12678
|
+
const facts = await distillSessionFacts(transcript, async (prompt) => {
|
|
12679
|
+
const res = await this.router.generate("T3", {
|
|
12680
|
+
messages: [{ role: "user", content: prompt }],
|
|
12681
|
+
maxTokens: 300,
|
|
12682
|
+
temperature: 0
|
|
12683
|
+
});
|
|
12684
|
+
return res.content;
|
|
12685
|
+
});
|
|
12686
|
+
for (const f of facts) db.upsertFact(f.entity, f.relation, f.value, "session-memory");
|
|
12687
|
+
}
|
|
10658
12688
|
getRouter() {
|
|
10659
12689
|
return this.router;
|
|
10660
12690
|
}
|
|
@@ -10719,7 +12749,7 @@ var Keystore = class {
|
|
|
10719
12749
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
10720
12750
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
10721
12751
|
this.backend = "keytar";
|
|
10722
|
-
if (password &&
|
|
12752
|
+
if (password && fs23.existsSync(this.storePath)) {
|
|
10723
12753
|
try {
|
|
10724
12754
|
const fileEntries = this.decryptFile(password);
|
|
10725
12755
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -10738,8 +12768,8 @@ var Keystore = class {
|
|
|
10738
12768
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
10739
12769
|
);
|
|
10740
12770
|
}
|
|
10741
|
-
if (!
|
|
10742
|
-
const salt =
|
|
12771
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
12772
|
+
const salt = crypto4.randomBytes(SALT_LEN);
|
|
10743
12773
|
this.masterKey = this.deriveKey(password, salt);
|
|
10744
12774
|
this.writeWithSalt({}, salt);
|
|
10745
12775
|
this.cache = {};
|
|
@@ -10752,8 +12782,8 @@ var Keystore = class {
|
|
|
10752
12782
|
}
|
|
10753
12783
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
10754
12784
|
unlockSync(password) {
|
|
10755
|
-
if (!
|
|
10756
|
-
const salt =
|
|
12785
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
12786
|
+
const salt = crypto4.randomBytes(SALT_LEN);
|
|
10757
12787
|
this.masterKey = this.deriveKey(password, salt);
|
|
10758
12788
|
this.writeWithSalt({}, salt);
|
|
10759
12789
|
this.cache = {};
|
|
@@ -10810,12 +12840,12 @@ var Keystore = class {
|
|
|
10810
12840
|
}
|
|
10811
12841
|
}
|
|
10812
12842
|
decryptFile(password, knownSalt) {
|
|
10813
|
-
if (!
|
|
12843
|
+
if (!fs23.existsSync(this.storePath)) return {};
|
|
10814
12844
|
try {
|
|
10815
12845
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
10816
12846
|
const useSalt = knownSalt ?? salt;
|
|
10817
12847
|
const key = this.masterKey ?? this.deriveKey(password, useSalt);
|
|
10818
|
-
const decipher =
|
|
12848
|
+
const decipher = crypto4.createDecipheriv(ALGORITHM, key, iv);
|
|
10819
12849
|
decipher.setAuthTag(tag);
|
|
10820
12850
|
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
10821
12851
|
return JSON.parse(decrypted.toString("utf-8"));
|
|
@@ -10826,28 +12856,28 @@ var Keystore = class {
|
|
|
10826
12856
|
saveAll(data) {
|
|
10827
12857
|
if (!this.masterKey) return;
|
|
10828
12858
|
const raw = this.readRaw();
|
|
10829
|
-
const iv =
|
|
10830
|
-
const cipher =
|
|
12859
|
+
const iv = crypto4.randomBytes(IV_LEN);
|
|
12860
|
+
const cipher = crypto4.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
10831
12861
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
10832
12862
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10833
12863
|
const tag = cipher.getAuthTag();
|
|
10834
12864
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
10835
|
-
|
|
10836
|
-
|
|
12865
|
+
fs23.mkdirSync(path24.dirname(this.storePath), { recursive: true });
|
|
12866
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10837
12867
|
}
|
|
10838
12868
|
writeWithSalt(data, salt) {
|
|
10839
12869
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
10840
|
-
const iv =
|
|
10841
|
-
const cipher =
|
|
12870
|
+
const iv = crypto4.randomBytes(IV_LEN);
|
|
12871
|
+
const cipher = crypto4.createCipheriv(ALGORITHM, this.masterKey, iv);
|
|
10842
12872
|
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
10843
12873
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10844
12874
|
const tag = cipher.getAuthTag();
|
|
10845
12875
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
10846
|
-
|
|
10847
|
-
|
|
12876
|
+
fs23.mkdirSync(path24.dirname(this.storePath), { recursive: true });
|
|
12877
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10848
12878
|
}
|
|
10849
12879
|
readRaw() {
|
|
10850
|
-
const buf =
|
|
12880
|
+
const buf = fs23.readFileSync(this.storePath);
|
|
10851
12881
|
let offset = 0;
|
|
10852
12882
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
10853
12883
|
offset += SALT_LEN;
|
|
@@ -10859,50 +12889,11 @@ var Keystore = class {
|
|
|
10859
12889
|
return { salt, iv, tag, ciphertext };
|
|
10860
12890
|
}
|
|
10861
12891
|
deriveKey(password, salt) {
|
|
10862
|
-
return
|
|
10863
|
-
}
|
|
10864
|
-
};
|
|
10865
|
-
var ignore3 = ignoreFactory.default ?? ignoreFactory;
|
|
10866
|
-
var CascadeIgnore = class {
|
|
10867
|
-
ig;
|
|
10868
|
-
loaded = false;
|
|
10869
|
-
constructor() {
|
|
10870
|
-
this.ig = ignore3();
|
|
10871
|
-
this.ig.add([
|
|
10872
|
-
".cascade/keystore.enc",
|
|
10873
|
-
".cascade/memory.db",
|
|
10874
|
-
".env",
|
|
10875
|
-
".env.*",
|
|
10876
|
-
"*.pem",
|
|
10877
|
-
"*.key",
|
|
10878
|
-
"id_rsa",
|
|
10879
|
-
"id_ed25519"
|
|
10880
|
-
]);
|
|
10881
|
-
}
|
|
10882
|
-
async load(workspacePath) {
|
|
10883
|
-
const filePath = path21.join(workspacePath, ".cascadeignore");
|
|
10884
|
-
try {
|
|
10885
|
-
const content = await fs4.readFile(filePath, "utf-8");
|
|
10886
|
-
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
10887
|
-
this.ig.add(lines);
|
|
10888
|
-
this.loaded = true;
|
|
10889
|
-
} catch {
|
|
10890
|
-
}
|
|
10891
|
-
}
|
|
10892
|
-
isIgnored(filePath, workspacePath) {
|
|
10893
|
-
try {
|
|
10894
|
-
const relative = workspacePath ? path21.relative(workspacePath, filePath) : filePath;
|
|
10895
|
-
return this.ig.ignores(relative);
|
|
10896
|
-
} catch {
|
|
10897
|
-
return false;
|
|
10898
|
-
}
|
|
10899
|
-
}
|
|
10900
|
-
getPatterns() {
|
|
10901
|
-
return this.ig._rules?.map((r) => r.pattern) ?? [];
|
|
12892
|
+
return crypto4.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
|
|
10902
12893
|
}
|
|
10903
12894
|
};
|
|
10904
12895
|
async function loadCascadeMd(workspacePath) {
|
|
10905
|
-
const filePath =
|
|
12896
|
+
const filePath = path24.join(workspacePath, "CASCADE.md");
|
|
10906
12897
|
try {
|
|
10907
12898
|
const raw = await fs4.readFile(filePath, "utf-8");
|
|
10908
12899
|
return parseCascadeMd(raw);
|
|
@@ -10933,7 +12924,7 @@ ${raw.trim()}`;
|
|
|
10933
12924
|
var MemoryStore = class _MemoryStore {
|
|
10934
12925
|
db;
|
|
10935
12926
|
constructor(dbPath) {
|
|
10936
|
-
|
|
12927
|
+
fs23.mkdirSync(path24.dirname(dbPath), { recursive: true });
|
|
10937
12928
|
try {
|
|
10938
12929
|
this.db = new Database(dbPath, { timeout: 5e3 });
|
|
10939
12930
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -11768,8 +13759,8 @@ Original error: ${err.message}`
|
|
|
11768
13759
|
};
|
|
11769
13760
|
}
|
|
11770
13761
|
};
|
|
11771
|
-
function credentialsPath(
|
|
11772
|
-
return
|
|
13762
|
+
function credentialsPath(globalDir2) {
|
|
13763
|
+
return path24.join(globalDir2, GLOBAL_CREDENTIALS_FILE);
|
|
11773
13764
|
}
|
|
11774
13765
|
function providerKey(p) {
|
|
11775
13766
|
if (p.type === "azure") {
|
|
@@ -11780,9 +13771,9 @@ function providerKey(p) {
|
|
|
11780
13771
|
function isPersistable(p) {
|
|
11781
13772
|
return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
|
|
11782
13773
|
}
|
|
11783
|
-
function loadGlobalCredentials(
|
|
13774
|
+
function loadGlobalCredentials(globalDir2) {
|
|
11784
13775
|
try {
|
|
11785
|
-
const raw =
|
|
13776
|
+
const raw = fs23.readFileSync(credentialsPath(globalDir2), "utf-8");
|
|
11786
13777
|
const parsed = JSON.parse(raw);
|
|
11787
13778
|
if (!Array.isArray(parsed.providers)) return [];
|
|
11788
13779
|
return parsed.providers.filter(
|
|
@@ -11792,15 +13783,15 @@ function loadGlobalCredentials(globalDir) {
|
|
|
11792
13783
|
return [];
|
|
11793
13784
|
}
|
|
11794
13785
|
}
|
|
11795
|
-
function saveGlobalCredentials(
|
|
11796
|
-
const filePath = credentialsPath(
|
|
11797
|
-
|
|
13786
|
+
function saveGlobalCredentials(globalDir2, providers) {
|
|
13787
|
+
const filePath = credentialsPath(globalDir2);
|
|
13788
|
+
fs23.mkdirSync(globalDir2, { recursive: true, mode: 448 });
|
|
11798
13789
|
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
11799
13790
|
const tmp = `${filePath}.tmp`;
|
|
11800
|
-
|
|
11801
|
-
|
|
13791
|
+
fs23.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
13792
|
+
fs23.renameSync(tmp, filePath);
|
|
11802
13793
|
try {
|
|
11803
|
-
|
|
13794
|
+
fs23.chmodSync(filePath, 384);
|
|
11804
13795
|
} catch {
|
|
11805
13796
|
}
|
|
11806
13797
|
}
|
|
@@ -11835,15 +13826,15 @@ var ConfigManager = class {
|
|
|
11835
13826
|
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
11836
13827
|
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
11837
13828
|
this.workspacePath = workspacePath;
|
|
11838
|
-
this.globalDir = globalDirOverride ??
|
|
13829
|
+
this.globalDir = globalDirOverride ?? path24.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
11839
13830
|
}
|
|
11840
13831
|
async load() {
|
|
11841
13832
|
this.config = await this.loadConfig();
|
|
11842
13833
|
this.ignore = new CascadeIgnore();
|
|
11843
13834
|
await this.ignore.load(this.workspacePath);
|
|
11844
13835
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
11845
|
-
this.keystore = new Keystore(
|
|
11846
|
-
this.store = new MemoryStore(
|
|
13836
|
+
this.keystore = new Keystore(path24.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
13837
|
+
this.store = new MemoryStore(path24.join(this.workspacePath, CASCADE_DB_FILE));
|
|
11847
13838
|
await this.injectEnvKeys();
|
|
11848
13839
|
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
11849
13840
|
await this.ensureDefaultIdentity();
|
|
@@ -11867,8 +13858,8 @@ var ConfigManager = class {
|
|
|
11867
13858
|
return this.workspacePath;
|
|
11868
13859
|
}
|
|
11869
13860
|
async save() {
|
|
11870
|
-
const configPath =
|
|
11871
|
-
await fs4.mkdir(
|
|
13861
|
+
const configPath = path24.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
13862
|
+
await fs4.mkdir(path24.dirname(configPath), { recursive: true });
|
|
11872
13863
|
await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
11873
13864
|
try {
|
|
11874
13865
|
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
@@ -11897,7 +13888,7 @@ var ConfigManager = class {
|
|
|
11897
13888
|
return configProvider?.apiKey;
|
|
11898
13889
|
}
|
|
11899
13890
|
async loadConfig() {
|
|
11900
|
-
const configPath =
|
|
13891
|
+
const configPath = path24.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11901
13892
|
try {
|
|
11902
13893
|
const raw = await fs4.readFile(configPath, "utf-8");
|
|
11903
13894
|
return validateConfig(JSON.parse(raw));
|
|
@@ -12317,7 +14308,7 @@ function aggregateCostStats(sessions, opts = {}) {
|
|
|
12317
14308
|
}
|
|
12318
14309
|
|
|
12319
14310
|
// src/dashboard/server.ts
|
|
12320
|
-
var __dirname$1 =
|
|
14311
|
+
var __dirname$1 = path24.dirname(fileURLToPath(import.meta.url));
|
|
12321
14312
|
var DashboardServer = class {
|
|
12322
14313
|
app;
|
|
12323
14314
|
httpServer;
|
|
@@ -12551,14 +14542,14 @@ var DashboardServer = class {
|
|
|
12551
14542
|
*/
|
|
12552
14543
|
persistConfig() {
|
|
12553
14544
|
try {
|
|
12554
|
-
const configPath =
|
|
12555
|
-
|
|
12556
|
-
|
|
14545
|
+
const configPath = path24.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
14546
|
+
fs23.mkdirSync(path24.dirname(configPath), { recursive: true });
|
|
14547
|
+
fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
12557
14548
|
} catch (err) {
|
|
12558
14549
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
12559
14550
|
}
|
|
12560
14551
|
try {
|
|
12561
|
-
saveGlobalCredentials(
|
|
14552
|
+
saveGlobalCredentials(path24.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
12562
14553
|
} catch (err) {
|
|
12563
14554
|
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
12564
14555
|
}
|
|
@@ -12573,15 +14564,15 @@ var DashboardServer = class {
|
|
|
12573
14564
|
resolveDashboardSecret() {
|
|
12574
14565
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
12575
14566
|
if (fromConfig) return fromConfig;
|
|
12576
|
-
const secretPath =
|
|
14567
|
+
const secretPath = path24.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
12577
14568
|
try {
|
|
12578
|
-
if (
|
|
12579
|
-
const existing =
|
|
14569
|
+
if (fs23.existsSync(secretPath)) {
|
|
14570
|
+
const existing = fs23.readFileSync(secretPath, "utf-8").trim();
|
|
12580
14571
|
if (existing.length >= 16) return existing;
|
|
12581
14572
|
}
|
|
12582
14573
|
const generated = randomUUID();
|
|
12583
|
-
|
|
12584
|
-
|
|
14574
|
+
fs23.mkdirSync(path24.dirname(secretPath), { recursive: true });
|
|
14575
|
+
fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
12585
14576
|
if (this.config.dashboard.auth) {
|
|
12586
14577
|
console.warn(
|
|
12587
14578
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -12608,7 +14599,7 @@ var DashboardServer = class {
|
|
|
12608
14599
|
// ── Setup ─────────────────────────────────────
|
|
12609
14600
|
getGlobalStore() {
|
|
12610
14601
|
if (!this.globalStore) {
|
|
12611
|
-
const globalDbPath =
|
|
14602
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12612
14603
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
12613
14604
|
}
|
|
12614
14605
|
return this.globalStore;
|
|
@@ -12877,12 +14868,12 @@ ${prompt}`;
|
|
|
12877
14868
|
}
|
|
12878
14869
|
}
|
|
12879
14870
|
watchRuntimeChanges() {
|
|
12880
|
-
const workspaceDbPath =
|
|
12881
|
-
const globalDbPath =
|
|
14871
|
+
const workspaceDbPath = path24.join(this.workspacePath, CASCADE_DB_FILE);
|
|
14872
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12882
14873
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
12883
14874
|
for (const watchPath of watchPaths) {
|
|
12884
|
-
if (!
|
|
12885
|
-
|
|
14875
|
+
if (!fs23.existsSync(watchPath)) continue;
|
|
14876
|
+
fs23.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
12886
14877
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
12887
14878
|
});
|
|
12888
14879
|
}
|
|
@@ -12904,7 +14895,7 @@ ${prompt}`;
|
|
|
12904
14895
|
}
|
|
12905
14896
|
setupRoutes() {
|
|
12906
14897
|
const authRequired = this.config.dashboard.auth;
|
|
12907
|
-
const
|
|
14898
|
+
const auth2 = authMiddleware(this.dashboardSecret, authRequired);
|
|
12908
14899
|
const passwordHash = authRequired ? this.resolvePasswordHash() : null;
|
|
12909
14900
|
const loginLimiter = rateLimit({
|
|
12910
14901
|
windowMs: 15 * 60 * 1e3,
|
|
@@ -12963,7 +14954,7 @@ ${prompt}`;
|
|
|
12963
14954
|
res.status(401).json({ error: "Invalid credentials" });
|
|
12964
14955
|
}
|
|
12965
14956
|
});
|
|
12966
|
-
this.app.post("/api/force-halt",
|
|
14957
|
+
this.app.post("/api/force-halt", auth2, mutationLimiter, (req, res) => {
|
|
12967
14958
|
const body = req.body;
|
|
12968
14959
|
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
12969
14960
|
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
@@ -12972,7 +14963,7 @@ ${prompt}`;
|
|
|
12972
14963
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
12973
14964
|
res.json({ success: true, ...payload });
|
|
12974
14965
|
});
|
|
12975
|
-
this.app.post("/api/inject",
|
|
14966
|
+
this.app.post("/api/inject", auth2, mutationLimiter, (req, res) => {
|
|
12976
14967
|
const body = req.body;
|
|
12977
14968
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
12978
14969
|
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
@@ -12987,11 +14978,11 @@ ${prompt}`;
|
|
|
12987
14978
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
12988
14979
|
res.json({ success: true, ...payload });
|
|
12989
14980
|
});
|
|
12990
|
-
this.app.get("/api/sessions",
|
|
14981
|
+
this.app.get("/api/sessions", auth2, (_req, res) => {
|
|
12991
14982
|
const sessions = this.store.listSessions();
|
|
12992
14983
|
res.json(sessions);
|
|
12993
14984
|
});
|
|
12994
|
-
this.app.get("/api/sessions/:id",
|
|
14985
|
+
this.app.get("/api/sessions/:id", auth2, (req, res) => {
|
|
12995
14986
|
const id = req.params.id;
|
|
12996
14987
|
const session = this.store.getSession(id);
|
|
12997
14988
|
if (!session) {
|
|
@@ -13000,11 +14991,11 @@ ${prompt}`;
|
|
|
13000
14991
|
}
|
|
13001
14992
|
res.json(session);
|
|
13002
14993
|
});
|
|
13003
|
-
this.app.delete("/api/sessions/:id",
|
|
14994
|
+
this.app.delete("/api/sessions/:id", auth2, (req, res) => {
|
|
13004
14995
|
const sessionId = req.params.id;
|
|
13005
14996
|
this.store.deleteSession(sessionId);
|
|
13006
14997
|
this.store.deleteRuntimeSession(sessionId);
|
|
13007
|
-
const globalDbPath =
|
|
14998
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
13008
14999
|
const globalStore = new MemoryStore(globalDbPath);
|
|
13009
15000
|
try {
|
|
13010
15001
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -13016,7 +15007,7 @@ ${prompt}`;
|
|
|
13016
15007
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
13017
15008
|
res.json({ ok: true });
|
|
13018
15009
|
});
|
|
13019
|
-
this.app.get("/api/export",
|
|
15010
|
+
this.app.get("/api/export", auth2, (req, res) => {
|
|
13020
15011
|
try {
|
|
13021
15012
|
const sessionsParam = typeof req.query["sessions"] === "string" ? req.query["sessions"] : "all";
|
|
13022
15013
|
const includeMemories = req.query["memories"] === "1" || req.query["memories"] === "true";
|
|
@@ -13039,7 +15030,7 @@ ${prompt}`;
|
|
|
13039
15030
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13040
15031
|
}
|
|
13041
15032
|
});
|
|
13042
|
-
this.app.post("/api/import",
|
|
15033
|
+
this.app.post("/api/import", auth2, mutationLimiter, (req, res) => {
|
|
13043
15034
|
try {
|
|
13044
15035
|
const bundle = req.body;
|
|
13045
15036
|
if (!bundle || bundle.format !== "cascade-export@1") {
|
|
@@ -13070,7 +15061,7 @@ ${prompt}`;
|
|
|
13070
15061
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13071
15062
|
}
|
|
13072
15063
|
});
|
|
13073
|
-
this.app.post("/api/sessions/:id/rollback",
|
|
15064
|
+
this.app.post("/api/sessions/:id/rollback", auth2, mutationLimiter, async (req, res) => {
|
|
13074
15065
|
const sessionId = req.params.id;
|
|
13075
15066
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13076
15067
|
if (!taskIds.length) {
|
|
@@ -13095,9 +15086,9 @@ ${prompt}`;
|
|
|
13095
15086
|
}
|
|
13096
15087
|
res.json({ ok: true, restored });
|
|
13097
15088
|
});
|
|
13098
|
-
this.app.delete("/api/sessions",
|
|
15089
|
+
this.app.delete("/api/sessions", auth2, (req, res) => {
|
|
13099
15090
|
const body = req.body;
|
|
13100
|
-
const globalDbPath =
|
|
15091
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
13101
15092
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
13102
15093
|
const globalStore = new MemoryStore(globalDbPath);
|
|
13103
15094
|
try {
|
|
@@ -13118,9 +15109,9 @@ ${prompt}`;
|
|
|
13118
15109
|
this.socket.broadcast("runtime:refresh", { scope: "workspace" });
|
|
13119
15110
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
13120
15111
|
});
|
|
13121
|
-
this.app.delete("/api/runtime",
|
|
15112
|
+
this.app.delete("/api/runtime", auth2, (_req, res) => {
|
|
13122
15113
|
this.store.deleteAllRuntimeNodes();
|
|
13123
|
-
const globalDbPath =
|
|
15114
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
13124
15115
|
const globalStore = new MemoryStore(globalDbPath);
|
|
13125
15116
|
try {
|
|
13126
15117
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -13131,10 +15122,10 @@ ${prompt}`;
|
|
|
13131
15122
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
13132
15123
|
res.json({ ok: true });
|
|
13133
15124
|
});
|
|
13134
|
-
this.app.get("/api/identities",
|
|
15125
|
+
this.app.get("/api/identities", auth2, (_req, res) => {
|
|
13135
15126
|
res.json(this.store.listIdentities());
|
|
13136
15127
|
});
|
|
13137
|
-
this.app.post("/api/identities",
|
|
15128
|
+
this.app.post("/api/identities", auth2, (req, res) => {
|
|
13138
15129
|
const body = req.body;
|
|
13139
15130
|
if (!body.name) {
|
|
13140
15131
|
res.status(400).json({ error: "name is required" });
|
|
@@ -13156,7 +15147,7 @@ ${prompt}`;
|
|
|
13156
15147
|
this.store.createIdentity(identity);
|
|
13157
15148
|
res.json(identity);
|
|
13158
15149
|
});
|
|
13159
|
-
this.app.put("/api/identities/:id",
|
|
15150
|
+
this.app.put("/api/identities/:id", auth2, (req, res) => {
|
|
13160
15151
|
const identityId = req.params.id;
|
|
13161
15152
|
const body = req.body;
|
|
13162
15153
|
if (body.setDefault) {
|
|
@@ -13167,11 +15158,11 @@ ${prompt}`;
|
|
|
13167
15158
|
this.store.updateIdentity(identityId, body);
|
|
13168
15159
|
res.json({ ok: true });
|
|
13169
15160
|
});
|
|
13170
|
-
this.app.delete("/api/identities/:id",
|
|
15161
|
+
this.app.delete("/api/identities/:id", auth2, (req, res) => {
|
|
13171
15162
|
this.store.deleteIdentity(req.params.id);
|
|
13172
15163
|
res.json({ ok: true });
|
|
13173
15164
|
});
|
|
13174
|
-
this.app.get("/api/audit/verify",
|
|
15165
|
+
this.app.get("/api/audit/verify", auth2, async (_req, res) => {
|
|
13175
15166
|
try {
|
|
13176
15167
|
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
13177
15168
|
const logger = new AuditLogger3(this.workspacePath);
|
|
@@ -13184,16 +15175,16 @@ ${prompt}`;
|
|
|
13184
15175
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13185
15176
|
}
|
|
13186
15177
|
});
|
|
13187
|
-
this.app.get("/api/audit/:sessionId",
|
|
15178
|
+
this.app.get("/api/audit/:sessionId", auth2, (req, res) => {
|
|
13188
15179
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
13189
15180
|
res.json(log);
|
|
13190
15181
|
});
|
|
13191
|
-
this.app.get("/api/config",
|
|
15182
|
+
this.app.get("/api/config", auth2, (_req, res) => {
|
|
13192
15183
|
const safe = { ...this.config };
|
|
13193
15184
|
safe.providers = safe.providers.map((p) => ({ ...p, apiKey: p.apiKey ? "***" : void 0 }));
|
|
13194
15185
|
res.json(safe);
|
|
13195
15186
|
});
|
|
13196
|
-
this.app.put("/api/config",
|
|
15187
|
+
this.app.put("/api/config", auth2, async (req, res) => {
|
|
13197
15188
|
const body = req.body;
|
|
13198
15189
|
if (body["tierLimits"] !== void 0 && (typeof body["tierLimits"] !== "object" || Array.isArray(body["tierLimits"]))) {
|
|
13199
15190
|
res.status(400).json({ error: "tierLimits must be an object" });
|
|
@@ -13206,18 +15197,18 @@ ${prompt}`;
|
|
|
13206
15197
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
13207
15198
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
13208
15199
|
try {
|
|
13209
|
-
const configPath =
|
|
13210
|
-
const existing =
|
|
15200
|
+
const configPath = path24.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
15201
|
+
const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
|
|
13211
15202
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
13212
15203
|
const tmp = configPath + ".tmp";
|
|
13213
|
-
|
|
13214
|
-
|
|
15204
|
+
fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
15205
|
+
fs23.renameSync(tmp, configPath);
|
|
13215
15206
|
res.json({ ok: true });
|
|
13216
15207
|
} catch (err) {
|
|
13217
15208
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
13218
15209
|
}
|
|
13219
15210
|
});
|
|
13220
|
-
this.app.get("/api/runtime/logs/:sessionId",
|
|
15211
|
+
this.app.get("/api/runtime/logs/:sessionId", auth2, (req, res) => {
|
|
13221
15212
|
const sessionId = req.params["sessionId"];
|
|
13222
15213
|
const before = req.query["before"];
|
|
13223
15214
|
const limitStr = req.query["limit"];
|
|
@@ -13228,7 +15219,7 @@ ${prompt}`;
|
|
|
13228
15219
|
);
|
|
13229
15220
|
res.json(logs);
|
|
13230
15221
|
});
|
|
13231
|
-
this.app.get("/api/stats",
|
|
15222
|
+
this.app.get("/api/stats", auth2, (_req, res) => {
|
|
13232
15223
|
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13233
15224
|
res.json({
|
|
13234
15225
|
totalSessions: sessions.length,
|
|
@@ -13236,10 +15227,10 @@ ${prompt}`;
|
|
|
13236
15227
|
totalCostUsd: sessions.reduce((acc, s) => acc + s.metadata.totalCostUsd, 0)
|
|
13237
15228
|
});
|
|
13238
15229
|
});
|
|
13239
|
-
this.app.get("/api/runtime",
|
|
15230
|
+
this.app.get("/api/runtime", auth2, (req, res) => {
|
|
13240
15231
|
const scope = req.query["scope"] ?? "workspace";
|
|
13241
15232
|
if (scope === "global") {
|
|
13242
|
-
const globalDbPath =
|
|
15233
|
+
const globalDbPath = path24.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
13243
15234
|
const globalStore = new MemoryStore(globalDbPath);
|
|
13244
15235
|
try {
|
|
13245
15236
|
res.json({
|
|
@@ -13260,7 +15251,7 @@ ${prompt}`;
|
|
|
13260
15251
|
logs: this.store.listRuntimeNodeLogs(void 0, void 0, 500)
|
|
13261
15252
|
});
|
|
13262
15253
|
});
|
|
13263
|
-
this.app.post("/api/run",
|
|
15254
|
+
this.app.post("/api/run", auth2, mutationLimiter, (req, res) => {
|
|
13264
15255
|
const body = req.body;
|
|
13265
15256
|
if (!body.prompt || typeof body.prompt !== "string") {
|
|
13266
15257
|
res.status(400).json({ error: "prompt is required" });
|
|
@@ -13319,7 +15310,7 @@ ${prompt}`;
|
|
|
13319
15310
|
}
|
|
13320
15311
|
})();
|
|
13321
15312
|
});
|
|
13322
|
-
this.app.get("/api/models",
|
|
15313
|
+
this.app.get("/api/models", auth2, (_req, res) => {
|
|
13323
15314
|
res.json({
|
|
13324
15315
|
t1: this.config.models?.t1 ?? "auto",
|
|
13325
15316
|
t2: this.config.models?.t2 ?? "auto",
|
|
@@ -13330,7 +15321,7 @@ ${prompt}`;
|
|
|
13330
15321
|
}))
|
|
13331
15322
|
});
|
|
13332
15323
|
});
|
|
13333
|
-
this.app.get("/api/sessions/:id/why",
|
|
15324
|
+
this.app.get("/api/sessions/:id/why", auth2, (req, res) => {
|
|
13334
15325
|
const report = this.whyBySession.get(req.params.id);
|
|
13335
15326
|
if (!report) {
|
|
13336
15327
|
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
@@ -13338,7 +15329,7 @@ ${prompt}`;
|
|
|
13338
15329
|
}
|
|
13339
15330
|
res.json(report);
|
|
13340
15331
|
});
|
|
13341
|
-
this.app.get("/api/sessions/:id/changes",
|
|
15332
|
+
this.app.get("/api/sessions/:id/changes", auth2, async (req, res) => {
|
|
13342
15333
|
const sessionId = req.params.id;
|
|
13343
15334
|
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13344
15335
|
const before = /* @__PURE__ */ new Map();
|
|
@@ -13362,7 +15353,7 @@ ${prompt}`;
|
|
|
13362
15353
|
}));
|
|
13363
15354
|
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
13364
15355
|
});
|
|
13365
|
-
this.app.post("/api/sessions/:id/revert-file",
|
|
15356
|
+
this.app.post("/api/sessions/:id/revert-file", auth2, mutationLimiter, async (req, res) => {
|
|
13366
15357
|
const sessionId = req.params.id;
|
|
13367
15358
|
const body = req.body;
|
|
13368
15359
|
if (!body.filePath || typeof body.filePath !== "string") {
|
|
@@ -13390,7 +15381,7 @@ ${prompt}`;
|
|
|
13390
15381
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13391
15382
|
}
|
|
13392
15383
|
});
|
|
13393
|
-
this.app.get("/api/costs",
|
|
15384
|
+
this.app.get("/api/costs", auth2, (_req, res) => {
|
|
13394
15385
|
try {
|
|
13395
15386
|
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13396
15387
|
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
@@ -13406,14 +15397,14 @@ ${prompt}`;
|
|
|
13406
15397
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13407
15398
|
}
|
|
13408
15399
|
});
|
|
13409
|
-
this.app.get("/api/schedules",
|
|
15400
|
+
this.app.get("/api/schedules", auth2, (_req, res) => {
|
|
13410
15401
|
try {
|
|
13411
15402
|
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
13412
15403
|
} catch (err) {
|
|
13413
15404
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13414
15405
|
}
|
|
13415
15406
|
});
|
|
13416
|
-
this.app.post("/api/schedules",
|
|
15407
|
+
this.app.post("/api/schedules", auth2, mutationLimiter, (req, res) => {
|
|
13417
15408
|
const body = req.body;
|
|
13418
15409
|
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
13419
15410
|
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
@@ -13439,7 +15430,7 @@ ${prompt}`;
|
|
|
13439
15430
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13440
15431
|
}
|
|
13441
15432
|
});
|
|
13442
|
-
this.app.put("/api/schedules/:id",
|
|
15433
|
+
this.app.put("/api/schedules/:id", auth2, mutationLimiter, (req, res) => {
|
|
13443
15434
|
const id = req.params.id;
|
|
13444
15435
|
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
13445
15436
|
if (!existing) {
|
|
@@ -13466,7 +15457,7 @@ ${prompt}`;
|
|
|
13466
15457
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13467
15458
|
}
|
|
13468
15459
|
});
|
|
13469
|
-
this.app.delete("/api/schedules/:id",
|
|
15460
|
+
this.app.delete("/api/schedules/:id", auth2, mutationLimiter, (req, res) => {
|
|
13470
15461
|
try {
|
|
13471
15462
|
this.scheduler.remove(req.params.id);
|
|
13472
15463
|
res.json({ ok: true });
|
|
@@ -13474,7 +15465,7 @@ ${prompt}`;
|
|
|
13474
15465
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13475
15466
|
}
|
|
13476
15467
|
});
|
|
13477
|
-
this.app.get("/api/knowledge",
|
|
15468
|
+
this.app.get("/api/knowledge", auth2, (_req, res) => {
|
|
13478
15469
|
try {
|
|
13479
15470
|
const ws = new WorldStateDB(this.workspacePath);
|
|
13480
15471
|
try {
|
|
@@ -13487,7 +15478,7 @@ ${prompt}`;
|
|
|
13487
15478
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13488
15479
|
}
|
|
13489
15480
|
});
|
|
13490
|
-
this.app.delete("/api/knowledge/fact",
|
|
15481
|
+
this.app.delete("/api/knowledge/fact", auth2, mutationLimiter, (req, res) => {
|
|
13491
15482
|
const body = req.body;
|
|
13492
15483
|
if (!body.entity || !body.relation) {
|
|
13493
15484
|
res.status(400).json({ error: "entity and relation are required" });
|
|
@@ -13504,7 +15495,7 @@ ${prompt}`;
|
|
|
13504
15495
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13505
15496
|
}
|
|
13506
15497
|
});
|
|
13507
|
-
this.app.delete("/api/knowledge",
|
|
15498
|
+
this.app.delete("/api/knowledge", auth2, mutationLimiter, (_req, res) => {
|
|
13508
15499
|
try {
|
|
13509
15500
|
const ws = new WorldStateDB(this.workspacePath);
|
|
13510
15501
|
try {
|
|
@@ -13516,7 +15507,42 @@ ${prompt}`;
|
|
|
13516
15507
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13517
15508
|
}
|
|
13518
15509
|
});
|
|
13519
|
-
this.app.get("/api/
|
|
15510
|
+
this.app.get("/api/knowledge/fact/history", auth2, (req, res) => {
|
|
15511
|
+
const entity = req.query["entity"];
|
|
15512
|
+
const relation = req.query["relation"];
|
|
15513
|
+
if (typeof entity !== "string" || typeof relation !== "string") {
|
|
15514
|
+
res.status(400).json({ error: "entity and relation are required" });
|
|
15515
|
+
return;
|
|
15516
|
+
}
|
|
15517
|
+
try {
|
|
15518
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
15519
|
+
try {
|
|
15520
|
+
res.json({ history: ws.getFactHistory(entity, relation) });
|
|
15521
|
+
} finally {
|
|
15522
|
+
ws.close();
|
|
15523
|
+
}
|
|
15524
|
+
} catch (err) {
|
|
15525
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
15526
|
+
}
|
|
15527
|
+
});
|
|
15528
|
+
this.app.post("/api/knowledge/fact/restore", auth2, mutationLimiter, (req, res) => {
|
|
15529
|
+
const body = req.body;
|
|
15530
|
+
if (!body.entity || !body.relation) {
|
|
15531
|
+
res.status(400).json({ error: "entity and relation are required" });
|
|
15532
|
+
return;
|
|
15533
|
+
}
|
|
15534
|
+
try {
|
|
15535
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
15536
|
+
try {
|
|
15537
|
+
res.json({ ok: true, restored: ws.restoreFact(body.entity, body.relation, body.validFrom) });
|
|
15538
|
+
} finally {
|
|
15539
|
+
ws.close();
|
|
15540
|
+
}
|
|
15541
|
+
} catch (err) {
|
|
15542
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
15543
|
+
}
|
|
15544
|
+
});
|
|
15545
|
+
this.app.get("/api/audit-chain", auth2, async (req, res) => {
|
|
13520
15546
|
try {
|
|
13521
15547
|
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
13522
15548
|
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
@@ -13534,13 +15560,13 @@ ${prompt}`;
|
|
|
13534
15560
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13535
15561
|
}
|
|
13536
15562
|
});
|
|
13537
|
-
const prodPath =
|
|
13538
|
-
const devPath =
|
|
13539
|
-
const webDistPath =
|
|
13540
|
-
if (
|
|
15563
|
+
const prodPath = path24.resolve(__dirname$1, "../web/dist");
|
|
15564
|
+
const devPath = path24.resolve(__dirname$1, "../../web/dist");
|
|
15565
|
+
const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
|
|
15566
|
+
if (fs23.existsSync(webDistPath)) {
|
|
13541
15567
|
this.app.use(express.static(webDistPath));
|
|
13542
15568
|
this.app.get("*", (_req, res) => {
|
|
13543
|
-
res.sendFile(
|
|
15569
|
+
res.sendFile(path24.join(webDistPath, "index.html"));
|
|
13544
15570
|
});
|
|
13545
15571
|
} else {
|
|
13546
15572
|
this.app.get("/", (_req, res) => {
|
|
@@ -13613,6 +15639,553 @@ var HooksRunner = class {
|
|
|
13613
15639
|
}
|
|
13614
15640
|
};
|
|
13615
15641
|
|
|
13616
|
-
|
|
15642
|
+
// src/retrieval/chunk.ts
|
|
15643
|
+
var DEFAULTS2 = { targetChars: 2e3, overlapChars: 200, minChars: 80 };
|
|
15644
|
+
function splitLongBlock(block, max) {
|
|
15645
|
+
if (block.length <= max) return [block];
|
|
15646
|
+
const out = [];
|
|
15647
|
+
const sentences = block.match(/[^.!?\n]+[.!?]*\s*/g) ?? [block];
|
|
15648
|
+
let cur = "";
|
|
15649
|
+
for (const s of sentences) {
|
|
15650
|
+
if (s.length > max) {
|
|
15651
|
+
if (cur) {
|
|
15652
|
+
out.push(cur);
|
|
15653
|
+
cur = "";
|
|
15654
|
+
}
|
|
15655
|
+
for (let i = 0; i < s.length; i += max) out.push(s.slice(i, i + max));
|
|
15656
|
+
continue;
|
|
15657
|
+
}
|
|
15658
|
+
if (cur.length + s.length > max) {
|
|
15659
|
+
out.push(cur);
|
|
15660
|
+
cur = s;
|
|
15661
|
+
} else cur += s;
|
|
15662
|
+
}
|
|
15663
|
+
if (cur.trim()) out.push(cur);
|
|
15664
|
+
return out.map((x) => x.trim()).filter(Boolean);
|
|
15665
|
+
}
|
|
15666
|
+
function overlapTail(text, overlap) {
|
|
15667
|
+
if (overlap <= 0 || text.length <= overlap) return text.length <= overlap ? text : "";
|
|
15668
|
+
const tail = text.slice(text.length - overlap);
|
|
15669
|
+
const sp = tail.indexOf(" ");
|
|
15670
|
+
return sp > 0 ? tail.slice(sp + 1) : tail;
|
|
15671
|
+
}
|
|
15672
|
+
function chunkText2(input, options = {}) {
|
|
15673
|
+
const { targetChars, overlapChars, minChars } = { ...DEFAULTS2, ...options };
|
|
15674
|
+
const normalized = input.replace(/\r\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
15675
|
+
if (!normalized) return [];
|
|
15676
|
+
const rawBlocks = normalized.split(/\n\s*\n/).map((b) => b.trim()).filter(Boolean);
|
|
15677
|
+
const blocks = [];
|
|
15678
|
+
for (const b of rawBlocks) blocks.push(...splitLongBlock(b, targetChars));
|
|
15679
|
+
const chunks = [];
|
|
15680
|
+
let cur = "";
|
|
15681
|
+
for (const block of blocks) {
|
|
15682
|
+
const candidate = cur ? `${cur}
|
|
15683
|
+
|
|
15684
|
+
${block}` : block;
|
|
15685
|
+
if (candidate.length > targetChars && cur) {
|
|
15686
|
+
chunks.push(cur);
|
|
15687
|
+
const carry = overlapTail(cur, overlapChars);
|
|
15688
|
+
cur = carry ? `${carry}
|
|
15689
|
+
|
|
15690
|
+
${block}` : block;
|
|
15691
|
+
} else {
|
|
15692
|
+
cur = candidate;
|
|
15693
|
+
}
|
|
15694
|
+
}
|
|
15695
|
+
if (cur.trim()) {
|
|
15696
|
+
if (chunks.length && cur.length < minChars) chunks[chunks.length - 1] += `
|
|
15697
|
+
|
|
15698
|
+
${cur}`;
|
|
15699
|
+
else chunks.push(cur);
|
|
15700
|
+
}
|
|
15701
|
+
return chunks.map((text, ord) => ({ text: text.trim(), ord }));
|
|
15702
|
+
}
|
|
15703
|
+
|
|
15704
|
+
// src/retrieval/plan.ts
|
|
15705
|
+
var CHARS_PER_TOKEN = 4;
|
|
15706
|
+
function cagCharBudget(contextWindowTokens, opts = {}) {
|
|
15707
|
+
const docFraction = opts.docFraction ?? 0.5;
|
|
15708
|
+
const charsPerToken = opts.charsPerToken ?? CHARS_PER_TOKEN;
|
|
15709
|
+
return Math.max(0, Math.floor(contextWindowTokens * docFraction * charsPerToken));
|
|
15710
|
+
}
|
|
15711
|
+
function planRetrieval(ctx) {
|
|
15712
|
+
if (ctx.sourceCount <= 0) return { mode: "none", reason: "no attached knowledge to retrieve" };
|
|
15713
|
+
if (ctx.fastAnswer) return { mode: "cag", reason: "fast answer \u2014 inject attached knowledge in full, no retrieval" };
|
|
15714
|
+
if (ctx.totalChars <= ctx.cagCharBudget) {
|
|
15715
|
+
return { mode: "cag", reason: "attached knowledge fits the context budget \u2014 inject in full" };
|
|
15716
|
+
}
|
|
15717
|
+
return { mode: "rag", reason: "attached knowledge exceeds the budget \u2014 retrieve relevant passages" };
|
|
15718
|
+
}
|
|
15719
|
+
var CLOUD_SESSION_FILE = "cloud-session.json";
|
|
15720
|
+
function globalDir(homeDir = os6.homedir()) {
|
|
15721
|
+
return path24.join(homeDir, GLOBAL_CONFIG_DIR);
|
|
15722
|
+
}
|
|
15723
|
+
function cloudSessionPath(dir = globalDir()) {
|
|
15724
|
+
return path24.join(dir, CLOUD_SESSION_FILE);
|
|
15725
|
+
}
|
|
15726
|
+
function loadCloudSession(dir = globalDir()) {
|
|
15727
|
+
try {
|
|
15728
|
+
const raw = fs23.readFileSync(cloudSessionPath(dir), "utf-8");
|
|
15729
|
+
const parsed = JSON.parse(raw);
|
|
15730
|
+
if (!parsed?.refreshToken || !parsed?.serverUrl) return null;
|
|
15731
|
+
return parsed;
|
|
15732
|
+
} catch {
|
|
15733
|
+
return null;
|
|
15734
|
+
}
|
|
15735
|
+
}
|
|
15736
|
+
function saveCloudSession(session, dir = globalDir()) {
|
|
15737
|
+
fs23.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
15738
|
+
const file = cloudSessionPath(dir);
|
|
15739
|
+
fs23.writeFileSync(file, JSON.stringify(session, null, 2), { mode: 384 });
|
|
15740
|
+
try {
|
|
15741
|
+
fs23.chmodSync(file, 384);
|
|
15742
|
+
} catch {
|
|
15743
|
+
}
|
|
15744
|
+
}
|
|
15745
|
+
function clearCloudSession(dir = globalDir()) {
|
|
15746
|
+
try {
|
|
15747
|
+
fs23.rmSync(cloudSessionPath(dir));
|
|
15748
|
+
} catch {
|
|
15749
|
+
}
|
|
15750
|
+
}
|
|
15751
|
+
|
|
15752
|
+
// src/cloud/client.ts
|
|
15753
|
+
var DEFAULT_CLOUD_URL = "https://app.cascadeai.in";
|
|
15754
|
+
var REFRESH_MARGIN_MS = 6e4;
|
|
15755
|
+
var FileCloudSessionStore = class {
|
|
15756
|
+
constructor(dir) {
|
|
15757
|
+
this.dir = dir;
|
|
15758
|
+
}
|
|
15759
|
+
dir;
|
|
15760
|
+
load() {
|
|
15761
|
+
return loadCloudSession(this.dir);
|
|
15762
|
+
}
|
|
15763
|
+
save(session) {
|
|
15764
|
+
saveCloudSession(session, this.dir);
|
|
15765
|
+
}
|
|
15766
|
+
clear() {
|
|
15767
|
+
clearCloudSession(this.dir);
|
|
15768
|
+
}
|
|
15769
|
+
};
|
|
15770
|
+
var CloudClient = class _CloudClient {
|
|
15771
|
+
constructor(serverUrl, dir, store) {
|
|
15772
|
+
this.serverUrl = serverUrl;
|
|
15773
|
+
this.store = store ?? new FileCloudSessionStore(dir);
|
|
15774
|
+
}
|
|
15775
|
+
serverUrl;
|
|
15776
|
+
store;
|
|
15777
|
+
/** Build a client from a stored session, or null if not signed in. */
|
|
15778
|
+
static fromSession(dir, store) {
|
|
15779
|
+
const src = store ?? new FileCloudSessionStore(dir);
|
|
15780
|
+
const s = src.load();
|
|
15781
|
+
return s ? new _CloudClient(s.serverUrl, dir, src) : null;
|
|
15782
|
+
}
|
|
15783
|
+
url(p) {
|
|
15784
|
+
return `${this.serverUrl.replace(/\/$/, "")}${p}`;
|
|
15785
|
+
}
|
|
15786
|
+
// ── Device login ──────────────────────────────
|
|
15787
|
+
async startDevice() {
|
|
15788
|
+
const res = await fetch(this.url("/api/native/device"), { method: "POST" });
|
|
15789
|
+
if (!res.ok) throw new Error(`Could not start sign-in (${res.status}).`);
|
|
15790
|
+
const b = await res.json();
|
|
15791
|
+
return { deviceCode: b.device_code, userCode: b.user_code, verificationUri: b.verification_uri, expiresIn: b.expires_in, interval: b.interval };
|
|
15792
|
+
}
|
|
15793
|
+
/** Single poll of the device grant. On approval, persists + returns the session. */
|
|
15794
|
+
async pollDevice(deviceCode) {
|
|
15795
|
+
const res = await fetch(this.url("/api/native/device/token"), {
|
|
15796
|
+
method: "POST",
|
|
15797
|
+
headers: { "Content-Type": "application/json" },
|
|
15798
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
15799
|
+
});
|
|
15800
|
+
if (res.status === 428) return "pending";
|
|
15801
|
+
if (res.status === 429) return "slow_down";
|
|
15802
|
+
if (!res.ok) return "expired";
|
|
15803
|
+
const tokens = await res.json();
|
|
15804
|
+
const session = await this.finishLogin(tokens);
|
|
15805
|
+
return { session };
|
|
15806
|
+
}
|
|
15807
|
+
/**
|
|
15808
|
+
* Run the full device flow: start it, hand the code to `onCode`, then poll at
|
|
15809
|
+
* the server's interval until approved or expired.
|
|
15810
|
+
*/
|
|
15811
|
+
async runDeviceLogin(onCode, opts = {}) {
|
|
15812
|
+
const start = await this.startDevice();
|
|
15813
|
+
onCode(start);
|
|
15814
|
+
let interval = start.interval;
|
|
15815
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
15816
|
+
while (Date.now() < deadline) {
|
|
15817
|
+
if (opts.signal?.aborted) throw new Error("Sign-in cancelled.");
|
|
15818
|
+
await sleep(interval * 1e3, opts.signal);
|
|
15819
|
+
const r = await this.pollDevice(start.deviceCode);
|
|
15820
|
+
if (r === "slow_down") {
|
|
15821
|
+
interval += 5;
|
|
15822
|
+
continue;
|
|
15823
|
+
}
|
|
15824
|
+
if (r === "pending") continue;
|
|
15825
|
+
if (r === "expired") throw new Error("The sign-in code expired. Run `cascade login` again.");
|
|
15826
|
+
return r.session;
|
|
15827
|
+
}
|
|
15828
|
+
throw new Error("The sign-in code expired. Run `cascade login` again.");
|
|
15829
|
+
}
|
|
15830
|
+
// ── Loopback login (desktop) — RFC 8252 ───────
|
|
15831
|
+
/**
|
|
15832
|
+
* Run the loopback flow for a rich native client (the desktop). Opens a
|
|
15833
|
+
* one-shot listener on `http://127.0.0.1:<random>`, sends the user to the
|
|
15834
|
+
* provider in the system browser via `openUrl`, catches the one-time code on
|
|
15835
|
+
* the loopback redirect, and exchanges it (+ the PKCE verifier) for tokens.
|
|
15836
|
+
*
|
|
15837
|
+
* No secret is involved: PKCE S256 proves the client that started the flow is
|
|
15838
|
+
* the one redeeming the code. Storage-agnostic — persists through the same
|
|
15839
|
+
* session store as the device flow, so callers get a ready `CloudSession`.
|
|
15840
|
+
*/
|
|
15841
|
+
async runLoopbackLogin(openUrl, opts = {}) {
|
|
15842
|
+
const provider = opts.provider ?? "google";
|
|
15843
|
+
const { verifier, challenge } = makePkce();
|
|
15844
|
+
const appState = base64url(randomBytes(16));
|
|
15845
|
+
const listener = await startLoopbackListener(appState, opts);
|
|
15846
|
+
try {
|
|
15847
|
+
const authorizeUrl = this.url(
|
|
15848
|
+
`/auth/native/${encodeURIComponent(provider)}?redirect_uri=${encodeURIComponent(listener.redirectUri)}&code_challenge=${encodeURIComponent(challenge)}&state=${encodeURIComponent(appState)}`
|
|
15849
|
+
);
|
|
15850
|
+
await openUrl(authorizeUrl);
|
|
15851
|
+
const code = await listener.waitForCode;
|
|
15852
|
+
const res = await fetch(this.url("/api/native/token"), {
|
|
15853
|
+
method: "POST",
|
|
15854
|
+
headers: { "Content-Type": "application/json" },
|
|
15855
|
+
body: JSON.stringify({ code, code_verifier: verifier })
|
|
15856
|
+
});
|
|
15857
|
+
if (!res.ok) throw new Error("Sign-in could not be completed. Please try again.");
|
|
15858
|
+
const tokens = await res.json();
|
|
15859
|
+
return await this.finishLogin(tokens);
|
|
15860
|
+
} finally {
|
|
15861
|
+
listener.close();
|
|
15862
|
+
}
|
|
15863
|
+
}
|
|
15864
|
+
/** Exchange a fresh token response for a stored session (fetches the user). */
|
|
15865
|
+
async finishLogin(tokens) {
|
|
15866
|
+
const partial = {
|
|
15867
|
+
serverUrl: this.serverUrl,
|
|
15868
|
+
accessToken: tokens.access_token,
|
|
15869
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
15870
|
+
refreshToken: tokens.refresh_token,
|
|
15871
|
+
user: { id: "", email: null, name: null }
|
|
15872
|
+
};
|
|
15873
|
+
const user = await this.fetchMe(partial.accessToken);
|
|
15874
|
+
const session = { ...partial, user };
|
|
15875
|
+
this.store.save(session);
|
|
15876
|
+
return session;
|
|
15877
|
+
}
|
|
15878
|
+
// ── Token lifecycle ───────────────────────────
|
|
15879
|
+
session() {
|
|
15880
|
+
const s = this.store.load();
|
|
15881
|
+
if (!s) throw new Error("Not signed in. Run `cascade login`.");
|
|
15882
|
+
return s;
|
|
15883
|
+
}
|
|
15884
|
+
async refresh() {
|
|
15885
|
+
const s = this.session();
|
|
15886
|
+
const res = await fetch(this.url("/api/native/refresh"), {
|
|
15887
|
+
method: "POST",
|
|
15888
|
+
headers: { "Content-Type": "application/json" },
|
|
15889
|
+
body: JSON.stringify({ refresh_token: s.refreshToken })
|
|
15890
|
+
});
|
|
15891
|
+
if (!res.ok) {
|
|
15892
|
+
this.store.clear();
|
|
15893
|
+
throw new Error("Your session expired. Run `cascade login` again.");
|
|
15894
|
+
}
|
|
15895
|
+
const tokens = await res.json();
|
|
15896
|
+
const next = {
|
|
15897
|
+
...s,
|
|
15898
|
+
accessToken: tokens.access_token,
|
|
15899
|
+
accessExpiresAt: Date.now() + tokens.expires_in * 1e3,
|
|
15900
|
+
refreshToken: tokens.refresh_token
|
|
15901
|
+
// rotated
|
|
15902
|
+
};
|
|
15903
|
+
this.store.save(next);
|
|
15904
|
+
return next;
|
|
15905
|
+
}
|
|
15906
|
+
async accessToken() {
|
|
15907
|
+
const s = this.session();
|
|
15908
|
+
if (s.accessExpiresAt - Date.now() > REFRESH_MARGIN_MS) return s.accessToken;
|
|
15909
|
+
return (await this.refresh()).accessToken;
|
|
15910
|
+
}
|
|
15911
|
+
/** Authenticated request that refreshes once on a 401 (e.g. server-side revoke). */
|
|
15912
|
+
async authed(path26, init = {}) {
|
|
15913
|
+
const call = (token) => fetch(this.url(path26), { ...init, headers: { ...init.headers ?? {}, Authorization: `Bearer ${token}` } });
|
|
15914
|
+
let res = await call(await this.accessToken());
|
|
15915
|
+
if (res.status === 401) res = await call((await this.refresh()).accessToken);
|
|
15916
|
+
if (!res.ok) throw new Error(`Request failed (${res.status}).`);
|
|
15917
|
+
const text = await res.text();
|
|
15918
|
+
return text ? JSON.parse(text) : {};
|
|
15919
|
+
}
|
|
15920
|
+
authedGet(path26) {
|
|
15921
|
+
return this.authed(path26);
|
|
15922
|
+
}
|
|
15923
|
+
async fetchMe(token) {
|
|
15924
|
+
const res = await fetch(this.url("/api/me"), { headers: { Authorization: `Bearer ${token}` } });
|
|
15925
|
+
if (!res.ok) throw new Error(`Could not load your account (${res.status}).`);
|
|
15926
|
+
const b = await res.json();
|
|
15927
|
+
return b.user ?? { id: "", email: null, name: null };
|
|
15928
|
+
}
|
|
15929
|
+
// ── Read API ──────────────────────────────────
|
|
15930
|
+
async me() {
|
|
15931
|
+
const b = await this.authedGet("/api/me");
|
|
15932
|
+
return b.user ?? { id: "", email: null, name: null };
|
|
15933
|
+
}
|
|
15934
|
+
async listConversations() {
|
|
15935
|
+
const b = await this.authedGet("/api/conversations");
|
|
15936
|
+
return b.conversations ?? [];
|
|
15937
|
+
}
|
|
15938
|
+
async getMessages(conversationId) {
|
|
15939
|
+
const b = await this.authedGet(`/api/conversations/${encodeURIComponent(conversationId)}/messages`);
|
|
15940
|
+
return b.messages ?? [];
|
|
15941
|
+
}
|
|
15942
|
+
// ── Write API (cloud-backed sessions for desktop + CLI) ───
|
|
15943
|
+
// Native surfaces execute runs LOCALLY (their own keys + shell/file/git tools)
|
|
15944
|
+
// and persist each finished turn into the shared cloud conversation tree, so
|
|
15945
|
+
// web, desktop, and CLI all show the same sessions and branching works across
|
|
15946
|
+
// every surface.
|
|
15947
|
+
/** Start a new cloud conversation and return it. */
|
|
15948
|
+
async createConversation(title) {
|
|
15949
|
+
const b = await this.authed("/api/conversations", {
|
|
15950
|
+
method: "POST",
|
|
15951
|
+
headers: { "Content-Type": "application/json" },
|
|
15952
|
+
body: JSON.stringify({ title: title ?? null })
|
|
15953
|
+
});
|
|
15954
|
+
return b.conversation;
|
|
15955
|
+
}
|
|
15956
|
+
/** Persist a locally-executed turn; returns the new active path (with siblings). */
|
|
15957
|
+
async appendTurn(conversationId, turn) {
|
|
15958
|
+
const b = await this.authed(
|
|
15959
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/turns`,
|
|
15960
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(turn) }
|
|
15961
|
+
);
|
|
15962
|
+
return b.messages ?? [];
|
|
15963
|
+
}
|
|
15964
|
+
/** Branching: switch the active path to a sibling; returns the new path. */
|
|
15965
|
+
async selectBranch(conversationId, messageId) {
|
|
15966
|
+
const b = await this.authed(
|
|
15967
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/select-branch`,
|
|
15968
|
+
{ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messageId }) }
|
|
15969
|
+
);
|
|
15970
|
+
return b.messages ?? [];
|
|
15971
|
+
}
|
|
15972
|
+
/** Branching: delete a message and its subtree; returns the new path. */
|
|
15973
|
+
async deleteMessage(conversationId, messageId) {
|
|
15974
|
+
const b = await this.authed(
|
|
15975
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/messages/${encodeURIComponent(messageId)}`,
|
|
15976
|
+
{ method: "DELETE" }
|
|
15977
|
+
);
|
|
15978
|
+
return b.messages ?? [];
|
|
15979
|
+
}
|
|
15980
|
+
async renameConversation(conversationId, title) {
|
|
15981
|
+
await this.authed(`/api/conversations/${encodeURIComponent(conversationId)}/title`, {
|
|
15982
|
+
method: "PATCH",
|
|
15983
|
+
headers: { "Content-Type": "application/json" },
|
|
15984
|
+
body: JSON.stringify({ title })
|
|
15985
|
+
});
|
|
15986
|
+
}
|
|
15987
|
+
async deleteConversation(conversationId) {
|
|
15988
|
+
await this.authed(`/api/conversations/${encodeURIComponent(conversationId)}`, { method: "DELETE" });
|
|
15989
|
+
}
|
|
15990
|
+
// ── Key sync (E2E-encrypted settings relay) ───
|
|
15991
|
+
/** Fetch the stored encrypted settings envelope (ciphertext only), or null. */
|
|
15992
|
+
async pullSecrets() {
|
|
15993
|
+
return this.authed("/api/keysync");
|
|
15994
|
+
}
|
|
15995
|
+
/** Store/replace the encrypted settings envelope. The server never decrypts it. */
|
|
15996
|
+
async pushSecrets(blob) {
|
|
15997
|
+
return this.authed("/api/keysync", {
|
|
15998
|
+
method: "PUT",
|
|
15999
|
+
headers: { "Content-Type": "application/json" },
|
|
16000
|
+
body: JSON.stringify({ blob })
|
|
16001
|
+
});
|
|
16002
|
+
}
|
|
16003
|
+
/** Forget the stored envelope on the server. */
|
|
16004
|
+
async deleteSecrets() {
|
|
16005
|
+
await this.authed("/api/keysync", { method: "DELETE" });
|
|
16006
|
+
}
|
|
16007
|
+
async logout() {
|
|
16008
|
+
const s = this.store.load();
|
|
16009
|
+
if (s) {
|
|
16010
|
+
try {
|
|
16011
|
+
await fetch(this.url("/api/native/logout"), {
|
|
16012
|
+
method: "POST",
|
|
16013
|
+
headers: { "Content-Type": "application/json" },
|
|
16014
|
+
body: JSON.stringify({ refresh_token: s.refreshToken })
|
|
16015
|
+
});
|
|
16016
|
+
} catch {
|
|
16017
|
+
}
|
|
16018
|
+
}
|
|
16019
|
+
this.store.clear();
|
|
16020
|
+
}
|
|
16021
|
+
};
|
|
16022
|
+
function sleep(ms, signal) {
|
|
16023
|
+
return new Promise((resolve, reject) => {
|
|
16024
|
+
const t = setTimeout(resolve, ms);
|
|
16025
|
+
signal?.addEventListener("abort", () => {
|
|
16026
|
+
clearTimeout(t);
|
|
16027
|
+
reject(new Error("aborted"));
|
|
16028
|
+
}, { once: true });
|
|
16029
|
+
});
|
|
16030
|
+
}
|
|
16031
|
+
function base64url(buf) {
|
|
16032
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
16033
|
+
}
|
|
16034
|
+
function makePkce() {
|
|
16035
|
+
const verifier = base64url(randomBytes(32));
|
|
16036
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
16037
|
+
return { verifier, challenge };
|
|
16038
|
+
}
|
|
16039
|
+
function startLoopbackListener(expectedState, opts) {
|
|
16040
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
16041
|
+
let resolveCode;
|
|
16042
|
+
let rejectCode;
|
|
16043
|
+
const waitForCode = new Promise((res, rej) => {
|
|
16044
|
+
resolveCode = res;
|
|
16045
|
+
rejectCode = rej;
|
|
16046
|
+
});
|
|
16047
|
+
let server;
|
|
16048
|
+
const close = () => {
|
|
16049
|
+
try {
|
|
16050
|
+
server?.close();
|
|
16051
|
+
} catch {
|
|
16052
|
+
}
|
|
16053
|
+
};
|
|
16054
|
+
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
16055
|
+
const timer = setTimeout(() => {
|
|
16056
|
+
rejectCode(new Error("Sign-in timed out. Please try again."));
|
|
16057
|
+
close();
|
|
16058
|
+
}, timeoutMs);
|
|
16059
|
+
timer.unref?.();
|
|
16060
|
+
const settleCode = (code) => {
|
|
16061
|
+
clearTimeout(timer);
|
|
16062
|
+
resolveCode(code);
|
|
16063
|
+
};
|
|
16064
|
+
const failCode = (err) => {
|
|
16065
|
+
clearTimeout(timer);
|
|
16066
|
+
rejectCode(err);
|
|
16067
|
+
};
|
|
16068
|
+
server = http.createServer((req, res) => {
|
|
16069
|
+
const reqUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
16070
|
+
if (reqUrl.pathname !== "/cb") {
|
|
16071
|
+
res.writeHead(404).end();
|
|
16072
|
+
return;
|
|
16073
|
+
}
|
|
16074
|
+
const code = reqUrl.searchParams.get("code");
|
|
16075
|
+
const state = reqUrl.searchParams.get("state");
|
|
16076
|
+
const err = reqUrl.searchParams.get("error");
|
|
16077
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
16078
|
+
if (err || !code || state !== expectedState) {
|
|
16079
|
+
res.end(resultPage("Sign-in failed", "Something went wrong. Return to Cascade and try again."));
|
|
16080
|
+
failCode(new Error(err ? `Sign-in failed: ${err}` : "The sign-in response was invalid."));
|
|
16081
|
+
return;
|
|
16082
|
+
}
|
|
16083
|
+
res.end(resultPage("Signed in to Cascade", "You can close this tab and return to the app."));
|
|
16084
|
+
settleCode(code);
|
|
16085
|
+
});
|
|
16086
|
+
opts.signal?.addEventListener("abort", () => {
|
|
16087
|
+
failCode(new Error("Sign-in cancelled."));
|
|
16088
|
+
close();
|
|
16089
|
+
}, { once: true });
|
|
16090
|
+
server.on("error", (e) => {
|
|
16091
|
+
clearTimeout(timer);
|
|
16092
|
+
rejectListener(e);
|
|
16093
|
+
});
|
|
16094
|
+
server.listen(0, "127.0.0.1", () => {
|
|
16095
|
+
const port = server.address().port;
|
|
16096
|
+
resolveListener({ redirectUri: `http://127.0.0.1:${port}/cb`, waitForCode, close });
|
|
16097
|
+
});
|
|
16098
|
+
});
|
|
16099
|
+
}
|
|
16100
|
+
function resultPage(title, body) {
|
|
16101
|
+
return `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title} \xB7 Cascade</title><body style="margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;background:#0b0d12;color:#e6e8ee"><div style="text-align:center;padding:40px"><div style="font-size:15px;font-weight:600;margin-bottom:8px">${title}</div><div style="font-size:13px;color:#9aa3b2">${body}</div></div></body>`;
|
|
16102
|
+
}
|
|
16103
|
+
var PBKDF2_ITERATIONS2 = 21e4;
|
|
16104
|
+
var SALT_BYTES = 16;
|
|
16105
|
+
var IV_BYTES = 12;
|
|
16106
|
+
function toBase64(buf) {
|
|
16107
|
+
const bytes = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
|
|
16108
|
+
return Buffer.from(bytes).toString("base64");
|
|
16109
|
+
}
|
|
16110
|
+
function fromBase64(b64) {
|
|
16111
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
16112
|
+
}
|
|
16113
|
+
async function deriveKey(passphrase, salt) {
|
|
16114
|
+
const baseKey = await webcrypto.subtle.importKey(
|
|
16115
|
+
"raw",
|
|
16116
|
+
new TextEncoder().encode(passphrase),
|
|
16117
|
+
"PBKDF2",
|
|
16118
|
+
false,
|
|
16119
|
+
["deriveKey"]
|
|
16120
|
+
);
|
|
16121
|
+
return webcrypto.subtle.deriveKey(
|
|
16122
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS2, hash: "SHA-256" },
|
|
16123
|
+
baseKey,
|
|
16124
|
+
{ name: "AES-GCM", length: 256 },
|
|
16125
|
+
false,
|
|
16126
|
+
["encrypt", "decrypt"]
|
|
16127
|
+
);
|
|
16128
|
+
}
|
|
16129
|
+
async function encryptJSON(data, passphrase) {
|
|
16130
|
+
const salt = webcrypto.getRandomValues(new Uint8Array(SALT_BYTES));
|
|
16131
|
+
const iv = webcrypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
16132
|
+
const key = await deriveKey(passphrase, salt);
|
|
16133
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
16134
|
+
const ciphertext = await webcrypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
|
|
16135
|
+
return { ciphertext: toBase64(ciphertext), salt: toBase64(salt), iv: toBase64(iv) };
|
|
16136
|
+
}
|
|
16137
|
+
async function decryptJSON(blob, passphrase) {
|
|
16138
|
+
const salt = fromBase64(blob.salt);
|
|
16139
|
+
const iv = fromBase64(blob.iv);
|
|
16140
|
+
const key = await deriveKey(passphrase, salt);
|
|
16141
|
+
const ciphertext = fromBase64(blob.ciphertext);
|
|
16142
|
+
const plaintext = await webcrypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
|
16143
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
16144
|
+
}
|
|
16145
|
+
|
|
16146
|
+
// src/cloud/keysync.ts
|
|
16147
|
+
function providerSig(p) {
|
|
16148
|
+
return [p.type, p.baseUrl ?? "", p.deploymentName ?? "", p.label ?? ""].join("\0");
|
|
16149
|
+
}
|
|
16150
|
+
function mergeByKey(local, incoming, key) {
|
|
16151
|
+
if (!incoming?.length) return local;
|
|
16152
|
+
const map = /* @__PURE__ */ new Map();
|
|
16153
|
+
for (const l of local ?? []) map.set(key(l), l);
|
|
16154
|
+
for (const i of incoming) map.set(key(i), i);
|
|
16155
|
+
return [...map.values()];
|
|
16156
|
+
}
|
|
16157
|
+
function gatherSyncBundle(config) {
|
|
16158
|
+
const bundle = { v: 1 };
|
|
16159
|
+
if (config.providers?.length) bundle.providers = config.providers;
|
|
16160
|
+
if (config.tools?.webSearch) bundle.webSearch = config.tools.webSearch;
|
|
16161
|
+
if (config.tools?.mcpServers?.length) bundle.mcpServers = config.tools.mcpServers;
|
|
16162
|
+
if (config.models) bundle.models = config.models;
|
|
16163
|
+
if (config.budget) bundle.budget = config.budget;
|
|
16164
|
+
const prefs = {};
|
|
16165
|
+
if (config.autoBias !== void 0) prefs.autoBias = config.autoBias;
|
|
16166
|
+
if (config.cascadeAuto !== void 0) prefs.cascadeAuto = config.cascadeAuto;
|
|
16167
|
+
if (config.extendedContext !== void 0) prefs.extendedContext = config.extendedContext;
|
|
16168
|
+
if (config.autonomy !== void 0) prefs.autonomy = config.autonomy;
|
|
16169
|
+
if (Object.keys(prefs).length) bundle.prefs = prefs;
|
|
16170
|
+
return bundle;
|
|
16171
|
+
}
|
|
16172
|
+
function applySyncBundle(bundle, config) {
|
|
16173
|
+
const next = { ...config, tools: { ...config.tools } };
|
|
16174
|
+
if (bundle.providers) next.providers = mergeByKey(config.providers, bundle.providers, providerSig) ?? [];
|
|
16175
|
+
if (bundle.webSearch) next.tools.webSearch = { ...config.tools?.webSearch, ...bundle.webSearch };
|
|
16176
|
+
if (bundle.mcpServers) next.tools.mcpServers = mergeByKey(config.tools?.mcpServers, bundle.mcpServers, (m) => m.name);
|
|
16177
|
+
if (bundle.models) next.models = { ...config.models, ...bundle.models };
|
|
16178
|
+
if (bundle.budget) next.budget = { ...config.budget, ...bundle.budget };
|
|
16179
|
+
if (bundle.prefs) {
|
|
16180
|
+
const p = bundle.prefs;
|
|
16181
|
+
if (p.autoBias !== void 0) next.autoBias = p.autoBias;
|
|
16182
|
+
if (p.cascadeAuto !== void 0) next.cascadeAuto = p.cascadeAuto;
|
|
16183
|
+
if (p.extendedContext !== void 0) next.extendedContext = p.extendedContext;
|
|
16184
|
+
if (p.autonomy !== void 0) next.autonomy = p.autonomy;
|
|
16185
|
+
}
|
|
16186
|
+
return next;
|
|
16187
|
+
}
|
|
16188
|
+
|
|
16189
|
+
export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, CHARS_PER_TOKEN, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, CloudClient, CodeSearchTool, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CLOUD_URL, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, FileMcpOAuthStore, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, GraphRetriever, GraphSearchTool, HooksRunner, Keystore, LLMReranker, LM_STUDIO_BASE_URL, MODELS, McpClient, McpOAuthProvider, MemoryStore, OLLAMA_BASE_URL, OpenAICompatibleEmbedder, PROVIDER_DISPLAY_NAMES, Retriever, SqliteVectorStore, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, WorkspaceIndex, applySyncBundle, azureModelForDeployment, beginMcpOAuth, buildManifest, buildSessionTranscript, cagCharBudget, chatCompleterFromProviders, chunkCode, chunkText2 as chunkText, completeMcpOAuth, connectMcpWithLoopbackOAuth, createCascade, decryptJSON as decryptSyncBlob, diffManifest, discoverMcpAuthServer, distillSessionFacts, embedderFromProviders, encryptJSON as encryptSyncBlob, fileOAuthProvider, gatherSyncBundle, hashContent, heuristicCodeChunker, inferAzureBaseModel, nodeHttpFetch, parseRankOrder, planRetrieval, preferIpv4Host, reciprocalRankFusion, refreshMcpToken, runCascade, sessionWorthRemembering, streamCascade };
|
|
13617
16190
|
//# sourceMappingURL=index.js.map
|
|
13618
16191
|
//# sourceMappingURL=index.js.map
|