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.cjs
CHANGED
|
@@ -43,6 +43,7 @@ var bcrypt = require('bcryptjs');
|
|
|
43
43
|
var socket_io = require('socket.io');
|
|
44
44
|
var parser = require('socket.io-msgpack-parser');
|
|
45
45
|
var jwt = require('jsonwebtoken');
|
|
46
|
+
var cron = require('node-cron');
|
|
46
47
|
|
|
47
48
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
48
49
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -93,6 +94,7 @@ var rateLimit__default = /*#__PURE__*/_interopDefault(rateLimit);
|
|
|
93
94
|
var bcrypt__default = /*#__PURE__*/_interopDefault(bcrypt);
|
|
94
95
|
var parser__default = /*#__PURE__*/_interopDefault(parser);
|
|
95
96
|
var jwt__default = /*#__PURE__*/_interopDefault(jwt);
|
|
97
|
+
var cron__default = /*#__PURE__*/_interopDefault(cron);
|
|
96
98
|
|
|
97
99
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
98
100
|
var __defProp = Object.defineProperty;
|
|
@@ -109,7 +111,7 @@ var __export = (target, all) => {
|
|
|
109
111
|
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;
|
|
110
112
|
var init_constants = __esm({
|
|
111
113
|
"src/constants.ts"() {
|
|
112
|
-
CASCADE_VERSION = "0.
|
|
114
|
+
CASCADE_VERSION = "0.17.0";
|
|
113
115
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
114
116
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
115
117
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -3230,6 +3232,11 @@ function validateConfig(raw) {
|
|
|
3230
3232
|
|
|
3231
3233
|
// src/config/index.ts
|
|
3232
3234
|
init_constants();
|
|
3235
|
+
var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
|
|
3236
|
+
function hasUsableProvider(providers) {
|
|
3237
|
+
if (!providers?.length) return false;
|
|
3238
|
+
return providers.some((p) => KEY_OPTIONAL_PROVIDER_TYPES.has(p.type) || !!p.apiKey);
|
|
3239
|
+
}
|
|
3233
3240
|
var ConfigManager = class {
|
|
3234
3241
|
config;
|
|
3235
3242
|
keystore;
|
|
@@ -7518,6 +7525,10 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7518
7525
|
const worker = new T3Worker(this.router, this.toolRegistry, this.id);
|
|
7519
7526
|
if (this.store) worker.setStore(this.store, taskId);
|
|
7520
7527
|
worker.setPeerBus(this.t3PeerBus);
|
|
7528
|
+
if (this.permissionEscalator) worker.setPermissionEscalator(this.permissionEscalator);
|
|
7529
|
+
if (this.toolCreator) worker.setToolCreator(this.toolCreator);
|
|
7530
|
+
worker.on("log", (e) => this.emit("log", e));
|
|
7531
|
+
worker.on("tier:status", (e) => this.emit("tier:status", e));
|
|
7521
7532
|
worker.on("stream:token", (e) => this.emit("stream:token", e));
|
|
7522
7533
|
worker.on("tool:approval-request", (e) => this.emit("tool:approval-request", {
|
|
7523
7534
|
...e,
|
|
@@ -7869,12 +7880,14 @@ Board guidance (must be followed in the plan): ${decision.note}`,
|
|
|
7869
7880
|
status: "IN_PROGRESS"
|
|
7870
7881
|
});
|
|
7871
7882
|
const okBefore = okCount(allT2Results);
|
|
7883
|
+
const completedSummary = this.summarizeCompletedSections(allT2Results);
|
|
7884
|
+
const correctionContext = [systemContext, completedSummary].filter(Boolean).join("\n\n") || void 0;
|
|
7872
7885
|
const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
|
|
7873
7886
|
Review reason: ${reviewResult.reason}
|
|
7874
7887
|
|
|
7875
7888
|
Original goal: ${enrichedPrompt}
|
|
7876
7889
|
|
|
7877
|
-
Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections
|
|
7890
|
+
Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`, correctionContext);
|
|
7878
7891
|
const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
|
|
7879
7892
|
allT2Results = [...allT2Results, ...correctionResults];
|
|
7880
7893
|
if (okCount(allT2Results) <= okBefore) {
|
|
@@ -7977,6 +7990,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7977
7990
|
return null;
|
|
7978
7991
|
}
|
|
7979
7992
|
}
|
|
7993
|
+
/** Structured, grounded summary of what's already done — used to keep
|
|
7994
|
+
* corrective replan passes from re-emitting completed sections. */
|
|
7995
|
+
summarizeCompletedSections(results) {
|
|
7996
|
+
const done = results.filter((r) => r.status === "COMPLETED" || r.status === "PARTIAL");
|
|
7997
|
+
if (done.length === 0) return "";
|
|
7998
|
+
const lines = done.map((r) => `- "${r.sectionTitle}" [${r.status}]: ${(r.sectionSummary || "(no summary)").slice(0, 300)}`);
|
|
7999
|
+
return `ALREADY COMPLETED SECTIONS (do not redo these \u2014 plan only what's still missing):
|
|
8000
|
+
${lines.join("\n")}`;
|
|
8001
|
+
}
|
|
7980
8002
|
async decomposeTask(prompt, systemContext) {
|
|
7981
8003
|
const db = this.router.getWorldStateDB?.();
|
|
7982
8004
|
let worldStateContext = "";
|
|
@@ -8244,7 +8266,7 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
8244
8266
|
}
|
|
8245
8267
|
}
|
|
8246
8268
|
if (readyIds.length === 0) return;
|
|
8247
|
-
|
|
8269
|
+
const runOne = async (id) => {
|
|
8248
8270
|
resultMap.set(id, null);
|
|
8249
8271
|
const index = sections.findIndex((s) => s.sectionId === id);
|
|
8250
8272
|
const section = sections[index];
|
|
@@ -8284,7 +8306,14 @@ Leave dependsOn empty for sections that can run immediately in parallel.`;
|
|
|
8284
8306
|
for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
|
|
8285
8307
|
inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
|
|
8286
8308
|
}
|
|
8287
|
-
}
|
|
8309
|
+
};
|
|
8310
|
+
if (this.router.getT3ExecutionMode?.() === "sequential") {
|
|
8311
|
+
for (const id of readyIds) {
|
|
8312
|
+
await runOne(id);
|
|
8313
|
+
}
|
|
8314
|
+
} else {
|
|
8315
|
+
await Promise.all(readyIds.map(runOne));
|
|
8316
|
+
}
|
|
8288
8317
|
if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
|
|
8289
8318
|
await executeWave();
|
|
8290
8319
|
}
|
|
@@ -10088,6 +10117,13 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
10088
10117
|
* All T3 workers under the same T2 share cached decisions for the same tool.
|
|
10089
10118
|
*/
|
|
10090
10119
|
sessionCache = /* @__PURE__ */ new Map();
|
|
10120
|
+
/**
|
|
10121
|
+
* Task-wide cache keyed by `toolName` alone, for USER- and T1-level
|
|
10122
|
+
* "always" decisions — these are meant to cover every sibling T2/T3 in the
|
|
10123
|
+
* run, not just the one that happened to ask first (see PermissionDecision
|
|
10124
|
+
* doc comment: "task-wide for T1").
|
|
10125
|
+
*/
|
|
10126
|
+
taskWideCache = /* @__PURE__ */ new Map();
|
|
10091
10127
|
t2Evaluator;
|
|
10092
10128
|
t1Evaluator;
|
|
10093
10129
|
/** Pending user-decision resolvers keyed by request ID */
|
|
@@ -10116,6 +10152,15 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
10116
10152
|
* Returns a PermissionDecision from whichever tier was able to decide.
|
|
10117
10153
|
*/
|
|
10118
10154
|
async requestPermission(req) {
|
|
10155
|
+
if (!req.forceReprompt && this.taskWideCache.has(req.toolName)) {
|
|
10156
|
+
return {
|
|
10157
|
+
requestId: req.id,
|
|
10158
|
+
approved: this.taskWideCache.get(req.toolName),
|
|
10159
|
+
always: true,
|
|
10160
|
+
decidedBy: "T1",
|
|
10161
|
+
reasoning: "Cached from a previous task-wide decision in this session"
|
|
10162
|
+
};
|
|
10163
|
+
}
|
|
10119
10164
|
const cacheKey = `${req.parentT2Id}:${req.toolName}`;
|
|
10120
10165
|
if (!req.forceReprompt && this.sessionCache.has(cacheKey)) {
|
|
10121
10166
|
return {
|
|
@@ -10160,7 +10205,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
10160
10205
|
try {
|
|
10161
10206
|
const t1Decision = await this.t1Evaluator(req);
|
|
10162
10207
|
if (t1Decision !== null) {
|
|
10163
|
-
if (t1Decision.always) this.
|
|
10208
|
+
if (t1Decision.always) this.taskWideCache.set(req.toolName, t1Decision.approved);
|
|
10164
10209
|
return t1Decision;
|
|
10165
10210
|
}
|
|
10166
10211
|
} catch {
|
|
@@ -10190,7 +10235,7 @@ var PermissionEscalator = class extends EventEmitter__default.default {
|
|
|
10190
10235
|
const wrappedResolver = (decision) => {
|
|
10191
10236
|
if (timer) clearTimeout(timer);
|
|
10192
10237
|
if (decision.always) {
|
|
10193
|
-
this.
|
|
10238
|
+
this.taskWideCache.set(req.toolName, decision.approved);
|
|
10194
10239
|
}
|
|
10195
10240
|
resolve(decision);
|
|
10196
10241
|
};
|
|
@@ -15269,11 +15314,18 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15269
15314
|
}
|
|
15270
15315
|
});
|
|
15271
15316
|
const currentEntry = state.entries[state.currentEntryIdx];
|
|
15317
|
+
const rejectIfBlank = React2.useCallback((val, message) => {
|
|
15318
|
+
if (val.trim()) return false;
|
|
15319
|
+
dispatch({ type: "SET_ERROR", error: message });
|
|
15320
|
+
return true;
|
|
15321
|
+
}, []);
|
|
15272
15322
|
const handleFieldSubmit = React2.useCallback((val) => {
|
|
15273
15323
|
if (!currentEntry) return;
|
|
15274
15324
|
if (currentEntry.type === "azure") {
|
|
15275
15325
|
if (fieldStage === "deploymentName") {
|
|
15326
|
+
if (rejectIfBlank(val, "Deployment name is required.")) return;
|
|
15276
15327
|
dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
|
|
15328
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15277
15329
|
setFieldBuffer("");
|
|
15278
15330
|
if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
|
|
15279
15331
|
setFieldStage("askMore");
|
|
@@ -15281,38 +15333,50 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15281
15333
|
setFieldStage("baseUrl");
|
|
15282
15334
|
}
|
|
15283
15335
|
} else if (fieldStage === "baseUrl") {
|
|
15336
|
+
if (rejectIfBlank(val, "Azure endpoint URL is required.")) return;
|
|
15284
15337
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
|
|
15338
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15285
15339
|
setFieldBuffer("");
|
|
15286
15340
|
setFieldStage("apiKey");
|
|
15287
15341
|
} else if (fieldStage === "apiKey") {
|
|
15342
|
+
if (rejectIfBlank(val, "API key is required.")) return;
|
|
15288
15343
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15344
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15289
15345
|
setFieldBuffer("");
|
|
15290
15346
|
setFieldStage("apiVersion");
|
|
15291
15347
|
} else if (fieldStage === "apiVersion") {
|
|
15292
15348
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
|
|
15349
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15293
15350
|
setFieldBuffer("");
|
|
15294
15351
|
setFieldStage("askMore");
|
|
15295
15352
|
}
|
|
15296
15353
|
} else if (currentEntry.type === "openai-compatible") {
|
|
15297
15354
|
if (fieldStage === "label") {
|
|
15298
15355
|
dispatch({ type: "SET_ENTRY_FIELD", field: "label", value: val || currentEntry.label });
|
|
15356
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15299
15357
|
setFieldBuffer("");
|
|
15300
15358
|
setFieldStage("baseUrl");
|
|
15301
15359
|
} else if (fieldStage === "baseUrl") {
|
|
15360
|
+
if (rejectIfBlank(val, "Base URL is required.")) return;
|
|
15302
15361
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
|
|
15362
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15303
15363
|
setFieldBuffer("");
|
|
15304
15364
|
setFieldStage("apiKey");
|
|
15305
15365
|
} else if (fieldStage === "apiKey") {
|
|
15306
15366
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15367
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15307
15368
|
setFieldBuffer("");
|
|
15308
15369
|
setFieldStage("askMore");
|
|
15309
15370
|
}
|
|
15310
15371
|
} else if (currentEntry.type === "ollama") {
|
|
15311
15372
|
dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
|
|
15373
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15312
15374
|
setFieldBuffer("");
|
|
15313
15375
|
setFieldStage("askMore");
|
|
15314
15376
|
} else {
|
|
15377
|
+
if (rejectIfBlank(val, "API key is required.")) return;
|
|
15315
15378
|
dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
|
|
15379
|
+
dispatch({ type: "SET_ERROR", error: "" });
|
|
15316
15380
|
setFieldBuffer("");
|
|
15317
15381
|
const nextEntry = state.entries[state.currentEntryIdx + 1];
|
|
15318
15382
|
if (nextEntry) {
|
|
@@ -15320,7 +15384,7 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15320
15384
|
}
|
|
15321
15385
|
dispatch({ type: "NEXT_ENTRY" });
|
|
15322
15386
|
}
|
|
15323
|
-
}, [currentEntry, fieldStage]);
|
|
15387
|
+
}, [currentEntry, fieldStage, rejectIfBlank, state.entries, state.currentEntryIdx]);
|
|
15324
15388
|
if (state.step === "PROVIDER_SELECT") {
|
|
15325
15389
|
return /* @__PURE__ */ jsxRuntime.jsxs(Frame, { theme, phase: "keys", children: [
|
|
15326
15390
|
/* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, children: [
|
|
@@ -16025,6 +16089,25 @@ var DashboardSocket = class {
|
|
|
16025
16089
|
});
|
|
16026
16090
|
});
|
|
16027
16091
|
}
|
|
16092
|
+
/**
|
|
16093
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
16094
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
16095
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
16096
|
+
*/
|
|
16097
|
+
onPlanDecision(callback) {
|
|
16098
|
+
this.io.on("connection", (socket) => {
|
|
16099
|
+
socket.on("plan:decision", (payload) => {
|
|
16100
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
16101
|
+
callback({
|
|
16102
|
+
sessionId: payload.sessionId,
|
|
16103
|
+
approved: payload.approved,
|
|
16104
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
16105
|
+
editedPlan: payload.editedPlan
|
|
16106
|
+
});
|
|
16107
|
+
}
|
|
16108
|
+
});
|
|
16109
|
+
});
|
|
16110
|
+
}
|
|
16028
16111
|
onSessionSteer(callback) {
|
|
16029
16112
|
this.io.on("connection", (socket) => {
|
|
16030
16113
|
socket.on("session:steer", (payload) => {
|
|
@@ -16069,6 +16152,122 @@ var DashboardSocket = class {
|
|
|
16069
16152
|
|
|
16070
16153
|
// src/dashboard/server.ts
|
|
16071
16154
|
init_constants();
|
|
16155
|
+
var TaskScheduler = class {
|
|
16156
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
16157
|
+
store;
|
|
16158
|
+
runner;
|
|
16159
|
+
constructor(store, runner) {
|
|
16160
|
+
this.store = store;
|
|
16161
|
+
this.runner = runner;
|
|
16162
|
+
}
|
|
16163
|
+
start() {
|
|
16164
|
+
const tasks = this.store.listScheduledTasks();
|
|
16165
|
+
for (const task of tasks) {
|
|
16166
|
+
if (task.enabled) this.schedule(task);
|
|
16167
|
+
}
|
|
16168
|
+
}
|
|
16169
|
+
stop() {
|
|
16170
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
16171
|
+
this.cronJobs.clear();
|
|
16172
|
+
}
|
|
16173
|
+
schedule(task) {
|
|
16174
|
+
if (!cron__default.default.validate(task.cronExpression)) {
|
|
16175
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
16176
|
+
}
|
|
16177
|
+
const existing = this.cronJobs.get(task.id);
|
|
16178
|
+
if (existing) {
|
|
16179
|
+
try {
|
|
16180
|
+
existing.stop();
|
|
16181
|
+
} catch {
|
|
16182
|
+
}
|
|
16183
|
+
}
|
|
16184
|
+
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
16185
|
+
try {
|
|
16186
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
16187
|
+
this.store.saveScheduledTask(task);
|
|
16188
|
+
await this.runner(task);
|
|
16189
|
+
} catch (err) {
|
|
16190
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
16191
|
+
}
|
|
16192
|
+
}, { timezone: "UTC" });
|
|
16193
|
+
this.cronJobs.set(task.id, job);
|
|
16194
|
+
}
|
|
16195
|
+
unschedule(taskId) {
|
|
16196
|
+
this.cronJobs.get(taskId)?.stop();
|
|
16197
|
+
this.cronJobs.delete(taskId);
|
|
16198
|
+
}
|
|
16199
|
+
add(task) {
|
|
16200
|
+
this.store.saveScheduledTask(task);
|
|
16201
|
+
if (task.enabled) this.schedule(task);
|
|
16202
|
+
}
|
|
16203
|
+
remove(taskId) {
|
|
16204
|
+
this.unschedule(taskId);
|
|
16205
|
+
this.store.deleteScheduledTask(taskId);
|
|
16206
|
+
}
|
|
16207
|
+
list() {
|
|
16208
|
+
return this.store.listScheduledTasks();
|
|
16209
|
+
}
|
|
16210
|
+
isRunning(taskId) {
|
|
16211
|
+
return this.cronJobs.has(taskId);
|
|
16212
|
+
}
|
|
16213
|
+
static validateCron(expression) {
|
|
16214
|
+
return cron__default.default.validate(expression);
|
|
16215
|
+
}
|
|
16216
|
+
};
|
|
16217
|
+
|
|
16218
|
+
// src/dashboard/cost-stats.ts
|
|
16219
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
16220
|
+
function utcDateKey(d) {
|
|
16221
|
+
return d.toISOString().slice(0, 10);
|
|
16222
|
+
}
|
|
16223
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
16224
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
16225
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
16226
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
16227
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
16228
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
16229
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
16230
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
16231
|
+
}
|
|
16232
|
+
let totalCostUsd = 0;
|
|
16233
|
+
let totalTokens = 0;
|
|
16234
|
+
let totalRuns = 0;
|
|
16235
|
+
for (const s of sessions) {
|
|
16236
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
16237
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
16238
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
16239
|
+
totalCostUsd += cost;
|
|
16240
|
+
totalTokens += tokens;
|
|
16241
|
+
totalRuns += runs;
|
|
16242
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
16243
|
+
if (!Number.isNaN(when.getTime())) {
|
|
16244
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
16245
|
+
if (bucket) {
|
|
16246
|
+
bucket.costUsd += cost;
|
|
16247
|
+
bucket.tokens += tokens;
|
|
16248
|
+
bucket.runs += runs;
|
|
16249
|
+
}
|
|
16250
|
+
}
|
|
16251
|
+
}
|
|
16252
|
+
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) => ({
|
|
16253
|
+
sessionId: s.id,
|
|
16254
|
+
title: s.title,
|
|
16255
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
16256
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
16257
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
16258
|
+
updatedAt: s.updatedAt
|
|
16259
|
+
}));
|
|
16260
|
+
return {
|
|
16261
|
+
totalCostUsd,
|
|
16262
|
+
totalTokens,
|
|
16263
|
+
totalSessions: sessions.length,
|
|
16264
|
+
totalRuns,
|
|
16265
|
+
perDay: [...buckets.values()],
|
|
16266
|
+
topSessions
|
|
16267
|
+
};
|
|
16268
|
+
}
|
|
16269
|
+
|
|
16270
|
+
// src/dashboard/server.ts
|
|
16072
16271
|
var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
16073
16272
|
var DashboardServer = class {
|
|
16074
16273
|
app;
|
|
@@ -16094,6 +16293,14 @@ var DashboardServer = class {
|
|
|
16094
16293
|
* map is how that answer reaches the run that's blocked on it.
|
|
16095
16294
|
*/
|
|
16096
16295
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
16296
|
+
/**
|
|
16297
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
16298
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
16299
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
16300
|
+
*/
|
|
16301
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
16302
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
16303
|
+
scheduler;
|
|
16097
16304
|
port;
|
|
16098
16305
|
host;
|
|
16099
16306
|
workspacePath;
|
|
@@ -16111,6 +16318,7 @@ var DashboardServer = class {
|
|
|
16111
16318
|
secret: this.dashboardSecret,
|
|
16112
16319
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
16113
16320
|
});
|
|
16321
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
16114
16322
|
this.setupMiddleware();
|
|
16115
16323
|
this.setupRoutes();
|
|
16116
16324
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -16174,6 +16382,9 @@ var DashboardServer = class {
|
|
|
16174
16382
|
cascade.on("peer:message", (e) => {
|
|
16175
16383
|
this.socket.emitPeerMessage(e);
|
|
16176
16384
|
});
|
|
16385
|
+
cascade.on("plan:approval-required", (e) => {
|
|
16386
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
16387
|
+
});
|
|
16177
16388
|
try {
|
|
16178
16389
|
const result = await cascade.run({
|
|
16179
16390
|
prompt: runPrompt,
|
|
@@ -16181,7 +16392,8 @@ var DashboardServer = class {
|
|
|
16181
16392
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16182
16393
|
});
|
|
16183
16394
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16184
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16395
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16396
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16185
16397
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
16186
16398
|
this.socket.broadcast("cost:update", {
|
|
16187
16399
|
sessionId,
|
|
@@ -16191,6 +16403,7 @@ var DashboardServer = class {
|
|
|
16191
16403
|
this.throttledBroadcast("workspace");
|
|
16192
16404
|
} catch (err) {
|
|
16193
16405
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16406
|
+
this.captureWhy(sessionId, cascade);
|
|
16194
16407
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
16195
16408
|
sessionId,
|
|
16196
16409
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16201,9 +16414,17 @@ var DashboardServer = class {
|
|
|
16201
16414
|
this.denyPendingApprovals(sessionId);
|
|
16202
16415
|
}
|
|
16203
16416
|
});
|
|
16417
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
16418
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
16419
|
+
approved,
|
|
16420
|
+
note,
|
|
16421
|
+
editedPlan
|
|
16422
|
+
);
|
|
16423
|
+
});
|
|
16204
16424
|
this.socket.onSessionHalt((sessionId) => {
|
|
16205
16425
|
this.activeControllers.get(sessionId)?.abort();
|
|
16206
16426
|
this.denyPendingApprovals(sessionId);
|
|
16427
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
16207
16428
|
});
|
|
16208
16429
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16209
16430
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -16236,12 +16457,21 @@ var DashboardServer = class {
|
|
|
16236
16457
|
resolve();
|
|
16237
16458
|
});
|
|
16238
16459
|
});
|
|
16460
|
+
try {
|
|
16461
|
+
this.scheduler.start();
|
|
16462
|
+
} catch (err) {
|
|
16463
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
16464
|
+
}
|
|
16239
16465
|
}
|
|
16240
16466
|
async stop() {
|
|
16241
16467
|
if (this.broadcastTimer) {
|
|
16242
16468
|
clearTimeout(this.broadcastTimer);
|
|
16243
16469
|
this.broadcastTimer = null;
|
|
16244
16470
|
}
|
|
16471
|
+
try {
|
|
16472
|
+
this.scheduler.stop();
|
|
16473
|
+
} catch {
|
|
16474
|
+
}
|
|
16245
16475
|
this.socket.close();
|
|
16246
16476
|
try {
|
|
16247
16477
|
this.globalStore?.close();
|
|
@@ -16255,6 +16485,16 @@ var DashboardServer = class {
|
|
|
16255
16485
|
getSocket() {
|
|
16256
16486
|
return this.socket;
|
|
16257
16487
|
}
|
|
16488
|
+
/**
|
|
16489
|
+
* Rebind the workspace tasks execute in — e.g. the desktop app's Code view
|
|
16490
|
+
* opening a different project folder — without tearing down the socket
|
|
16491
|
+
* server (which would drop the port/auth token/connection mid-session).
|
|
16492
|
+
* The next `cascade:run` picks this up immediately since `this.workspacePath`
|
|
16493
|
+
* is read live per-run (see onCascadeRun below).
|
|
16494
|
+
*/
|
|
16495
|
+
setWorkspacePath(workspacePath) {
|
|
16496
|
+
this.workspacePath = workspacePath;
|
|
16497
|
+
}
|
|
16258
16498
|
/**
|
|
16259
16499
|
* Write the in-memory config back to the workspace config file so mutations
|
|
16260
16500
|
* made over the socket (Settings → Save) persist across restarts. Best-effort:
|
|
@@ -16352,16 +16592,60 @@ var DashboardServer = class {
|
|
|
16352
16592
|
return title;
|
|
16353
16593
|
}
|
|
16354
16594
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
16355
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
16595
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
16356
16596
|
try {
|
|
16357
16597
|
if (reply && reply.trim()) {
|
|
16358
16598
|
this.store.addMessage({ id: crypto.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16359
16599
|
}
|
|
16600
|
+
if (result) {
|
|
16601
|
+
const session = this.store.getSession(sessionId);
|
|
16602
|
+
if (session) {
|
|
16603
|
+
this.store.updateSession(sessionId, {
|
|
16604
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16605
|
+
metadata: {
|
|
16606
|
+
...session.metadata,
|
|
16607
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
16608
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
16609
|
+
taskCount: session.metadata.taskCount + 1
|
|
16610
|
+
}
|
|
16611
|
+
});
|
|
16612
|
+
}
|
|
16613
|
+
}
|
|
16360
16614
|
} catch (err) {
|
|
16361
16615
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
16362
16616
|
}
|
|
16363
16617
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
16364
16618
|
}
|
|
16619
|
+
/**
|
|
16620
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
16621
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
16622
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
16623
|
+
*/
|
|
16624
|
+
captureWhy(sessionId, cascade, result) {
|
|
16625
|
+
try {
|
|
16626
|
+
const stats = cascade.getRouter().getStats();
|
|
16627
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
16628
|
+
const report = {
|
|
16629
|
+
sessionId,
|
|
16630
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16631
|
+
decisions: cascade.getDecisionLog(),
|
|
16632
|
+
savedUsd: savings.savedUsd,
|
|
16633
|
+
savedPct: savings.savedPct,
|
|
16634
|
+
totalCostUsd: stats.totalCostUsd,
|
|
16635
|
+
totalTokens: stats.totalTokens,
|
|
16636
|
+
costByTier: stats.costByTier,
|
|
16637
|
+
durationMs: result?.durationMs
|
|
16638
|
+
};
|
|
16639
|
+
this.whyBySession.set(sessionId, report);
|
|
16640
|
+
if (this.whyBySession.size > 50) {
|
|
16641
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
16642
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
16643
|
+
}
|
|
16644
|
+
this.socket.broadcast("run:why", report);
|
|
16645
|
+
} catch (err) {
|
|
16646
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16365
16649
|
/**
|
|
16366
16650
|
* Route steering text into running Cascade instances. Targets the given
|
|
16367
16651
|
* session, or every active session when none is specified (the desktop
|
|
@@ -16391,6 +16675,52 @@ var DashboardServer = class {
|
|
|
16391
16675
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16392
16676
|
});
|
|
16393
16677
|
}
|
|
16678
|
+
/**
|
|
16679
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
16680
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
16681
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
16682
|
+
* client so an open desktop sees the run appear live.
|
|
16683
|
+
*/
|
|
16684
|
+
async runScheduledTask(task) {
|
|
16685
|
+
const sessionId = crypto.randomUUID();
|
|
16686
|
+
const prompt = task.prompt;
|
|
16687
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
16688
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
16689
|
+
this.activeSessions.set(sessionId, cascade);
|
|
16690
|
+
cascade.on("tier:status", (e) => {
|
|
16691
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
16692
|
+
});
|
|
16693
|
+
cascade.on("peer:message", (e) => {
|
|
16694
|
+
this.socket.emitPeerMessage(e);
|
|
16695
|
+
});
|
|
16696
|
+
try {
|
|
16697
|
+
const result = await cascade.run({
|
|
16698
|
+
prompt,
|
|
16699
|
+
identityId: task.identityId,
|
|
16700
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
16701
|
+
});
|
|
16702
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
16703
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16704
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16705
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
16706
|
+
this.socket.broadcast("cost:update", {
|
|
16707
|
+
sessionId,
|
|
16708
|
+
totalTokens: result.usage.totalTokens,
|
|
16709
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
16710
|
+
});
|
|
16711
|
+
this.throttledBroadcast("workspace");
|
|
16712
|
+
} catch (err) {
|
|
16713
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16714
|
+
this.captureWhy(sessionId, cascade);
|
|
16715
|
+
this.socket.broadcast("session:error", {
|
|
16716
|
+
sessionId,
|
|
16717
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16718
|
+
});
|
|
16719
|
+
throw err;
|
|
16720
|
+
} finally {
|
|
16721
|
+
this.activeSessions.delete(sessionId);
|
|
16722
|
+
}
|
|
16723
|
+
}
|
|
16394
16724
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16395
16725
|
denyPendingApprovals(sessionId) {
|
|
16396
16726
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -16903,6 +17233,9 @@ ${prompt}`;
|
|
|
16903
17233
|
cascade.on("peer:message", (e) => {
|
|
16904
17234
|
this.socket.emitPeerMessage(e);
|
|
16905
17235
|
});
|
|
17236
|
+
cascade.on("plan:approval-required", (e) => {
|
|
17237
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
17238
|
+
});
|
|
16906
17239
|
try {
|
|
16907
17240
|
const result = await cascade.run({
|
|
16908
17241
|
prompt: runPrompt,
|
|
@@ -16910,7 +17243,8 @@ ${prompt}`;
|
|
|
16910
17243
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16911
17244
|
});
|
|
16912
17245
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16913
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
17246
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17247
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16914
17248
|
this.socket.broadcast("cost:update", {
|
|
16915
17249
|
sessionId,
|
|
16916
17250
|
totalTokens: result.usage.totalTokens,
|
|
@@ -16920,6 +17254,7 @@ ${prompt}`;
|
|
|
16920
17254
|
this.throttledBroadcast("workspace");
|
|
16921
17255
|
} catch (err) {
|
|
16922
17256
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17257
|
+
this.captureWhy(sessionId, cascade);
|
|
16923
17258
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
16924
17259
|
sessionId,
|
|
16925
17260
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16941,6 +17276,168 @@ ${prompt}`;
|
|
|
16941
17276
|
}))
|
|
16942
17277
|
});
|
|
16943
17278
|
});
|
|
17279
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
17280
|
+
const report = this.whyBySession.get(req.params.id);
|
|
17281
|
+
if (!report) {
|
|
17282
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
17283
|
+
return;
|
|
17284
|
+
}
|
|
17285
|
+
res.json(report);
|
|
17286
|
+
});
|
|
17287
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
17288
|
+
const sessionId = req.params.id;
|
|
17289
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17290
|
+
const before = /* @__PURE__ */ new Map();
|
|
17291
|
+
for (const taskId of taskIds) {
|
|
17292
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
17293
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
17294
|
+
}
|
|
17295
|
+
}
|
|
17296
|
+
const { readFile } = await import('fs/promises');
|
|
17297
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
17298
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
17299
|
+
let after = "";
|
|
17300
|
+
let missing = false;
|
|
17301
|
+
try {
|
|
17302
|
+
const buf = await readFile(filePath);
|
|
17303
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
17304
|
+
} catch {
|
|
17305
|
+
missing = true;
|
|
17306
|
+
}
|
|
17307
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
17308
|
+
}));
|
|
17309
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
17310
|
+
});
|
|
17311
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
17312
|
+
const sessionId = req.params.id;
|
|
17313
|
+
const body = req.body;
|
|
17314
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
17315
|
+
res.status(400).json({ error: "filePath is required" });
|
|
17316
|
+
return;
|
|
17317
|
+
}
|
|
17318
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17319
|
+
let content;
|
|
17320
|
+
for (const taskId of taskIds) {
|
|
17321
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
17322
|
+
if (snap) {
|
|
17323
|
+
content = snap.content;
|
|
17324
|
+
break;
|
|
17325
|
+
}
|
|
17326
|
+
}
|
|
17327
|
+
if (content === void 0) {
|
|
17328
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
17329
|
+
return;
|
|
17330
|
+
}
|
|
17331
|
+
try {
|
|
17332
|
+
const { writeFile } = await import('fs/promises');
|
|
17333
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
17334
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
17335
|
+
} catch (err) {
|
|
17336
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17337
|
+
}
|
|
17338
|
+
});
|
|
17339
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
17340
|
+
try {
|
|
17341
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
17342
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
17343
|
+
res.json({
|
|
17344
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
17345
|
+
budget: {
|
|
17346
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
17347
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
17348
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
17349
|
+
}
|
|
17350
|
+
});
|
|
17351
|
+
} catch (err) {
|
|
17352
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17353
|
+
}
|
|
17354
|
+
});
|
|
17355
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
17356
|
+
try {
|
|
17357
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
17358
|
+
} catch (err) {
|
|
17359
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17360
|
+
}
|
|
17361
|
+
});
|
|
17362
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
17363
|
+
const body = req.body;
|
|
17364
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
17365
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
17366
|
+
return;
|
|
17367
|
+
}
|
|
17368
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
17369
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17370
|
+
return;
|
|
17371
|
+
}
|
|
17372
|
+
const task = {
|
|
17373
|
+
id: crypto.randomUUID(),
|
|
17374
|
+
name: body.name.trim(),
|
|
17375
|
+
cronExpression: body.cronExpression.trim(),
|
|
17376
|
+
prompt: body.prompt.trim(),
|
|
17377
|
+
workspacePath: this.workspacePath,
|
|
17378
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17379
|
+
enabled: body.enabled !== false
|
|
17380
|
+
};
|
|
17381
|
+
try {
|
|
17382
|
+
this.scheduler.add(task);
|
|
17383
|
+
res.json(task);
|
|
17384
|
+
} catch (err) {
|
|
17385
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17386
|
+
}
|
|
17387
|
+
});
|
|
17388
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17389
|
+
const id = req.params.id;
|
|
17390
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
17391
|
+
if (!existing) {
|
|
17392
|
+
res.status(404).json({ error: "Not found" });
|
|
17393
|
+
return;
|
|
17394
|
+
}
|
|
17395
|
+
const body = req.body;
|
|
17396
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
17397
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17398
|
+
return;
|
|
17399
|
+
}
|
|
17400
|
+
const updated = {
|
|
17401
|
+
...existing,
|
|
17402
|
+
name: body.name?.trim() || existing.name,
|
|
17403
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
17404
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
17405
|
+
enabled: body.enabled ?? existing.enabled
|
|
17406
|
+
};
|
|
17407
|
+
try {
|
|
17408
|
+
this.scheduler.add(updated);
|
|
17409
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
17410
|
+
res.json(updated);
|
|
17411
|
+
} catch (err) {
|
|
17412
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17413
|
+
}
|
|
17414
|
+
});
|
|
17415
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17416
|
+
try {
|
|
17417
|
+
this.scheduler.remove(req.params.id);
|
|
17418
|
+
res.json({ ok: true });
|
|
17419
|
+
} catch (err) {
|
|
17420
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17421
|
+
}
|
|
17422
|
+
});
|
|
17423
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
17424
|
+
try {
|
|
17425
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
17426
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
17427
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
17428
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
17429
|
+
try {
|
|
17430
|
+
const all = logger.getAllLogs();
|
|
17431
|
+
const total = all.length;
|
|
17432
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
17433
|
+
res.json({ total, offset, entries });
|
|
17434
|
+
} finally {
|
|
17435
|
+
logger.close();
|
|
17436
|
+
}
|
|
17437
|
+
} catch (err) {
|
|
17438
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17439
|
+
}
|
|
17440
|
+
});
|
|
16944
17441
|
const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
|
|
16945
17442
|
const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
|
|
16946
17443
|
const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
@@ -17558,7 +18055,7 @@ async function startRepl(options) {
|
|
|
17558
18055
|
process.exit(1);
|
|
17559
18056
|
}
|
|
17560
18057
|
let config = cm.getConfig();
|
|
17561
|
-
const needsSetup = !config.providers
|
|
18058
|
+
const needsSetup = !hasUsableProvider(config.providers);
|
|
17562
18059
|
if (needsSetup) {
|
|
17563
18060
|
console.log(chalk9__default.default.magenta(" \u25C8 No providers configured \u2014 launching setup wizard\u2026"));
|
|
17564
18061
|
console.log();
|
|
@@ -17599,7 +18096,7 @@ async function runHeadless(prompt, options) {
|
|
|
17599
18096
|
process.exit(1);
|
|
17600
18097
|
}
|
|
17601
18098
|
const config = cm.getConfig();
|
|
17602
|
-
const needsSetup = !config.providers
|
|
18099
|
+
const needsSetup = !hasUsableProvider(config.providers);
|
|
17603
18100
|
if (needsSetup) {
|
|
17604
18101
|
console.error(chalk9__default.default.red("No providers configured. Run `cascade init` first."));
|
|
17605
18102
|
process.exit(1);
|