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.js
CHANGED
|
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// src/constants.ts
|
|
193
|
-
var CASCADE_VERSION = "0.
|
|
193
|
+
var CASCADE_VERSION = "0.17.0";
|
|
194
194
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
195
195
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
196
196
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -11899,6 +11899,25 @@ var DashboardSocket = class {
|
|
|
11899
11899
|
});
|
|
11900
11900
|
});
|
|
11901
11901
|
}
|
|
11902
|
+
/**
|
|
11903
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
11904
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
11905
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
11906
|
+
*/
|
|
11907
|
+
onPlanDecision(callback) {
|
|
11908
|
+
this.io.on("connection", (socket) => {
|
|
11909
|
+
socket.on("plan:decision", (payload) => {
|
|
11910
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
11911
|
+
callback({
|
|
11912
|
+
sessionId: payload.sessionId,
|
|
11913
|
+
approved: payload.approved,
|
|
11914
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
11915
|
+
editedPlan: payload.editedPlan
|
|
11916
|
+
});
|
|
11917
|
+
}
|
|
11918
|
+
});
|
|
11919
|
+
});
|
|
11920
|
+
}
|
|
11902
11921
|
onSessionSteer(callback) {
|
|
11903
11922
|
this.io.on("connection", (socket) => {
|
|
11904
11923
|
socket.on("session:steer", (payload) => {
|
|
@@ -11940,6 +11959,122 @@ var DashboardSocket = class {
|
|
|
11940
11959
|
this.io.close();
|
|
11941
11960
|
}
|
|
11942
11961
|
};
|
|
11962
|
+
var TaskScheduler = class {
|
|
11963
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
11964
|
+
store;
|
|
11965
|
+
runner;
|
|
11966
|
+
constructor(store, runner) {
|
|
11967
|
+
this.store = store;
|
|
11968
|
+
this.runner = runner;
|
|
11969
|
+
}
|
|
11970
|
+
start() {
|
|
11971
|
+
const tasks = this.store.listScheduledTasks();
|
|
11972
|
+
for (const task of tasks) {
|
|
11973
|
+
if (task.enabled) this.schedule(task);
|
|
11974
|
+
}
|
|
11975
|
+
}
|
|
11976
|
+
stop() {
|
|
11977
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
11978
|
+
this.cronJobs.clear();
|
|
11979
|
+
}
|
|
11980
|
+
schedule(task) {
|
|
11981
|
+
if (!cron.validate(task.cronExpression)) {
|
|
11982
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
11983
|
+
}
|
|
11984
|
+
const existing = this.cronJobs.get(task.id);
|
|
11985
|
+
if (existing) {
|
|
11986
|
+
try {
|
|
11987
|
+
existing.stop();
|
|
11988
|
+
} catch {
|
|
11989
|
+
}
|
|
11990
|
+
}
|
|
11991
|
+
const job = cron.schedule(task.cronExpression, async () => {
|
|
11992
|
+
try {
|
|
11993
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
11994
|
+
this.store.saveScheduledTask(task);
|
|
11995
|
+
await this.runner(task);
|
|
11996
|
+
} catch (err) {
|
|
11997
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
11998
|
+
}
|
|
11999
|
+
}, { timezone: "UTC" });
|
|
12000
|
+
this.cronJobs.set(task.id, job);
|
|
12001
|
+
}
|
|
12002
|
+
unschedule(taskId) {
|
|
12003
|
+
this.cronJobs.get(taskId)?.stop();
|
|
12004
|
+
this.cronJobs.delete(taskId);
|
|
12005
|
+
}
|
|
12006
|
+
add(task) {
|
|
12007
|
+
this.store.saveScheduledTask(task);
|
|
12008
|
+
if (task.enabled) this.schedule(task);
|
|
12009
|
+
}
|
|
12010
|
+
remove(taskId) {
|
|
12011
|
+
this.unschedule(taskId);
|
|
12012
|
+
this.store.deleteScheduledTask(taskId);
|
|
12013
|
+
}
|
|
12014
|
+
list() {
|
|
12015
|
+
return this.store.listScheduledTasks();
|
|
12016
|
+
}
|
|
12017
|
+
isRunning(taskId) {
|
|
12018
|
+
return this.cronJobs.has(taskId);
|
|
12019
|
+
}
|
|
12020
|
+
static validateCron(expression) {
|
|
12021
|
+
return cron.validate(expression);
|
|
12022
|
+
}
|
|
12023
|
+
};
|
|
12024
|
+
|
|
12025
|
+
// src/dashboard/cost-stats.ts
|
|
12026
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
12027
|
+
function utcDateKey(d) {
|
|
12028
|
+
return d.toISOString().slice(0, 10);
|
|
12029
|
+
}
|
|
12030
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
12031
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
12032
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
12033
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12034
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
12035
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
12036
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
12037
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
12038
|
+
}
|
|
12039
|
+
let totalCostUsd = 0;
|
|
12040
|
+
let totalTokens = 0;
|
|
12041
|
+
let totalRuns = 0;
|
|
12042
|
+
for (const s of sessions) {
|
|
12043
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
12044
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
12045
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
12046
|
+
totalCostUsd += cost;
|
|
12047
|
+
totalTokens += tokens;
|
|
12048
|
+
totalRuns += runs;
|
|
12049
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
12050
|
+
if (!Number.isNaN(when.getTime())) {
|
|
12051
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
12052
|
+
if (bucket) {
|
|
12053
|
+
bucket.costUsd += cost;
|
|
12054
|
+
bucket.tokens += tokens;
|
|
12055
|
+
bucket.runs += runs;
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
12058
|
+
}
|
|
12059
|
+
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) => ({
|
|
12060
|
+
sessionId: s.id,
|
|
12061
|
+
title: s.title,
|
|
12062
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
12063
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
12064
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
12065
|
+
updatedAt: s.updatedAt
|
|
12066
|
+
}));
|
|
12067
|
+
return {
|
|
12068
|
+
totalCostUsd,
|
|
12069
|
+
totalTokens,
|
|
12070
|
+
totalSessions: sessions.length,
|
|
12071
|
+
totalRuns,
|
|
12072
|
+
perDay: [...buckets.values()],
|
|
12073
|
+
topSessions
|
|
12074
|
+
};
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12077
|
+
// src/dashboard/server.ts
|
|
11943
12078
|
var __dirname$1 = path20.dirname(fileURLToPath(import.meta.url));
|
|
11944
12079
|
var DashboardServer = class {
|
|
11945
12080
|
app;
|
|
@@ -11965,6 +12100,14 @@ var DashboardServer = class {
|
|
|
11965
12100
|
* map is how that answer reaches the run that's blocked on it.
|
|
11966
12101
|
*/
|
|
11967
12102
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
12103
|
+
/**
|
|
12104
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
12105
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
12106
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
12107
|
+
*/
|
|
12108
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
12109
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
12110
|
+
scheduler;
|
|
11968
12111
|
port;
|
|
11969
12112
|
host;
|
|
11970
12113
|
workspacePath;
|
|
@@ -11982,6 +12125,7 @@ var DashboardServer = class {
|
|
|
11982
12125
|
secret: this.dashboardSecret,
|
|
11983
12126
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
11984
12127
|
});
|
|
12128
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
11985
12129
|
this.setupMiddleware();
|
|
11986
12130
|
this.setupRoutes();
|
|
11987
12131
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -12045,6 +12189,9 @@ var DashboardServer = class {
|
|
|
12045
12189
|
cascade.on("peer:message", (e) => {
|
|
12046
12190
|
this.socket.emitPeerMessage(e);
|
|
12047
12191
|
});
|
|
12192
|
+
cascade.on("plan:approval-required", (e) => {
|
|
12193
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
12194
|
+
});
|
|
12048
12195
|
try {
|
|
12049
12196
|
const result = await cascade.run({
|
|
12050
12197
|
prompt: runPrompt,
|
|
@@ -12052,7 +12199,8 @@ var DashboardServer = class {
|
|
|
12052
12199
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12053
12200
|
});
|
|
12054
12201
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12055
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
12202
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12203
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12056
12204
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
12057
12205
|
this.socket.broadcast("cost:update", {
|
|
12058
12206
|
sessionId,
|
|
@@ -12062,6 +12210,7 @@ var DashboardServer = class {
|
|
|
12062
12210
|
this.throttledBroadcast("workspace");
|
|
12063
12211
|
} catch (err) {
|
|
12064
12212
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12213
|
+
this.captureWhy(sessionId, cascade);
|
|
12065
12214
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
12066
12215
|
sessionId,
|
|
12067
12216
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12072,9 +12221,17 @@ var DashboardServer = class {
|
|
|
12072
12221
|
this.denyPendingApprovals(sessionId);
|
|
12073
12222
|
}
|
|
12074
12223
|
});
|
|
12224
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
12225
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
12226
|
+
approved,
|
|
12227
|
+
note,
|
|
12228
|
+
editedPlan
|
|
12229
|
+
);
|
|
12230
|
+
});
|
|
12075
12231
|
this.socket.onSessionHalt((sessionId) => {
|
|
12076
12232
|
this.activeControllers.get(sessionId)?.abort();
|
|
12077
12233
|
this.denyPendingApprovals(sessionId);
|
|
12234
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
12078
12235
|
});
|
|
12079
12236
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
12080
12237
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -12107,12 +12264,21 @@ var DashboardServer = class {
|
|
|
12107
12264
|
resolve();
|
|
12108
12265
|
});
|
|
12109
12266
|
});
|
|
12267
|
+
try {
|
|
12268
|
+
this.scheduler.start();
|
|
12269
|
+
} catch (err) {
|
|
12270
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
12271
|
+
}
|
|
12110
12272
|
}
|
|
12111
12273
|
async stop() {
|
|
12112
12274
|
if (this.broadcastTimer) {
|
|
12113
12275
|
clearTimeout(this.broadcastTimer);
|
|
12114
12276
|
this.broadcastTimer = null;
|
|
12115
12277
|
}
|
|
12278
|
+
try {
|
|
12279
|
+
this.scheduler.stop();
|
|
12280
|
+
} catch {
|
|
12281
|
+
}
|
|
12116
12282
|
this.socket.close();
|
|
12117
12283
|
try {
|
|
12118
12284
|
this.globalStore?.close();
|
|
@@ -12233,16 +12399,60 @@ var DashboardServer = class {
|
|
|
12233
12399
|
return title;
|
|
12234
12400
|
}
|
|
12235
12401
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
12236
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
12402
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
12237
12403
|
try {
|
|
12238
12404
|
if (reply && reply.trim()) {
|
|
12239
12405
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12240
12406
|
}
|
|
12407
|
+
if (result) {
|
|
12408
|
+
const session = this.store.getSession(sessionId);
|
|
12409
|
+
if (session) {
|
|
12410
|
+
this.store.updateSession(sessionId, {
|
|
12411
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12412
|
+
metadata: {
|
|
12413
|
+
...session.metadata,
|
|
12414
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
12415
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
12416
|
+
taskCount: session.metadata.taskCount + 1
|
|
12417
|
+
}
|
|
12418
|
+
});
|
|
12419
|
+
}
|
|
12420
|
+
}
|
|
12241
12421
|
} catch (err) {
|
|
12242
12422
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
12243
12423
|
}
|
|
12244
12424
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
12245
12425
|
}
|
|
12426
|
+
/**
|
|
12427
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
12428
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
12429
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
12430
|
+
*/
|
|
12431
|
+
captureWhy(sessionId, cascade, result) {
|
|
12432
|
+
try {
|
|
12433
|
+
const stats = cascade.getRouter().getStats();
|
|
12434
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
12435
|
+
const report = {
|
|
12436
|
+
sessionId,
|
|
12437
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12438
|
+
decisions: cascade.getDecisionLog(),
|
|
12439
|
+
savedUsd: savings.savedUsd,
|
|
12440
|
+
savedPct: savings.savedPct,
|
|
12441
|
+
totalCostUsd: stats.totalCostUsd,
|
|
12442
|
+
totalTokens: stats.totalTokens,
|
|
12443
|
+
costByTier: stats.costByTier,
|
|
12444
|
+
durationMs: result?.durationMs
|
|
12445
|
+
};
|
|
12446
|
+
this.whyBySession.set(sessionId, report);
|
|
12447
|
+
if (this.whyBySession.size > 50) {
|
|
12448
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
12449
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
12450
|
+
}
|
|
12451
|
+
this.socket.broadcast("run:why", report);
|
|
12452
|
+
} catch (err) {
|
|
12453
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
12454
|
+
}
|
|
12455
|
+
}
|
|
12246
12456
|
/**
|
|
12247
12457
|
* Route steering text into running Cascade instances. Targets the given
|
|
12248
12458
|
* session, or every active session when none is specified (the desktop
|
|
@@ -12272,6 +12482,52 @@ var DashboardServer = class {
|
|
|
12272
12482
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
12273
12483
|
});
|
|
12274
12484
|
}
|
|
12485
|
+
/**
|
|
12486
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
12487
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
12488
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
12489
|
+
* client so an open desktop sees the run appear live.
|
|
12490
|
+
*/
|
|
12491
|
+
async runScheduledTask(task) {
|
|
12492
|
+
const sessionId = randomUUID();
|
|
12493
|
+
const prompt = task.prompt;
|
|
12494
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
12495
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
12496
|
+
this.activeSessions.set(sessionId, cascade);
|
|
12497
|
+
cascade.on("tier:status", (e) => {
|
|
12498
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
12499
|
+
});
|
|
12500
|
+
cascade.on("peer:message", (e) => {
|
|
12501
|
+
this.socket.emitPeerMessage(e);
|
|
12502
|
+
});
|
|
12503
|
+
try {
|
|
12504
|
+
const result = await cascade.run({
|
|
12505
|
+
prompt,
|
|
12506
|
+
identityId: task.identityId,
|
|
12507
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
12508
|
+
});
|
|
12509
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
12510
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12511
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12512
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
12513
|
+
this.socket.broadcast("cost:update", {
|
|
12514
|
+
sessionId,
|
|
12515
|
+
totalTokens: result.usage.totalTokens,
|
|
12516
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
12517
|
+
});
|
|
12518
|
+
this.throttledBroadcast("workspace");
|
|
12519
|
+
} catch (err) {
|
|
12520
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12521
|
+
this.captureWhy(sessionId, cascade);
|
|
12522
|
+
this.socket.broadcast("session:error", {
|
|
12523
|
+
sessionId,
|
|
12524
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12525
|
+
});
|
|
12526
|
+
throw err;
|
|
12527
|
+
} finally {
|
|
12528
|
+
this.activeSessions.delete(sessionId);
|
|
12529
|
+
}
|
|
12530
|
+
}
|
|
12275
12531
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
12276
12532
|
denyPendingApprovals(sessionId) {
|
|
12277
12533
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -12784,6 +13040,9 @@ ${prompt}`;
|
|
|
12784
13040
|
cascade.on("peer:message", (e) => {
|
|
12785
13041
|
this.socket.emitPeerMessage(e);
|
|
12786
13042
|
});
|
|
13043
|
+
cascade.on("plan:approval-required", (e) => {
|
|
13044
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
13045
|
+
});
|
|
12787
13046
|
try {
|
|
12788
13047
|
const result = await cascade.run({
|
|
12789
13048
|
prompt: runPrompt,
|
|
@@ -12791,7 +13050,8 @@ ${prompt}`;
|
|
|
12791
13050
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12792
13051
|
});
|
|
12793
13052
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12794
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
13053
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
13054
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12795
13055
|
this.socket.broadcast("cost:update", {
|
|
12796
13056
|
sessionId,
|
|
12797
13057
|
totalTokens: result.usage.totalTokens,
|
|
@@ -12801,6 +13061,7 @@ ${prompt}`;
|
|
|
12801
13061
|
this.throttledBroadcast("workspace");
|
|
12802
13062
|
} catch (err) {
|
|
12803
13063
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
13064
|
+
this.captureWhy(sessionId, cascade);
|
|
12804
13065
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
12805
13066
|
sessionId,
|
|
12806
13067
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12822,6 +13083,168 @@ ${prompt}`;
|
|
|
12822
13083
|
}))
|
|
12823
13084
|
});
|
|
12824
13085
|
});
|
|
13086
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
13087
|
+
const report = this.whyBySession.get(req.params.id);
|
|
13088
|
+
if (!report) {
|
|
13089
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
13090
|
+
return;
|
|
13091
|
+
}
|
|
13092
|
+
res.json(report);
|
|
13093
|
+
});
|
|
13094
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
13095
|
+
const sessionId = req.params.id;
|
|
13096
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13097
|
+
const before = /* @__PURE__ */ new Map();
|
|
13098
|
+
for (const taskId of taskIds) {
|
|
13099
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
13100
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
13101
|
+
}
|
|
13102
|
+
}
|
|
13103
|
+
const { readFile } = await import('fs/promises');
|
|
13104
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
13105
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
13106
|
+
let after = "";
|
|
13107
|
+
let missing = false;
|
|
13108
|
+
try {
|
|
13109
|
+
const buf = await readFile(filePath);
|
|
13110
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
13111
|
+
} catch {
|
|
13112
|
+
missing = true;
|
|
13113
|
+
}
|
|
13114
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
13115
|
+
}));
|
|
13116
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
13117
|
+
});
|
|
13118
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
13119
|
+
const sessionId = req.params.id;
|
|
13120
|
+
const body = req.body;
|
|
13121
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
13122
|
+
res.status(400).json({ error: "filePath is required" });
|
|
13123
|
+
return;
|
|
13124
|
+
}
|
|
13125
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13126
|
+
let content;
|
|
13127
|
+
for (const taskId of taskIds) {
|
|
13128
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
13129
|
+
if (snap) {
|
|
13130
|
+
content = snap.content;
|
|
13131
|
+
break;
|
|
13132
|
+
}
|
|
13133
|
+
}
|
|
13134
|
+
if (content === void 0) {
|
|
13135
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
13136
|
+
return;
|
|
13137
|
+
}
|
|
13138
|
+
try {
|
|
13139
|
+
const { writeFile } = await import('fs/promises');
|
|
13140
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
13141
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
13142
|
+
} catch (err) {
|
|
13143
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13144
|
+
}
|
|
13145
|
+
});
|
|
13146
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
13147
|
+
try {
|
|
13148
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13149
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
13150
|
+
res.json({
|
|
13151
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
13152
|
+
budget: {
|
|
13153
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
13154
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
13155
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
13156
|
+
}
|
|
13157
|
+
});
|
|
13158
|
+
} catch (err) {
|
|
13159
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13160
|
+
}
|
|
13161
|
+
});
|
|
13162
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
13163
|
+
try {
|
|
13164
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
13165
|
+
} catch (err) {
|
|
13166
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13167
|
+
}
|
|
13168
|
+
});
|
|
13169
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
13170
|
+
const body = req.body;
|
|
13171
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
13172
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
13175
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
13176
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13177
|
+
return;
|
|
13178
|
+
}
|
|
13179
|
+
const task = {
|
|
13180
|
+
id: randomUUID(),
|
|
13181
|
+
name: body.name.trim(),
|
|
13182
|
+
cronExpression: body.cronExpression.trim(),
|
|
13183
|
+
prompt: body.prompt.trim(),
|
|
13184
|
+
workspacePath: this.workspacePath,
|
|
13185
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13186
|
+
enabled: body.enabled !== false
|
|
13187
|
+
};
|
|
13188
|
+
try {
|
|
13189
|
+
this.scheduler.add(task);
|
|
13190
|
+
res.json(task);
|
|
13191
|
+
} catch (err) {
|
|
13192
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13193
|
+
}
|
|
13194
|
+
});
|
|
13195
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13196
|
+
const id = req.params.id;
|
|
13197
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
13198
|
+
if (!existing) {
|
|
13199
|
+
res.status(404).json({ error: "Not found" });
|
|
13200
|
+
return;
|
|
13201
|
+
}
|
|
13202
|
+
const body = req.body;
|
|
13203
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
13204
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13205
|
+
return;
|
|
13206
|
+
}
|
|
13207
|
+
const updated = {
|
|
13208
|
+
...existing,
|
|
13209
|
+
name: body.name?.trim() || existing.name,
|
|
13210
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
13211
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
13212
|
+
enabled: body.enabled ?? existing.enabled
|
|
13213
|
+
};
|
|
13214
|
+
try {
|
|
13215
|
+
this.scheduler.add(updated);
|
|
13216
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
13217
|
+
res.json(updated);
|
|
13218
|
+
} catch (err) {
|
|
13219
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13220
|
+
}
|
|
13221
|
+
});
|
|
13222
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13223
|
+
try {
|
|
13224
|
+
this.scheduler.remove(req.params.id);
|
|
13225
|
+
res.json({ ok: true });
|
|
13226
|
+
} catch (err) {
|
|
13227
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13228
|
+
}
|
|
13229
|
+
});
|
|
13230
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
13231
|
+
try {
|
|
13232
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
13233
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
13234
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
13235
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
13236
|
+
try {
|
|
13237
|
+
const all = logger.getAllLogs();
|
|
13238
|
+
const total = all.length;
|
|
13239
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
13240
|
+
res.json({ total, offset, entries });
|
|
13241
|
+
} finally {
|
|
13242
|
+
logger.close();
|
|
13243
|
+
}
|
|
13244
|
+
} catch (err) {
|
|
13245
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13246
|
+
}
|
|
13247
|
+
});
|
|
12825
13248
|
const prodPath = path20.resolve(__dirname$1, "../web/dist");
|
|
12826
13249
|
const devPath = path20.resolve(__dirname$1, "../../web/dist");
|
|
12827
13250
|
const webDistPath = fs19.existsSync(prodPath) ? prodPath : devPath;
|
|
@@ -12843,68 +13266,6 @@ ${prompt}`;
|
|
|
12843
13266
|
}
|
|
12844
13267
|
}
|
|
12845
13268
|
};
|
|
12846
|
-
var TaskScheduler = class {
|
|
12847
|
-
cronJobs = /* @__PURE__ */ new Map();
|
|
12848
|
-
store;
|
|
12849
|
-
runner;
|
|
12850
|
-
constructor(store, runner) {
|
|
12851
|
-
this.store = store;
|
|
12852
|
-
this.runner = runner;
|
|
12853
|
-
}
|
|
12854
|
-
start() {
|
|
12855
|
-
const tasks = this.store.listScheduledTasks();
|
|
12856
|
-
for (const task of tasks) {
|
|
12857
|
-
if (task.enabled) this.schedule(task);
|
|
12858
|
-
}
|
|
12859
|
-
}
|
|
12860
|
-
stop() {
|
|
12861
|
-
for (const job of this.cronJobs.values()) job.stop();
|
|
12862
|
-
this.cronJobs.clear();
|
|
12863
|
-
}
|
|
12864
|
-
schedule(task) {
|
|
12865
|
-
if (!cron.validate(task.cronExpression)) {
|
|
12866
|
-
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12867
|
-
}
|
|
12868
|
-
const existing = this.cronJobs.get(task.id);
|
|
12869
|
-
if (existing) {
|
|
12870
|
-
try {
|
|
12871
|
-
existing.stop();
|
|
12872
|
-
} catch {
|
|
12873
|
-
}
|
|
12874
|
-
}
|
|
12875
|
-
const job = cron.schedule(task.cronExpression, async () => {
|
|
12876
|
-
try {
|
|
12877
|
-
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12878
|
-
this.store.saveScheduledTask(task);
|
|
12879
|
-
await this.runner(task);
|
|
12880
|
-
} catch (err) {
|
|
12881
|
-
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12882
|
-
}
|
|
12883
|
-
}, { timezone: "UTC" });
|
|
12884
|
-
this.cronJobs.set(task.id, job);
|
|
12885
|
-
}
|
|
12886
|
-
unschedule(taskId) {
|
|
12887
|
-
this.cronJobs.get(taskId)?.stop();
|
|
12888
|
-
this.cronJobs.delete(taskId);
|
|
12889
|
-
}
|
|
12890
|
-
add(task) {
|
|
12891
|
-
this.store.saveScheduledTask(task);
|
|
12892
|
-
if (task.enabled) this.schedule(task);
|
|
12893
|
-
}
|
|
12894
|
-
remove(taskId) {
|
|
12895
|
-
this.unschedule(taskId);
|
|
12896
|
-
this.store.deleteScheduledTask(taskId);
|
|
12897
|
-
}
|
|
12898
|
-
list() {
|
|
12899
|
-
return this.store.listScheduledTasks();
|
|
12900
|
-
}
|
|
12901
|
-
isRunning(taskId) {
|
|
12902
|
-
return this.cronJobs.has(taskId);
|
|
12903
|
-
}
|
|
12904
|
-
static validateCron(expression) {
|
|
12905
|
-
return cron.validate(expression);
|
|
12906
|
-
}
|
|
12907
|
-
};
|
|
12908
13269
|
var execFileAsync2 = promisify(execFile);
|
|
12909
13270
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
12910
13271
|
function sanitizeEnvValue(v) {
|