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