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/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import Database from 'better-sqlite3';
2
- import path20 from 'path';
3
- import fs19 from 'fs';
2
+ import path21 from 'path';
3
+ import fs20 from 'fs';
4
4
  import os6 from 'os';
5
5
  import crypto3, { randomUUID, timingSafeEqual } from 'crypto';
6
6
  import EventEmitter from 'events';
@@ -58,12 +58,12 @@ var init_audit_logger = __esm({
58
58
  constructor(workspacePath, debugMode = false) {
59
59
  this.workspacePath = workspacePath;
60
60
  this.debugMode = debugMode;
61
- const cascadeDir = path20.join(workspacePath, ".cascade");
62
- if (!fs19.existsSync(cascadeDir)) {
63
- fs19.mkdirSync(cascadeDir, { recursive: true });
61
+ const cascadeDir = path21.join(workspacePath, ".cascade");
62
+ if (!fs20.existsSync(cascadeDir)) {
63
+ fs20.mkdirSync(cascadeDir, { recursive: true });
64
64
  }
65
- this.keyPath = path20.join(cascadeDir, "audit_log.key");
66
- this.dbPath = path20.join(cascadeDir, "audit_log.db");
65
+ this.keyPath = path21.join(cascadeDir, "audit_log.key");
66
+ this.dbPath = path21.join(cascadeDir, "audit_log.db");
67
67
  this.initEncryptionKey();
68
68
  this.db = new Database(this.dbPath);
69
69
  this.db.pragma("journal_mode = WAL");
@@ -93,11 +93,11 @@ var init_audit_logger = __esm({
93
93
  return crypto3.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
94
94
  }
95
95
  initEncryptionKey() {
96
- if (fs19.existsSync(this.keyPath)) {
97
- this.encryptionKey = fs19.readFileSync(this.keyPath);
96
+ if (fs20.existsSync(this.keyPath)) {
97
+ this.encryptionKey = fs20.readFileSync(this.keyPath);
98
98
  } else {
99
99
  this.encryptionKey = crypto3.randomBytes(32);
100
- fs19.writeFileSync(this.keyPath, this.encryptionKey);
100
+ fs20.writeFileSync(this.keyPath, this.encryptionKey);
101
101
  }
102
102
  }
103
103
  encrypt(text) {
@@ -175,9 +175,9 @@ var init_audit_logger = __esm({
175
175
  dumpDebugIfNeeded() {
176
176
  if (!this.debugMode) return;
177
177
  try {
178
- const dumpPath = path20.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
178
+ const dumpPath = path21.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
179
179
  const entries = this.getAllLogs();
180
- fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
180
+ fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
181
181
  } catch (err) {
182
182
  console.error("Failed to dump debug audit logs", err);
183
183
  }
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
190
190
  });
191
191
 
192
192
  // src/constants.ts
193
- var CASCADE_VERSION = "0.16.0";
193
+ var CASCADE_VERSION = "0.18.0";
194
194
  var CASCADE_CONFIG_DIR = ".cascade";
195
195
  var CASCADE_MD_FILE = "CASCADE.md";
196
196
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -202,6 +202,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
202
202
  var GLOBAL_CONFIG_DIR = ".cascade-ai";
203
203
  var GLOBAL_DB_FILE = "memory.db";
204
204
  var GLOBAL_KEYSTORE_FILE = "keystore.enc";
205
+ var GLOBAL_CREDENTIALS_FILE = "credentials.json";
205
206
  var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
206
207
  var DEFAULT_DASHBOARD_PORT = 4891;
207
208
  var DEFAULT_API_PORT = 4892;
@@ -2164,7 +2165,7 @@ function computeDelegationSavings(stats, t1Model) {
2164
2165
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
2165
2166
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2166
2167
  var FETCH_TIMEOUT_MS = 8e3;
2167
- var DEFAULT_CACHE_FILE = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2168
+ var DEFAULT_CACHE_FILE = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2168
2169
  function normalizeModelId(id) {
2169
2170
  let s = id.toLowerCase();
2170
2171
  const slash = s.lastIndexOf("/");
@@ -2309,7 +2310,7 @@ var LiveDataProvider = class {
2309
2310
  }
2310
2311
  async saveCache() {
2311
2312
  try {
2312
- await fs4.mkdir(path20.dirname(this.opts.cacheFile), { recursive: true });
2313
+ await fs4.mkdir(path21.dirname(this.opts.cacheFile), { recursive: true });
2313
2314
  const cache = {
2314
2315
  fetchedAt: this.fetchedAt,
2315
2316
  snapshot: this.snapshot ?? void 0,
@@ -4212,8 +4213,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4212
4213
  tierId: this.id,
4213
4214
  sessionId: this.taskId,
4214
4215
  requireApproval: false,
4215
- saveSnapshot: async (path21, content) => {
4216
- this.store?.addFileSnapshot(this.taskId, path21, content);
4216
+ saveSnapshot: async (path22, content) => {
4217
+ this.store?.addFileSnapshot(this.taskId, path22, content);
4217
4218
  },
4218
4219
  sendPeerSync: (to, syncType, content) => {
4219
4220
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -4383,7 +4384,7 @@ ${assignment.expectedOutput}`;
4383
4384
  const { promisify: promisify4 } = await import('util');
4384
4385
  const execAsync2 = promisify4(exec2);
4385
4386
  for (const artifactPath of artifactPaths) {
4386
- const absolutePath = path20.resolve(process.cwd(), artifactPath);
4387
+ const absolutePath = path21.resolve(process.cwd(), artifactPath);
4387
4388
  try {
4388
4389
  const stat = await fs4.stat(absolutePath);
4389
4390
  if (!stat.isFile()) {
@@ -4404,7 +4405,7 @@ ${assignment.expectedOutput}`;
4404
4405
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4405
4406
  continue;
4406
4407
  }
4407
- const ext = path20.extname(absolutePath).toLowerCase();
4408
+ const ext = path21.extname(absolutePath).toLowerCase();
4408
4409
  try {
4409
4410
  if (ext === ".ts" || ext === ".tsx") {
4410
4411
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -6486,16 +6487,16 @@ function resolveInWorkspace(workspaceRoot, input) {
6486
6487
  if (typeof input !== "string" || input.length === 0) {
6487
6488
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
6488
6489
  }
6489
- const root = path20.resolve(workspaceRoot);
6490
- const abs = path20.isAbsolute(input) ? path20.resolve(input) : path20.resolve(root, input);
6491
- const rel = path20.relative(root, abs);
6492
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20.isAbsolute(rel)) {
6490
+ const root = path21.resolve(workspaceRoot);
6491
+ const abs = path21.isAbsolute(input) ? path21.resolve(input) : path21.resolve(root, input);
6492
+ const rel = path21.relative(root, abs);
6493
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21.isAbsolute(rel)) {
6493
6494
  throw new WorkspaceSandboxError(input, root);
6494
6495
  }
6495
6496
  try {
6496
- const real = fs19.realpathSync(abs);
6497
- const realRel = path20.relative(root, real);
6498
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20.isAbsolute(realRel))) {
6497
+ const real = fs20.realpathSync(abs);
6498
+ const realRel = path21.relative(root, real);
6499
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21.isAbsolute(realRel))) {
6499
6500
  throw new WorkspaceSandboxError(input, root);
6500
6501
  }
6501
6502
  } catch (e) {
@@ -6556,7 +6557,7 @@ var FileWriteTool = class extends BaseTool {
6556
6557
  } catch {
6557
6558
  }
6558
6559
  }
6559
- await fs4.mkdir(path20.dirname(absPath), { recursive: true });
6560
+ await fs4.mkdir(path21.dirname(absPath), { recursive: true });
6560
6561
  await fs4.writeFile(absPath, content, "utf-8");
6561
6562
  return `Written ${content.length} characters to ${filePath}`;
6562
6563
  }
@@ -7054,7 +7055,7 @@ var ImageAnalyzeTool = class extends BaseTool {
7054
7055
  };
7055
7056
  async function fileToImageAttachment(filePath) {
7056
7057
  const data = await fs4.readFile(filePath);
7057
- const ext = path20.extname(filePath).toLowerCase();
7058
+ const ext = path21.extname(filePath).toLowerCase();
7058
7059
  const mimeMap = {
7059
7060
  ".jpg": "image/jpeg",
7060
7061
  ".jpeg": "image/jpeg",
@@ -7088,14 +7089,14 @@ var PDFCreateTool = class extends BaseTool {
7088
7089
  const filePath = input["path"];
7089
7090
  const content = input["content"];
7090
7091
  const title = input["title"];
7091
- const dir = path20.dirname(filePath);
7092
- if (!fs19.existsSync(dir)) {
7093
- fs19.mkdirSync(dir, { recursive: true });
7092
+ const dir = path21.dirname(filePath);
7093
+ if (!fs20.existsSync(dir)) {
7094
+ fs20.mkdirSync(dir, { recursive: true });
7094
7095
  }
7095
7096
  return new Promise((resolve, reject) => {
7096
7097
  try {
7097
7098
  const doc = new PDFDocument({ margin: 50 });
7098
- const stream = fs19.createWriteStream(filePath);
7099
+ const stream = fs20.createWriteStream(filePath);
7099
7100
  doc.pipe(stream);
7100
7101
  if (title) {
7101
7102
  doc.info["Title"] = title;
@@ -7173,22 +7174,22 @@ var CodeInterpreterTool = class extends BaseTool {
7173
7174
  }
7174
7175
  cmdPrefix = NODE_CMD;
7175
7176
  }
7176
- const tmpDir = path20.join(this.workspaceRoot, ".cascade", "tmp");
7177
- if (!fs19.existsSync(tmpDir)) {
7178
- fs19.mkdirSync(tmpDir, { recursive: true });
7177
+ const tmpDir = path21.join(this.workspaceRoot, ".cascade", "tmp");
7178
+ if (!fs20.existsSync(tmpDir)) {
7179
+ fs20.mkdirSync(tmpDir, { recursive: true });
7179
7180
  }
7180
7181
  const extension = language === "python" ? "py" : "js";
7181
7182
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
7182
- const filePath = path20.join(tmpDir, fileName);
7183
- fs19.writeFileSync(filePath, code, "utf-8");
7183
+ const filePath = path21.join(tmpDir, fileName);
7184
+ fs20.writeFileSync(filePath, code, "utf-8");
7184
7185
  const execArgs = [filePath, ...args];
7185
7186
  return new Promise((resolve) => {
7186
7187
  const startMs = Date.now();
7187
7188
  execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
7188
7189
  const duration = Date.now() - startMs;
7189
7190
  try {
7190
- if (fs19.existsSync(filePath)) {
7191
- fs19.unlinkSync(filePath);
7191
+ if (fs20.existsSync(filePath)) {
7192
+ fs20.unlinkSync(filePath);
7192
7193
  }
7193
7194
  } catch (cleanupErr) {
7194
7195
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -7467,7 +7468,7 @@ var GlobTool = class extends BaseTool {
7467
7468
  };
7468
7469
  async execute(input, _options) {
7469
7470
  const pattern = input["pattern"];
7470
- const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7471
+ const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7471
7472
  const matches = await glob(pattern, {
7472
7473
  cwd: searchPath,
7473
7474
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -7480,7 +7481,7 @@ var GlobTool = class extends BaseTool {
7480
7481
  const withMtime = await Promise.all(
7481
7482
  matches.map(async (rel) => {
7482
7483
  try {
7483
- const stat = await fs4.stat(path20.join(searchPath, rel));
7484
+ const stat = await fs4.stat(path21.join(searchPath, rel));
7484
7485
  return { rel, mtime: stat.mtimeMs };
7485
7486
  } catch {
7486
7487
  return { rel, mtime: 0 };
@@ -7529,7 +7530,7 @@ var GrepTool = class extends BaseTool {
7529
7530
  };
7530
7531
  async execute(input, _options) {
7531
7532
  const pattern = input["pattern"];
7532
- const searchPath = input["path"] ? path20.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7533
+ const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7533
7534
  const globPattern = input["glob"];
7534
7535
  const outputMode = input["output_mode"] ?? "content";
7535
7536
  const context = input["context"] ?? 0;
@@ -7583,12 +7584,12 @@ var GrepTool = class extends BaseTool {
7583
7584
  nodir: true
7584
7585
  });
7585
7586
  } catch {
7586
- files = [path20.relative(searchPath, searchPath) || "."];
7587
+ files = [path21.relative(searchPath, searchPath) || "."];
7587
7588
  }
7588
7589
  const results = [];
7589
7590
  let totalCount = 0;
7590
7591
  for (const rel of files) {
7591
- const abs = path20.join(searchPath, rel);
7592
+ const abs = path21.join(searchPath, rel);
7592
7593
  let content;
7593
7594
  try {
7594
7595
  content = await fs4.readFile(abs, "utf-8");
@@ -7950,10 +7951,10 @@ var ToolRegistry = class extends EventEmitter {
7950
7951
  }
7951
7952
  isIgnored(filePath) {
7952
7953
  if (!filePath) return false;
7953
- const abs = path20.resolve(this.workspaceRoot, filePath);
7954
- const rel = path20.relative(this.workspaceRoot, abs);
7955
- if (!rel || rel.startsWith("..") || path20.isAbsolute(rel)) return true;
7956
- const posixRel = rel.split(path20.sep).join("/");
7954
+ const abs = path21.resolve(this.workspaceRoot, filePath);
7955
+ const rel = path21.relative(this.workspaceRoot, abs);
7956
+ if (!rel || rel.startsWith("..") || path21.isAbsolute(rel)) return true;
7957
+ const posixRel = rel.split(path21.sep).join("/");
7957
7958
  return this.ignoreMatcher.ignores(posixRel);
7958
7959
  }
7959
7960
  };
@@ -8787,7 +8788,7 @@ var TaskAnalyzer = class {
8787
8788
  analysisCache.clear();
8788
8789
  }
8789
8790
  };
8790
- var DEFAULT_STATS_FILE = path20.join(os6.homedir(), ".cascade", "model-perf.json");
8791
+ var DEFAULT_STATS_FILE = path21.join(os6.homedir(), ".cascade", "model-perf.json");
8791
8792
  var ModelPerformanceTracker = class {
8792
8793
  stats = /* @__PURE__ */ new Map();
8793
8794
  featureStats = /* @__PURE__ */ new Map();
@@ -8819,7 +8820,7 @@ var ModelPerformanceTracker = class {
8819
8820
  }
8820
8821
  async save() {
8821
8822
  try {
8822
- await fs4.mkdir(path20.dirname(this.statsFile), { recursive: true });
8823
+ await fs4.mkdir(path21.dirname(this.statsFile), { recursive: true });
8823
8824
  const modelsObj = {};
8824
8825
  const featuresObj = {};
8825
8826
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -9358,7 +9359,7 @@ Required capability: ${description.slice(0, 300)}`;
9358
9359
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
9359
9360
  async loadPersistedTools() {
9360
9361
  if (!this.workspacePath || !this.persistEnabled) return;
9361
- const file = path20.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9362
+ const file = path21.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9362
9363
  try {
9363
9364
  const raw = await fs4.readFile(file, "utf-8");
9364
9365
  const specs = JSON.parse(raw);
@@ -9382,8 +9383,8 @@ Required capability: ${description.slice(0, 300)}`;
9382
9383
  }
9383
9384
  async persist() {
9384
9385
  if (!this.workspacePath || !this.persistEnabled) return;
9385
- const dir = path20.join(this.workspacePath, ".cascade");
9386
- const file = path20.join(dir, DYNAMIC_TOOLS_FILE);
9386
+ const dir = path21.join(this.workspacePath, ".cascade");
9387
+ const file = path21.join(dir, DYNAMIC_TOOLS_FILE);
9387
9388
  try {
9388
9389
  await fs4.mkdir(dir, { recursive: true });
9389
9390
  await fs4.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -9403,12 +9404,12 @@ var WorldStateDB = class {
9403
9404
  constructor(workspacePath, debugMode = false) {
9404
9405
  this.workspacePath = workspacePath;
9405
9406
  this.debugMode = debugMode;
9406
- const cascadeDir = path20.join(workspacePath, ".cascade");
9407
- if (!fs19.existsSync(cascadeDir)) {
9408
- fs19.mkdirSync(cascadeDir, { recursive: true });
9407
+ const cascadeDir = path21.join(workspacePath, ".cascade");
9408
+ if (!fs20.existsSync(cascadeDir)) {
9409
+ fs20.mkdirSync(cascadeDir, { recursive: true });
9409
9410
  }
9410
- this.keyPath = path20.join(cascadeDir, "world_state.key");
9411
- this.dbPath = path20.join(cascadeDir, "world_state.db");
9411
+ this.keyPath = path21.join(cascadeDir, "world_state.key");
9412
+ this.dbPath = path21.join(cascadeDir, "world_state.db");
9412
9413
  this.initEncryptionKey();
9413
9414
  this.db = new Database(this.dbPath);
9414
9415
  this.db.pragma("journal_mode = WAL");
@@ -9438,11 +9439,11 @@ var WorldStateDB = class {
9438
9439
  dbPath;
9439
9440
  encryptionKey;
9440
9441
  initEncryptionKey() {
9441
- if (fs19.existsSync(this.keyPath)) {
9442
- this.encryptionKey = fs19.readFileSync(this.keyPath);
9442
+ if (fs20.existsSync(this.keyPath)) {
9443
+ this.encryptionKey = fs20.readFileSync(this.keyPath);
9443
9444
  } else {
9444
9445
  this.encryptionKey = crypto3.randomBytes(32);
9445
- fs19.writeFileSync(this.keyPath, this.encryptionKey);
9446
+ fs20.writeFileSync(this.keyPath, this.encryptionKey);
9446
9447
  }
9447
9448
  }
9448
9449
  encrypt(text) {
@@ -9583,6 +9584,22 @@ var WorldStateDB = class {
9583
9584
  }
9584
9585
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
9585
9586
  }
9587
+ /**
9588
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
9589
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
9590
+ * delete — users can prune what the planner remembers about their project.
9591
+ */
9592
+ deleteFact(entity, relation) {
9593
+ const e = normalizeKey(entity);
9594
+ const r = normalizeKey(relation);
9595
+ if (!e || !r) return false;
9596
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
9597
+ return result.changes > 0;
9598
+ }
9599
+ /** Delete every fact. Returns how many were removed. */
9600
+ clearFacts() {
9601
+ return this.db.prepare("DELETE FROM facts").run().changes;
9602
+ }
9586
9603
  // ── Export / Import ──────────────────────────
9587
9604
  //
9588
9605
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -9631,9 +9648,9 @@ var WorldStateDB = class {
9631
9648
  dumpDebugIfNeeded() {
9632
9649
  if (!this.debugMode) return;
9633
9650
  try {
9634
- const dumpPath = path20.join(os6.tmpdir(), "cascade_world_state_debug.json");
9651
+ const dumpPath = path21.join(os6.tmpdir(), "cascade_world_state_debug.json");
9635
9652
  const entries = this.getAllEntries();
9636
- fs19.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9653
+ fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9637
9654
  } catch (err) {
9638
9655
  console.error("Failed to dump debug world state", err);
9639
9656
  }
@@ -10543,7 +10560,7 @@ var Keystore = class {
10543
10560
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
10544
10561
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
10545
10562
  this.backend = "keytar";
10546
- if (password && fs19.existsSync(this.storePath)) {
10563
+ if (password && fs20.existsSync(this.storePath)) {
10547
10564
  try {
10548
10565
  const fileEntries = this.decryptFile(password);
10549
10566
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -10562,7 +10579,7 @@ var Keystore = class {
10562
10579
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
10563
10580
  );
10564
10581
  }
10565
- if (!fs19.existsSync(this.storePath)) {
10582
+ if (!fs20.existsSync(this.storePath)) {
10566
10583
  const salt = crypto3.randomBytes(SALT_LEN);
10567
10584
  this.masterKey = this.deriveKey(password, salt);
10568
10585
  this.writeWithSalt({}, salt);
@@ -10576,7 +10593,7 @@ var Keystore = class {
10576
10593
  }
10577
10594
  /** Synchronous legacy unlock kept for AES-only environments. */
10578
10595
  unlockSync(password) {
10579
- if (!fs19.existsSync(this.storePath)) {
10596
+ if (!fs20.existsSync(this.storePath)) {
10580
10597
  const salt = crypto3.randomBytes(SALT_LEN);
10581
10598
  this.masterKey = this.deriveKey(password, salt);
10582
10599
  this.writeWithSalt({}, salt);
@@ -10634,7 +10651,7 @@ var Keystore = class {
10634
10651
  }
10635
10652
  }
10636
10653
  decryptFile(password, knownSalt) {
10637
- if (!fs19.existsSync(this.storePath)) return {};
10654
+ if (!fs20.existsSync(this.storePath)) return {};
10638
10655
  try {
10639
10656
  const { salt, ciphertext, iv, tag } = this.readRaw();
10640
10657
  const useSalt = knownSalt ?? salt;
@@ -10656,8 +10673,8 @@ var Keystore = class {
10656
10673
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10657
10674
  const tag = cipher.getAuthTag();
10658
10675
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
10659
- fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
10660
- fs19.writeFileSync(this.storePath, out, { mode: 384 });
10676
+ fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10677
+ fs20.writeFileSync(this.storePath, out, { mode: 384 });
10661
10678
  }
10662
10679
  writeWithSalt(data, salt) {
10663
10680
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -10667,11 +10684,11 @@ var Keystore = class {
10667
10684
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10668
10685
  const tag = cipher.getAuthTag();
10669
10686
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
10670
- fs19.mkdirSync(path20.dirname(this.storePath), { recursive: true });
10671
- fs19.writeFileSync(this.storePath, out, { mode: 384 });
10687
+ fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
10688
+ fs20.writeFileSync(this.storePath, out, { mode: 384 });
10672
10689
  }
10673
10690
  readRaw() {
10674
- const buf = fs19.readFileSync(this.storePath);
10691
+ const buf = fs20.readFileSync(this.storePath);
10675
10692
  let offset = 0;
10676
10693
  const salt = buf.subarray(offset, offset + SALT_LEN);
10677
10694
  offset += SALT_LEN;
@@ -10704,7 +10721,7 @@ var CascadeIgnore = class {
10704
10721
  ]);
10705
10722
  }
10706
10723
  async load(workspacePath) {
10707
- const filePath = path20.join(workspacePath, ".cascadeignore");
10724
+ const filePath = path21.join(workspacePath, ".cascadeignore");
10708
10725
  try {
10709
10726
  const content = await fs4.readFile(filePath, "utf-8");
10710
10727
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -10715,7 +10732,7 @@ var CascadeIgnore = class {
10715
10732
  }
10716
10733
  isIgnored(filePath, workspacePath) {
10717
10734
  try {
10718
- const relative = workspacePath ? path20.relative(workspacePath, filePath) : filePath;
10735
+ const relative = workspacePath ? path21.relative(workspacePath, filePath) : filePath;
10719
10736
  return this.ig.ignores(relative);
10720
10737
  } catch {
10721
10738
  return false;
@@ -10726,7 +10743,7 @@ var CascadeIgnore = class {
10726
10743
  }
10727
10744
  };
10728
10745
  async function loadCascadeMd(workspacePath) {
10729
- const filePath = path20.join(workspacePath, "CASCADE.md");
10746
+ const filePath = path21.join(workspacePath, "CASCADE.md");
10730
10747
  try {
10731
10748
  const raw = await fs4.readFile(filePath, "utf-8");
10732
10749
  return parseCascadeMd(raw);
@@ -10757,7 +10774,7 @@ ${raw.trim()}`;
10757
10774
  var MemoryStore = class _MemoryStore {
10758
10775
  db;
10759
10776
  constructor(dbPath) {
10760
- fs19.mkdirSync(path20.dirname(dbPath), { recursive: true });
10777
+ fs20.mkdirSync(path21.dirname(dbPath), { recursive: true });
10761
10778
  try {
10762
10779
  this.db = new Database(dbPath, { timeout: 5e3 });
10763
10780
  this.db.pragma("journal_mode = WAL");
@@ -11592,6 +11609,60 @@ Original error: ${err.message}`
11592
11609
  };
11593
11610
  }
11594
11611
  };
11612
+ function credentialsPath(globalDir) {
11613
+ return path21.join(globalDir, GLOBAL_CREDENTIALS_FILE);
11614
+ }
11615
+ function providerKey(p) {
11616
+ if (p.type === "azure") {
11617
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
11618
+ }
11619
+ return p.type;
11620
+ }
11621
+ function isPersistable(p) {
11622
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
11623
+ }
11624
+ function loadGlobalCredentials(globalDir) {
11625
+ try {
11626
+ const raw = fs20.readFileSync(credentialsPath(globalDir), "utf-8");
11627
+ const parsed = JSON.parse(raw);
11628
+ if (!Array.isArray(parsed.providers)) return [];
11629
+ return parsed.providers.filter(
11630
+ (p) => Boolean(p) && typeof p.type === "string"
11631
+ );
11632
+ } catch {
11633
+ return [];
11634
+ }
11635
+ }
11636
+ function saveGlobalCredentials(globalDir, providers) {
11637
+ const filePath = credentialsPath(globalDir);
11638
+ fs20.mkdirSync(globalDir, { recursive: true, mode: 448 });
11639
+ const body = { version: 1, providers: providers.filter(isPersistable) };
11640
+ const tmp = `${filePath}.tmp`;
11641
+ fs20.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
11642
+ fs20.renameSync(tmp, filePath);
11643
+ try {
11644
+ fs20.chmodSync(filePath, 384);
11645
+ } catch {
11646
+ }
11647
+ }
11648
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
11649
+ const merged = [...workspaceProviders];
11650
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
11651
+ for (const g of globalProviders) {
11652
+ const existing = byKey.get(providerKey(g));
11653
+ if (!existing) {
11654
+ merged.push({ ...g });
11655
+ byKey.set(providerKey(g), merged[merged.length - 1]);
11656
+ continue;
11657
+ }
11658
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
11659
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
11660
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
11661
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
11662
+ if (!existing.label && g.label) existing.label = g.label;
11663
+ }
11664
+ return merged;
11665
+ }
11595
11666
 
11596
11667
  // src/config/index.ts
11597
11668
  var ConfigManager = class {
@@ -11602,18 +11673,20 @@ var ConfigManager = class {
11602
11673
  cascadeMd = null;
11603
11674
  workspacePath;
11604
11675
  globalDir;
11605
- constructor(workspacePath = process.cwd()) {
11676
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
11677
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
11606
11678
  this.workspacePath = workspacePath;
11607
- this.globalDir = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR);
11679
+ this.globalDir = globalDirOverride ?? path21.join(os6.homedir(), GLOBAL_CONFIG_DIR);
11608
11680
  }
11609
11681
  async load() {
11610
11682
  this.config = await this.loadConfig();
11611
11683
  this.ignore = new CascadeIgnore();
11612
11684
  await this.ignore.load(this.workspacePath);
11613
11685
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
11614
- this.keystore = new Keystore(path20.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11615
- this.store = new MemoryStore(path20.join(this.workspacePath, CASCADE_DB_FILE));
11686
+ this.keystore = new Keystore(path21.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11687
+ this.store = new MemoryStore(path21.join(this.workspacePath, CASCADE_DB_FILE));
11616
11688
  await this.injectEnvKeys();
11689
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
11617
11690
  await this.ensureDefaultIdentity();
11618
11691
  }
11619
11692
  getConfig() {
@@ -11635,9 +11708,14 @@ var ConfigManager = class {
11635
11708
  return this.workspacePath;
11636
11709
  }
11637
11710
  async save() {
11638
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
11639
- await fs4.mkdir(path20.dirname(configPath), { recursive: true });
11711
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
11712
+ await fs4.mkdir(path21.dirname(configPath), { recursive: true });
11640
11713
  await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
11714
+ try {
11715
+ saveGlobalCredentials(this.globalDir, this.config.providers);
11716
+ } catch (err) {
11717
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
11718
+ }
11641
11719
  }
11642
11720
  async updateConfig(updates) {
11643
11721
  this.config = validateConfig({ ...this.config, ...updates });
@@ -11660,7 +11738,7 @@ var ConfigManager = class {
11660
11738
  return configProvider?.apiKey;
11661
11739
  }
11662
11740
  async loadConfig() {
11663
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
11741
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
11664
11742
  try {
11665
11743
  const raw = await fs4.readFile(configPath, "utf-8");
11666
11744
  return validateConfig(JSON.parse(raw));
@@ -11767,6 +11845,11 @@ function authMiddleware(secret, required = true) {
11767
11845
  }
11768
11846
  const user = verifyToken(token, secret);
11769
11847
  if (!user) {
11848
+ if (!required) {
11849
+ req.user = void 0;
11850
+ next();
11851
+ return;
11852
+ }
11770
11853
  res.status(401).json({ error: "Invalid or expired token" });
11771
11854
  return;
11772
11855
  }
@@ -11899,6 +11982,25 @@ var DashboardSocket = class {
11899
11982
  });
11900
11983
  });
11901
11984
  }
11985
+ /**
11986
+ * Boardroom plan decisions from a connected client. The desktop shows a
11987
+ * plan-review modal on `plan:approval-required` and answers here; the
11988
+ * server routes the decision into the paused run via resolvePlanApproval.
11989
+ */
11990
+ onPlanDecision(callback) {
11991
+ this.io.on("connection", (socket) => {
11992
+ socket.on("plan:decision", (payload) => {
11993
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
11994
+ callback({
11995
+ sessionId: payload.sessionId,
11996
+ approved: payload.approved,
11997
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
11998
+ editedPlan: payload.editedPlan
11999
+ });
12000
+ }
12001
+ });
12002
+ });
12003
+ }
11902
12004
  onSessionSteer(callback) {
11903
12005
  this.io.on("connection", (socket) => {
11904
12006
  socket.on("session:steer", (payload) => {
@@ -11940,7 +12042,123 @@ var DashboardSocket = class {
11940
12042
  this.io.close();
11941
12043
  }
11942
12044
  };
11943
- var __dirname$1 = path20.dirname(fileURLToPath(import.meta.url));
12045
+ var TaskScheduler = class {
12046
+ cronJobs = /* @__PURE__ */ new Map();
12047
+ store;
12048
+ runner;
12049
+ constructor(store, runner) {
12050
+ this.store = store;
12051
+ this.runner = runner;
12052
+ }
12053
+ start() {
12054
+ const tasks = this.store.listScheduledTasks();
12055
+ for (const task of tasks) {
12056
+ if (task.enabled) this.schedule(task);
12057
+ }
12058
+ }
12059
+ stop() {
12060
+ for (const job of this.cronJobs.values()) job.stop();
12061
+ this.cronJobs.clear();
12062
+ }
12063
+ schedule(task) {
12064
+ if (!cron.validate(task.cronExpression)) {
12065
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
12066
+ }
12067
+ const existing = this.cronJobs.get(task.id);
12068
+ if (existing) {
12069
+ try {
12070
+ existing.stop();
12071
+ } catch {
12072
+ }
12073
+ }
12074
+ const job = cron.schedule(task.cronExpression, async () => {
12075
+ try {
12076
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
12077
+ this.store.saveScheduledTask(task);
12078
+ await this.runner(task);
12079
+ } catch (err) {
12080
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
12081
+ }
12082
+ }, { timezone: "UTC" });
12083
+ this.cronJobs.set(task.id, job);
12084
+ }
12085
+ unschedule(taskId) {
12086
+ this.cronJobs.get(taskId)?.stop();
12087
+ this.cronJobs.delete(taskId);
12088
+ }
12089
+ add(task) {
12090
+ this.store.saveScheduledTask(task);
12091
+ if (task.enabled) this.schedule(task);
12092
+ }
12093
+ remove(taskId) {
12094
+ this.unschedule(taskId);
12095
+ this.store.deleteScheduledTask(taskId);
12096
+ }
12097
+ list() {
12098
+ return this.store.listScheduledTasks();
12099
+ }
12100
+ isRunning(taskId) {
12101
+ return this.cronJobs.has(taskId);
12102
+ }
12103
+ static validateCron(expression) {
12104
+ return cron.validate(expression);
12105
+ }
12106
+ };
12107
+
12108
+ // src/dashboard/cost-stats.ts
12109
+ var DAY_MS = 24 * 60 * 60 * 1e3;
12110
+ function utcDateKey(d) {
12111
+ return d.toISOString().slice(0, 10);
12112
+ }
12113
+ function aggregateCostStats(sessions, opts = {}) {
12114
+ const days = Math.max(1, opts.days ?? 30);
12115
+ const topN = Math.max(1, opts.topN ?? 8);
12116
+ const now = opts.now ?? /* @__PURE__ */ new Date();
12117
+ const buckets = /* @__PURE__ */ new Map();
12118
+ for (let i = days - 1; i >= 0; i--) {
12119
+ const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
12120
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
12121
+ }
12122
+ let totalCostUsd = 0;
12123
+ let totalTokens = 0;
12124
+ let totalRuns = 0;
12125
+ for (const s of sessions) {
12126
+ const cost = s.metadata?.totalCostUsd ?? 0;
12127
+ const tokens = s.metadata?.totalTokens ?? 0;
12128
+ const runs = s.metadata?.taskCount ?? 0;
12129
+ totalCostUsd += cost;
12130
+ totalTokens += tokens;
12131
+ totalRuns += runs;
12132
+ const when = new Date(s.updatedAt || s.createdAt);
12133
+ if (!Number.isNaN(when.getTime())) {
12134
+ const bucket = buckets.get(utcDateKey(when));
12135
+ if (bucket) {
12136
+ bucket.costUsd += cost;
12137
+ bucket.tokens += tokens;
12138
+ bucket.runs += runs;
12139
+ }
12140
+ }
12141
+ }
12142
+ 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) => ({
12143
+ sessionId: s.id,
12144
+ title: s.title,
12145
+ costUsd: s.metadata?.totalCostUsd ?? 0,
12146
+ tokens: s.metadata?.totalTokens ?? 0,
12147
+ runs: s.metadata?.taskCount ?? 0,
12148
+ updatedAt: s.updatedAt
12149
+ }));
12150
+ return {
12151
+ totalCostUsd,
12152
+ totalTokens,
12153
+ totalSessions: sessions.length,
12154
+ totalRuns,
12155
+ perDay: [...buckets.values()],
12156
+ topSessions
12157
+ };
12158
+ }
12159
+
12160
+ // src/dashboard/server.ts
12161
+ var __dirname$1 = path21.dirname(fileURLToPath(import.meta.url));
11944
12162
  var DashboardServer = class {
11945
12163
  app;
11946
12164
  httpServer;
@@ -11965,6 +12183,14 @@ var DashboardServer = class {
11965
12183
  * map is how that answer reaches the run that's blocked on it.
11966
12184
  */
11967
12185
  pendingApprovals = /* @__PURE__ */ new Map();
12186
+ /**
12187
+ * The orchestration decision trail ("why") of each session's most recent
12188
+ * run — captured when the run ends so the desktop's Why panel can show it
12189
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
12190
+ */
12191
+ whyBySession = /* @__PURE__ */ new Map();
12192
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
12193
+ scheduler;
11968
12194
  port;
11969
12195
  host;
11970
12196
  workspacePath;
@@ -11982,6 +12208,7 @@ var DashboardServer = class {
11982
12208
  secret: this.dashboardSecret,
11983
12209
  corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
11984
12210
  });
12211
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
11985
12212
  this.setupMiddleware();
11986
12213
  this.setupRoutes();
11987
12214
  this.socket.onSessionRate((sessionId, rating) => {
@@ -12045,6 +12272,9 @@ var DashboardServer = class {
12045
12272
  cascade.on("peer:message", (e) => {
12046
12273
  this.socket.emitPeerMessage(e);
12047
12274
  });
12275
+ cascade.on("plan:approval-required", (e) => {
12276
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
12277
+ });
12048
12278
  try {
12049
12279
  const result = await cascade.run({
12050
12280
  prompt: runPrompt,
@@ -12052,7 +12282,8 @@ var DashboardServer = class {
12052
12282
  approvalCallback: this.makeApprovalCallback(sessionId)
12053
12283
  });
12054
12284
  this.recordSessionTask(sessionId, result.taskId);
12055
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
12285
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
12286
+ this.captureWhy(sessionId, cascade, result);
12056
12287
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
12057
12288
  this.socket.broadcast("cost:update", {
12058
12289
  sessionId,
@@ -12062,6 +12293,7 @@ var DashboardServer = class {
12062
12293
  this.throttledBroadcast("workspace");
12063
12294
  } catch (err) {
12064
12295
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
12296
+ this.captureWhy(sessionId, cascade);
12065
12297
  this.socket.emitToSocket(socketId, "session:error", {
12066
12298
  sessionId,
12067
12299
  error: err instanceof Error ? err.message : String(err)
@@ -12072,9 +12304,17 @@ var DashboardServer = class {
12072
12304
  this.denyPendingApprovals(sessionId);
12073
12305
  }
12074
12306
  });
12307
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
12308
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
12309
+ approved,
12310
+ note,
12311
+ editedPlan
12312
+ );
12313
+ });
12075
12314
  this.socket.onSessionHalt((sessionId) => {
12076
12315
  this.activeControllers.get(sessionId)?.abort();
12077
12316
  this.denyPendingApprovals(sessionId);
12317
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
12078
12318
  });
12079
12319
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
12080
12320
  const pending = this.pendingApprovals.get(requestId);
@@ -12107,12 +12347,21 @@ var DashboardServer = class {
12107
12347
  resolve();
12108
12348
  });
12109
12349
  });
12350
+ try {
12351
+ this.scheduler.start();
12352
+ } catch (err) {
12353
+ console.warn("[dashboard] failed to start task scheduler:", err);
12354
+ }
12110
12355
  }
12111
12356
  async stop() {
12112
12357
  if (this.broadcastTimer) {
12113
12358
  clearTimeout(this.broadcastTimer);
12114
12359
  this.broadcastTimer = null;
12115
12360
  }
12361
+ try {
12362
+ this.scheduler.stop();
12363
+ } catch {
12364
+ }
12116
12365
  this.socket.close();
12117
12366
  try {
12118
12367
  this.globalStore?.close();
@@ -12143,12 +12392,17 @@ var DashboardServer = class {
12143
12392
  */
12144
12393
  persistConfig() {
12145
12394
  try {
12146
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
12147
- fs19.mkdirSync(path20.dirname(configPath), { recursive: true });
12148
- fs19.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12395
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
12396
+ fs20.mkdirSync(path21.dirname(configPath), { recursive: true });
12397
+ fs20.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12149
12398
  } catch (err) {
12150
12399
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
12151
12400
  }
12401
+ try {
12402
+ saveGlobalCredentials(path21.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
12403
+ } catch (err) {
12404
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
12405
+ }
12152
12406
  }
12153
12407
  /**
12154
12408
  * Produce a stable dashboard JWT signing secret.
@@ -12160,15 +12414,15 @@ var DashboardServer = class {
12160
12414
  resolveDashboardSecret() {
12161
12415
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
12162
12416
  if (fromConfig) return fromConfig;
12163
- const secretPath = path20.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12417
+ const secretPath = path21.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12164
12418
  try {
12165
- if (fs19.existsSync(secretPath)) {
12166
- const existing = fs19.readFileSync(secretPath, "utf-8").trim();
12419
+ if (fs20.existsSync(secretPath)) {
12420
+ const existing = fs20.readFileSync(secretPath, "utf-8").trim();
12167
12421
  if (existing.length >= 16) return existing;
12168
12422
  }
12169
12423
  const generated = randomUUID();
12170
- fs19.mkdirSync(path20.dirname(secretPath), { recursive: true });
12171
- fs19.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12424
+ fs20.mkdirSync(path21.dirname(secretPath), { recursive: true });
12425
+ fs20.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12172
12426
  if (this.config.dashboard.auth) {
12173
12427
  console.warn(
12174
12428
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -12195,7 +12449,7 @@ var DashboardServer = class {
12195
12449
  // ── Setup ─────────────────────────────────────
12196
12450
  getGlobalStore() {
12197
12451
  if (!this.globalStore) {
12198
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12452
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12199
12453
  this.globalStore = new MemoryStore(globalDbPath);
12200
12454
  }
12201
12455
  return this.globalStore;
@@ -12233,16 +12487,60 @@ var DashboardServer = class {
12233
12487
  return title;
12234
12488
  }
12235
12489
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
12236
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
12490
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
12237
12491
  try {
12238
12492
  if (reply && reply.trim()) {
12239
12493
  this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
12240
12494
  }
12495
+ if (result) {
12496
+ const session = this.store.getSession(sessionId);
12497
+ if (session) {
12498
+ this.store.updateSession(sessionId, {
12499
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
12500
+ metadata: {
12501
+ ...session.metadata,
12502
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
12503
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
12504
+ taskCount: session.metadata.taskCount + 1
12505
+ }
12506
+ });
12507
+ }
12508
+ }
12241
12509
  } catch (err) {
12242
12510
  console.warn("[dashboard] failed to persist run end:", err);
12243
12511
  }
12244
12512
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
12245
12513
  }
12514
+ /**
12515
+ * Capture the run's decision trail + router economics ("why") and broadcast
12516
+ * it so the desktop's Why panel updates live; kept per-session for the
12517
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
12518
+ */
12519
+ captureWhy(sessionId, cascade, result) {
12520
+ try {
12521
+ const stats = cascade.getRouter().getStats();
12522
+ const savings = cascade.getRouter().getDelegationSavings();
12523
+ const report = {
12524
+ sessionId,
12525
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
12526
+ decisions: cascade.getDecisionLog(),
12527
+ savedUsd: savings.savedUsd,
12528
+ savedPct: savings.savedPct,
12529
+ totalCostUsd: stats.totalCostUsd,
12530
+ totalTokens: stats.totalTokens,
12531
+ costByTier: stats.costByTier,
12532
+ durationMs: result?.durationMs
12533
+ };
12534
+ this.whyBySession.set(sessionId, report);
12535
+ if (this.whyBySession.size > 50) {
12536
+ const oldest = this.whyBySession.keys().next().value;
12537
+ if (oldest) this.whyBySession.delete(oldest);
12538
+ }
12539
+ this.socket.broadcast("run:why", report);
12540
+ } catch (err) {
12541
+ console.warn("[dashboard] failed to capture decision trail:", err);
12542
+ }
12543
+ }
12246
12544
  /**
12247
12545
  * Route steering text into running Cascade instances. Targets the given
12248
12546
  * session, or every active session when none is specified (the desktop
@@ -12272,6 +12570,52 @@ var DashboardServer = class {
12272
12570
  this.pendingApprovals.set(request.id, { resolve, sessionId });
12273
12571
  });
12274
12572
  }
12573
+ /**
12574
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
12575
+ * tool approvals are auto-granted since nobody may be watching when the cron
12576
+ * fires — the Schedules UI states this. Events broadcast to every connected
12577
+ * client so an open desktop sees the run appear live.
12578
+ */
12579
+ async runScheduledTask(task) {
12580
+ const sessionId = randomUUID();
12581
+ const prompt = task.prompt;
12582
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
12583
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
12584
+ this.activeSessions.set(sessionId, cascade);
12585
+ cascade.on("tier:status", (e) => {
12586
+ this.socket.broadcast("tier:status", { sessionId, ...e });
12587
+ });
12588
+ cascade.on("peer:message", (e) => {
12589
+ this.socket.emitPeerMessage(e);
12590
+ });
12591
+ try {
12592
+ const result = await cascade.run({
12593
+ prompt,
12594
+ identityId: task.identityId,
12595
+ approvalCallback: async () => ({ approved: true, always: false })
12596
+ });
12597
+ this.recordSessionTask(sessionId, result.taskId);
12598
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
12599
+ this.captureWhy(sessionId, cascade, result);
12600
+ this.socket.broadcast("session:complete", { sessionId, result });
12601
+ this.socket.broadcast("cost:update", {
12602
+ sessionId,
12603
+ totalTokens: result.usage.totalTokens,
12604
+ totalCostUsd: result.usage.estimatedCostUsd
12605
+ });
12606
+ this.throttledBroadcast("workspace");
12607
+ } catch (err) {
12608
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
12609
+ this.captureWhy(sessionId, cascade);
12610
+ this.socket.broadcast("session:error", {
12611
+ sessionId,
12612
+ error: err instanceof Error ? err.message : String(err)
12613
+ });
12614
+ throw err;
12615
+ } finally {
12616
+ this.activeSessions.delete(sessionId);
12617
+ }
12618
+ }
12275
12619
  /** Deny + clear any approvals still pending for a session (run end / abort). */
12276
12620
  denyPendingApprovals(sessionId) {
12277
12621
  for (const [id, pending] of this.pendingApprovals) {
@@ -12374,12 +12718,12 @@ ${prompt}`;
12374
12718
  }
12375
12719
  }
12376
12720
  watchRuntimeChanges() {
12377
- const workspaceDbPath = path20.join(this.workspacePath, CASCADE_DB_FILE);
12378
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12721
+ const workspaceDbPath = path21.join(this.workspacePath, CASCADE_DB_FILE);
12722
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12379
12723
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
12380
12724
  for (const watchPath of watchPaths) {
12381
- if (!fs19.existsSync(watchPath)) continue;
12382
- fs19.watchFile(watchPath, { interval: 3e3 }, () => {
12725
+ if (!fs20.existsSync(watchPath)) continue;
12726
+ fs20.watchFile(watchPath, { interval: 3e3 }, () => {
12383
12727
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
12384
12728
  });
12385
12729
  }
@@ -12501,7 +12845,7 @@ ${prompt}`;
12501
12845
  const sessionId = req.params.id;
12502
12846
  this.store.deleteSession(sessionId);
12503
12847
  this.store.deleteRuntimeSession(sessionId);
12504
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12848
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12505
12849
  const globalStore = new MemoryStore(globalDbPath);
12506
12850
  try {
12507
12851
  globalStore.deleteRuntimeSession(sessionId);
@@ -12594,7 +12938,7 @@ ${prompt}`;
12594
12938
  });
12595
12939
  this.app.delete("/api/sessions", auth, (req, res) => {
12596
12940
  const body = req.body;
12597
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12941
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12598
12942
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
12599
12943
  const globalStore = new MemoryStore(globalDbPath);
12600
12944
  try {
@@ -12617,7 +12961,7 @@ ${prompt}`;
12617
12961
  });
12618
12962
  this.app.delete("/api/runtime", auth, (_req, res) => {
12619
12963
  this.store.deleteAllRuntimeNodes();
12620
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12964
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12621
12965
  const globalStore = new MemoryStore(globalDbPath);
12622
12966
  try {
12623
12967
  globalStore.deleteAllRuntimeNodes();
@@ -12703,12 +13047,12 @@ ${prompt}`;
12703
13047
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
12704
13048
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
12705
13049
  try {
12706
- const configPath = path20.join(this.workspacePath, CASCADE_CONFIG_FILE);
12707
- const existing = fs19.existsSync(configPath) ? JSON.parse(fs19.readFileSync(configPath, "utf-8")) : {};
13050
+ const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
13051
+ const existing = fs20.existsSync(configPath) ? JSON.parse(fs20.readFileSync(configPath, "utf-8")) : {};
12708
13052
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
12709
13053
  const tmp = configPath + ".tmp";
12710
- fs19.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
12711
- fs19.renameSync(tmp, configPath);
13054
+ fs20.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13055
+ fs20.renameSync(tmp, configPath);
12712
13056
  res.json({ ok: true });
12713
13057
  } catch (err) {
12714
13058
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -12736,7 +13080,7 @@ ${prompt}`;
12736
13080
  this.app.get("/api/runtime", auth, (req, res) => {
12737
13081
  const scope = req.query["scope"] ?? "workspace";
12738
13082
  if (scope === "global") {
12739
- const globalDbPath = path20.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13083
+ const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12740
13084
  const globalStore = new MemoryStore(globalDbPath);
12741
13085
  try {
12742
13086
  res.json({
@@ -12784,6 +13128,9 @@ ${prompt}`;
12784
13128
  cascade.on("peer:message", (e) => {
12785
13129
  this.socket.emitPeerMessage(e);
12786
13130
  });
13131
+ cascade.on("plan:approval-required", (e) => {
13132
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
13133
+ });
12787
13134
  try {
12788
13135
  const result = await cascade.run({
12789
13136
  prompt: runPrompt,
@@ -12791,7 +13138,8 @@ ${prompt}`;
12791
13138
  approvalCallback: this.makeApprovalCallback(sessionId)
12792
13139
  });
12793
13140
  this.recordSessionTask(sessionId, result.taskId);
12794
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
13141
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
13142
+ this.captureWhy(sessionId, cascade, result);
12795
13143
  this.socket.broadcast("cost:update", {
12796
13144
  sessionId,
12797
13145
  totalTokens: result.usage.totalTokens,
@@ -12801,6 +13149,7 @@ ${prompt}`;
12801
13149
  this.throttledBroadcast("workspace");
12802
13150
  } catch (err) {
12803
13151
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
13152
+ this.captureWhy(sessionId, cascade);
12804
13153
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
12805
13154
  sessionId,
12806
13155
  error: err instanceof Error ? err.message : String(err)
@@ -12822,13 +13171,217 @@ ${prompt}`;
12822
13171
  }))
12823
13172
  });
12824
13173
  });
12825
- const prodPath = path20.resolve(__dirname$1, "../web/dist");
12826
- const devPath = path20.resolve(__dirname$1, "../../web/dist");
12827
- const webDistPath = fs19.existsSync(prodPath) ? prodPath : devPath;
12828
- if (fs19.existsSync(webDistPath)) {
13174
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
13175
+ const report = this.whyBySession.get(req.params.id);
13176
+ if (!report) {
13177
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
13178
+ return;
13179
+ }
13180
+ res.json(report);
13181
+ });
13182
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
13183
+ const sessionId = req.params.id;
13184
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
13185
+ const before = /* @__PURE__ */ new Map();
13186
+ for (const taskId of taskIds) {
13187
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
13188
+ if (!before.has(filePath)) before.set(filePath, content);
13189
+ }
13190
+ }
13191
+ const { readFile } = await import('fs/promises');
13192
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
13193
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
13194
+ let after = "";
13195
+ let missing = false;
13196
+ try {
13197
+ const buf = await readFile(filePath);
13198
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
13199
+ } catch {
13200
+ missing = true;
13201
+ }
13202
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
13203
+ }));
13204
+ res.json({ sessionId, changes: changes.filter((c) => c.changed) });
13205
+ });
13206
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
13207
+ const sessionId = req.params.id;
13208
+ const body = req.body;
13209
+ if (!body.filePath || typeof body.filePath !== "string") {
13210
+ res.status(400).json({ error: "filePath is required" });
13211
+ return;
13212
+ }
13213
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
13214
+ let content;
13215
+ for (const taskId of taskIds) {
13216
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
13217
+ if (snap) {
13218
+ content = snap.content;
13219
+ break;
13220
+ }
13221
+ }
13222
+ if (content === void 0) {
13223
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
13224
+ return;
13225
+ }
13226
+ try {
13227
+ const { writeFile } = await import('fs/promises');
13228
+ await writeFile(body.filePath, content, "utf-8");
13229
+ res.json({ ok: true, filePath: body.filePath });
13230
+ } catch (err) {
13231
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13232
+ }
13233
+ });
13234
+ this.app.get("/api/costs", auth, (_req, res) => {
13235
+ try {
13236
+ const sessions = this.store.listSessions(void 0, 1e3);
13237
+ const budget = this.config.budget ?? { warnAtPct: 80 };
13238
+ res.json({
13239
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
13240
+ budget: {
13241
+ dailyBudgetUsd: budget.dailyBudgetUsd,
13242
+ sessionBudgetUsd: budget.sessionBudgetUsd,
13243
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
13244
+ }
13245
+ });
13246
+ } catch (err) {
13247
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13248
+ }
13249
+ });
13250
+ this.app.get("/api/schedules", auth, (_req, res) => {
13251
+ try {
13252
+ res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
13253
+ } catch (err) {
13254
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13255
+ }
13256
+ });
13257
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
13258
+ const body = req.body;
13259
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
13260
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
13261
+ return;
13262
+ }
13263
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
13264
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
13265
+ return;
13266
+ }
13267
+ const task = {
13268
+ id: randomUUID(),
13269
+ name: body.name.trim(),
13270
+ cronExpression: body.cronExpression.trim(),
13271
+ prompt: body.prompt.trim(),
13272
+ workspacePath: this.workspacePath,
13273
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13274
+ enabled: body.enabled !== false
13275
+ };
13276
+ try {
13277
+ this.scheduler.add(task);
13278
+ res.json(task);
13279
+ } catch (err) {
13280
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13281
+ }
13282
+ });
13283
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
13284
+ const id = req.params.id;
13285
+ const existing = this.scheduler.list().find((t) => t.id === id);
13286
+ if (!existing) {
13287
+ res.status(404).json({ error: "Not found" });
13288
+ return;
13289
+ }
13290
+ const body = req.body;
13291
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
13292
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
13293
+ return;
13294
+ }
13295
+ const updated = {
13296
+ ...existing,
13297
+ name: body.name?.trim() || existing.name,
13298
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
13299
+ prompt: body.prompt?.trim() || existing.prompt,
13300
+ enabled: body.enabled ?? existing.enabled
13301
+ };
13302
+ try {
13303
+ this.scheduler.add(updated);
13304
+ if (!updated.enabled) this.scheduler.unschedule(id);
13305
+ res.json(updated);
13306
+ } catch (err) {
13307
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13308
+ }
13309
+ });
13310
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
13311
+ try {
13312
+ this.scheduler.remove(req.params.id);
13313
+ res.json({ ok: true });
13314
+ } catch (err) {
13315
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13316
+ }
13317
+ });
13318
+ this.app.get("/api/knowledge", auth, (_req, res) => {
13319
+ try {
13320
+ const ws = new WorldStateDB(this.workspacePath);
13321
+ try {
13322
+ const facts = ws.getAllFacts();
13323
+ res.json({ total: facts.length, facts });
13324
+ } finally {
13325
+ ws.close();
13326
+ }
13327
+ } catch (err) {
13328
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13329
+ }
13330
+ });
13331
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
13332
+ const body = req.body;
13333
+ if (!body.entity || !body.relation) {
13334
+ res.status(400).json({ error: "entity and relation are required" });
13335
+ return;
13336
+ }
13337
+ try {
13338
+ const ws = new WorldStateDB(this.workspacePath);
13339
+ try {
13340
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
13341
+ } finally {
13342
+ ws.close();
13343
+ }
13344
+ } catch (err) {
13345
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13346
+ }
13347
+ });
13348
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
13349
+ try {
13350
+ const ws = new WorldStateDB(this.workspacePath);
13351
+ try {
13352
+ res.json({ ok: true, deleted: ws.clearFacts() });
13353
+ } finally {
13354
+ ws.close();
13355
+ }
13356
+ } catch (err) {
13357
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13358
+ }
13359
+ });
13360
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
13361
+ try {
13362
+ const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
13363
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
13364
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
13365
+ const logger = new AuditLogger3(this.workspacePath);
13366
+ try {
13367
+ const all = logger.getAllLogs();
13368
+ const total = all.length;
13369
+ const entries = all.reverse().slice(offset, offset + limit);
13370
+ res.json({ total, offset, entries });
13371
+ } finally {
13372
+ logger.close();
13373
+ }
13374
+ } catch (err) {
13375
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13376
+ }
13377
+ });
13378
+ const prodPath = path21.resolve(__dirname$1, "../web/dist");
13379
+ const devPath = path21.resolve(__dirname$1, "../../web/dist");
13380
+ const webDistPath = fs20.existsSync(prodPath) ? prodPath : devPath;
13381
+ if (fs20.existsSync(webDistPath)) {
12829
13382
  this.app.use(express.static(webDistPath));
12830
13383
  this.app.get("*", (_req, res) => {
12831
- res.sendFile(path20.join(webDistPath, "index.html"));
13384
+ res.sendFile(path21.join(webDistPath, "index.html"));
12832
13385
  });
12833
13386
  } else {
12834
13387
  this.app.get("/", (_req, res) => {
@@ -12843,68 +13396,6 @@ ${prompt}`;
12843
13396
  }
12844
13397
  }
12845
13398
  };
12846
- var TaskScheduler = class {
12847
- cronJobs = /* @__PURE__ */ new Map();
12848
- store;
12849
- runner;
12850
- constructor(store, runner) {
12851
- this.store = store;
12852
- this.runner = runner;
12853
- }
12854
- start() {
12855
- const tasks = this.store.listScheduledTasks();
12856
- for (const task of tasks) {
12857
- if (task.enabled) this.schedule(task);
12858
- }
12859
- }
12860
- stop() {
12861
- for (const job of this.cronJobs.values()) job.stop();
12862
- this.cronJobs.clear();
12863
- }
12864
- schedule(task) {
12865
- if (!cron.validate(task.cronExpression)) {
12866
- throw new Error(`Invalid cron expression: ${task.cronExpression}`);
12867
- }
12868
- const existing = this.cronJobs.get(task.id);
12869
- if (existing) {
12870
- try {
12871
- existing.stop();
12872
- } catch {
12873
- }
12874
- }
12875
- const job = cron.schedule(task.cronExpression, async () => {
12876
- try {
12877
- task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
12878
- this.store.saveScheduledTask(task);
12879
- await this.runner(task);
12880
- } catch (err) {
12881
- console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
12882
- }
12883
- }, { timezone: "UTC" });
12884
- this.cronJobs.set(task.id, job);
12885
- }
12886
- unschedule(taskId) {
12887
- this.cronJobs.get(taskId)?.stop();
12888
- this.cronJobs.delete(taskId);
12889
- }
12890
- add(task) {
12891
- this.store.saveScheduledTask(task);
12892
- if (task.enabled) this.schedule(task);
12893
- }
12894
- remove(taskId) {
12895
- this.unschedule(taskId);
12896
- this.store.deleteScheduledTask(taskId);
12897
- }
12898
- list() {
12899
- return this.store.listScheduledTasks();
12900
- }
12901
- isRunning(taskId) {
12902
- return this.cronJobs.has(taskId);
12903
- }
12904
- static validateCron(expression) {
12905
- return cron.validate(expression);
12906
- }
12907
- };
12908
13399
  var execFileAsync2 = promisify(execFile);
12909
13400
  var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
12910
13401
  function sanitizeEnvValue(v) {
@@ -12963,6 +13454,6 @@ var HooksRunner = class {
12963
13454
  }
12964
13455
  };
12965
13456
 
12966
- export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
13457
+ export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
12967
13458
  //# sourceMappingURL=index.js.map
12968
13459
  //# sourceMappingURL=index.js.map