cascade-ai 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -43,6 +43,7 @@ var bcrypt = require('bcryptjs');
43
43
  var socket_io = require('socket.io');
44
44
  var parser = require('socket.io-msgpack-parser');
45
45
  var jwt = require('jsonwebtoken');
46
+ var cron = require('node-cron');
46
47
 
47
48
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
48
49
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -93,6 +94,7 @@ var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
93
94
  var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
94
95
  var parser__default = /*#__PURE__*/_interopDefault(parser);
95
96
  var jwt__default = /*#__PURE__*/_interopDefault(jwt);
97
+ var cron__default = /*#__PURE__*/_interopDefault(cron);
96
98
 
97
99
  // Cascade AI — Multi-tier AI Orchestration System
98
100
  var __defProp = Object.defineProperty;
@@ -109,7 +111,7 @@ var __export = (target, all) => {
109
111
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
110
112
  var init_constants = __esm({
111
113
  "src/constants.ts"() {
112
- CASCADE_VERSION = "0.16.0";
114
+ CASCADE_VERSION = "0.17.0";
113
115
  CASCADE_CONFIG_FILE = ".cascade/config.json";
114
116
  CASCADE_DB_FILE = ".cascade/memory.db";
115
117
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -16087,6 +16089,25 @@ var DashboardSocket = class {
16087
16089
  });
16088
16090
  });
16089
16091
  }
16092
+ /**
16093
+ * Boardroom plan decisions from a connected client. The desktop shows a
16094
+ * plan-review modal on `plan:approval-required` and answers here; the
16095
+ * server routes the decision into the paused run via resolvePlanApproval.
16096
+ */
16097
+ onPlanDecision(callback) {
16098
+ this.io.on("connection", (socket) => {
16099
+ socket.on("plan:decision", (payload) => {
16100
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
16101
+ callback({
16102
+ sessionId: payload.sessionId,
16103
+ approved: payload.approved,
16104
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
16105
+ editedPlan: payload.editedPlan
16106
+ });
16107
+ }
16108
+ });
16109
+ });
16110
+ }
16090
16111
  onSessionSteer(callback) {
16091
16112
  this.io.on("connection", (socket) => {
16092
16113
  socket.on("session:steer", (payload) => {
@@ -16131,6 +16152,122 @@ var DashboardSocket = class {
16131
16152
 
16132
16153
  // src/dashboard/server.ts
16133
16154
  init_constants();
16155
+ var TaskScheduler = class {
16156
+ cronJobs = /* @__PURE__ */ new Map();
16157
+ store;
16158
+ runner;
16159
+ constructor(store, runner) {
16160
+ this.store = store;
16161
+ this.runner = runner;
16162
+ }
16163
+ start() {
16164
+ const tasks = this.store.listScheduledTasks();
16165
+ for (const task of tasks) {
16166
+ if (task.enabled) this.schedule(task);
16167
+ }
16168
+ }
16169
+ stop() {
16170
+ for (const job of this.cronJobs.values()) job.stop();
16171
+ this.cronJobs.clear();
16172
+ }
16173
+ schedule(task) {
16174
+ if (!cron__default.default.validate(task.cronExpression)) {
16175
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
16176
+ }
16177
+ const existing = this.cronJobs.get(task.id);
16178
+ if (existing) {
16179
+ try {
16180
+ existing.stop();
16181
+ } catch {
16182
+ }
16183
+ }
16184
+ const job = cron__default.default.schedule(task.cronExpression, async () => {
16185
+ try {
16186
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
16187
+ this.store.saveScheduledTask(task);
16188
+ await this.runner(task);
16189
+ } catch (err) {
16190
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
16191
+ }
16192
+ }, { timezone: "UTC" });
16193
+ this.cronJobs.set(task.id, job);
16194
+ }
16195
+ unschedule(taskId) {
16196
+ this.cronJobs.get(taskId)?.stop();
16197
+ this.cronJobs.delete(taskId);
16198
+ }
16199
+ add(task) {
16200
+ this.store.saveScheduledTask(task);
16201
+ if (task.enabled) this.schedule(task);
16202
+ }
16203
+ remove(taskId) {
16204
+ this.unschedule(taskId);
16205
+ this.store.deleteScheduledTask(taskId);
16206
+ }
16207
+ list() {
16208
+ return this.store.listScheduledTasks();
16209
+ }
16210
+ isRunning(taskId) {
16211
+ return this.cronJobs.has(taskId);
16212
+ }
16213
+ static validateCron(expression) {
16214
+ return cron__default.default.validate(expression);
16215
+ }
16216
+ };
16217
+
16218
+ // src/dashboard/cost-stats.ts
16219
+ var DAY_MS = 24 * 60 * 60 * 1e3;
16220
+ function utcDateKey(d) {
16221
+ return d.toISOString().slice(0, 10);
16222
+ }
16223
+ function aggregateCostStats(sessions, opts = {}) {
16224
+ const days = Math.max(1, opts.days ?? 30);
16225
+ const topN = Math.max(1, opts.topN ?? 8);
16226
+ const now = opts.now ?? /* @__PURE__ */ new Date();
16227
+ const buckets = /* @__PURE__ */ new Map();
16228
+ for (let i = days - 1; i >= 0; i--) {
16229
+ const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
16230
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
16231
+ }
16232
+ let totalCostUsd = 0;
16233
+ let totalTokens = 0;
16234
+ let totalRuns = 0;
16235
+ for (const s of sessions) {
16236
+ const cost = s.metadata?.totalCostUsd ?? 0;
16237
+ const tokens = s.metadata?.totalTokens ?? 0;
16238
+ const runs = s.metadata?.taskCount ?? 0;
16239
+ totalCostUsd += cost;
16240
+ totalTokens += tokens;
16241
+ totalRuns += runs;
16242
+ const when = new Date(s.updatedAt || s.createdAt);
16243
+ if (!Number.isNaN(when.getTime())) {
16244
+ const bucket = buckets.get(utcDateKey(when));
16245
+ if (bucket) {
16246
+ bucket.costUsd += cost;
16247
+ bucket.tokens += tokens;
16248
+ bucket.runs += runs;
16249
+ }
16250
+ }
16251
+ }
16252
+ 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) => ({
16253
+ sessionId: s.id,
16254
+ title: s.title,
16255
+ costUsd: s.metadata?.totalCostUsd ?? 0,
16256
+ tokens: s.metadata?.totalTokens ?? 0,
16257
+ runs: s.metadata?.taskCount ?? 0,
16258
+ updatedAt: s.updatedAt
16259
+ }));
16260
+ return {
16261
+ totalCostUsd,
16262
+ totalTokens,
16263
+ totalSessions: sessions.length,
16264
+ totalRuns,
16265
+ perDay: [...buckets.values()],
16266
+ topSessions
16267
+ };
16268
+ }
16269
+
16270
+ // src/dashboard/server.ts
16134
16271
  var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
16135
16272
  var DashboardServer = class {
16136
16273
  app;
@@ -16156,6 +16293,14 @@ var DashboardServer = class {
16156
16293
  * map is how that answer reaches the run that's blocked on it.
16157
16294
  */
16158
16295
  pendingApprovals = /* @__PURE__ */ new Map();
16296
+ /**
16297
+ * The orchestration decision trail ("why") of each session's most recent
16298
+ * run — captured when the run ends so the desktop's Why panel can show it
16299
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
16300
+ */
16301
+ whyBySession = /* @__PURE__ */ new Map();
16302
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
16303
+ scheduler;
16159
16304
  port;
16160
16305
  host;
16161
16306
  workspacePath;
@@ -16173,6 +16318,7 @@ var DashboardServer = class {
16173
16318
  secret: this.dashboardSecret,
16174
16319
  corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
16175
16320
  });
16321
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
16176
16322
  this.setupMiddleware();
16177
16323
  this.setupRoutes();
16178
16324
  this.socket.onSessionRate((sessionId, rating) => {
@@ -16236,6 +16382,9 @@ var DashboardServer = class {
16236
16382
  cascade.on("peer:message", (e) => {
16237
16383
  this.socket.emitPeerMessage(e);
16238
16384
  });
16385
+ cascade.on("plan:approval-required", (e) => {
16386
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
16387
+ });
16239
16388
  try {
16240
16389
  const result = await cascade.run({
16241
16390
  prompt: runPrompt,
@@ -16243,7 +16392,8 @@ var DashboardServer = class {
16243
16392
  approvalCallback: this.makeApprovalCallback(sessionId)
16244
16393
  });
16245
16394
  this.recordSessionTask(sessionId, result.taskId);
16246
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
16395
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16396
+ this.captureWhy(sessionId, cascade, result);
16247
16397
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
16248
16398
  this.socket.broadcast("cost:update", {
16249
16399
  sessionId,
@@ -16253,6 +16403,7 @@ var DashboardServer = class {
16253
16403
  this.throttledBroadcast("workspace");
16254
16404
  } catch (err) {
16255
16405
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16406
+ this.captureWhy(sessionId, cascade);
16256
16407
  this.socket.emitToSocket(socketId, "session:error", {
16257
16408
  sessionId,
16258
16409
  error: err instanceof Error ? err.message : String(err)
@@ -16263,9 +16414,17 @@ var DashboardServer = class {
16263
16414
  this.denyPendingApprovals(sessionId);
16264
16415
  }
16265
16416
  });
16417
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
16418
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
16419
+ approved,
16420
+ note,
16421
+ editedPlan
16422
+ );
16423
+ });
16266
16424
  this.socket.onSessionHalt((sessionId) => {
16267
16425
  this.activeControllers.get(sessionId)?.abort();
16268
16426
  this.denyPendingApprovals(sessionId);
16427
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
16269
16428
  });
16270
16429
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
16271
16430
  const pending = this.pendingApprovals.get(requestId);
@@ -16298,12 +16457,21 @@ var DashboardServer = class {
16298
16457
  resolve();
16299
16458
  });
16300
16459
  });
16460
+ try {
16461
+ this.scheduler.start();
16462
+ } catch (err) {
16463
+ console.warn("[dashboard] failed to start task scheduler:", err);
16464
+ }
16301
16465
  }
16302
16466
  async stop() {
16303
16467
  if (this.broadcastTimer) {
16304
16468
  clearTimeout(this.broadcastTimer);
16305
16469
  this.broadcastTimer = null;
16306
16470
  }
16471
+ try {
16472
+ this.scheduler.stop();
16473
+ } catch {
16474
+ }
16307
16475
  this.socket.close();
16308
16476
  try {
16309
16477
  this.globalStore?.close();
@@ -16424,16 +16592,60 @@ var DashboardServer = class {
16424
16592
  return title;
16425
16593
  }
16426
16594
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
16427
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
16595
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
16428
16596
  try {
16429
16597
  if (reply && reply.trim()) {
16430
16598
  this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
16431
16599
  }
16600
+ if (result) {
16601
+ const session = this.store.getSession(sessionId);
16602
+ if (session) {
16603
+ this.store.updateSession(sessionId, {
16604
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16605
+ metadata: {
16606
+ ...session.metadata,
16607
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
16608
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
16609
+ taskCount: session.metadata.taskCount + 1
16610
+ }
16611
+ });
16612
+ }
16613
+ }
16432
16614
  } catch (err) {
16433
16615
  console.warn("[dashboard] failed to persist run end:", err);
16434
16616
  }
16435
16617
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
16436
16618
  }
16619
+ /**
16620
+ * Capture the run's decision trail + router economics ("why") and broadcast
16621
+ * it so the desktop's Why panel updates live; kept per-session for the
16622
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
16623
+ */
16624
+ captureWhy(sessionId, cascade, result) {
16625
+ try {
16626
+ const stats = cascade.getRouter().getStats();
16627
+ const savings = cascade.getRouter().getDelegationSavings();
16628
+ const report = {
16629
+ sessionId,
16630
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
16631
+ decisions: cascade.getDecisionLog(),
16632
+ savedUsd: savings.savedUsd,
16633
+ savedPct: savings.savedPct,
16634
+ totalCostUsd: stats.totalCostUsd,
16635
+ totalTokens: stats.totalTokens,
16636
+ costByTier: stats.costByTier,
16637
+ durationMs: result?.durationMs
16638
+ };
16639
+ this.whyBySession.set(sessionId, report);
16640
+ if (this.whyBySession.size > 50) {
16641
+ const oldest = this.whyBySession.keys().next().value;
16642
+ if (oldest) this.whyBySession.delete(oldest);
16643
+ }
16644
+ this.socket.broadcast("run:why", report);
16645
+ } catch (err) {
16646
+ console.warn("[dashboard] failed to capture decision trail:", err);
16647
+ }
16648
+ }
16437
16649
  /**
16438
16650
  * Route steering text into running Cascade instances. Targets the given
16439
16651
  * session, or every active session when none is specified (the desktop
@@ -16463,6 +16675,52 @@ var DashboardServer = class {
16463
16675
  this.pendingApprovals.set(request.id, { resolve, sessionId });
16464
16676
  });
16465
16677
  }
16678
+ /**
16679
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
16680
+ * tool approvals are auto-granted since nobody may be watching when the cron
16681
+ * fires — the Schedules UI states this. Events broadcast to every connected
16682
+ * client so an open desktop sees the run appear live.
16683
+ */
16684
+ async runScheduledTask(task) {
16685
+ const sessionId = crypto.randomUUID();
16686
+ const prompt = task.prompt;
16687
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
16688
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
16689
+ this.activeSessions.set(sessionId, cascade);
16690
+ cascade.on("tier:status", (e) => {
16691
+ this.socket.broadcast("tier:status", { sessionId, ...e });
16692
+ });
16693
+ cascade.on("peer:message", (e) => {
16694
+ this.socket.emitPeerMessage(e);
16695
+ });
16696
+ try {
16697
+ const result = await cascade.run({
16698
+ prompt,
16699
+ identityId: task.identityId,
16700
+ approvalCallback: async () => ({ approved: true, always: false })
16701
+ });
16702
+ this.recordSessionTask(sessionId, result.taskId);
16703
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16704
+ this.captureWhy(sessionId, cascade, result);
16705
+ this.socket.broadcast("session:complete", { sessionId, result });
16706
+ this.socket.broadcast("cost:update", {
16707
+ sessionId,
16708
+ totalTokens: result.usage.totalTokens,
16709
+ totalCostUsd: result.usage.estimatedCostUsd
16710
+ });
16711
+ this.throttledBroadcast("workspace");
16712
+ } catch (err) {
16713
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16714
+ this.captureWhy(sessionId, cascade);
16715
+ this.socket.broadcast("session:error", {
16716
+ sessionId,
16717
+ error: err instanceof Error ? err.message : String(err)
16718
+ });
16719
+ throw err;
16720
+ } finally {
16721
+ this.activeSessions.delete(sessionId);
16722
+ }
16723
+ }
16466
16724
  /** Deny + clear any approvals still pending for a session (run end / abort). */
16467
16725
  denyPendingApprovals(sessionId) {
16468
16726
  for (const [id, pending] of this.pendingApprovals) {
@@ -16975,6 +17233,9 @@ ${prompt}`;
16975
17233
  cascade.on("peer:message", (e) => {
16976
17234
  this.socket.emitPeerMessage(e);
16977
17235
  });
17236
+ cascade.on("plan:approval-required", (e) => {
17237
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
17238
+ });
16978
17239
  try {
16979
17240
  const result = await cascade.run({
16980
17241
  prompt: runPrompt,
@@ -16982,7 +17243,8 @@ ${prompt}`;
16982
17243
  approvalCallback: this.makeApprovalCallback(sessionId)
16983
17244
  });
16984
17245
  this.recordSessionTask(sessionId, result.taskId);
16985
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
17246
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
17247
+ this.captureWhy(sessionId, cascade, result);
16986
17248
  this.socket.broadcast("cost:update", {
16987
17249
  sessionId,
16988
17250
  totalTokens: result.usage.totalTokens,
@@ -16992,6 +17254,7 @@ ${prompt}`;
16992
17254
  this.throttledBroadcast("workspace");
16993
17255
  } catch (err) {
16994
17256
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
17257
+ this.captureWhy(sessionId, cascade);
16995
17258
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
16996
17259
  sessionId,
16997
17260
  error: err instanceof Error ? err.message : String(err)
@@ -17013,6 +17276,168 @@ ${prompt}`;
17013
17276
  }))
17014
17277
  });
17015
17278
  });
17279
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
17280
+ const report = this.whyBySession.get(req.params.id);
17281
+ if (!report) {
17282
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
17283
+ return;
17284
+ }
17285
+ res.json(report);
17286
+ });
17287
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
17288
+ const sessionId = req.params.id;
17289
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17290
+ const before = /* @__PURE__ */ new Map();
17291
+ for (const taskId of taskIds) {
17292
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
17293
+ if (!before.has(filePath)) before.set(filePath, content);
17294
+ }
17295
+ }
17296
+ const { readFile } = await import('fs/promises');
17297
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
17298
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
17299
+ let after = "";
17300
+ let missing = false;
17301
+ try {
17302
+ const buf = await readFile(filePath);
17303
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
17304
+ } catch {
17305
+ missing = true;
17306
+ }
17307
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
17308
+ }));
17309
+ res.json({ sessionId, changes: changes.filter((c) => c.changed) });
17310
+ });
17311
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
17312
+ const sessionId = req.params.id;
17313
+ const body = req.body;
17314
+ if (!body.filePath || typeof body.filePath !== "string") {
17315
+ res.status(400).json({ error: "filePath is required" });
17316
+ return;
17317
+ }
17318
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17319
+ let content;
17320
+ for (const taskId of taskIds) {
17321
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
17322
+ if (snap) {
17323
+ content = snap.content;
17324
+ break;
17325
+ }
17326
+ }
17327
+ if (content === void 0) {
17328
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
17329
+ return;
17330
+ }
17331
+ try {
17332
+ const { writeFile } = await import('fs/promises');
17333
+ await writeFile(body.filePath, content, "utf-8");
17334
+ res.json({ ok: true, filePath: body.filePath });
17335
+ } catch (err) {
17336
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17337
+ }
17338
+ });
17339
+ this.app.get("/api/costs", auth, (_req, res) => {
17340
+ try {
17341
+ const sessions = this.store.listSessions(void 0, 1e3);
17342
+ const budget = this.config.budget ?? { warnAtPct: 80 };
17343
+ res.json({
17344
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
17345
+ budget: {
17346
+ dailyBudgetUsd: budget.dailyBudgetUsd,
17347
+ sessionBudgetUsd: budget.sessionBudgetUsd,
17348
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
17349
+ }
17350
+ });
17351
+ } catch (err) {
17352
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17353
+ }
17354
+ });
17355
+ this.app.get("/api/schedules", auth, (_req, res) => {
17356
+ try {
17357
+ res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
17358
+ } catch (err) {
17359
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17360
+ }
17361
+ });
17362
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
17363
+ const body = req.body;
17364
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
17365
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
17366
+ return;
17367
+ }
17368
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
17369
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17370
+ return;
17371
+ }
17372
+ const task = {
17373
+ id: crypto.randomUUID(),
17374
+ name: body.name.trim(),
17375
+ cronExpression: body.cronExpression.trim(),
17376
+ prompt: body.prompt.trim(),
17377
+ workspacePath: this.workspacePath,
17378
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17379
+ enabled: body.enabled !== false
17380
+ };
17381
+ try {
17382
+ this.scheduler.add(task);
17383
+ res.json(task);
17384
+ } catch (err) {
17385
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17386
+ }
17387
+ });
17388
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17389
+ const id = req.params.id;
17390
+ const existing = this.scheduler.list().find((t) => t.id === id);
17391
+ if (!existing) {
17392
+ res.status(404).json({ error: "Not found" });
17393
+ return;
17394
+ }
17395
+ const body = req.body;
17396
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
17397
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17398
+ return;
17399
+ }
17400
+ const updated = {
17401
+ ...existing,
17402
+ name: body.name?.trim() || existing.name,
17403
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
17404
+ prompt: body.prompt?.trim() || existing.prompt,
17405
+ enabled: body.enabled ?? existing.enabled
17406
+ };
17407
+ try {
17408
+ this.scheduler.add(updated);
17409
+ if (!updated.enabled) this.scheduler.unschedule(id);
17410
+ res.json(updated);
17411
+ } catch (err) {
17412
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17413
+ }
17414
+ });
17415
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17416
+ try {
17417
+ this.scheduler.remove(req.params.id);
17418
+ res.json({ ok: true });
17419
+ } catch (err) {
17420
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17421
+ }
17422
+ });
17423
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
17424
+ try {
17425
+ const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
17426
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
17427
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
17428
+ const logger = new AuditLogger3(this.workspacePath);
17429
+ try {
17430
+ const all = logger.getAllLogs();
17431
+ const total = all.length;
17432
+ const entries = all.reverse().slice(offset, offset + limit);
17433
+ res.json({ total, offset, entries });
17434
+ } finally {
17435
+ logger.close();
17436
+ }
17437
+ } catch (err) {
17438
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17439
+ }
17440
+ });
17016
17441
  const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
17017
17442
  const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
17018
17443
  const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;