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.js CHANGED
@@ -42,6 +42,7 @@ import bcrypt from 'bcryptjs';
42
42
  import { Server } from 'socket.io';
43
43
  import parser from 'socket.io-msgpack-parser';
44
44
  import jwt from 'jsonwebtoken';
45
+ import cron from 'node-cron';
45
46
 
46
47
  // Cascade AI — Multi-tier AI Orchestration System
47
48
  var __defProp = Object.defineProperty;
@@ -58,7 +59,7 @@ var __export = (target, all) => {
58
59
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
59
60
  var init_constants = __esm({
60
61
  "src/constants.ts"() {
61
- CASCADE_VERSION = "0.16.0";
62
+ CASCADE_VERSION = "0.17.0";
62
63
  CASCADE_CONFIG_FILE = ".cascade/config.json";
63
64
  CASCADE_DB_FILE = ".cascade/memory.db";
64
65
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -16036,6 +16037,25 @@ var DashboardSocket = class {
16036
16037
  });
16037
16038
  });
16038
16039
  }
16040
+ /**
16041
+ * Boardroom plan decisions from a connected client. The desktop shows a
16042
+ * plan-review modal on `plan:approval-required` and answers here; the
16043
+ * server routes the decision into the paused run via resolvePlanApproval.
16044
+ */
16045
+ onPlanDecision(callback) {
16046
+ this.io.on("connection", (socket) => {
16047
+ socket.on("plan:decision", (payload) => {
16048
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
16049
+ callback({
16050
+ sessionId: payload.sessionId,
16051
+ approved: payload.approved,
16052
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
16053
+ editedPlan: payload.editedPlan
16054
+ });
16055
+ }
16056
+ });
16057
+ });
16058
+ }
16039
16059
  onSessionSteer(callback) {
16040
16060
  this.io.on("connection", (socket) => {
16041
16061
  socket.on("session:steer", (payload) => {
@@ -16080,6 +16100,122 @@ var DashboardSocket = class {
16080
16100
 
16081
16101
  // src/dashboard/server.ts
16082
16102
  init_constants();
16103
+ var TaskScheduler = class {
16104
+ cronJobs = /* @__PURE__ */ new Map();
16105
+ store;
16106
+ runner;
16107
+ constructor(store, runner) {
16108
+ this.store = store;
16109
+ this.runner = runner;
16110
+ }
16111
+ start() {
16112
+ const tasks = this.store.listScheduledTasks();
16113
+ for (const task of tasks) {
16114
+ if (task.enabled) this.schedule(task);
16115
+ }
16116
+ }
16117
+ stop() {
16118
+ for (const job of this.cronJobs.values()) job.stop();
16119
+ this.cronJobs.clear();
16120
+ }
16121
+ schedule(task) {
16122
+ if (!cron.validate(task.cronExpression)) {
16123
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
16124
+ }
16125
+ const existing = this.cronJobs.get(task.id);
16126
+ if (existing) {
16127
+ try {
16128
+ existing.stop();
16129
+ } catch {
16130
+ }
16131
+ }
16132
+ const job = cron.schedule(task.cronExpression, async () => {
16133
+ try {
16134
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
16135
+ this.store.saveScheduledTask(task);
16136
+ await this.runner(task);
16137
+ } catch (err) {
16138
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
16139
+ }
16140
+ }, { timezone: "UTC" });
16141
+ this.cronJobs.set(task.id, job);
16142
+ }
16143
+ unschedule(taskId) {
16144
+ this.cronJobs.get(taskId)?.stop();
16145
+ this.cronJobs.delete(taskId);
16146
+ }
16147
+ add(task) {
16148
+ this.store.saveScheduledTask(task);
16149
+ if (task.enabled) this.schedule(task);
16150
+ }
16151
+ remove(taskId) {
16152
+ this.unschedule(taskId);
16153
+ this.store.deleteScheduledTask(taskId);
16154
+ }
16155
+ list() {
16156
+ return this.store.listScheduledTasks();
16157
+ }
16158
+ isRunning(taskId) {
16159
+ return this.cronJobs.has(taskId);
16160
+ }
16161
+ static validateCron(expression) {
16162
+ return cron.validate(expression);
16163
+ }
16164
+ };
16165
+
16166
+ // src/dashboard/cost-stats.ts
16167
+ var DAY_MS = 24 * 60 * 60 * 1e3;
16168
+ function utcDateKey(d) {
16169
+ return d.toISOString().slice(0, 10);
16170
+ }
16171
+ function aggregateCostStats(sessions, opts = {}) {
16172
+ const days = Math.max(1, opts.days ?? 30);
16173
+ const topN = Math.max(1, opts.topN ?? 8);
16174
+ const now = opts.now ?? /* @__PURE__ */ new Date();
16175
+ const buckets = /* @__PURE__ */ new Map();
16176
+ for (let i = days - 1; i >= 0; i--) {
16177
+ const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
16178
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
16179
+ }
16180
+ let totalCostUsd = 0;
16181
+ let totalTokens = 0;
16182
+ let totalRuns = 0;
16183
+ for (const s of sessions) {
16184
+ const cost = s.metadata?.totalCostUsd ?? 0;
16185
+ const tokens = s.metadata?.totalTokens ?? 0;
16186
+ const runs = s.metadata?.taskCount ?? 0;
16187
+ totalCostUsd += cost;
16188
+ totalTokens += tokens;
16189
+ totalRuns += runs;
16190
+ const when = new Date(s.updatedAt || s.createdAt);
16191
+ if (!Number.isNaN(when.getTime())) {
16192
+ const bucket = buckets.get(utcDateKey(when));
16193
+ if (bucket) {
16194
+ bucket.costUsd += cost;
16195
+ bucket.tokens += tokens;
16196
+ bucket.runs += runs;
16197
+ }
16198
+ }
16199
+ }
16200
+ 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) => ({
16201
+ sessionId: s.id,
16202
+ title: s.title,
16203
+ costUsd: s.metadata?.totalCostUsd ?? 0,
16204
+ tokens: s.metadata?.totalTokens ?? 0,
16205
+ runs: s.metadata?.taskCount ?? 0,
16206
+ updatedAt: s.updatedAt
16207
+ }));
16208
+ return {
16209
+ totalCostUsd,
16210
+ totalTokens,
16211
+ totalSessions: sessions.length,
16212
+ totalRuns,
16213
+ perDay: [...buckets.values()],
16214
+ topSessions
16215
+ };
16216
+ }
16217
+
16218
+ // src/dashboard/server.ts
16083
16219
  var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
16084
16220
  var DashboardServer = class {
16085
16221
  app;
@@ -16105,6 +16241,14 @@ var DashboardServer = class {
16105
16241
  * map is how that answer reaches the run that's blocked on it.
16106
16242
  */
16107
16243
  pendingApprovals = /* @__PURE__ */ new Map();
16244
+ /**
16245
+ * The orchestration decision trail ("why") of each session's most recent
16246
+ * run — captured when the run ends so the desktop's Why panel can show it
16247
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
16248
+ */
16249
+ whyBySession = /* @__PURE__ */ new Map();
16250
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
16251
+ scheduler;
16108
16252
  port;
16109
16253
  host;
16110
16254
  workspacePath;
@@ -16122,6 +16266,7 @@ var DashboardServer = class {
16122
16266
  secret: this.dashboardSecret,
16123
16267
  corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
16124
16268
  });
16269
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
16125
16270
  this.setupMiddleware();
16126
16271
  this.setupRoutes();
16127
16272
  this.socket.onSessionRate((sessionId, rating) => {
@@ -16185,6 +16330,9 @@ var DashboardServer = class {
16185
16330
  cascade.on("peer:message", (e) => {
16186
16331
  this.socket.emitPeerMessage(e);
16187
16332
  });
16333
+ cascade.on("plan:approval-required", (e) => {
16334
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
16335
+ });
16188
16336
  try {
16189
16337
  const result = await cascade.run({
16190
16338
  prompt: runPrompt,
@@ -16192,7 +16340,8 @@ var DashboardServer = class {
16192
16340
  approvalCallback: this.makeApprovalCallback(sessionId)
16193
16341
  });
16194
16342
  this.recordSessionTask(sessionId, result.taskId);
16195
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
16343
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16344
+ this.captureWhy(sessionId, cascade, result);
16196
16345
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
16197
16346
  this.socket.broadcast("cost:update", {
16198
16347
  sessionId,
@@ -16202,6 +16351,7 @@ var DashboardServer = class {
16202
16351
  this.throttledBroadcast("workspace");
16203
16352
  } catch (err) {
16204
16353
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16354
+ this.captureWhy(sessionId, cascade);
16205
16355
  this.socket.emitToSocket(socketId, "session:error", {
16206
16356
  sessionId,
16207
16357
  error: err instanceof Error ? err.message : String(err)
@@ -16212,9 +16362,17 @@ var DashboardServer = class {
16212
16362
  this.denyPendingApprovals(sessionId);
16213
16363
  }
16214
16364
  });
16365
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
16366
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
16367
+ approved,
16368
+ note,
16369
+ editedPlan
16370
+ );
16371
+ });
16215
16372
  this.socket.onSessionHalt((sessionId) => {
16216
16373
  this.activeControllers.get(sessionId)?.abort();
16217
16374
  this.denyPendingApprovals(sessionId);
16375
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
16218
16376
  });
16219
16377
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
16220
16378
  const pending = this.pendingApprovals.get(requestId);
@@ -16247,12 +16405,21 @@ var DashboardServer = class {
16247
16405
  resolve();
16248
16406
  });
16249
16407
  });
16408
+ try {
16409
+ this.scheduler.start();
16410
+ } catch (err) {
16411
+ console.warn("[dashboard] failed to start task scheduler:", err);
16412
+ }
16250
16413
  }
16251
16414
  async stop() {
16252
16415
  if (this.broadcastTimer) {
16253
16416
  clearTimeout(this.broadcastTimer);
16254
16417
  this.broadcastTimer = null;
16255
16418
  }
16419
+ try {
16420
+ this.scheduler.stop();
16421
+ } catch {
16422
+ }
16256
16423
  this.socket.close();
16257
16424
  try {
16258
16425
  this.globalStore?.close();
@@ -16373,16 +16540,60 @@ var DashboardServer = class {
16373
16540
  return title;
16374
16541
  }
16375
16542
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
16376
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
16543
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
16377
16544
  try {
16378
16545
  if (reply && reply.trim()) {
16379
16546
  this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
16380
16547
  }
16548
+ if (result) {
16549
+ const session = this.store.getSession(sessionId);
16550
+ if (session) {
16551
+ this.store.updateSession(sessionId, {
16552
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
16553
+ metadata: {
16554
+ ...session.metadata,
16555
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
16556
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
16557
+ taskCount: session.metadata.taskCount + 1
16558
+ }
16559
+ });
16560
+ }
16561
+ }
16381
16562
  } catch (err) {
16382
16563
  console.warn("[dashboard] failed to persist run end:", err);
16383
16564
  }
16384
16565
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
16385
16566
  }
16567
+ /**
16568
+ * Capture the run's decision trail + router economics ("why") and broadcast
16569
+ * it so the desktop's Why panel updates live; kept per-session for the
16570
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
16571
+ */
16572
+ captureWhy(sessionId, cascade, result) {
16573
+ try {
16574
+ const stats = cascade.getRouter().getStats();
16575
+ const savings = cascade.getRouter().getDelegationSavings();
16576
+ const report = {
16577
+ sessionId,
16578
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
16579
+ decisions: cascade.getDecisionLog(),
16580
+ savedUsd: savings.savedUsd,
16581
+ savedPct: savings.savedPct,
16582
+ totalCostUsd: stats.totalCostUsd,
16583
+ totalTokens: stats.totalTokens,
16584
+ costByTier: stats.costByTier,
16585
+ durationMs: result?.durationMs
16586
+ };
16587
+ this.whyBySession.set(sessionId, report);
16588
+ if (this.whyBySession.size > 50) {
16589
+ const oldest = this.whyBySession.keys().next().value;
16590
+ if (oldest) this.whyBySession.delete(oldest);
16591
+ }
16592
+ this.socket.broadcast("run:why", report);
16593
+ } catch (err) {
16594
+ console.warn("[dashboard] failed to capture decision trail:", err);
16595
+ }
16596
+ }
16386
16597
  /**
16387
16598
  * Route steering text into running Cascade instances. Targets the given
16388
16599
  * session, or every active session when none is specified (the desktop
@@ -16412,6 +16623,52 @@ var DashboardServer = class {
16412
16623
  this.pendingApprovals.set(request.id, { resolve, sessionId });
16413
16624
  });
16414
16625
  }
16626
+ /**
16627
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
16628
+ * tool approvals are auto-granted since nobody may be watching when the cron
16629
+ * fires — the Schedules UI states this. Events broadcast to every connected
16630
+ * client so an open desktop sees the run appear live.
16631
+ */
16632
+ async runScheduledTask(task) {
16633
+ const sessionId = randomUUID();
16634
+ const prompt = task.prompt;
16635
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
16636
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
16637
+ this.activeSessions.set(sessionId, cascade);
16638
+ cascade.on("tier:status", (e) => {
16639
+ this.socket.broadcast("tier:status", { sessionId, ...e });
16640
+ });
16641
+ cascade.on("peer:message", (e) => {
16642
+ this.socket.emitPeerMessage(e);
16643
+ });
16644
+ try {
16645
+ const result = await cascade.run({
16646
+ prompt,
16647
+ identityId: task.identityId,
16648
+ approvalCallback: async () => ({ approved: true, always: false })
16649
+ });
16650
+ this.recordSessionTask(sessionId, result.taskId);
16651
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
16652
+ this.captureWhy(sessionId, cascade, result);
16653
+ this.socket.broadcast("session:complete", { sessionId, result });
16654
+ this.socket.broadcast("cost:update", {
16655
+ sessionId,
16656
+ totalTokens: result.usage.totalTokens,
16657
+ totalCostUsd: result.usage.estimatedCostUsd
16658
+ });
16659
+ this.throttledBroadcast("workspace");
16660
+ } catch (err) {
16661
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
16662
+ this.captureWhy(sessionId, cascade);
16663
+ this.socket.broadcast("session:error", {
16664
+ sessionId,
16665
+ error: err instanceof Error ? err.message : String(err)
16666
+ });
16667
+ throw err;
16668
+ } finally {
16669
+ this.activeSessions.delete(sessionId);
16670
+ }
16671
+ }
16415
16672
  /** Deny + clear any approvals still pending for a session (run end / abort). */
16416
16673
  denyPendingApprovals(sessionId) {
16417
16674
  for (const [id, pending] of this.pendingApprovals) {
@@ -16924,6 +17181,9 @@ ${prompt}`;
16924
17181
  cascade.on("peer:message", (e) => {
16925
17182
  this.socket.emitPeerMessage(e);
16926
17183
  });
17184
+ cascade.on("plan:approval-required", (e) => {
17185
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
17186
+ });
16927
17187
  try {
16928
17188
  const result = await cascade.run({
16929
17189
  prompt: runPrompt,
@@ -16931,7 +17191,8 @@ ${prompt}`;
16931
17191
  approvalCallback: this.makeApprovalCallback(sessionId)
16932
17192
  });
16933
17193
  this.recordSessionTask(sessionId, result.taskId);
16934
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
17194
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
17195
+ this.captureWhy(sessionId, cascade, result);
16935
17196
  this.socket.broadcast("cost:update", {
16936
17197
  sessionId,
16937
17198
  totalTokens: result.usage.totalTokens,
@@ -16941,6 +17202,7 @@ ${prompt}`;
16941
17202
  this.throttledBroadcast("workspace");
16942
17203
  } catch (err) {
16943
17204
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
17205
+ this.captureWhy(sessionId, cascade);
16944
17206
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
16945
17207
  sessionId,
16946
17208
  error: err instanceof Error ? err.message : String(err)
@@ -16962,6 +17224,168 @@ ${prompt}`;
16962
17224
  }))
16963
17225
  });
16964
17226
  });
17227
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
17228
+ const report = this.whyBySession.get(req.params.id);
17229
+ if (!report) {
17230
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
17231
+ return;
17232
+ }
17233
+ res.json(report);
17234
+ });
17235
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
17236
+ const sessionId = req.params.id;
17237
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17238
+ const before = /* @__PURE__ */ new Map();
17239
+ for (const taskId of taskIds) {
17240
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
17241
+ if (!before.has(filePath)) before.set(filePath, content);
17242
+ }
17243
+ }
17244
+ const { readFile } = await import('fs/promises');
17245
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
17246
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
17247
+ let after = "";
17248
+ let missing = false;
17249
+ try {
17250
+ const buf = await readFile(filePath);
17251
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
17252
+ } catch {
17253
+ missing = true;
17254
+ }
17255
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
17256
+ }));
17257
+ res.json({ sessionId, changes: changes.filter((c) => c.changed) });
17258
+ });
17259
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
17260
+ const sessionId = req.params.id;
17261
+ const body = req.body;
17262
+ if (!body.filePath || typeof body.filePath !== "string") {
17263
+ res.status(400).json({ error: "filePath is required" });
17264
+ return;
17265
+ }
17266
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
17267
+ let content;
17268
+ for (const taskId of taskIds) {
17269
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
17270
+ if (snap) {
17271
+ content = snap.content;
17272
+ break;
17273
+ }
17274
+ }
17275
+ if (content === void 0) {
17276
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
17277
+ return;
17278
+ }
17279
+ try {
17280
+ const { writeFile } = await import('fs/promises');
17281
+ await writeFile(body.filePath, content, "utf-8");
17282
+ res.json({ ok: true, filePath: body.filePath });
17283
+ } catch (err) {
17284
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17285
+ }
17286
+ });
17287
+ this.app.get("/api/costs", auth, (_req, res) => {
17288
+ try {
17289
+ const sessions = this.store.listSessions(void 0, 1e3);
17290
+ const budget = this.config.budget ?? { warnAtPct: 80 };
17291
+ res.json({
17292
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
17293
+ budget: {
17294
+ dailyBudgetUsd: budget.dailyBudgetUsd,
17295
+ sessionBudgetUsd: budget.sessionBudgetUsd,
17296
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
17297
+ }
17298
+ });
17299
+ } catch (err) {
17300
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17301
+ }
17302
+ });
17303
+ this.app.get("/api/schedules", auth, (_req, res) => {
17304
+ try {
17305
+ res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
17306
+ } catch (err) {
17307
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17308
+ }
17309
+ });
17310
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
17311
+ const body = req.body;
17312
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
17313
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
17314
+ return;
17315
+ }
17316
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
17317
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17318
+ return;
17319
+ }
17320
+ const task = {
17321
+ id: randomUUID(),
17322
+ name: body.name.trim(),
17323
+ cronExpression: body.cronExpression.trim(),
17324
+ prompt: body.prompt.trim(),
17325
+ workspacePath: this.workspacePath,
17326
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
17327
+ enabled: body.enabled !== false
17328
+ };
17329
+ try {
17330
+ this.scheduler.add(task);
17331
+ res.json(task);
17332
+ } catch (err) {
17333
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17334
+ }
17335
+ });
17336
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17337
+ const id = req.params.id;
17338
+ const existing = this.scheduler.list().find((t) => t.id === id);
17339
+ if (!existing) {
17340
+ res.status(404).json({ error: "Not found" });
17341
+ return;
17342
+ }
17343
+ const body = req.body;
17344
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
17345
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
17346
+ return;
17347
+ }
17348
+ const updated = {
17349
+ ...existing,
17350
+ name: body.name?.trim() || existing.name,
17351
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
17352
+ prompt: body.prompt?.trim() || existing.prompt,
17353
+ enabled: body.enabled ?? existing.enabled
17354
+ };
17355
+ try {
17356
+ this.scheduler.add(updated);
17357
+ if (!updated.enabled) this.scheduler.unschedule(id);
17358
+ res.json(updated);
17359
+ } catch (err) {
17360
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17361
+ }
17362
+ });
17363
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
17364
+ try {
17365
+ this.scheduler.remove(req.params.id);
17366
+ res.json({ ok: true });
17367
+ } catch (err) {
17368
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17369
+ }
17370
+ });
17371
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
17372
+ try {
17373
+ const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
17374
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
17375
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
17376
+ const logger = new AuditLogger3(this.workspacePath);
17377
+ try {
17378
+ const all = logger.getAllLogs();
17379
+ const total = all.length;
17380
+ const entries = all.reverse().slice(offset, offset + limit);
17381
+ res.json({ total, offset, entries });
17382
+ } finally {
17383
+ logger.close();
17384
+ }
17385
+ } catch (err) {
17386
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
17387
+ }
17388
+ });
16965
17389
  const prodPath = path25.resolve(__dirname$1, "../web/dist");
16966
17390
  const devPath = path25.resolve(__dirname$1, "../../web/dist");
16967
17391
  const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;