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.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  var Database = require('better-sqlite3');
4
- var path20 = require('path');
5
- var fs19 = require('fs');
4
+ var path21 = require('path');
5
+ var fs20 = require('fs');
6
6
  var os6 = require('os');
7
7
  var crypto3 = require('crypto');
8
8
  var EventEmitter = require('events');
@@ -58,8 +58,8 @@ function _interopNamespace(e) {
58
58
  }
59
59
 
60
60
  var Database__default = /*#__PURE__*/_interopDefault(Database);
61
- var path20__default = /*#__PURE__*/_interopDefault(path20);
62
- var fs19__default = /*#__PURE__*/_interopDefault(fs19);
61
+ var path21__default = /*#__PURE__*/_interopDefault(path21);
62
+ var fs20__default = /*#__PURE__*/_interopDefault(fs20);
63
63
  var os6__default = /*#__PURE__*/_interopDefault(os6);
64
64
  var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
65
65
  var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
@@ -104,12 +104,12 @@ var init_audit_logger = __esm({
104
104
  constructor(workspacePath, debugMode = false) {
105
105
  this.workspacePath = workspacePath;
106
106
  this.debugMode = debugMode;
107
- const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
108
- if (!fs19__default.default.existsSync(cascadeDir)) {
109
- fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
107
+ const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
108
+ if (!fs20__default.default.existsSync(cascadeDir)) {
109
+ fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
110
110
  }
111
- this.keyPath = path20__default.default.join(cascadeDir, "audit_log.key");
112
- this.dbPath = path20__default.default.join(cascadeDir, "audit_log.db");
111
+ this.keyPath = path21__default.default.join(cascadeDir, "audit_log.key");
112
+ this.dbPath = path21__default.default.join(cascadeDir, "audit_log.db");
113
113
  this.initEncryptionKey();
114
114
  this.db = new Database__default.default(this.dbPath);
115
115
  this.db.pragma("journal_mode = WAL");
@@ -139,11 +139,11 @@ var init_audit_logger = __esm({
139
139
  return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
140
140
  }
141
141
  initEncryptionKey() {
142
- if (fs19__default.default.existsSync(this.keyPath)) {
143
- this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
142
+ if (fs20__default.default.existsSync(this.keyPath)) {
143
+ this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
144
144
  } else {
145
145
  this.encryptionKey = crypto3__default.default.randomBytes(32);
146
- fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
146
+ fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
147
147
  }
148
148
  }
149
149
  encrypt(text) {
@@ -221,9 +221,9 @@ var init_audit_logger = __esm({
221
221
  dumpDebugIfNeeded() {
222
222
  if (!this.debugMode) return;
223
223
  try {
224
- const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
224
+ const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
225
225
  const entries = this.getAllLogs();
226
- fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
226
+ fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
227
227
  } catch (err) {
228
228
  console.error("Failed to dump debug audit logs", err);
229
229
  }
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
236
236
  });
237
237
 
238
238
  // src/constants.ts
239
- var CASCADE_VERSION = "0.16.0";
239
+ var CASCADE_VERSION = "0.18.0";
240
240
  var CASCADE_CONFIG_DIR = ".cascade";
241
241
  var CASCADE_MD_FILE = "CASCADE.md";
242
242
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -248,6 +248,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
248
248
  var GLOBAL_CONFIG_DIR = ".cascade-ai";
249
249
  var GLOBAL_DB_FILE = "memory.db";
250
250
  var GLOBAL_KEYSTORE_FILE = "keystore.enc";
251
+ var GLOBAL_CREDENTIALS_FILE = "credentials.json";
251
252
  var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
252
253
  var DEFAULT_DASHBOARD_PORT = 4891;
253
254
  var DEFAULT_API_PORT = 4892;
@@ -2210,7 +2211,7 @@ function computeDelegationSavings(stats, t1Model) {
2210
2211
  var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
2211
2212
  var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
2212
2213
  var FETCH_TIMEOUT_MS = 8e3;
2213
- var DEFAULT_CACHE_FILE = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2214
+ var DEFAULT_CACHE_FILE = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
2214
2215
  function normalizeModelId(id) {
2215
2216
  let s = id.toLowerCase();
2216
2217
  const slash = s.lastIndexOf("/");
@@ -2355,7 +2356,7 @@ var LiveDataProvider = class {
2355
2356
  }
2356
2357
  async saveCache() {
2357
2358
  try {
2358
- await fs4__default.default.mkdir(path20__default.default.dirname(this.opts.cacheFile), { recursive: true });
2359
+ await fs4__default.default.mkdir(path21__default.default.dirname(this.opts.cacheFile), { recursive: true });
2359
2360
  const cache = {
2360
2361
  fetchedAt: this.fetchedAt,
2361
2362
  snapshot: this.snapshot ?? void 0,
@@ -4258,8 +4259,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
4258
4259
  tierId: this.id,
4259
4260
  sessionId: this.taskId,
4260
4261
  requireApproval: false,
4261
- saveSnapshot: async (path21, content) => {
4262
- this.store?.addFileSnapshot(this.taskId, path21, content);
4262
+ saveSnapshot: async (path22, content) => {
4263
+ this.store?.addFileSnapshot(this.taskId, path22, content);
4263
4264
  },
4264
4265
  sendPeerSync: (to, syncType, content) => {
4265
4266
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
@@ -4429,7 +4430,7 @@ ${assignment.expectedOutput}`;
4429
4430
  const { promisify: promisify4 } = await import('util');
4430
4431
  const execAsync2 = promisify4(exec2);
4431
4432
  for (const artifactPath of artifactPaths) {
4432
- const absolutePath = path20__default.default.resolve(process.cwd(), artifactPath);
4433
+ const absolutePath = path21__default.default.resolve(process.cwd(), artifactPath);
4433
4434
  try {
4434
4435
  const stat = await fs4__default.default.stat(absolutePath);
4435
4436
  if (!stat.isFile()) {
@@ -4450,7 +4451,7 @@ ${assignment.expectedOutput}`;
4450
4451
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4451
4452
  continue;
4452
4453
  }
4453
- const ext = path20__default.default.extname(absolutePath).toLowerCase();
4454
+ const ext = path21__default.default.extname(absolutePath).toLowerCase();
4454
4455
  try {
4455
4456
  if (ext === ".ts" || ext === ".tsx") {
4456
4457
  await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
@@ -6532,16 +6533,16 @@ function resolveInWorkspace(workspaceRoot, input) {
6532
6533
  if (typeof input !== "string" || input.length === 0) {
6533
6534
  throw new WorkspaceSandboxError(String(input), workspaceRoot);
6534
6535
  }
6535
- const root = path20__default.default.resolve(workspaceRoot);
6536
- const abs = path20__default.default.isAbsolute(input) ? path20__default.default.resolve(input) : path20__default.default.resolve(root, input);
6537
- const rel = path20__default.default.relative(root, abs);
6538
- if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path20__default.default.isAbsolute(rel)) {
6536
+ const root = path21__default.default.resolve(workspaceRoot);
6537
+ const abs = path21__default.default.isAbsolute(input) ? path21__default.default.resolve(input) : path21__default.default.resolve(root, input);
6538
+ const rel = path21__default.default.relative(root, abs);
6539
+ if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21__default.default.isAbsolute(rel)) {
6539
6540
  throw new WorkspaceSandboxError(input, root);
6540
6541
  }
6541
6542
  try {
6542
- const real = fs19__default.default.realpathSync(abs);
6543
- const realRel = path20__default.default.relative(root, real);
6544
- if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path20__default.default.isAbsolute(realRel))) {
6543
+ const real = fs20__default.default.realpathSync(abs);
6544
+ const realRel = path21__default.default.relative(root, real);
6545
+ if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21__default.default.isAbsolute(realRel))) {
6545
6546
  throw new WorkspaceSandboxError(input, root);
6546
6547
  }
6547
6548
  } catch (e) {
@@ -6602,7 +6603,7 @@ var FileWriteTool = class extends BaseTool {
6602
6603
  } catch {
6603
6604
  }
6604
6605
  }
6605
- await fs4__default.default.mkdir(path20__default.default.dirname(absPath), { recursive: true });
6606
+ await fs4__default.default.mkdir(path21__default.default.dirname(absPath), { recursive: true });
6606
6607
  await fs4__default.default.writeFile(absPath, content, "utf-8");
6607
6608
  return `Written ${content.length} characters to ${filePath}`;
6608
6609
  }
@@ -7100,7 +7101,7 @@ var ImageAnalyzeTool = class extends BaseTool {
7100
7101
  };
7101
7102
  async function fileToImageAttachment(filePath) {
7102
7103
  const data = await fs4__default.default.readFile(filePath);
7103
- const ext = path20__default.default.extname(filePath).toLowerCase();
7104
+ const ext = path21__default.default.extname(filePath).toLowerCase();
7104
7105
  const mimeMap = {
7105
7106
  ".jpg": "image/jpeg",
7106
7107
  ".jpeg": "image/jpeg",
@@ -7134,14 +7135,14 @@ var PDFCreateTool = class extends BaseTool {
7134
7135
  const filePath = input["path"];
7135
7136
  const content = input["content"];
7136
7137
  const title = input["title"];
7137
- const dir = path20__default.default.dirname(filePath);
7138
- if (!fs19__default.default.existsSync(dir)) {
7139
- fs19__default.default.mkdirSync(dir, { recursive: true });
7138
+ const dir = path21__default.default.dirname(filePath);
7139
+ if (!fs20__default.default.existsSync(dir)) {
7140
+ fs20__default.default.mkdirSync(dir, { recursive: true });
7140
7141
  }
7141
7142
  return new Promise((resolve, reject) => {
7142
7143
  try {
7143
7144
  const doc = new PDFDocument__default.default({ margin: 50 });
7144
- const stream = fs19__default.default.createWriteStream(filePath);
7145
+ const stream = fs20__default.default.createWriteStream(filePath);
7145
7146
  doc.pipe(stream);
7146
7147
  if (title) {
7147
7148
  doc.info["Title"] = title;
@@ -7219,22 +7220,22 @@ var CodeInterpreterTool = class extends BaseTool {
7219
7220
  }
7220
7221
  cmdPrefix = NODE_CMD;
7221
7222
  }
7222
- const tmpDir = path20__default.default.join(this.workspaceRoot, ".cascade", "tmp");
7223
- if (!fs19__default.default.existsSync(tmpDir)) {
7224
- fs19__default.default.mkdirSync(tmpDir, { recursive: true });
7223
+ const tmpDir = path21__default.default.join(this.workspaceRoot, ".cascade", "tmp");
7224
+ if (!fs20__default.default.existsSync(tmpDir)) {
7225
+ fs20__default.default.mkdirSync(tmpDir, { recursive: true });
7225
7226
  }
7226
7227
  const extension = language === "python" ? "py" : "js";
7227
7228
  const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
7228
- const filePath = path20__default.default.join(tmpDir, fileName);
7229
- fs19__default.default.writeFileSync(filePath, code, "utf-8");
7229
+ const filePath = path21__default.default.join(tmpDir, fileName);
7230
+ fs20__default.default.writeFileSync(filePath, code, "utf-8");
7230
7231
  const execArgs = [filePath, ...args];
7231
7232
  return new Promise((resolve) => {
7232
7233
  const startMs = Date.now();
7233
7234
  child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
7234
7235
  const duration = Date.now() - startMs;
7235
7236
  try {
7236
- if (fs19__default.default.existsSync(filePath)) {
7237
- fs19__default.default.unlinkSync(filePath);
7237
+ if (fs20__default.default.existsSync(filePath)) {
7238
+ fs20__default.default.unlinkSync(filePath);
7238
7239
  }
7239
7240
  } catch (cleanupErr) {
7240
7241
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
@@ -7513,7 +7514,7 @@ var GlobTool = class extends BaseTool {
7513
7514
  };
7514
7515
  async execute(input, _options) {
7515
7516
  const pattern = input["pattern"];
7516
- const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7517
+ const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7517
7518
  const matches = await glob.glob(pattern, {
7518
7519
  cwd: searchPath,
7519
7520
  ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
@@ -7526,7 +7527,7 @@ var GlobTool = class extends BaseTool {
7526
7527
  const withMtime = await Promise.all(
7527
7528
  matches.map(async (rel) => {
7528
7529
  try {
7529
- const stat = await fs4__default.default.stat(path20__default.default.join(searchPath, rel));
7530
+ const stat = await fs4__default.default.stat(path21__default.default.join(searchPath, rel));
7530
7531
  return { rel, mtime: stat.mtimeMs };
7531
7532
  } catch {
7532
7533
  return { rel, mtime: 0 };
@@ -7575,7 +7576,7 @@ var GrepTool = class extends BaseTool {
7575
7576
  };
7576
7577
  async execute(input, _options) {
7577
7578
  const pattern = input["pattern"];
7578
- const searchPath = input["path"] ? path20__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7579
+ const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
7579
7580
  const globPattern = input["glob"];
7580
7581
  const outputMode = input["output_mode"] ?? "content";
7581
7582
  const context = input["context"] ?? 0;
@@ -7629,12 +7630,12 @@ var GrepTool = class extends BaseTool {
7629
7630
  nodir: true
7630
7631
  });
7631
7632
  } catch {
7632
- files = [path20__default.default.relative(searchPath, searchPath) || "."];
7633
+ files = [path21__default.default.relative(searchPath, searchPath) || "."];
7633
7634
  }
7634
7635
  const results = [];
7635
7636
  let totalCount = 0;
7636
7637
  for (const rel of files) {
7637
- const abs = path20__default.default.join(searchPath, rel);
7638
+ const abs = path21__default.default.join(searchPath, rel);
7638
7639
  let content;
7639
7640
  try {
7640
7641
  content = await fs4__default.default.readFile(abs, "utf-8");
@@ -7996,10 +7997,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
7996
7997
  }
7997
7998
  isIgnored(filePath) {
7998
7999
  if (!filePath) return false;
7999
- const abs = path20__default.default.resolve(this.workspaceRoot, filePath);
8000
- const rel = path20__default.default.relative(this.workspaceRoot, abs);
8001
- if (!rel || rel.startsWith("..") || path20__default.default.isAbsolute(rel)) return true;
8002
- const posixRel = rel.split(path20__default.default.sep).join("/");
8000
+ const abs = path21__default.default.resolve(this.workspaceRoot, filePath);
8001
+ const rel = path21__default.default.relative(this.workspaceRoot, abs);
8002
+ if (!rel || rel.startsWith("..") || path21__default.default.isAbsolute(rel)) return true;
8003
+ const posixRel = rel.split(path21__default.default.sep).join("/");
8003
8004
  return this.ignoreMatcher.ignores(posixRel);
8004
8005
  }
8005
8006
  };
@@ -8833,7 +8834,7 @@ var TaskAnalyzer = class {
8833
8834
  analysisCache.clear();
8834
8835
  }
8835
8836
  };
8836
- var DEFAULT_STATS_FILE = path20__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
8837
+ var DEFAULT_STATS_FILE = path21__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
8837
8838
  var ModelPerformanceTracker = class {
8838
8839
  stats = /* @__PURE__ */ new Map();
8839
8840
  featureStats = /* @__PURE__ */ new Map();
@@ -8865,7 +8866,7 @@ var ModelPerformanceTracker = class {
8865
8866
  }
8866
8867
  async save() {
8867
8868
  try {
8868
- await fs4__default.default.mkdir(path20__default.default.dirname(this.statsFile), { recursive: true });
8869
+ await fs4__default.default.mkdir(path21__default.default.dirname(this.statsFile), { recursive: true });
8869
8870
  const modelsObj = {};
8870
8871
  const featuresObj = {};
8871
8872
  for (const [key, stat] of this.stats) modelsObj[key] = stat;
@@ -9404,7 +9405,7 @@ Required capability: ${description.slice(0, 300)}`;
9404
9405
  * any dangerous action, so a silently-reloaded tool can't act without approval. */
9405
9406
  async loadPersistedTools() {
9406
9407
  if (!this.workspacePath || !this.persistEnabled) return;
9407
- const file = path20__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9408
+ const file = path21__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
9408
9409
  try {
9409
9410
  const raw = await fs4__default.default.readFile(file, "utf-8");
9410
9411
  const specs = JSON.parse(raw);
@@ -9428,8 +9429,8 @@ Required capability: ${description.slice(0, 300)}`;
9428
9429
  }
9429
9430
  async persist() {
9430
9431
  if (!this.workspacePath || !this.persistEnabled) return;
9431
- const dir = path20__default.default.join(this.workspacePath, ".cascade");
9432
- const file = path20__default.default.join(dir, DYNAMIC_TOOLS_FILE);
9432
+ const dir = path21__default.default.join(this.workspacePath, ".cascade");
9433
+ const file = path21__default.default.join(dir, DYNAMIC_TOOLS_FILE);
9433
9434
  try {
9434
9435
  await fs4__default.default.mkdir(dir, { recursive: true });
9435
9436
  await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
@@ -9449,12 +9450,12 @@ var WorldStateDB = class {
9449
9450
  constructor(workspacePath, debugMode = false) {
9450
9451
  this.workspacePath = workspacePath;
9451
9452
  this.debugMode = debugMode;
9452
- const cascadeDir = path20__default.default.join(workspacePath, ".cascade");
9453
- if (!fs19__default.default.existsSync(cascadeDir)) {
9454
- fs19__default.default.mkdirSync(cascadeDir, { recursive: true });
9453
+ const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
9454
+ if (!fs20__default.default.existsSync(cascadeDir)) {
9455
+ fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
9455
9456
  }
9456
- this.keyPath = path20__default.default.join(cascadeDir, "world_state.key");
9457
- this.dbPath = path20__default.default.join(cascadeDir, "world_state.db");
9457
+ this.keyPath = path21__default.default.join(cascadeDir, "world_state.key");
9458
+ this.dbPath = path21__default.default.join(cascadeDir, "world_state.db");
9458
9459
  this.initEncryptionKey();
9459
9460
  this.db = new Database__default.default(this.dbPath);
9460
9461
  this.db.pragma("journal_mode = WAL");
@@ -9484,11 +9485,11 @@ var WorldStateDB = class {
9484
9485
  dbPath;
9485
9486
  encryptionKey;
9486
9487
  initEncryptionKey() {
9487
- if (fs19__default.default.existsSync(this.keyPath)) {
9488
- this.encryptionKey = fs19__default.default.readFileSync(this.keyPath);
9488
+ if (fs20__default.default.existsSync(this.keyPath)) {
9489
+ this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
9489
9490
  } else {
9490
9491
  this.encryptionKey = crypto3__default.default.randomBytes(32);
9491
- fs19__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9492
+ fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
9492
9493
  }
9493
9494
  }
9494
9495
  encrypt(text) {
@@ -9629,6 +9630,22 @@ var WorldStateDB = class {
9629
9630
  }
9630
9631
  return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
9631
9632
  }
9633
+ /**
9634
+ * Delete one fact by its (normalized) entity + relation key. Returns whether
9635
+ * a row was actually removed. Powers the desktop Knowledge tab's per-fact
9636
+ * delete — users can prune what the planner remembers about their project.
9637
+ */
9638
+ deleteFact(entity, relation) {
9639
+ const e = normalizeKey(entity);
9640
+ const r = normalizeKey(relation);
9641
+ if (!e || !r) return false;
9642
+ const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
9643
+ return result.changes > 0;
9644
+ }
9645
+ /** Delete every fact. Returns how many were removed. */
9646
+ clearFacts() {
9647
+ return this.db.prepare("DELETE FROM facts").run().changes;
9648
+ }
9632
9649
  // ── Export / Import ──────────────────────────
9633
9650
  //
9634
9651
  // Knowledge travels DECRYPTED in the export bundle (a portable plaintext
@@ -9677,9 +9694,9 @@ var WorldStateDB = class {
9677
9694
  dumpDebugIfNeeded() {
9678
9695
  if (!this.debugMode) return;
9679
9696
  try {
9680
- const dumpPath = path20__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
9697
+ const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
9681
9698
  const entries = this.getAllEntries();
9682
- fs19__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9699
+ fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
9683
9700
  } catch (err) {
9684
9701
  console.error("Failed to dump debug world state", err);
9685
9702
  }
@@ -10589,7 +10606,7 @@ var Keystore = class {
10589
10606
  const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
10590
10607
  this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
10591
10608
  this.backend = "keytar";
10592
- if (password && fs19__default.default.existsSync(this.storePath)) {
10609
+ if (password && fs20__default.default.existsSync(this.storePath)) {
10593
10610
  try {
10594
10611
  const fileEntries = this.decryptFile(password);
10595
10612
  for (const [k, v] of Object.entries(fileEntries)) {
@@ -10608,7 +10625,7 @@ var Keystore = class {
10608
10625
  "Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
10609
10626
  );
10610
10627
  }
10611
- if (!fs19__default.default.existsSync(this.storePath)) {
10628
+ if (!fs20__default.default.existsSync(this.storePath)) {
10612
10629
  const salt = crypto3__default.default.randomBytes(SALT_LEN);
10613
10630
  this.masterKey = this.deriveKey(password, salt);
10614
10631
  this.writeWithSalt({}, salt);
@@ -10622,7 +10639,7 @@ var Keystore = class {
10622
10639
  }
10623
10640
  /** Synchronous legacy unlock kept for AES-only environments. */
10624
10641
  unlockSync(password) {
10625
- if (!fs19__default.default.existsSync(this.storePath)) {
10642
+ if (!fs20__default.default.existsSync(this.storePath)) {
10626
10643
  const salt = crypto3__default.default.randomBytes(SALT_LEN);
10627
10644
  this.masterKey = this.deriveKey(password, salt);
10628
10645
  this.writeWithSalt({}, salt);
@@ -10680,7 +10697,7 @@ var Keystore = class {
10680
10697
  }
10681
10698
  }
10682
10699
  decryptFile(password, knownSalt) {
10683
- if (!fs19__default.default.existsSync(this.storePath)) return {};
10700
+ if (!fs20__default.default.existsSync(this.storePath)) return {};
10684
10701
  try {
10685
10702
  const { salt, ciphertext, iv, tag } = this.readRaw();
10686
10703
  const useSalt = knownSalt ?? salt;
@@ -10702,8 +10719,8 @@ var Keystore = class {
10702
10719
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10703
10720
  const tag = cipher.getAuthTag();
10704
10721
  const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
10705
- fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10706
- fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10722
+ fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
10723
+ fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10707
10724
  }
10708
10725
  writeWithSalt(data, salt) {
10709
10726
  if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
@@ -10713,11 +10730,11 @@ var Keystore = class {
10713
10730
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
10714
10731
  const tag = cipher.getAuthTag();
10715
10732
  const out = Buffer.concat([salt, iv, tag, ciphertext]);
10716
- fs19__default.default.mkdirSync(path20__default.default.dirname(this.storePath), { recursive: true });
10717
- fs19__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10733
+ fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
10734
+ fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
10718
10735
  }
10719
10736
  readRaw() {
10720
- const buf = fs19__default.default.readFileSync(this.storePath);
10737
+ const buf = fs20__default.default.readFileSync(this.storePath);
10721
10738
  let offset = 0;
10722
10739
  const salt = buf.subarray(offset, offset + SALT_LEN);
10723
10740
  offset += SALT_LEN;
@@ -10750,7 +10767,7 @@ var CascadeIgnore = class {
10750
10767
  ]);
10751
10768
  }
10752
10769
  async load(workspacePath) {
10753
- const filePath = path20__default.default.join(workspacePath, ".cascadeignore");
10770
+ const filePath = path21__default.default.join(workspacePath, ".cascadeignore");
10754
10771
  try {
10755
10772
  const content = await fs4__default.default.readFile(filePath, "utf-8");
10756
10773
  const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
@@ -10761,7 +10778,7 @@ var CascadeIgnore = class {
10761
10778
  }
10762
10779
  isIgnored(filePath, workspacePath) {
10763
10780
  try {
10764
- const relative = workspacePath ? path20__default.default.relative(workspacePath, filePath) : filePath;
10781
+ const relative = workspacePath ? path21__default.default.relative(workspacePath, filePath) : filePath;
10765
10782
  return this.ig.ignores(relative);
10766
10783
  } catch {
10767
10784
  return false;
@@ -10772,7 +10789,7 @@ var CascadeIgnore = class {
10772
10789
  }
10773
10790
  };
10774
10791
  async function loadCascadeMd(workspacePath) {
10775
- const filePath = path20__default.default.join(workspacePath, "CASCADE.md");
10792
+ const filePath = path21__default.default.join(workspacePath, "CASCADE.md");
10776
10793
  try {
10777
10794
  const raw = await fs4__default.default.readFile(filePath, "utf-8");
10778
10795
  return parseCascadeMd(raw);
@@ -10803,7 +10820,7 @@ ${raw.trim()}`;
10803
10820
  var MemoryStore = class _MemoryStore {
10804
10821
  db;
10805
10822
  constructor(dbPath) {
10806
- fs19__default.default.mkdirSync(path20__default.default.dirname(dbPath), { recursive: true });
10823
+ fs20__default.default.mkdirSync(path21__default.default.dirname(dbPath), { recursive: true });
10807
10824
  try {
10808
10825
  this.db = new Database__default.default(dbPath, { timeout: 5e3 });
10809
10826
  this.db.pragma("journal_mode = WAL");
@@ -11638,6 +11655,60 @@ Original error: ${err.message}`
11638
11655
  };
11639
11656
  }
11640
11657
  };
11658
+ function credentialsPath(globalDir) {
11659
+ return path21__default.default.join(globalDir, GLOBAL_CREDENTIALS_FILE);
11660
+ }
11661
+ function providerKey(p) {
11662
+ if (p.type === "azure") {
11663
+ return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
11664
+ }
11665
+ return p.type;
11666
+ }
11667
+ function isPersistable(p) {
11668
+ return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
11669
+ }
11670
+ function loadGlobalCredentials(globalDir) {
11671
+ try {
11672
+ const raw = fs20__default.default.readFileSync(credentialsPath(globalDir), "utf-8");
11673
+ const parsed = JSON.parse(raw);
11674
+ if (!Array.isArray(parsed.providers)) return [];
11675
+ return parsed.providers.filter(
11676
+ (p) => Boolean(p) && typeof p.type === "string"
11677
+ );
11678
+ } catch {
11679
+ return [];
11680
+ }
11681
+ }
11682
+ function saveGlobalCredentials(globalDir, providers) {
11683
+ const filePath = credentialsPath(globalDir);
11684
+ fs20__default.default.mkdirSync(globalDir, { recursive: true, mode: 448 });
11685
+ const body = { version: 1, providers: providers.filter(isPersistable) };
11686
+ const tmp = `${filePath}.tmp`;
11687
+ fs20__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
11688
+ fs20__default.default.renameSync(tmp, filePath);
11689
+ try {
11690
+ fs20__default.default.chmodSync(filePath, 384);
11691
+ } catch {
11692
+ }
11693
+ }
11694
+ function mergeGlobalCredentials(workspaceProviders, globalProviders) {
11695
+ const merged = [...workspaceProviders];
11696
+ const byKey = new Map(merged.map((p) => [providerKey(p), p]));
11697
+ for (const g of globalProviders) {
11698
+ const existing = byKey.get(providerKey(g));
11699
+ if (!existing) {
11700
+ merged.push({ ...g });
11701
+ byKey.set(providerKey(g), merged[merged.length - 1]);
11702
+ continue;
11703
+ }
11704
+ if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
11705
+ if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
11706
+ if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
11707
+ if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
11708
+ if (!existing.label && g.label) existing.label = g.label;
11709
+ }
11710
+ return merged;
11711
+ }
11641
11712
 
11642
11713
  // src/config/index.ts
11643
11714
  var ConfigManager = class {
@@ -11648,18 +11719,20 @@ var ConfigManager = class {
11648
11719
  cascadeMd = null;
11649
11720
  workspacePath;
11650
11721
  globalDir;
11651
- constructor(workspacePath = process.cwd()) {
11722
+ /** `globalDirOverride` exists for tests — never point it at the real home dir there. */
11723
+ constructor(workspacePath = process.cwd(), globalDirOverride) {
11652
11724
  this.workspacePath = workspacePath;
11653
- this.globalDir = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
11725
+ this.globalDir = globalDirOverride ?? path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
11654
11726
  }
11655
11727
  async load() {
11656
11728
  this.config = await this.loadConfig();
11657
11729
  this.ignore = new CascadeIgnore();
11658
11730
  await this.ignore.load(this.workspacePath);
11659
11731
  this.cascadeMd = await loadCascadeMd(this.workspacePath);
11660
- this.keystore = new Keystore(path20__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11661
- this.store = new MemoryStore(path20__default.default.join(this.workspacePath, CASCADE_DB_FILE));
11732
+ this.keystore = new Keystore(path21__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
11733
+ this.store = new MemoryStore(path21__default.default.join(this.workspacePath, CASCADE_DB_FILE));
11662
11734
  await this.injectEnvKeys();
11735
+ this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
11663
11736
  await this.ensureDefaultIdentity();
11664
11737
  }
11665
11738
  getConfig() {
@@ -11681,9 +11754,14 @@ var ConfigManager = class {
11681
11754
  return this.workspacePath;
11682
11755
  }
11683
11756
  async save() {
11684
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11685
- await fs4__default.default.mkdir(path20__default.default.dirname(configPath), { recursive: true });
11757
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11758
+ await fs4__default.default.mkdir(path21__default.default.dirname(configPath), { recursive: true });
11686
11759
  await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
11760
+ try {
11761
+ saveGlobalCredentials(this.globalDir, this.config.providers);
11762
+ } catch (err) {
11763
+ console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
11764
+ }
11687
11765
  }
11688
11766
  async updateConfig(updates) {
11689
11767
  this.config = validateConfig({ ...this.config, ...updates });
@@ -11706,7 +11784,7 @@ var ConfigManager = class {
11706
11784
  return configProvider?.apiKey;
11707
11785
  }
11708
11786
  async loadConfig() {
11709
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11787
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
11710
11788
  try {
11711
11789
  const raw = await fs4__default.default.readFile(configPath, "utf-8");
11712
11790
  return validateConfig(JSON.parse(raw));
@@ -11813,6 +11891,11 @@ function authMiddleware(secret, required = true) {
11813
11891
  }
11814
11892
  const user = verifyToken(token, secret);
11815
11893
  if (!user) {
11894
+ if (!required) {
11895
+ req.user = void 0;
11896
+ next();
11897
+ return;
11898
+ }
11816
11899
  res.status(401).json({ error: "Invalid or expired token" });
11817
11900
  return;
11818
11901
  }
@@ -11945,6 +12028,25 @@ var DashboardSocket = class {
11945
12028
  });
11946
12029
  });
11947
12030
  }
12031
+ /**
12032
+ * Boardroom plan decisions from a connected client. The desktop shows a
12033
+ * plan-review modal on `plan:approval-required` and answers here; the
12034
+ * server routes the decision into the paused run via resolvePlanApproval.
12035
+ */
12036
+ onPlanDecision(callback) {
12037
+ this.io.on("connection", (socket) => {
12038
+ socket.on("plan:decision", (payload) => {
12039
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
12040
+ callback({
12041
+ sessionId: payload.sessionId,
12042
+ approved: payload.approved,
12043
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
12044
+ editedPlan: payload.editedPlan
12045
+ });
12046
+ }
12047
+ });
12048
+ });
12049
+ }
11948
12050
  onSessionSteer(callback) {
11949
12051
  this.io.on("connection", (socket) => {
11950
12052
  socket.on("session:steer", (payload) => {
@@ -11986,7 +12088,123 @@ var DashboardSocket = class {
11986
12088
  this.io.close();
11987
12089
  }
11988
12090
  };
11989
- var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
12091
+ var TaskScheduler = class {
12092
+ cronJobs = /* @__PURE__ */ new Map();
12093
+ store;
12094
+ runner;
12095
+ constructor(store, runner) {
12096
+ this.store = store;
12097
+ this.runner = runner;
12098
+ }
12099
+ start() {
12100
+ const tasks = this.store.listScheduledTasks();
12101
+ for (const task of tasks) {
12102
+ if (task.enabled) this.schedule(task);
12103
+ }
12104
+ }
12105
+ stop() {
12106
+ for (const job of this.cronJobs.values()) job.stop();
12107
+ this.cronJobs.clear();
12108
+ }
12109
+ schedule(task) {
12110
+ if (!cron__default.default.validate(task.cronExpression)) {
12111
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
12112
+ }
12113
+ const existing = this.cronJobs.get(task.id);
12114
+ if (existing) {
12115
+ try {
12116
+ existing.stop();
12117
+ } catch {
12118
+ }
12119
+ }
12120
+ const job = cron__default.default.schedule(task.cronExpression, async () => {
12121
+ try {
12122
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
12123
+ this.store.saveScheduledTask(task);
12124
+ await this.runner(task);
12125
+ } catch (err) {
12126
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
12127
+ }
12128
+ }, { timezone: "UTC" });
12129
+ this.cronJobs.set(task.id, job);
12130
+ }
12131
+ unschedule(taskId) {
12132
+ this.cronJobs.get(taskId)?.stop();
12133
+ this.cronJobs.delete(taskId);
12134
+ }
12135
+ add(task) {
12136
+ this.store.saveScheduledTask(task);
12137
+ if (task.enabled) this.schedule(task);
12138
+ }
12139
+ remove(taskId) {
12140
+ this.unschedule(taskId);
12141
+ this.store.deleteScheduledTask(taskId);
12142
+ }
12143
+ list() {
12144
+ return this.store.listScheduledTasks();
12145
+ }
12146
+ isRunning(taskId) {
12147
+ return this.cronJobs.has(taskId);
12148
+ }
12149
+ static validateCron(expression) {
12150
+ return cron__default.default.validate(expression);
12151
+ }
12152
+ };
12153
+
12154
+ // src/dashboard/cost-stats.ts
12155
+ var DAY_MS = 24 * 60 * 60 * 1e3;
12156
+ function utcDateKey(d) {
12157
+ return d.toISOString().slice(0, 10);
12158
+ }
12159
+ function aggregateCostStats(sessions, opts = {}) {
12160
+ const days = Math.max(1, opts.days ?? 30);
12161
+ const topN = Math.max(1, opts.topN ?? 8);
12162
+ const now = opts.now ?? /* @__PURE__ */ new Date();
12163
+ const buckets = /* @__PURE__ */ new Map();
12164
+ for (let i = days - 1; i >= 0; i--) {
12165
+ const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
12166
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
12167
+ }
12168
+ let totalCostUsd = 0;
12169
+ let totalTokens = 0;
12170
+ let totalRuns = 0;
12171
+ for (const s of sessions) {
12172
+ const cost = s.metadata?.totalCostUsd ?? 0;
12173
+ const tokens = s.metadata?.totalTokens ?? 0;
12174
+ const runs = s.metadata?.taskCount ?? 0;
12175
+ totalCostUsd += cost;
12176
+ totalTokens += tokens;
12177
+ totalRuns += runs;
12178
+ const when = new Date(s.updatedAt || s.createdAt);
12179
+ if (!Number.isNaN(when.getTime())) {
12180
+ const bucket = buckets.get(utcDateKey(when));
12181
+ if (bucket) {
12182
+ bucket.costUsd += cost;
12183
+ bucket.tokens += tokens;
12184
+ bucket.runs += runs;
12185
+ }
12186
+ }
12187
+ }
12188
+ 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) => ({
12189
+ sessionId: s.id,
12190
+ title: s.title,
12191
+ costUsd: s.metadata?.totalCostUsd ?? 0,
12192
+ tokens: s.metadata?.totalTokens ?? 0,
12193
+ runs: s.metadata?.taskCount ?? 0,
12194
+ updatedAt: s.updatedAt
12195
+ }));
12196
+ return {
12197
+ totalCostUsd,
12198
+ totalTokens,
12199
+ totalSessions: sessions.length,
12200
+ totalRuns,
12201
+ perDay: [...buckets.values()],
12202
+ topSessions
12203
+ };
12204
+ }
12205
+
12206
+ // src/dashboard/server.ts
12207
+ var __dirname$1 = path21__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
11990
12208
  var DashboardServer = class {
11991
12209
  app;
11992
12210
  httpServer;
@@ -12011,6 +12229,14 @@ var DashboardServer = class {
12011
12229
  * map is how that answer reaches the run that's blocked on it.
12012
12230
  */
12013
12231
  pendingApprovals = /* @__PURE__ */ new Map();
12232
+ /**
12233
+ * The orchestration decision trail ("why") of each session's most recent
12234
+ * run — captured when the run ends so the desktop's Why panel can show it
12235
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
12236
+ */
12237
+ whyBySession = /* @__PURE__ */ new Map();
12238
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
12239
+ scheduler;
12014
12240
  port;
12015
12241
  host;
12016
12242
  workspacePath;
@@ -12028,6 +12254,7 @@ var DashboardServer = class {
12028
12254
  secret: this.dashboardSecret,
12029
12255
  corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
12030
12256
  });
12257
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
12031
12258
  this.setupMiddleware();
12032
12259
  this.setupRoutes();
12033
12260
  this.socket.onSessionRate((sessionId, rating) => {
@@ -12091,6 +12318,9 @@ var DashboardServer = class {
12091
12318
  cascade.on("peer:message", (e) => {
12092
12319
  this.socket.emitPeerMessage(e);
12093
12320
  });
12321
+ cascade.on("plan:approval-required", (e) => {
12322
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
12323
+ });
12094
12324
  try {
12095
12325
  const result = await cascade.run({
12096
12326
  prompt: runPrompt,
@@ -12098,7 +12328,8 @@ var DashboardServer = class {
12098
12328
  approvalCallback: this.makeApprovalCallback(sessionId)
12099
12329
  });
12100
12330
  this.recordSessionTask(sessionId, result.taskId);
12101
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
12331
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
12332
+ this.captureWhy(sessionId, cascade, result);
12102
12333
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
12103
12334
  this.socket.broadcast("cost:update", {
12104
12335
  sessionId,
@@ -12108,6 +12339,7 @@ var DashboardServer = class {
12108
12339
  this.throttledBroadcast("workspace");
12109
12340
  } catch (err) {
12110
12341
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
12342
+ this.captureWhy(sessionId, cascade);
12111
12343
  this.socket.emitToSocket(socketId, "session:error", {
12112
12344
  sessionId,
12113
12345
  error: err instanceof Error ? err.message : String(err)
@@ -12118,9 +12350,17 @@ var DashboardServer = class {
12118
12350
  this.denyPendingApprovals(sessionId);
12119
12351
  }
12120
12352
  });
12353
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
12354
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
12355
+ approved,
12356
+ note,
12357
+ editedPlan
12358
+ );
12359
+ });
12121
12360
  this.socket.onSessionHalt((sessionId) => {
12122
12361
  this.activeControllers.get(sessionId)?.abort();
12123
12362
  this.denyPendingApprovals(sessionId);
12363
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
12124
12364
  });
12125
12365
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
12126
12366
  const pending = this.pendingApprovals.get(requestId);
@@ -12153,12 +12393,21 @@ var DashboardServer = class {
12153
12393
  resolve();
12154
12394
  });
12155
12395
  });
12396
+ try {
12397
+ this.scheduler.start();
12398
+ } catch (err) {
12399
+ console.warn("[dashboard] failed to start task scheduler:", err);
12400
+ }
12156
12401
  }
12157
12402
  async stop() {
12158
12403
  if (this.broadcastTimer) {
12159
12404
  clearTimeout(this.broadcastTimer);
12160
12405
  this.broadcastTimer = null;
12161
12406
  }
12407
+ try {
12408
+ this.scheduler.stop();
12409
+ } catch {
12410
+ }
12162
12411
  this.socket.close();
12163
12412
  try {
12164
12413
  this.globalStore?.close();
@@ -12189,12 +12438,17 @@ var DashboardServer = class {
12189
12438
  */
12190
12439
  persistConfig() {
12191
12440
  try {
12192
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
12193
- fs19__default.default.mkdirSync(path20__default.default.dirname(configPath), { recursive: true });
12194
- fs19__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12441
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
12442
+ fs20__default.default.mkdirSync(path21__default.default.dirname(configPath), { recursive: true });
12443
+ fs20__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
12195
12444
  } catch (err) {
12196
12445
  console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
12197
12446
  }
12447
+ try {
12448
+ saveGlobalCredentials(path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
12449
+ } catch (err) {
12450
+ console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
12451
+ }
12198
12452
  }
12199
12453
  /**
12200
12454
  * Produce a stable dashboard JWT signing secret.
@@ -12206,15 +12460,15 @@ var DashboardServer = class {
12206
12460
  resolveDashboardSecret() {
12207
12461
  const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
12208
12462
  if (fromConfig) return fromConfig;
12209
- const secretPath = path20__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12463
+ const secretPath = path21__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
12210
12464
  try {
12211
- if (fs19__default.default.existsSync(secretPath)) {
12212
- const existing = fs19__default.default.readFileSync(secretPath, "utf-8").trim();
12465
+ if (fs20__default.default.existsSync(secretPath)) {
12466
+ const existing = fs20__default.default.readFileSync(secretPath, "utf-8").trim();
12213
12467
  if (existing.length >= 16) return existing;
12214
12468
  }
12215
12469
  const generated = crypto3.randomUUID();
12216
- fs19__default.default.mkdirSync(path20__default.default.dirname(secretPath), { recursive: true });
12217
- fs19__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12470
+ fs20__default.default.mkdirSync(path21__default.default.dirname(secretPath), { recursive: true });
12471
+ fs20__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
12218
12472
  if (this.config.dashboard.auth) {
12219
12473
  console.warn(
12220
12474
  `Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
@@ -12241,7 +12495,7 @@ var DashboardServer = class {
12241
12495
  // ── Setup ─────────────────────────────────────
12242
12496
  getGlobalStore() {
12243
12497
  if (!this.globalStore) {
12244
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12498
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12245
12499
  this.globalStore = new MemoryStore(globalDbPath);
12246
12500
  }
12247
12501
  return this.globalStore;
@@ -12279,16 +12533,60 @@ var DashboardServer = class {
12279
12533
  return title;
12280
12534
  }
12281
12535
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
12282
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
12536
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
12283
12537
  try {
12284
12538
  if (reply && reply.trim()) {
12285
12539
  this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
12286
12540
  }
12541
+ if (result) {
12542
+ const session = this.store.getSession(sessionId);
12543
+ if (session) {
12544
+ this.store.updateSession(sessionId, {
12545
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
12546
+ metadata: {
12547
+ ...session.metadata,
12548
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
12549
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
12550
+ taskCount: session.metadata.taskCount + 1
12551
+ }
12552
+ });
12553
+ }
12554
+ }
12287
12555
  } catch (err) {
12288
12556
  console.warn("[dashboard] failed to persist run end:", err);
12289
12557
  }
12290
12558
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
12291
12559
  }
12560
+ /**
12561
+ * Capture the run's decision trail + router economics ("why") and broadcast
12562
+ * it so the desktop's Why panel updates live; kept per-session for the
12563
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
12564
+ */
12565
+ captureWhy(sessionId, cascade, result) {
12566
+ try {
12567
+ const stats = cascade.getRouter().getStats();
12568
+ const savings = cascade.getRouter().getDelegationSavings();
12569
+ const report = {
12570
+ sessionId,
12571
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
12572
+ decisions: cascade.getDecisionLog(),
12573
+ savedUsd: savings.savedUsd,
12574
+ savedPct: savings.savedPct,
12575
+ totalCostUsd: stats.totalCostUsd,
12576
+ totalTokens: stats.totalTokens,
12577
+ costByTier: stats.costByTier,
12578
+ durationMs: result?.durationMs
12579
+ };
12580
+ this.whyBySession.set(sessionId, report);
12581
+ if (this.whyBySession.size > 50) {
12582
+ const oldest = this.whyBySession.keys().next().value;
12583
+ if (oldest) this.whyBySession.delete(oldest);
12584
+ }
12585
+ this.socket.broadcast("run:why", report);
12586
+ } catch (err) {
12587
+ console.warn("[dashboard] failed to capture decision trail:", err);
12588
+ }
12589
+ }
12292
12590
  /**
12293
12591
  * Route steering text into running Cascade instances. Targets the given
12294
12592
  * session, or every active session when none is specified (the desktop
@@ -12318,6 +12616,52 @@ var DashboardServer = class {
12318
12616
  this.pendingApprovals.set(request.id, { resolve, sessionId });
12319
12617
  });
12320
12618
  }
12619
+ /**
12620
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
12621
+ * tool approvals are auto-granted since nobody may be watching when the cron
12622
+ * fires — the Schedules UI states this. Events broadcast to every connected
12623
+ * client so an open desktop sees the run appear live.
12624
+ */
12625
+ async runScheduledTask(task) {
12626
+ const sessionId = crypto3.randomUUID();
12627
+ const prompt = task.prompt;
12628
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
12629
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
12630
+ this.activeSessions.set(sessionId, cascade);
12631
+ cascade.on("tier:status", (e) => {
12632
+ this.socket.broadcast("tier:status", { sessionId, ...e });
12633
+ });
12634
+ cascade.on("peer:message", (e) => {
12635
+ this.socket.emitPeerMessage(e);
12636
+ });
12637
+ try {
12638
+ const result = await cascade.run({
12639
+ prompt,
12640
+ identityId: task.identityId,
12641
+ approvalCallback: async () => ({ approved: true, always: false })
12642
+ });
12643
+ this.recordSessionTask(sessionId, result.taskId);
12644
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
12645
+ this.captureWhy(sessionId, cascade, result);
12646
+ this.socket.broadcast("session:complete", { sessionId, result });
12647
+ this.socket.broadcast("cost:update", {
12648
+ sessionId,
12649
+ totalTokens: result.usage.totalTokens,
12650
+ totalCostUsd: result.usage.estimatedCostUsd
12651
+ });
12652
+ this.throttledBroadcast("workspace");
12653
+ } catch (err) {
12654
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
12655
+ this.captureWhy(sessionId, cascade);
12656
+ this.socket.broadcast("session:error", {
12657
+ sessionId,
12658
+ error: err instanceof Error ? err.message : String(err)
12659
+ });
12660
+ throw err;
12661
+ } finally {
12662
+ this.activeSessions.delete(sessionId);
12663
+ }
12664
+ }
12321
12665
  /** Deny + clear any approvals still pending for a session (run end / abort). */
12322
12666
  denyPendingApprovals(sessionId) {
12323
12667
  for (const [id, pending] of this.pendingApprovals) {
@@ -12420,12 +12764,12 @@ ${prompt}`;
12420
12764
  }
12421
12765
  }
12422
12766
  watchRuntimeChanges() {
12423
- const workspaceDbPath = path20__default.default.join(this.workspacePath, CASCADE_DB_FILE);
12424
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12767
+ const workspaceDbPath = path21__default.default.join(this.workspacePath, CASCADE_DB_FILE);
12768
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12425
12769
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
12426
12770
  for (const watchPath of watchPaths) {
12427
- if (!fs19__default.default.existsSync(watchPath)) continue;
12428
- fs19__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
12771
+ if (!fs20__default.default.existsSync(watchPath)) continue;
12772
+ fs20__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
12429
12773
  this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
12430
12774
  });
12431
12775
  }
@@ -12547,7 +12891,7 @@ ${prompt}`;
12547
12891
  const sessionId = req.params.id;
12548
12892
  this.store.deleteSession(sessionId);
12549
12893
  this.store.deleteRuntimeSession(sessionId);
12550
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12894
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12551
12895
  const globalStore = new MemoryStore(globalDbPath);
12552
12896
  try {
12553
12897
  globalStore.deleteRuntimeSession(sessionId);
@@ -12640,7 +12984,7 @@ ${prompt}`;
12640
12984
  });
12641
12985
  this.app.delete("/api/sessions", auth, (req, res) => {
12642
12986
  const body = req.body;
12643
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12987
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12644
12988
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
12645
12989
  const globalStore = new MemoryStore(globalDbPath);
12646
12990
  try {
@@ -12663,7 +13007,7 @@ ${prompt}`;
12663
13007
  });
12664
13008
  this.app.delete("/api/runtime", auth, (_req, res) => {
12665
13009
  this.store.deleteAllRuntimeNodes();
12666
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13010
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12667
13011
  const globalStore = new MemoryStore(globalDbPath);
12668
13012
  try {
12669
13013
  globalStore.deleteAllRuntimeNodes();
@@ -12749,12 +13093,12 @@ ${prompt}`;
12749
13093
  if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
12750
13094
  if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
12751
13095
  try {
12752
- const configPath = path20__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
12753
- const existing = fs19__default.default.existsSync(configPath) ? JSON.parse(fs19__default.default.readFileSync(configPath, "utf-8")) : {};
13096
+ const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
13097
+ const existing = fs20__default.default.existsSync(configPath) ? JSON.parse(fs20__default.default.readFileSync(configPath, "utf-8")) : {};
12754
13098
  const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
12755
13099
  const tmp = configPath + ".tmp";
12756
- fs19__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
12757
- fs19__default.default.renameSync(tmp, configPath);
13100
+ fs20__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
13101
+ fs20__default.default.renameSync(tmp, configPath);
12758
13102
  res.json({ ok: true });
12759
13103
  } catch (err) {
12760
13104
  res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
@@ -12782,7 +13126,7 @@ ${prompt}`;
12782
13126
  this.app.get("/api/runtime", auth, (req, res) => {
12783
13127
  const scope = req.query["scope"] ?? "workspace";
12784
13128
  if (scope === "global") {
12785
- const globalDbPath = path20__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
13129
+ const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
12786
13130
  const globalStore = new MemoryStore(globalDbPath);
12787
13131
  try {
12788
13132
  res.json({
@@ -12830,6 +13174,9 @@ ${prompt}`;
12830
13174
  cascade.on("peer:message", (e) => {
12831
13175
  this.socket.emitPeerMessage(e);
12832
13176
  });
13177
+ cascade.on("plan:approval-required", (e) => {
13178
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
13179
+ });
12833
13180
  try {
12834
13181
  const result = await cascade.run({
12835
13182
  prompt: runPrompt,
@@ -12837,7 +13184,8 @@ ${prompt}`;
12837
13184
  approvalCallback: this.makeApprovalCallback(sessionId)
12838
13185
  });
12839
13186
  this.recordSessionTask(sessionId, result.taskId);
12840
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
13187
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
13188
+ this.captureWhy(sessionId, cascade, result);
12841
13189
  this.socket.broadcast("cost:update", {
12842
13190
  sessionId,
12843
13191
  totalTokens: result.usage.totalTokens,
@@ -12847,6 +13195,7 @@ ${prompt}`;
12847
13195
  this.throttledBroadcast("workspace");
12848
13196
  } catch (err) {
12849
13197
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
13198
+ this.captureWhy(sessionId, cascade);
12850
13199
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
12851
13200
  sessionId,
12852
13201
  error: err instanceof Error ? err.message : String(err)
@@ -12868,13 +13217,217 @@ ${prompt}`;
12868
13217
  }))
12869
13218
  });
12870
13219
  });
12871
- const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
12872
- const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
12873
- const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
12874
- if (fs19__default.default.existsSync(webDistPath)) {
13220
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
13221
+ const report = this.whyBySession.get(req.params.id);
13222
+ if (!report) {
13223
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
13224
+ return;
13225
+ }
13226
+ res.json(report);
13227
+ });
13228
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
13229
+ const sessionId = req.params.id;
13230
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
13231
+ const before = /* @__PURE__ */ new Map();
13232
+ for (const taskId of taskIds) {
13233
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
13234
+ if (!before.has(filePath)) before.set(filePath, content);
13235
+ }
13236
+ }
13237
+ const { readFile } = await import('fs/promises');
13238
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
13239
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
13240
+ let after = "";
13241
+ let missing = false;
13242
+ try {
13243
+ const buf = await readFile(filePath);
13244
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
13245
+ } catch {
13246
+ missing = true;
13247
+ }
13248
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
13249
+ }));
13250
+ res.json({ sessionId, changes: changes.filter((c) => c.changed) });
13251
+ });
13252
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
13253
+ const sessionId = req.params.id;
13254
+ const body = req.body;
13255
+ if (!body.filePath || typeof body.filePath !== "string") {
13256
+ res.status(400).json({ error: "filePath is required" });
13257
+ return;
13258
+ }
13259
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
13260
+ let content;
13261
+ for (const taskId of taskIds) {
13262
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
13263
+ if (snap) {
13264
+ content = snap.content;
13265
+ break;
13266
+ }
13267
+ }
13268
+ if (content === void 0) {
13269
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
13270
+ return;
13271
+ }
13272
+ try {
13273
+ const { writeFile } = await import('fs/promises');
13274
+ await writeFile(body.filePath, content, "utf-8");
13275
+ res.json({ ok: true, filePath: body.filePath });
13276
+ } catch (err) {
13277
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13278
+ }
13279
+ });
13280
+ this.app.get("/api/costs", auth, (_req, res) => {
13281
+ try {
13282
+ const sessions = this.store.listSessions(void 0, 1e3);
13283
+ const budget = this.config.budget ?? { warnAtPct: 80 };
13284
+ res.json({
13285
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
13286
+ budget: {
13287
+ dailyBudgetUsd: budget.dailyBudgetUsd,
13288
+ sessionBudgetUsd: budget.sessionBudgetUsd,
13289
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
13290
+ }
13291
+ });
13292
+ } catch (err) {
13293
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13294
+ }
13295
+ });
13296
+ this.app.get("/api/schedules", auth, (_req, res) => {
13297
+ try {
13298
+ res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
13299
+ } catch (err) {
13300
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13301
+ }
13302
+ });
13303
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
13304
+ const body = req.body;
13305
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
13306
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
13307
+ return;
13308
+ }
13309
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
13310
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
13311
+ return;
13312
+ }
13313
+ const task = {
13314
+ id: crypto3.randomUUID(),
13315
+ name: body.name.trim(),
13316
+ cronExpression: body.cronExpression.trim(),
13317
+ prompt: body.prompt.trim(),
13318
+ workspacePath: this.workspacePath,
13319
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13320
+ enabled: body.enabled !== false
13321
+ };
13322
+ try {
13323
+ this.scheduler.add(task);
13324
+ res.json(task);
13325
+ } catch (err) {
13326
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13327
+ }
13328
+ });
13329
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
13330
+ const id = req.params.id;
13331
+ const existing = this.scheduler.list().find((t) => t.id === id);
13332
+ if (!existing) {
13333
+ res.status(404).json({ error: "Not found" });
13334
+ return;
13335
+ }
13336
+ const body = req.body;
13337
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
13338
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
13339
+ return;
13340
+ }
13341
+ const updated = {
13342
+ ...existing,
13343
+ name: body.name?.trim() || existing.name,
13344
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
13345
+ prompt: body.prompt?.trim() || existing.prompt,
13346
+ enabled: body.enabled ?? existing.enabled
13347
+ };
13348
+ try {
13349
+ this.scheduler.add(updated);
13350
+ if (!updated.enabled) this.scheduler.unschedule(id);
13351
+ res.json(updated);
13352
+ } catch (err) {
13353
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13354
+ }
13355
+ });
13356
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
13357
+ try {
13358
+ this.scheduler.remove(req.params.id);
13359
+ res.json({ ok: true });
13360
+ } catch (err) {
13361
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13362
+ }
13363
+ });
13364
+ this.app.get("/api/knowledge", auth, (_req, res) => {
13365
+ try {
13366
+ const ws = new WorldStateDB(this.workspacePath);
13367
+ try {
13368
+ const facts = ws.getAllFacts();
13369
+ res.json({ total: facts.length, facts });
13370
+ } finally {
13371
+ ws.close();
13372
+ }
13373
+ } catch (err) {
13374
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13375
+ }
13376
+ });
13377
+ this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
13378
+ const body = req.body;
13379
+ if (!body.entity || !body.relation) {
13380
+ res.status(400).json({ error: "entity and relation are required" });
13381
+ return;
13382
+ }
13383
+ try {
13384
+ const ws = new WorldStateDB(this.workspacePath);
13385
+ try {
13386
+ res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
13387
+ } finally {
13388
+ ws.close();
13389
+ }
13390
+ } catch (err) {
13391
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13392
+ }
13393
+ });
13394
+ this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
13395
+ try {
13396
+ const ws = new WorldStateDB(this.workspacePath);
13397
+ try {
13398
+ res.json({ ok: true, deleted: ws.clearFacts() });
13399
+ } finally {
13400
+ ws.close();
13401
+ }
13402
+ } catch (err) {
13403
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13404
+ }
13405
+ });
13406
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
13407
+ try {
13408
+ const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
13409
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
13410
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
13411
+ const logger = new AuditLogger3(this.workspacePath);
13412
+ try {
13413
+ const all = logger.getAllLogs();
13414
+ const total = all.length;
13415
+ const entries = all.reverse().slice(offset, offset + limit);
13416
+ res.json({ total, offset, entries });
13417
+ } finally {
13418
+ logger.close();
13419
+ }
13420
+ } catch (err) {
13421
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
13422
+ }
13423
+ });
13424
+ const prodPath = path21__default.default.resolve(__dirname$1, "../web/dist");
13425
+ const devPath = path21__default.default.resolve(__dirname$1, "../../web/dist");
13426
+ const webDistPath = fs20__default.default.existsSync(prodPath) ? prodPath : devPath;
13427
+ if (fs20__default.default.existsSync(webDistPath)) {
12875
13428
  this.app.use(express__default.default.static(webDistPath));
12876
13429
  this.app.get("*", (_req, res) => {
12877
- res.sendFile(path20__default.default.join(webDistPath, "index.html"));
13430
+ res.sendFile(path21__default.default.join(webDistPath, "index.html"));
12878
13431
  });
12879
13432
  } else {
12880
13433
  this.app.get("/", (_req, res) => {
@@ -12889,68 +13442,6 @@ ${prompt}`;
12889
13442
  }
12890
13443
  }
12891
13444
  };
12892
- var TaskScheduler = class {
12893
- cronJobs = /* @__PURE__ */ new Map();
12894
- store;
12895
- runner;
12896
- constructor(store, runner) {
12897
- this.store = store;
12898
- this.runner = runner;
12899
- }
12900
- start() {
12901
- const tasks = this.store.listScheduledTasks();
12902
- for (const task of tasks) {
12903
- if (task.enabled) this.schedule(task);
12904
- }
12905
- }
12906
- stop() {
12907
- for (const job of this.cronJobs.values()) job.stop();
12908
- this.cronJobs.clear();
12909
- }
12910
- schedule(task) {
12911
- if (!cron__default.default.validate(task.cronExpression)) {
12912
- throw new Error(`Invalid cron expression: ${task.cronExpression}`);
12913
- }
12914
- const existing = this.cronJobs.get(task.id);
12915
- if (existing) {
12916
- try {
12917
- existing.stop();
12918
- } catch {
12919
- }
12920
- }
12921
- const job = cron__default.default.schedule(task.cronExpression, async () => {
12922
- try {
12923
- task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
12924
- this.store.saveScheduledTask(task);
12925
- await this.runner(task);
12926
- } catch (err) {
12927
- console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
12928
- }
12929
- }, { timezone: "UTC" });
12930
- this.cronJobs.set(task.id, job);
12931
- }
12932
- unschedule(taskId) {
12933
- this.cronJobs.get(taskId)?.stop();
12934
- this.cronJobs.delete(taskId);
12935
- }
12936
- add(task) {
12937
- this.store.saveScheduledTask(task);
12938
- if (task.enabled) this.schedule(task);
12939
- }
12940
- remove(taskId) {
12941
- this.unschedule(taskId);
12942
- this.store.deleteScheduledTask(taskId);
12943
- }
12944
- list() {
12945
- return this.store.listScheduledTasks();
12946
- }
12947
- isRunning(taskId) {
12948
- return this.cronJobs.has(taskId);
12949
- }
12950
- static validateCron(expression) {
12951
- return cron__default.default.validate(expression);
12952
- }
12953
- };
12954
13445
  var execFileAsync2 = util.promisify(child_process.execFile);
12955
13446
  var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
12956
13447
  function sanitizeEnvValue(v) {
@@ -13037,6 +13528,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
13037
13528
  exports.DEFAULT_THEME = DEFAULT_THEME;
13038
13529
  exports.DashboardServer = DashboardServer;
13039
13530
  exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
13531
+ exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
13040
13532
  exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
13041
13533
  exports.GLOBAL_KEYSTORE_FILE = GLOBAL_KEYSTORE_FILE;
13042
13534
  exports.GLOBAL_RUNTIME_DB_FILE = GLOBAL_RUNTIME_DB_FILE;