cascade-ai 0.16.0 → 0.18.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.cjs CHANGED
@@ -9,8 +9,8 @@ var https = require('https');
9
9
  var zlib = require('zlib');
10
10
  var stream = require('stream');
11
11
  var Database2 = require('better-sqlite3');
12
- var path25 = require('path');
13
- var fs23 = require('fs');
12
+ var path26 = require('path');
13
+ var fs24 = require('fs');
14
14
  var os8 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var ink = require('ink');
@@ -19,7 +19,7 @@ var React2 = require('react');
19
19
  var chalk9 = require('chalk');
20
20
  var dotenv = require('dotenv');
21
21
  var url = require('url');
22
- var fs9 = require('fs/promises');
22
+ var fs10 = require('fs/promises');
23
23
  var _ignoreModule = require('ignore');
24
24
  var zod = require('zod');
25
25
  var child_process = require('child_process');
@@ -43,6 +43,7 @@ var bcrypt = require('bcryptjs');
43
43
  var socket_io = require('socket.io');
44
44
  var parser = require('socket.io-msgpack-parser');
45
45
  var jwt = require('jsonwebtoken');
46
+ var cron = require('node-cron');
46
47
 
47
48
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
48
49
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -72,14 +73,14 @@ var http__default = /*#__PURE__*/_interopDefault(http);
72
73
  var https__default = /*#__PURE__*/_interopDefault(https);
73
74
  var zlib__default = /*#__PURE__*/_interopDefault(zlib);
74
75
  var Database2__default = /*#__PURE__*/_interopDefault(Database2);
75
- var path25__default = /*#__PURE__*/_interopDefault(path25);
76
- var fs23__default = /*#__PURE__*/_interopDefault(fs23);
76
+ var path26__default = /*#__PURE__*/_interopDefault(path26);
77
+ var fs24__default = /*#__PURE__*/_interopDefault(fs24);
77
78
  var os8__default = /*#__PURE__*/_interopDefault(os8);
78
79
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
79
80
  var React2__default = /*#__PURE__*/_interopDefault(React2);
80
81
  var chalk9__default = /*#__PURE__*/_interopDefault(chalk9);
81
82
  var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
82
- var fs9__default = /*#__PURE__*/_interopDefault(fs9);
83
+ var fs10__default = /*#__PURE__*/_interopDefault(fs10);
83
84
  var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
84
85
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
85
86
  var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
@@ -93,6 +94,7 @@ var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
93
94
  var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
94
95
  var parser__default = /*#__PURE__*/_interopDefault(parser);
95
96
  var jwt__default = /*#__PURE__*/_interopDefault(jwt);
97
+ var cron__default = /*#__PURE__*/_interopDefault(cron);
96
98
 
97
99
  // Cascade AI — Multi-tier AI Orchestration System
98
100
  var __defProp = Object.defineProperty;
@@ -106,16 +108,17 @@ var __export = (target, all) => {
106
108
  };
107
109
 
108
110
  // src/constants.ts
109
- 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;
111
+ 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;
110
112
  var init_constants = __esm({
111
113
  "src/constants.ts"() {
112
- CASCADE_VERSION = "0.16.0";
114
+ CASCADE_VERSION = "0.18.0";
113
115
  CASCADE_CONFIG_FILE = ".cascade/config.json";
114
116
  CASCADE_DB_FILE = ".cascade/memory.db";
115
117
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
116
118
  GLOBAL_CONFIG_DIR = ".cascade-ai";
117
119
  GLOBAL_DB_FILE = "memory.db";
118
120
  GLOBAL_KEYSTORE_FILE = "keystore.enc";
121
+ GLOBAL_CREDENTIALS_FILE = "credentials.json";
119
122
  GLOBAL_RUNTIME_DB_FILE = "runtime.db";
120
123
  DEFAULT_DASHBOARD_PORT = 4891;
121
124
  DEFAULT_CONTEXT_LIMIT = 2e5;
@@ -1630,12 +1633,12 @@ var init_audit_logger = __esm({
1630
1633
  constructor(workspacePath, debugMode = false) {
1631
1634
  this.workspacePath = workspacePath;
1632
1635
  this.debugMode = debugMode;
1633
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
1634
- if (!fs23__default.default.existsSync(cascadeDir)) {
1635
- fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
1636
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
1637
+ if (!fs24__default.default.existsSync(cascadeDir)) {
1638
+ fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
1636
1639
  }
1637
- this.keyPath = path25__default.default.join(cascadeDir, "audit_log.key");
1638
- this.dbPath = path25__default.default.join(cascadeDir, "audit_log.db");
1640
+ this.keyPath = path26__default.default.join(cascadeDir, "audit_log.key");
1641
+ this.dbPath = path26__default.default.join(cascadeDir, "audit_log.db");
1639
1642
  this.initEncryptionKey();
1640
1643
  this.db = new Database2__default.default(this.dbPath);
1641
1644
  this.db.pragma("journal_mode = WAL");
@@ -1665,11 +1668,11 @@ var init_audit_logger = __esm({
1665
1668
  return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
1666
1669
  }
1667
1670
  initEncryptionKey() {
1668
- if (fs23__default.default.existsSync(this.keyPath)) {
1669
- this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
1671
+ if (fs24__default.default.existsSync(this.keyPath)) {
1672
+ this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
1670
1673
  } else {
1671
1674
  this.encryptionKey = crypto__default.default.randomBytes(32);
1672
- fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
1675
+ fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
1673
1676
  }
1674
1677
  }
1675
1678
  encrypt(text) {
@@ -1747,9 +1750,9 @@ var init_audit_logger = __esm({
1747
1750
  dumpDebugIfNeeded() {
1748
1751
  if (!this.debugMode) return;
1749
1752
  try {
1750
- const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_audit_logs_debug.json");
1753
+ const dumpPath = path26__default.default.join(os8__default.default.tmpdir(), "cascade_audit_logs_debug.json");
1751
1754
  const entries = this.getAllLogs();
1752
- fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1755
+ fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
1753
1756
  } catch (err) {
1754
1757
  console.error("Failed to dump debug audit logs", err);
1755
1758
  }
@@ -1804,7 +1807,7 @@ var Keystore = class {
1804
1807
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
1805
1808
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
1806
1809
  this.backend = "keytar";
1807
- if (password && fs23__default.default.existsSync(this.storePath)) {
1810
+ if (password && fs24__default.default.existsSync(this.storePath)) {
1808
1811
  try {
1809
1812
  const fileEntries = this.decryptFile(password);
1810
1813
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -1823,7 +1826,7 @@ var Keystore = class {
1823
1826
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
1824
1827
  );
1825
1828
  }
1826
- if (!fs23__default.default.existsSync(this.storePath)) {
1829
+ if (!fs24__default.default.existsSync(this.storePath)) {
1827
1830
  const salt = crypto__default.default.randomBytes(SALT_LEN);
1828
1831
  this.masterKey = this.deriveKey(password, salt);
1829
1832
  this.writeWithSalt({}, salt);
@@ -1837,7 +1840,7 @@ var Keystore = class {
1837
1840
  }
1838
1841
  /** Synchronous legacy unlock kept for AES-only environments. */
1839
1842
  unlockSync(password) {
1840
- if (!fs23__default.default.existsSync(this.storePath)) {
1843
+ if (!fs24__default.default.existsSync(this.storePath)) {
1841
1844
  const salt = crypto__default.default.randomBytes(SALT_LEN);
1842
1845
  this.masterKey = this.deriveKey(password, salt);
1843
1846
  this.writeWithSalt({}, salt);
@@ -1895,7 +1898,7 @@ var Keystore = class {
1895
1898
  }
1896
1899
  }
1897
1900
  decryptFile(password, knownSalt) {
1898
- if (!fs23__default.default.existsSync(this.storePath)) return {};
1901
+ if (!fs24__default.default.existsSync(this.storePath)) return {};
1899
1902
  try {
1900
1903
  const { salt, ciphertext, iv, tag } = this.readRaw();
1901
1904
  const useSalt = knownSalt ?? salt;
@@ -1917,8 +1920,8 @@ var Keystore = class {
1917
1920
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1918
1921
  const tag = cipher.getAuthTag();
1919
1922
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
1920
- fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
1921
- fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1923
+ fs24__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
1924
+ fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1922
1925
  }
1923
1926
  writeWithSalt(data, salt) {
1924
1927
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -1928,11 +1931,11 @@ var Keystore = class {
1928
1931
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1929
1932
  const tag = cipher.getAuthTag();
1930
1933
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
1931
- fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
1932
- fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1934
+ fs24__default.default.mkdirSync(path26__default.default.dirname(this.storePath), { recursive: true });
1935
+ fs24__default.default.writeFileSync(this.storePath, out, { mode: 384 });
1933
1936
  }
1934
1937
  readRaw() {
1935
- const buf = fs23__default.default.readFileSync(this.storePath);
1938
+ const buf = fs24__default.default.readFileSync(this.storePath);
1936
1939
  let offset = 0;
1937
1940
  const salt = buf.subarray(offset, offset + SALT_LEN);
1938
1941
  offset += SALT_LEN;
@@ -1965,9 +1968,9 @@ var CascadeIgnore = class {
1965
1968
  ]);
1966
1969
  }
1967
1970
  async load(workspacePath) {
1968
- const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
1971
+ const filePath = path26__default.default.join(workspacePath, ".cascadeignore");
1969
1972
  try {
1970
- const content = await fs9__default.default.readFile(filePath, "utf-8");
1973
+ const content = await fs10__default.default.readFile(filePath, "utf-8");
1971
1974
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
1972
1975
  this.ig.add(lines);
1973
1976
  this.loaded = true;
@@ -1976,7 +1979,7 @@ var CascadeIgnore = class {
1976
1979
  }
1977
1980
  isIgnored(filePath, workspacePath) {
1978
1981
  try {
1979
- const relative = workspacePath ? path25__default.default.relative(workspacePath, filePath) : filePath;
1982
+ const relative = workspacePath ? path26__default.default.relative(workspacePath, filePath) : filePath;
1980
1983
  return this.ig.ignores(relative);
1981
1984
  } catch {
1982
1985
  return false;
@@ -1987,7 +1990,7 @@ var CascadeIgnore = class {
1987
1990
  }
1988
1991
  };
1989
1992
  async function createDefaultIgnoreFile(workspacePath) {
1990
- const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
1993
+ const filePath = path26__default.default.join(workspacePath, ".cascadeignore");
1991
1994
  const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
1992
1995
  # Syntax identical to .gitignore
1993
1996
 
@@ -2014,12 +2017,12 @@ build/
2014
2017
  .DS_Store
2015
2018
  Thumbs.db
2016
2019
  `;
2017
- await fs9__default.default.writeFile(filePath, content, "utf-8");
2020
+ await fs10__default.default.writeFile(filePath, content, "utf-8");
2018
2021
  }
2019
2022
  async function loadCascadeMd(workspacePath) {
2020
- const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
2023
+ const filePath = path26__default.default.join(workspacePath, "CASCADE.md");
2021
2024
  try {
2022
- const raw = await fs9__default.default.readFile(filePath, "utf-8");
2025
+ const raw = await fs10__default.default.readFile(filePath, "utf-8");
2023
2026
  return parseCascadeMd(raw);
2024
2027
  } catch {
2025
2028
  return null;
@@ -2046,7 +2049,7 @@ ${raw.trim()}`;
2046
2049
  return { raw, sections, systemPrompt };
2047
2050
  }
2048
2051
  async function createDefaultCascadeMd(workspacePath) {
2049
- const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
2052
+ const filePath = path26__default.default.join(workspacePath, "CASCADE.md");
2050
2053
  const content = `# Cascade Project Instructions
2051
2054
 
2052
2055
  This file contains project-specific instructions for the Cascade AI agent.
@@ -2076,12 +2079,12 @@ All tools are allowed unless specified otherwise.
2076
2079
 
2077
2080
  List any areas the agent should not touch.
2078
2081
  `;
2079
- await fs9__default.default.writeFile(filePath, content, "utf-8");
2082
+ await fs10__default.default.writeFile(filePath, content, "utf-8");
2080
2083
  }
2081
2084
  var MemoryStore = class _MemoryStore {
2082
2085
  db;
2083
2086
  constructor(dbPath) {
2084
- fs23__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
2087
+ fs24__default.default.mkdirSync(path26__default.default.dirname(dbPath), { recursive: true });
2085
2088
  try {
2086
2089
  this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
2087
2090
  this.db.pragma("journal_mode = WAL");
@@ -3228,6 +3231,63 @@ function validateConfig(raw) {
3228
3231
  return result.data;
3229
3232
  }
3230
3233
 
3234
+ // src/config/global-credentials.ts
3235
+ init_constants();
3236
+ function credentialsPath(globalDir) {
3237
+ return path26__default.default.join(globalDir, GLOBAL_CREDENTIALS_FILE);
3238
+ }
3239
+ function providerKey(p) {
3240
+ if (p.type === "azure") {
3241
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
3242
+ }
3243
+ return p.type;
3244
+ }
3245
+ function isPersistable(p) {
3246
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
3247
+ }
3248
+ function loadGlobalCredentials(globalDir) {
3249
+ try {
3250
+ const raw = fs24__default.default.readFileSync(credentialsPath(globalDir), "utf-8");
3251
+ const parsed = JSON.parse(raw);
3252
+ if (!Array.isArray(parsed.providers)) return [];
3253
+ return parsed.providers.filter(
3254
+ (p) => Boolean(p) && typeof p.type === "string"
3255
+ );
3256
+ } catch {
3257
+ return [];
3258
+ }
3259
+ }
3260
+ function saveGlobalCredentials(globalDir, providers) {
3261
+ const filePath = credentialsPath(globalDir);
3262
+ fs24__default.default.mkdirSync(globalDir, { recursive: true, mode: 448 });
3263
+ const body = { version: 1, providers: providers.filter(isPersistable) };
3264
+ const tmp = `${filePath}.tmp`;
3265
+ fs24__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
3266
+ fs24__default.default.renameSync(tmp, filePath);
3267
+ try {
3268
+ fs24__default.default.chmodSync(filePath, 384);
3269
+ } catch {
3270
+ }
3271
+ }
3272
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
3273
+ const merged = [...workspaceProviders];
3274
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
3275
+ for (const g of globalProviders) {
3276
+ const existing = byKey.get(providerKey(g));
3277
+ if (!existing) {
3278
+ merged.push({ ...g });
3279
+ byKey.set(providerKey(g), merged[merged.length - 1]);
3280
+ continue;
3281
+ }
3282
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
3283
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
3284
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
3285
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
3286
+ if (!existing.label && g.label) existing.label = g.label;
3287
+ }
3288
+ return merged;
3289
+ }
3290
+
3231
3291
  // src/config/index.ts
3232
3292
  init_constants();
3233
3293
  var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
@@ -3243,18 +3303,20 @@ var ConfigManager = class {
3243
3303
  cascadeMd = null;
3244
3304
  workspacePath;
3245
3305
  globalDir;
3246
- constructor(workspacePath = process.cwd()) {
3306
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
3307
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
3247
3308
  this.workspacePath = workspacePath;
3248
- this.globalDir = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR);
3309
+ this.globalDir = globalDirOverride ?? path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR);
3249
3310
  }
3250
3311
  async load() {
3251
3312
  this.config = await this.loadConfig();
3252
3313
  this.ignore = new CascadeIgnore();
3253
3314
  await this.ignore.load(this.workspacePath);
3254
3315
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
3255
- this.keystore = new Keystore(path25__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3256
- this.store = new MemoryStore(path25__default.default.join(this.workspacePath, CASCADE_DB_FILE));
3316
+ this.keystore = new Keystore(path26__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
3317
+ this.store = new MemoryStore(path26__default.default.join(this.workspacePath, CASCADE_DB_FILE));
3257
3318
  await this.injectEnvKeys();
3319
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
3258
3320
  await this.ensureDefaultIdentity();
3259
3321
  }
3260
3322
  getConfig() {
@@ -3276,9 +3338,14 @@ var ConfigManager = class {
3276
3338
  return this.workspacePath;
3277
3339
  }
3278
3340
  async save() {
3279
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3280
- await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
3281
- await fs9__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
3341
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3342
+ await fs10__default.default.mkdir(path26__default.default.dirname(configPath), { recursive: true });
3343
+ await fs10__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
3344
+ try {
3345
+ saveGlobalCredentials(this.globalDir, this.config.providers);
3346
+ } catch (err) {
3347
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
3348
+ }
3282
3349
  }
3283
3350
  async updateConfig(updates) {
3284
3351
  this.config = validateConfig({ ...this.config, ...updates });
@@ -3301,9 +3368,9 @@ var ConfigManager = class {
3301
3368
  return configProvider?.apiKey;
3302
3369
  }
3303
3370
  async loadConfig() {
3304
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3371
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
3305
3372
  try {
3306
- const raw = await fs9__default.default.readFile(configPath, "utf-8");
3373
+ const raw = await fs10__default.default.readFile(configPath, "utf-8");
3307
3374
  return validateConfig(JSON.parse(raw));
3308
3375
  } catch (err) {
3309
3376
  if (err.code === "ENOENT") {
@@ -4180,7 +4247,7 @@ init_constants();
4180
4247
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
4181
4248
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
4182
4249
  var FETCH_TIMEOUT_MS = 8e3;
4183
- var DEFAULT_CACHE_FILE = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4250
+ var DEFAULT_CACHE_FILE = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
4184
4251
  function normalizeModelId(id) {
4185
4252
  let s = id.toLowerCase();
4186
4253
  const slash = s.lastIndexOf("/");
@@ -4213,7 +4280,7 @@ var LiveDataProvider = class {
4213
4280
  if (this.loaded) return;
4214
4281
  this.loaded = true;
4215
4282
  try {
4216
- const raw = await fs9__default.default.readFile(this.opts.cacheFile, "utf-8");
4283
+ const raw = await fs10__default.default.readFile(this.opts.cacheFile, "utf-8");
4217
4284
  const cache = JSON.parse(raw);
4218
4285
  if (cache.snapshot?.families) {
4219
4286
  this.snapshot = cache.snapshot;
@@ -4325,14 +4392,14 @@ var LiveDataProvider = class {
4325
4392
  }
4326
4393
  async saveCache() {
4327
4394
  try {
4328
- await fs9__default.default.mkdir(path25__default.default.dirname(this.opts.cacheFile), { recursive: true });
4395
+ await fs10__default.default.mkdir(path26__default.default.dirname(this.opts.cacheFile), { recursive: true });
4329
4396
  const cache = {
4330
4397
  fetchedAt: this.fetchedAt,
4331
4398
  snapshot: this.snapshot ?? void 0,
4332
4399
  prices: Object.fromEntries(this.prices),
4333
4400
  capabilities: Object.fromEntries(this.capabilities)
4334
4401
  };
4335
- await fs9__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
4402
+ await fs10__default.default.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
4336
4403
  } catch {
4337
4404
  }
4338
4405
  }
@@ -6229,8 +6296,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
6229
6296
  tierId: this.id,
6230
6297
  sessionId: this.taskId,
6231
6298
  requireApproval: false,
6232
- saveSnapshot: async (path30, content) => {
6233
- this.store?.addFileSnapshot(this.taskId, path30, content);
6299
+ saveSnapshot: async (path31, content) => {
6300
+ this.store?.addFileSnapshot(this.taskId, path31, content);
6234
6301
  },
6235
6302
  sendPeerSync: (to, syncType, content) => {
6236
6303
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -6400,9 +6467,9 @@ ${assignment.expectedOutput}`;
6400
6467
  const { promisify: promisify4 } = await import('util');
6401
6468
  const execAsync3 = promisify4(exec3);
6402
6469
  for (const artifactPath of artifactPaths) {
6403
- const absolutePath = path25__default.default.resolve(process.cwd(), artifactPath);
6470
+ const absolutePath = path26__default.default.resolve(process.cwd(), artifactPath);
6404
6471
  try {
6405
- const stat = await fs9__default.default.stat(absolutePath);
6472
+ const stat = await fs10__default.default.stat(absolutePath);
6406
6473
  if (!stat.isFile()) {
6407
6474
  issues.push(`Expected artifact is not a file: ${artifactPath}`);
6408
6475
  continue;
@@ -6412,7 +6479,7 @@ ${assignment.expectedOutput}`;
6412
6479
  continue;
6413
6480
  }
6414
6481
  if (!/\.pdf$/i.test(artifactPath)) {
6415
- const content = await fs9__default.default.readFile(absolutePath, "utf-8");
6482
+ const content = await fs10__default.default.readFile(absolutePath, "utf-8");
6416
6483
  if (!content.trim()) {
6417
6484
  issues.push(`Artifact content is empty: ${artifactPath}`);
6418
6485
  continue;
@@ -6421,7 +6488,7 @@ ${assignment.expectedOutput}`;
6421
6488
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
6422
6489
  continue;
6423
6490
  }
6424
- const ext = path25__default.default.extname(absolutePath).toLowerCase();
6491
+ const ext = path26__default.default.extname(absolutePath).toLowerCase();
6425
6492
  try {
6426
6493
  if (ext === ".ts" || ext === ".tsx") {
6427
6494
  await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -8512,16 +8579,16 @@ function resolveInWorkspace(workspaceRoot, input) {
8512
8579
  if (typeof input !== "string" || input.length === 0) {
8513
8580
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
8514
8581
  }
8515
- const root = path25__default.default.resolve(workspaceRoot);
8516
- const abs = path25__default.default.isAbsolute(input) ? path25__default.default.resolve(input) : path25__default.default.resolve(root, input);
8517
- const rel = path25__default.default.relative(root, abs);
8518
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25__default.default.isAbsolute(rel)) {
8582
+ const root = path26__default.default.resolve(workspaceRoot);
8583
+ const abs = path26__default.default.isAbsolute(input) ? path26__default.default.resolve(input) : path26__default.default.resolve(root, input);
8584
+ const rel = path26__default.default.relative(root, abs);
8585
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26__default.default.isAbsolute(rel)) {
8519
8586
  throw new WorkspaceSandboxError(input, root);
8520
8587
  }
8521
8588
  try {
8522
- const real = fs23__default.default.realpathSync(abs);
8523
- const realRel = path25__default.default.relative(root, real);
8524
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25__default.default.isAbsolute(realRel))) {
8589
+ const real = fs24__default.default.realpathSync(abs);
8590
+ const realRel = path26__default.default.relative(root, real);
8591
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26__default.default.isAbsolute(realRel))) {
8525
8592
  throw new WorkspaceSandboxError(input, root);
8526
8593
  }
8527
8594
  } catch (e) {
@@ -8548,7 +8615,7 @@ var FileReadTool = class extends BaseTool {
8548
8615
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
8549
8616
  const offset = input["offset"] ?? 1;
8550
8617
  const limit = input["limit"];
8551
- const content = await fs9__default.default.readFile(absPath, "utf-8");
8618
+ const content = await fs10__default.default.readFile(absPath, "utf-8");
8552
8619
  const lines = content.split("\n");
8553
8620
  const start = Math.max(0, offset - 1);
8554
8621
  const end = limit ? start + limit : lines.length;
@@ -8577,13 +8644,13 @@ var FileWriteTool = class extends BaseTool {
8577
8644
  const content = input["content"];
8578
8645
  if (options.saveSnapshot) {
8579
8646
  try {
8580
- const oldContent = await fs9__default.default.readFile(absPath, "utf-8");
8647
+ const oldContent = await fs10__default.default.readFile(absPath, "utf-8");
8581
8648
  await options.saveSnapshot(absPath, oldContent);
8582
8649
  } catch {
8583
8650
  }
8584
8651
  }
8585
- await fs9__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
8586
- await fs9__default.default.writeFile(absPath, content, "utf-8");
8652
+ await fs10__default.default.mkdir(path26__default.default.dirname(absPath), { recursive: true });
8653
+ await fs10__default.default.writeFile(absPath, content, "utf-8");
8587
8654
  return `Written ${content.length} characters to ${filePath}`;
8588
8655
  }
8589
8656
  };
@@ -8609,7 +8676,7 @@ var FileEditTool = class extends BaseTool {
8609
8676
  const oldString = input["old_string"];
8610
8677
  const newString = input["new_string"];
8611
8678
  const replaceAll = input["replace_all"] ?? false;
8612
- const rawContent = await fs9__default.default.readFile(absPath, "utf-8");
8679
+ const rawContent = await fs10__default.default.readFile(absPath, "utf-8");
8613
8680
  if (options.saveSnapshot) {
8614
8681
  await options.saveSnapshot(absPath, rawContent);
8615
8682
  }
@@ -8621,7 +8688,7 @@ var FileEditTool = class extends BaseTool {
8621
8688
  );
8622
8689
  }
8623
8690
  const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
8624
- await fs9__default.default.writeFile(absPath, updated, "utf-8");
8691
+ await fs10__default.default.writeFile(absPath, updated, "utf-8");
8625
8692
  const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
8626
8693
  return `Replaced ${count} occurrence(s) in ${filePath}`;
8627
8694
  }
@@ -8644,12 +8711,12 @@ var FileDeleteTool = class extends BaseTool {
8644
8711
  const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
8645
8712
  if (options.saveSnapshot) {
8646
8713
  try {
8647
- const oldContent = await fs9__default.default.readFile(absPath, "utf-8");
8714
+ const oldContent = await fs10__default.default.readFile(absPath, "utf-8");
8648
8715
  await options.saveSnapshot(absPath, oldContent);
8649
8716
  } catch {
8650
8717
  }
8651
8718
  }
8652
- await fs9__default.default.rm(absPath, { recursive: false });
8719
+ await fs10__default.default.rm(absPath, { recursive: false });
8653
8720
  return `Deleted ${filePath}`;
8654
8721
  }
8655
8722
  };
@@ -8666,7 +8733,7 @@ var FileListTool = class extends BaseTool {
8666
8733
  async execute(input, _options) {
8667
8734
  const inputPath = input["path"] || ".";
8668
8735
  const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
8669
- const entries = await fs9__default.default.readdir(absPath, { withFileTypes: true });
8736
+ const entries = await fs10__default.default.readdir(absPath, { withFileTypes: true });
8670
8737
  return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
8671
8738
  }
8672
8739
  };
@@ -9079,8 +9146,8 @@ var ImageAnalyzeTool = class extends BaseTool {
9079
9146
  }
9080
9147
  };
9081
9148
  async function fileToImageAttachment(filePath) {
9082
- const data = await fs9__default.default.readFile(filePath);
9083
- const ext = path25__default.default.extname(filePath).toLowerCase();
9149
+ const data = await fs10__default.default.readFile(filePath);
9150
+ const ext = path26__default.default.extname(filePath).toLowerCase();
9084
9151
  const mimeMap = {
9085
9152
  ".jpg": "image/jpeg",
9086
9153
  ".jpeg": "image/jpeg",
@@ -9114,14 +9181,14 @@ var PDFCreateTool = class extends BaseTool {
9114
9181
  const filePath = input["path"];
9115
9182
  const content = input["content"];
9116
9183
  const title = input["title"];
9117
- const dir = path25__default.default.dirname(filePath);
9118
- if (!fs23__default.default.existsSync(dir)) {
9119
- fs23__default.default.mkdirSync(dir, { recursive: true });
9184
+ const dir = path26__default.default.dirname(filePath);
9185
+ if (!fs24__default.default.existsSync(dir)) {
9186
+ fs24__default.default.mkdirSync(dir, { recursive: true });
9120
9187
  }
9121
9188
  return new Promise((resolve, reject) => {
9122
9189
  try {
9123
9190
  const doc = new PDFDocument__default.default({ margin: 50 });
9124
- const stream = fs23__default.default.createWriteStream(filePath);
9191
+ const stream = fs24__default.default.createWriteStream(filePath);
9125
9192
  doc.pipe(stream);
9126
9193
  if (title) {
9127
9194
  doc.info["Title"] = title;
@@ -9199,22 +9266,22 @@ var CodeInterpreterTool = class extends BaseTool {
9199
9266
  }
9200
9267
  cmdPrefix = NODE_CMD;
9201
9268
  }
9202
- const tmpDir = path25__default.default.join(this.workspaceRoot, ".cascade", "tmp");
9203
- if (!fs23__default.default.existsSync(tmpDir)) {
9204
- fs23__default.default.mkdirSync(tmpDir, { recursive: true });
9269
+ const tmpDir = path26__default.default.join(this.workspaceRoot, ".cascade", "tmp");
9270
+ if (!fs24__default.default.existsSync(tmpDir)) {
9271
+ fs24__default.default.mkdirSync(tmpDir, { recursive: true });
9205
9272
  }
9206
9273
  const extension = language === "python" ? "py" : "js";
9207
9274
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
9208
- const filePath = path25__default.default.join(tmpDir, fileName);
9209
- fs23__default.default.writeFileSync(filePath, code, "utf-8");
9275
+ const filePath = path26__default.default.join(tmpDir, fileName);
9276
+ fs24__default.default.writeFileSync(filePath, code, "utf-8");
9210
9277
  const execArgs = [filePath, ...args];
9211
9278
  return new Promise((resolve) => {
9212
9279
  const startMs = Date.now();
9213
9280
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
9214
9281
  const duration = Date.now() - startMs;
9215
9282
  try {
9216
- if (fs23__default.default.existsSync(filePath)) {
9217
- fs23__default.default.unlinkSync(filePath);
9283
+ if (fs24__default.default.existsSync(filePath)) {
9284
+ fs24__default.default.unlinkSync(filePath);
9218
9285
  }
9219
9286
  } catch (cleanupErr) {
9220
9287
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -9493,7 +9560,7 @@ var GlobTool = class extends BaseTool {
9493
9560
  };
9494
9561
  async execute(input, _options) {
9495
9562
  const pattern = input["pattern"];
9496
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9563
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9497
9564
  const matches = await glob.glob(pattern, {
9498
9565
  cwd: searchPath,
9499
9566
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -9506,7 +9573,7 @@ var GlobTool = class extends BaseTool {
9506
9573
  const withMtime = await Promise.all(
9507
9574
  matches.map(async (rel) => {
9508
9575
  try {
9509
- const stat = await fs9__default.default.stat(path25__default.default.join(searchPath, rel));
9576
+ const stat = await fs10__default.default.stat(path26__default.default.join(searchPath, rel));
9510
9577
  return { rel, mtime: stat.mtimeMs };
9511
9578
  } catch {
9512
9579
  return { rel, mtime: 0 };
@@ -9555,7 +9622,7 @@ var GrepTool = class extends BaseTool {
9555
9622
  };
9556
9623
  async execute(input, _options) {
9557
9624
  const pattern = input["pattern"];
9558
- const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9625
+ const searchPath = input["path"] ? path26__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
9559
9626
  const globPattern = input["glob"];
9560
9627
  const outputMode = input["output_mode"] ?? "content";
9561
9628
  const context = input["context"] ?? 0;
@@ -9609,15 +9676,15 @@ var GrepTool = class extends BaseTool {
9609
9676
  nodir: true
9610
9677
  });
9611
9678
  } catch {
9612
- files = [path25__default.default.relative(searchPath, searchPath) || "."];
9679
+ files = [path26__default.default.relative(searchPath, searchPath) || "."];
9613
9680
  }
9614
9681
  const results = [];
9615
9682
  let totalCount = 0;
9616
9683
  for (const rel of files) {
9617
- const abs = path25__default.default.join(searchPath, rel);
9684
+ const abs = path26__default.default.join(searchPath, rel);
9618
9685
  let content;
9619
9686
  try {
9620
- content = await fs9__default.default.readFile(abs, "utf-8");
9687
+ content = await fs10__default.default.readFile(abs, "utf-8");
9621
9688
  } catch {
9622
9689
  continue;
9623
9690
  }
@@ -9976,10 +10043,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
9976
10043
  }
9977
10044
  isIgnored(filePath) {
9978
10045
  if (!filePath) return false;
9979
- const abs = path25__default.default.resolve(this.workspaceRoot, filePath);
9980
- const rel = path25__default.default.relative(this.workspaceRoot, abs);
9981
- if (!rel || rel.startsWith("..") || path25__default.default.isAbsolute(rel)) return true;
9982
- const posixRel = rel.split(path25__default.default.sep).join("/");
10046
+ const abs = path26__default.default.resolve(this.workspaceRoot, filePath);
10047
+ const rel = path26__default.default.relative(this.workspaceRoot, abs);
10048
+ if (!rel || rel.startsWith("..") || path26__default.default.isAbsolute(rel)) return true;
10049
+ const posixRel = rel.split(path26__default.default.sep).join("/");
9983
10050
  return this.ignoreMatcher.ignores(posixRel);
9984
10051
  }
9985
10052
  };
@@ -10502,7 +10569,7 @@ var TaskAnalyzer = class {
10502
10569
  analysisCache.clear();
10503
10570
  }
10504
10571
  };
10505
- var DEFAULT_STATS_FILE = path25__default.default.join(os8__default.default.homedir(), ".cascade", "model-perf.json");
10572
+ var DEFAULT_STATS_FILE = path26__default.default.join(os8__default.default.homedir(), ".cascade", "model-perf.json");
10506
10573
  var ModelPerformanceTracker = class {
10507
10574
  stats = /* @__PURE__ */ new Map();
10508
10575
  featureStats = /* @__PURE__ */ new Map();
@@ -10515,7 +10582,7 @@ var ModelPerformanceTracker = class {
10515
10582
  if (this.loaded) return;
10516
10583
  this.loaded = true;
10517
10584
  try {
10518
- const raw = await fs9__default.default.readFile(this.statsFile, "utf-8");
10585
+ const raw = await fs10__default.default.readFile(this.statsFile, "utf-8");
10519
10586
  const parsed = JSON.parse(raw);
10520
10587
  if (parsed.models) {
10521
10588
  for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
@@ -10534,12 +10601,12 @@ var ModelPerformanceTracker = class {
10534
10601
  }
10535
10602
  async save() {
10536
10603
  try {
10537
- await fs9__default.default.mkdir(path25__default.default.dirname(this.statsFile), { recursive: true });
10604
+ await fs10__default.default.mkdir(path26__default.default.dirname(this.statsFile), { recursive: true });
10538
10605
  const modelsObj = {};
10539
10606
  const featuresObj = {};
10540
10607
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
10541
10608
  for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
10542
- await fs9__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
10609
+ await fs10__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
10543
10610
  } catch {
10544
10611
  }
10545
10612
  }
@@ -11073,9 +11140,9 @@ Required capability: ${description.slice(0, 300)}`;
11073
11140
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
11074
11141
  async loadPersistedTools() {
11075
11142
  if (!this.workspacePath || !this.persistEnabled) return;
11076
- const file = path25__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
11143
+ const file = path26__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
11077
11144
  try {
11078
- const raw = await fs9__default.default.readFile(file, "utf-8");
11145
+ const raw = await fs10__default.default.readFile(file, "utf-8");
11079
11146
  const specs = JSON.parse(raw);
11080
11147
  if (!Array.isArray(specs)) return;
11081
11148
  let loaded = 0;
@@ -11097,11 +11164,11 @@ Required capability: ${description.slice(0, 300)}`;
11097
11164
  }
11098
11165
  async persist() {
11099
11166
  if (!this.workspacePath || !this.persistEnabled) return;
11100
- const dir = path25__default.default.join(this.workspacePath, ".cascade");
11101
- const file = path25__default.default.join(dir, DYNAMIC_TOOLS_FILE);
11167
+ const dir = path26__default.default.join(this.workspacePath, ".cascade");
11168
+ const file = path26__default.default.join(dir, DYNAMIC_TOOLS_FILE);
11102
11169
  try {
11103
- await fs9__default.default.mkdir(dir, { recursive: true });
11104
- await fs9__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
11170
+ await fs10__default.default.mkdir(dir, { recursive: true });
11171
+ await fs10__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
11105
11172
  } catch (err) {
11106
11173
  this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
11107
11174
  }
@@ -11118,12 +11185,12 @@ var WorldStateDB = class {
11118
11185
  constructor(workspacePath, debugMode = false) {
11119
11186
  this.workspacePath = workspacePath;
11120
11187
  this.debugMode = debugMode;
11121
- const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
11122
- if (!fs23__default.default.existsSync(cascadeDir)) {
11123
- fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
11188
+ const cascadeDir = path26__default.default.join(workspacePath, ".cascade");
11189
+ if (!fs24__default.default.existsSync(cascadeDir)) {
11190
+ fs24__default.default.mkdirSync(cascadeDir, { recursive: true });
11124
11191
  }
11125
- this.keyPath = path25__default.default.join(cascadeDir, "world_state.key");
11126
- this.dbPath = path25__default.default.join(cascadeDir, "world_state.db");
11192
+ this.keyPath = path26__default.default.join(cascadeDir, "world_state.key");
11193
+ this.dbPath = path26__default.default.join(cascadeDir, "world_state.db");
11127
11194
  this.initEncryptionKey();
11128
11195
  this.db = new Database2__default.default(this.dbPath);
11129
11196
  this.db.pragma("journal_mode = WAL");
@@ -11153,11 +11220,11 @@ var WorldStateDB = class {
11153
11220
  dbPath;
11154
11221
  encryptionKey;
11155
11222
  initEncryptionKey() {
11156
- if (fs23__default.default.existsSync(this.keyPath)) {
11157
- this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
11223
+ if (fs24__default.default.existsSync(this.keyPath)) {
11224
+ this.encryptionKey = fs24__default.default.readFileSync(this.keyPath);
11158
11225
  } else {
11159
11226
  this.encryptionKey = crypto__default.default.randomBytes(32);
11160
- fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
11227
+ fs24__default.default.writeFileSync(this.keyPath, this.encryptionKey);
11161
11228
  }
11162
11229
  }
11163
11230
  encrypt(text) {
@@ -11298,6 +11365,22 @@ var WorldStateDB = class {
11298
11365
  }
11299
11366
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
11300
11367
  }
11368
+ /**
11369
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
11370
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
11371
+ * delete — users can prune what the planner remembers about their project.
11372
+ */
11373
+ deleteFact(entity, relation) {
11374
+ const e = normalizeKey(entity);
11375
+ const r = normalizeKey(relation);
11376
+ if (!e || !r) return false;
11377
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
11378
+ return result.changes > 0;
11379
+ }
11380
+ /** Delete every fact. Returns how many were removed. */
11381
+ clearFacts() {
11382
+ return this.db.prepare("DELETE FROM facts").run().changes;
11383
+ }
11301
11384
  // ── Export / Import ──────────────────────────
11302
11385
  //
11303
11386
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -11346,9 +11429,9 @@ var WorldStateDB = class {
11346
11429
  dumpDebugIfNeeded() {
11347
11430
  if (!this.debugMode) return;
11348
11431
  try {
11349
- const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_world_state_debug.json");
11432
+ const dumpPath = path26__default.default.join(os8__default.default.tmpdir(), "cascade_world_state_debug.json");
11350
11433
  const entries = this.getAllEntries();
11351
- fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
11434
+ fs24__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
11352
11435
  } catch (err) {
11353
11436
  console.error("Failed to dump debug world state", err);
11354
11437
  }
@@ -13766,10 +13849,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13766
13849
  }
13767
13850
  } catch {
13768
13851
  }
13769
- const configPath = path25__default.default.join(workspacePath, ".cascade", "config.json");
13852
+ const configPath = path26__default.default.join(workspacePath, ".cascade", "config.json");
13770
13853
  try {
13771
- await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
13772
- await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13854
+ await fs10__default.default.mkdir(path26__default.default.dirname(configPath), { recursive: true });
13855
+ await fs10__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
13773
13856
  } catch (err) {
13774
13857
  const msg = err instanceof Error ? err.message : String(err);
13775
13858
  dispatch({
@@ -13820,9 +13903,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13820
13903
  if (msg.includes("non-text parts")) return;
13821
13904
  originalLog(...args);
13822
13905
  };
13823
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
13906
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
13824
13907
  storeRef.current = store;
13825
- globalStoreRef.current = new MemoryStore(path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13908
+ globalStoreRef.current = new MemoryStore(path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
13826
13909
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
13827
13910
  setIdentities(identityRows);
13828
13911
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -13885,14 +13968,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
13885
13968
  onThemeChange: (name) => setTheme(getTheme(name)),
13886
13969
  onExport: async (fmt) => {
13887
13970
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13888
- const exportPath = path25__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13971
+ const exportPath = path26__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
13889
13972
  if (fmt === "json") {
13890
- await fs9__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
13973
+ await fs10__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
13891
13974
  } else {
13892
13975
  const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
13893
13976
 
13894
13977
  ${msg.content}`).join("\n\n");
13895
- await fs9__default.default.writeFile(exportPath, markdown, "utf-8");
13978
+ await fs10__default.default.writeFile(exportPath, markdown, "utf-8");
13896
13979
  }
13897
13980
  },
13898
13981
  onRollback: async () => {
@@ -13902,7 +13985,7 @@ ${msg.content}`).join("\n\n");
13902
13985
  if (!snapshots.length) return "No file snapshots found for this session.";
13903
13986
  for (const { filePath, content } of snapshots) {
13904
13987
  try {
13905
- await fs9__default.default.writeFile(filePath, content, "utf-8");
13988
+ await fs10__default.default.writeFile(filePath, content, "utf-8");
13906
13989
  } catch (err) {
13907
13990
  console.error(`Restore failed: ${filePath}`, err);
13908
13991
  }
@@ -14781,9 +14864,9 @@ function stringifySlashOutput(val) {
14781
14864
  }
14782
14865
  async function searchSessionsAndMessages(query, workspacePath) {
14783
14866
  if (!query) return "Usage: /search <query>";
14784
- const dbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
14867
+ const dbPath = path26__default.default.join(workspacePath, CASCADE_DB_FILE);
14785
14868
  try {
14786
- await fs9__default.default.access(dbPath);
14869
+ await fs10__default.default.access(dbPath);
14787
14870
  } catch {
14788
14871
  return "No database found. Start a conversation first.";
14789
14872
  }
@@ -14815,7 +14898,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
14815
14898
  async function diagnoseRuntime(config, workspacePath) {
14816
14899
  const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
14817
14900
  const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
14818
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
14901
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
14819
14902
  try {
14820
14903
  const sessions = store.listSessions(void 0, 3);
14821
14904
  return [
@@ -14834,7 +14917,7 @@ async function diagnoseRuntime(config, workspacePath) {
14834
14917
  }
14835
14918
  async function showRecentLogs(args, workspacePath) {
14836
14919
  const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
14837
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
14920
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
14838
14921
  try {
14839
14922
  const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
14840
14923
  if (!logs.length) return "No recent runtime logs.";
@@ -14846,7 +14929,7 @@ async function showRecentLogs(args, workspacePath) {
14846
14929
  async function loadSessionSnapshot(args, workspacePath) {
14847
14930
  const sessionId = args[0];
14848
14931
  if (!sessionId) return "Usage: /resume <sessionId>";
14849
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
14932
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
14850
14933
  try {
14851
14934
  const session = store.getSession(sessionId);
14852
14935
  if (!session) return `Session not found: ${sessionId}`;
@@ -15266,10 +15349,10 @@ function SetupWizard({ workspacePath, onComplete }) {
15266
15349
  ...anyAuto ? { cascadeAuto: true } : {}
15267
15350
  };
15268
15351
  const config = CascadeConfigSchema.parse(rawConfig);
15269
- const configDir = path25__default.default.join(workspacePath, ".cascade");
15270
- await fs9__default.default.mkdir(configDir, { recursive: true });
15271
- const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
15272
- await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
15352
+ const configDir = path26__default.default.join(workspacePath, ".cascade");
15353
+ await fs10__default.default.mkdir(configDir, { recursive: true });
15354
+ const configPath = path26__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
15355
+ await fs10__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
15273
15356
  savedConfigRef.current = config;
15274
15357
  dispatchRef.current({ type: "GO_DONE" });
15275
15358
  } catch (err) {
@@ -15609,14 +15692,14 @@ function printTelemetryBanner() {
15609
15692
  async function initCommand(workspacePath = process.cwd()) {
15610
15693
  const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
15611
15694
  try {
15612
- const configDir = path25__default.default.join(workspacePath, ".cascade");
15613
- await fs9__default.default.mkdir(configDir, { recursive: true });
15614
- const mdPath = path25__default.default.join(workspacePath, "CASCADE.md");
15695
+ const configDir = path26__default.default.join(workspacePath, ".cascade");
15696
+ await fs10__default.default.mkdir(configDir, { recursive: true });
15697
+ const mdPath = path26__default.default.join(workspacePath, "CASCADE.md");
15615
15698
  if (!await fileExists(mdPath)) {
15616
15699
  await createDefaultCascadeMd(workspacePath);
15617
15700
  spin.succeed(chalk9__default.default.green("Created CASCADE.md"));
15618
15701
  }
15619
- const ignorePath = path25__default.default.join(workspacePath, ".cascadeignore");
15702
+ const ignorePath = path26__default.default.join(workspacePath, ".cascadeignore");
15620
15703
  if (!await fileExists(ignorePath)) {
15621
15704
  await createDefaultIgnoreFile(workspacePath);
15622
15705
  spin.succeed(chalk9__default.default.green("Created .cascadeignore"));
@@ -15625,7 +15708,7 @@ async function initCommand(workspacePath = process.cwd()) {
15625
15708
  console.log();
15626
15709
  console.log(chalk9__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
15627
15710
  console.log();
15628
- const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
15711
+ const configPath = path26__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
15629
15712
  if (await fileExists(configPath)) {
15630
15713
  console.log(chalk9__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
15631
15714
  console.log();
@@ -15644,7 +15727,7 @@ async function initCommand(workspacePath = process.cwd()) {
15644
15727
  }
15645
15728
  async function fileExists(p) {
15646
15729
  try {
15647
- await fs9__default.default.access(p);
15730
+ await fs10__default.default.access(p);
15648
15731
  return true;
15649
15732
  } catch {
15650
15733
  return false;
@@ -15656,7 +15739,7 @@ init_constants();
15656
15739
  var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
15657
15740
  async function readJson(file) {
15658
15741
  try {
15659
- const raw = await fs9__default.default.readFile(file, "utf-8");
15742
+ const raw = await fs10__default.default.readFile(file, "utf-8");
15660
15743
  return JSON.parse(raw);
15661
15744
  } catch {
15662
15745
  return null;
@@ -15683,7 +15766,7 @@ function fromEnv(env) {
15683
15766
  return out;
15684
15767
  }
15685
15768
  async function fromClaudeCode(home) {
15686
- const file = path25__default.default.join(home, ".claude", ".credentials.json");
15769
+ const file = path26__default.default.join(home, ".claude", ".credentials.json");
15687
15770
  const data = await readJson(file);
15688
15771
  if (!data) return [];
15689
15772
  const oauth = data["claudeAiOauth"];
@@ -15706,7 +15789,7 @@ async function fromClaudeCode(home) {
15706
15789
  return [];
15707
15790
  }
15708
15791
  async function fromCodex(home) {
15709
- const file = path25__default.default.join(home, ".codex", "auth.json");
15792
+ const file = path26__default.default.join(home, ".codex", "auth.json");
15710
15793
  const data = await readJson(file);
15711
15794
  if (!data) return [];
15712
15795
  const apiKey = str(data["OPENAI_API_KEY"]);
@@ -15729,7 +15812,7 @@ async function fromCodex(home) {
15729
15812
  return [];
15730
15813
  }
15731
15814
  async function fromGemini(home) {
15732
- const file = path25__default.default.join(home, ".gemini", "oauth_creds.json");
15815
+ const file = path26__default.default.join(home, ".gemini", "oauth_creds.json");
15733
15816
  const data = await readJson(file);
15734
15817
  const accessToken = str(data?.["access_token"]);
15735
15818
  if (!accessToken) return [];
@@ -15745,7 +15828,7 @@ async function fromGemini(home) {
15745
15828
  }
15746
15829
  async function fromCopilot(home) {
15747
15830
  for (const name of ["apps.json", "hosts.json"]) {
15748
- const file = path25__default.default.join(home, ".config", "github-copilot", name);
15831
+ const file = path26__default.default.join(home, ".config", "github-copilot", name);
15749
15832
  const data = await readJson(file);
15750
15833
  if (!data) continue;
15751
15834
  for (const value of Object.values(data)) {
@@ -15799,7 +15882,7 @@ async function doctorCommand() {
15799
15882
  checks.push({
15800
15883
  label: "Cascade config",
15801
15884
  ok: true,
15802
- detail: `Loaded ${path25__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15885
+ detail: `Loaded ${path26__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
15803
15886
  });
15804
15887
  const providers = [
15805
15888
  { type: "anthropic", name: "Anthropic" },
@@ -15955,6 +16038,11 @@ function authMiddleware(secret, required = true) {
15955
16038
  }
15956
16039
  const user = verifyToken(token, secret);
15957
16040
  if (!user) {
16041
+ if (!required) {
16042
+ req.user = void 0;
16043
+ next();
16044
+ return;
16045
+ }
15958
16046
  res.status(401).json({ error: "Invalid or expired token" });
15959
16047
  return;
15960
16048
  }
@@ -16087,6 +16175,25 @@ var DashboardSocket = class {
16087
16175
  });
16088
16176
  });
16089
16177
  }
16178
+ /**
16179
+ * Boardroom plan decisions from a connected client. The desktop shows a
16180
+ * plan-review modal on `plan:approval-required` and answers here; the
16181
+ * server routes the decision into the paused run via resolvePlanApproval.
16182
+ */
16183
+ onPlanDecision(callback) {
16184
+ this.io.on("connection", (socket) => {
16185
+ socket.on("plan:decision", (payload) => {
16186
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
16187
+ callback({
16188
+ sessionId: payload.sessionId,
16189
+ approved: payload.approved,
16190
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
16191
+ editedPlan: payload.editedPlan
16192
+ });
16193
+ }
16194
+ });
16195
+ });
16196
+ }
16090
16197
  onSessionSteer(callback) {
16091
16198
  this.io.on("connection", (socket) => {
16092
16199
  socket.on("session:steer", (payload) => {
@@ -16131,7 +16238,123 @@ var DashboardSocket = class {
16131
16238
 
16132
16239
  // src/dashboard/server.ts
16133
16240
  init_constants();
16134
- var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
16241
+ var TaskScheduler = class {
16242
+ cronJobs = /* @__PURE__ */ new Map();
16243
+ store;
16244
+ runner;
16245
+ constructor(store, runner) {
16246
+ this.store = store;
16247
+ this.runner = runner;
16248
+ }
16249
+ start() {
16250
+ const tasks = this.store.listScheduledTasks();
16251
+ for (const task of tasks) {
16252
+ if (task.enabled) this.schedule(task);
16253
+ }
16254
+ }
16255
+ stop() {
16256
+ for (const job of this.cronJobs.values()) job.stop();
16257
+ this.cronJobs.clear();
16258
+ }
16259
+ schedule(task) {
16260
+ if (!cron__default.default.validate(task.cronExpression)) {
16261
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
16262
+ }
16263
+ const existing = this.cronJobs.get(task.id);
16264
+ if (existing) {
16265
+ try {
16266
+ existing.stop();
16267
+ } catch {
16268
+ }
16269
+ }
16270
+ const job = cron__default.default.schedule(task.cronExpression, async () => {
16271
+ try {
16272
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
16273
+ this.store.saveScheduledTask(task);
16274
+ await this.runner(task);
16275
+ } catch (err) {
16276
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
16277
+ }
16278
+ }, { timezone: "UTC" });
16279
+ this.cronJobs.set(task.id, job);
16280
+ }
16281
+ unschedule(taskId) {
16282
+ this.cronJobs.get(taskId)?.stop();
16283
+ this.cronJobs.delete(taskId);
16284
+ }
16285
+ add(task) {
16286
+ this.store.saveScheduledTask(task);
16287
+ if (task.enabled) this.schedule(task);
16288
+ }
16289
+ remove(taskId) {
16290
+ this.unschedule(taskId);
16291
+ this.store.deleteScheduledTask(taskId);
16292
+ }
16293
+ list() {
16294
+ return this.store.listScheduledTasks();
16295
+ }
16296
+ isRunning(taskId) {
16297
+ return this.cronJobs.has(taskId);
16298
+ }
16299
+ static validateCron(expression) {
16300
+ return cron__default.default.validate(expression);
16301
+ }
16302
+ };
16303
+
16304
+ // src/dashboard/cost-stats.ts
16305
+ var DAY_MS = 24 * 60 * 60 * 1e3;
16306
+ function utcDateKey(d) {
16307
+ return d.toISOString().slice(0, 10);
16308
+ }
16309
+ function aggregateCostStats(sessions, opts = {}) {
16310
+ const days = Math.max(1, opts.days ?? 30);
16311
+ const topN = Math.max(1, opts.topN ?? 8);
16312
+ const now = opts.now ?? /* @__PURE__ */ new Date();
16313
+ const buckets = /* @__PURE__ */ new Map();
16314
+ for (let i = days - 1; i >= 0; i--) {
16315
+ const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
16316
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
16317
+ }
16318
+ let totalCostUsd = 0;
16319
+ let totalTokens = 0;
16320
+ let totalRuns = 0;
16321
+ for (const s of sessions) {
16322
+ const cost = s.metadata?.totalCostUsd ?? 0;
16323
+ const tokens = s.metadata?.totalTokens ?? 0;
16324
+ const runs = s.metadata?.taskCount ?? 0;
16325
+ totalCostUsd += cost;
16326
+ totalTokens += tokens;
16327
+ totalRuns += runs;
16328
+ const when = new Date(s.updatedAt || s.createdAt);
16329
+ if (!Number.isNaN(when.getTime())) {
16330
+ const bucket = buckets.get(utcDateKey(when));
16331
+ if (bucket) {
16332
+ bucket.costUsd += cost;
16333
+ bucket.tokens += tokens;
16334
+ bucket.runs += runs;
16335
+ }
16336
+ }
16337
+ }
16338
+ const topSessions = [...sessions].filter((s) => (s.metadata?.totalCostUsd ?? 0) > 0).sort((a, b) => (b.metadata?.totalCostUsd ?? 0) - (a.metadata?.totalCostUsd ?? 0)).slice(0, topN).map((s) => ({
16339
+ sessionId: s.id,
16340
+ title: s.title,
16341
+ costUsd: s.metadata?.totalCostUsd ?? 0,
16342
+ tokens: s.metadata?.totalTokens ?? 0,
16343
+ runs: s.metadata?.taskCount ?? 0,
16344
+ updatedAt: s.updatedAt
16345
+ }));
16346
+ return {
16347
+ totalCostUsd,
16348
+ totalTokens,
16349
+ totalSessions: sessions.length,
16350
+ totalRuns,
16351
+ perDay: [...buckets.values()],
16352
+ topSessions
16353
+ };
16354
+ }
16355
+
16356
+ // src/dashboard/server.ts
16357
+ var __dirname$1 = path26__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
16135
16358
  var DashboardServer = class {
16136
16359
  app;
16137
16360
  httpServer;
@@ -16156,6 +16379,14 @@ var DashboardServer = class {
16156
16379
  * map is how that answer reaches the run that's blocked on it.
16157
16380
  */
16158
16381
  pendingApprovals = /* @__PURE__ */ new Map();
16382
+ /**
16383
+ * The orchestration decision trail ("why") of each session's most recent
16384
+ * run — captured when the run ends so the desktop's Why panel can show it
16385
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
16386
+ */
16387
+ whyBySession = /* @__PURE__ */ new Map();
16388
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
16389
+ scheduler;
16159
16390
  port;
16160
16391
  host;
16161
16392
  workspacePath;
@@ -16173,6 +16404,7 @@ var DashboardServer = class {
16173
16404
  secret: this.dashboardSecret,
16174
16405
  corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
16175
16406
  });
16407
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
16176
16408
  this.setupMiddleware();
16177
16409
  this.setupRoutes();
16178
16410
  this.socket.onSessionRate((sessionId, rating) => {
@@ -16236,6 +16468,9 @@ var DashboardServer = class {
16236
16468
  cascade.on("peer:message", (e) => {
16237
16469
  this.socket.emitPeerMessage(e);
16238
16470
  });
16471
+ cascade.on("plan:approval-required", (e) => {
16472
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
16473
+ });
16239
16474
  try {
16240
16475
  const result = await cascade.run({
16241
16476
  prompt: runPrompt,
@@ -16243,7 +16478,8 @@ var DashboardServer = class {
16243
16478
  approvalCallback: this.makeApprovalCallback(sessionId)
16244
16479
  });
16245
16480
  this.recordSessionTask(sessionId, result.taskId);
16246
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
16481
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16482
+ this.captureWhy(sessionId, cascade, result);
16247
16483
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
16248
16484
  this.socket.broadcast("cost:update", {
16249
16485
  sessionId,
@@ -16253,6 +16489,7 @@ var DashboardServer = class {
16253
16489
  this.throttledBroadcast("workspace");
16254
16490
  } catch (err) {
16255
16491
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16492
+ this.captureWhy(sessionId, cascade);
16256
16493
  this.socket.emitToSocket(socketId, "session:error", {
16257
16494
  sessionId,
16258
16495
  error: err instanceof Error ? err.message : String(err)
@@ -16263,9 +16500,17 @@ var DashboardServer = class {
16263
16500
  this.denyPendingApprovals(sessionId);
16264
16501
  }
16265
16502
  });
16503
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
16504
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
16505
+ approved,
16506
+ note,
16507
+ editedPlan
16508
+ );
16509
+ });
16266
16510
  this.socket.onSessionHalt((sessionId) => {
16267
16511
  this.activeControllers.get(sessionId)?.abort();
16268
16512
  this.denyPendingApprovals(sessionId);
16513
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
16269
16514
  });
16270
16515
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
16271
16516
  const pending = this.pendingApprovals.get(requestId);
@@ -16298,12 +16543,21 @@ var DashboardServer = class {
16298
16543
  resolve();
16299
16544
  });
16300
16545
  });
16546
+ try {
16547
+ this.scheduler.start();
16548
+ } catch (err) {
16549
+ console.warn("[dashboard] failed to start task scheduler:", err);
16550
+ }
16301
16551
  }
16302
16552
  async stop() {
16303
16553
  if (this.broadcastTimer) {
16304
16554
  clearTimeout(this.broadcastTimer);
16305
16555
  this.broadcastTimer = null;
16306
16556
  }
16557
+ try {
16558
+ this.scheduler.stop();
16559
+ } catch {
16560
+ }
16307
16561
  this.socket.close();
16308
16562
  try {
16309
16563
  this.globalStore?.close();
@@ -16334,12 +16588,17 @@ var DashboardServer = class {
16334
16588
  */
16335
16589
  persistConfig() {
16336
16590
  try {
16337
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16338
- fs23__default.default.mkdirSync(path25__default.default.dirname(configPath), { recursive: true });
16339
- fs23__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
16591
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16592
+ fs24__default.default.mkdirSync(path26__default.default.dirname(configPath), { recursive: true });
16593
+ fs24__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
16340
16594
  } catch (err) {
16341
16595
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
16342
16596
  }
16597
+ try {
16598
+ saveGlobalCredentials(path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
16599
+ } catch (err) {
16600
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
16601
+ }
16343
16602
  }
16344
16603
  /**
16345
16604
  * Produce a stable dashboard JWT signing secret.
@@ -16351,15 +16610,15 @@ var DashboardServer = class {
16351
16610
  resolveDashboardSecret() {
16352
16611
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
16353
16612
  if (fromConfig) return fromConfig;
16354
- const secretPath = path25__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
16613
+ const secretPath = path26__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
16355
16614
  try {
16356
- if (fs23__default.default.existsSync(secretPath)) {
16357
- const existing = fs23__default.default.readFileSync(secretPath, "utf-8").trim();
16615
+ if (fs24__default.default.existsSync(secretPath)) {
16616
+ const existing = fs24__default.default.readFileSync(secretPath, "utf-8").trim();
16358
16617
  if (existing.length >= 16) return existing;
16359
16618
  }
16360
16619
  const generated = crypto.randomUUID();
16361
- fs23__default.default.mkdirSync(path25__default.default.dirname(secretPath), { recursive: true });
16362
- fs23__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
16620
+ fs24__default.default.mkdirSync(path26__default.default.dirname(secretPath), { recursive: true });
16621
+ fs24__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
16363
16622
  if (this.config.dashboard.auth) {
16364
16623
  console.warn(
16365
16624
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -16386,7 +16645,7 @@ var DashboardServer = class {
16386
16645
  // ── Setup ─────────────────────────────────────
16387
16646
  getGlobalStore() {
16388
16647
  if (!this.globalStore) {
16389
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16648
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16390
16649
  this.globalStore = new MemoryStore(globalDbPath);
16391
16650
  }
16392
16651
  return this.globalStore;
@@ -16424,16 +16683,60 @@ var DashboardServer = class {
16424
16683
  return title;
16425
16684
  }
16426
16685
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
16427
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
16686
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
16428
16687
  try {
16429
16688
  if (reply && reply.trim()) {
16430
16689
  this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
16431
16690
  }
16691
+ if (result) {
16692
+ const session = this.store.getSession(sessionId);
16693
+ if (session) {
16694
+ this.store.updateSession(sessionId, {
16695
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16696
+ metadata: {
16697
+ ...session.metadata,
16698
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
16699
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
16700
+ taskCount: session.metadata.taskCount + 1
16701
+ }
16702
+ });
16703
+ }
16704
+ }
16432
16705
  } catch (err) {
16433
16706
  console.warn("[dashboard] failed to persist run end:", err);
16434
16707
  }
16435
16708
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
16436
16709
  }
16710
+ /**
16711
+ * Capture the run's decision trail + router economics ("why") and broadcast
16712
+ * it so the desktop's Why panel updates live; kept per-session for the
16713
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
16714
+ */
16715
+ captureWhy(sessionId, cascade, result) {
16716
+ try {
16717
+ const stats = cascade.getRouter().getStats();
16718
+ const savings = cascade.getRouter().getDelegationSavings();
16719
+ const report = {
16720
+ sessionId,
16721
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
16722
+ decisions: cascade.getDecisionLog(),
16723
+ savedUsd: savings.savedUsd,
16724
+ savedPct: savings.savedPct,
16725
+ totalCostUsd: stats.totalCostUsd,
16726
+ totalTokens: stats.totalTokens,
16727
+ costByTier: stats.costByTier,
16728
+ durationMs: result?.durationMs
16729
+ };
16730
+ this.whyBySession.set(sessionId, report);
16731
+ if (this.whyBySession.size > 50) {
16732
+ const oldest = this.whyBySession.keys().next().value;
16733
+ if (oldest) this.whyBySession.delete(oldest);
16734
+ }
16735
+ this.socket.broadcast("run:why", report);
16736
+ } catch (err) {
16737
+ console.warn("[dashboard] failed to capture decision trail:", err);
16738
+ }
16739
+ }
16437
16740
  /**
16438
16741
  * Route steering text into running Cascade instances. Targets the given
16439
16742
  * session, or every active session when none is specified (the desktop
@@ -16463,6 +16766,52 @@ var DashboardServer = class {
16463
16766
  this.pendingApprovals.set(request.id, { resolve, sessionId });
16464
16767
  });
16465
16768
  }
16769
+ /**
16770
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
16771
+ * tool approvals are auto-granted since nobody may be watching when the cron
16772
+ * fires — the Schedules UI states this. Events broadcast to every connected
16773
+ * client so an open desktop sees the run appear live.
16774
+ */
16775
+ async runScheduledTask(task) {
16776
+ const sessionId = crypto.randomUUID();
16777
+ const prompt = task.prompt;
16778
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
16779
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
16780
+ this.activeSessions.set(sessionId, cascade);
16781
+ cascade.on("tier:status", (e) => {
16782
+ this.socket.broadcast("tier:status", { sessionId, ...e });
16783
+ });
16784
+ cascade.on("peer:message", (e) => {
16785
+ this.socket.emitPeerMessage(e);
16786
+ });
16787
+ try {
16788
+ const result = await cascade.run({
16789
+ prompt,
16790
+ identityId: task.identityId,
16791
+ approvalCallback: async () => ({ approved: true, always: false })
16792
+ });
16793
+ this.recordSessionTask(sessionId, result.taskId);
16794
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16795
+ this.captureWhy(sessionId, cascade, result);
16796
+ this.socket.broadcast("session:complete", { sessionId, result });
16797
+ this.socket.broadcast("cost:update", {
16798
+ sessionId,
16799
+ totalTokens: result.usage.totalTokens,
16800
+ totalCostUsd: result.usage.estimatedCostUsd
16801
+ });
16802
+ this.throttledBroadcast("workspace");
16803
+ } catch (err) {
16804
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16805
+ this.captureWhy(sessionId, cascade);
16806
+ this.socket.broadcast("session:error", {
16807
+ sessionId,
16808
+ error: err instanceof Error ? err.message : String(err)
16809
+ });
16810
+ throw err;
16811
+ } finally {
16812
+ this.activeSessions.delete(sessionId);
16813
+ }
16814
+ }
16466
16815
  /** Deny + clear any approvals still pending for a session (run end / abort). */
16467
16816
  denyPendingApprovals(sessionId) {
16468
16817
  for (const [id, pending] of this.pendingApprovals) {
@@ -16565,12 +16914,12 @@ ${prompt}`;
16565
16914
  }
16566
16915
  }
16567
16916
  watchRuntimeChanges() {
16568
- const workspaceDbPath = path25__default.default.join(this.workspacePath, CASCADE_DB_FILE);
16569
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16917
+ const workspaceDbPath = path26__default.default.join(this.workspacePath, CASCADE_DB_FILE);
16918
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16570
16919
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
16571
16920
  for (const watchPath of watchPaths) {
16572
- if (!fs23__default.default.existsSync(watchPath)) continue;
16573
- fs23__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
16921
+ if (!fs24__default.default.existsSync(watchPath)) continue;
16922
+ fs24__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
16574
16923
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
16575
16924
  });
16576
16925
  }
@@ -16692,7 +17041,7 @@ ${prompt}`;
16692
17041
  const sessionId = req.params.id;
16693
17042
  this.store.deleteSession(sessionId);
16694
17043
  this.store.deleteRuntimeSession(sessionId);
16695
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17044
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16696
17045
  const globalStore = new MemoryStore(globalDbPath);
16697
17046
  try {
16698
17047
  globalStore.deleteRuntimeSession(sessionId);
@@ -16785,7 +17134,7 @@ ${prompt}`;
16785
17134
  });
16786
17135
  this.app.delete("/api/sessions", auth, (req, res) => {
16787
17136
  const body = req.body;
16788
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17137
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16789
17138
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
16790
17139
  const globalStore = new MemoryStore(globalDbPath);
16791
17140
  try {
@@ -16808,7 +17157,7 @@ ${prompt}`;
16808
17157
  });
16809
17158
  this.app.delete("/api/runtime", auth, (_req, res) => {
16810
17159
  this.store.deleteAllRuntimeNodes();
16811
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17160
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16812
17161
  const globalStore = new MemoryStore(globalDbPath);
16813
17162
  try {
16814
17163
  globalStore.deleteAllRuntimeNodes();
@@ -16894,12 +17243,12 @@ ${prompt}`;
16894
17243
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
16895
17244
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
16896
17245
  try {
16897
- const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
16898
- const existing = fs23__default.default.existsSync(configPath) ? JSON.parse(fs23__default.default.readFileSync(configPath, "utf-8")) : {};
17246
+ const configPath = path26__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
17247
+ const existing = fs24__default.default.existsSync(configPath) ? JSON.parse(fs24__default.default.readFileSync(configPath, "utf-8")) : {};
16899
17248
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
16900
17249
  const tmp = configPath + ".tmp";
16901
- fs23__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
16902
- fs23__default.default.renameSync(tmp, configPath);
17250
+ fs24__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
17251
+ fs24__default.default.renameSync(tmp, configPath);
16903
17252
  res.json({ ok: true });
16904
17253
  } catch (err) {
16905
17254
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -16927,7 +17276,7 @@ ${prompt}`;
16927
17276
  this.app.get("/api/runtime", auth, (req, res) => {
16928
17277
  const scope = req.query["scope"] ?? "workspace";
16929
17278
  if (scope === "global") {
16930
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
17279
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
16931
17280
  const globalStore = new MemoryStore(globalDbPath);
16932
17281
  try {
16933
17282
  res.json({
@@ -16975,6 +17324,9 @@ ${prompt}`;
16975
17324
  cascade.on("peer:message", (e) => {
16976
17325
  this.socket.emitPeerMessage(e);
16977
17326
  });
17327
+ cascade.on("plan:approval-required", (e) => {
17328
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
17329
+ });
16978
17330
  try {
16979
17331
  const result = await cascade.run({
16980
17332
  prompt: runPrompt,
@@ -16982,7 +17334,8 @@ ${prompt}`;
16982
17334
  approvalCallback: this.makeApprovalCallback(sessionId)
16983
17335
  });
16984
17336
  this.recordSessionTask(sessionId, result.taskId);
16985
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
17337
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
17338
+ this.captureWhy(sessionId, cascade, result);
16986
17339
  this.socket.broadcast("cost:update", {
16987
17340
  sessionId,
16988
17341
  totalTokens: result.usage.totalTokens,
@@ -16992,6 +17345,7 @@ ${prompt}`;
16992
17345
  this.throttledBroadcast("workspace");
16993
17346
  } catch (err) {
16994
17347
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
17348
+ this.captureWhy(sessionId, cascade);
16995
17349
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
16996
17350
  sessionId,
16997
17351
  error: err instanceof Error ? err.message : String(err)
@@ -17013,13 +17367,217 @@ ${prompt}`;
17013
17367
  }))
17014
17368
  });
17015
17369
  });
17016
- const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
17017
- const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
17018
- const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;
17019
- if (fs23__default.default.existsSync(webDistPath)) {
17370
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
17371
+ const report = this.whyBySession.get(req.params.id);
17372
+ if (!report) {
17373
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
17374
+ return;
17375
+ }
17376
+ res.json(report);
17377
+ });
17378
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
17379
+ const sessionId = req.params.id;
17380
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17381
+ const before = /* @__PURE__ */ new Map();
17382
+ for (const taskId of taskIds) {
17383
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
17384
+ if (!before.has(filePath)) before.set(filePath, content);
17385
+ }
17386
+ }
17387
+ const { readFile } = await import('fs/promises');
17388
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
17389
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
17390
+ let after = "";
17391
+ let missing = false;
17392
+ try {
17393
+ const buf = await readFile(filePath);
17394
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
17395
+ } catch {
17396
+ missing = true;
17397
+ }
17398
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
17399
+ }));
17400
+ res.json({ sessionId, changes: changes.filter((c) => c.changed) });
17401
+ });
17402
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
17403
+ const sessionId = req.params.id;
17404
+ const body = req.body;
17405
+ if (!body.filePath || typeof body.filePath !== "string") {
17406
+ res.status(400).json({ error: "filePath is required" });
17407
+ return;
17408
+ }
17409
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17410
+ let content;
17411
+ for (const taskId of taskIds) {
17412
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
17413
+ if (snap) {
17414
+ content = snap.content;
17415
+ break;
17416
+ }
17417
+ }
17418
+ if (content === void 0) {
17419
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
17420
+ return;
17421
+ }
17422
+ try {
17423
+ const { writeFile } = await import('fs/promises');
17424
+ await writeFile(body.filePath, content, "utf-8");
17425
+ res.json({ ok: true, filePath: body.filePath });
17426
+ } catch (err) {
17427
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17428
+ }
17429
+ });
17430
+ this.app.get("/api/costs", auth, (_req, res) => {
17431
+ try {
17432
+ const sessions = this.store.listSessions(void 0, 1e3);
17433
+ const budget = this.config.budget ?? { warnAtPct: 80 };
17434
+ res.json({
17435
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
17436
+ budget: {
17437
+ dailyBudgetUsd: budget.dailyBudgetUsd,
17438
+ sessionBudgetUsd: budget.sessionBudgetUsd,
17439
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
17440
+ }
17441
+ });
17442
+ } catch (err) {
17443
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17444
+ }
17445
+ });
17446
+ this.app.get("/api/schedules", auth, (_req, res) => {
17447
+ try {
17448
+ res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
17449
+ } catch (err) {
17450
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17451
+ }
17452
+ });
17453
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
17454
+ const body = req.body;
17455
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
17456
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
17457
+ return;
17458
+ }
17459
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
17460
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17461
+ return;
17462
+ }
17463
+ const task = {
17464
+ id: crypto.randomUUID(),
17465
+ name: body.name.trim(),
17466
+ cronExpression: body.cronExpression.trim(),
17467
+ prompt: body.prompt.trim(),
17468
+ workspacePath: this.workspacePath,
17469
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17470
+ enabled: body.enabled !== false
17471
+ };
17472
+ try {
17473
+ this.scheduler.add(task);
17474
+ res.json(task);
17475
+ } catch (err) {
17476
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17477
+ }
17478
+ });
17479
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17480
+ const id = req.params.id;
17481
+ const existing = this.scheduler.list().find((t) => t.id === id);
17482
+ if (!existing) {
17483
+ res.status(404).json({ error: "Not found" });
17484
+ return;
17485
+ }
17486
+ const body = req.body;
17487
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
17488
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17489
+ return;
17490
+ }
17491
+ const updated = {
17492
+ ...existing,
17493
+ name: body.name?.trim() || existing.name,
17494
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
17495
+ prompt: body.prompt?.trim() || existing.prompt,
17496
+ enabled: body.enabled ?? existing.enabled
17497
+ };
17498
+ try {
17499
+ this.scheduler.add(updated);
17500
+ if (!updated.enabled) this.scheduler.unschedule(id);
17501
+ res.json(updated);
17502
+ } catch (err) {
17503
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17504
+ }
17505
+ });
17506
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17507
+ try {
17508
+ this.scheduler.remove(req.params.id);
17509
+ res.json({ ok: true });
17510
+ } catch (err) {
17511
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17512
+ }
17513
+ });
17514
+ this.app.get("/api/knowledge", auth, (_req, res) => {
17515
+ try {
17516
+ const ws = new WorldStateDB(this.workspacePath);
17517
+ try {
17518
+ const facts = ws.getAllFacts();
17519
+ res.json({ total: facts.length, facts });
17520
+ } finally {
17521
+ ws.close();
17522
+ }
17523
+ } catch (err) {
17524
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17525
+ }
17526
+ });
17527
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
17528
+ const body = req.body;
17529
+ if (!body.entity || !body.relation) {
17530
+ res.status(400).json({ error: "entity and relation are required" });
17531
+ return;
17532
+ }
17533
+ try {
17534
+ const ws = new WorldStateDB(this.workspacePath);
17535
+ try {
17536
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
17537
+ } finally {
17538
+ ws.close();
17539
+ }
17540
+ } catch (err) {
17541
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17542
+ }
17543
+ });
17544
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
17545
+ try {
17546
+ const ws = new WorldStateDB(this.workspacePath);
17547
+ try {
17548
+ res.json({ ok: true, deleted: ws.clearFacts() });
17549
+ } finally {
17550
+ ws.close();
17551
+ }
17552
+ } catch (err) {
17553
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17554
+ }
17555
+ });
17556
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
17557
+ try {
17558
+ const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
17559
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
17560
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
17561
+ const logger = new AuditLogger3(this.workspacePath);
17562
+ try {
17563
+ const all = logger.getAllLogs();
17564
+ const total = all.length;
17565
+ const entries = all.reverse().slice(offset, offset + limit);
17566
+ res.json({ total, offset, entries });
17567
+ } finally {
17568
+ logger.close();
17569
+ }
17570
+ } catch (err) {
17571
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17572
+ }
17573
+ });
17574
+ const prodPath = path26__default.default.resolve(__dirname$1, "../web/dist");
17575
+ const devPath = path26__default.default.resolve(__dirname$1, "../../web/dist");
17576
+ const webDistPath = fs24__default.default.existsSync(prodPath) ? prodPath : devPath;
17577
+ if (fs24__default.default.existsSync(webDistPath)) {
17020
17578
  this.app.use(express__default.default.static(webDistPath));
17021
17579
  this.app.get("*", (_req, res) => {
17022
- res.sendFile(path25__default.default.join(webDistPath, "index.html"));
17580
+ res.sendFile(path26__default.default.join(webDistPath, "index.html"));
17023
17581
  });
17024
17582
  } else {
17025
17583
  this.app.get("/", (_req, res) => {
@@ -17040,7 +17598,7 @@ init_constants();
17040
17598
  async function dashboardCommand(config, workspacePath = process.cwd()) {
17041
17599
  const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
17042
17600
  const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
17043
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
17601
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
17044
17602
  const server = new DashboardServer(config, store, workspacePath);
17045
17603
  server.watchRuntimeChanges();
17046
17604
  const gThis = globalThis;
@@ -17072,7 +17630,7 @@ init_constants();
17072
17630
  function makeIdentityCommand(workspacePath = process.cwd()) {
17073
17631
  const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
17074
17632
  identity.command("list").description("List all available identities").action(() => {
17075
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
17633
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
17076
17634
  try {
17077
17635
  const identities = store.listIdentities();
17078
17636
  console.log(chalk9__default.default.bold("\n Identities:"));
@@ -17092,7 +17650,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
17092
17650
  }
17093
17651
  });
17094
17652
  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) => {
17095
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
17653
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
17096
17654
  try {
17097
17655
  if (options.default) {
17098
17656
  const existingDefault = store.getDefaultIdentity();
@@ -17118,7 +17676,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
17118
17676
  }
17119
17677
  });
17120
17678
  identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
17121
- const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
17679
+ const store = new MemoryStore(path26__default.default.join(workspacePath, CASCADE_DB_FILE));
17122
17680
  try {
17123
17681
  const identities = store.listIdentities();
17124
17682
  const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
@@ -17246,11 +17804,11 @@ async function exportCommand(options = {}) {
17246
17804
  let store;
17247
17805
  try {
17248
17806
  const workspacePath = options.workspacePath ?? process.cwd();
17249
- const workspaceDbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
17250
- const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
17807
+ const workspaceDbPath = path26__default.default.join(workspacePath, CASCADE_DB_FILE);
17808
+ const globalDbPath = path26__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
17251
17809
  let dbPath = globalDbPath;
17252
17810
  try {
17253
- await fs9__default.default.access(workspaceDbPath);
17811
+ await fs10__default.default.access(workspaceDbPath);
17254
17812
  dbPath = workspaceDbPath;
17255
17813
  } catch {
17256
17814
  }
@@ -17289,8 +17847,8 @@ async function exportCommand(options = {}) {
17289
17847
  const ext = format === "json" ? ".json" : ".md";
17290
17848
  const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
17291
17849
  const defaultFile = `cascade-export-${safeName}${ext}`;
17292
- const outPath = options.output ? path25__default.default.resolve(options.output) : path25__default.default.join(process.cwd(), defaultFile);
17293
- await fs9__default.default.writeFile(outPath, content, "utf-8");
17850
+ const outPath = options.output ? path26__default.default.resolve(options.output) : path26__default.default.join(process.cwd(), defaultFile);
17851
+ await fs10__default.default.writeFile(outPath, content, "utf-8");
17294
17852
  spin.succeed(chalk9__default.default.green(`Exported to ${chalk9__default.default.white(outPath)}`));
17295
17853
  const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
17296
17854
  console.log();
@@ -17517,11 +18075,11 @@ async function statsCommand() {
17517
18075
  dotenv__default.default.config();
17518
18076
  function warnIfBuildIsStale() {
17519
18077
  try {
17520
- const here = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
18078
+ const here = path26__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
17521
18079
  for (const rel of ["..", "../.."]) {
17522
- const pkgPath = path25__default.default.join(here, rel, "package.json");
17523
- if (!fs23__default.default.existsSync(pkgPath)) continue;
17524
- const pkg = JSON.parse(fs23__default.default.readFileSync(pkgPath, "utf-8"));
18080
+ const pkgPath = path26__default.default.join(here, rel, "package.json");
18081
+ if (!fs24__default.default.existsSync(pkgPath)) continue;
18082
+ const pkg = JSON.parse(fs24__default.default.readFileSync(pkgPath, "utf-8"));
17525
18083
  if (pkg.name !== "cascade-ai") continue;
17526
18084
  if (pkg.version && pkg.version !== CASCADE_VERSION) {
17527
18085
  console.error(