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/cli.js CHANGED
@@ -7,8 +7,8 @@ import https from 'https';
7
7
  import zlib from 'zlib';
8
8
  import { Readable } from 'stream';
9
9
  import Database2 from 'better-sqlite3';
10
- import path25 from 'path';
11
- import fs23 from 'fs';
10
+ import path26 from 'path';
11
+ import fs24 from 'fs';
12
12
  import os8 from 'os';
13
13
  import crypto, { randomUUID, timingSafeEqual } from 'crypto';
14
14
  import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
@@ -17,7 +17,7 @@ import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo }
17
17
  import chalk9 from 'chalk';
18
18
  import dotenv from 'dotenv';
19
19
  import { fileURLToPath } from 'url';
20
- import fs9 from 'fs/promises';
20
+ import fs10 from 'fs/promises';
21
21
  import * as _ignoreModule from 'ignore';
22
22
  import _ignoreModule__default from 'ignore';
23
23
  import { z } from 'zod';
@@ -56,16 +56,17 @@ var __export = (target, all) => {
56
56
  };
57
57
 
58
58
  // src/constants.ts
59
- var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
59
+ var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
60
60
  var init_constants = __esm({
61
61
  "src/constants.ts"() {
62
- CASCADE_VERSION = "0.17.0";
62
+ CASCADE_VERSION = "0.19.0";
63
63
  CASCADE_CONFIG_FILE = ".cascade/config.json";
64
64
  CASCADE_DB_FILE = ".cascade/memory.db";
65
65
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
66
66
  GLOBAL_CONFIG_DIR = ".cascade-ai";
67
67
  GLOBAL_DB_FILE = "memory.db";
68
68
  GLOBAL_KEYSTORE_FILE = "keystore.enc";
69
+ GLOBAL_CREDENTIALS_FILE = "credentials.json";
69
70
  GLOBAL_RUNTIME_DB_FILE = "runtime.db";
70
71
  DEFAULT_DASHBOARD_PORT = 4891;
71
72
  DEFAULT_CONTEXT_LIMIT = 2e5;
@@ -884,8 +885,26 @@ var init_openai = __esm({
884
885
  // src/providers/azure.ts
885
886
  var azure_exports = {};
886
887
  __export(azure_exports, {
887
- AzureOpenAIProvider: () => AzureOpenAIProvider
888
+ AzureOpenAIProvider: () => AzureOpenAIProvider,
889
+ azureModelForDeployment: () => azureModelForDeployment
888
890
  });
891
+ function azureModelForDeployment(cfg) {
892
+ if (cfg.type !== "azure" || !cfg.deploymentName?.trim()) return null;
893
+ const id = cfg.deploymentName.trim();
894
+ return {
895
+ id,
896
+ name: cfg.label?.trim() || id,
897
+ provider: "azure",
898
+ contextWindow: 128e3,
899
+ isVisionCapable: false,
900
+ inputCostPer1kTokens: 25e-4,
901
+ outputCostPer1kTokens: 0.01,
902
+ maxOutputTokens: 16e3,
903
+ supportsStreaming: true,
904
+ isLocal: false,
905
+ supportsToolUse: true
906
+ };
907
+ }
889
908
  var AzureOpenAIProvider;
890
909
  var init_azure = __esm({
891
910
  "src/providers/azure.ts"() {
@@ -911,7 +930,8 @@ var init_azure = __esm({
911
930
  });
912
931
  }
913
932
  async listModels() {
914
- return [this.model];
933
+ const fromDeployment = azureModelForDeployment(this.config);
934
+ return [fromDeployment ?? this.model];
915
935
  }
916
936
  async isAvailable() {
917
937
  try {
@@ -1580,12 +1600,12 @@ var init_audit_logger = __esm({
1580
1600
  constructor(workspacePath, debugMode = false) {
1581
1601
  this.workspacePath = workspacePath;
1582
1602
  this.debugMode = debugMode;
1583
- const cascadeDir = path25.join(workspacePath, ".cascade");
1584
- if (!fs23.existsSync(cascadeDir)) {
1585
- fs23.mkdirSync(cascadeDir, { recursive: true });
1603
+ const cascadeDir = path26.join(workspacePath, ".cascade");
1604
+ if (!fs24.existsSync(cascadeDir)) {
1605
+ fs24.mkdirSync(cascadeDir, { recursive: true });
1586
1606
  }
1587
- this.keyPath = path25.join(cascadeDir, "audit_log.key");
1588
- this.dbPath = path25.join(cascadeDir, "audit_log.db");
1607
+ this.keyPath = path26.join(cascadeDir, "audit_log.key");
1608
+ this.dbPath = path26.join(cascadeDir, "audit_log.db");
1589
1609
  this.initEncryptionKey();
1590
1610
  this.db = new Database2(this.dbPath);
1591
1611
  this.db.pragma("journal_mode = WAL");
@@ -1615,11 +1635,11 @@ var init_audit_logger = __esm({
1615
1635
  return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
1616
1636
  }
1617
1637
  initEncryptionKey() {
1618
- if (fs23.existsSync(this.keyPath)) {
1619
- this.encryptionKey = fs23.readFileSync(this.keyPath);
1638
+ if (fs24.existsSync(this.keyPath)) {
1639
+ this.encryptionKey = fs24.readFileSync(this.keyPath);
1620
1640
  } else {
1621
1641
  this.encryptionKey = crypto.randomBytes(32);
1622
- fs23.writeFileSync(this.keyPath, this.encryptionKey);
1642
+ fs24.writeFileSync(this.keyPath, this.encryptionKey);
1623
1643
  }
1624
1644
  }
1625
1645
  encrypt(text) {
@@ -1697,9 +1717,9 @@ var init_audit_logger = __esm({
1697
1717
  dumpDebugIfNeeded() {
1698
1718
  if (!this.debugMode) return;
1699
1719
  try {
1700
- const dumpPath = path25.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
1720
+ const dumpPath = path26.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
1701
1721
  const entries = this.getAllLogs();
1702
- fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1722
+ fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1703
1723
  } catch (err) {
1704
1724
  console.error("Failed to dump debug audit logs", err);
1705
1725
  }
@@ -1754,7 +1774,7 @@ var Keystore = class {
1754
1774
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
1755
1775
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
1756
1776
  this.backend = "keytar";
1757
- if (password && fs23.existsSync(this.storePath)) {
1777
+ if (password && fs24.existsSync(this.storePath)) {
1758
1778
  try {
1759
1779
  const fileEntries = this.decryptFile(password);
1760
1780
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -1773,7 +1793,7 @@ var Keystore = class {
1773
1793
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
1774
1794
  );
1775
1795
  }
1776
- if (!fs23.existsSync(this.storePath)) {
1796
+ if (!fs24.existsSync(this.storePath)) {
1777
1797
  const salt = crypto.randomBytes(SALT_LEN);
1778
1798
  this.masterKey = this.deriveKey(password, salt);
1779
1799
  this.writeWithSalt({}, salt);
@@ -1787,7 +1807,7 @@ var Keystore = class {
1787
1807
  }
1788
1808
  /** Synchronous legacy unlock kept for AES-only environments. */
1789
1809
  unlockSync(password) {
1790
- if (!fs23.existsSync(this.storePath)) {
1810
+ if (!fs24.existsSync(this.storePath)) {
1791
1811
  const salt = crypto.randomBytes(SALT_LEN);
1792
1812
  this.masterKey = this.deriveKey(password, salt);
1793
1813
  this.writeWithSalt({}, salt);
@@ -1845,7 +1865,7 @@ var Keystore = class {
1845
1865
  }
1846
1866
  }
1847
1867
  decryptFile(password, knownSalt) {
1848
- if (!fs23.existsSync(this.storePath)) return {};
1868
+ if (!fs24.existsSync(this.storePath)) return {};
1849
1869
  try {
1850
1870
  const { salt, ciphertext, iv, tag } = this.readRaw();
1851
1871
  const useSalt = knownSalt ?? salt;
@@ -1867,8 +1887,8 @@ var Keystore = class {
1867
1887
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1868
1888
  const tag = cipher.getAuthTag();
1869
1889
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
1870
- fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
1871
- fs23.writeFileSync(this.storePath, out, { mode: 384 });
1890
+ fs24.mkdirSync(path26.dirname(this.storePath), { recursive: true });
1891
+ fs24.writeFileSync(this.storePath, out, { mode: 384 });
1872
1892
  }
1873
1893
  writeWithSalt(data, salt) {
1874
1894
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -1878,11 +1898,11 @@ var Keystore = class {
1878
1898
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1879
1899
  const tag = cipher.getAuthTag();
1880
1900
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
1881
- fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
1882
- fs23.writeFileSync(this.storePath, out, { mode: 384 });
1901
+ fs24.mkdirSync(path26.dirname(this.storePath), { recursive: true });
1902
+ fs24.writeFileSync(this.storePath, out, { mode: 384 });
1883
1903
  }
1884
1904
  readRaw() {
1885
- const buf = fs23.readFileSync(this.storePath);
1905
+ const buf = fs24.readFileSync(this.storePath);
1886
1906
  let offset = 0;
1887
1907
  const salt = buf.subarray(offset, offset + SALT_LEN);
1888
1908
  offset += SALT_LEN;
@@ -1915,9 +1935,9 @@ var CascadeIgnore = class {
1915
1935
  ]);
1916
1936
  }
1917
1937
  async load(workspacePath) {
1918
- const filePath = path25.join(workspacePath, ".cascadeignore");
1938
+ const filePath = path26.join(workspacePath, ".cascadeignore");
1919
1939
  try {
1920
- const content = await fs9.readFile(filePath, "utf-8");
1940
+ const content = await fs10.readFile(filePath, "utf-8");
1921
1941
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
1922
1942
  this.ig.add(lines);
1923
1943
  this.loaded = true;
@@ -1926,7 +1946,7 @@ var CascadeIgnore = class {
1926
1946
  }
1927
1947
  isIgnored(filePath, workspacePath) {
1928
1948
  try {
1929
- const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
1949
+ const relative = workspacePath ? path26.relative(workspacePath, filePath) : filePath;
1930
1950
  return this.ig.ignores(relative);
1931
1951
  } catch {
1932
1952
  return false;
@@ -1937,7 +1957,7 @@ var CascadeIgnore = class {
1937
1957
  }
1938
1958
  };
1939
1959
  async function createDefaultIgnoreFile(workspacePath) {
1940
- const filePath = path25.join(workspacePath, ".cascadeignore");
1960
+ const filePath = path26.join(workspacePath, ".cascadeignore");
1941
1961
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
1942
1962
  # Syntax identical to .gitignore
1943
1963
 
@@ -1964,12 +1984,12 @@ build/
1964
1984
  .DS_Store
1965
1985
  Thumbs.db
1966
1986
  `;
1967
- await fs9.writeFile(filePath, content, "utf-8");
1987
+ await fs10.writeFile(filePath, content, "utf-8");
1968
1988
  }
1969
1989
  async function loadCascadeMd(workspacePath) {
1970
- const filePath = path25.join(workspacePath, "CASCADE.md");
1990
+ const filePath = path26.join(workspacePath, "CASCADE.md");
1971
1991
  try {
1972
- const raw = await fs9.readFile(filePath, "utf-8");
1992
+ const raw = await fs10.readFile(filePath, "utf-8");
1973
1993
  return parseCascadeMd(raw);
1974
1994
  } catch {
1975
1995
  return null;
@@ -1996,7 +2016,7 @@ ${raw.trim()}`;
1996
2016
  return { raw, sections, systemPrompt };
1997
2017
  }
1998
2018
  async function createDefaultCascadeMd(workspacePath) {
1999
- const filePath = path25.join(workspacePath, "CASCADE.md");
2019
+ const filePath = path26.join(workspacePath, "CASCADE.md");
2000
2020
  const content = `# Cascade Project Instructions
2001
2021
 
2002
2022
  This file contains project-specific instructions for the Cascade AI agent.
@@ -2026,12 +2046,12 @@ All tools are allowed unless specified otherwise.
2026
2046
 
2027
2047
  List any areas the agent should not touch.
2028
2048
  `;
2029
- await fs9.writeFile(filePath, content, "utf-8");
2049
+ await fs10.writeFile(filePath, content, "utf-8");
2030
2050
  }
2031
2051
  var MemoryStore = class _MemoryStore {
2032
2052
  db;
2033
2053
  constructor(dbPath) {
2034
- fs23.mkdirSync(path25.dirname(dbPath), { recursive: true });
2054
+ fs24.mkdirSync(path26.dirname(dbPath), { recursive: true });
2035
2055
  try {
2036
2056
  this.db = new Database2(dbPath, { timeout: 5e3 });
2037
2057
  this.db.pragma("journal_mode = WAL");
@@ -2996,8 +3016,11 @@ var CascadeConfigSchema = z.object({
2996
3016
  * Cascade Auto: when true, the TaskAnalyzer selects the optimal model for each
2997
3017
  * tier based on task type and complexity, overriding the static priority lists.
2998
3018
  * Heuristic-first with AI inference fallback (adds ~0–500ms per task).
3019
+ * ON by default since v0.19.0 — "Auto" without it was just a static priority
3020
+ * list, not the benchmark-value routing the docs describe. Explicit per-tier
3021
+ * model pins are unaffected; disable via config/Settings → Advanced.
2999
3022
  */
3000
- cascadeAuto: z.boolean().default(false),
3023
+ cascadeAuto: z.boolean().default(true),
3001
3024
  /**
3002
3025
  * Cascade Auto trade-off bias when picking a model for a task:
3003
3026
  * - 'balanced' (default): quality × cost-efficiency — cheap models win
@@ -3178,6 +3201,63 @@ function validateConfig(raw) {
3178
3201
  return result.data;
3179
3202
  }
3180
3203
 
3204
+ // src/config/global-credentials.ts
3205
+ init_constants();
3206
+ function credentialsPath(globalDir) {
3207
+ return path26.join(globalDir, GLOBAL_CREDENTIALS_FILE);
3208
+ }
3209
+ function providerKey(p) {
3210
+ if (p.type === "azure") {
3211
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
3212
+ }
3213
+ return p.type;
3214
+ }
3215
+ function isPersistable(p) {
3216
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
3217
+ }
3218
+ function loadGlobalCredentials(globalDir) {
3219
+ try {
3220
+ const raw = fs24.readFileSync(credentialsPath(globalDir), "utf-8");
3221
+ const parsed = JSON.parse(raw);
3222
+ if (!Array.isArray(parsed.providers)) return [];
3223
+ return parsed.providers.filter(
3224
+ (p) => Boolean(p) && typeof p.type === "string"
3225
+ );
3226
+ } catch {
3227
+ return [];
3228
+ }
3229
+ }
3230
+ function saveGlobalCredentials(globalDir, providers) {
3231
+ const filePath = credentialsPath(globalDir);
3232
+ fs24.mkdirSync(globalDir, { recursive: true, mode: 448 });
3233
+ const body = { version: 1, providers: providers.filter(isPersistable) };
3234
+ const tmp = `${filePath}.tmp`;
3235
+ fs24.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
3236
+ fs24.renameSync(tmp, filePath);
3237
+ try {
3238
+ fs24.chmodSync(filePath, 384);
3239
+ } catch {
3240
+ }
3241
+ }
3242
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
3243
+ const merged = [...workspaceProviders];
3244
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
3245
+ for (const g of globalProviders) {
3246
+ const existing = byKey.get(providerKey(g));
3247
+ if (!existing) {
3248
+ merged.push({ ...g });
3249
+ byKey.set(providerKey(g), merged[merged.length - 1]);
3250
+ continue;
3251
+ }
3252
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
3253
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
3254
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
3255
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
3256
+ if (!existing.label && g.label) existing.label = g.label;
3257
+ }
3258
+ return merged;
3259
+ }
3260
+
3181
3261
  // src/config/index.ts
3182
3262
  init_constants();
3183
3263
  var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
@@ -3193,18 +3273,20 @@ var ConfigManager = class {
3193
3273
  cascadeMd = null;
3194
3274
  workspacePath;
3195
3275
  globalDir;
3196
- constructor(workspacePath = process.cwd()) {
3276
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
3277
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
3197
3278
  this.workspacePath = workspacePath;
3198
- this.globalDir = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR);
3279
+ this.globalDir = globalDirOverride ?? path26.join(os8.homedir(), GLOBAL_CONFIG_DIR);
3199
3280
  }
3200
3281
  async load() {
3201
3282
  this.config = await this.loadConfig();
3202
3283
  this.ignore = new CascadeIgnore();
3203
3284
  await this.ignore.load(this.workspacePath);
3204
3285
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
3205
- this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3206
- this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
3286
+ this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3287
+ this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
3207
3288
  await this.injectEnvKeys();
3289
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
3208
3290
  await this.ensureDefaultIdentity();
3209
3291
  }
3210
3292
  getConfig() {
@@ -3226,9 +3308,14 @@ var ConfigManager = class {
3226
3308
  return this.workspacePath;
3227
3309
  }
3228
3310
  async save() {
3229
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
3230
- await fs9.mkdir(path25.dirname(configPath), { recursive: true });
3231
- await fs9.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
3311
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
3312
+ await fs10.mkdir(path26.dirname(configPath), { recursive: true });
3313
+ await fs10.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
3314
+ try {
3315
+ saveGlobalCredentials(this.globalDir, this.config.providers);
3316
+ } catch (err) {
3317
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
3318
+ }
3232
3319
  }
3233
3320
  async updateConfig(updates) {
3234
3321
  this.config = validateConfig({ ...this.config, ...updates });
@@ -3251,9 +3338,9 @@ var ConfigManager = class {
3251
3338
  return configProvider?.apiKey;
3252
3339
  }
3253
3340
  async loadConfig() {
3254
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
3341
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
3255
3342
  try {
3256
- const raw = await fs9.readFile(configPath, "utf-8");
3343
+ const raw = await fs10.readFile(configPath, "utf-8");
3257
3344
  return validateConfig(JSON.parse(raw));
3258
3345
  } catch (err) {
3259
3346
  if (err.code === "ENOENT") {
@@ -4130,7 +4217,7 @@ init_constants();
4130
4217
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
4131
4218
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
4132
4219
  var FETCH_TIMEOUT_MS = 8e3;
4133
- var DEFAULT_CACHE_FILE = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4220
+ var DEFAULT_CACHE_FILE = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4134
4221
  function normalizeModelId(id) {
4135
4222
  let s = id.toLowerCase();
4136
4223
  const slash = s.lastIndexOf("/");
@@ -4163,7 +4250,7 @@ var LiveDataProvider = class {
4163
4250
  if (this.loaded) return;
4164
4251
  this.loaded = true;
4165
4252
  try {
4166
- const raw = await fs9.readFile(this.opts.cacheFile, "utf-8");
4253
+ const raw = await fs10.readFile(this.opts.cacheFile, "utf-8");
4167
4254
  const cache = JSON.parse(raw);
4168
4255
  if (cache.snapshot?.families) {
4169
4256
  this.snapshot = cache.snapshot;
@@ -4275,14 +4362,14 @@ var LiveDataProvider = class {
4275
4362
  }
4276
4363
  async saveCache() {
4277
4364
  try {
4278
- await fs9.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
4365
+ await fs10.mkdir(path26.dirname(this.opts.cacheFile), { recursive: true });
4279
4366
  const cache = {
4280
4367
  fetchedAt: this.fetchedAt,
4281
4368
  snapshot: this.snapshot ?? void 0,
4282
4369
  prices: Object.fromEntries(this.prices),
4283
4370
  capabilities: Object.fromEntries(this.capabilities)
4284
4371
  };
4285
- await fs9.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
4372
+ await fs10.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
4286
4373
  } catch {
4287
4374
  }
4288
4375
  }
@@ -4487,6 +4574,12 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
4487
4574
  const results = await Promise.all(ocConfigs.map((cfg) => this.discoverOpenAICompatibleModels(cfg)));
4488
4575
  if (results.some(Boolean)) this.selector.markProviderAvailable("openai-compatible");
4489
4576
  }
4577
+ if (availableProviders.has("azure")) {
4578
+ for (const cfg of config.providers) {
4579
+ const model = azureModelForDeployment(cfg);
4580
+ if (model) this.selector.addDynamicModel(model);
4581
+ }
4582
+ }
4490
4583
  for (const tier of ["T1", "T2", "T3"]) {
4491
4584
  const override = tier === "T1" ? config.models.t1 : tier === "T2" ? config.models.t2 : config.models.t3;
4492
4585
  if (!override || override === "auto") continue;
@@ -5093,7 +5186,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
5093
5186
  ensureProvider(model, configs) {
5094
5187
  const key = `${model.provider}:${model.id}`;
5095
5188
  if (this.providers.has(key)) return;
5096
- const cfg = configs.find((c) => c.type === model.provider) ?? { type: model.provider };
5189
+ 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 };
5097
5190
  const provider = this.createProvider(cfg, model);
5098
5191
  this.providers.set(key, provider);
5099
5192
  }
@@ -5253,6 +5346,12 @@ var BaseTier = class extends EventEmitter {
5253
5346
  * which would interleave, are not tagged.
5254
5347
  */
5255
5348
  isPresenter = false;
5349
+ /**
5350
+ * The model actually serving this tier (`provider:id`), once resolved —
5351
+ * rides on every tier:status event so the desktop can show which model ran
5352
+ * which node (Cockpit node panel / Why panel).
5353
+ */
5354
+ servingModel;
5256
5355
  constructor(role, id, parentId) {
5257
5356
  super();
5258
5357
  this.role = role;
@@ -5277,11 +5376,16 @@ var BaseTier = class extends EventEmitter {
5277
5376
  label: this.label,
5278
5377
  status,
5279
5378
  timestamp,
5280
- output
5379
+ output,
5380
+ model: this.servingModel
5281
5381
  };
5282
5382
  this.emit("status", event);
5283
5383
  this.emit("tier:status", event);
5284
5384
  }
5385
+ /** Record the model serving this tier; future status events carry it. */
5386
+ setServingModel(model) {
5387
+ this.servingModel = model || void 0;
5388
+ }
5285
5389
  setLabel(label) {
5286
5390
  this.label = label;
5287
5391
  }
@@ -5304,7 +5408,8 @@ var BaseTier = class extends EventEmitter {
5304
5408
  currentAction: update.currentAction,
5305
5409
  progressPct: update.progressPct,
5306
5410
  timestamp,
5307
- output: update.output
5411
+ output: update.output,
5412
+ model: this.servingModel
5308
5413
  });
5309
5414
  }
5310
5415
  buildMessage(type, to, payload) {
@@ -5671,6 +5776,21 @@ Available tools: ${tools.map((t) => t.name).join(", ")}.
5671
5776
  When you have enough information, stop calling tools and write your final answer.`;
5672
5777
  }
5673
5778
 
5779
+ // src/utils/truncate.ts
5780
+ function truncateForContext(text, maxChars = 12e3) {
5781
+ if (text.length <= maxChars) return text;
5782
+ const headLen = Math.floor(maxChars * 0.75);
5783
+ const tailLen = maxChars - headLen;
5784
+ const head = text.slice(0, headLen);
5785
+ const tail = text.slice(-tailLen);
5786
+ const elided = text.length - headLen - tailLen;
5787
+ return `${head}
5788
+
5789
+ [... ${elided.toLocaleString()} characters elided to keep context small \u2014 re-read the file with a line range if you need the middle ...]
5790
+
5791
+ ${tail}`;
5792
+ }
5793
+
5674
5794
  // src/core/tiers/t3-worker.ts
5675
5795
  var CriticalToolError = class extends Error {
5676
5796
  constructor(message, toolName) {
@@ -5978,6 +6098,7 @@ Now execute your subtask using this context where relevant.`
5978
6098
  } catch {
5979
6099
  }
5980
6100
  const effectiveModel = subtaskModel ?? this.router.getModelForTier("T3");
6101
+ if (effectiveModel) this.setServingModel(`${effectiveModel.provider}:${effectiveModel.id}`);
5981
6102
  const useTextTools = effectiveModel?.supportsToolUse === false && tools.length > 0;
5982
6103
  let sentFullTextContract = false;
5983
6104
  let textContractSignature = "";
@@ -6075,7 +6196,7 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6075
6196
  const toolResult = await this.executeTool(tc);
6076
6197
  await this.context.addMessage({
6077
6198
  role: "tool",
6078
- content: toolResult,
6199
+ content: truncateForContext(toolResult),
6079
6200
  toolCallId: tc.id
6080
6201
  });
6081
6202
  }
@@ -6179,8 +6300,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6179
6300
  tierId: this.id,
6180
6301
  sessionId: this.taskId,
6181
6302
  requireApproval: false,
6182
- saveSnapshot: async (path30, content) => {
6183
- this.store?.addFileSnapshot(this.taskId, path30, content);
6303
+ saveSnapshot: async (path31, content) => {
6304
+ this.store?.addFileSnapshot(this.taskId, path31, content);
6184
6305
  },
6185
6306
  sendPeerSync: (to, syncType, content) => {
6186
6307
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -6332,15 +6453,17 @@ ${assignment.expectedOutput}`;
6332
6453
  };
6333
6454
  }
6334
6455
  requiresArtifact() {
6456
+ if (this.assignment?.files?.length) return true;
6335
6457
  const haystack = `${this.assignment?.description ?? ""}
6336
6458
  ${this.assignment?.expectedOutput ?? ""}`;
6337
6459
  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);
6338
6460
  }
6339
6461
  extractArtifactPaths(assignment) {
6462
+ const declared = (assignment.files ?? []).map((f) => f.trim()).filter((f) => f.includes("."));
6340
6463
  const haystack = `${assignment.description}
6341
6464
  ${assignment.expectedOutput}`;
6342
6465
  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) ?? [];
6343
- return [...new Set(matches.map((m) => m.trim()))];
6466
+ return [.../* @__PURE__ */ new Set([...declared, ...matches.map((m) => m.trim())])];
6344
6467
  }
6345
6468
  async verifyArtifacts(assignment) {
6346
6469
  const artifactPaths = this.extractArtifactPaths(assignment);
@@ -6350,9 +6473,9 @@ ${assignment.expectedOutput}`;
6350
6473
  const { promisify: promisify4 } = await import('util');
6351
6474
  const execAsync3 = promisify4(exec3);
6352
6475
  for (const artifactPath of artifactPaths) {
6353
- const absolutePath = path25.resolve(process.cwd(), artifactPath);
6476
+ const absolutePath = path26.resolve(process.cwd(), artifactPath);
6354
6477
  try {
6355
- const stat = await fs9.stat(absolutePath);
6478
+ const stat = await fs10.stat(absolutePath);
6356
6479
  if (!stat.isFile()) {
6357
6480
  issues.push(`Expected artifact is not a file: ${artifactPath}`);
6358
6481
  continue;
@@ -6362,7 +6485,7 @@ ${assignment.expectedOutput}`;
6362
6485
  continue;
6363
6486
  }
6364
6487
  if (!/\.pdf$/i.test(artifactPath)) {
6365
- const content = await fs9.readFile(absolutePath, "utf-8");
6488
+ const content = await fs10.readFile(absolutePath, "utf-8");
6366
6489
  if (!content.trim()) {
6367
6490
  issues.push(`Artifact content is empty: ${artifactPath}`);
6368
6491
  continue;
@@ -6371,7 +6494,7 @@ ${assignment.expectedOutput}`;
6371
6494
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
6372
6495
  continue;
6373
6496
  }
6374
- const ext = path25.extname(absolutePath).toLowerCase();
6497
+ const ext = path26.extname(absolutePath).toLowerCase();
6375
6498
  try {
6376
6499
  if (ext === ".ts" || ext === ".tsx") {
6377
6500
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -6463,7 +6586,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
6463
6586
  Assignment: ${assignment.description}
6464
6587
  Expected output: ${assignment.expectedOutput}
6465
6588
  Constraints: ${assignment.constraints.join("; ")}
6466
-
6589
+ ${assignment.acceptance?.length ? `Acceptance criteria \u2014 ALL must be satisfied for "completeness" to pass:
6590
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6591
+ ` : ""}
6467
6592
  Output to test:
6468
6593
  ${output}
6469
6594
 
@@ -6552,17 +6677,27 @@ Your subtask:
6552
6677
  - Title: ${assignment.subtaskTitle}
6553
6678
  - Description: ${assignment.description}
6554
6679
  - Expected output: ${assignment.expectedOutput}
6555
- - Constraints: ${assignment.constraints.join("; ")}`;
6680
+ - Constraints: ${assignment.constraints.join("; ")}${assignment.files?.length ? `
6681
+ - Files you own (create/edit ONLY these): ${assignment.files.join(", ")}` : ""}${assignment.acceptance?.length ? `
6682
+ - Definition of done: ${assignment.acceptance.join("; ")}` : ""}`;
6556
6683
  }
6557
6684
  buildInitialPrompt(assignment) {
6558
6685
  return `Execute the following subtask completely:
6559
6686
 
6560
6687
  **${assignment.subtaskTitle}**
6561
-
6688
+ ${assignment.contextBrief ? `
6689
+ Context: ${assignment.contextBrief}
6690
+ ` : ""}
6562
6691
  ${assignment.description}
6563
6692
 
6564
6693
  Expected output: ${assignment.expectedOutput}
6565
-
6694
+ ${assignment.files?.length ? `
6695
+ Files you own (create or edit exactly these paths):
6696
+ ${assignment.files.map((f) => `- ${f}`).join("\n")}
6697
+ ` : ""}${assignment.acceptance?.length ? `
6698
+ Definition of done (your output must satisfy ALL of these):
6699
+ ${assignment.acceptance.map((a) => `- ${a}`).join("\n")}
6700
+ ` : ""}
6566
6701
  Constraints:
6567
6702
  ${assignment.constraints.map((c) => `- ${c}`).join("\n")}
6568
6703
 
@@ -7043,6 +7178,8 @@ var T2Manager = class extends BaseTier {
7043
7178
  this.assignment = assignment;
7044
7179
  this.taskId = taskId;
7045
7180
  this.setLabel(assignment.sectionTitle);
7181
+ const m = this.router.getModelForTier("T2");
7182
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7046
7183
  this.setStatus("ACTIVE");
7047
7184
  this.sendStatusUpdate({
7048
7185
  progressPct: 0,
@@ -7129,7 +7266,7 @@ Guidance (must be followed): ${decision.note}`
7129
7266
  // ── Private ──────────────────────────────────
7130
7267
  async decomposeSection(assignment) {
7131
7268
  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");
7132
- const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
7269
+ 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).
7133
7270
 
7134
7271
  Section: ${assignment.sectionTitle}
7135
7272
  Description: ${assignment.description}
@@ -7148,6 +7285,9 @@ Return a JSON array of subtask objects, each with:
7148
7285
  - peerT3Ids: string[] (empty for now)
7149
7286
  - dependsOn: string[] (array of subtaskIds this task depends on to start)
7150
7287
  - executionMode: "parallel|sequential" (default is parallel)
7288
+ - files: string[] (the EXACT relative paths this subtask creates or edits)
7289
+ - acceptance: string[] (1-3 mechanically checkable done-criteria: file exists / contains X / command exits 0)
7290
+ - contextBrief: string (1-3 short sentences with ALL the background the worker needs \u2014 it sees nothing else)
7151
7291
 
7152
7292
  Return ONLY the JSON array.`;
7153
7293
  const messages = [{ role: "user", content: prompt }];
@@ -7748,6 +7888,8 @@ var T1Administrator = class extends BaseTier {
7748
7888
  this.signal = signal;
7749
7889
  this.taskId = randomUUID();
7750
7890
  this.setLabel("Administrator");
7891
+ const m = this.router.getModelForTier("T1");
7892
+ if (m) this.setServingModel(`${m.provider}:${m.id}`);
7751
7893
  this.setStatus("ACTIVE");
7752
7894
  this.taskGoal = userPrompt;
7753
7895
  this.sendStatusUpdate({
@@ -7994,10 +8136,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
7994
8136
  "description": "Run npm init",
7995
8137
  "expectedOutput": "package.json created",
7996
8138
  "constraints": [],
7997
- "dependsOn": []
8139
+ "dependsOn": [],
8140
+ "files": ["package.json"], // \u2190 exact paths this subtask owns
8141
+ "acceptance": ["package.json exists and parses as JSON"], // \u2190 objectively checkable
8142
+ "contextBrief": "Fresh Node 20 project; npm available." // \u2190 ALL the background the worker gets
7998
8143
  }]
7999
8144
  }, {
8000
- "sectionId": "s2",
8145
+ "sectionId": "s2",
8001
8146
  "sectionTitle": "Write Tests",
8002
8147
  "description": "Write tests for the project",
8003
8148
  "expectedOutput": "Tests passing",
@@ -8007,7 +8152,13 @@ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
8007
8152
  }]
8008
8153
  }
8009
8154
  Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
8010
- Leave dependsOn empty for sections that can run immediately in parallel.`;
8155
+ Leave dependsOn empty for sections that can run immediately in parallel.
8156
+
8157
+ SPEC RULES \u2014 each subtask is a self-contained spec slice (workers execute from their slice ALONE):
8158
+ - "files": the exact relative paths the subtask creates or edits. Never vague ("some files"); always concrete.
8159
+ - "acceptance": 1-3 checks a reviewer could verify mechanically (file exists / contains X / command exits 0). These define done.
8160
+ - "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.
8161
+ - 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.`;
8011
8162
  const messages = [{ role: "user", content: decompositionPrompt }];
8012
8163
  const result = await this.router.generate("T1", {
8013
8164
  messages,
@@ -8462,16 +8613,16 @@ function resolveInWorkspace(workspaceRoot, input) {
8462
8613
  if (typeof input !== "string" || input.length === 0) {
8463
8614
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
8464
8615
  }
8465
- const root = path25.resolve(workspaceRoot);
8466
- const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
8467
- const rel = path25.relative(root, abs);
8468
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
8616
+ const root = path26.resolve(workspaceRoot);
8617
+ const abs = path26.isAbsolute(input) ? path26.resolve(input) : path26.resolve(root, input);
8618
+ const rel = path26.relative(root, abs);
8619
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26.isAbsolute(rel)) {
8469
8620
  throw new WorkspaceSandboxError(input, root);
8470
8621
  }
8471
8622
  try {
8472
- const real = fs23.realpathSync(abs);
8473
- const realRel = path25.relative(root, real);
8474
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
8623
+ const real = fs24.realpathSync(abs);
8624
+ const realRel = path26.relative(root, real);
8625
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26.isAbsolute(realRel))) {
8475
8626
  throw new WorkspaceSandboxError(input, root);
8476
8627
  }
8477
8628
  } catch (e) {
@@ -8498,7 +8649,7 @@ var FileReadTool = class extends BaseTool {
8498
8649
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
8499
8650
  const offset = input["offset"] ?? 1;
8500
8651
  const limit = input["limit"];
8501
- const content = await fs9.readFile(absPath, "utf-8");
8652
+ const content = await fs10.readFile(absPath, "utf-8");
8502
8653
  const lines = content.split("\n");
8503
8654
  const start = Math.max(0, offset - 1);
8504
8655
  const end = limit ? start + limit : lines.length;
@@ -8527,13 +8678,13 @@ var FileWriteTool = class extends BaseTool {
8527
8678
  const content = input["content"];
8528
8679
  if (options.saveSnapshot) {
8529
8680
  try {
8530
- const oldContent = await fs9.readFile(absPath, "utf-8");
8681
+ const oldContent = await fs10.readFile(absPath, "utf-8");
8531
8682
  await options.saveSnapshot(absPath, oldContent);
8532
8683
  } catch {
8533
8684
  }
8534
8685
  }
8535
- await fs9.mkdir(path25.dirname(absPath), { recursive: true });
8536
- await fs9.writeFile(absPath, content, "utf-8");
8686
+ await fs10.mkdir(path26.dirname(absPath), { recursive: true });
8687
+ await fs10.writeFile(absPath, content, "utf-8");
8537
8688
  return `Written ${content.length} characters to ${filePath}`;
8538
8689
  }
8539
8690
  };
@@ -8559,7 +8710,7 @@ var FileEditTool = class extends BaseTool {
8559
8710
  const oldString = input["old_string"];
8560
8711
  const newString = input["new_string"];
8561
8712
  const replaceAll = input["replace_all"] ?? false;
8562
- const rawContent = await fs9.readFile(absPath, "utf-8");
8713
+ const rawContent = await fs10.readFile(absPath, "utf-8");
8563
8714
  if (options.saveSnapshot) {
8564
8715
  await options.saveSnapshot(absPath, rawContent);
8565
8716
  }
@@ -8571,7 +8722,7 @@ var FileEditTool = class extends BaseTool {
8571
8722
  );
8572
8723
  }
8573
8724
  const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
8574
- await fs9.writeFile(absPath, updated, "utf-8");
8725
+ await fs10.writeFile(absPath, updated, "utf-8");
8575
8726
  const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
8576
8727
  return `Replaced ${count} occurrence(s) in ${filePath}`;
8577
8728
  }
@@ -8594,12 +8745,12 @@ var FileDeleteTool = class extends BaseTool {
8594
8745
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
8595
8746
  if (options.saveSnapshot) {
8596
8747
  try {
8597
- const oldContent = await fs9.readFile(absPath, "utf-8");
8748
+ const oldContent = await fs10.readFile(absPath, "utf-8");
8598
8749
  await options.saveSnapshot(absPath, oldContent);
8599
8750
  } catch {
8600
8751
  }
8601
8752
  }
8602
- await fs9.rm(absPath, { recursive: false });
8753
+ await fs10.rm(absPath, { recursive: false });
8603
8754
  return `Deleted ${filePath}`;
8604
8755
  }
8605
8756
  };
@@ -8616,7 +8767,7 @@ var FileListTool = class extends BaseTool {
8616
8767
  async execute(input, _options) {
8617
8768
  const inputPath = input["path"] || ".";
8618
8769
  const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
8619
- const entries = await fs9.readdir(absPath, { withFileTypes: true });
8770
+ const entries = await fs10.readdir(absPath, { withFileTypes: true });
8620
8771
  return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
8621
8772
  }
8622
8773
  };
@@ -9029,8 +9180,8 @@ var ImageAnalyzeTool = class extends BaseTool {
9029
9180
  }
9030
9181
  };
9031
9182
  async function fileToImageAttachment(filePath) {
9032
- const data = await fs9.readFile(filePath);
9033
- const ext = path25.extname(filePath).toLowerCase();
9183
+ const data = await fs10.readFile(filePath);
9184
+ const ext = path26.extname(filePath).toLowerCase();
9034
9185
  const mimeMap = {
9035
9186
  ".jpg": "image/jpeg",
9036
9187
  ".jpeg": "image/jpeg",
@@ -9064,14 +9215,14 @@ var PDFCreateTool = class extends BaseTool {
9064
9215
  const filePath = input["path"];
9065
9216
  const content = input["content"];
9066
9217
  const title = input["title"];
9067
- const dir = path25.dirname(filePath);
9068
- if (!fs23.existsSync(dir)) {
9069
- fs23.mkdirSync(dir, { recursive: true });
9218
+ const dir = path26.dirname(filePath);
9219
+ if (!fs24.existsSync(dir)) {
9220
+ fs24.mkdirSync(dir, { recursive: true });
9070
9221
  }
9071
9222
  return new Promise((resolve, reject) => {
9072
9223
  try {
9073
9224
  const doc = new PDFDocument({ margin: 50 });
9074
- const stream = fs23.createWriteStream(filePath);
9225
+ const stream = fs24.createWriteStream(filePath);
9075
9226
  doc.pipe(stream);
9076
9227
  if (title) {
9077
9228
  doc.info["Title"] = title;
@@ -9149,22 +9300,22 @@ var CodeInterpreterTool = class extends BaseTool {
9149
9300
  }
9150
9301
  cmdPrefix = NODE_CMD;
9151
9302
  }
9152
- const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
9153
- if (!fs23.existsSync(tmpDir)) {
9154
- fs23.mkdirSync(tmpDir, { recursive: true });
9303
+ const tmpDir = path26.join(this.workspaceRoot, ".cascade", "tmp");
9304
+ if (!fs24.existsSync(tmpDir)) {
9305
+ fs24.mkdirSync(tmpDir, { recursive: true });
9155
9306
  }
9156
9307
  const extension = language === "python" ? "py" : "js";
9157
9308
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
9158
- const filePath = path25.join(tmpDir, fileName);
9159
- fs23.writeFileSync(filePath, code, "utf-8");
9309
+ const filePath = path26.join(tmpDir, fileName);
9310
+ fs24.writeFileSync(filePath, code, "utf-8");
9160
9311
  const execArgs = [filePath, ...args];
9161
9312
  return new Promise((resolve) => {
9162
9313
  const startMs = Date.now();
9163
9314
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
9164
9315
  const duration = Date.now() - startMs;
9165
9316
  try {
9166
- if (fs23.existsSync(filePath)) {
9167
- fs23.unlinkSync(filePath);
9317
+ if (fs24.existsSync(filePath)) {
9318
+ fs24.unlinkSync(filePath);
9168
9319
  }
9169
9320
  } catch (cleanupErr) {
9170
9321
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -9315,30 +9466,56 @@ async function searchTavily(query, apiKey, maxResults) {
9315
9466
  engine: "tavily"
9316
9467
  }));
9317
9468
  }
9318
- async function searchDuckDuckGoLite(query, maxResults) {
9319
- const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
9320
- headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
9321
- signal: AbortSignal.timeout(1e4)
9322
- });
9323
- if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
9324
- const html = await resp.text();
9325
- const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
9326
- const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
9327
- const links = [];
9328
- const snippets = [];
9469
+ var BROWSER_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
9470
+ function unwrapDdgRedirect(href) {
9471
+ try {
9472
+ const url = new URL(href.startsWith("//") ? `https:${href}` : href, "https://duckduckgo.com");
9473
+ if (/(^|\.)duckduckgo\.com$/i.test(url.hostname) && url.pathname.startsWith("/l/")) {
9474
+ const target = url.searchParams.get("uddg");
9475
+ if (target) return decodeURIComponent(target);
9476
+ }
9477
+ return url.protocol === "http:" || url.protocol === "https:" ? url.toString() : href;
9478
+ } catch {
9479
+ return href;
9480
+ }
9481
+ }
9482
+ function stripTags(html) {
9483
+ return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
9484
+ }
9485
+ function decodeEntities(text) {
9486
+ return text.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;|&#39;/g, "'").replace(/&nbsp;/g, " ");
9487
+ }
9488
+ function parseDdgAnchors(html, anchorClass, snippetClass) {
9489
+ const anchorRe = new RegExp(`<a\\b[^>]*class=["']?[^"'>]*\\b${anchorClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/a>`, "gi");
9490
+ const snippetRe = new RegExp(`class=["']?[^"'>]*\\b${snippetClass}\\b[^"'>]*["']?[^>]*>([\\s\\S]*?)<\\/(?:td|a|div|span)>`, "gi");
9491
+ const results = [];
9329
9492
  let m;
9330
- while ((m = linkPattern.exec(html)) !== null) {
9331
- links.push({ url: m[1], title: m[2].trim() });
9493
+ while ((m = anchorRe.exec(html)) !== null) {
9494
+ const tag = m[0];
9495
+ const href = /href=["']([^"']+)["']/i.exec(tag)?.[1];
9496
+ const title = decodeEntities(stripTags(m[1] ?? ""));
9497
+ if (!href || !title) continue;
9498
+ results.push({ title, url: unwrapDdgRedirect(decodeEntities(href)), snippet: "" });
9332
9499
  }
9333
- while ((m = snippetPattern.exec(html)) !== null) {
9334
- snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
9500
+ const snippets = [];
9501
+ while ((m = snippetRe.exec(html)) !== null) {
9502
+ snippets.push(decodeEntities(stripTags(m[1] ?? "")));
9335
9503
  }
9336
- return links.slice(0, maxResults).map((link, i) => ({
9337
- title: link.title,
9338
- url: link.url,
9339
- snippet: snippets[i] ?? "",
9340
- engine: "duckduckgo-lite"
9341
- }));
9504
+ for (let i = 0; i < results.length; i++) {
9505
+ if (snippets[i]) results[i].snippet = snippets[i];
9506
+ }
9507
+ return results;
9508
+ }
9509
+ async function searchDuckDuckGo(query, maxResults, variant) {
9510
+ const base = variant === "html" ? "https://html.duckduckgo.com/html/?q=" : "https://lite.duckduckgo.com/lite/?q=";
9511
+ const resp = await fetch(`${base}${encodeURIComponent(query)}`, {
9512
+ headers: { "User-Agent": BROWSER_UA, Accept: "text/html" },
9513
+ signal: AbortSignal.timeout(1e4)
9514
+ });
9515
+ if (!resp.ok) throw new Error(`DuckDuckGo ${variant} returned HTTP ${resp.status}`);
9516
+ const html = await resp.text();
9517
+ const parsed = variant === "html" ? parseDdgAnchors(html, "result__a", "result__snippet") : parseDdgAnchors(html, "result-link", "result-snippet");
9518
+ return parsed.slice(0, maxResults).map((r) => ({ ...r, engine: `duckduckgo-${variant}` }));
9342
9519
  }
9343
9520
  var WebSearchTool = class extends BaseTool {
9344
9521
  name = "web_search";
@@ -9397,12 +9574,14 @@ var WebSearchTool = class extends BaseTool {
9397
9574
  errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
9398
9575
  }
9399
9576
  }
9400
- try {
9401
- results = await searchDuckDuckGoLite(query, maxResults);
9402
- if (results.length > 0) return this.formatResults(query, results);
9403
- errors.push("DuckDuckGo Lite: returned 0 results");
9404
- } catch (err) {
9405
- errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
9577
+ for (const variant of ["html", "lite"]) {
9578
+ try {
9579
+ results = await searchDuckDuckGo(query, maxResults, variant);
9580
+ if (results.length > 0) return this.formatResults(query, results);
9581
+ errors.push(`DuckDuckGo ${variant}: returned 0 results`);
9582
+ } catch (err) {
9583
+ errors.push(`DuckDuckGo ${variant}: ${err instanceof Error ? err.message : String(err)}`);
9584
+ }
9406
9585
  }
9407
9586
  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" : "";
9408
9587
  return [
@@ -9443,7 +9622,7 @@ var GlobTool = class extends BaseTool {
9443
9622
  };
9444
9623
  async execute(input, _options) {
9445
9624
  const pattern = input["pattern"];
9446
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9625
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9447
9626
  const matches = await glob(pattern, {
9448
9627
  cwd: searchPath,
9449
9628
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -9456,7 +9635,7 @@ var GlobTool = class extends BaseTool {
9456
9635
  const withMtime = await Promise.all(
9457
9636
  matches.map(async (rel) => {
9458
9637
  try {
9459
- const stat = await fs9.stat(path25.join(searchPath, rel));
9638
+ const stat = await fs10.stat(path26.join(searchPath, rel));
9460
9639
  return { rel, mtime: stat.mtimeMs };
9461
9640
  } catch {
9462
9641
  return { rel, mtime: 0 };
@@ -9505,7 +9684,7 @@ var GrepTool = class extends BaseTool {
9505
9684
  };
9506
9685
  async execute(input, _options) {
9507
9686
  const pattern = input["pattern"];
9508
- const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9687
+ const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9509
9688
  const globPattern = input["glob"];
9510
9689
  const outputMode = input["output_mode"] ?? "content";
9511
9690
  const context = input["context"] ?? 0;
@@ -9559,15 +9738,15 @@ var GrepTool = class extends BaseTool {
9559
9738
  nodir: true
9560
9739
  });
9561
9740
  } catch {
9562
- files = [path25.relative(searchPath, searchPath) || "."];
9741
+ files = [path26.relative(searchPath, searchPath) || "."];
9563
9742
  }
9564
9743
  const results = [];
9565
9744
  let totalCount = 0;
9566
9745
  for (const rel of files) {
9567
- const abs = path25.join(searchPath, rel);
9746
+ const abs = path26.join(searchPath, rel);
9568
9747
  let content;
9569
9748
  try {
9570
- content = await fs9.readFile(abs, "utf-8");
9749
+ content = await fs10.readFile(abs, "utf-8");
9571
9750
  } catch {
9572
9751
  continue;
9573
9752
  }
@@ -9926,10 +10105,10 @@ var ToolRegistry = class extends EventEmitter {
9926
10105
  }
9927
10106
  isIgnored(filePath) {
9928
10107
  if (!filePath) return false;
9929
- const abs = path25.resolve(this.workspaceRoot, filePath);
9930
- const rel = path25.relative(this.workspaceRoot, abs);
9931
- if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
9932
- const posixRel = rel.split(path25.sep).join("/");
10108
+ const abs = path26.resolve(this.workspaceRoot, filePath);
10109
+ const rel = path26.relative(this.workspaceRoot, abs);
10110
+ if (!rel || rel.startsWith("..") || path26.isAbsolute(rel)) return true;
10111
+ const posixRel = rel.split(path26.sep).join("/");
9933
10112
  return this.ignoreMatcher.ignores(posixRel);
9934
10113
  }
9935
10114
  };
@@ -10452,7 +10631,7 @@ var TaskAnalyzer = class {
10452
10631
  analysisCache.clear();
10453
10632
  }
10454
10633
  };
10455
- var DEFAULT_STATS_FILE = path25.join(os8.homedir(), ".cascade", "model-perf.json");
10634
+ var DEFAULT_STATS_FILE = path26.join(os8.homedir(), ".cascade", "model-perf.json");
10456
10635
  var ModelPerformanceTracker = class {
10457
10636
  stats = /* @__PURE__ */ new Map();
10458
10637
  featureStats = /* @__PURE__ */ new Map();
@@ -10465,7 +10644,7 @@ var ModelPerformanceTracker = class {
10465
10644
  if (this.loaded) return;
10466
10645
  this.loaded = true;
10467
10646
  try {
10468
- const raw = await fs9.readFile(this.statsFile, "utf-8");
10647
+ const raw = await fs10.readFile(this.statsFile, "utf-8");
10469
10648
  const parsed = JSON.parse(raw);
10470
10649
  if (parsed.models) {
10471
10650
  for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
@@ -10484,12 +10663,12 @@ var ModelPerformanceTracker = class {
10484
10663
  }
10485
10664
  async save() {
10486
10665
  try {
10487
- await fs9.mkdir(path25.dirname(this.statsFile), { recursive: true });
10666
+ await fs10.mkdir(path26.dirname(this.statsFile), { recursive: true });
10488
10667
  const modelsObj = {};
10489
10668
  const featuresObj = {};
10490
10669
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
10491
10670
  for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
10492
- await fs9.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
10671
+ await fs10.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
10493
10672
  } catch {
10494
10673
  }
10495
10674
  }
@@ -11023,9 +11202,9 @@ Required capability: ${description.slice(0, 300)}`;
11023
11202
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
11024
11203
  async loadPersistedTools() {
11025
11204
  if (!this.workspacePath || !this.persistEnabled) return;
11026
- const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
11205
+ const file = path26.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
11027
11206
  try {
11028
- const raw = await fs9.readFile(file, "utf-8");
11207
+ const raw = await fs10.readFile(file, "utf-8");
11029
11208
  const specs = JSON.parse(raw);
11030
11209
  if (!Array.isArray(specs)) return;
11031
11210
  let loaded = 0;
@@ -11047,11 +11226,11 @@ Required capability: ${description.slice(0, 300)}`;
11047
11226
  }
11048
11227
  async persist() {
11049
11228
  if (!this.workspacePath || !this.persistEnabled) return;
11050
- const dir = path25.join(this.workspacePath, ".cascade");
11051
- const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
11229
+ const dir = path26.join(this.workspacePath, ".cascade");
11230
+ const file = path26.join(dir, DYNAMIC_TOOLS_FILE);
11052
11231
  try {
11053
- await fs9.mkdir(dir, { recursive: true });
11054
- await fs9.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
11232
+ await fs10.mkdir(dir, { recursive: true });
11233
+ await fs10.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
11055
11234
  } catch (err) {
11056
11235
  this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
11057
11236
  }
@@ -11068,12 +11247,12 @@ var WorldStateDB = class {
11068
11247
  constructor(workspacePath, debugMode = false) {
11069
11248
  this.workspacePath = workspacePath;
11070
11249
  this.debugMode = debugMode;
11071
- const cascadeDir = path25.join(workspacePath, ".cascade");
11072
- if (!fs23.existsSync(cascadeDir)) {
11073
- fs23.mkdirSync(cascadeDir, { recursive: true });
11250
+ const cascadeDir = path26.join(workspacePath, ".cascade");
11251
+ if (!fs24.existsSync(cascadeDir)) {
11252
+ fs24.mkdirSync(cascadeDir, { recursive: true });
11074
11253
  }
11075
- this.keyPath = path25.join(cascadeDir, "world_state.key");
11076
- this.dbPath = path25.join(cascadeDir, "world_state.db");
11254
+ this.keyPath = path26.join(cascadeDir, "world_state.key");
11255
+ this.dbPath = path26.join(cascadeDir, "world_state.db");
11077
11256
  this.initEncryptionKey();
11078
11257
  this.db = new Database2(this.dbPath);
11079
11258
  this.db.pragma("journal_mode = WAL");
@@ -11103,11 +11282,11 @@ var WorldStateDB = class {
11103
11282
  dbPath;
11104
11283
  encryptionKey;
11105
11284
  initEncryptionKey() {
11106
- if (fs23.existsSync(this.keyPath)) {
11107
- this.encryptionKey = fs23.readFileSync(this.keyPath);
11285
+ if (fs24.existsSync(this.keyPath)) {
11286
+ this.encryptionKey = fs24.readFileSync(this.keyPath);
11108
11287
  } else {
11109
11288
  this.encryptionKey = crypto.randomBytes(32);
11110
- fs23.writeFileSync(this.keyPath, this.encryptionKey);
11289
+ fs24.writeFileSync(this.keyPath, this.encryptionKey);
11111
11290
  }
11112
11291
  }
11113
11292
  encrypt(text) {
@@ -11248,6 +11427,22 @@ var WorldStateDB = class {
11248
11427
  }
11249
11428
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
11250
11429
  }
11430
+ /**
11431
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
11432
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
11433
+ * delete — users can prune what the planner remembers about their project.
11434
+ */
11435
+ deleteFact(entity, relation) {
11436
+ const e = normalizeKey(entity);
11437
+ const r = normalizeKey(relation);
11438
+ if (!e || !r) return false;
11439
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
11440
+ return result.changes > 0;
11441
+ }
11442
+ /** Delete every fact. Returns how many were removed. */
11443
+ clearFacts() {
11444
+ return this.db.prepare("DELETE FROM facts").run().changes;
11445
+ }
11251
11446
  // ── Export / Import ──────────────────────────
11252
11447
  //
11253
11448
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -11296,9 +11491,9 @@ var WorldStateDB = class {
11296
11491
  dumpDebugIfNeeded() {
11297
11492
  if (!this.debugMode) return;
11298
11493
  try {
11299
- const dumpPath = path25.join(os8.tmpdir(), "cascade_world_state_debug.json");
11494
+ const dumpPath = path26.join(os8.tmpdir(), "cascade_world_state_debug.json");
11300
11495
  const entries = this.getAllEntries();
11301
- fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
11496
+ fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
11302
11497
  } catch (err) {
11303
11498
  console.error("Failed to dump debug world state", err);
11304
11499
  }
@@ -11733,13 +11928,30 @@ ${last.partialOutput}` : "");
11733
11928
  * explicit multi-part structure, so ordinary single-file asks (handled as
11734
11929
  * Simple/Moderate) don't get over-escalated.
11735
11930
  */
11736
- looksClearlyComplex(prompt) {
11931
+ /** Shared build/scale signals for the complexity floors below. */
11932
+ buildSignals(prompt) {
11737
11933
  const p = prompt.trim();
11738
- if (p.length < 24) return false;
11934
+ if (p.length < 24) return { buildVerb: false, scaleCount: 0, multiPart: false };
11739
11935
  const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
11740
- 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);
11936
+ 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;
11741
11937
  const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
11742
- return buildVerb && (scaleNoun || multiPart);
11938
+ return { buildVerb, scaleCount, multiPart };
11939
+ }
11940
+ /**
11941
+ * A build prompt with REAL scale: multiple system-level deliverables, or a
11942
+ * deliverable plus explicitly multi-part phrasing. Only these floor to the
11943
+ * full T1→T2→T3 hierarchy — "create a todo app" is a build prompt too, but
11944
+ * flooring every small build to Complex was the #1 token bomb (3-5 managers
11945
+ * × workers for a task one worker handles).
11946
+ */
11947
+ looksClearlyComplex(prompt) {
11948
+ const s = this.buildSignals(prompt);
11949
+ return s.buildVerb && (s.scaleCount >= 2 || s.scaleCount >= 1 && s.multiPart);
11950
+ }
11951
+ /** A small single-deliverable build — real work, but one manager's worth. */
11952
+ looksLikeModerateBuild(prompt) {
11953
+ const s = this.buildSignals(prompt);
11954
+ return s.buildVerb && (s.scaleCount >= 1 || s.multiPart);
11743
11955
  }
11744
11956
  // Cache glob scan results per workspace path to avoid repeated I/O.
11745
11957
  static globCache = /* @__PURE__ */ new Map();
@@ -11827,6 +12039,9 @@ ${prompt}` : prompt;
11827
12039
  if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
11828
12040
  this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
11829
12041
  verdict = "Complex";
12042
+ } else if (verdict === "Simple" && this.looksLikeModerateBuild(prompt)) {
12043
+ this.recordDecision("complexity", 'Moderate \u2014 heuristic floor over classifier "Simple": build signals without multi-system scale (single manager)');
12044
+ verdict = "Moderate";
11830
12045
  } else {
11831
12046
  this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
11832
12047
  }
@@ -13716,10 +13931,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13716
13931
  }
13717
13932
  } catch {
13718
13933
  }
13719
- const configPath = path25.join(workspacePath, ".cascade", "config.json");
13934
+ const configPath = path26.join(workspacePath, ".cascade", "config.json");
13720
13935
  try {
13721
- await fs9.mkdir(path25.dirname(configPath), { recursive: true });
13722
- await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13936
+ await fs10.mkdir(path26.dirname(configPath), { recursive: true });
13937
+ await fs10.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13723
13938
  } catch (err) {
13724
13939
  const msg = err instanceof Error ? err.message : String(err);
13725
13940
  dispatch({
@@ -13770,9 +13985,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13770
13985
  if (msg.includes("non-text parts")) return;
13771
13986
  originalLog(...args);
13772
13987
  };
13773
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
13988
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
13774
13989
  storeRef.current = store;
13775
- globalStoreRef.current = new MemoryStore(path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13990
+ globalStoreRef.current = new MemoryStore(path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13776
13991
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
13777
13992
  setIdentities(identityRows);
13778
13993
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -13835,14 +14050,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13835
14050
  onThemeChange: (name) => setTheme(getTheme(name)),
13836
14051
  onExport: async (fmt) => {
13837
14052
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13838
- const exportPath = path25.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
14053
+ const exportPath = path26.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13839
14054
  if (fmt === "json") {
13840
- await fs9.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
14055
+ await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
13841
14056
  } else {
13842
14057
  const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
13843
14058
 
13844
14059
  ${msg.content}`).join("\n\n");
13845
- await fs9.writeFile(exportPath, markdown, "utf-8");
14060
+ await fs10.writeFile(exportPath, markdown, "utf-8");
13846
14061
  }
13847
14062
  },
13848
14063
  onRollback: async () => {
@@ -13852,7 +14067,7 @@ ${msg.content}`).join("\n\n");
13852
14067
  if (!snapshots.length) return "No file snapshots found for this session.";
13853
14068
  for (const { filePath, content } of snapshots) {
13854
14069
  try {
13855
- await fs9.writeFile(filePath, content, "utf-8");
14070
+ await fs10.writeFile(filePath, content, "utf-8");
13856
14071
  } catch (err) {
13857
14072
  console.error(`Restore failed: ${filePath}`, err);
13858
14073
  }
@@ -14731,9 +14946,9 @@ function stringifySlashOutput(val) {
14731
14946
  }
14732
14947
  async function searchSessionsAndMessages(query, workspacePath) {
14733
14948
  if (!query) return "Usage: /search <query>";
14734
- const dbPath = path25.join(workspacePath, CASCADE_DB_FILE);
14949
+ const dbPath = path26.join(workspacePath, CASCADE_DB_FILE);
14735
14950
  try {
14736
- await fs9.access(dbPath);
14951
+ await fs10.access(dbPath);
14737
14952
  } catch {
14738
14953
  return "No database found. Start a conversation first.";
14739
14954
  }
@@ -14765,7 +14980,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
14765
14980
  async function diagnoseRuntime(config, workspacePath) {
14766
14981
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
14767
14982
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
14768
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
14983
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
14769
14984
  try {
14770
14985
  const sessions = store.listSessions(void 0, 3);
14771
14986
  return [
@@ -14784,7 +14999,7 @@ async function diagnoseRuntime(config, workspacePath) {
14784
14999
  }
14785
15000
  async function showRecentLogs(args, workspacePath) {
14786
15001
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
14787
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15002
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
14788
15003
  try {
14789
15004
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
14790
15005
  if (!logs.length) return "No recent runtime logs.";
@@ -14796,7 +15011,7 @@ async function showRecentLogs(args, workspacePath) {
14796
15011
  async function loadSessionSnapshot(args, workspacePath) {
14797
15012
  const sessionId = args[0];
14798
15013
  if (!sessionId) return "Usage: /resume <sessionId>";
14799
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
15014
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
14800
15015
  try {
14801
15016
  const session = store.getSession(sessionId);
14802
15017
  if (!session) return `Session not found: ${sessionId}`;
@@ -15216,10 +15431,10 @@ function SetupWizard({ workspacePath, onComplete }) {
15216
15431
  ...anyAuto ? { cascadeAuto: true } : {}
15217
15432
  };
15218
15433
  const config = CascadeConfigSchema.parse(rawConfig);
15219
- const configDir = path25.join(workspacePath, ".cascade");
15220
- await fs9.mkdir(configDir, { recursive: true });
15221
- const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
15222
- await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
15434
+ const configDir = path26.join(workspacePath, ".cascade");
15435
+ await fs10.mkdir(configDir, { recursive: true });
15436
+ const configPath = path26.join(workspacePath, CASCADE_CONFIG_FILE);
15437
+ await fs10.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
15223
15438
  savedConfigRef.current = config;
15224
15439
  dispatchRef.current({ type: "GO_DONE" });
15225
15440
  } catch (err) {
@@ -15559,14 +15774,14 @@ function printTelemetryBanner() {
15559
15774
  async function initCommand(workspacePath = process.cwd()) {
15560
15775
  const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
15561
15776
  try {
15562
- const configDir = path25.join(workspacePath, ".cascade");
15563
- await fs9.mkdir(configDir, { recursive: true });
15564
- const mdPath = path25.join(workspacePath, "CASCADE.md");
15777
+ const configDir = path26.join(workspacePath, ".cascade");
15778
+ await fs10.mkdir(configDir, { recursive: true });
15779
+ const mdPath = path26.join(workspacePath, "CASCADE.md");
15565
15780
  if (!await fileExists(mdPath)) {
15566
15781
  await createDefaultCascadeMd(workspacePath);
15567
15782
  spin.succeed(chalk9.green("Created CASCADE.md"));
15568
15783
  }
15569
- const ignorePath = path25.join(workspacePath, ".cascadeignore");
15784
+ const ignorePath = path26.join(workspacePath, ".cascadeignore");
15570
15785
  if (!await fileExists(ignorePath)) {
15571
15786
  await createDefaultIgnoreFile(workspacePath);
15572
15787
  spin.succeed(chalk9.green("Created .cascadeignore"));
@@ -15575,7 +15790,7 @@ async function initCommand(workspacePath = process.cwd()) {
15575
15790
  console.log();
15576
15791
  console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
15577
15792
  console.log();
15578
- const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
15793
+ const configPath = path26.join(workspacePath, CASCADE_CONFIG_FILE);
15579
15794
  if (await fileExists(configPath)) {
15580
15795
  console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
15581
15796
  console.log();
@@ -15594,7 +15809,7 @@ async function initCommand(workspacePath = process.cwd()) {
15594
15809
  }
15595
15810
  async function fileExists(p) {
15596
15811
  try {
15597
- await fs9.access(p);
15812
+ await fs10.access(p);
15598
15813
  return true;
15599
15814
  } catch {
15600
15815
  return false;
@@ -15606,7 +15821,7 @@ init_constants();
15606
15821
  var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
15607
15822
  async function readJson(file) {
15608
15823
  try {
15609
- const raw = await fs9.readFile(file, "utf-8");
15824
+ const raw = await fs10.readFile(file, "utf-8");
15610
15825
  return JSON.parse(raw);
15611
15826
  } catch {
15612
15827
  return null;
@@ -15633,7 +15848,7 @@ function fromEnv(env) {
15633
15848
  return out;
15634
15849
  }
15635
15850
  async function fromClaudeCode(home) {
15636
- const file = path25.join(home, ".claude", ".credentials.json");
15851
+ const file = path26.join(home, ".claude", ".credentials.json");
15637
15852
  const data = await readJson(file);
15638
15853
  if (!data) return [];
15639
15854
  const oauth = data["claudeAiOauth"];
@@ -15656,7 +15871,7 @@ async function fromClaudeCode(home) {
15656
15871
  return [];
15657
15872
  }
15658
15873
  async function fromCodex(home) {
15659
- const file = path25.join(home, ".codex", "auth.json");
15874
+ const file = path26.join(home, ".codex", "auth.json");
15660
15875
  const data = await readJson(file);
15661
15876
  if (!data) return [];
15662
15877
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -15679,7 +15894,7 @@ async function fromCodex(home) {
15679
15894
  return [];
15680
15895
  }
15681
15896
  async function fromGemini(home) {
15682
- const file = path25.join(home, ".gemini", "oauth_creds.json");
15897
+ const file = path26.join(home, ".gemini", "oauth_creds.json");
15683
15898
  const data = await readJson(file);
15684
15899
  const accessToken = str(data?.["access_token"]);
15685
15900
  if (!accessToken) return [];
@@ -15695,7 +15910,7 @@ async function fromGemini(home) {
15695
15910
  }
15696
15911
  async function fromCopilot(home) {
15697
15912
  for (const name of ["apps.json", "hosts.json"]) {
15698
- const file = path25.join(home, ".config", "github-copilot", name);
15913
+ const file = path26.join(home, ".config", "github-copilot", name);
15699
15914
  const data = await readJson(file);
15700
15915
  if (!data) continue;
15701
15916
  for (const value of Object.values(data)) {
@@ -15749,7 +15964,7 @@ async function doctorCommand() {
15749
15964
  checks.push({
15750
15965
  label: "Cascade config",
15751
15966
  ok: true,
15752
- detail: `Loaded ${path25.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15967
+ detail: `Loaded ${path26.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15753
15968
  });
15754
15969
  const providers = [
15755
15970
  { type: "anthropic", name: "Anthropic" },
@@ -15905,6 +16120,11 @@ function authMiddleware(secret, required = true) {
15905
16120
  }
15906
16121
  const user = verifyToken(token, secret);
15907
16122
  if (!user) {
16123
+ if (!required) {
16124
+ req.user = void 0;
16125
+ next();
16126
+ return;
16127
+ }
15908
16128
  res.status(401).json({ error: "Invalid or expired token" });
15909
16129
  return;
15910
16130
  }
@@ -16216,7 +16436,7 @@ function aggregateCostStats(sessions, opts = {}) {
16216
16436
  }
16217
16437
 
16218
16438
  // src/dashboard/server.ts
16219
- var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
16439
+ var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
16220
16440
  var DashboardServer = class {
16221
16441
  app;
16222
16442
  httpServer;
@@ -16450,12 +16670,17 @@ var DashboardServer = class {
16450
16670
  */
16451
16671
  persistConfig() {
16452
16672
  try {
16453
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
16454
- fs23.mkdirSync(path25.dirname(configPath), { recursive: true });
16455
- fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
16673
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
16674
+ fs24.mkdirSync(path26.dirname(configPath), { recursive: true });
16675
+ fs24.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
16456
16676
  } catch (err) {
16457
16677
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
16458
16678
  }
16679
+ try {
16680
+ saveGlobalCredentials(path26.join(os8.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
16681
+ } catch (err) {
16682
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
16683
+ }
16459
16684
  }
16460
16685
  /**
16461
16686
  * Produce a stable dashboard JWT signing secret.
@@ -16467,15 +16692,15 @@ var DashboardServer = class {
16467
16692
  resolveDashboardSecret() {
16468
16693
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
16469
16694
  if (fromConfig) return fromConfig;
16470
- const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
16695
+ const secretPath = path26.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
16471
16696
  try {
16472
- if (fs23.existsSync(secretPath)) {
16473
- const existing = fs23.readFileSync(secretPath, "utf-8").trim();
16697
+ if (fs24.existsSync(secretPath)) {
16698
+ const existing = fs24.readFileSync(secretPath, "utf-8").trim();
16474
16699
  if (existing.length >= 16) return existing;
16475
16700
  }
16476
16701
  const generated = randomUUID();
16477
- fs23.mkdirSync(path25.dirname(secretPath), { recursive: true });
16478
- fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
16702
+ fs24.mkdirSync(path26.dirname(secretPath), { recursive: true });
16703
+ fs24.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
16479
16704
  if (this.config.dashboard.auth) {
16480
16705
  console.warn(
16481
16706
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -16502,7 +16727,7 @@ var DashboardServer = class {
16502
16727
  // ── Setup ─────────────────────────────────────
16503
16728
  getGlobalStore() {
16504
16729
  if (!this.globalStore) {
16505
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16730
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16506
16731
  this.globalStore = new MemoryStore(globalDbPath);
16507
16732
  }
16508
16733
  return this.globalStore;
@@ -16771,12 +16996,12 @@ ${prompt}`;
16771
16996
  }
16772
16997
  }
16773
16998
  watchRuntimeChanges() {
16774
- const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
16775
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16999
+ const workspaceDbPath = path26.join(this.workspacePath, CASCADE_DB_FILE);
17000
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16776
17001
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
16777
17002
  for (const watchPath of watchPaths) {
16778
- if (!fs23.existsSync(watchPath)) continue;
16779
- fs23.watchFile(watchPath, { interval: 3e3 }, () => {
17003
+ if (!fs24.existsSync(watchPath)) continue;
17004
+ fs24.watchFile(watchPath, { interval: 3e3 }, () => {
16780
17005
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
16781
17006
  });
16782
17007
  }
@@ -16898,7 +17123,7 @@ ${prompt}`;
16898
17123
  const sessionId = req.params.id;
16899
17124
  this.store.deleteSession(sessionId);
16900
17125
  this.store.deleteRuntimeSession(sessionId);
16901
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17126
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16902
17127
  const globalStore = new MemoryStore(globalDbPath);
16903
17128
  try {
16904
17129
  globalStore.deleteRuntimeSession(sessionId);
@@ -16991,7 +17216,7 @@ ${prompt}`;
16991
17216
  });
16992
17217
  this.app.delete("/api/sessions", auth, (req, res) => {
16993
17218
  const body = req.body;
16994
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17219
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16995
17220
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
16996
17221
  const globalStore = new MemoryStore(globalDbPath);
16997
17222
  try {
@@ -17014,7 +17239,7 @@ ${prompt}`;
17014
17239
  });
17015
17240
  this.app.delete("/api/runtime", auth, (_req, res) => {
17016
17241
  this.store.deleteAllRuntimeNodes();
17017
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17242
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17018
17243
  const globalStore = new MemoryStore(globalDbPath);
17019
17244
  try {
17020
17245
  globalStore.deleteAllRuntimeNodes();
@@ -17100,12 +17325,12 @@ ${prompt}`;
17100
17325
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
17101
17326
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
17102
17327
  try {
17103
- const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
17104
- const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
17328
+ const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
17329
+ const existing = fs24.existsSync(configPath) ? JSON.parse(fs24.readFileSync(configPath, "utf-8")) : {};
17105
17330
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
17106
17331
  const tmp = configPath + ".tmp";
17107
- fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
17108
- fs23.renameSync(tmp, configPath);
17332
+ fs24.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
17333
+ fs24.renameSync(tmp, configPath);
17109
17334
  res.json({ ok: true });
17110
17335
  } catch (err) {
17111
17336
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -17133,7 +17358,7 @@ ${prompt}`;
17133
17358
  this.app.get("/api/runtime", auth, (req, res) => {
17134
17359
  const scope = req.query["scope"] ?? "workspace";
17135
17360
  if (scope === "global") {
17136
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17361
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17137
17362
  const globalStore = new MemoryStore(globalDbPath);
17138
17363
  try {
17139
17364
  res.json({
@@ -17368,6 +17593,48 @@ ${prompt}`;
17368
17593
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17369
17594
  }
17370
17595
  });
17596
+ this.app.get("/api/knowledge", auth, (_req, res) => {
17597
+ try {
17598
+ const ws = new WorldStateDB(this.workspacePath);
17599
+ try {
17600
+ const facts = ws.getAllFacts();
17601
+ res.json({ total: facts.length, facts });
17602
+ } finally {
17603
+ ws.close();
17604
+ }
17605
+ } catch (err) {
17606
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17607
+ }
17608
+ });
17609
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
17610
+ const body = req.body;
17611
+ if (!body.entity || !body.relation) {
17612
+ res.status(400).json({ error: "entity and relation are required" });
17613
+ return;
17614
+ }
17615
+ try {
17616
+ const ws = new WorldStateDB(this.workspacePath);
17617
+ try {
17618
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
17619
+ } finally {
17620
+ ws.close();
17621
+ }
17622
+ } catch (err) {
17623
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17624
+ }
17625
+ });
17626
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
17627
+ try {
17628
+ const ws = new WorldStateDB(this.workspacePath);
17629
+ try {
17630
+ res.json({ ok: true, deleted: ws.clearFacts() });
17631
+ } finally {
17632
+ ws.close();
17633
+ }
17634
+ } catch (err) {
17635
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17636
+ }
17637
+ });
17371
17638
  this.app.get("/api/audit-chain", auth, async (req, res) => {
17372
17639
  try {
17373
17640
  const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
@@ -17386,13 +17653,13 @@ ${prompt}`;
17386
17653
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17387
17654
  }
17388
17655
  });
17389
- const prodPath = path25.resolve(__dirname$1, "../web/dist");
17390
- const devPath = path25.resolve(__dirname$1, "../../web/dist");
17391
- const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
17392
- if (fs23.existsSync(webDistPath)) {
17656
+ const prodPath = path26.resolve(__dirname$1, "../web/dist");
17657
+ const devPath = path26.resolve(__dirname$1, "../../web/dist");
17658
+ const webDistPath = fs24.existsSync(prodPath) ? prodPath : devPath;
17659
+ if (fs24.existsSync(webDistPath)) {
17393
17660
  this.app.use(express.static(webDistPath));
17394
17661
  this.app.get("*", (_req, res) => {
17395
- res.sendFile(path25.join(webDistPath, "index.html"));
17662
+ res.sendFile(path26.join(webDistPath, "index.html"));
17396
17663
  });
17397
17664
  } else {
17398
17665
  this.app.get("/", (_req, res) => {
@@ -17413,7 +17680,7 @@ init_constants();
17413
17680
  async function dashboardCommand(config, workspacePath = process.cwd()) {
17414
17681
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
17415
17682
  const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
17416
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
17683
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
17417
17684
  const server = new DashboardServer(config, store, workspacePath);
17418
17685
  server.watchRuntimeChanges();
17419
17686
  const gThis = globalThis;
@@ -17445,7 +17712,7 @@ init_constants();
17445
17712
  function makeIdentityCommand(workspacePath = process.cwd()) {
17446
17713
  const identity = new Command("identity").alias("id").description("Manage Cascade identities");
17447
17714
  identity.command("list").description("List all available identities").action(() => {
17448
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
17715
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
17449
17716
  try {
17450
17717
  const identities = store.listIdentities();
17451
17718
  console.log(chalk9.bold("\n Identities:"));
@@ -17465,7 +17732,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
17465
17732
  }
17466
17733
  });
17467
17734
  identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
17468
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
17735
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
17469
17736
  try {
17470
17737
  if (options.default) {
17471
17738
  const existingDefault = store.getDefaultIdentity();
@@ -17491,7 +17758,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
17491
17758
  }
17492
17759
  });
17493
17760
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
17494
- const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
17761
+ const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
17495
17762
  try {
17496
17763
  const identities = store.listIdentities();
17497
17764
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -17619,11 +17886,11 @@ async function exportCommand(options = {}) {
17619
17886
  let store;
17620
17887
  try {
17621
17888
  const workspacePath = options.workspacePath ?? process.cwd();
17622
- const workspaceDbPath = path25.join(workspacePath, CASCADE_DB_FILE);
17623
- const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
17889
+ const workspaceDbPath = path26.join(workspacePath, CASCADE_DB_FILE);
17890
+ const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
17624
17891
  let dbPath = globalDbPath;
17625
17892
  try {
17626
- await fs9.access(workspaceDbPath);
17893
+ await fs10.access(workspaceDbPath);
17627
17894
  dbPath = workspaceDbPath;
17628
17895
  } catch {
17629
17896
  }
@@ -17662,8 +17929,8 @@ async function exportCommand(options = {}) {
17662
17929
  const ext = format === "json" ? ".json" : ".md";
17663
17930
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
17664
17931
  const defaultFile = `cascade-export-${safeName}${ext}`;
17665
- const outPath = options.output ? path25.resolve(options.output) : path25.join(process.cwd(), defaultFile);
17666
- await fs9.writeFile(outPath, content, "utf-8");
17932
+ const outPath = options.output ? path26.resolve(options.output) : path26.join(process.cwd(), defaultFile);
17933
+ await fs10.writeFile(outPath, content, "utf-8");
17667
17934
  spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
17668
17935
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
17669
17936
  console.log();
@@ -17890,11 +18157,11 @@ async function statsCommand() {
17890
18157
  dotenv.config();
17891
18158
  function warnIfBuildIsStale() {
17892
18159
  try {
17893
- const here = path25.dirname(fileURLToPath(import.meta.url));
18160
+ const here = path26.dirname(fileURLToPath(import.meta.url));
17894
18161
  for (const rel of ["..", "../.."]) {
17895
- const pkgPath = path25.join(here, rel, "package.json");
17896
- if (!fs23.existsSync(pkgPath)) continue;
17897
- const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
18162
+ const pkgPath = path26.join(here, rel, "package.json");
18163
+ if (!fs24.existsSync(pkgPath)) continue;
18164
+ const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
17898
18165
  if (pkg.name !== "cascade-ai") continue;
17899
18166
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
17900
18167
  console.error(