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/index.cjs
CHANGED
|
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
// src/constants.ts
|
|
239
|
-
var CASCADE_VERSION = "0.
|
|
239
|
+
var CASCADE_VERSION = "0.17.0";
|
|
240
240
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
241
241
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
242
242
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -11945,6 +11945,25 @@ var DashboardSocket = class {
|
|
|
11945
11945
|
});
|
|
11946
11946
|
});
|
|
11947
11947
|
}
|
|
11948
|
+
/**
|
|
11949
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
11950
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
11951
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
11952
|
+
*/
|
|
11953
|
+
onPlanDecision(callback) {
|
|
11954
|
+
this.io.on("connection", (socket) => {
|
|
11955
|
+
socket.on("plan:decision", (payload) => {
|
|
11956
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
11957
|
+
callback({
|
|
11958
|
+
sessionId: payload.sessionId,
|
|
11959
|
+
approved: payload.approved,
|
|
11960
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
11961
|
+
editedPlan: payload.editedPlan
|
|
11962
|
+
});
|
|
11963
|
+
}
|
|
11964
|
+
});
|
|
11965
|
+
});
|
|
11966
|
+
}
|
|
11948
11967
|
onSessionSteer(callback) {
|
|
11949
11968
|
this.io.on("connection", (socket) => {
|
|
11950
11969
|
socket.on("session:steer", (payload) => {
|
|
@@ -11986,6 +12005,122 @@ var DashboardSocket = class {
|
|
|
11986
12005
|
this.io.close();
|
|
11987
12006
|
}
|
|
11988
12007
|
};
|
|
12008
|
+
var TaskScheduler = class {
|
|
12009
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
12010
|
+
store;
|
|
12011
|
+
runner;
|
|
12012
|
+
constructor(store, runner) {
|
|
12013
|
+
this.store = store;
|
|
12014
|
+
this.runner = runner;
|
|
12015
|
+
}
|
|
12016
|
+
start() {
|
|
12017
|
+
const tasks = this.store.listScheduledTasks();
|
|
12018
|
+
for (const task of tasks) {
|
|
12019
|
+
if (task.enabled) this.schedule(task);
|
|
12020
|
+
}
|
|
12021
|
+
}
|
|
12022
|
+
stop() {
|
|
12023
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
12024
|
+
this.cronJobs.clear();
|
|
12025
|
+
}
|
|
12026
|
+
schedule(task) {
|
|
12027
|
+
if (!cron__default.default.validate(task.cronExpression)) {
|
|
12028
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12029
|
+
}
|
|
12030
|
+
const existing = this.cronJobs.get(task.id);
|
|
12031
|
+
if (existing) {
|
|
12032
|
+
try {
|
|
12033
|
+
existing.stop();
|
|
12034
|
+
} catch {
|
|
12035
|
+
}
|
|
12036
|
+
}
|
|
12037
|
+
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
12038
|
+
try {
|
|
12039
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12040
|
+
this.store.saveScheduledTask(task);
|
|
12041
|
+
await this.runner(task);
|
|
12042
|
+
} catch (err) {
|
|
12043
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12044
|
+
}
|
|
12045
|
+
}, { timezone: "UTC" });
|
|
12046
|
+
this.cronJobs.set(task.id, job);
|
|
12047
|
+
}
|
|
12048
|
+
unschedule(taskId) {
|
|
12049
|
+
this.cronJobs.get(taskId)?.stop();
|
|
12050
|
+
this.cronJobs.delete(taskId);
|
|
12051
|
+
}
|
|
12052
|
+
add(task) {
|
|
12053
|
+
this.store.saveScheduledTask(task);
|
|
12054
|
+
if (task.enabled) this.schedule(task);
|
|
12055
|
+
}
|
|
12056
|
+
remove(taskId) {
|
|
12057
|
+
this.unschedule(taskId);
|
|
12058
|
+
this.store.deleteScheduledTask(taskId);
|
|
12059
|
+
}
|
|
12060
|
+
list() {
|
|
12061
|
+
return this.store.listScheduledTasks();
|
|
12062
|
+
}
|
|
12063
|
+
isRunning(taskId) {
|
|
12064
|
+
return this.cronJobs.has(taskId);
|
|
12065
|
+
}
|
|
12066
|
+
static validateCron(expression) {
|
|
12067
|
+
return cron__default.default.validate(expression);
|
|
12068
|
+
}
|
|
12069
|
+
};
|
|
12070
|
+
|
|
12071
|
+
// src/dashboard/cost-stats.ts
|
|
12072
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
12073
|
+
function utcDateKey(d) {
|
|
12074
|
+
return d.toISOString().slice(0, 10);
|
|
12075
|
+
}
|
|
12076
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
12077
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
12078
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
12079
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12080
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
12081
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
12082
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
12083
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
12084
|
+
}
|
|
12085
|
+
let totalCostUsd = 0;
|
|
12086
|
+
let totalTokens = 0;
|
|
12087
|
+
let totalRuns = 0;
|
|
12088
|
+
for (const s of sessions) {
|
|
12089
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
12090
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
12091
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
12092
|
+
totalCostUsd += cost;
|
|
12093
|
+
totalTokens += tokens;
|
|
12094
|
+
totalRuns += runs;
|
|
12095
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
12096
|
+
if (!Number.isNaN(when.getTime())) {
|
|
12097
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
12098
|
+
if (bucket) {
|
|
12099
|
+
bucket.costUsd += cost;
|
|
12100
|
+
bucket.tokens += tokens;
|
|
12101
|
+
bucket.runs += runs;
|
|
12102
|
+
}
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
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) => ({
|
|
12106
|
+
sessionId: s.id,
|
|
12107
|
+
title: s.title,
|
|
12108
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
12109
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
12110
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
12111
|
+
updatedAt: s.updatedAt
|
|
12112
|
+
}));
|
|
12113
|
+
return {
|
|
12114
|
+
totalCostUsd,
|
|
12115
|
+
totalTokens,
|
|
12116
|
+
totalSessions: sessions.length,
|
|
12117
|
+
totalRuns,
|
|
12118
|
+
perDay: [...buckets.values()],
|
|
12119
|
+
topSessions
|
|
12120
|
+
};
|
|
12121
|
+
}
|
|
12122
|
+
|
|
12123
|
+
// src/dashboard/server.ts
|
|
11989
12124
|
var __dirname$1 = path20__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
11990
12125
|
var DashboardServer = class {
|
|
11991
12126
|
app;
|
|
@@ -12011,6 +12146,14 @@ var DashboardServer = class {
|
|
|
12011
12146
|
* map is how that answer reaches the run that's blocked on it.
|
|
12012
12147
|
*/
|
|
12013
12148
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
12149
|
+
/**
|
|
12150
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
12151
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
12152
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
12153
|
+
*/
|
|
12154
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
12155
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
12156
|
+
scheduler;
|
|
12014
12157
|
port;
|
|
12015
12158
|
host;
|
|
12016
12159
|
workspacePath;
|
|
@@ -12028,6 +12171,7 @@ var DashboardServer = class {
|
|
|
12028
12171
|
secret: this.dashboardSecret,
|
|
12029
12172
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
12030
12173
|
});
|
|
12174
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
12031
12175
|
this.setupMiddleware();
|
|
12032
12176
|
this.setupRoutes();
|
|
12033
12177
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -12091,6 +12235,9 @@ var DashboardServer = class {
|
|
|
12091
12235
|
cascade.on("peer:message", (e) => {
|
|
12092
12236
|
this.socket.emitPeerMessage(e);
|
|
12093
12237
|
});
|
|
12238
|
+
cascade.on("plan:approval-required", (e) => {
|
|
12239
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
12240
|
+
});
|
|
12094
12241
|
try {
|
|
12095
12242
|
const result = await cascade.run({
|
|
12096
12243
|
prompt: runPrompt,
|
|
@@ -12098,7 +12245,8 @@ var DashboardServer = class {
|
|
|
12098
12245
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12099
12246
|
});
|
|
12100
12247
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12101
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
12248
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12249
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12102
12250
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
12103
12251
|
this.socket.broadcast("cost:update", {
|
|
12104
12252
|
sessionId,
|
|
@@ -12108,6 +12256,7 @@ var DashboardServer = class {
|
|
|
12108
12256
|
this.throttledBroadcast("workspace");
|
|
12109
12257
|
} catch (err) {
|
|
12110
12258
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12259
|
+
this.captureWhy(sessionId, cascade);
|
|
12111
12260
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
12112
12261
|
sessionId,
|
|
12113
12262
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12118,9 +12267,17 @@ var DashboardServer = class {
|
|
|
12118
12267
|
this.denyPendingApprovals(sessionId);
|
|
12119
12268
|
}
|
|
12120
12269
|
});
|
|
12270
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
12271
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
12272
|
+
approved,
|
|
12273
|
+
note,
|
|
12274
|
+
editedPlan
|
|
12275
|
+
);
|
|
12276
|
+
});
|
|
12121
12277
|
this.socket.onSessionHalt((sessionId) => {
|
|
12122
12278
|
this.activeControllers.get(sessionId)?.abort();
|
|
12123
12279
|
this.denyPendingApprovals(sessionId);
|
|
12280
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
12124
12281
|
});
|
|
12125
12282
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
12126
12283
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -12153,12 +12310,21 @@ var DashboardServer = class {
|
|
|
12153
12310
|
resolve();
|
|
12154
12311
|
});
|
|
12155
12312
|
});
|
|
12313
|
+
try {
|
|
12314
|
+
this.scheduler.start();
|
|
12315
|
+
} catch (err) {
|
|
12316
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
12317
|
+
}
|
|
12156
12318
|
}
|
|
12157
12319
|
async stop() {
|
|
12158
12320
|
if (this.broadcastTimer) {
|
|
12159
12321
|
clearTimeout(this.broadcastTimer);
|
|
12160
12322
|
this.broadcastTimer = null;
|
|
12161
12323
|
}
|
|
12324
|
+
try {
|
|
12325
|
+
this.scheduler.stop();
|
|
12326
|
+
} catch {
|
|
12327
|
+
}
|
|
12162
12328
|
this.socket.close();
|
|
12163
12329
|
try {
|
|
12164
12330
|
this.globalStore?.close();
|
|
@@ -12279,16 +12445,60 @@ var DashboardServer = class {
|
|
|
12279
12445
|
return title;
|
|
12280
12446
|
}
|
|
12281
12447
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
12282
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
12448
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
12283
12449
|
try {
|
|
12284
12450
|
if (reply && reply.trim()) {
|
|
12285
12451
|
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12286
12452
|
}
|
|
12453
|
+
if (result) {
|
|
12454
|
+
const session = this.store.getSession(sessionId);
|
|
12455
|
+
if (session) {
|
|
12456
|
+
this.store.updateSession(sessionId, {
|
|
12457
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12458
|
+
metadata: {
|
|
12459
|
+
...session.metadata,
|
|
12460
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
12461
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
12462
|
+
taskCount: session.metadata.taskCount + 1
|
|
12463
|
+
}
|
|
12464
|
+
});
|
|
12465
|
+
}
|
|
12466
|
+
}
|
|
12287
12467
|
} catch (err) {
|
|
12288
12468
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
12289
12469
|
}
|
|
12290
12470
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
12291
12471
|
}
|
|
12472
|
+
/**
|
|
12473
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
12474
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
12475
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
12476
|
+
*/
|
|
12477
|
+
captureWhy(sessionId, cascade, result) {
|
|
12478
|
+
try {
|
|
12479
|
+
const stats = cascade.getRouter().getStats();
|
|
12480
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
12481
|
+
const report = {
|
|
12482
|
+
sessionId,
|
|
12483
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12484
|
+
decisions: cascade.getDecisionLog(),
|
|
12485
|
+
savedUsd: savings.savedUsd,
|
|
12486
|
+
savedPct: savings.savedPct,
|
|
12487
|
+
totalCostUsd: stats.totalCostUsd,
|
|
12488
|
+
totalTokens: stats.totalTokens,
|
|
12489
|
+
costByTier: stats.costByTier,
|
|
12490
|
+
durationMs: result?.durationMs
|
|
12491
|
+
};
|
|
12492
|
+
this.whyBySession.set(sessionId, report);
|
|
12493
|
+
if (this.whyBySession.size > 50) {
|
|
12494
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
12495
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
12496
|
+
}
|
|
12497
|
+
this.socket.broadcast("run:why", report);
|
|
12498
|
+
} catch (err) {
|
|
12499
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
12500
|
+
}
|
|
12501
|
+
}
|
|
12292
12502
|
/**
|
|
12293
12503
|
* Route steering text into running Cascade instances. Targets the given
|
|
12294
12504
|
* session, or every active session when none is specified (the desktop
|
|
@@ -12318,6 +12528,52 @@ var DashboardServer = class {
|
|
|
12318
12528
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
12319
12529
|
});
|
|
12320
12530
|
}
|
|
12531
|
+
/**
|
|
12532
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
12533
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
12534
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
12535
|
+
* client so an open desktop sees the run appear live.
|
|
12536
|
+
*/
|
|
12537
|
+
async runScheduledTask(task) {
|
|
12538
|
+
const sessionId = crypto3.randomUUID();
|
|
12539
|
+
const prompt = task.prompt;
|
|
12540
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
12541
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
12542
|
+
this.activeSessions.set(sessionId, cascade);
|
|
12543
|
+
cascade.on("tier:status", (e) => {
|
|
12544
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
12545
|
+
});
|
|
12546
|
+
cascade.on("peer:message", (e) => {
|
|
12547
|
+
this.socket.emitPeerMessage(e);
|
|
12548
|
+
});
|
|
12549
|
+
try {
|
|
12550
|
+
const result = await cascade.run({
|
|
12551
|
+
prompt,
|
|
12552
|
+
identityId: task.identityId,
|
|
12553
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
12554
|
+
});
|
|
12555
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
12556
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12557
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12558
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
12559
|
+
this.socket.broadcast("cost:update", {
|
|
12560
|
+
sessionId,
|
|
12561
|
+
totalTokens: result.usage.totalTokens,
|
|
12562
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
12563
|
+
});
|
|
12564
|
+
this.throttledBroadcast("workspace");
|
|
12565
|
+
} catch (err) {
|
|
12566
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12567
|
+
this.captureWhy(sessionId, cascade);
|
|
12568
|
+
this.socket.broadcast("session:error", {
|
|
12569
|
+
sessionId,
|
|
12570
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12571
|
+
});
|
|
12572
|
+
throw err;
|
|
12573
|
+
} finally {
|
|
12574
|
+
this.activeSessions.delete(sessionId);
|
|
12575
|
+
}
|
|
12576
|
+
}
|
|
12321
12577
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
12322
12578
|
denyPendingApprovals(sessionId) {
|
|
12323
12579
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -12830,6 +13086,9 @@ ${prompt}`;
|
|
|
12830
13086
|
cascade.on("peer:message", (e) => {
|
|
12831
13087
|
this.socket.emitPeerMessage(e);
|
|
12832
13088
|
});
|
|
13089
|
+
cascade.on("plan:approval-required", (e) => {
|
|
13090
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
13091
|
+
});
|
|
12833
13092
|
try {
|
|
12834
13093
|
const result = await cascade.run({
|
|
12835
13094
|
prompt: runPrompt,
|
|
@@ -12837,7 +13096,8 @@ ${prompt}`;
|
|
|
12837
13096
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12838
13097
|
});
|
|
12839
13098
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12840
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
13099
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
13100
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12841
13101
|
this.socket.broadcast("cost:update", {
|
|
12842
13102
|
sessionId,
|
|
12843
13103
|
totalTokens: result.usage.totalTokens,
|
|
@@ -12847,6 +13107,7 @@ ${prompt}`;
|
|
|
12847
13107
|
this.throttledBroadcast("workspace");
|
|
12848
13108
|
} catch (err) {
|
|
12849
13109
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
13110
|
+
this.captureWhy(sessionId, cascade);
|
|
12850
13111
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
12851
13112
|
sessionId,
|
|
12852
13113
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12868,6 +13129,168 @@ ${prompt}`;
|
|
|
12868
13129
|
}))
|
|
12869
13130
|
});
|
|
12870
13131
|
});
|
|
13132
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
13133
|
+
const report = this.whyBySession.get(req.params.id);
|
|
13134
|
+
if (!report) {
|
|
13135
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
13136
|
+
return;
|
|
13137
|
+
}
|
|
13138
|
+
res.json(report);
|
|
13139
|
+
});
|
|
13140
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
13141
|
+
const sessionId = req.params.id;
|
|
13142
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13143
|
+
const before = /* @__PURE__ */ new Map();
|
|
13144
|
+
for (const taskId of taskIds) {
|
|
13145
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
13146
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
13147
|
+
}
|
|
13148
|
+
}
|
|
13149
|
+
const { readFile } = await import('fs/promises');
|
|
13150
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
13151
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
13152
|
+
let after = "";
|
|
13153
|
+
let missing = false;
|
|
13154
|
+
try {
|
|
13155
|
+
const buf = await readFile(filePath);
|
|
13156
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
13157
|
+
} catch {
|
|
13158
|
+
missing = true;
|
|
13159
|
+
}
|
|
13160
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
13161
|
+
}));
|
|
13162
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
13163
|
+
});
|
|
13164
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
13165
|
+
const sessionId = req.params.id;
|
|
13166
|
+
const body = req.body;
|
|
13167
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
13168
|
+
res.status(400).json({ error: "filePath is required" });
|
|
13169
|
+
return;
|
|
13170
|
+
}
|
|
13171
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13172
|
+
let content;
|
|
13173
|
+
for (const taskId of taskIds) {
|
|
13174
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
13175
|
+
if (snap) {
|
|
13176
|
+
content = snap.content;
|
|
13177
|
+
break;
|
|
13178
|
+
}
|
|
13179
|
+
}
|
|
13180
|
+
if (content === void 0) {
|
|
13181
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
13182
|
+
return;
|
|
13183
|
+
}
|
|
13184
|
+
try {
|
|
13185
|
+
const { writeFile } = await import('fs/promises');
|
|
13186
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
13187
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
13188
|
+
} catch (err) {
|
|
13189
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13190
|
+
}
|
|
13191
|
+
});
|
|
13192
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
13193
|
+
try {
|
|
13194
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13195
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
13196
|
+
res.json({
|
|
13197
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
13198
|
+
budget: {
|
|
13199
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
13200
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
13201
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
13202
|
+
}
|
|
13203
|
+
});
|
|
13204
|
+
} catch (err) {
|
|
13205
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13206
|
+
}
|
|
13207
|
+
});
|
|
13208
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
13209
|
+
try {
|
|
13210
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
13211
|
+
} catch (err) {
|
|
13212
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13213
|
+
}
|
|
13214
|
+
});
|
|
13215
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
13216
|
+
const body = req.body;
|
|
13217
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
13218
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
13219
|
+
return;
|
|
13220
|
+
}
|
|
13221
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
13222
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13223
|
+
return;
|
|
13224
|
+
}
|
|
13225
|
+
const task = {
|
|
13226
|
+
id: crypto3.randomUUID(),
|
|
13227
|
+
name: body.name.trim(),
|
|
13228
|
+
cronExpression: body.cronExpression.trim(),
|
|
13229
|
+
prompt: body.prompt.trim(),
|
|
13230
|
+
workspacePath: this.workspacePath,
|
|
13231
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13232
|
+
enabled: body.enabled !== false
|
|
13233
|
+
};
|
|
13234
|
+
try {
|
|
13235
|
+
this.scheduler.add(task);
|
|
13236
|
+
res.json(task);
|
|
13237
|
+
} catch (err) {
|
|
13238
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13239
|
+
}
|
|
13240
|
+
});
|
|
13241
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13242
|
+
const id = req.params.id;
|
|
13243
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
13244
|
+
if (!existing) {
|
|
13245
|
+
res.status(404).json({ error: "Not found" });
|
|
13246
|
+
return;
|
|
13247
|
+
}
|
|
13248
|
+
const body = req.body;
|
|
13249
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
13250
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13251
|
+
return;
|
|
13252
|
+
}
|
|
13253
|
+
const updated = {
|
|
13254
|
+
...existing,
|
|
13255
|
+
name: body.name?.trim() || existing.name,
|
|
13256
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
13257
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
13258
|
+
enabled: body.enabled ?? existing.enabled
|
|
13259
|
+
};
|
|
13260
|
+
try {
|
|
13261
|
+
this.scheduler.add(updated);
|
|
13262
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
13263
|
+
res.json(updated);
|
|
13264
|
+
} catch (err) {
|
|
13265
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13266
|
+
}
|
|
13267
|
+
});
|
|
13268
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13269
|
+
try {
|
|
13270
|
+
this.scheduler.remove(req.params.id);
|
|
13271
|
+
res.json({ ok: true });
|
|
13272
|
+
} catch (err) {
|
|
13273
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13274
|
+
}
|
|
13275
|
+
});
|
|
13276
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
13277
|
+
try {
|
|
13278
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
13279
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
13280
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
13281
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
13282
|
+
try {
|
|
13283
|
+
const all = logger.getAllLogs();
|
|
13284
|
+
const total = all.length;
|
|
13285
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
13286
|
+
res.json({ total, offset, entries });
|
|
13287
|
+
} finally {
|
|
13288
|
+
logger.close();
|
|
13289
|
+
}
|
|
13290
|
+
} catch (err) {
|
|
13291
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13292
|
+
}
|
|
13293
|
+
});
|
|
12871
13294
|
const prodPath = path20__default.default.resolve(__dirname$1, "../web/dist");
|
|
12872
13295
|
const devPath = path20__default.default.resolve(__dirname$1, "../../web/dist");
|
|
12873
13296
|
const webDistPath = fs19__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
@@ -12889,68 +13312,6 @@ ${prompt}`;
|
|
|
12889
13312
|
}
|
|
12890
13313
|
}
|
|
12891
13314
|
};
|
|
12892
|
-
var TaskScheduler = class {
|
|
12893
|
-
cronJobs = /* @__PURE__ */ new Map();
|
|
12894
|
-
store;
|
|
12895
|
-
runner;
|
|
12896
|
-
constructor(store, runner) {
|
|
12897
|
-
this.store = store;
|
|
12898
|
-
this.runner = runner;
|
|
12899
|
-
}
|
|
12900
|
-
start() {
|
|
12901
|
-
const tasks = this.store.listScheduledTasks();
|
|
12902
|
-
for (const task of tasks) {
|
|
12903
|
-
if (task.enabled) this.schedule(task);
|
|
12904
|
-
}
|
|
12905
|
-
}
|
|
12906
|
-
stop() {
|
|
12907
|
-
for (const job of this.cronJobs.values()) job.stop();
|
|
12908
|
-
this.cronJobs.clear();
|
|
12909
|
-
}
|
|
12910
|
-
schedule(task) {
|
|
12911
|
-
if (!cron__default.default.validate(task.cronExpression)) {
|
|
12912
|
-
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12913
|
-
}
|
|
12914
|
-
const existing = this.cronJobs.get(task.id);
|
|
12915
|
-
if (existing) {
|
|
12916
|
-
try {
|
|
12917
|
-
existing.stop();
|
|
12918
|
-
} catch {
|
|
12919
|
-
}
|
|
12920
|
-
}
|
|
12921
|
-
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
12922
|
-
try {
|
|
12923
|
-
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12924
|
-
this.store.saveScheduledTask(task);
|
|
12925
|
-
await this.runner(task);
|
|
12926
|
-
} catch (err) {
|
|
12927
|
-
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12928
|
-
}
|
|
12929
|
-
}, { timezone: "UTC" });
|
|
12930
|
-
this.cronJobs.set(task.id, job);
|
|
12931
|
-
}
|
|
12932
|
-
unschedule(taskId) {
|
|
12933
|
-
this.cronJobs.get(taskId)?.stop();
|
|
12934
|
-
this.cronJobs.delete(taskId);
|
|
12935
|
-
}
|
|
12936
|
-
add(task) {
|
|
12937
|
-
this.store.saveScheduledTask(task);
|
|
12938
|
-
if (task.enabled) this.schedule(task);
|
|
12939
|
-
}
|
|
12940
|
-
remove(taskId) {
|
|
12941
|
-
this.unschedule(taskId);
|
|
12942
|
-
this.store.deleteScheduledTask(taskId);
|
|
12943
|
-
}
|
|
12944
|
-
list() {
|
|
12945
|
-
return this.store.listScheduledTasks();
|
|
12946
|
-
}
|
|
12947
|
-
isRunning(taskId) {
|
|
12948
|
-
return this.cronJobs.has(taskId);
|
|
12949
|
-
}
|
|
12950
|
-
static validateCron(expression) {
|
|
12951
|
-
return cron__default.default.validate(expression);
|
|
12952
|
-
}
|
|
12953
|
-
};
|
|
12954
13315
|
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
12955
13316
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
12956
13317
|
function sanitizeEnvValue(v) {
|