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.
@@ -222595,7 +222595,7 @@ Anthropic.Models = Models2;
222595
222595
  Anthropic.Beta = Beta;
222596
222596
 
222597
222597
  // src/constants.ts
222598
- var CASCADE_VERSION = "0.16.0";
222598
+ var CASCADE_VERSION = "0.17.0";
222599
222599
  var CASCADE_CONFIG_DIR = ".cascade";
222600
222600
  var CASCADE_MD_FILE = "CASCADE.md";
222601
222601
  var CASCADE_IGNORE_FILE = ".cascadeignore";
@@ -299170,6 +299170,25 @@ var DashboardSocket = class {
299170
299170
  });
299171
299171
  });
299172
299172
  }
299173
+ /**
299174
+ * Boardroom plan decisions from a connected client. The desktop shows a
299175
+ * plan-review modal on `plan:approval-required` and answers here; the
299176
+ * server routes the decision into the paused run via resolvePlanApproval.
299177
+ */
299178
+ onPlanDecision(callback) {
299179
+ this.io.on("connection", (socket) => {
299180
+ socket.on("plan:decision", (payload) => {
299181
+ if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
299182
+ callback({
299183
+ sessionId: payload.sessionId,
299184
+ approved: payload.approved,
299185
+ note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
299186
+ editedPlan: payload.editedPlan
299187
+ });
299188
+ }
299189
+ });
299190
+ });
299191
+ }
299173
299192
  onSessionSteer(callback) {
299174
299193
  this.io.on("connection", (socket) => {
299175
299194
  socket.on("session:steer", (payload) => {
@@ -299211,6 +299230,125 @@ var DashboardSocket = class {
299211
299230
  this.io.close();
299212
299231
  }
299213
299232
  };
299233
+
299234
+ // src/scheduler/index.ts
299235
+ var import_node_cron = __toESM(require_node_cron(), 1);
299236
+ var TaskScheduler = class {
299237
+ cronJobs = /* @__PURE__ */ new Map();
299238
+ store;
299239
+ runner;
299240
+ constructor(store, runner) {
299241
+ this.store = store;
299242
+ this.runner = runner;
299243
+ }
299244
+ start() {
299245
+ const tasks = this.store.listScheduledTasks();
299246
+ for (const task of tasks) {
299247
+ if (task.enabled) this.schedule(task);
299248
+ }
299249
+ }
299250
+ stop() {
299251
+ for (const job of this.cronJobs.values()) job.stop();
299252
+ this.cronJobs.clear();
299253
+ }
299254
+ schedule(task) {
299255
+ if (!import_node_cron.default.validate(task.cronExpression)) {
299256
+ throw new Error(`Invalid cron expression: ${task.cronExpression}`);
299257
+ }
299258
+ const existing = this.cronJobs.get(task.id);
299259
+ if (existing) {
299260
+ try {
299261
+ existing.stop();
299262
+ } catch {
299263
+ }
299264
+ }
299265
+ const job = import_node_cron.default.schedule(task.cronExpression, async () => {
299266
+ try {
299267
+ task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
299268
+ this.store.saveScheduledTask(task);
299269
+ await this.runner(task);
299270
+ } catch (err) {
299271
+ console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
299272
+ }
299273
+ }, { timezone: "UTC" });
299274
+ this.cronJobs.set(task.id, job);
299275
+ }
299276
+ unschedule(taskId) {
299277
+ this.cronJobs.get(taskId)?.stop();
299278
+ this.cronJobs.delete(taskId);
299279
+ }
299280
+ add(task) {
299281
+ this.store.saveScheduledTask(task);
299282
+ if (task.enabled) this.schedule(task);
299283
+ }
299284
+ remove(taskId) {
299285
+ this.unschedule(taskId);
299286
+ this.store.deleteScheduledTask(taskId);
299287
+ }
299288
+ list() {
299289
+ return this.store.listScheduledTasks();
299290
+ }
299291
+ isRunning(taskId) {
299292
+ return this.cronJobs.has(taskId);
299293
+ }
299294
+ static validateCron(expression) {
299295
+ return import_node_cron.default.validate(expression);
299296
+ }
299297
+ };
299298
+
299299
+ // src/dashboard/cost-stats.ts
299300
+ var DAY_MS = 24 * 60 * 60 * 1e3;
299301
+ function utcDateKey(d2) {
299302
+ return d2.toISOString().slice(0, 10);
299303
+ }
299304
+ function aggregateCostStats(sessions, opts = {}) {
299305
+ const days = Math.max(1, opts.days ?? 30);
299306
+ const topN = Math.max(1, opts.topN ?? 8);
299307
+ const now = opts.now ?? /* @__PURE__ */ new Date();
299308
+ const buckets = /* @__PURE__ */ new Map();
299309
+ for (let i4 = days - 1; i4 >= 0; i4--) {
299310
+ const key = utcDateKey(new Date(now.getTime() - i4 * DAY_MS));
299311
+ buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
299312
+ }
299313
+ let totalCostUsd = 0;
299314
+ let totalTokens = 0;
299315
+ let totalRuns = 0;
299316
+ for (const s3 of sessions) {
299317
+ const cost = s3.metadata?.totalCostUsd ?? 0;
299318
+ const tokens = s3.metadata?.totalTokens ?? 0;
299319
+ const runs = s3.metadata?.taskCount ?? 0;
299320
+ totalCostUsd += cost;
299321
+ totalTokens += tokens;
299322
+ totalRuns += runs;
299323
+ const when = new Date(s3.updatedAt || s3.createdAt);
299324
+ if (!Number.isNaN(when.getTime())) {
299325
+ const bucket = buckets.get(utcDateKey(when));
299326
+ if (bucket) {
299327
+ bucket.costUsd += cost;
299328
+ bucket.tokens += tokens;
299329
+ bucket.runs += runs;
299330
+ }
299331
+ }
299332
+ }
299333
+ const topSessions = [...sessions].filter((s3) => (s3.metadata?.totalCostUsd ?? 0) > 0).sort((a2, b3) => (b3.metadata?.totalCostUsd ?? 0) - (a2.metadata?.totalCostUsd ?? 0)).slice(0, topN).map((s3) => ({
299334
+ sessionId: s3.id,
299335
+ title: s3.title,
299336
+ costUsd: s3.metadata?.totalCostUsd ?? 0,
299337
+ tokens: s3.metadata?.totalTokens ?? 0,
299338
+ runs: s3.metadata?.taskCount ?? 0,
299339
+ updatedAt: s3.updatedAt
299340
+ }));
299341
+ return {
299342
+ totalCostUsd,
299343
+ totalTokens,
299344
+ totalSessions: sessions.length,
299345
+ totalRuns,
299346
+ perDay: [...buckets.values()],
299347
+ topSessions
299348
+ };
299349
+ }
299350
+
299351
+ // src/dashboard/server.ts
299214
299352
  var __dirname2 = path22__namespace.default.dirname(Url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('desktop-core.cjs', document.baseURI).href))));
299215
299353
  var DashboardServer = class {
299216
299354
  app;
@@ -299236,6 +299374,14 @@ var DashboardServer = class {
299236
299374
  * map is how that answer reaches the run that's blocked on it.
299237
299375
  */
299238
299376
  pendingApprovals = /* @__PURE__ */ new Map();
299377
+ /**
299378
+ * The orchestration decision trail ("why") of each session's most recent
299379
+ * run — captured when the run ends so the desktop's Why panel can show it
299380
+ * after the fact. Bounded: oldest entry evicted past 50 sessions.
299381
+ */
299382
+ whyBySession = /* @__PURE__ */ new Map();
299383
+ /** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
299384
+ scheduler;
299239
299385
  port;
299240
299386
  host;
299241
299387
  workspacePath;
@@ -299253,6 +299399,7 @@ var DashboardServer = class {
299253
299399
  secret: this.dashboardSecret,
299254
299400
  corsOrigin: config2.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
299255
299401
  });
299402
+ this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
299256
299403
  this.setupMiddleware();
299257
299404
  this.setupRoutes();
299258
299405
  this.socket.onSessionRate((sessionId, rating) => {
@@ -299316,6 +299463,9 @@ var DashboardServer = class {
299316
299463
  cascade.on("peer:message", (e3) => {
299317
299464
  this.socket.emitPeerMessage(e3);
299318
299465
  });
299466
+ cascade.on("plan:approval-required", (e3) => {
299467
+ this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e3 });
299468
+ });
299319
299469
  try {
299320
299470
  const result = await cascade.run({
299321
299471
  prompt: runPrompt,
@@ -299323,7 +299473,8 @@ var DashboardServer = class {
299323
299473
  approvalCallback: this.makeApprovalCallback(sessionId)
299324
299474
  });
299325
299475
  this.recordSessionTask(sessionId, result.taskId);
299326
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
299476
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
299477
+ this.captureWhy(sessionId, cascade, result);
299327
299478
  this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
299328
299479
  this.socket.broadcast("cost:update", {
299329
299480
  sessionId,
@@ -299333,6 +299484,7 @@ var DashboardServer = class {
299333
299484
  this.throttledBroadcast("workspace");
299334
299485
  } catch (err) {
299335
299486
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
299487
+ this.captureWhy(sessionId, cascade);
299336
299488
  this.socket.emitToSocket(socketId, "session:error", {
299337
299489
  sessionId,
299338
299490
  error: err instanceof Error ? err.message : String(err)
@@ -299343,9 +299495,17 @@ var DashboardServer = class {
299343
299495
  this.denyPendingApprovals(sessionId);
299344
299496
  }
299345
299497
  });
299498
+ this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
299499
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(
299500
+ approved,
299501
+ note,
299502
+ editedPlan
299503
+ );
299504
+ });
299346
299505
  this.socket.onSessionHalt((sessionId) => {
299347
299506
  this.activeControllers.get(sessionId)?.abort();
299348
299507
  this.denyPendingApprovals(sessionId);
299508
+ this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
299349
299509
  });
299350
299510
  this.socket.onApprovalResponse(({ requestId, approved, always }) => {
299351
299511
  const pending = this.pendingApprovals.get(requestId);
@@ -299378,12 +299538,21 @@ var DashboardServer = class {
299378
299538
  resolve();
299379
299539
  });
299380
299540
  });
299541
+ try {
299542
+ this.scheduler.start();
299543
+ } catch (err) {
299544
+ console.warn("[dashboard] failed to start task scheduler:", err);
299545
+ }
299381
299546
  }
299382
299547
  async stop() {
299383
299548
  if (this.broadcastTimer) {
299384
299549
  clearTimeout(this.broadcastTimer);
299385
299550
  this.broadcastTimer = null;
299386
299551
  }
299552
+ try {
299553
+ this.scheduler.stop();
299554
+ } catch {
299555
+ }
299387
299556
  this.socket.close();
299388
299557
  try {
299389
299558
  this.globalStore?.close();
@@ -299504,16 +299673,60 @@ var DashboardServer = class {
299504
299673
  return title;
299505
299674
  }
299506
299675
  /** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
299507
- persistRunEnd(sessionId, title, latestPrompt, reply, status) {
299676
+ persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
299508
299677
  try {
299509
299678
  if (reply && reply.trim()) {
299510
299679
  this.store.addMessage({ id: crypto4.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
299511
299680
  }
299681
+ if (result) {
299682
+ const session = this.store.getSession(sessionId);
299683
+ if (session) {
299684
+ this.store.updateSession(sessionId, {
299685
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
299686
+ metadata: {
299687
+ ...session.metadata,
299688
+ totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
299689
+ totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
299690
+ taskCount: session.metadata.taskCount + 1
299691
+ }
299692
+ });
299693
+ }
299694
+ }
299512
299695
  } catch (err) {
299513
299696
  console.warn("[dashboard] failed to persist run end:", err);
299514
299697
  }
299515
299698
  this.persistRuntimeRow(sessionId, title, status, latestPrompt);
299516
299699
  }
299700
+ /**
299701
+ * Capture the run's decision trail + router economics ("why") and broadcast
299702
+ * it so the desktop's Why panel updates live; kept per-session for the
299703
+ * GET /api/sessions/:id/why fallback (panel opened after the run).
299704
+ */
299705
+ captureWhy(sessionId, cascade, result) {
299706
+ try {
299707
+ const stats = cascade.getRouter().getStats();
299708
+ const savings = cascade.getRouter().getDelegationSavings();
299709
+ const report = {
299710
+ sessionId,
299711
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
299712
+ decisions: cascade.getDecisionLog(),
299713
+ savedUsd: savings.savedUsd,
299714
+ savedPct: savings.savedPct,
299715
+ totalCostUsd: stats.totalCostUsd,
299716
+ totalTokens: stats.totalTokens,
299717
+ costByTier: stats.costByTier,
299718
+ durationMs: result?.durationMs
299719
+ };
299720
+ this.whyBySession.set(sessionId, report);
299721
+ if (this.whyBySession.size > 50) {
299722
+ const oldest = this.whyBySession.keys().next().value;
299723
+ if (oldest) this.whyBySession.delete(oldest);
299724
+ }
299725
+ this.socket.broadcast("run:why", report);
299726
+ } catch (err) {
299727
+ console.warn("[dashboard] failed to capture decision trail:", err);
299728
+ }
299729
+ }
299517
299730
  /**
299518
299731
  * Route steering text into running Cascade instances. Targets the given
299519
299732
  * session, or every active session when none is specified (the desktop
@@ -299543,6 +299756,52 @@ var DashboardServer = class {
299543
299756
  this.pendingApprovals.set(request.id, { resolve, sessionId });
299544
299757
  });
299545
299758
  }
299759
+ /**
299760
+ * Execute one scheduled task firing. Runs headless (like `cascade run -p`):
299761
+ * tool approvals are auto-granted since nobody may be watching when the cron
299762
+ * fires — the Schedules UI states this. Events broadcast to every connected
299763
+ * client so an open desktop sees the run appear live.
299764
+ */
299765
+ async runScheduledTask(task) {
299766
+ const sessionId = crypto4.randomUUID();
299767
+ const prompt = task.prompt;
299768
+ const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
299769
+ const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
299770
+ this.activeSessions.set(sessionId, cascade);
299771
+ cascade.on("tier:status", (e3) => {
299772
+ this.socket.broadcast("tier:status", { sessionId, ...e3 });
299773
+ });
299774
+ cascade.on("peer:message", (e3) => {
299775
+ this.socket.emitPeerMessage(e3);
299776
+ });
299777
+ try {
299778
+ const result = await cascade.run({
299779
+ prompt,
299780
+ identityId: task.identityId,
299781
+ approvalCallback: async () => ({ approved: true, always: false })
299782
+ });
299783
+ this.recordSessionTask(sessionId, result.taskId);
299784
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
299785
+ this.captureWhy(sessionId, cascade, result);
299786
+ this.socket.broadcast("session:complete", { sessionId, result });
299787
+ this.socket.broadcast("cost:update", {
299788
+ sessionId,
299789
+ totalTokens: result.usage.totalTokens,
299790
+ totalCostUsd: result.usage.estimatedCostUsd
299791
+ });
299792
+ this.throttledBroadcast("workspace");
299793
+ } catch (err) {
299794
+ this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
299795
+ this.captureWhy(sessionId, cascade);
299796
+ this.socket.broadcast("session:error", {
299797
+ sessionId,
299798
+ error: err instanceof Error ? err.message : String(err)
299799
+ });
299800
+ throw err;
299801
+ } finally {
299802
+ this.activeSessions.delete(sessionId);
299803
+ }
299804
+ }
299546
299805
  /** Deny + clear any approvals still pending for a session (run end / abort). */
299547
299806
  denyPendingApprovals(sessionId) {
299548
299807
  for (const [id, pending] of this.pendingApprovals) {
@@ -300055,6 +300314,9 @@ ${prompt}`;
300055
300314
  cascade.on("peer:message", (e3) => {
300056
300315
  this.socket.emitPeerMessage(e3);
300057
300316
  });
300317
+ cascade.on("plan:approval-required", (e3) => {
300318
+ this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e3 });
300319
+ });
300058
300320
  try {
300059
300321
  const result = await cascade.run({
300060
300322
  prompt: runPrompt,
@@ -300062,7 +300324,8 @@ ${prompt}`;
300062
300324
  approvalCallback: this.makeApprovalCallback(sessionId)
300063
300325
  });
300064
300326
  this.recordSessionTask(sessionId, result.taskId);
300065
- this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
300327
+ this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
300328
+ this.captureWhy(sessionId, cascade, result);
300066
300329
  this.socket.broadcast("cost:update", {
300067
300330
  sessionId,
300068
300331
  totalTokens: result.usage.totalTokens,
@@ -300072,6 +300335,7 @@ ${prompt}`;
300072
300335
  this.throttledBroadcast("workspace");
300073
300336
  } catch (err) {
300074
300337
  this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
300338
+ this.captureWhy(sessionId, cascade);
300075
300339
  this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
300076
300340
  sessionId,
300077
300341
  error: err instanceof Error ? err.message : String(err)
@@ -300093,6 +300357,168 @@ ${prompt}`;
300093
300357
  }))
300094
300358
  });
300095
300359
  });
300360
+ this.app.get("/api/sessions/:id/why", auth, (req, res) => {
300361
+ const report = this.whyBySession.get(req.params.id);
300362
+ if (!report) {
300363
+ res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
300364
+ return;
300365
+ }
300366
+ res.json(report);
300367
+ });
300368
+ this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
300369
+ const sessionId = req.params.id;
300370
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
300371
+ const before = /* @__PURE__ */ new Map();
300372
+ for (const taskId of taskIds) {
300373
+ for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
300374
+ if (!before.has(filePath)) before.set(filePath, content);
300375
+ }
300376
+ }
300377
+ const { readFile } = await import('fs/promises');
300378
+ const MAX_DIFF_BYTES = 2 * 1024 * 1024;
300379
+ const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
300380
+ let after = "";
300381
+ let missing = false;
300382
+ try {
300383
+ const buf = await readFile(filePath);
300384
+ after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
300385
+ } catch {
300386
+ missing = true;
300387
+ }
300388
+ return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
300389
+ }));
300390
+ res.json({ sessionId, changes: changes.filter((c4) => c4.changed) });
300391
+ });
300392
+ this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
300393
+ const sessionId = req.params.id;
300394
+ const body = req.body;
300395
+ if (!body.filePath || typeof body.filePath !== "string") {
300396
+ res.status(400).json({ error: "filePath is required" });
300397
+ return;
300398
+ }
300399
+ const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
300400
+ let content;
300401
+ for (const taskId of taskIds) {
300402
+ const snap = this.store.getLatestFileSnapshots(taskId).find((s3) => s3.filePath === body.filePath);
300403
+ if (snap) {
300404
+ content = snap.content;
300405
+ break;
300406
+ }
300407
+ }
300408
+ if (content === void 0) {
300409
+ res.status(404).json({ error: "No snapshot recorded for that file in this session." });
300410
+ return;
300411
+ }
300412
+ try {
300413
+ const { writeFile: writeFile2 } = await import('fs/promises');
300414
+ await writeFile2(body.filePath, content, "utf-8");
300415
+ res.json({ ok: true, filePath: body.filePath });
300416
+ } catch (err) {
300417
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300418
+ }
300419
+ });
300420
+ this.app.get("/api/costs", auth, (_req, res) => {
300421
+ try {
300422
+ const sessions = this.store.listSessions(void 0, 1e3);
300423
+ const budget = this.config.budget ?? { warnAtPct: 80 };
300424
+ res.json({
300425
+ ...aggregateCostStats(sessions, { days: 30, topN: 8 }),
300426
+ budget: {
300427
+ dailyBudgetUsd: budget.dailyBudgetUsd,
300428
+ sessionBudgetUsd: budget.sessionBudgetUsd,
300429
+ maxCostPerRunUsd: budget.maxCostPerRunUsd
300430
+ }
300431
+ });
300432
+ } catch (err) {
300433
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300434
+ }
300435
+ });
300436
+ this.app.get("/api/schedules", auth, (_req, res) => {
300437
+ try {
300438
+ res.json(this.scheduler.list().map((t4) => ({ ...t4, armed: this.scheduler.isRunning(t4.id) })));
300439
+ } catch (err) {
300440
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300441
+ }
300442
+ });
300443
+ this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
300444
+ const body = req.body;
300445
+ if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
300446
+ res.status(400).json({ error: "name, cronExpression, and prompt are required" });
300447
+ return;
300448
+ }
300449
+ if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
300450
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
300451
+ return;
300452
+ }
300453
+ const task = {
300454
+ id: crypto4.randomUUID(),
300455
+ name: body.name.trim(),
300456
+ cronExpression: body.cronExpression.trim(),
300457
+ prompt: body.prompt.trim(),
300458
+ workspacePath: this.workspacePath,
300459
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
300460
+ enabled: body.enabled !== false
300461
+ };
300462
+ try {
300463
+ this.scheduler.add(task);
300464
+ res.json(task);
300465
+ } catch (err) {
300466
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300467
+ }
300468
+ });
300469
+ this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
300470
+ const id = req.params.id;
300471
+ const existing = this.scheduler.list().find((t4) => t4.id === id);
300472
+ if (!existing) {
300473
+ res.status(404).json({ error: "Not found" });
300474
+ return;
300475
+ }
300476
+ const body = req.body;
300477
+ if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
300478
+ res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
300479
+ return;
300480
+ }
300481
+ const updated = {
300482
+ ...existing,
300483
+ name: body.name?.trim() || existing.name,
300484
+ cronExpression: body.cronExpression?.trim() || existing.cronExpression,
300485
+ prompt: body.prompt?.trim() || existing.prompt,
300486
+ enabled: body.enabled ?? existing.enabled
300487
+ };
300488
+ try {
300489
+ this.scheduler.add(updated);
300490
+ if (!updated.enabled) this.scheduler.unschedule(id);
300491
+ res.json(updated);
300492
+ } catch (err) {
300493
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300494
+ }
300495
+ });
300496
+ this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
300497
+ try {
300498
+ this.scheduler.remove(req.params.id);
300499
+ res.json({ ok: true });
300500
+ } catch (err) {
300501
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300502
+ }
300503
+ });
300504
+ this.app.get("/api/audit-chain", auth, async (req, res) => {
300505
+ try {
300506
+ const limit2 = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
300507
+ const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
300508
+ const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
300509
+ const logger = new AuditLogger3(this.workspacePath);
300510
+ try {
300511
+ const all = logger.getAllLogs();
300512
+ const total = all.length;
300513
+ const entries = all.reverse().slice(offset, offset + limit2);
300514
+ res.json({ total, offset, entries });
300515
+ } finally {
300516
+ logger.close();
300517
+ }
300518
+ } catch (err) {
300519
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
300520
+ }
300521
+ });
300096
300522
  const prodPath = path22__namespace.default.resolve(__dirname2, "../web/dist");
300097
300523
  const devPath = path22__namespace.default.resolve(__dirname2, "../../web/dist");
300098
300524
  const webDistPath = fs11__namespace.default.existsSync(prodPath) ? prodPath : devPath;
@@ -300114,71 +300540,6 @@ ${prompt}`;
300114
300540
  }
300115
300541
  }
300116
300542
  };
300117
-
300118
- // src/scheduler/index.ts
300119
- var import_node_cron = __toESM(require_node_cron(), 1);
300120
- var TaskScheduler = class {
300121
- cronJobs = /* @__PURE__ */ new Map();
300122
- store;
300123
- runner;
300124
- constructor(store, runner) {
300125
- this.store = store;
300126
- this.runner = runner;
300127
- }
300128
- start() {
300129
- const tasks = this.store.listScheduledTasks();
300130
- for (const task of tasks) {
300131
- if (task.enabled) this.schedule(task);
300132
- }
300133
- }
300134
- stop() {
300135
- for (const job of this.cronJobs.values()) job.stop();
300136
- this.cronJobs.clear();
300137
- }
300138
- schedule(task) {
300139
- if (!import_node_cron.default.validate(task.cronExpression)) {
300140
- throw new Error(`Invalid cron expression: ${task.cronExpression}`);
300141
- }
300142
- const existing = this.cronJobs.get(task.id);
300143
- if (existing) {
300144
- try {
300145
- existing.stop();
300146
- } catch {
300147
- }
300148
- }
300149
- const job = import_node_cron.default.schedule(task.cronExpression, async () => {
300150
- try {
300151
- task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
300152
- this.store.saveScheduledTask(task);
300153
- await this.runner(task);
300154
- } catch (err) {
300155
- console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
300156
- }
300157
- }, { timezone: "UTC" });
300158
- this.cronJobs.set(task.id, job);
300159
- }
300160
- unschedule(taskId) {
300161
- this.cronJobs.get(taskId)?.stop();
300162
- this.cronJobs.delete(taskId);
300163
- }
300164
- add(task) {
300165
- this.store.saveScheduledTask(task);
300166
- if (task.enabled) this.schedule(task);
300167
- }
300168
- remove(taskId) {
300169
- this.unschedule(taskId);
300170
- this.store.deleteScheduledTask(taskId);
300171
- }
300172
- list() {
300173
- return this.store.listScheduledTasks();
300174
- }
300175
- isRunning(taskId) {
300176
- return this.cronJobs.has(taskId);
300177
- }
300178
- static validateCron(expression) {
300179
- return import_node_cron.default.validate(expression);
300180
- }
300181
- };
300182
300543
  var execFileAsync2 = util$1.promisify(child_process.execFile);
300183
300544
  var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
300184
300545
  function sanitizeEnvValue(v6) {