cascade-ai 0.15.2 → 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 +509 -12
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +508 -12
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +483 -74
- package/dist/index.cjs +480 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -4
- package/dist/index.d.ts +59 -4
- package/dist/index.js +480 -71
- 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";
|
|
@@ -3179,6 +3180,11 @@ function validateConfig(raw) {
|
|
|
3179
3180
|
|
|
3180
3181
|
// src/config/index.ts
|
|
3181
3182
|
init_constants();
|
|
3183
|
+
var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
|
|
3184
|
+
function hasUsableProvider(providers) {
|
|
3185
|
+
if (!providers?.length) return false;
|
|
3186
|
+
return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey);
|
|
3187
|
+
}
|
|
3182
3188
|
var ConfigManager = class {
|
|
3183
3189
|
config;
|
|
3184
3190
|
keystore;
|
|
@@ -7467,6 +7473,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7467
7473
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
7468
7474
|
if (this.store) worker.setStore(this.store, taskId);
|
|
7469
7475
|
worker.setPeerBus(this.t3PeerBus);
|
|
7476
|
+
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
7477
|
+
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
7478
|
+
worker.on("log", (e) => this.emit("log", e));
|
|
7479
|
+
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
7470
7480
|
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
7471
7481
|
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
7472
7482
|
...e,
|
|
@@ -7818,12 +7828,14 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
7818
7828
|
status: "IN_PROGRESS"
|
|
7819
7829
|
});
|
|
7820
7830
|
const okBefore = okCount(allT2Results);
|
|
7831
|
+
const completedSummary = this.summarizeCompletedSections(allT2Results);
|
|
7832
|
+
const correctionContext = [systemContext, completedSummary].filter(Boolean).join("\n\n") || void 0;
|
|
7821
7833
|
const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
|
|
7822
7834
|
Review reason: ${reviewResult.reason}
|
|
7823
7835
|
|
|
7824
7836
|
Original goal: ${enrichedPrompt}
|
|
7825
7837
|
|
|
7826
|
-
Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections
|
|
7838
|
+
Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`, correctionContext);
|
|
7827
7839
|
const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
|
|
7828
7840
|
allT2Results = [...allT2Results, ...correctionResults];
|
|
7829
7841
|
if (okCount(allT2Results) <= okBefore) {
|
|
@@ -7926,6 +7938,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7926
7938
|
return null;
|
|
7927
7939
|
}
|
|
7928
7940
|
}
|
|
7941
|
+
/** Structured, grounded summary of what's already done — used to keep
|
|
7942
|
+
* corrective replan passes from re-emitting completed sections. */
|
|
7943
|
+
summarizeCompletedSections(results) {
|
|
7944
|
+
const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
|
|
7945
|
+
if (done.length === 0) return "";
|
|
7946
|
+
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
7947
|
+
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
7948
|
+
${lines.join("\n")}`;
|
|
7949
|
+
}
|
|
7929
7950
|
async decomposeTask(prompt, systemContext) {
|
|
7930
7951
|
const db = this.router.getWorldStateDB?.();
|
|
7931
7952
|
let worldStateContext = "";
|
|
@@ -8193,7 +8214,7 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
8193
8214
|
}
|
|
8194
8215
|
}
|
|
8195
8216
|
if (readyIds.length === 0) return;
|
|
8196
|
-
|
|
8217
|
+
const runOne = async (id) => {
|
|
8197
8218
|
resultMap.set(id, null);
|
|
8198
8219
|
const index = sections.findIndex((s) => s.sectionId === id);
|
|
8199
8220
|
const section = sections[index];
|
|
@@ -8233,7 +8254,14 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
8233
8254
|
for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
|
|
8234
8255
|
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
8235
8256
|
}
|
|
8236
|
-
}
|
|
8257
|
+
};
|
|
8258
|
+
if (this.router.getT3ExecutionMode?.() === "sequential") {
|
|
8259
|
+
for (const id of readyIds) {
|
|
8260
|
+
await runOne(id);
|
|
8261
|
+
}
|
|
8262
|
+
} else {
|
|
8263
|
+
await Promise.all(readyIds.map(runOne));
|
|
8264
|
+
}
|
|
8237
8265
|
if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
|
|
8238
8266
|
await executeWave();
|
|
8239
8267
|
}
|
|
@@ -10037,6 +10065,13 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
10037
10065
|
* All T3 workers under the same T2 share cached decisions for the same tool.
|
|
10038
10066
|
*/
|
|
10039
10067
|
sessionCache = /* @__PURE__ */ new Map();
|
|
10068
|
+
/**
|
|
10069
|
+
* Task-wide cache keyed by `toolName` alone, for USER- and T1-level
|
|
10070
|
+
* "always" decisions — these are meant to cover every sibling T2/T3 in the
|
|
10071
|
+
* run, not just the one that happened to ask first (see PermissionDecision
|
|
10072
|
+
* doc comment: "task-wide for T1").
|
|
10073
|
+
*/
|
|
10074
|
+
taskWideCache = /* @__PURE__ */ new Map();
|
|
10040
10075
|
t2Evaluator;
|
|
10041
10076
|
t1Evaluator;
|
|
10042
10077
|
/** Pending user-decision resolvers keyed by request ID */
|
|
@@ -10065,6 +10100,15 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
10065
10100
|
* Returns a PermissionDecision from whichever tier was able to decide.
|
|
10066
10101
|
*/
|
|
10067
10102
|
async requestPermission(req) {
|
|
10103
|
+
if (!req.forceReprompt && this.taskWideCache.has(req.toolName)) {
|
|
10104
|
+
return {
|
|
10105
|
+
requestId: req.id,
|
|
10106
|
+
approved: this.taskWideCache.get(req.toolName),
|
|
10107
|
+
always: true,
|
|
10108
|
+
decidedBy: "T1",
|
|
10109
|
+
reasoning: "Cached from a previous task-wide decision in this session"
|
|
10110
|
+
};
|
|
10111
|
+
}
|
|
10068
10112
|
const cacheKey = `${req.parentT2Id}:${req.toolName}`;
|
|
10069
10113
|
if (!req.forceReprompt && this.sessionCache.has(cacheKey)) {
|
|
10070
10114
|
return {
|
|
@@ -10109,7 +10153,7 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
10109
10153
|
try {
|
|
10110
10154
|
const t1Decision = await this.t1Evaluator(req);
|
|
10111
10155
|
if (t1Decision !== null) {
|
|
10112
|
-
if (t1Decision.always) this.
|
|
10156
|
+
if (t1Decision.always) this.taskWideCache.set(req.toolName, t1Decision.approved);
|
|
10113
10157
|
return t1Decision;
|
|
10114
10158
|
}
|
|
10115
10159
|
} catch {
|
|
@@ -10139,7 +10183,7 @@ var PermissionEscalator = class extends EventEmitter {
|
|
|
10139
10183
|
const wrappedResolver = (decision) => {
|
|
10140
10184
|
if (timer) clearTimeout(timer);
|
|
10141
10185
|
if (decision.always) {
|
|
10142
|
-
this.
|
|
10186
|
+
this.taskWideCache.set(req.toolName, decision.approved);
|
|
10143
10187
|
}
|
|
10144
10188
|
resolve(decision);
|
|
10145
10189
|
};
|
|
@@ -15218,11 +15262,18 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15218
15262
|
}
|
|
15219
15263
|
});
|
|
15220
15264
|
const currentEntry = state.entries[state.currentEntryIdx];
|
|
15265
|
+
const rejectIfBlank = useCallback((val, message) => {
|
|
15266
|
+
if (val.trim()) return false;
|
|
15267
|
+
dispatch({ type: "SET_ERROR", error: message });
|
|
15268
|
+
return true;
|
|
15269
|
+
}, []);
|
|
15221
15270
|
const handleFieldSubmit = useCallback((val) => {
|
|
15222
15271
|
if (!currentEntry) return;
|
|
15223
15272
|
if (currentEntry.type === "azure") {
|
|
15224
15273
|
if (fieldStage === "deploymentName") {
|
|
15274
|
+
if (rejectIfBlank(val, "Deployment name is required.")) return;
|
|
15225
15275
|
dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
|
|
15276
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15226
15277
|
setFieldBuffer("");
|
|
15227
15278
|
if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
|
|
15228
15279
|
setFieldStage("askMore");
|
|
@@ -15230,38 +15281,50 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15230
15281
|
setFieldStage("baseUrl");
|
|
15231
15282
|
}
|
|
15232
15283
|
} else if (fieldStage === "baseUrl") {
|
|
15284
|
+
if (rejectIfBlank(val, "Azure endpoint URL is required.")) return;
|
|
15233
15285
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
|
|
15286
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15234
15287
|
setFieldBuffer("");
|
|
15235
15288
|
setFieldStage("apiKey");
|
|
15236
15289
|
} else if (fieldStage === "apiKey") {
|
|
15290
|
+
if (rejectIfBlank(val, "API key is required.")) return;
|
|
15237
15291
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15292
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15238
15293
|
setFieldBuffer("");
|
|
15239
15294
|
setFieldStage("apiVersion");
|
|
15240
15295
|
} else if (fieldStage === "apiVersion") {
|
|
15241
15296
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
|
|
15297
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15242
15298
|
setFieldBuffer("");
|
|
15243
15299
|
setFieldStage("askMore");
|
|
15244
15300
|
}
|
|
15245
15301
|
} else if (currentEntry.type === "openai-compatible") {
|
|
15246
15302
|
if (fieldStage === "label") {
|
|
15247
15303
|
dispatch({ type: "SET_ENTRY_FIELD", field: "label", value: val || currentEntry.label });
|
|
15304
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15248
15305
|
setFieldBuffer("");
|
|
15249
15306
|
setFieldStage("baseUrl");
|
|
15250
15307
|
} else if (fieldStage === "baseUrl") {
|
|
15308
|
+
if (rejectIfBlank(val, "Base URL is required.")) return;
|
|
15251
15309
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
|
|
15310
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15252
15311
|
setFieldBuffer("");
|
|
15253
15312
|
setFieldStage("apiKey");
|
|
15254
15313
|
} else if (fieldStage === "apiKey") {
|
|
15255
15314
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15315
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15256
15316
|
setFieldBuffer("");
|
|
15257
15317
|
setFieldStage("askMore");
|
|
15258
15318
|
}
|
|
15259
15319
|
} else if (currentEntry.type === "ollama") {
|
|
15260
15320
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
|
|
15321
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15261
15322
|
setFieldBuffer("");
|
|
15262
15323
|
setFieldStage("askMore");
|
|
15263
15324
|
} else {
|
|
15325
|
+
if (rejectIfBlank(val, "API key is required.")) return;
|
|
15264
15326
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15327
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15265
15328
|
setFieldBuffer("");
|
|
15266
15329
|
const nextEntry = state.entries[state.currentEntryIdx + 1];
|
|
15267
15330
|
if (nextEntry) {
|
|
@@ -15269,7 +15332,7 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15269
15332
|
}
|
|
15270
15333
|
dispatch({ type: "NEXT_ENTRY" });
|
|
15271
15334
|
}
|
|
15272
|
-
}, [currentEntry, fieldStage]);
|
|
15335
|
+
}, [currentEntry, fieldStage, rejectIfBlank, state.entries, state.currentEntryIdx]);
|
|
15273
15336
|
if (state.step === "PROVIDER_SELECT") {
|
|
15274
15337
|
return /* @__PURE__ */ jsxs(Frame, { theme, phase: "keys", children: [
|
|
15275
15338
|
/* @__PURE__ */ jsxs(Box, { marginBottom: 1, children: [
|
|
@@ -15974,6 +16037,25 @@ var DashboardSocket = class {
|
|
|
15974
16037
|
});
|
|
15975
16038
|
});
|
|
15976
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
|
+
}
|
|
15977
16059
|
onSessionSteer(callback) {
|
|
15978
16060
|
this.io.on("connection", (socket) => {
|
|
15979
16061
|
socket.on("session:steer", (payload) => {
|
|
@@ -16018,6 +16100,122 @@ var DashboardSocket = class {
|
|
|
16018
16100
|
|
|
16019
16101
|
// src/dashboard/server.ts
|
|
16020
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
|
|
16021
16219
|
var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
|
|
16022
16220
|
var DashboardServer = class {
|
|
16023
16221
|
app;
|
|
@@ -16043,6 +16241,14 @@ var DashboardServer = class {
|
|
|
16043
16241
|
* map is how that answer reaches the run that's blocked on it.
|
|
16044
16242
|
*/
|
|
16045
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;
|
|
16046
16252
|
port;
|
|
16047
16253
|
host;
|
|
16048
16254
|
workspacePath;
|
|
@@ -16060,6 +16266,7 @@ var DashboardServer = class {
|
|
|
16060
16266
|
secret: this.dashboardSecret,
|
|
16061
16267
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
16062
16268
|
});
|
|
16269
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
16063
16270
|
this.setupMiddleware();
|
|
16064
16271
|
this.setupRoutes();
|
|
16065
16272
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -16123,6 +16330,9 @@ var DashboardServer = class {
|
|
|
16123
16330
|
cascade.on("peer:message", (e) => {
|
|
16124
16331
|
this.socket.emitPeerMessage(e);
|
|
16125
16332
|
});
|
|
16333
|
+
cascade.on("plan:approval-required", (e) => {
|
|
16334
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
16335
|
+
});
|
|
16126
16336
|
try {
|
|
16127
16337
|
const result = await cascade.run({
|
|
16128
16338
|
prompt: runPrompt,
|
|
@@ -16130,7 +16340,8 @@ var DashboardServer = class {
|
|
|
16130
16340
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16131
16341
|
});
|
|
16132
16342
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16133
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16343
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16344
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16134
16345
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
16135
16346
|
this.socket.broadcast("cost:update", {
|
|
16136
16347
|
sessionId,
|
|
@@ -16140,6 +16351,7 @@ var DashboardServer = class {
|
|
|
16140
16351
|
this.throttledBroadcast("workspace");
|
|
16141
16352
|
} catch (err) {
|
|
16142
16353
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16354
|
+
this.captureWhy(sessionId, cascade);
|
|
16143
16355
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
16144
16356
|
sessionId,
|
|
16145
16357
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16150,9 +16362,17 @@ var DashboardServer = class {
|
|
|
16150
16362
|
this.denyPendingApprovals(sessionId);
|
|
16151
16363
|
}
|
|
16152
16364
|
});
|
|
16365
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
16366
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
16367
|
+
approved,
|
|
16368
|
+
note,
|
|
16369
|
+
editedPlan
|
|
16370
|
+
);
|
|
16371
|
+
});
|
|
16153
16372
|
this.socket.onSessionHalt((sessionId) => {
|
|
16154
16373
|
this.activeControllers.get(sessionId)?.abort();
|
|
16155
16374
|
this.denyPendingApprovals(sessionId);
|
|
16375
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
16156
16376
|
});
|
|
16157
16377
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16158
16378
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -16185,12 +16405,21 @@ var DashboardServer = class {
|
|
|
16185
16405
|
resolve();
|
|
16186
16406
|
});
|
|
16187
16407
|
});
|
|
16408
|
+
try {
|
|
16409
|
+
this.scheduler.start();
|
|
16410
|
+
} catch (err) {
|
|
16411
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
16412
|
+
}
|
|
16188
16413
|
}
|
|
16189
16414
|
async stop() {
|
|
16190
16415
|
if (this.broadcastTimer) {
|
|
16191
16416
|
clearTimeout(this.broadcastTimer);
|
|
16192
16417
|
this.broadcastTimer = null;
|
|
16193
16418
|
}
|
|
16419
|
+
try {
|
|
16420
|
+
this.scheduler.stop();
|
|
16421
|
+
} catch {
|
|
16422
|
+
}
|
|
16194
16423
|
this.socket.close();
|
|
16195
16424
|
try {
|
|
16196
16425
|
this.globalStore?.close();
|
|
@@ -16204,6 +16433,16 @@ var DashboardServer = class {
|
|
|
16204
16433
|
getSocket() {
|
|
16205
16434
|
return this.socket;
|
|
16206
16435
|
}
|
|
16436
|
+
/**
|
|
16437
|
+
* Rebind the workspace tasks execute in — e.g. the desktop app's Code view
|
|
16438
|
+
* opening a different project folder — without tearing down the socket
|
|
16439
|
+
* server (which would drop the port/auth token/connection mid-session).
|
|
16440
|
+
* The next `cascade:run` picks this up immediately since `this.workspacePath`
|
|
16441
|
+
* is read live per-run (see onCascadeRun below).
|
|
16442
|
+
*/
|
|
16443
|
+
setWorkspacePath(workspacePath) {
|
|
16444
|
+
this.workspacePath = workspacePath;
|
|
16445
|
+
}
|
|
16207
16446
|
/**
|
|
16208
16447
|
* Write the in-memory config back to the workspace config file so mutations
|
|
16209
16448
|
* made over the socket (Settings → Save) persist across restarts. Best-effort:
|
|
@@ -16301,16 +16540,60 @@ var DashboardServer = class {
|
|
|
16301
16540
|
return title;
|
|
16302
16541
|
}
|
|
16303
16542
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
16304
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
16543
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
16305
16544
|
try {
|
|
16306
16545
|
if (reply && reply.trim()) {
|
|
16307
16546
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16308
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
|
+
}
|
|
16309
16562
|
} catch (err) {
|
|
16310
16563
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
16311
16564
|
}
|
|
16312
16565
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
16313
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
|
+
}
|
|
16314
16597
|
/**
|
|
16315
16598
|
* Route steering text into running Cascade instances. Targets the given
|
|
16316
16599
|
* session, or every active session when none is specified (the desktop
|
|
@@ -16340,6 +16623,52 @@ var DashboardServer = class {
|
|
|
16340
16623
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16341
16624
|
});
|
|
16342
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
|
+
}
|
|
16343
16672
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16344
16673
|
denyPendingApprovals(sessionId) {
|
|
16345
16674
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -16852,6 +17181,9 @@ ${prompt}`;
|
|
|
16852
17181
|
cascade.on("peer:message", (e) => {
|
|
16853
17182
|
this.socket.emitPeerMessage(e);
|
|
16854
17183
|
});
|
|
17184
|
+
cascade.on("plan:approval-required", (e) => {
|
|
17185
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
17186
|
+
});
|
|
16855
17187
|
try {
|
|
16856
17188
|
const result = await cascade.run({
|
|
16857
17189
|
prompt: runPrompt,
|
|
@@ -16859,7 +17191,8 @@ ${prompt}`;
|
|
|
16859
17191
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16860
17192
|
});
|
|
16861
17193
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16862
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
17194
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17195
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16863
17196
|
this.socket.broadcast("cost:update", {
|
|
16864
17197
|
sessionId,
|
|
16865
17198
|
totalTokens: result.usage.totalTokens,
|
|
@@ -16869,6 +17202,7 @@ ${prompt}`;
|
|
|
16869
17202
|
this.throttledBroadcast("workspace");
|
|
16870
17203
|
} catch (err) {
|
|
16871
17204
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17205
|
+
this.captureWhy(sessionId, cascade);
|
|
16872
17206
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
16873
17207
|
sessionId,
|
|
16874
17208
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16890,6 +17224,168 @@ ${prompt}`;
|
|
|
16890
17224
|
}))
|
|
16891
17225
|
});
|
|
16892
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
|
+
});
|
|
16893
17389
|
const prodPath = path25.resolve(__dirname$1, "../web/dist");
|
|
16894
17390
|
const devPath = path25.resolve(__dirname$1, "../../web/dist");
|
|
16895
17391
|
const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
|
|
@@ -17507,7 +18003,7 @@ async function startRepl(options) {
|
|
|
17507
18003
|
process.exit(1);
|
|
17508
18004
|
}
|
|
17509
18005
|
let config = cm.getConfig();
|
|
17510
|
-
const needsSetup = !config.providers
|
|
18006
|
+
const needsSetup = !hasUsableProvider(config.providers);
|
|
17511
18007
|
if (needsSetup) {
|
|
17512
18008
|
console.log(chalk9.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
|
|
17513
18009
|
console.log();
|
|
@@ -17548,7 +18044,7 @@ async function runHeadless(prompt, options) {
|
|
|
17548
18044
|
process.exit(1);
|
|
17549
18045
|
}
|
|
17550
18046
|
const config = cm.getConfig();
|
|
17551
|
-
const needsSetup = !config.providers
|
|
18047
|
+
const needsSetup = !hasUsableProvider(config.providers);
|
|
17552
18048
|
if (needsSetup) {
|
|
17553
18049
|
console.error(chalk9.red("No providers configured. Run `cascade init` first."));
|
|
17554
18050
|
process.exit(1);
|