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.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var Database = require('better-sqlite3');
4
- var path20 = require('path');
5
- var fs19 = require('fs');
4
+ var path21 = require('path');
5
+ var fs20 = require('fs');
6
6
  var os6 = require('os');
7
7
  var crypto3 = require('crypto');
8
8
  var EventEmitter = require('events');
@@ -58,8 +58,8 @@ function _interopNamespace(e) {
58
58
  }
59
59
 
60
60
  var Database__default = /*#__PURE__*/_interopDefault(Database);
61
- var path20__default = /*#__PURE__*/_interopDefault(path20);
62
- var fs19__default = /*#__PURE__*/_interopDefault(fs19);
61
+ var path21__default = /*#__PURE__*/_interopDefault(path21);
62
+ var fs20__default = /*#__PURE__*/_interopDefault(fs20);
63
63
  var os6__default = /*#__PURE__*/_interopDefault(os6);
64
64
  var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
65
65
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
@@ -104,12 +104,12 @@ var init_audit_logger = __esm({
104
104
  constructor(workspacePath, debugMode = false) {
105
105
  this.workspacePath = workspacePath;
106
106
  this.debugMode = debugMode;
107
- const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
108
- if (!fs19__default.default.existsSync(cascadeDir)) {
109
- fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
107
+ const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
108
+ if (!fs20__default.default.existsSync(cascadeDir)) {
109
+ fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
110
110
  }
111
- this.keyPath = path20__default.default.join(cascadeDir, "audit_log.key");
112
- this.dbPath = path20__default.default.join(cascadeDir, "audit_log.db");
111
+ this.keyPath = path21__default.default.join(cascadeDir, "audit_log.key");
112
+ this.dbPath = path21__default.default.join(cascadeDir, "audit_log.db");
113
113
  this.initEncryptionKey();
114
114
  this.db = new Database__default.default(this.dbPath);
115
115
  this.db.pragma("journal_mode = WAL");
@@ -139,11 +139,11 @@ var init_audit_logger = __esm({
139
139
  return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
140
140
  }
141
141
  initEncryptionKey() {
142
- if (fs19__default.default.existsSync(this.keyPath)) {
143
- this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
142
+ if (fs20__default.default.existsSync(this.keyPath)) {
143
+ this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
144
144
  } else {
145
145
  this.encryptionKey = crypto3__default.default.randomBytes(32);
146
- fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
146
+ fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
147
147
  }
148
148
  }
149
149
  encrypt(text) {
@@ -221,9 +221,9 @@ var init_audit_logger = __esm({
221
221
  dumpDebugIfNeeded() {
222
222
  if (!this.debugMode) return;
223
223
  try {
224
- const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
224
+ const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
225
225
  const entries = this.getAllLogs();
226
- fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
226
+ fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
227
227
  } catch (err) {
228
228
  console.error("Failed to dump debug audit logs", err);
229
229
  }
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
236
236
  });
237
237
 
238
238
  // src/constants.ts
239
- var CASCADE_VERSION = "0.17.0";
239
+ var CASCADE_VERSION = "0.19.0";
240
240
  var CASCADE_CONFIG_DIR = ".cascade";
241
241
  var CASCADE_MD_FILE = "CASCADE.md";
242
242
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -248,6 +248,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
248
248
  var GLOBAL_CONFIG_DIR = ".cascade-ai";
249
249
  var GLOBAL_DB_FILE = "memory.db";
250
250
  var GLOBAL_KEYSTORE_FILE = "keystore.enc";
251
+ var GLOBAL_CREDENTIALS_FILE = "credentials.json";
251
252
  var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
252
253
  var DEFAULT_DASHBOARD_PORT = 4891;
253
254
  var DEFAULT_API_PORT = 4892;
@@ -1038,6 +1039,23 @@ var OpenAIProvider = class extends BaseProvider {
1038
1039
  };
1039
1040
 
1040
1041
  // src/providers/azure.ts
1042
+ function azureModelForDeployment(cfg) {
1043
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
1044
+ const id = cfg.deploymentName.trim();
1045
+ return {
1046
+ id,
1047
+ name: cfg.label?.trim() || id,
1048
+ provider: "azure",
1049
+ contextWindow: 128e3,
1050
+ isVisionCapable: false,
1051
+ inputCostPer1kTokens: 25e-4,
1052
+ outputCostPer1kTokens: 0.01,
1053
+ maxOutputTokens: 16e3,
1054
+ supportsStreaming: true,
1055
+ isLocal: false,
1056
+ supportsToolUse: true
1057
+ };
1058
+ }
1041
1059
  var AzureOpenAIProvider = class extends OpenAIProvider {
1042
1060
  constructor(config, model) {
1043
1061
  const rawUrl = config.baseUrl ?? AZURE_BASE_URL_TEMPLATE.replace("{resource}", "YOUR_RESOURCE");
@@ -1058,7 +1076,8 @@ var AzureOpenAIProvider = class extends OpenAIProvider {
1058
1076
  });
1059
1077
  }
1060
1078
  async listModels() {
1061
- return [this.model];
1079
+ const fromDeployment = azureModelForDeployment(this.config);
1080
+ return [fromDeployment ?? this.model];
1062
1081
  }
1063
1082
  async isAvailable() {
1064
1083
  try {
@@ -2210,7 +2229,7 @@ function computeDelegationSavings(stats, t1Model) {
2210
2229
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
2211
2230
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2212
2231
  var FETCH_TIMEOUT_MS = 8e3;
2213
- var DEFAULT_CACHE_FILE = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2232
+ var DEFAULT_CACHE_FILE = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2214
2233
  function normalizeModelId(id) {
2215
2234
  let s = id.toLowerCase();
2216
2235
  const slash = s.lastIndexOf("/");
@@ -2355,7 +2374,7 @@ var LiveDataProvider = class {
2355
2374
  }
2356
2375
  async saveCache() {
2357
2376
  try {
2358
- await fs4__default.default.mkdir(path20__default.default.dirname(this.opts.cacheFile), { recursive: true });
2377
+ await fs4__default.default.mkdir(path21__default.default.dirname(this.opts.cacheFile), { recursive: true });
2359
2378
  const cache = {
2360
2379
  fetchedAt: this.fetchedAt,
2361
2380
  snapshot: this.snapshot ?? void 0,
@@ -2567,6 +2586,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
2567
2586
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
2568
2587
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
2569
2588
  }
2589
+ if (availableProviders.has("azure")) {
2590
+ for (const cfg of config.providers) {
2591
+ const model = azureModelForDeployment(cfg);
2592
+ if (model) this.selector.addDynamicModel(model);
2593
+ }
2594
+ }
2570
2595
  for (const tier of ["T1", "T2", "T3"]) {
2571
2596
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
2572
2597
  if (!override || override === "auto") continue;
@@ -3173,7 +3198,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
3173
3198
  ensureProvider(model, configs) {
3174
3199
  const key = `${model.provider}:${model.id}`;
3175
3200
  if (this.providers.has(key)) return;
3176
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
3201
+ 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 };
3177
3202
  const provider = this.createProvider(cfg, model);
3178
3203
  this.providers.set(key, provider);
3179
3204
  }
@@ -3333,6 +3358,12 @@ var BaseTier = class extends EventEmitter__default.default {
3333
3358
  * which would interleave, are not tagged.
3334
3359
  */
3335
3360
  isPresenter = false;
3361
+ /**
3362
+ * The model actually serving this tier (`provider:id`), once resolved —
3363
+ * rides on every tier:status event so the desktop can show which model ran
3364
+ * which node (Cockpit node panel / Why panel).
3365
+ */
3366
+ servingModel;
3336
3367
  constructor(role, id, parentId) {
3337
3368
  super();
3338
3369
  this.role = role;
@@ -3357,11 +3388,16 @@ var BaseTier = class extends EventEmitter__default.default {
3357
3388
  label: this.label,
3358
3389
  status,
3359
3390
  timestamp,
3360
- output
3391
+ output,
3392
+ model: this.servingModel
3361
3393
  };
3362
3394
  this.emit("status", event);
3363
3395
  this.emit("tier:status", event);
3364
3396
  }
3397
+ /** Record the model serving this tier; future status events carry it. */
3398
+ setServingModel(model) {
3399
+ this.servingModel = model || void 0;
3400
+ }
3365
3401
  setLabel(label) {
3366
3402
  this.label = label;
3367
3403
  }
@@ -3384,7 +3420,8 @@ var BaseTier = class extends EventEmitter__default.default {
3384
3420
  currentAction: update.currentAction,
3385
3421
  progressPct: update.progressPct,
3386
3422
  timestamp,
3387
- output: update.output
3423
+ output: update.output,
3424
+ model: this.servingModel
3388
3425
  });
3389
3426
  }
3390
3427
  buildMessage(type, to, payload) {
@@ -3750,6 +3787,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
3750
3787
  When you have enough information, stop calling tools and write your final answer.`;
3751
3788
  }
3752
3789
 
3790
+ // src/utils/truncate.ts
3791
+ function truncateForContext(text, maxChars = 12e3) {
3792
+ if (text.length <= maxChars) return text;
3793
+ const headLen = Math.floor(maxChars * 0.75);
3794
+ const tailLen = maxChars - headLen;
3795
+ const head = text.slice(0, headLen);
3796
+ const tail = text.slice(-tailLen);
3797
+ const elided = text.length - headLen - tailLen;
3798
+ return `${head}
3799
+
3800
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
3801
+
3802
+ ${tail}`;
3803
+ }
3804
+
3753
3805
  // src/core/tiers/t3-worker.ts
3754
3806
  var CriticalToolError = class extends Error {
3755
3807
  constructor(message, toolName) {
@@ -4057,6 +4109,7 @@ Now execute your subtask using this context where relevant.`
4057
4109
  } catch {
4058
4110
  }
4059
4111
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
4112
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
4060
4113
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
4061
4114
  let sentFullTextContract = false;
4062
4115
  let textContractSignature = "";
@@ -4154,7 +4207,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4154
4207
  const toolResult = await this.executeTool(tc);
4155
4208
  await this.context.addMessage({
4156
4209
  role: "tool",
4157
- content: toolResult,
4210
+ content: truncateForContext(toolResult),
4158
4211
  toolCallId: tc.id
4159
4212
  });
4160
4213
  }
@@ -4258,8 +4311,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4258
4311
  tierId: this.id,
4259
4312
  sessionId: this.taskId,
4260
4313
  requireApproval: false,
4261
- saveSnapshot: async (path21, content) => {
4262
- this.store?.addFileSnapshot(this.taskId, path21, content);
4314
+ saveSnapshot: async (path22, content) => {
4315
+ this.store?.addFileSnapshot(this.taskId, path22, content);
4263
4316
  },
4264
4317
  sendPeerSync: (to, syncType, content) => {
4265
4318
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -4411,15 +4464,17 @@ ${assignment.expectedOutput}`;
4411
4464
  };
4412
4465
  }
4413
4466
  requiresArtifact() {
4467
+ if (this.assignment?.files?.length) return true;
4414
4468
  const haystack = `${this.assignment?.description ?? ""}
4415
4469
  ${this.assignment?.expectedOutput ?? ""}`;
4416
4470
  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);
4417
4471
  }
4418
4472
  extractArtifactPaths(assignment) {
4473
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
4419
4474
  const haystack = `${assignment.description}
4420
4475
  ${assignment.expectedOutput}`;
4421
4476
  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) ?? [];
4422
- return [...new Set(matches.map((m) => m.trim()))];
4477
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
4423
4478
  }
4424
4479
  async verifyArtifacts(assignment) {
4425
4480
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -4429,7 +4484,7 @@ ${assignment.expectedOutput}`;
4429
4484
  const { promisify: promisify4 } = await import('util');
4430
4485
  const execAsync2 = promisify4(exec2);
4431
4486
  for (const artifactPath of artifactPaths) {
4432
- const absolutePath = path20__default.default.resolve(process.cwd(), artifactPath);
4487
+ const absolutePath = path21__default.default.resolve(process.cwd(), artifactPath);
4433
4488
  try {
4434
4489
  const stat = await fs4__default.default.stat(absolutePath);
4435
4490
  if (!stat.isFile()) {
@@ -4450,7 +4505,7 @@ ${assignment.expectedOutput}`;
4450
4505
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4451
4506
  continue;
4452
4507
  }
4453
- const ext = path20__default.default.extname(absolutePath).toLowerCase();
4508
+ const ext = path21__default.default.extname(absolutePath).toLowerCase();
4454
4509
  try {
4455
4510
  if (ext === ".ts" || ext === ".tsx") {
4456
4511
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -4542,7 +4597,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4542
4597
  Assignment: ${assignment.description}
4543
4598
  Expected output: ${assignment.expectedOutput}
4544
4599
  Constraints: ${assignment.constraints.join("; ")}
4545
-
4600
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
4601
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4602
+ ` : ""}
4546
4603
  Output to test:
4547
4604
  ${output}
4548
4605
 
@@ -4631,17 +4688,27 @@ Your subtask:
4631
4688
  - Title: ${assignment.subtaskTitle}
4632
4689
  - Description: ${assignment.description}
4633
4690
  - Expected output: ${assignment.expectedOutput}
4634
- - Constraints: ${assignment.constraints.join("; ")}`;
4691
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
4692
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
4693
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
4635
4694
  }
4636
4695
  buildInitialPrompt(assignment) {
4637
4696
  return `Execute the following subtask completely:
4638
4697
 
4639
4698
  **${assignment.subtaskTitle}**
4640
-
4699
+ ${assignment.contextBrief ? `
4700
+ Context: ${assignment.contextBrief}
4701
+ ` : ""}
4641
4702
  ${assignment.description}
4642
4703
 
4643
4704
  Expected output: ${assignment.expectedOutput}
4644
-
4705
+ ${assignment.files?.length ? `
4706
+ Files you own (create or edit exactly these paths):
4707
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
4708
+ ` : ""}${assignment.acceptance?.length ? `
4709
+ Definition of done (your output must satisfy ALL of these):
4710
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
4711
+ ` : ""}
4645
4712
  Constraints:
4646
4713
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
4647
4714
 
@@ -5122,6 +5189,8 @@ var T2Manager = class extends BaseTier {
5122
5189
  this.assignment = assignment;
5123
5190
  this.taskId = taskId;
5124
5191
  this.setLabel(assignment.sectionTitle);
5192
+ const m = this.router.getModelForTier("T2");
5193
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5125
5194
  this.setStatus("ACTIVE");
5126
5195
  this.sendStatusUpdate({
5127
5196
  progressPct: 0,
@@ -5208,7 +5277,7 @@ Guidance (must be followed): ${decision.note}`
5208
5277
  // ── Private ──────────────────────────────────
5209
5278
  async decomposeSection(assignment) {
5210
5279
  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");
5211
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
5280
+ 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).
5212
5281
 
5213
5282
  Section: ${assignment.sectionTitle}
5214
5283
  Description: ${assignment.description}
@@ -5227,6 +5296,9 @@ Return a JSON array of subtask objects, each with:
5227
5296
  - peerT3Ids: string[] (empty for now)
5228
5297
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
5229
5298
  - executionMode: "parallel|sequential" (default is parallel)
5299
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
5300
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
5301
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
5230
5302
 
5231
5303
  Return ONLY the JSON array.`;
5232
5304
  const messages = [{ role: "user", content: prompt }];
@@ -5824,6 +5896,8 @@ var T1Administrator = class extends BaseTier {
5824
5896
  this.signal = signal;
5825
5897
  this.taskId = crypto3.randomUUID();
5826
5898
  this.setLabel("Administrator");
5899
+ const m = this.router.getModelForTier("T1");
5900
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
5827
5901
  this.setStatus("ACTIVE");
5828
5902
  this.taskGoal = userPrompt;
5829
5903
  this.sendStatusUpdate({
@@ -6070,10 +6144,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6070
6144
  "description": "Run npm init",
6071
6145
  "expectedOutput": "package.json created",
6072
6146
  "constraints": [],
6073
- "dependsOn": []
6147
+ "dependsOn": [],
6148
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
6149
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
6150
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
6074
6151
  }]
6075
6152
  }, {
6076
- "sectionId": "s2",
6153
+ "sectionId": "s2",
6077
6154
  "sectionTitle": "Write Tests",
6078
6155
  "description": "Write tests for the project",
6079
6156
  "expectedOutput": "Tests passing",
@@ -6083,7 +6160,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
6083
6160
  }]
6084
6161
  }
6085
6162
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
6086
- Leave dependsOn empty for sections that can run immediately in parallel.`;
6163
+ Leave dependsOn empty for sections that can run immediately in parallel.
6164
+
6165
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
6166
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
6167
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
6168
+ - "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.
6169
+ - 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.`;
6087
6170
  const messages = [{ role: "user", content: decompositionPrompt }];
6088
6171
  const result = await this.router.generate("T1", {
6089
6172
  messages,
@@ -6532,16 +6615,16 @@ function resolveInWorkspace(workspaceRoot, input) {
6532
6615
  if (typeof input !== "string" || input.length === 0) {
6533
6616
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
6534
6617
  }
6535
- const root = path20__default.default.resolve(workspaceRoot);
6536
- const abs = path20__default.default.isAbsolute(input) ? path20__default.default.resolve(input) : path20__default.default.resolve(root, input);
6537
- const rel = path20__default.default.relative(root, abs);
6538
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20__default.default.isAbsolute(rel)) {
6618
+ const root = path21__default.default.resolve(workspaceRoot);
6619
+ const abs = path21__default.default.isAbsolute(input) ? path21__default.default.resolve(input) : path21__default.default.resolve(root, input);
6620
+ const rel = path21__default.default.relative(root, abs);
6621
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21__default.default.isAbsolute(rel)) {
6539
6622
  throw new WorkspaceSandboxError(input, root);
6540
6623
  }
6541
6624
  try {
6542
- const real = fs19__default.default.realpathSync(abs);
6543
- const realRel = path20__default.default.relative(root, real);
6544
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20__default.default.isAbsolute(realRel))) {
6625
+ const real = fs20__default.default.realpathSync(abs);
6626
+ const realRel = path21__default.default.relative(root, real);
6627
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21__default.default.isAbsolute(realRel))) {
6545
6628
  throw new WorkspaceSandboxError(input, root);
6546
6629
  }
6547
6630
  } catch (e) {
@@ -6602,7 +6685,7 @@ var FileWriteTool = class extends BaseTool {
6602
6685
  } catch {
6603
6686
  }
6604
6687
  }
6605
- await fs4__default.default.mkdir(path20__default.default.dirname(absPath), { recursive: true });
6688
+ await fs4__default.default.mkdir(path21__default.default.dirname(absPath), { recursive: true });
6606
6689
  await fs4__default.default.writeFile(absPath, content, "utf-8");
6607
6690
  return `Written ${content.length} characters to ${filePath}`;
6608
6691
  }
@@ -7100,7 +7183,7 @@ var ImageAnalyzeTool = class extends BaseTool {
7100
7183
  };
7101
7184
  async function fileToImageAttachment(filePath) {
7102
7185
  const data = await fs4__default.default.readFile(filePath);
7103
- const ext = path20__default.default.extname(filePath).toLowerCase();
7186
+ const ext = path21__default.default.extname(filePath).toLowerCase();
7104
7187
  const mimeMap = {
7105
7188
  ".jpg": "image/jpeg",
7106
7189
  ".jpeg": "image/jpeg",
@@ -7134,14 +7217,14 @@ var PDFCreateTool = class extends BaseTool {
7134
7217
  const filePath = input["path"];
7135
7218
  const content = input["content"];
7136
7219
  const title = input["title"];
7137
- const dir = path20__default.default.dirname(filePath);
7138
- if (!fs19__default.default.existsSync(dir)) {
7139
- fs19__default.default.mkdirSync(dir, { recursive: true });
7220
+ const dir = path21__default.default.dirname(filePath);
7221
+ if (!fs20__default.default.existsSync(dir)) {
7222
+ fs20__default.default.mkdirSync(dir, { recursive: true });
7140
7223
  }
7141
7224
  return new Promise((resolve, reject) => {
7142
7225
  try {
7143
7226
  const doc = new PDFDocument__default.default({ margin: 50 });
7144
- const stream = fs19__default.default.createWriteStream(filePath);
7227
+ const stream = fs20__default.default.createWriteStream(filePath);
7145
7228
  doc.pipe(stream);
7146
7229
  if (title) {
7147
7230
  doc.info["Title"] = title;
@@ -7219,22 +7302,22 @@ var CodeInterpreterTool = class extends BaseTool {
7219
7302
  }
7220
7303
  cmdPrefix = NODE_CMD;
7221
7304
  }
7222
- const tmpDir = path20__default.default.join(this.workspaceRoot, ".cascade", "tmp");
7223
- if (!fs19__default.default.existsSync(tmpDir)) {
7224
- fs19__default.default.mkdirSync(tmpDir, { recursive: true });
7305
+ const tmpDir = path21__default.default.join(this.workspaceRoot, ".cascade", "tmp");
7306
+ if (!fs20__default.default.existsSync(tmpDir)) {
7307
+ fs20__default.default.mkdirSync(tmpDir, { recursive: true });
7225
7308
  }
7226
7309
  const extension = language === "python" ? "py" : "js";
7227
7310
  const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
7228
- const filePath = path20__default.default.join(tmpDir, fileName);
7229
- fs19__default.default.writeFileSync(filePath, code, "utf-8");
7311
+ const filePath = path21__default.default.join(tmpDir, fileName);
7312
+ fs20__default.default.writeFileSync(filePath, code, "utf-8");
7230
7313
  const execArgs = [filePath, ...args];
7231
7314
  return new Promise((resolve) => {
7232
7315
  const startMs = Date.now();
7233
7316
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
7234
7317
  const duration = Date.now() - startMs;
7235
7318
  try {
7236
- if (fs19__default.default.existsSync(filePath)) {
7237
- fs19__default.default.unlinkSync(filePath);
7319
+ if (fs20__default.default.existsSync(filePath)) {
7320
+ fs20__default.default.unlinkSync(filePath);
7238
7321
  }
7239
7322
  } catch (cleanupErr) {
7240
7323
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -7385,30 +7468,56 @@ async function searchTavily(query, apiKey, maxResults) {
7385
7468
  engine: "tavily"
7386
7469
  }));
7387
7470
  }
7388
- async function searchDuckDuckGoLite(query, maxResults) {
7389
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
7390
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
7391
- signal: AbortSignal.timeout(1e4)
7392
- });
7393
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
7394
- const html = await resp.text();
7395
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
7396
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
7397
- const links = [];
7398
- const snippets = [];
7471
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
7472
+ function unwrapDdgRedirect(href) {
7473
+ try {
7474
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
7475
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
7476
+ const target = url.searchParams.get("uddg");
7477
+ if (target) return decodeURIComponent(target);
7478
+ }
7479
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
7480
+ } catch {
7481
+ return href;
7482
+ }
7483
+ }
7484
+ function stripTags(html) {
7485
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
7486
+ }
7487
+ function decodeEntities(text) {
7488
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
7489
+ }
7490
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
7491
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
7492
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
7493
+ const results = [];
7399
7494
  let m;
7400
- while ((m = linkPattern.exec(html)) !== null) {
7401
- links.push({ url: m[1], title: m[2].trim() });
7495
+ while ((m = anchorRe.exec(html)) !== null) {
7496
+ const tag = m[0];
7497
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
7498
+ const title = decodeEntities(stripTags(m[1] ?? ""));
7499
+ if (!href || !title) continue;
7500
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
7402
7501
  }
7403
- while ((m = snippetPattern.exec(html)) !== null) {
7404
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
7502
+ const snippets = [];
7503
+ while ((m = snippetRe.exec(html)) !== null) {
7504
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
7405
7505
  }
7406
- return links.slice(0, maxResults).map((link, i) => ({
7407
- title: link.title,
7408
- url: link.url,
7409
- snippet: snippets[i] ?? "",
7410
- engine: "duckduckgo-lite"
7411
- }));
7506
+ for (let i = 0; i < results.length; i++) {
7507
+ if (snippets[i]) results[i].snippet = snippets[i];
7508
+ }
7509
+ return results;
7510
+ }
7511
+ async function searchDuckDuckGo(query, maxResults, variant) {
7512
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
7513
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
7514
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
7515
+ signal: AbortSignal.timeout(1e4)
7516
+ });
7517
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
7518
+ const html = await resp.text();
7519
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
7520
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
7412
7521
  }
7413
7522
  var WebSearchTool = class extends BaseTool {
7414
7523
  name = "web_search";
@@ -7467,12 +7576,14 @@ var WebSearchTool = class extends BaseTool {
7467
7576
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
7468
7577
  }
7469
7578
  }
7470
- try {
7471
- results = await searchDuckDuckGoLite(query, maxResults);
7472
- if (results.length > 0) return this.formatResults(query, results);
7473
- errors.push("DuckDuckGo Lite: returned 0 results");
7474
- } catch (err) {
7475
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
7579
+ for (const variant of ["html", "lite"]) {
7580
+ try {
7581
+ results = await searchDuckDuckGo(query, maxResults, variant);
7582
+ if (results.length > 0) return this.formatResults(query, results);
7583
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
7584
+ } catch (err) {
7585
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
7586
+ }
7476
7587
  }
7477
7588
  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" : "";
7478
7589
  return [
@@ -7513,7 +7624,7 @@ var GlobTool = class extends BaseTool {
7513
7624
  };
7514
7625
  async execute(input, _options) {
7515
7626
  const pattern = input["pattern"];
7516
- const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7627
+ const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7517
7628
  const matches = await glob.glob(pattern, {
7518
7629
  cwd: searchPath,
7519
7630
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -7526,7 +7637,7 @@ var GlobTool = class extends BaseTool {
7526
7637
  const withMtime = await Promise.all(
7527
7638
  matches.map(async (rel) => {
7528
7639
  try {
7529
- const stat = await fs4__default.default.stat(path20__default.default.join(searchPath, rel));
7640
+ const stat = await fs4__default.default.stat(path21__default.default.join(searchPath, rel));
7530
7641
  return { rel, mtime: stat.mtimeMs };
7531
7642
  } catch {
7532
7643
  return { rel, mtime: 0 };
@@ -7575,7 +7686,7 @@ var GrepTool = class extends BaseTool {
7575
7686
  };
7576
7687
  async execute(input, _options) {
7577
7688
  const pattern = input["pattern"];
7578
- const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7689
+ const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7579
7690
  const globPattern = input["glob"];
7580
7691
  const outputMode = input["output_mode"] ?? "content";
7581
7692
  const context = input["context"] ?? 0;
@@ -7629,12 +7740,12 @@ var GrepTool = class extends BaseTool {
7629
7740
  nodir: true
7630
7741
  });
7631
7742
  } catch {
7632
- files = [path20__default.default.relative(searchPath, searchPath) || "."];
7743
+ files = [path21__default.default.relative(searchPath, searchPath) || "."];
7633
7744
  }
7634
7745
  const results = [];
7635
7746
  let totalCount = 0;
7636
7747
  for (const rel of files) {
7637
- const abs = path20__default.default.join(searchPath, rel);
7748
+ const abs = path21__default.default.join(searchPath, rel);
7638
7749
  let content;
7639
7750
  try {
7640
7751
  content = await fs4__default.default.readFile(abs, "utf-8");
@@ -7996,10 +8107,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
7996
8107
  }
7997
8108
  isIgnored(filePath) {
7998
8109
  if (!filePath) return false;
7999
- const abs = path20__default.default.resolve(this.workspaceRoot, filePath);
8000
- const rel = path20__default.default.relative(this.workspaceRoot, abs);
8001
- if (!rel || rel.startsWith("..") || path20__default.default.isAbsolute(rel)) return true;
8002
- const posixRel = rel.split(path20__default.default.sep).join("/");
8110
+ const abs = path21__default.default.resolve(this.workspaceRoot, filePath);
8111
+ const rel = path21__default.default.relative(this.workspaceRoot, abs);
8112
+ if (!rel || rel.startsWith("..") || path21__default.default.isAbsolute(rel)) return true;
8113
+ const posixRel = rel.split(path21__default.default.sep).join("/");
8003
8114
  return this.ignoreMatcher.ignores(posixRel);
8004
8115
  }
8005
8116
  };
@@ -8416,8 +8527,11 @@ var CascadeConfigSchema = zod.z.object({
8416
8527
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
8417
8528
  * tier based on task type and complexity, overriding the static priority lists.
8418
8529
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
8530
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
8531
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
8532
+ * model pins are unaffected; disable via config/Settings → Advanced.
8419
8533
  */
8420
- cascadeAuto: zod.z.boolean().default(false),
8534
+ cascadeAuto: zod.z.boolean().default(true),
8421
8535
  /**
8422
8536
  * Cascade Auto trade-off bias when picking a model for a task:
8423
8537
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -8833,7 +8947,7 @@ var TaskAnalyzer = class {
8833
8947
  analysisCache.clear();
8834
8948
  }
8835
8949
  };
8836
- var DEFAULT_STATS_FILE = path20__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
8950
+ var DEFAULT_STATS_FILE = path21__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
8837
8951
  var ModelPerformanceTracker = class {
8838
8952
  stats = /* @__PURE__ */ new Map();
8839
8953
  featureStats = /* @__PURE__ */ new Map();
@@ -8865,7 +8979,7 @@ var ModelPerformanceTracker = class {
8865
8979
  }
8866
8980
  async save() {
8867
8981
  try {
8868
- await fs4__default.default.mkdir(path20__default.default.dirname(this.statsFile), { recursive: true });
8982
+ await fs4__default.default.mkdir(path21__default.default.dirname(this.statsFile), { recursive: true });
8869
8983
  const modelsObj = {};
8870
8984
  const featuresObj = {};
8871
8985
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -9404,7 +9518,7 @@ Required capability: ${description.slice(0, 300)}`;
9404
9518
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
9405
9519
  async loadPersistedTools() {
9406
9520
  if (!this.workspacePath || !this.persistEnabled) return;
9407
- const file = path20__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9521
+ const file = path21__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9408
9522
  try {
9409
9523
  const raw = await fs4__default.default.readFile(file, "utf-8");
9410
9524
  const specs = JSON.parse(raw);
@@ -9428,8 +9542,8 @@ Required capability: ${description.slice(0, 300)}`;
9428
9542
  }
9429
9543
  async persist() {
9430
9544
  if (!this.workspacePath || !this.persistEnabled) return;
9431
- const dir = path20__default.default.join(this.workspacePath, ".cascade");
9432
- const file = path20__default.default.join(dir, DYNAMIC_TOOLS_FILE);
9545
+ const dir = path21__default.default.join(this.workspacePath, ".cascade");
9546
+ const file = path21__default.default.join(dir, DYNAMIC_TOOLS_FILE);
9433
9547
  try {
9434
9548
  await fs4__default.default.mkdir(dir, { recursive: true });
9435
9549
  await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -9449,12 +9563,12 @@ var WorldStateDB = class {
9449
9563
  constructor(workspacePath, debugMode = false) {
9450
9564
  this.workspacePath = workspacePath;
9451
9565
  this.debugMode = debugMode;
9452
- const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
9453
- if (!fs19__default.default.existsSync(cascadeDir)) {
9454
- fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
9566
+ const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
9567
+ if (!fs20__default.default.existsSync(cascadeDir)) {
9568
+ fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
9455
9569
  }
9456
- this.keyPath = path20__default.default.join(cascadeDir, "world_state.key");
9457
- this.dbPath = path20__default.default.join(cascadeDir, "world_state.db");
9570
+ this.keyPath = path21__default.default.join(cascadeDir, "world_state.key");
9571
+ this.dbPath = path21__default.default.join(cascadeDir, "world_state.db");
9458
9572
  this.initEncryptionKey();
9459
9573
  this.db = new Database__default.default(this.dbPath);
9460
9574
  this.db.pragma("journal_mode = WAL");
@@ -9484,11 +9598,11 @@ var WorldStateDB = class {
9484
9598
  dbPath;
9485
9599
  encryptionKey;
9486
9600
  initEncryptionKey() {
9487
- if (fs19__default.default.existsSync(this.keyPath)) {
9488
- this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
9601
+ if (fs20__default.default.existsSync(this.keyPath)) {
9602
+ this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
9489
9603
  } else {
9490
9604
  this.encryptionKey = crypto3__default.default.randomBytes(32);
9491
- fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9605
+ fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9492
9606
  }
9493
9607
  }
9494
9608
  encrypt(text) {
@@ -9629,6 +9743,22 @@ var WorldStateDB = class {
9629
9743
  }
9630
9744
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
9631
9745
  }
9746
+ /**
9747
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
9748
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
9749
+ * delete — users can prune what the planner remembers about their project.
9750
+ */
9751
+ deleteFact(entity, relation) {
9752
+ const e = normalizeKey(entity);
9753
+ const r = normalizeKey(relation);
9754
+ if (!e || !r) return false;
9755
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
9756
+ return result.changes > 0;
9757
+ }
9758
+ /** Delete every fact. Returns how many were removed. */
9759
+ clearFacts() {
9760
+ return this.db.prepare("DELETE FROM facts").run().changes;
9761
+ }
9632
9762
  // ── Export / Import ──────────────────────────
9633
9763
  //
9634
9764
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -9677,9 +9807,9 @@ var WorldStateDB = class {
9677
9807
  dumpDebugIfNeeded() {
9678
9808
  if (!this.debugMode) return;
9679
9809
  try {
9680
- const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
9810
+ const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
9681
9811
  const entries = this.getAllEntries();
9682
- fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9812
+ fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9683
9813
  } catch (err) {
9684
9814
  console.error("Failed to dump debug world state", err);
9685
9815
  }
@@ -10114,13 +10244,30 @@ ${last.partialOutput}` : "");
10114
10244
  * explicit multi-part structure, so ordinary single-file asks (handled as
10115
10245
  * Simple/Moderate) don't get over-escalated.
10116
10246
  */
10117
- looksClearlyComplex(prompt) {
10247
+ /** Shared build/scale signals for the complexity floors below. */
10248
+ buildSignals(prompt) {
10118
10249
  const p = prompt.trim();
10119
- if (p.length < 24) return false;
10250
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
10120
10251
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
10121
- 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);
10252
+ 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;
10122
10253
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
10123
- return buildVerb && (scaleNoun || multiPart);
10254
+ return { buildVerb, scaleCount, multiPart };
10255
+ }
10256
+ /**
10257
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
10258
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
10259
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
10260
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
10261
+ * × workers for a task one worker handles).
10262
+ */
10263
+ looksClearlyComplex(prompt) {
10264
+ const s = this.buildSignals(prompt);
10265
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
10266
+ }
10267
+ /** A small single-deliverable build — real work, but one manager's worth. */
10268
+ looksLikeModerateBuild(prompt) {
10269
+ const s = this.buildSignals(prompt);
10270
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
10124
10271
  }
10125
10272
  // Cache glob scan results per workspace path to avoid repeated I/O.
10126
10273
  static globCache = /* @__PURE__ */ new Map();
@@ -10208,6 +10355,9 @@ ${prompt}` : prompt;
10208
10355
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
10209
10356
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
10210
10357
  verdict = "Complex";
10358
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
10359
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
10360
+ verdict = "Moderate";
10211
10361
  } else {
10212
10362
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
10213
10363
  }
@@ -10589,7 +10739,7 @@ var Keystore = class {
10589
10739
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
10590
10740
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
10591
10741
  this.backend = "keytar";
10592
- if (password && fs19__default.default.existsSync(this.storePath)) {
10742
+ if (password && fs20__default.default.existsSync(this.storePath)) {
10593
10743
  try {
10594
10744
  const fileEntries = this.decryptFile(password);
10595
10745
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -10608,7 +10758,7 @@ var Keystore = class {
10608
10758
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
10609
10759
  );
10610
10760
  }
10611
- if (!fs19__default.default.existsSync(this.storePath)) {
10761
+ if (!fs20__default.default.existsSync(this.storePath)) {
10612
10762
  const salt = crypto3__default.default.randomBytes(SALT_LEN);
10613
10763
  this.masterKey = this.deriveKey(password, salt);
10614
10764
  this.writeWithSalt({}, salt);
@@ -10622,7 +10772,7 @@ var Keystore = class {
10622
10772
  }
10623
10773
  /** Synchronous legacy unlock kept for AES-only environments. */
10624
10774
  unlockSync(password) {
10625
- if (!fs19__default.default.existsSync(this.storePath)) {
10775
+ if (!fs20__default.default.existsSync(this.storePath)) {
10626
10776
  const salt = crypto3__default.default.randomBytes(SALT_LEN);
10627
10777
  this.masterKey = this.deriveKey(password, salt);
10628
10778
  this.writeWithSalt({}, salt);
@@ -10680,7 +10830,7 @@ var Keystore = class {
10680
10830
  }
10681
10831
  }
10682
10832
  decryptFile(password, knownSalt) {
10683
- if (!fs19__default.default.existsSync(this.storePath)) return {};
10833
+ if (!fs20__default.default.existsSync(this.storePath)) return {};
10684
10834
  try {
10685
10835
  const { salt, ciphertext, iv, tag } = this.readRaw();
10686
10836
  const useSalt = knownSalt ?? salt;
@@ -10702,8 +10852,8 @@ var Keystore = class {
10702
10852
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10703
10853
  const tag = cipher.getAuthTag();
10704
10854
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
10705
- fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10706
- fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10855
+ fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
10856
+ fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10707
10857
  }
10708
10858
  writeWithSalt(data, salt) {
10709
10859
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -10713,11 +10863,11 @@ var Keystore = class {
10713
10863
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10714
10864
  const tag = cipher.getAuthTag();
10715
10865
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
10716
- fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10717
- fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10866
+ fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
10867
+ fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10718
10868
  }
10719
10869
  readRaw() {
10720
- const buf = fs19__default.default.readFileSync(this.storePath);
10870
+ const buf = fs20__default.default.readFileSync(this.storePath);
10721
10871
  let offset = 0;
10722
10872
  const salt = buf.subarray(offset, offset + SALT_LEN);
10723
10873
  offset += SALT_LEN;
@@ -10750,7 +10900,7 @@ var CascadeIgnore = class {
10750
10900
  ]);
10751
10901
  }
10752
10902
  async load(workspacePath) {
10753
- const filePath = path20__default.default.join(workspacePath, ".cascadeignore");
10903
+ const filePath = path21__default.default.join(workspacePath, ".cascadeignore");
10754
10904
  try {
10755
10905
  const content = await fs4__default.default.readFile(filePath, "utf-8");
10756
10906
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -10761,7 +10911,7 @@ var CascadeIgnore = class {
10761
10911
  }
10762
10912
  isIgnored(filePath, workspacePath) {
10763
10913
  try {
10764
- const relative = workspacePath ? path20__default.default.relative(workspacePath, filePath) : filePath;
10914
+ const relative = workspacePath ? path21__default.default.relative(workspacePath, filePath) : filePath;
10765
10915
  return this.ig.ignores(relative);
10766
10916
  } catch {
10767
10917
  return false;
@@ -10772,7 +10922,7 @@ var CascadeIgnore = class {
10772
10922
  }
10773
10923
  };
10774
10924
  async function loadCascadeMd(workspacePath) {
10775
- const filePath = path20__default.default.join(workspacePath, "CASCADE.md");
10925
+ const filePath = path21__default.default.join(workspacePath, "CASCADE.md");
10776
10926
  try {
10777
10927
  const raw = await fs4__default.default.readFile(filePath, "utf-8");
10778
10928
  return parseCascadeMd(raw);
@@ -10803,7 +10953,7 @@ ${raw.trim()}`;
10803
10953
  var MemoryStore = class _MemoryStore {
10804
10954
  db;
10805
10955
  constructor(dbPath) {
10806
- fs19__default.default.mkdirSync(path20__default.default.dirname(dbPath), { recursive: true });
10956
+ fs20__default.default.mkdirSync(path21__default.default.dirname(dbPath), { recursive: true });
10807
10957
  try {
10808
10958
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
10809
10959
  this.db.pragma("journal_mode = WAL");
@@ -11638,6 +11788,60 @@ Original error: ${err.message}`
11638
11788
  };
11639
11789
  }
11640
11790
  };
11791
+ function credentialsPath(globalDir) {
11792
+ return path21__default.default.join(globalDir, GLOBAL_CREDENTIALS_FILE);
11793
+ }
11794
+ function providerKey(p) {
11795
+ if (p.type === "azure") {
11796
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
11797
+ }
11798
+ return p.type;
11799
+ }
11800
+ function isPersistable(p) {
11801
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
11802
+ }
11803
+ function loadGlobalCredentials(globalDir) {
11804
+ try {
11805
+ const raw = fs20__default.default.readFileSync(credentialsPath(globalDir), "utf-8");
11806
+ const parsed = JSON.parse(raw);
11807
+ if (!Array.isArray(parsed.providers)) return [];
11808
+ return parsed.providers.filter(
11809
+ (p) => Boolean(p) && typeof p.type === "string"
11810
+ );
11811
+ } catch {
11812
+ return [];
11813
+ }
11814
+ }
11815
+ function saveGlobalCredentials(globalDir, providers) {
11816
+ const filePath = credentialsPath(globalDir);
11817
+ fs20__default.default.mkdirSync(globalDir, { recursive: true, mode: 448 });
11818
+ const body = { version: 1, providers: providers.filter(isPersistable) };
11819
+ const tmp = `${filePath}.tmp`;
11820
+ fs20__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
11821
+ fs20__default.default.renameSync(tmp, filePath);
11822
+ try {
11823
+ fs20__default.default.chmodSync(filePath, 384);
11824
+ } catch {
11825
+ }
11826
+ }
11827
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
11828
+ const merged = [...workspaceProviders];
11829
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
11830
+ for (const g of globalProviders) {
11831
+ const existing = byKey.get(providerKey(g));
11832
+ if (!existing) {
11833
+ merged.push({ ...g });
11834
+ byKey.set(providerKey(g), merged[merged.length - 1]);
11835
+ continue;
11836
+ }
11837
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
11838
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
11839
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
11840
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
11841
+ if (!existing.label && g.label) existing.label = g.label;
11842
+ }
11843
+ return merged;
11844
+ }
11641
11845
 
11642
11846
  // src/config/index.ts
11643
11847
  var ConfigManager = class {
@@ -11648,18 +11852,20 @@ var ConfigManager = class {
11648
11852
  cascadeMd = null;
11649
11853
  workspacePath;
11650
11854
  globalDir;
11651
- constructor(workspacePath = process.cwd()) {
11855
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
11856
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
11652
11857
  this.workspacePath = workspacePath;
11653
- this.globalDir = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
11858
+ this.globalDir = globalDirOverride ?? path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
11654
11859
  }
11655
11860
  async load() {
11656
11861
  this.config = await this.loadConfig();
11657
11862
  this.ignore = new CascadeIgnore();
11658
11863
  await this.ignore.load(this.workspacePath);
11659
11864
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
11660
- this.keystore = new Keystore(path20__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11661
- this.store = new MemoryStore(path20__default.default.join(this.workspacePath, CASCADE_DB_FILE));
11865
+ this.keystore = new Keystore(path21__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11866
+ this.store = new MemoryStore(path21__default.default.join(this.workspacePath, CASCADE_DB_FILE));
11662
11867
  await this.injectEnvKeys();
11868
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
11663
11869
  await this.ensureDefaultIdentity();
11664
11870
  }
11665
11871
  getConfig() {
@@ -11681,9 +11887,14 @@ var ConfigManager = class {
11681
11887
  return this.workspacePath;
11682
11888
  }
11683
11889
  async save() {
11684
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11685
- await fs4__default.default.mkdir(path20__default.default.dirname(configPath), { recursive: true });
11890
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11891
+ await fs4__default.default.mkdir(path21__default.default.dirname(configPath), { recursive: true });
11686
11892
  await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
11893
+ try {
11894
+ saveGlobalCredentials(this.globalDir, this.config.providers);
11895
+ } catch (err) {
11896
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
11897
+ }
11687
11898
  }
11688
11899
  async updateConfig(updates) {
11689
11900
  this.config = validateConfig({ ...this.config, ...updates });
@@ -11706,7 +11917,7 @@ var ConfigManager = class {
11706
11917
  return configProvider?.apiKey;
11707
11918
  }
11708
11919
  async loadConfig() {
11709
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11920
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11710
11921
  try {
11711
11922
  const raw = await fs4__default.default.readFile(configPath, "utf-8");
11712
11923
  return validateConfig(JSON.parse(raw));
@@ -11813,6 +12024,11 @@ function authMiddleware(secret, required = true) {
11813
12024
  }
11814
12025
  const user = verifyToken(token, secret);
11815
12026
  if (!user) {
12027
+ if (!required) {
12028
+ req.user = void 0;
12029
+ next();
12030
+ return;
12031
+ }
11816
12032
  res.status(401).json({ error: "Invalid or expired token" });
11817
12033
  return;
11818
12034
  }
@@ -12121,7 +12337,7 @@ function aggregateCostStats(sessions, opts = {}) {
12121
12337
  }
12122
12338
 
12123
12339
  // src/dashboard/server.ts
12124
- var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
12340
+ var __dirname$1 = path21__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
12125
12341
  var DashboardServer = class {
12126
12342
  app;
12127
12343
  httpServer;
@@ -12355,12 +12571,17 @@ var DashboardServer = class {
12355
12571
  */
12356
12572
  persistConfig() {
12357
12573
  try {
12358
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
12359
- fs19__default.default.mkdirSync(path20__default.default.dirname(configPath), { recursive: true });
12360
- fs19__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12574
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
12575
+ fs20__default.default.mkdirSync(path21__default.default.dirname(configPath), { recursive: true });
12576
+ fs20__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12361
12577
  } catch (err) {
12362
12578
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
12363
12579
  }
12580
+ try {
12581
+ saveGlobalCredentials(path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
12582
+ } catch (err) {
12583
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
12584
+ }
12364
12585
  }
12365
12586
  /**
12366
12587
  * Produce a stable dashboard JWT signing secret.
@@ -12372,15 +12593,15 @@ var DashboardServer = class {
12372
12593
  resolveDashboardSecret() {
12373
12594
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
12374
12595
  if (fromConfig) return fromConfig;
12375
- const secretPath = path20__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12596
+ const secretPath = path21__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12376
12597
  try {
12377
- if (fs19__default.default.existsSync(secretPath)) {
12378
- const existing = fs19__default.default.readFileSync(secretPath, "utf-8").trim();
12598
+ if (fs20__default.default.existsSync(secretPath)) {
12599
+ const existing = fs20__default.default.readFileSync(secretPath, "utf-8").trim();
12379
12600
  if (existing.length >= 16) return existing;
12380
12601
  }
12381
12602
  const generated = crypto3.randomUUID();
12382
- fs19__default.default.mkdirSync(path20__default.default.dirname(secretPath), { recursive: true });
12383
- fs19__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12603
+ fs20__default.default.mkdirSync(path21__default.default.dirname(secretPath), { recursive: true });
12604
+ fs20__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12384
12605
  if (this.config.dashboard.auth) {
12385
12606
  console.warn(
12386
12607
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -12407,7 +12628,7 @@ var DashboardServer = class {
12407
12628
  // ── Setup ─────────────────────────────────────
12408
12629
  getGlobalStore() {
12409
12630
  if (!this.globalStore) {
12410
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12631
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12411
12632
  this.globalStore = new MemoryStore(globalDbPath);
12412
12633
  }
12413
12634
  return this.globalStore;
@@ -12676,12 +12897,12 @@ ${prompt}`;
12676
12897
  }
12677
12898
  }
12678
12899
  watchRuntimeChanges() {
12679
- const workspaceDbPath = path20__default.default.join(this.workspacePath, CASCADE_DB_FILE);
12680
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12900
+ const workspaceDbPath = path21__default.default.join(this.workspacePath, CASCADE_DB_FILE);
12901
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12681
12902
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
12682
12903
  for (const watchPath of watchPaths) {
12683
- if (!fs19__default.default.existsSync(watchPath)) continue;
12684
- fs19__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
12904
+ if (!fs20__default.default.existsSync(watchPath)) continue;
12905
+ fs20__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
12685
12906
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
12686
12907
  });
12687
12908
  }
@@ -12803,7 +13024,7 @@ ${prompt}`;
12803
13024
  const sessionId = req.params.id;
12804
13025
  this.store.deleteSession(sessionId);
12805
13026
  this.store.deleteRuntimeSession(sessionId);
12806
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13027
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12807
13028
  const globalStore = new MemoryStore(globalDbPath);
12808
13029
  try {
12809
13030
  globalStore.deleteRuntimeSession(sessionId);
@@ -12896,7 +13117,7 @@ ${prompt}`;
12896
13117
  });
12897
13118
  this.app.delete("/api/sessions", auth, (req, res) => {
12898
13119
  const body = req.body;
12899
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13120
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12900
13121
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
12901
13122
  const globalStore = new MemoryStore(globalDbPath);
12902
13123
  try {
@@ -12919,7 +13140,7 @@ ${prompt}`;
12919
13140
  });
12920
13141
  this.app.delete("/api/runtime", auth, (_req, res) => {
12921
13142
  this.store.deleteAllRuntimeNodes();
12922
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13143
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12923
13144
  const globalStore = new MemoryStore(globalDbPath);
12924
13145
  try {
12925
13146
  globalStore.deleteAllRuntimeNodes();
@@ -13005,12 +13226,12 @@ ${prompt}`;
13005
13226
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
13006
13227
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
13007
13228
  try {
13008
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
13009
- const existing = fs19__default.default.existsSync(configPath) ? JSON.parse(fs19__default.default.readFileSync(configPath, "utf-8")) : {};
13229
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
13230
+ const existing = fs20__default.default.existsSync(configPath) ? JSON.parse(fs20__default.default.readFileSync(configPath, "utf-8")) : {};
13010
13231
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
13011
13232
  const tmp = configPath + ".tmp";
13012
- fs19__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13013
- fs19__default.default.renameSync(tmp, configPath);
13233
+ fs20__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13234
+ fs20__default.default.renameSync(tmp, configPath);
13014
13235
  res.json({ ok: true });
13015
13236
  } catch (err) {
13016
13237
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -13038,7 +13259,7 @@ ${prompt}`;
13038
13259
  this.app.get("/api/runtime", auth, (req, res) => {
13039
13260
  const scope = req.query["scope"] ?? "workspace";
13040
13261
  if (scope === "global") {
13041
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13262
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13042
13263
  const globalStore = new MemoryStore(globalDbPath);
13043
13264
  try {
13044
13265
  res.json({
@@ -13273,6 +13494,48 @@ ${prompt}`;
13273
13494
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13274
13495
  }
13275
13496
  });
13497
+ this.app.get("/api/knowledge", auth, (_req, res) => {
13498
+ try {
13499
+ const ws = new WorldStateDB(this.workspacePath);
13500
+ try {
13501
+ const facts = ws.getAllFacts();
13502
+ res.json({ total: facts.length, facts });
13503
+ } finally {
13504
+ ws.close();
13505
+ }
13506
+ } catch (err) {
13507
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13508
+ }
13509
+ });
13510
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
13511
+ const body = req.body;
13512
+ if (!body.entity || !body.relation) {
13513
+ res.status(400).json({ error: "entity and relation are required" });
13514
+ return;
13515
+ }
13516
+ try {
13517
+ const ws = new WorldStateDB(this.workspacePath);
13518
+ try {
13519
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
13520
+ } finally {
13521
+ ws.close();
13522
+ }
13523
+ } catch (err) {
13524
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13525
+ }
13526
+ });
13527
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
13528
+ try {
13529
+ const ws = new WorldStateDB(this.workspacePath);
13530
+ try {
13531
+ res.json({ ok: true, deleted: ws.clearFacts() });
13532
+ } finally {
13533
+ ws.close();
13534
+ }
13535
+ } catch (err) {
13536
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13537
+ }
13538
+ });
13276
13539
  this.app.get("/api/audit-chain", auth, async (req, res) => {
13277
13540
  try {
13278
13541
  const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
@@ -13291,13 +13554,13 @@ ${prompt}`;
13291
13554
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13292
13555
  }
13293
13556
  });
13294
- const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
13295
- const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
13296
- const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
13297
- if (fs19__default.default.existsSync(webDistPath)) {
13557
+ const prodPath = path21__default.default.resolve(__dirname$1, "../web/dist");
13558
+ const devPath = path21__default.default.resolve(__dirname$1, "../../web/dist");
13559
+ const webDistPath = fs20__default.default.existsSync(prodPath) ? prodPath : devPath;
13560
+ if (fs20__default.default.existsSync(webDistPath)) {
13298
13561
  this.app.use(express__default.default.static(webDistPath));
13299
13562
  this.app.get("*", (_req, res) => {
13300
- res.sendFile(path20__default.default.join(webDistPath, "index.html"));
13563
+ res.sendFile(path21__default.default.join(webDistPath, "index.html"));
13301
13564
  });
13302
13565
  } else {
13303
13566
  this.app.get("/", (_req, res) => {
@@ -13398,6 +13661,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
13398
13661
  exports.DEFAULT_THEME = DEFAULT_THEME;
13399
13662
  exports.DashboardServer = DashboardServer;
13400
13663
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
13664
+ exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
13401
13665
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
13402
13666
  exports.GLOBAL_KEYSTORE_FILE = GLOBAL_KEYSTORE_FILE;
13403
13667
  exports.GLOBAL_RUNTIME_DB_FILE = GLOBAL_RUNTIME_DB_FILE;