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 +429 -4
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +428 -4
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +430 -69
- package/dist/index.cjs +427 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +427 -66
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/desktop-core.cjs
CHANGED
|
@@ -222595,7 +222595,7 @@ Anthropic.Models = Models2;
|
|
|
222595
222595
|
Anthropic.Beta = Beta;
|
|
222596
222596
|
|
|
222597
222597
|
// src/constants.ts
|
|
222598
|
-
var CASCADE_VERSION = "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) {
|