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/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import Database from 'better-sqlite3';
2
- import path21 from 'path';
3
- import fs20 from 'fs';
2
+ import path24 from 'path';
3
+ import fs23 from 'fs';
4
4
  import os6 from 'os';
5
- import crypto3, { randomUUID, timingSafeEqual } from 'crypto';
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 = path21.join(workspacePath, ".cascade");
62
- if (!fs20.existsSync(cascadeDir)) {
63
- fs20.mkdirSync(cascadeDir, { recursive: true });
64
+ const cascadeDir = path24.join(workspacePath, ".cascade");
65
+ if (!fs23.existsSync(cascadeDir)) {
66
+ fs23.mkdirSync(cascadeDir, { recursive: true });
64
67
  }
65
- this.keyPath = path21.join(cascadeDir, "audit_log.key");
66
- this.dbPath = path21.join(cascadeDir, "audit_log.db");
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 crypto3.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
96
+ return crypto4.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
94
97
  }
95
98
  initEncryptionKey() {
96
- if (fs20.existsSync(this.keyPath)) {
97
- this.encryptionKey = fs20.readFileSync(this.keyPath);
99
+ if (fs23.existsSync(this.keyPath)) {
100
+ this.encryptionKey = fs23.readFileSync(this.keyPath);
98
101
  } else {
99
- this.encryptionKey = crypto3.randomBytes(32);
100
- fs20.writeFileSync(this.keyPath, this.encryptionKey);
102
+ this.encryptionKey = crypto4.randomBytes(32);
103
+ fs23.writeFileSync(this.keyPath, this.encryptionKey);
101
104
  }
102
105
  }
103
106
  encrypt(text) {
104
- const iv = crypto3.randomBytes(16);
105
- const cipher = crypto3.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
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 = crypto3.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
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 = crypto3.randomUUID();
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 = path21.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
181
+ const dumpPath = path24.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
179
182
  const entries = this.getAllLogs();
180
- fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
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.0";
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 params = {
916
+ const budget = options.maxTokens ?? this.model.maxOutputTokens;
917
+ const buildParams = (useCompletionTokens) => ({
819
918
  model: this.model.id,
820
919
  messages,
821
- max_tokens: options.maxTokens ?? this.model.maxOutputTokens,
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(params, { signal: options.signal });
928
+ stream = await this.client.chat.completions.create(buildParams(this.useMaxCompletionTokens), { signal: options.signal });
830
929
  } catch (err) {
831
- if (err.message && err.message.includes("max_completion_tokens")) {
832
- const fallbackParams = { ...params };
833
- delete fallbackParams.max_tokens;
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: cfg.label?.trim() || id,
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 ?? "2024-08-01-preview"
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 params = {
1166
+ const ping = (extra) => this.client.chat.completions.create({
1038
1167
  model: this.model.id,
1039
1168
  messages: [{ role: "user", content: "ping" }],
1040
- max_tokens: 1
1041
- };
1169
+ ...extra
1170
+ });
1171
+ const reasoning = isReasoningModel(this.model.id);
1042
1172
  try {
1043
- await this.client.chat.completions.create(params);
1173
+ await ping(reasoning ? { max_completion_tokens: 16 } : { max_tokens: 1 });
1044
1174
  return true;
1045
1175
  } catch (err) {
1046
- if (err?.message && String(err.message).includes("max_completion_tokens")) {
1176
+ if (isParamShapeError(err)) {
1047
1177
  try {
1048
- await this.client.chat.completions.create({
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 false;
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.availableProviders.has(model.provider)) return model;
1858
+ if (model && this.isUsable(model)) return model;
1708
1859
  }
1709
1860
  for (const [, model] of this.availableModels) {
1710
- if (this.availableProviders.has(model.provider)) return model;
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.availableProviders.has(model.provider)) return model;
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.availableProviders.has(model.provider)) {
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 = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2204
- function normalizeModelId(id) {
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 = normalizeModelId(m.id);
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(path21.dirname(this.opts.cacheFile), { recursive: true });
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(normalizeModelId(modelId)) ?? null;
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(normalizeModelId(modelId)) ?? null;
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
- if (availableProviders.has("azure")) {
2561
- for (const cfg of config.providers) {
2562
- const model = azureModelForDeployment(cfg);
2563
- if (model) this.selector.addDynamicModel(model);
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
- const limits = this.config?.tierLimits;
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 next = this.selector.selectForTier(tier);
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 retryOpts = options.model && options.model.id === model.id ? { ...options, model: void 0 } : options;
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
- } catch {
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 T3_SYSTEM_PROMPT = `You are a T3 Worker agent in the Cascade AI system. Your job is to execute a specific subtask completely and accurately.
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
- - Execute the subtask completely \u2014 do not stop partway through.
3797
- - Use tools when needed. Ask for approval only when the tool registry requires it.
3798
- - 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.
3799
- - Use the "web_search" tool to find current information, documentation, news, or general web data.
3800
- - Use the "pdf_create" tool for PDF requests.
3801
- - 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.
3802
- - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
3803
- - 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.
3804
- - Return structured output that directly addresses the expected output specification.`;
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 (path22, content) => {
4286
- this.store?.addFileSnapshot(this.taskId, path22, content);
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
- if (this.assignment?.files?.length) return true;
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 = path21.resolve(process.cwd(), artifactPath);
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 = path21.extname(absolutePath).toLowerCase();
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
- return `${T3_SYSTEM_PROMPT}
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
- var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
5056
- Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
5057
- If subtasks have dependencies, you can specify "executionMode": "sequential" for the section.
5058
- Provide "peerT3Ids" to subtasks so they can coordinate using the peer_message tool.
5059
- Return ONLY valid JSON matching the T3 subtask array schema \u2014 no other text.`;
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 + T2_SYSTEM_PROMPT + (this.hierarchyContext ? `
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 chunkText = "";
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 (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
5935
+ if (chunkText3.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
5640
5936
  break;
5641
5937
  }
5642
- chunkText += nextOutput;
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"}${chunkText}${peerContext}`;
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" + chunkText;
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
- var T1_SYSTEM_PROMPT = `You are T1, the Administrator in the Cascade AI orchestration system.
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
- - Simple \u2192 1 T3, Moderate \u2192 2-3 T2s, Complex \u2192 3-5 T2s, Highly Complex \u2192 5+ T2s
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 + T1_SYSTEM_PROMPT,
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 = path21.resolve(workspaceRoot);
6590
- const abs = path21.isAbsolute(input) ? path21.resolve(input) : path21.resolve(root, input);
6591
- const rel = path21.relative(root, abs);
6592
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21.isAbsolute(rel)) {
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 = fs20.realpathSync(abs);
6597
- const realRel = path21.relative(root, real);
6598
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21.isAbsolute(realRel))) {
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(path21.dirname(absPath), { recursive: true });
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 page = this.page;
7374
+ const page2 = this.page;
7072
7375
  try {
7073
7376
  switch (action) {
7074
7377
  case "navigate": {
7075
- await page.goto(input["url"], { timeout });
7076
- const title = await page.title();
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 page.click(input["selector"], { timeout });
7383
+ await page2.click(input["selector"], { timeout });
7081
7384
  return `Clicked ${input["selector"]}`;
7082
7385
  }
7083
7386
  case "fill": {
7084
- await page.fill(input["selector"], input["value"]);
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 page.screenshot({ type: "png" });
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 page.evaluate(input["script"]);
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 page.locator("body").innerText();
7399
+ const text = await page2.locator("body").innerText();
7097
7400
  return text.slice(0, 1e4);
7098
7401
  }
7099
7402
  case "wait": {
7100
- await page.waitForTimeout(timeout);
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 = path21.extname(filePath).toLowerCase();
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 = path21.dirname(filePath);
7192
- if (!fs20.existsSync(dir)) {
7193
- fs20.mkdirSync(dir, { recursive: true });
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 = fs20.createWriteStream(filePath);
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 = path21.join(this.workspaceRoot, ".cascade", "tmp");
7277
- if (!fs20.existsSync(tmpDir)) {
7278
- fs20.mkdirSync(tmpDir, { recursive: true });
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 = path21.join(tmpDir, fileName);
7283
- fs20.writeFileSync(filePath, code, "utf-8");
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 (fs20.existsSync(filePath)) {
7291
- fs20.unlinkSync(filePath);
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"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
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(path21.join(searchPath, rel));
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"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
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 = [path21.relative(searchPath, searchPath) || "."];
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 = path21.join(searchPath, rel);
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 = path21.resolve(this.workspaceRoot, filePath);
8085
- const rel = path21.relative(this.workspaceRoot, abs);
8086
- if (!rel || rel.startsWith("..") || path21.isAbsolute(rel)) return true;
8087
- const posixRel = rel.split(path21.sep).join("/");
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
- await client.connect(transport);
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.pid) _McpClient.activeProcessPids.add(transport.pid);
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?.pid) _McpClient.activeProcessPids.delete(transport.pid);
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
- command: z.string(),
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({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
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 = path21.join(os6.homedir(), ".cascade", "model-perf.json");
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
- constructor(statsFile = DEFAULT_STATS_FILE) {
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(path21.dirname(this.statsFile), { recursive: true });
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
- await fs4.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
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 = path21.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_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 = path21.join(this.workspacePath, ".cascade");
9526
- const file = path21.join(dir, DYNAMIC_TOOLS_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 = path21.join(workspacePath, ".cascade");
9547
- if (!fs20.existsSync(cascadeDir)) {
9548
- fs20.mkdirSync(cascadeDir, { recursive: true });
10232
+ const cascadeDir = path24.join(workspacePath, ".cascade");
10233
+ if (!fs23.existsSync(cascadeDir)) {
10234
+ fs23.mkdirSync(cascadeDir, { recursive: true });
9549
10235
  }
9550
- this.keyPath = path21.join(cascadeDir, "world_state.key");
9551
- this.dbPath = path21.join(cascadeDir, "world_state.db");
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 (fs20.existsSync(this.keyPath)) {
9582
- this.encryptionKey = fs20.readFileSync(this.keyPath);
10294
+ if (fs23.existsSync(this.keyPath)) {
10295
+ this.encryptionKey = fs23.readFileSync(this.keyPath);
9583
10296
  } else {
9584
- this.encryptionKey = crypto3.randomBytes(32);
9585
- fs20.writeFileSync(this.keyPath, this.encryptionKey);
10297
+ this.encryptionKey = crypto4.randomBytes(32);
10298
+ fs23.writeFileSync(this.keyPath, this.encryptionKey);
9586
10299
  }
9587
10300
  }
9588
10301
  encrypt(text) {
9589
- const iv = crypto3.randomBytes(16);
9590
- const cipher = crypto3.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
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 = crypto3.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
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 = crypto3.randomUUID();
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 stmt = this.db.prepare(`
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
- stmt.run(e, r, encrypted, sourceWorker, timestamp ?? (/* @__PURE__ */ new Date()).toISOString());
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 result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
9736
- return result.changes > 0;
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
- return this.db.prepare("DELETE FROM facts").run().changes;
9741
- }
9742
- // ── Export / Import ──────────────────────────
9743
- //
9744
- // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
9745
- // JSON file — the encryption key never leaves this machine) and re-encrypts
9746
- // with the LOCAL key on import.
9747
- exportKnowledge() {
9748
- return { facts: this.getAllFacts(), worldLog: this.getAllEntries() };
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
- * Merge imported knowledge. Facts upsert on (entity, relation) with
9752
- * newer-timestamp-wins (a local fact newer than the imported one is kept);
9753
- * log entries append with fresh ids, skipping exact duplicates
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
- importKnowledge(data) {
9757
- let facts = 0;
9758
- let logEntries = 0;
9759
- if (Array.isArray(data.facts)) {
9760
- const local = new Map(this.getAllFacts().map((f) => [`${f.entity}\0${f.relation}`, f.timestamp]));
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(crypto3.randomUUID(), timestamp, workerId, this.encrypt(JSON.stringify({ summary: e.summary })));
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 = path21.join(os6.tmpdir(), "cascade_world_state_debug.json");
10584
+ const dumpPath = path24.join(os6.tmpdir(), "cascade_world_state_debug.json");
9791
10585
  const entries = this.getAllEntries();
9792
- fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
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
- await this.mcpClient.connect(server);
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
- async determineComplexity(prompt, workspacePath, conversationHistory = []) {
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
- verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
10335
- if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
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", "Moderate \u2014 classifier unavailable; defaulting to the mid-cost route");
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(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
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(options.prompt, tier, this.router.getSelector());
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: options.prompt,
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: options.prompt,
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
- finalOutput = "Task failed to complete successfully.";
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(options.prompt, options.images, void 0, options.signal);
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 && fs20.existsSync(this.storePath)) {
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 (!fs20.existsSync(this.storePath)) {
10742
- const salt = crypto3.randomBytes(SALT_LEN);
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 (!fs20.existsSync(this.storePath)) {
10756
- const salt = crypto3.randomBytes(SALT_LEN);
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 (!fs20.existsSync(this.storePath)) return {};
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 = crypto3.createDecipheriv(ALGORITHM, key, iv);
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 = crypto3.randomBytes(IV_LEN);
10830
- const cipher = crypto3.createCipheriv(ALGORITHM, this.masterKey, iv);
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
- fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10836
- fs20.writeFileSync(this.storePath, out, { mode: 384 });
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 = crypto3.randomBytes(IV_LEN);
10841
- const cipher = crypto3.createCipheriv(ALGORITHM, this.masterKey, iv);
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
- fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10847
- fs20.writeFileSync(this.storePath, out, { mode: 384 });
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 = fs20.readFileSync(this.storePath);
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 crypto3.pbkdf2Sync(password, salt, PBKDF2_ITERATIONS, KEY_LEN, "sha256");
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 = path21.join(workspacePath, "CASCADE.md");
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
- fs20.mkdirSync(path21.dirname(dbPath), { recursive: true });
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(globalDir) {
11772
- return path21.join(globalDir, GLOBAL_CREDENTIALS_FILE);
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(globalDir) {
13774
+ function loadGlobalCredentials(globalDir2) {
11784
13775
  try {
11785
- const raw = fs20.readFileSync(credentialsPath(globalDir), "utf-8");
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(globalDir, providers) {
11796
- const filePath = credentialsPath(globalDir);
11797
- fs20.mkdirSync(globalDir, { recursive: true, mode: 448 });
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
- fs20.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
11801
- fs20.renameSync(tmp, filePath);
13791
+ fs23.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
13792
+ fs23.renameSync(tmp, filePath);
11802
13793
  try {
11803
- fs20.chmodSync(filePath, 384);
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 ?? path21.join(os6.homedir(), GLOBAL_CONFIG_DIR);
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(path21.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11846
- this.store = new MemoryStore(path21.join(this.workspacePath, CASCADE_DB_FILE));
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 = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
11871
- await fs4.mkdir(path21.dirname(configPath), { recursive: true });
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 = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
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 = path21.dirname(fileURLToPath(import.meta.url));
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 = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
12555
- fs20.mkdirSync(path21.dirname(configPath), { recursive: true });
12556
- fs20.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
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(path21.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
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 = path21.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
14567
+ const secretPath = path24.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12577
14568
  try {
12578
- if (fs20.existsSync(secretPath)) {
12579
- const existing = fs20.readFileSync(secretPath, "utf-8").trim();
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
- fs20.mkdirSync(path21.dirname(secretPath), { recursive: true });
12584
- fs20.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
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 = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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 = path21.join(this.workspacePath, CASCADE_DB_FILE);
12881
- const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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 (!fs20.existsSync(watchPath)) continue;
12885
- fs20.watchFile(watchPath, { interval: 3e3 }, () => {
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 auth = authMiddleware(this.dashboardSecret, authRequired);
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", auth, mutationLimiter, (req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, (_req, res) => {
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", auth, (req, res) => {
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", auth, (req, res) => {
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 = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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", auth, (req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, mutationLimiter, async (req, res) => {
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", auth, (req, res) => {
15089
+ this.app.delete("/api/sessions", auth2, (req, res) => {
13099
15090
  const body = req.body;
13100
- const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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", auth, (_req, res) => {
15112
+ this.app.delete("/api/runtime", auth2, (_req, res) => {
13122
15113
  this.store.deleteAllRuntimeNodes();
13123
- const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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", auth, (_req, res) => {
15125
+ this.app.get("/api/identities", auth2, (_req, res) => {
13135
15126
  res.json(this.store.listIdentities());
13136
15127
  });
13137
- this.app.post("/api/identities", auth, (req, res) => {
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", auth, (req, res) => {
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", auth, (req, res) => {
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", auth, async (_req, res) => {
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", auth, (req, res) => {
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", auth, (_req, res) => {
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", auth, async (req, res) => {
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 = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
13210
- const existing = fs20.existsSync(configPath) ? JSON.parse(fs20.readFileSync(configPath, "utf-8")) : {};
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
- fs20.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13214
- fs20.renameSync(tmp, configPath);
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", auth, (req, res) => {
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", auth, (_req, res) => {
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", auth, (req, res) => {
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 = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
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", auth, mutationLimiter, (req, res) => {
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", auth, (_req, res) => {
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", auth, (req, res) => {
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", auth, async (req, res) => {
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", auth, mutationLimiter, async (req, res) => {
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", auth, (_req, res) => {
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", auth, (_req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, (_req, res) => {
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", auth, mutationLimiter, (req, res) => {
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", auth, mutationLimiter, (_req, res) => {
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/audit-chain", auth, async (req, res) => {
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 = path21.resolve(__dirname$1, "../web/dist");
13538
- const devPath = path21.resolve(__dirname$1, "../../web/dist");
13539
- const webDistPath = fs20.existsSync(prodPath) ? prodPath : devPath;
13540
- if (fs20.existsSync(webDistPath)) {
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(path21.join(webDistPath, "index.html"));
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
- 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, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
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