cascade-ai 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
- import path20 from 'path';
3
- import fs19 from 'fs';
2
+ import path21 from 'path';
3
+ import fs20 from 'fs';
4
4
  import os6 from 'os';
5
5
  import crypto3, { randomUUID, timingSafeEqual } from 'crypto';
6
6
  import EventEmitter from 'events';
@@ -58,12 +58,12 @@ var init_audit_logger = __esm({
58
58
  constructor(workspacePath, debugMode = false) {
59
59
  this.workspacePath = workspacePath;
60
60
  this.debugMode = debugMode;
61
- const cascadeDir = path20.join(workspacePath, ".cascade");
62
- if (!fs19.existsSync(cascadeDir)) {
63
- fs19.mkdirSync(cascadeDir, { recursive: true });
61
+ const cascadeDir = path21.join(workspacePath, ".cascade");
62
+ if (!fs20.existsSync(cascadeDir)) {
63
+ fs20.mkdirSync(cascadeDir, { recursive: true });
64
64
  }
65
- this.keyPath = path20.join(cascadeDir, "audit_log.key");
66
- this.dbPath = path20.join(cascadeDir, "audit_log.db");
65
+ this.keyPath = path21.join(cascadeDir, "audit_log.key");
66
+ this.dbPath = path21.join(cascadeDir, "audit_log.db");
67
67
  this.initEncryptionKey();
68
68
  this.db = new Database(this.dbPath);
69
69
  this.db.pragma("journal_mode = WAL");
@@ -93,11 +93,11 @@ var init_audit_logger = __esm({
93
93
  return crypto3.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
94
94
  }
95
95
  initEncryptionKey() {
96
- if (fs19.existsSync(this.keyPath)) {
97
- this.encryptionKey = fs19.readFileSync(this.keyPath);
96
+ if (fs20.existsSync(this.keyPath)) {
97
+ this.encryptionKey = fs20.readFileSync(this.keyPath);
98
98
  } else {
99
99
  this.encryptionKey = crypto3.randomBytes(32);
100
- fs19.writeFileSync(this.keyPath, this.encryptionKey);
100
+ fs20.writeFileSync(this.keyPath, this.encryptionKey);
101
101
  }
102
102
  }
103
103
  encrypt(text) {
@@ -175,9 +175,9 @@ var init_audit_logger = __esm({
175
175
  dumpDebugIfNeeded() {
176
176
  if (!this.debugMode) return;
177
177
  try {
178
- const dumpPath = path20.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
178
+ const dumpPath = path21.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
179
179
  const entries = this.getAllLogs();
180
- fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
180
+ fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
181
181
  } catch (err) {
182
182
  console.error("Failed to dump debug audit logs", err);
183
183
  }
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
190
190
  });
191
191
 
192
192
  // src/constants.ts
193
- var CASCADE_VERSION = "0.17.0";
193
+ var CASCADE_VERSION = "0.19.0";
194
194
  var CASCADE_CONFIG_DIR = ".cascade";
195
195
  var CASCADE_MD_FILE = "CASCADE.md";
196
196
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -202,6 +202,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
202
202
  var GLOBAL_CONFIG_DIR = ".cascade-ai";
203
203
  var GLOBAL_DB_FILE = "memory.db";
204
204
  var GLOBAL_KEYSTORE_FILE = "keystore.enc";
205
+ var GLOBAL_CREDENTIALS_FILE = "credentials.json";
205
206
  var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
206
207
  var DEFAULT_DASHBOARD_PORT = 4891;
207
208
  var DEFAULT_API_PORT = 4892;
@@ -992,6 +993,23 @@ var OpenAIProvider = class extends BaseProvider {
992
993
  };
993
994
 
994
995
  // src/providers/azure.ts
996
+ function azureModelForDeployment(cfg) {
997
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
998
+ const id = cfg.deploymentName.trim();
999
+ return {
1000
+ id,
1001
+ name: cfg.label?.trim() || id,
1002
+ provider: "azure",
1003
+ contextWindow: 128e3,
1004
+ isVisionCapable: false,
1005
+ inputCostPer1kTokens: 25e-4,
1006
+ outputCostPer1kTokens: 0.01,
1007
+ maxOutputTokens: 16e3,
1008
+ supportsStreaming: true,
1009
+ isLocal: false,
1010
+ supportsToolUse: true
1011
+ };
1012
+ }
995
1013
  var AzureOpenAIProvider = class extends OpenAIProvider {
996
1014
  constructor(config, model) {
997
1015
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -1012,7 +1030,8 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
1012
1030
  });
1013
1031
  }
1014
1032
  async listModels() {
1015
- return [this.model];
1033
+ const fromDeployment = azureModelForDeployment(this.config);
1034
+ return [fromDeployment ?? this.model];
1016
1035
  }
1017
1036
  async isAvailable() {
1018
1037
  try {
@@ -2164,7 +2183,7 @@ function computeDelegationSavings(stats, t1Model) {
2164
2183
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
2165
2184
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2166
2185
  var FETCH_TIMEOUT_MS = 8e3;
2167
- var DEFAULT_CACHE_FILE = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2186
+ var DEFAULT_CACHE_FILE = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2168
2187
  function normalizeModelId(id) {
2169
2188
  let s = id.toLowerCase();
2170
2189
  const slash = s.lastIndexOf("/");
@@ -2309,7 +2328,7 @@ var LiveDataProvider = class {
2309
2328
  }
2310
2329
  async saveCache() {
2311
2330
  try {
2312
- await fs4.mkdir(path20.dirname(this.opts.cacheFile), { recursive: true });
2331
+ await fs4.mkdir(path21.dirname(this.opts.cacheFile), { recursive: true });
2313
2332
  const cache = {
2314
2333
  fetchedAt: this.fetchedAt,
2315
2334
  snapshot: this.snapshot ?? void 0,
@@ -2521,6 +2540,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
2521
2540
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
2522
2541
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
2523
2542
  }
2543
+ if (availableProviders.has("azure")) {
2544
+ for (const cfg of config.providers) {
2545
+ const model = azureModelForDeployment(cfg);
2546
+ if (model) this.selector.addDynamicModel(model);
2547
+ }
2548
+ }
2524
2549
  for (const tier of ["T1", "T2", "T3"]) {
2525
2550
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
2526
2551
  if (!override || override === "auto") continue;
@@ -3127,7 +3152,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
3127
3152
  ensureProvider(model, configs) {
3128
3153
  const key = `${model.provider}:${model.id}`;
3129
3154
  if (this.providers.has(key)) return;
3130
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3155
+ const cfg = (model.provider === "azure" ? configs.find((c) => c.type === "azure" && c.deploymentName === model.id) : void 0) ?? configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3131
3156
  const provider = this.createProvider(cfg, model);
3132
3157
  this.providers.set(key, provider);
3133
3158
  }
@@ -3287,6 +3312,12 @@ var BaseTier = class extends EventEmitter {
3287
3312
  * which would interleave, are not tagged.
3288
3313
  */
3289
3314
  isPresenter = false;
3315
+ /**
3316
+ * The model actually serving this tier (`provider:id`), once resolved —
3317
+ * rides on every tier:status event so the desktop can show which model ran
3318
+ * which node (Cockpit node panel / Why panel).
3319
+ */
3320
+ servingModel;
3290
3321
  constructor(role, id, parentId) {
3291
3322
  super();
3292
3323
  this.role = role;
@@ -3311,11 +3342,16 @@ var BaseTier = class extends EventEmitter {
3311
3342
  label: this.label,
3312
3343
  status,
3313
3344
  timestamp,
3314
- output
3345
+ output,
3346
+ model: this.servingModel
3315
3347
  };
3316
3348
  this.emit("status", event);
3317
3349
  this.emit("tier:status", event);
3318
3350
  }
3351
+ /** Record the model serving this tier; future status events carry it. */
3352
+ setServingModel(model) {
3353
+ this.servingModel = model || void 0;
3354
+ }
3319
3355
  setLabel(label) {
3320
3356
  this.label = label;
3321
3357
  }
@@ -3338,7 +3374,8 @@ var BaseTier = class extends EventEmitter {
3338
3374
  currentAction: update.currentAction,
3339
3375
  progressPct: update.progressPct,
3340
3376
  timestamp,
3341
- output: update.output
3377
+ output: update.output,
3378
+ model: this.servingModel
3342
3379
  });
3343
3380
  }
3344
3381
  buildMessage(type, to, payload) {
@@ -3704,6 +3741,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
3704
3741
  When you have enough information, stop calling tools and write your final answer.`;
3705
3742
  }
3706
3743
 
3744
+ // src/utils/truncate.ts
3745
+ function truncateForContext(text, maxChars = 12e3) {
3746
+ if (text.length <= maxChars) return text;
3747
+ const headLen = Math.floor(maxChars * 0.75);
3748
+ const tailLen = maxChars - headLen;
3749
+ const head = text.slice(0, headLen);
3750
+ const tail = text.slice(-tailLen);
3751
+ const elided = text.length - headLen - tailLen;
3752
+ return `${head}
3753
+
3754
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
3755
+
3756
+ ${tail}`;
3757
+ }
3758
+
3707
3759
  // src/core/tiers/t3-worker.ts
3708
3760
  var CriticalToolError = class extends Error {
3709
3761
  constructor(message, toolName) {
@@ -4011,6 +4063,7 @@ Now execute your subtask using this context where relevant.`
4011
4063
  } catch {
4012
4064
  }
4013
4065
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
4066
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
4014
4067
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
4015
4068
  let sentFullTextContract = false;
4016
4069
  let textContractSignature = "";
@@ -4108,7 +4161,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4108
4161
  const toolResult = await this.executeTool(tc);
4109
4162
  await this.context.addMessage({
4110
4163
  role: "tool",
4111
- content: toolResult,
4164
+ content: truncateForContext(toolResult),
4112
4165
  toolCallId: tc.id
4113
4166
  });
4114
4167
  }
@@ -4212,8 +4265,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4212
4265
  tierId: this.id,
4213
4266
  sessionId: this.taskId,
4214
4267
  requireApproval: false,
4215
- saveSnapshot: async (path21, content) => {
4216
- this.store?.addFileSnapshot(this.taskId, path21, content);
4268
+ saveSnapshot: async (path22, content) => {
4269
+ this.store?.addFileSnapshot(this.taskId, path22, content);
4217
4270
  },
4218
4271
  sendPeerSync: (to, syncType, content) => {
4219
4272
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -4365,15 +4418,17 @@ ${assignment.expectedOutput}`;
4365
4418
  };
4366
4419
  }
4367
4420
  requiresArtifact() {
4421
+ if (this.assignment?.files?.length) return true;
4368
4422
  const haystack = `${this.assignment?.description ?? ""}
4369
4423
  ${this.assignment?.expectedOutput ?? ""}`;
4370
4424
  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);
4371
4425
  }
4372
4426
  extractArtifactPaths(assignment) {
4427
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
4373
4428
  const haystack = `${assignment.description}
4374
4429
  ${assignment.expectedOutput}`;
4375
4430
  const matches = haystack.match(/\b[\w./-]+\.(pdf|md|html|txt|json|csv|py|js|ts|tsx|jsx|docx?|png|jpg|jpeg|svg|gif)\b/gi) ?? [];
4376
- return [...new Set(matches.map((m) => m.trim()))];
4431
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
4377
4432
  }
4378
4433
  async verifyArtifacts(assignment) {
4379
4434
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -4383,7 +4438,7 @@ ${assignment.expectedOutput}`;
4383
4438
  const { promisify: promisify4 } = await import('util');
4384
4439
  const execAsync2 = promisify4(exec2);
4385
4440
  for (const artifactPath of artifactPaths) {
4386
- const absolutePath = path20.resolve(process.cwd(), artifactPath);
4441
+ const absolutePath = path21.resolve(process.cwd(), artifactPath);
4387
4442
  try {
4388
4443
  const stat = await fs4.stat(absolutePath);
4389
4444
  if (!stat.isFile()) {
@@ -4404,7 +4459,7 @@ ${assignment.expectedOutput}`;
4404
4459
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4405
4460
  continue;
4406
4461
  }
4407
- const ext = path20.extname(absolutePath).toLowerCase();
4462
+ const ext = path21.extname(absolutePath).toLowerCase();
4408
4463
  try {
4409
4464
  if (ext === ".ts" || ext === ".tsx") {
4410
4465
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -4496,7 +4551,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4496
4551
  Assignment: ${assignment.description}
4497
4552
  Expected output: ${assignment.expectedOutput}
4498
4553
  Constraints: ${assignment.constraints.join("; ")}
4499
-
4554
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
4555
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4556
+ ` : ""}
4500
4557
  Output to test:
4501
4558
  ${output}
4502
4559
 
@@ -4585,17 +4642,27 @@ Your subtask:
4585
4642
  - Title: ${assignment.subtaskTitle}
4586
4643
  - Description: ${assignment.description}
4587
4644
  - Expected output: ${assignment.expectedOutput}
4588
- - Constraints: ${assignment.constraints.join("; ")}`;
4645
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
4646
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
4647
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
4589
4648
  }
4590
4649
  buildInitialPrompt(assignment) {
4591
4650
  return `Execute the following subtask completely:
4592
4651
 
4593
4652
  **${assignment.subtaskTitle}**
4594
-
4653
+ ${assignment.contextBrief ? `
4654
+ Context: ${assignment.contextBrief}
4655
+ ` : ""}
4595
4656
  ${assignment.description}
4596
4657
 
4597
4658
  Expected output: ${assignment.expectedOutput}
4598
-
4659
+ ${assignment.files?.length ? `
4660
+ Files you own (create or edit exactly these paths):
4661
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
4662
+ ` : ""}${assignment.acceptance?.length ? `
4663
+ Definition of done (your output must satisfy ALL of these):
4664
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4665
+ ` : ""}
4599
4666
  Constraints:
4600
4667
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
4601
4668
 
@@ -5076,6 +5143,8 @@ var T2Manager = class extends BaseTier {
5076
5143
  this.assignment = assignment;
5077
5144
  this.taskId = taskId;
5078
5145
  this.setLabel(assignment.sectionTitle);
5146
+ const m = this.router.getModelForTier("T2");
5147
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5079
5148
  this.setStatus("ACTIVE");
5080
5149
  this.sendStatusUpdate({
5081
5150
  progressPct: 0,
@@ -5162,7 +5231,7 @@ Guidance (must be followed): ${decision.note}`
5162
5231
  // ── Private ──────────────────────────────────
5163
5232
  async decomposeSection(assignment) {
5164
5233
  const peerPlans = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_PLAN_ANNOUNCEMENT").map((p) => `[Peer ${p.fromId} Plan]: ${p.content.sectionTitle} - ${p.content.subtaskTitles?.join(", ")}`).join("\n");
5165
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
5234
+ const prompt = `Decompose this section into 1-4 concrete subtasks for T3 workers \u2014 the FEWEST that fully cover it (one subtask is the correct answer for a small section).
5166
5235
 
5167
5236
  Section: ${assignment.sectionTitle}
5168
5237
  Description: ${assignment.description}
@@ -5181,6 +5250,9 @@ Return a JSON array of subtask objects, each with:
5181
5250
  - peerT3Ids: string[] (empty for now)
5182
5251
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
5183
5252
  - executionMode: "parallel|sequential" (default is parallel)
5253
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
5254
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
5255
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
5184
5256
 
5185
5257
  Return ONLY the JSON array.`;
5186
5258
  const messages = [{ role: "user", content: prompt }];
@@ -5778,6 +5850,8 @@ var T1Administrator = class extends BaseTier {
5778
5850
  this.signal = signal;
5779
5851
  this.taskId = randomUUID();
5780
5852
  this.setLabel("Administrator");
5853
+ const m = this.router.getModelForTier("T1");
5854
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5781
5855
  this.setStatus("ACTIVE");
5782
5856
  this.taskGoal = userPrompt;
5783
5857
  this.sendStatusUpdate({
@@ -6024,10 +6098,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6024
6098
  "description": "Run npm init",
6025
6099
  "expectedOutput": "package.json created",
6026
6100
  "constraints": [],
6027
- "dependsOn": []
6101
+ "dependsOn": [],
6102
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
6103
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
6104
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
6028
6105
  }]
6029
6106
  }, {
6030
- "sectionId": "s2",
6107
+ "sectionId": "s2",
6031
6108
  "sectionTitle": "Write Tests",
6032
6109
  "description": "Write tests for the project",
6033
6110
  "expectedOutput": "Tests passing",
@@ -6037,7 +6114,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6037
6114
  }]
6038
6115
  }
6039
6116
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
6040
- Leave dependsOn empty for sections that can run immediately in parallel.`;
6117
+ Leave dependsOn empty for sections that can run immediately in parallel.
6118
+
6119
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
6120
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
6121
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
6122
+ - "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.
6123
+ - 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.`;
6041
6124
  const messages = [{ role: "user", content: decompositionPrompt }];
6042
6125
  const result = await this.router.generate("T1", {
6043
6126
  messages,
@@ -6486,16 +6569,16 @@ function resolveInWorkspace(workspaceRoot, input) {
6486
6569
  if (typeof input !== "string" || input.length === 0) {
6487
6570
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
6488
6571
  }
6489
- const root = path20.resolve(workspaceRoot);
6490
- const abs = path20.isAbsolute(input) ? path20.resolve(input) : path20.resolve(root, input);
6491
- const rel = path20.relative(root, abs);
6492
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20.isAbsolute(rel)) {
6572
+ const root = path21.resolve(workspaceRoot);
6573
+ const abs = path21.isAbsolute(input) ? path21.resolve(input) : path21.resolve(root, input);
6574
+ const rel = path21.relative(root, abs);
6575
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21.isAbsolute(rel)) {
6493
6576
  throw new WorkspaceSandboxError(input, root);
6494
6577
  }
6495
6578
  try {
6496
- const real = fs19.realpathSync(abs);
6497
- const realRel = path20.relative(root, real);
6498
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20.isAbsolute(realRel))) {
6579
+ const real = fs20.realpathSync(abs);
6580
+ const realRel = path21.relative(root, real);
6581
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21.isAbsolute(realRel))) {
6499
6582
  throw new WorkspaceSandboxError(input, root);
6500
6583
  }
6501
6584
  } catch (e) {
@@ -6556,7 +6639,7 @@ var FileWriteTool = class extends BaseTool {
6556
6639
  } catch {
6557
6640
  }
6558
6641
  }
6559
- await fs4.mkdir(path20.dirname(absPath), { recursive: true });
6642
+ await fs4.mkdir(path21.dirname(absPath), { recursive: true });
6560
6643
  await fs4.writeFile(absPath, content, "utf-8");
6561
6644
  return `Written ${content.length} characters to ${filePath}`;
6562
6645
  }
@@ -7054,7 +7137,7 @@ var ImageAnalyzeTool = class extends BaseTool {
7054
7137
  };
7055
7138
  async function fileToImageAttachment(filePath) {
7056
7139
  const data = await fs4.readFile(filePath);
7057
- const ext = path20.extname(filePath).toLowerCase();
7140
+ const ext = path21.extname(filePath).toLowerCase();
7058
7141
  const mimeMap = {
7059
7142
  ".jpg": "image/jpeg",
7060
7143
  ".jpeg": "image/jpeg",
@@ -7088,14 +7171,14 @@ var PDFCreateTool = class extends BaseTool {
7088
7171
  const filePath = input["path"];
7089
7172
  const content = input["content"];
7090
7173
  const title = input["title"];
7091
- const dir = path20.dirname(filePath);
7092
- if (!fs19.existsSync(dir)) {
7093
- fs19.mkdirSync(dir, { recursive: true });
7174
+ const dir = path21.dirname(filePath);
7175
+ if (!fs20.existsSync(dir)) {
7176
+ fs20.mkdirSync(dir, { recursive: true });
7094
7177
  }
7095
7178
  return new Promise((resolve, reject) => {
7096
7179
  try {
7097
7180
  const doc = new PDFDocument({ margin: 50 });
7098
- const stream = fs19.createWriteStream(filePath);
7181
+ const stream = fs20.createWriteStream(filePath);
7099
7182
  doc.pipe(stream);
7100
7183
  if (title) {
7101
7184
  doc.info["Title"] = title;
@@ -7173,22 +7256,22 @@ var CodeInterpreterTool = class extends BaseTool {
7173
7256
  }
7174
7257
  cmdPrefix = NODE_CMD;
7175
7258
  }
7176
- const tmpDir = path20.join(this.workspaceRoot, ".cascade", "tmp");
7177
- if (!fs19.existsSync(tmpDir)) {
7178
- fs19.mkdirSync(tmpDir, { recursive: true });
7259
+ const tmpDir = path21.join(this.workspaceRoot, ".cascade", "tmp");
7260
+ if (!fs20.existsSync(tmpDir)) {
7261
+ fs20.mkdirSync(tmpDir, { recursive: true });
7179
7262
  }
7180
7263
  const extension = language === "python" ? "py" : "js";
7181
7264
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
7182
- const filePath = path20.join(tmpDir, fileName);
7183
- fs19.writeFileSync(filePath, code, "utf-8");
7265
+ const filePath = path21.join(tmpDir, fileName);
7266
+ fs20.writeFileSync(filePath, code, "utf-8");
7184
7267
  const execArgs = [filePath, ...args];
7185
7268
  return new Promise((resolve) => {
7186
7269
  const startMs = Date.now();
7187
7270
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
7188
7271
  const duration = Date.now() - startMs;
7189
7272
  try {
7190
- if (fs19.existsSync(filePath)) {
7191
- fs19.unlinkSync(filePath);
7273
+ if (fs20.existsSync(filePath)) {
7274
+ fs20.unlinkSync(filePath);
7192
7275
  }
7193
7276
  } catch (cleanupErr) {
7194
7277
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -7339,30 +7422,56 @@ async function searchTavily(query, apiKey, maxResults) {
7339
7422
  engine: "tavily"
7340
7423
  }));
7341
7424
  }
7342
- async function searchDuckDuckGoLite(query, maxResults) {
7343
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
7344
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
7345
- signal: AbortSignal.timeout(1e4)
7346
- });
7347
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
7348
- const html = await resp.text();
7349
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
7350
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
7351
- const links = [];
7352
- const snippets = [];
7425
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
7426
+ function unwrapDdgRedirect(href) {
7427
+ try {
7428
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
7429
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
7430
+ const target = url.searchParams.get("uddg");
7431
+ if (target) return decodeURIComponent(target);
7432
+ }
7433
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
7434
+ } catch {
7435
+ return href;
7436
+ }
7437
+ }
7438
+ function stripTags(html) {
7439
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
7440
+ }
7441
+ function decodeEntities(text) {
7442
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
7443
+ }
7444
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
7445
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
7446
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
7447
+ const results = [];
7353
7448
  let m;
7354
- while ((m = linkPattern.exec(html)) !== null) {
7355
- links.push({ url: m[1], title: m[2].trim() });
7449
+ while ((m = anchorRe.exec(html)) !== null) {
7450
+ const tag = m[0];
7451
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
7452
+ const title = decodeEntities(stripTags(m[1] ?? ""));
7453
+ if (!href || !title) continue;
7454
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
7356
7455
  }
7357
- while ((m = snippetPattern.exec(html)) !== null) {
7358
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
7456
+ const snippets = [];
7457
+ while ((m = snippetRe.exec(html)) !== null) {
7458
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
7359
7459
  }
7360
- return links.slice(0, maxResults).map((link, i) => ({
7361
- title: link.title,
7362
- url: link.url,
7363
- snippet: snippets[i] ?? "",
7364
- engine: "duckduckgo-lite"
7365
- }));
7460
+ for (let i = 0; i < results.length; i++) {
7461
+ if (snippets[i]) results[i].snippet = snippets[i];
7462
+ }
7463
+ return results;
7464
+ }
7465
+ async function searchDuckDuckGo(query, maxResults, variant) {
7466
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
7467
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
7468
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
7469
+ signal: AbortSignal.timeout(1e4)
7470
+ });
7471
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
7472
+ const html = await resp.text();
7473
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
7474
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
7366
7475
  }
7367
7476
  var WebSearchTool = class extends BaseTool {
7368
7477
  name = "web_search";
@@ -7421,12 +7530,14 @@ var WebSearchTool = class extends BaseTool {
7421
7530
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
7422
7531
  }
7423
7532
  }
7424
- try {
7425
- results = await searchDuckDuckGoLite(query, maxResults);
7426
- if (results.length > 0) return this.formatResults(query, results);
7427
- errors.push("DuckDuckGo Lite: returned 0 results");
7428
- } catch (err) {
7429
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
7533
+ for (const variant of ["html", "lite"]) {
7534
+ try {
7535
+ results = await searchDuckDuckGo(query, maxResults, variant);
7536
+ if (results.length > 0) return this.formatResults(query, results);
7537
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
7538
+ } catch (err) {
7539
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
7540
+ }
7430
7541
  }
7431
7542
  const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
7432
7543
  return [
@@ -7467,7 +7578,7 @@ var GlobTool = class extends BaseTool {
7467
7578
  };
7468
7579
  async execute(input, _options) {
7469
7580
  const pattern = input["pattern"];
7470
- const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7581
+ const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7471
7582
  const matches = await glob(pattern, {
7472
7583
  cwd: searchPath,
7473
7584
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -7480,7 +7591,7 @@ var GlobTool = class extends BaseTool {
7480
7591
  const withMtime = await Promise.all(
7481
7592
  matches.map(async (rel) => {
7482
7593
  try {
7483
- const stat = await fs4.stat(path20.join(searchPath, rel));
7594
+ const stat = await fs4.stat(path21.join(searchPath, rel));
7484
7595
  return { rel, mtime: stat.mtimeMs };
7485
7596
  } catch {
7486
7597
  return { rel, mtime: 0 };
@@ -7529,7 +7640,7 @@ var GrepTool = class extends BaseTool {
7529
7640
  };
7530
7641
  async execute(input, _options) {
7531
7642
  const pattern = input["pattern"];
7532
- const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7643
+ const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7533
7644
  const globPattern = input["glob"];
7534
7645
  const outputMode = input["output_mode"] ?? "content";
7535
7646
  const context = input["context"] ?? 0;
@@ -7583,12 +7694,12 @@ var GrepTool = class extends BaseTool {
7583
7694
  nodir: true
7584
7695
  });
7585
7696
  } catch {
7586
- files = [path20.relative(searchPath, searchPath) || "."];
7697
+ files = [path21.relative(searchPath, searchPath) || "."];
7587
7698
  }
7588
7699
  const results = [];
7589
7700
  let totalCount = 0;
7590
7701
  for (const rel of files) {
7591
- const abs = path20.join(searchPath, rel);
7702
+ const abs = path21.join(searchPath, rel);
7592
7703
  let content;
7593
7704
  try {
7594
7705
  content = await fs4.readFile(abs, "utf-8");
@@ -7950,10 +8061,10 @@ var ToolRegistry = class extends EventEmitter {
7950
8061
  }
7951
8062
  isIgnored(filePath) {
7952
8063
  if (!filePath) return false;
7953
- const abs = path20.resolve(this.workspaceRoot, filePath);
7954
- const rel = path20.relative(this.workspaceRoot, abs);
7955
- if (!rel || rel.startsWith("..") || path20.isAbsolute(rel)) return true;
7956
- const posixRel = rel.split(path20.sep).join("/");
8064
+ const abs = path21.resolve(this.workspaceRoot, filePath);
8065
+ const rel = path21.relative(this.workspaceRoot, abs);
8066
+ if (!rel || rel.startsWith("..") || path21.isAbsolute(rel)) return true;
8067
+ const posixRel = rel.split(path21.sep).join("/");
7957
8068
  return this.ignoreMatcher.ignores(posixRel);
7958
8069
  }
7959
8070
  };
@@ -8370,8 +8481,11 @@ var CascadeConfigSchema = z.object({
8370
8481
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
8371
8482
  * tier based on task type and complexity, overriding the static priority lists.
8372
8483
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
8484
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
8485
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
8486
+ * model pins are unaffected; disable via config/Settings → Advanced.
8373
8487
  */
8374
- cascadeAuto: z.boolean().default(false),
8488
+ cascadeAuto: z.boolean().default(true),
8375
8489
  /**
8376
8490
  * Cascade Auto trade-off bias when picking a model for a task:
8377
8491
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -8787,7 +8901,7 @@ var TaskAnalyzer = class {
8787
8901
  analysisCache.clear();
8788
8902
  }
8789
8903
  };
8790
- var DEFAULT_STATS_FILE = path20.join(os6.homedir(), ".cascade", "model-perf.json");
8904
+ var DEFAULT_STATS_FILE = path21.join(os6.homedir(), ".cascade", "model-perf.json");
8791
8905
  var ModelPerformanceTracker = class {
8792
8906
  stats = /* @__PURE__ */ new Map();
8793
8907
  featureStats = /* @__PURE__ */ new Map();
@@ -8819,7 +8933,7 @@ var ModelPerformanceTracker = class {
8819
8933
  }
8820
8934
  async save() {
8821
8935
  try {
8822
- await fs4.mkdir(path20.dirname(this.statsFile), { recursive: true });
8936
+ await fs4.mkdir(path21.dirname(this.statsFile), { recursive: true });
8823
8937
  const modelsObj = {};
8824
8938
  const featuresObj = {};
8825
8939
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -9358,7 +9472,7 @@ Required capability: ${description.slice(0, 300)}`;
9358
9472
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
9359
9473
  async loadPersistedTools() {
9360
9474
  if (!this.workspacePath || !this.persistEnabled) return;
9361
- const file = path20.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9475
+ const file = path21.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9362
9476
  try {
9363
9477
  const raw = await fs4.readFile(file, "utf-8");
9364
9478
  const specs = JSON.parse(raw);
@@ -9382,8 +9496,8 @@ Required capability: ${description.slice(0, 300)}`;
9382
9496
  }
9383
9497
  async persist() {
9384
9498
  if (!this.workspacePath || !this.persistEnabled) return;
9385
- const dir = path20.join(this.workspacePath, ".cascade");
9386
- const file = path20.join(dir, DYNAMIC_TOOLS_FILE);
9499
+ const dir = path21.join(this.workspacePath, ".cascade");
9500
+ const file = path21.join(dir, DYNAMIC_TOOLS_FILE);
9387
9501
  try {
9388
9502
  await fs4.mkdir(dir, { recursive: true });
9389
9503
  await fs4.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -9403,12 +9517,12 @@ var WorldStateDB = class {
9403
9517
  constructor(workspacePath, debugMode = false) {
9404
9518
  this.workspacePath = workspacePath;
9405
9519
  this.debugMode = debugMode;
9406
- const cascadeDir = path20.join(workspacePath, ".cascade");
9407
- if (!fs19.existsSync(cascadeDir)) {
9408
- fs19.mkdirSync(cascadeDir, { recursive: true });
9520
+ const cascadeDir = path21.join(workspacePath, ".cascade");
9521
+ if (!fs20.existsSync(cascadeDir)) {
9522
+ fs20.mkdirSync(cascadeDir, { recursive: true });
9409
9523
  }
9410
- this.keyPath = path20.join(cascadeDir, "world_state.key");
9411
- this.dbPath = path20.join(cascadeDir, "world_state.db");
9524
+ this.keyPath = path21.join(cascadeDir, "world_state.key");
9525
+ this.dbPath = path21.join(cascadeDir, "world_state.db");
9412
9526
  this.initEncryptionKey();
9413
9527
  this.db = new Database(this.dbPath);
9414
9528
  this.db.pragma("journal_mode = WAL");
@@ -9438,11 +9552,11 @@ var WorldStateDB = class {
9438
9552
  dbPath;
9439
9553
  encryptionKey;
9440
9554
  initEncryptionKey() {
9441
- if (fs19.existsSync(this.keyPath)) {
9442
- this.encryptionKey = fs19.readFileSync(this.keyPath);
9555
+ if (fs20.existsSync(this.keyPath)) {
9556
+ this.encryptionKey = fs20.readFileSync(this.keyPath);
9443
9557
  } else {
9444
9558
  this.encryptionKey = crypto3.randomBytes(32);
9445
- fs19.writeFileSync(this.keyPath, this.encryptionKey);
9559
+ fs20.writeFileSync(this.keyPath, this.encryptionKey);
9446
9560
  }
9447
9561
  }
9448
9562
  encrypt(text) {
@@ -9583,6 +9697,22 @@ var WorldStateDB = class {
9583
9697
  }
9584
9698
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
9585
9699
  }
9700
+ /**
9701
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
9702
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
9703
+ * delete — users can prune what the planner remembers about their project.
9704
+ */
9705
+ deleteFact(entity, relation) {
9706
+ const e = normalizeKey(entity);
9707
+ const r = normalizeKey(relation);
9708
+ if (!e || !r) return false;
9709
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
9710
+ return result.changes > 0;
9711
+ }
9712
+ /** Delete every fact. Returns how many were removed. */
9713
+ clearFacts() {
9714
+ return this.db.prepare("DELETE FROM facts").run().changes;
9715
+ }
9586
9716
  // ── Export / Import ──────────────────────────
9587
9717
  //
9588
9718
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -9631,9 +9761,9 @@ var WorldStateDB = class {
9631
9761
  dumpDebugIfNeeded() {
9632
9762
  if (!this.debugMode) return;
9633
9763
  try {
9634
- const dumpPath = path20.join(os6.tmpdir(), "cascade_world_state_debug.json");
9764
+ const dumpPath = path21.join(os6.tmpdir(), "cascade_world_state_debug.json");
9635
9765
  const entries = this.getAllEntries();
9636
- fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9766
+ fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9637
9767
  } catch (err) {
9638
9768
  console.error("Failed to dump debug world state", err);
9639
9769
  }
@@ -10068,13 +10198,30 @@ ${last.partialOutput}` : "");
10068
10198
  * explicit multi-part structure, so ordinary single-file asks (handled as
10069
10199
  * Simple/Moderate) don't get over-escalated.
10070
10200
  */
10071
- looksClearlyComplex(prompt) {
10201
+ /** Shared build/scale signals for the complexity floors below. */
10202
+ buildSignals(prompt) {
10072
10203
  const p = prompt.trim();
10073
- if (p.length < 24) return false;
10204
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
10074
10205
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
10075
- const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
10206
+ const scaleCount = (p.match(/\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/gi) ?? []).length;
10076
10207
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
10077
- return buildVerb && (scaleNoun || multiPart);
10208
+ return { buildVerb, scaleCount, multiPart };
10209
+ }
10210
+ /**
10211
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
10212
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
10213
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
10214
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
10215
+ * × workers for a task one worker handles).
10216
+ */
10217
+ looksClearlyComplex(prompt) {
10218
+ const s = this.buildSignals(prompt);
10219
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
10220
+ }
10221
+ /** A small single-deliverable build — real work, but one manager's worth. */
10222
+ looksLikeModerateBuild(prompt) {
10223
+ const s = this.buildSignals(prompt);
10224
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
10078
10225
  }
10079
10226
  // Cache glob scan results per workspace path to avoid repeated I/O.
10080
10227
  static globCache = /* @__PURE__ */ new Map();
@@ -10162,6 +10309,9 @@ ${prompt}` : prompt;
10162
10309
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
10163
10310
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
10164
10311
  verdict = "Complex";
10312
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
10313
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
10314
+ verdict = "Moderate";
10165
10315
  } else {
10166
10316
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
10167
10317
  }
@@ -10543,7 +10693,7 @@ var Keystore = class {
10543
10693
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
10544
10694
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
10545
10695
  this.backend = "keytar";
10546
- if (password && fs19.existsSync(this.storePath)) {
10696
+ if (password && fs20.existsSync(this.storePath)) {
10547
10697
  try {
10548
10698
  const fileEntries = this.decryptFile(password);
10549
10699
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -10562,7 +10712,7 @@ var Keystore = class {
10562
10712
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
10563
10713
  );
10564
10714
  }
10565
- if (!fs19.existsSync(this.storePath)) {
10715
+ if (!fs20.existsSync(this.storePath)) {
10566
10716
  const salt = crypto3.randomBytes(SALT_LEN);
10567
10717
  this.masterKey = this.deriveKey(password, salt);
10568
10718
  this.writeWithSalt({}, salt);
@@ -10576,7 +10726,7 @@ var Keystore = class {
10576
10726
  }
10577
10727
  /** Synchronous legacy unlock kept for AES-only environments. */
10578
10728
  unlockSync(password) {
10579
- if (!fs19.existsSync(this.storePath)) {
10729
+ if (!fs20.existsSync(this.storePath)) {
10580
10730
  const salt = crypto3.randomBytes(SALT_LEN);
10581
10731
  this.masterKey = this.deriveKey(password, salt);
10582
10732
  this.writeWithSalt({}, salt);
@@ -10634,7 +10784,7 @@ var Keystore = class {
10634
10784
  }
10635
10785
  }
10636
10786
  decryptFile(password, knownSalt) {
10637
- if (!fs19.existsSync(this.storePath)) return {};
10787
+ if (!fs20.existsSync(this.storePath)) return {};
10638
10788
  try {
10639
10789
  const { salt, ciphertext, iv, tag } = this.readRaw();
10640
10790
  const useSalt = knownSalt ?? salt;
@@ -10656,8 +10806,8 @@ var Keystore = class {
10656
10806
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10657
10807
  const tag = cipher.getAuthTag();
10658
10808
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
10659
- fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
10660
- fs19.writeFileSync(this.storePath, out, { mode: 384 });
10809
+ fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10810
+ fs20.writeFileSync(this.storePath, out, { mode: 384 });
10661
10811
  }
10662
10812
  writeWithSalt(data, salt) {
10663
10813
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -10667,11 +10817,11 @@ var Keystore = class {
10667
10817
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10668
10818
  const tag = cipher.getAuthTag();
10669
10819
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
10670
- fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
10671
- fs19.writeFileSync(this.storePath, out, { mode: 384 });
10820
+ fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10821
+ fs20.writeFileSync(this.storePath, out, { mode: 384 });
10672
10822
  }
10673
10823
  readRaw() {
10674
- const buf = fs19.readFileSync(this.storePath);
10824
+ const buf = fs20.readFileSync(this.storePath);
10675
10825
  let offset = 0;
10676
10826
  const salt = buf.subarray(offset, offset + SALT_LEN);
10677
10827
  offset += SALT_LEN;
@@ -10704,7 +10854,7 @@ var CascadeIgnore = class {
10704
10854
  ]);
10705
10855
  }
10706
10856
  async load(workspacePath) {
10707
- const filePath = path20.join(workspacePath, ".cascadeignore");
10857
+ const filePath = path21.join(workspacePath, ".cascadeignore");
10708
10858
  try {
10709
10859
  const content = await fs4.readFile(filePath, "utf-8");
10710
10860
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -10715,7 +10865,7 @@ var CascadeIgnore = class {
10715
10865
  }
10716
10866
  isIgnored(filePath, workspacePath) {
10717
10867
  try {
10718
- const relative = workspacePath ? path20.relative(workspacePath, filePath) : filePath;
10868
+ const relative = workspacePath ? path21.relative(workspacePath, filePath) : filePath;
10719
10869
  return this.ig.ignores(relative);
10720
10870
  } catch {
10721
10871
  return false;
@@ -10726,7 +10876,7 @@ var CascadeIgnore = class {
10726
10876
  }
10727
10877
  };
10728
10878
  async function loadCascadeMd(workspacePath) {
10729
- const filePath = path20.join(workspacePath, "CASCADE.md");
10879
+ const filePath = path21.join(workspacePath, "CASCADE.md");
10730
10880
  try {
10731
10881
  const raw = await fs4.readFile(filePath, "utf-8");
10732
10882
  return parseCascadeMd(raw);
@@ -10757,7 +10907,7 @@ ${raw.trim()}`;
10757
10907
  var MemoryStore = class _MemoryStore {
10758
10908
  db;
10759
10909
  constructor(dbPath) {
10760
- fs19.mkdirSync(path20.dirname(dbPath), { recursive: true });
10910
+ fs20.mkdirSync(path21.dirname(dbPath), { recursive: true });
10761
10911
  try {
10762
10912
  this.db = new Database(dbPath, { timeout: 5e3 });
10763
10913
  this.db.pragma("journal_mode = WAL");
@@ -11592,6 +11742,60 @@ Original error: ${err.message}`
11592
11742
  };
11593
11743
  }
11594
11744
  };
11745
+ function credentialsPath(globalDir) {
11746
+ return path21.join(globalDir, GLOBAL_CREDENTIALS_FILE);
11747
+ }
11748
+ function providerKey(p) {
11749
+ if (p.type === "azure") {
11750
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
11751
+ }
11752
+ return p.type;
11753
+ }
11754
+ function isPersistable(p) {
11755
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
11756
+ }
11757
+ function loadGlobalCredentials(globalDir) {
11758
+ try {
11759
+ const raw = fs20.readFileSync(credentialsPath(globalDir), "utf-8");
11760
+ const parsed = JSON.parse(raw);
11761
+ if (!Array.isArray(parsed.providers)) return [];
11762
+ return parsed.providers.filter(
11763
+ (p) => Boolean(p) && typeof p.type === "string"
11764
+ );
11765
+ } catch {
11766
+ return [];
11767
+ }
11768
+ }
11769
+ function saveGlobalCredentials(globalDir, providers) {
11770
+ const filePath = credentialsPath(globalDir);
11771
+ fs20.mkdirSync(globalDir, { recursive: true, mode: 448 });
11772
+ const body = { version: 1, providers: providers.filter(isPersistable) };
11773
+ const tmp = `${filePath}.tmp`;
11774
+ fs20.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
11775
+ fs20.renameSync(tmp, filePath);
11776
+ try {
11777
+ fs20.chmodSync(filePath, 384);
11778
+ } catch {
11779
+ }
11780
+ }
11781
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
11782
+ const merged = [...workspaceProviders];
11783
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
11784
+ for (const g of globalProviders) {
11785
+ const existing = byKey.get(providerKey(g));
11786
+ if (!existing) {
11787
+ merged.push({ ...g });
11788
+ byKey.set(providerKey(g), merged[merged.length - 1]);
11789
+ continue;
11790
+ }
11791
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
11792
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
11793
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
11794
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
11795
+ if (!existing.label && g.label) existing.label = g.label;
11796
+ }
11797
+ return merged;
11798
+ }
11595
11799
 
11596
11800
  // src/config/index.ts
11597
11801
  var ConfigManager = class {
@@ -11602,18 +11806,20 @@ var ConfigManager = class {
11602
11806
  cascadeMd = null;
11603
11807
  workspacePath;
11604
11808
  globalDir;
11605
- constructor(workspacePath = process.cwd()) {
11809
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
11810
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
11606
11811
  this.workspacePath = workspacePath;
11607
- this.globalDir = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR);
11812
+ this.globalDir = globalDirOverride ?? path21.join(os6.homedir(), GLOBAL_CONFIG_DIR);
11608
11813
  }
11609
11814
  async load() {
11610
11815
  this.config = await this.loadConfig();
11611
11816
  this.ignore = new CascadeIgnore();
11612
11817
  await this.ignore.load(this.workspacePath);
11613
11818
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
11614
- this.keystore = new Keystore(path20.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11615
- this.store = new MemoryStore(path20.join(this.workspacePath, CASCADE_DB_FILE));
11819
+ this.keystore = new Keystore(path21.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11820
+ this.store = new MemoryStore(path21.join(this.workspacePath, CASCADE_DB_FILE));
11616
11821
  await this.injectEnvKeys();
11822
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
11617
11823
  await this.ensureDefaultIdentity();
11618
11824
  }
11619
11825
  getConfig() {
@@ -11635,9 +11841,14 @@ var ConfigManager = class {
11635
11841
  return this.workspacePath;
11636
11842
  }
11637
11843
  async save() {
11638
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
11639
- await fs4.mkdir(path20.dirname(configPath), { recursive: true });
11844
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
11845
+ await fs4.mkdir(path21.dirname(configPath), { recursive: true });
11640
11846
  await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
11847
+ try {
11848
+ saveGlobalCredentials(this.globalDir, this.config.providers);
11849
+ } catch (err) {
11850
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
11851
+ }
11641
11852
  }
11642
11853
  async updateConfig(updates) {
11643
11854
  this.config = validateConfig({ ...this.config, ...updates });
@@ -11660,7 +11871,7 @@ var ConfigManager = class {
11660
11871
  return configProvider?.apiKey;
11661
11872
  }
11662
11873
  async loadConfig() {
11663
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
11874
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
11664
11875
  try {
11665
11876
  const raw = await fs4.readFile(configPath, "utf-8");
11666
11877
  return validateConfig(JSON.parse(raw));
@@ -11767,6 +11978,11 @@ function authMiddleware(secret, required = true) {
11767
11978
  }
11768
11979
  const user = verifyToken(token, secret);
11769
11980
  if (!user) {
11981
+ if (!required) {
11982
+ req.user = void 0;
11983
+ next();
11984
+ return;
11985
+ }
11770
11986
  res.status(401).json({ error: "Invalid or expired token" });
11771
11987
  return;
11772
11988
  }
@@ -12075,7 +12291,7 @@ function aggregateCostStats(sessions, opts = {}) {
12075
12291
  }
12076
12292
 
12077
12293
  // src/dashboard/server.ts
12078
- var __dirname$1 = path20.dirname(fileURLToPath(import.meta.url));
12294
+ var __dirname$1 = path21.dirname(fileURLToPath(import.meta.url));
12079
12295
  var DashboardServer = class {
12080
12296
  app;
12081
12297
  httpServer;
@@ -12309,12 +12525,17 @@ var DashboardServer = class {
12309
12525
  */
12310
12526
  persistConfig() {
12311
12527
  try {
12312
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
12313
- fs19.mkdirSync(path20.dirname(configPath), { recursive: true });
12314
- fs19.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12528
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
12529
+ fs20.mkdirSync(path21.dirname(configPath), { recursive: true });
12530
+ fs20.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12315
12531
  } catch (err) {
12316
12532
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
12317
12533
  }
12534
+ try {
12535
+ saveGlobalCredentials(path21.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
12536
+ } catch (err) {
12537
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
12538
+ }
12318
12539
  }
12319
12540
  /**
12320
12541
  * Produce a stable dashboard JWT signing secret.
@@ -12326,15 +12547,15 @@ var DashboardServer = class {
12326
12547
  resolveDashboardSecret() {
12327
12548
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
12328
12549
  if (fromConfig) return fromConfig;
12329
- const secretPath = path20.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12550
+ const secretPath = path21.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12330
12551
  try {
12331
- if (fs19.existsSync(secretPath)) {
12332
- const existing = fs19.readFileSync(secretPath, "utf-8").trim();
12552
+ if (fs20.existsSync(secretPath)) {
12553
+ const existing = fs20.readFileSync(secretPath, "utf-8").trim();
12333
12554
  if (existing.length >= 16) return existing;
12334
12555
  }
12335
12556
  const generated = randomUUID();
12336
- fs19.mkdirSync(path20.dirname(secretPath), { recursive: true });
12337
- fs19.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12557
+ fs20.mkdirSync(path21.dirname(secretPath), { recursive: true });
12558
+ fs20.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12338
12559
  if (this.config.dashboard.auth) {
12339
12560
  console.warn(
12340
12561
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -12361,7 +12582,7 @@ var DashboardServer = class {
12361
12582
  // ── Setup ─────────────────────────────────────
12362
12583
  getGlobalStore() {
12363
12584
  if (!this.globalStore) {
12364
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12585
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12365
12586
  this.globalStore = new MemoryStore(globalDbPath);
12366
12587
  }
12367
12588
  return this.globalStore;
@@ -12630,12 +12851,12 @@ ${prompt}`;
12630
12851
  }
12631
12852
  }
12632
12853
  watchRuntimeChanges() {
12633
- const workspaceDbPath = path20.join(this.workspacePath, CASCADE_DB_FILE);
12634
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12854
+ const workspaceDbPath = path21.join(this.workspacePath, CASCADE_DB_FILE);
12855
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12635
12856
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
12636
12857
  for (const watchPath of watchPaths) {
12637
- if (!fs19.existsSync(watchPath)) continue;
12638
- fs19.watchFile(watchPath, { interval: 3e3 }, () => {
12858
+ if (!fs20.existsSync(watchPath)) continue;
12859
+ fs20.watchFile(watchPath, { interval: 3e3 }, () => {
12639
12860
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
12640
12861
  });
12641
12862
  }
@@ -12757,7 +12978,7 @@ ${prompt}`;
12757
12978
  const sessionId = req.params.id;
12758
12979
  this.store.deleteSession(sessionId);
12759
12980
  this.store.deleteRuntimeSession(sessionId);
12760
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12981
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12761
12982
  const globalStore = new MemoryStore(globalDbPath);
12762
12983
  try {
12763
12984
  globalStore.deleteRuntimeSession(sessionId);
@@ -12850,7 +13071,7 @@ ${prompt}`;
12850
13071
  });
12851
13072
  this.app.delete("/api/sessions", auth, (req, res) => {
12852
13073
  const body = req.body;
12853
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13074
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12854
13075
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
12855
13076
  const globalStore = new MemoryStore(globalDbPath);
12856
13077
  try {
@@ -12873,7 +13094,7 @@ ${prompt}`;
12873
13094
  });
12874
13095
  this.app.delete("/api/runtime", auth, (_req, res) => {
12875
13096
  this.store.deleteAllRuntimeNodes();
12876
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13097
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12877
13098
  const globalStore = new MemoryStore(globalDbPath);
12878
13099
  try {
12879
13100
  globalStore.deleteAllRuntimeNodes();
@@ -12959,12 +13180,12 @@ ${prompt}`;
12959
13180
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
12960
13181
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
12961
13182
  try {
12962
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
12963
- const existing = fs19.existsSync(configPath) ? JSON.parse(fs19.readFileSync(configPath, "utf-8")) : {};
13183
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
13184
+ const existing = fs20.existsSync(configPath) ? JSON.parse(fs20.readFileSync(configPath, "utf-8")) : {};
12964
13185
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
12965
13186
  const tmp = configPath + ".tmp";
12966
- fs19.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
12967
- fs19.renameSync(tmp, configPath);
13187
+ fs20.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13188
+ fs20.renameSync(tmp, configPath);
12968
13189
  res.json({ ok: true });
12969
13190
  } catch (err) {
12970
13191
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -12992,7 +13213,7 @@ ${prompt}`;
12992
13213
  this.app.get("/api/runtime", auth, (req, res) => {
12993
13214
  const scope = req.query["scope"] ?? "workspace";
12994
13215
  if (scope === "global") {
12995
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13216
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12996
13217
  const globalStore = new MemoryStore(globalDbPath);
12997
13218
  try {
12998
13219
  res.json({
@@ -13227,6 +13448,48 @@ ${prompt}`;
13227
13448
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13228
13449
  }
13229
13450
  });
13451
+ this.app.get("/api/knowledge", auth, (_req, res) => {
13452
+ try {
13453
+ const ws = new WorldStateDB(this.workspacePath);
13454
+ try {
13455
+ const facts = ws.getAllFacts();
13456
+ res.json({ total: facts.length, facts });
13457
+ } finally {
13458
+ ws.close();
13459
+ }
13460
+ } catch (err) {
13461
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13462
+ }
13463
+ });
13464
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
13465
+ const body = req.body;
13466
+ if (!body.entity || !body.relation) {
13467
+ res.status(400).json({ error: "entity and relation are required" });
13468
+ return;
13469
+ }
13470
+ try {
13471
+ const ws = new WorldStateDB(this.workspacePath);
13472
+ try {
13473
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
13474
+ } finally {
13475
+ ws.close();
13476
+ }
13477
+ } catch (err) {
13478
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13479
+ }
13480
+ });
13481
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
13482
+ try {
13483
+ const ws = new WorldStateDB(this.workspacePath);
13484
+ try {
13485
+ res.json({ ok: true, deleted: ws.clearFacts() });
13486
+ } finally {
13487
+ ws.close();
13488
+ }
13489
+ } catch (err) {
13490
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13491
+ }
13492
+ });
13230
13493
  this.app.get("/api/audit-chain", auth, async (req, res) => {
13231
13494
  try {
13232
13495
  const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
@@ -13245,13 +13508,13 @@ ${prompt}`;
13245
13508
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13246
13509
  }
13247
13510
  });
13248
- const prodPath = path20.resolve(__dirname$1, "../web/dist");
13249
- const devPath = path20.resolve(__dirname$1, "../../web/dist");
13250
- const webDistPath = fs19.existsSync(prodPath) ? prodPath : devPath;
13251
- if (fs19.existsSync(webDistPath)) {
13511
+ const prodPath = path21.resolve(__dirname$1, "../web/dist");
13512
+ const devPath = path21.resolve(__dirname$1, "../../web/dist");
13513
+ const webDistPath = fs20.existsSync(prodPath) ? prodPath : devPath;
13514
+ if (fs20.existsSync(webDistPath)) {
13252
13515
  this.app.use(express.static(webDistPath));
13253
13516
  this.app.get("*", (_req, res) => {
13254
- res.sendFile(path20.join(webDistPath, "index.html"));
13517
+ res.sendFile(path21.join(webDistPath, "index.html"));
13255
13518
  });
13256
13519
  } else {
13257
13520
  this.app.get("/", (_req, res) => {
@@ -13324,6 +13587,6 @@ var HooksRunner = class {
13324
13587
  }
13325
13588
  };
13326
13589
 
13327
- 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_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 };
13590
+ 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 };
13328
13591
  //# sourceMappingURL=index.js.map
13329
13592
  //# sourceMappingURL=index.js.map