cascade-ai 0.16.0 → 0.18.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 +746 -188
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +742 -185
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1316 -824
- package/dist/index.cjs +669 -177
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +45 -2
- package/dist/index.d.ts +45 -2
- package/dist/index.js +667 -176
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import path21 from 'path';
|
|
3
|
+
import fs20 from 'fs';
|
|
4
4
|
import os6 from 'os';
|
|
5
5
|
import crypto3, { randomUUID, timingSafeEqual } from 'crypto';
|
|
6
6
|
import EventEmitter from 'events';
|
|
@@ -58,12 +58,12 @@ var init_audit_logger = __esm({
|
|
|
58
58
|
constructor(workspacePath, debugMode = false) {
|
|
59
59
|
this.workspacePath = workspacePath;
|
|
60
60
|
this.debugMode = debugMode;
|
|
61
|
-
const cascadeDir =
|
|
62
|
-
if (!
|
|
63
|
-
|
|
61
|
+
const cascadeDir = path21.join(workspacePath, ".cascade");
|
|
62
|
+
if (!fs20.existsSync(cascadeDir)) {
|
|
63
|
+
fs20.mkdirSync(cascadeDir, { recursive: true });
|
|
64
64
|
}
|
|
65
|
-
this.keyPath =
|
|
66
|
-
this.dbPath =
|
|
65
|
+
this.keyPath = path21.join(cascadeDir, "audit_log.key");
|
|
66
|
+
this.dbPath = path21.join(cascadeDir, "audit_log.db");
|
|
67
67
|
this.initEncryptionKey();
|
|
68
68
|
this.db = new Database(this.dbPath);
|
|
69
69
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -93,11 +93,11 @@ var init_audit_logger = __esm({
|
|
|
93
93
|
return crypto3.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
94
94
|
}
|
|
95
95
|
initEncryptionKey() {
|
|
96
|
-
if (
|
|
97
|
-
this.encryptionKey =
|
|
96
|
+
if (fs20.existsSync(this.keyPath)) {
|
|
97
|
+
this.encryptionKey = fs20.readFileSync(this.keyPath);
|
|
98
98
|
} else {
|
|
99
99
|
this.encryptionKey = crypto3.randomBytes(32);
|
|
100
|
-
|
|
100
|
+
fs20.writeFileSync(this.keyPath, this.encryptionKey);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
encrypt(text) {
|
|
@@ -175,9 +175,9 @@ var init_audit_logger = __esm({
|
|
|
175
175
|
dumpDebugIfNeeded() {
|
|
176
176
|
if (!this.debugMode) return;
|
|
177
177
|
try {
|
|
178
|
-
const dumpPath =
|
|
178
|
+
const dumpPath = path21.join(os6.tmpdir(), "cascade_audit_logs_debug.json");
|
|
179
179
|
const entries = this.getAllLogs();
|
|
180
|
-
|
|
180
|
+
fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
181
181
|
} catch (err) {
|
|
182
182
|
console.error("Failed to dump debug audit logs", err);
|
|
183
183
|
}
|
|
@@ -190,7 +190,7 @@ var init_audit_logger = __esm({
|
|
|
190
190
|
});
|
|
191
191
|
|
|
192
192
|
// src/constants.ts
|
|
193
|
-
var CASCADE_VERSION = "0.
|
|
193
|
+
var CASCADE_VERSION = "0.18.0";
|
|
194
194
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
195
195
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
196
196
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -202,6 +202,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
|
202
202
|
var GLOBAL_CONFIG_DIR = ".cascade-ai";
|
|
203
203
|
var GLOBAL_DB_FILE = "memory.db";
|
|
204
204
|
var GLOBAL_KEYSTORE_FILE = "keystore.enc";
|
|
205
|
+
var GLOBAL_CREDENTIALS_FILE = "credentials.json";
|
|
205
206
|
var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
|
|
206
207
|
var DEFAULT_DASHBOARD_PORT = 4891;
|
|
207
208
|
var DEFAULT_API_PORT = 4892;
|
|
@@ -2164,7 +2165,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
2164
2165
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
2165
2166
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
2166
2167
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
2167
|
-
var DEFAULT_CACHE_FILE =
|
|
2168
|
+
var DEFAULT_CACHE_FILE = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
2168
2169
|
function normalizeModelId(id) {
|
|
2169
2170
|
let s = id.toLowerCase();
|
|
2170
2171
|
const slash = s.lastIndexOf("/");
|
|
@@ -2309,7 +2310,7 @@ var LiveDataProvider = class {
|
|
|
2309
2310
|
}
|
|
2310
2311
|
async saveCache() {
|
|
2311
2312
|
try {
|
|
2312
|
-
await fs4.mkdir(
|
|
2313
|
+
await fs4.mkdir(path21.dirname(this.opts.cacheFile), { recursive: true });
|
|
2313
2314
|
const cache = {
|
|
2314
2315
|
fetchedAt: this.fetchedAt,
|
|
2315
2316
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -4212,8 +4213,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
4212
4213
|
tierId: this.id,
|
|
4213
4214
|
sessionId: this.taskId,
|
|
4214
4215
|
requireApproval: false,
|
|
4215
|
-
saveSnapshot: async (
|
|
4216
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4216
|
+
saveSnapshot: async (path22, content) => {
|
|
4217
|
+
this.store?.addFileSnapshot(this.taskId, path22, content);
|
|
4217
4218
|
},
|
|
4218
4219
|
sendPeerSync: (to, syncType, content) => {
|
|
4219
4220
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -4383,7 +4384,7 @@ ${assignment.expectedOutput}`;
|
|
|
4383
4384
|
const { promisify: promisify4 } = await import('util');
|
|
4384
4385
|
const execAsync2 = promisify4(exec2);
|
|
4385
4386
|
for (const artifactPath of artifactPaths) {
|
|
4386
|
-
const absolutePath =
|
|
4387
|
+
const absolutePath = path21.resolve(process.cwd(), artifactPath);
|
|
4387
4388
|
try {
|
|
4388
4389
|
const stat = await fs4.stat(absolutePath);
|
|
4389
4390
|
if (!stat.isFile()) {
|
|
@@ -4404,7 +4405,7 @@ ${assignment.expectedOutput}`;
|
|
|
4404
4405
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
4405
4406
|
continue;
|
|
4406
4407
|
}
|
|
4407
|
-
const ext =
|
|
4408
|
+
const ext = path21.extname(absolutePath).toLowerCase();
|
|
4408
4409
|
try {
|
|
4409
4410
|
if (ext === ".ts" || ext === ".tsx") {
|
|
4410
4411
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -6486,16 +6487,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
6486
6487
|
if (typeof input !== "string" || input.length === 0) {
|
|
6487
6488
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
6488
6489
|
}
|
|
6489
|
-
const root =
|
|
6490
|
-
const abs =
|
|
6491
|
-
const rel =
|
|
6492
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6490
|
+
const root = path21.resolve(workspaceRoot);
|
|
6491
|
+
const abs = path21.isAbsolute(input) ? path21.resolve(input) : path21.resolve(root, input);
|
|
6492
|
+
const rel = path21.relative(root, abs);
|
|
6493
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21.isAbsolute(rel)) {
|
|
6493
6494
|
throw new WorkspaceSandboxError(input, root);
|
|
6494
6495
|
}
|
|
6495
6496
|
try {
|
|
6496
|
-
const real =
|
|
6497
|
-
const realRel =
|
|
6498
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6497
|
+
const real = fs20.realpathSync(abs);
|
|
6498
|
+
const realRel = path21.relative(root, real);
|
|
6499
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21.isAbsolute(realRel))) {
|
|
6499
6500
|
throw new WorkspaceSandboxError(input, root);
|
|
6500
6501
|
}
|
|
6501
6502
|
} catch (e) {
|
|
@@ -6556,7 +6557,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6556
6557
|
} catch {
|
|
6557
6558
|
}
|
|
6558
6559
|
}
|
|
6559
|
-
await fs4.mkdir(
|
|
6560
|
+
await fs4.mkdir(path21.dirname(absPath), { recursive: true });
|
|
6560
6561
|
await fs4.writeFile(absPath, content, "utf-8");
|
|
6561
6562
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6562
6563
|
}
|
|
@@ -7054,7 +7055,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
7054
7055
|
};
|
|
7055
7056
|
async function fileToImageAttachment(filePath) {
|
|
7056
7057
|
const data = await fs4.readFile(filePath);
|
|
7057
|
-
const ext =
|
|
7058
|
+
const ext = path21.extname(filePath).toLowerCase();
|
|
7058
7059
|
const mimeMap = {
|
|
7059
7060
|
".jpg": "image/jpeg",
|
|
7060
7061
|
".jpeg": "image/jpeg",
|
|
@@ -7088,14 +7089,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
7088
7089
|
const filePath = input["path"];
|
|
7089
7090
|
const content = input["content"];
|
|
7090
7091
|
const title = input["title"];
|
|
7091
|
-
const dir =
|
|
7092
|
-
if (!
|
|
7093
|
-
|
|
7092
|
+
const dir = path21.dirname(filePath);
|
|
7093
|
+
if (!fs20.existsSync(dir)) {
|
|
7094
|
+
fs20.mkdirSync(dir, { recursive: true });
|
|
7094
7095
|
}
|
|
7095
7096
|
return new Promise((resolve, reject) => {
|
|
7096
7097
|
try {
|
|
7097
7098
|
const doc = new PDFDocument({ margin: 50 });
|
|
7098
|
-
const stream =
|
|
7099
|
+
const stream = fs20.createWriteStream(filePath);
|
|
7099
7100
|
doc.pipe(stream);
|
|
7100
7101
|
if (title) {
|
|
7101
7102
|
doc.info["Title"] = title;
|
|
@@ -7173,22 +7174,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
7173
7174
|
}
|
|
7174
7175
|
cmdPrefix = NODE_CMD;
|
|
7175
7176
|
}
|
|
7176
|
-
const tmpDir =
|
|
7177
|
-
if (!
|
|
7178
|
-
|
|
7177
|
+
const tmpDir = path21.join(this.workspaceRoot, ".cascade", "tmp");
|
|
7178
|
+
if (!fs20.existsSync(tmpDir)) {
|
|
7179
|
+
fs20.mkdirSync(tmpDir, { recursive: true });
|
|
7179
7180
|
}
|
|
7180
7181
|
const extension = language === "python" ? "py" : "js";
|
|
7181
7182
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
7182
|
-
const filePath =
|
|
7183
|
-
|
|
7183
|
+
const filePath = path21.join(tmpDir, fileName);
|
|
7184
|
+
fs20.writeFileSync(filePath, code, "utf-8");
|
|
7184
7185
|
const execArgs = [filePath, ...args];
|
|
7185
7186
|
return new Promise((resolve) => {
|
|
7186
7187
|
const startMs = Date.now();
|
|
7187
7188
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
7188
7189
|
const duration = Date.now() - startMs;
|
|
7189
7190
|
try {
|
|
7190
|
-
if (
|
|
7191
|
-
|
|
7191
|
+
if (fs20.existsSync(filePath)) {
|
|
7192
|
+
fs20.unlinkSync(filePath);
|
|
7192
7193
|
}
|
|
7193
7194
|
} catch (cleanupErr) {
|
|
7194
7195
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -7467,7 +7468,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7467
7468
|
};
|
|
7468
7469
|
async execute(input, _options) {
|
|
7469
7470
|
const pattern = input["pattern"];
|
|
7470
|
-
const searchPath = input["path"] ?
|
|
7471
|
+
const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7471
7472
|
const matches = await glob(pattern, {
|
|
7472
7473
|
cwd: searchPath,
|
|
7473
7474
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -7480,7 +7481,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7480
7481
|
const withMtime = await Promise.all(
|
|
7481
7482
|
matches.map(async (rel) => {
|
|
7482
7483
|
try {
|
|
7483
|
-
const stat = await fs4.stat(
|
|
7484
|
+
const stat = await fs4.stat(path21.join(searchPath, rel));
|
|
7484
7485
|
return { rel, mtime: stat.mtimeMs };
|
|
7485
7486
|
} catch {
|
|
7486
7487
|
return { rel, mtime: 0 };
|
|
@@ -7529,7 +7530,7 @@ var GrepTool = class extends BaseTool {
|
|
|
7529
7530
|
};
|
|
7530
7531
|
async execute(input, _options) {
|
|
7531
7532
|
const pattern = input["pattern"];
|
|
7532
|
-
const searchPath = input["path"] ?
|
|
7533
|
+
const searchPath = input["path"] ? path21.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7533
7534
|
const globPattern = input["glob"];
|
|
7534
7535
|
const outputMode = input["output_mode"] ?? "content";
|
|
7535
7536
|
const context = input["context"] ?? 0;
|
|
@@ -7583,12 +7584,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7583
7584
|
nodir: true
|
|
7584
7585
|
});
|
|
7585
7586
|
} catch {
|
|
7586
|
-
files = [
|
|
7587
|
+
files = [path21.relative(searchPath, searchPath) || "."];
|
|
7587
7588
|
}
|
|
7588
7589
|
const results = [];
|
|
7589
7590
|
let totalCount = 0;
|
|
7590
7591
|
for (const rel of files) {
|
|
7591
|
-
const abs =
|
|
7592
|
+
const abs = path21.join(searchPath, rel);
|
|
7592
7593
|
let content;
|
|
7593
7594
|
try {
|
|
7594
7595
|
content = await fs4.readFile(abs, "utf-8");
|
|
@@ -7950,10 +7951,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
7950
7951
|
}
|
|
7951
7952
|
isIgnored(filePath) {
|
|
7952
7953
|
if (!filePath) return false;
|
|
7953
|
-
const abs =
|
|
7954
|
-
const rel =
|
|
7955
|
-
if (!rel || rel.startsWith("..") ||
|
|
7956
|
-
const posixRel = rel.split(
|
|
7954
|
+
const abs = path21.resolve(this.workspaceRoot, filePath);
|
|
7955
|
+
const rel = path21.relative(this.workspaceRoot, abs);
|
|
7956
|
+
if (!rel || rel.startsWith("..") || path21.isAbsolute(rel)) return true;
|
|
7957
|
+
const posixRel = rel.split(path21.sep).join("/");
|
|
7957
7958
|
return this.ignoreMatcher.ignores(posixRel);
|
|
7958
7959
|
}
|
|
7959
7960
|
};
|
|
@@ -8787,7 +8788,7 @@ var TaskAnalyzer = class {
|
|
|
8787
8788
|
analysisCache.clear();
|
|
8788
8789
|
}
|
|
8789
8790
|
};
|
|
8790
|
-
var DEFAULT_STATS_FILE =
|
|
8791
|
+
var DEFAULT_STATS_FILE = path21.join(os6.homedir(), ".cascade", "model-perf.json");
|
|
8791
8792
|
var ModelPerformanceTracker = class {
|
|
8792
8793
|
stats = /* @__PURE__ */ new Map();
|
|
8793
8794
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -8819,7 +8820,7 @@ var ModelPerformanceTracker = class {
|
|
|
8819
8820
|
}
|
|
8820
8821
|
async save() {
|
|
8821
8822
|
try {
|
|
8822
|
-
await fs4.mkdir(
|
|
8823
|
+
await fs4.mkdir(path21.dirname(this.statsFile), { recursive: true });
|
|
8823
8824
|
const modelsObj = {};
|
|
8824
8825
|
const featuresObj = {};
|
|
8825
8826
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -9358,7 +9359,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9358
9359
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
9359
9360
|
async loadPersistedTools() {
|
|
9360
9361
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9361
|
-
const file =
|
|
9362
|
+
const file = path21.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
9362
9363
|
try {
|
|
9363
9364
|
const raw = await fs4.readFile(file, "utf-8");
|
|
9364
9365
|
const specs = JSON.parse(raw);
|
|
@@ -9382,8 +9383,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9382
9383
|
}
|
|
9383
9384
|
async persist() {
|
|
9384
9385
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9385
|
-
const dir =
|
|
9386
|
-
const file =
|
|
9386
|
+
const dir = path21.join(this.workspacePath, ".cascade");
|
|
9387
|
+
const file = path21.join(dir, DYNAMIC_TOOLS_FILE);
|
|
9387
9388
|
try {
|
|
9388
9389
|
await fs4.mkdir(dir, { recursive: true });
|
|
9389
9390
|
await fs4.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -9403,12 +9404,12 @@ var WorldStateDB = class {
|
|
|
9403
9404
|
constructor(workspacePath, debugMode = false) {
|
|
9404
9405
|
this.workspacePath = workspacePath;
|
|
9405
9406
|
this.debugMode = debugMode;
|
|
9406
|
-
const cascadeDir =
|
|
9407
|
-
if (!
|
|
9408
|
-
|
|
9407
|
+
const cascadeDir = path21.join(workspacePath, ".cascade");
|
|
9408
|
+
if (!fs20.existsSync(cascadeDir)) {
|
|
9409
|
+
fs20.mkdirSync(cascadeDir, { recursive: true });
|
|
9409
9410
|
}
|
|
9410
|
-
this.keyPath =
|
|
9411
|
-
this.dbPath =
|
|
9411
|
+
this.keyPath = path21.join(cascadeDir, "world_state.key");
|
|
9412
|
+
this.dbPath = path21.join(cascadeDir, "world_state.db");
|
|
9412
9413
|
this.initEncryptionKey();
|
|
9413
9414
|
this.db = new Database(this.dbPath);
|
|
9414
9415
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -9438,11 +9439,11 @@ var WorldStateDB = class {
|
|
|
9438
9439
|
dbPath;
|
|
9439
9440
|
encryptionKey;
|
|
9440
9441
|
initEncryptionKey() {
|
|
9441
|
-
if (
|
|
9442
|
-
this.encryptionKey =
|
|
9442
|
+
if (fs20.existsSync(this.keyPath)) {
|
|
9443
|
+
this.encryptionKey = fs20.readFileSync(this.keyPath);
|
|
9443
9444
|
} else {
|
|
9444
9445
|
this.encryptionKey = crypto3.randomBytes(32);
|
|
9445
|
-
|
|
9446
|
+
fs20.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9446
9447
|
}
|
|
9447
9448
|
}
|
|
9448
9449
|
encrypt(text) {
|
|
@@ -9583,6 +9584,22 @@ var WorldStateDB = class {
|
|
|
9583
9584
|
}
|
|
9584
9585
|
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
9585
9586
|
}
|
|
9587
|
+
/**
|
|
9588
|
+
* Delete one fact by its (normalized) entity + relation key. Returns whether
|
|
9589
|
+
* a row was actually removed. Powers the desktop Knowledge tab's per-fact
|
|
9590
|
+
* delete — users can prune what the planner remembers about their project.
|
|
9591
|
+
*/
|
|
9592
|
+
deleteFact(entity, relation) {
|
|
9593
|
+
const e = normalizeKey(entity);
|
|
9594
|
+
const r = normalizeKey(relation);
|
|
9595
|
+
if (!e || !r) return false;
|
|
9596
|
+
const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
|
|
9597
|
+
return result.changes > 0;
|
|
9598
|
+
}
|
|
9599
|
+
/** Delete every fact. Returns how many were removed. */
|
|
9600
|
+
clearFacts() {
|
|
9601
|
+
return this.db.prepare("DELETE FROM facts").run().changes;
|
|
9602
|
+
}
|
|
9586
9603
|
// ── Export / Import ──────────────────────────
|
|
9587
9604
|
//
|
|
9588
9605
|
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
@@ -9631,9 +9648,9 @@ var WorldStateDB = class {
|
|
|
9631
9648
|
dumpDebugIfNeeded() {
|
|
9632
9649
|
if (!this.debugMode) return;
|
|
9633
9650
|
try {
|
|
9634
|
-
const dumpPath =
|
|
9651
|
+
const dumpPath = path21.join(os6.tmpdir(), "cascade_world_state_debug.json");
|
|
9635
9652
|
const entries = this.getAllEntries();
|
|
9636
|
-
|
|
9653
|
+
fs20.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9637
9654
|
} catch (err) {
|
|
9638
9655
|
console.error("Failed to dump debug world state", err);
|
|
9639
9656
|
}
|
|
@@ -10543,7 +10560,7 @@ var Keystore = class {
|
|
|
10543
10560
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
10544
10561
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
10545
10562
|
this.backend = "keytar";
|
|
10546
|
-
if (password &&
|
|
10563
|
+
if (password && fs20.existsSync(this.storePath)) {
|
|
10547
10564
|
try {
|
|
10548
10565
|
const fileEntries = this.decryptFile(password);
|
|
10549
10566
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -10562,7 +10579,7 @@ var Keystore = class {
|
|
|
10562
10579
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
10563
10580
|
);
|
|
10564
10581
|
}
|
|
10565
|
-
if (!
|
|
10582
|
+
if (!fs20.existsSync(this.storePath)) {
|
|
10566
10583
|
const salt = crypto3.randomBytes(SALT_LEN);
|
|
10567
10584
|
this.masterKey = this.deriveKey(password, salt);
|
|
10568
10585
|
this.writeWithSalt({}, salt);
|
|
@@ -10576,7 +10593,7 @@ var Keystore = class {
|
|
|
10576
10593
|
}
|
|
10577
10594
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
10578
10595
|
unlockSync(password) {
|
|
10579
|
-
if (!
|
|
10596
|
+
if (!fs20.existsSync(this.storePath)) {
|
|
10580
10597
|
const salt = crypto3.randomBytes(SALT_LEN);
|
|
10581
10598
|
this.masterKey = this.deriveKey(password, salt);
|
|
10582
10599
|
this.writeWithSalt({}, salt);
|
|
@@ -10634,7 +10651,7 @@ var Keystore = class {
|
|
|
10634
10651
|
}
|
|
10635
10652
|
}
|
|
10636
10653
|
decryptFile(password, knownSalt) {
|
|
10637
|
-
if (!
|
|
10654
|
+
if (!fs20.existsSync(this.storePath)) return {};
|
|
10638
10655
|
try {
|
|
10639
10656
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
10640
10657
|
const useSalt = knownSalt ?? salt;
|
|
@@ -10656,8 +10673,8 @@ var Keystore = class {
|
|
|
10656
10673
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10657
10674
|
const tag = cipher.getAuthTag();
|
|
10658
10675
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
10659
|
-
|
|
10660
|
-
|
|
10676
|
+
fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
|
|
10677
|
+
fs20.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10661
10678
|
}
|
|
10662
10679
|
writeWithSalt(data, salt) {
|
|
10663
10680
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -10667,11 +10684,11 @@ var Keystore = class {
|
|
|
10667
10684
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10668
10685
|
const tag = cipher.getAuthTag();
|
|
10669
10686
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
10670
|
-
|
|
10671
|
-
|
|
10687
|
+
fs20.mkdirSync(path21.dirname(this.storePath), { recursive: true });
|
|
10688
|
+
fs20.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10672
10689
|
}
|
|
10673
10690
|
readRaw() {
|
|
10674
|
-
const buf =
|
|
10691
|
+
const buf = fs20.readFileSync(this.storePath);
|
|
10675
10692
|
let offset = 0;
|
|
10676
10693
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
10677
10694
|
offset += SALT_LEN;
|
|
@@ -10704,7 +10721,7 @@ var CascadeIgnore = class {
|
|
|
10704
10721
|
]);
|
|
10705
10722
|
}
|
|
10706
10723
|
async load(workspacePath) {
|
|
10707
|
-
const filePath =
|
|
10724
|
+
const filePath = path21.join(workspacePath, ".cascadeignore");
|
|
10708
10725
|
try {
|
|
10709
10726
|
const content = await fs4.readFile(filePath, "utf-8");
|
|
10710
10727
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -10715,7 +10732,7 @@ var CascadeIgnore = class {
|
|
|
10715
10732
|
}
|
|
10716
10733
|
isIgnored(filePath, workspacePath) {
|
|
10717
10734
|
try {
|
|
10718
|
-
const relative = workspacePath ?
|
|
10735
|
+
const relative = workspacePath ? path21.relative(workspacePath, filePath) : filePath;
|
|
10719
10736
|
return this.ig.ignores(relative);
|
|
10720
10737
|
} catch {
|
|
10721
10738
|
return false;
|
|
@@ -10726,7 +10743,7 @@ var CascadeIgnore = class {
|
|
|
10726
10743
|
}
|
|
10727
10744
|
};
|
|
10728
10745
|
async function loadCascadeMd(workspacePath) {
|
|
10729
|
-
const filePath =
|
|
10746
|
+
const filePath = path21.join(workspacePath, "CASCADE.md");
|
|
10730
10747
|
try {
|
|
10731
10748
|
const raw = await fs4.readFile(filePath, "utf-8");
|
|
10732
10749
|
return parseCascadeMd(raw);
|
|
@@ -10757,7 +10774,7 @@ ${raw.trim()}`;
|
|
|
10757
10774
|
var MemoryStore = class _MemoryStore {
|
|
10758
10775
|
db;
|
|
10759
10776
|
constructor(dbPath) {
|
|
10760
|
-
|
|
10777
|
+
fs20.mkdirSync(path21.dirname(dbPath), { recursive: true });
|
|
10761
10778
|
try {
|
|
10762
10779
|
this.db = new Database(dbPath, { timeout: 5e3 });
|
|
10763
10780
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -11592,6 +11609,60 @@ Original error: ${err.message}`
|
|
|
11592
11609
|
};
|
|
11593
11610
|
}
|
|
11594
11611
|
};
|
|
11612
|
+
function credentialsPath(globalDir) {
|
|
11613
|
+
return path21.join(globalDir, GLOBAL_CREDENTIALS_FILE);
|
|
11614
|
+
}
|
|
11615
|
+
function providerKey(p) {
|
|
11616
|
+
if (p.type === "azure") {
|
|
11617
|
+
return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
|
|
11618
|
+
}
|
|
11619
|
+
return p.type;
|
|
11620
|
+
}
|
|
11621
|
+
function isPersistable(p) {
|
|
11622
|
+
return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
|
|
11623
|
+
}
|
|
11624
|
+
function loadGlobalCredentials(globalDir) {
|
|
11625
|
+
try {
|
|
11626
|
+
const raw = fs20.readFileSync(credentialsPath(globalDir), "utf-8");
|
|
11627
|
+
const parsed = JSON.parse(raw);
|
|
11628
|
+
if (!Array.isArray(parsed.providers)) return [];
|
|
11629
|
+
return parsed.providers.filter(
|
|
11630
|
+
(p) => Boolean(p) && typeof p.type === "string"
|
|
11631
|
+
);
|
|
11632
|
+
} catch {
|
|
11633
|
+
return [];
|
|
11634
|
+
}
|
|
11635
|
+
}
|
|
11636
|
+
function saveGlobalCredentials(globalDir, providers) {
|
|
11637
|
+
const filePath = credentialsPath(globalDir);
|
|
11638
|
+
fs20.mkdirSync(globalDir, { recursive: true, mode: 448 });
|
|
11639
|
+
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
11640
|
+
const tmp = `${filePath}.tmp`;
|
|
11641
|
+
fs20.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
11642
|
+
fs20.renameSync(tmp, filePath);
|
|
11643
|
+
try {
|
|
11644
|
+
fs20.chmodSync(filePath, 384);
|
|
11645
|
+
} catch {
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
11649
|
+
const merged = [...workspaceProviders];
|
|
11650
|
+
const byKey = new Map(merged.map((p) => [providerKey(p), p]));
|
|
11651
|
+
for (const g of globalProviders) {
|
|
11652
|
+
const existing = byKey.get(providerKey(g));
|
|
11653
|
+
if (!existing) {
|
|
11654
|
+
merged.push({ ...g });
|
|
11655
|
+
byKey.set(providerKey(g), merged[merged.length - 1]);
|
|
11656
|
+
continue;
|
|
11657
|
+
}
|
|
11658
|
+
if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
|
|
11659
|
+
if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
|
|
11660
|
+
if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
|
|
11661
|
+
if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
|
|
11662
|
+
if (!existing.label && g.label) existing.label = g.label;
|
|
11663
|
+
}
|
|
11664
|
+
return merged;
|
|
11665
|
+
}
|
|
11595
11666
|
|
|
11596
11667
|
// src/config/index.ts
|
|
11597
11668
|
var ConfigManager = class {
|
|
@@ -11602,18 +11673,20 @@ var ConfigManager = class {
|
|
|
11602
11673
|
cascadeMd = null;
|
|
11603
11674
|
workspacePath;
|
|
11604
11675
|
globalDir;
|
|
11605
|
-
|
|
11676
|
+
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
11677
|
+
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
11606
11678
|
this.workspacePath = workspacePath;
|
|
11607
|
-
this.globalDir =
|
|
11679
|
+
this.globalDir = globalDirOverride ?? path21.join(os6.homedir(), GLOBAL_CONFIG_DIR);
|
|
11608
11680
|
}
|
|
11609
11681
|
async load() {
|
|
11610
11682
|
this.config = await this.loadConfig();
|
|
11611
11683
|
this.ignore = new CascadeIgnore();
|
|
11612
11684
|
await this.ignore.load(this.workspacePath);
|
|
11613
11685
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
11614
|
-
this.keystore = new Keystore(
|
|
11615
|
-
this.store = new MemoryStore(
|
|
11686
|
+
this.keystore = new Keystore(path21.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
11687
|
+
this.store = new MemoryStore(path21.join(this.workspacePath, CASCADE_DB_FILE));
|
|
11616
11688
|
await this.injectEnvKeys();
|
|
11689
|
+
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
11617
11690
|
await this.ensureDefaultIdentity();
|
|
11618
11691
|
}
|
|
11619
11692
|
getConfig() {
|
|
@@ -11635,9 +11708,14 @@ var ConfigManager = class {
|
|
|
11635
11708
|
return this.workspacePath;
|
|
11636
11709
|
}
|
|
11637
11710
|
async save() {
|
|
11638
|
-
const configPath =
|
|
11639
|
-
await fs4.mkdir(
|
|
11711
|
+
const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11712
|
+
await fs4.mkdir(path21.dirname(configPath), { recursive: true });
|
|
11640
11713
|
await fs4.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
11714
|
+
try {
|
|
11715
|
+
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
11716
|
+
} catch (err) {
|
|
11717
|
+
console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
|
|
11718
|
+
}
|
|
11641
11719
|
}
|
|
11642
11720
|
async updateConfig(updates) {
|
|
11643
11721
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -11660,7 +11738,7 @@ var ConfigManager = class {
|
|
|
11660
11738
|
return configProvider?.apiKey;
|
|
11661
11739
|
}
|
|
11662
11740
|
async loadConfig() {
|
|
11663
|
-
const configPath =
|
|
11741
|
+
const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11664
11742
|
try {
|
|
11665
11743
|
const raw = await fs4.readFile(configPath, "utf-8");
|
|
11666
11744
|
return validateConfig(JSON.parse(raw));
|
|
@@ -11767,6 +11845,11 @@ function authMiddleware(secret, required = true) {
|
|
|
11767
11845
|
}
|
|
11768
11846
|
const user = verifyToken(token, secret);
|
|
11769
11847
|
if (!user) {
|
|
11848
|
+
if (!required) {
|
|
11849
|
+
req.user = void 0;
|
|
11850
|
+
next();
|
|
11851
|
+
return;
|
|
11852
|
+
}
|
|
11770
11853
|
res.status(401).json({ error: "Invalid or expired token" });
|
|
11771
11854
|
return;
|
|
11772
11855
|
}
|
|
@@ -11899,6 +11982,25 @@ var DashboardSocket = class {
|
|
|
11899
11982
|
});
|
|
11900
11983
|
});
|
|
11901
11984
|
}
|
|
11985
|
+
/**
|
|
11986
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
11987
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
11988
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
11989
|
+
*/
|
|
11990
|
+
onPlanDecision(callback) {
|
|
11991
|
+
this.io.on("connection", (socket) => {
|
|
11992
|
+
socket.on("plan:decision", (payload) => {
|
|
11993
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
11994
|
+
callback({
|
|
11995
|
+
sessionId: payload.sessionId,
|
|
11996
|
+
approved: payload.approved,
|
|
11997
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
11998
|
+
editedPlan: payload.editedPlan
|
|
11999
|
+
});
|
|
12000
|
+
}
|
|
12001
|
+
});
|
|
12002
|
+
});
|
|
12003
|
+
}
|
|
11902
12004
|
onSessionSteer(callback) {
|
|
11903
12005
|
this.io.on("connection", (socket) => {
|
|
11904
12006
|
socket.on("session:steer", (payload) => {
|
|
@@ -11940,7 +12042,123 @@ var DashboardSocket = class {
|
|
|
11940
12042
|
this.io.close();
|
|
11941
12043
|
}
|
|
11942
12044
|
};
|
|
11943
|
-
var
|
|
12045
|
+
var TaskScheduler = class {
|
|
12046
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
12047
|
+
store;
|
|
12048
|
+
runner;
|
|
12049
|
+
constructor(store, runner) {
|
|
12050
|
+
this.store = store;
|
|
12051
|
+
this.runner = runner;
|
|
12052
|
+
}
|
|
12053
|
+
start() {
|
|
12054
|
+
const tasks = this.store.listScheduledTasks();
|
|
12055
|
+
for (const task of tasks) {
|
|
12056
|
+
if (task.enabled) this.schedule(task);
|
|
12057
|
+
}
|
|
12058
|
+
}
|
|
12059
|
+
stop() {
|
|
12060
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
12061
|
+
this.cronJobs.clear();
|
|
12062
|
+
}
|
|
12063
|
+
schedule(task) {
|
|
12064
|
+
if (!cron.validate(task.cronExpression)) {
|
|
12065
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12066
|
+
}
|
|
12067
|
+
const existing = this.cronJobs.get(task.id);
|
|
12068
|
+
if (existing) {
|
|
12069
|
+
try {
|
|
12070
|
+
existing.stop();
|
|
12071
|
+
} catch {
|
|
12072
|
+
}
|
|
12073
|
+
}
|
|
12074
|
+
const job = cron.schedule(task.cronExpression, async () => {
|
|
12075
|
+
try {
|
|
12076
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12077
|
+
this.store.saveScheduledTask(task);
|
|
12078
|
+
await this.runner(task);
|
|
12079
|
+
} catch (err) {
|
|
12080
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12081
|
+
}
|
|
12082
|
+
}, { timezone: "UTC" });
|
|
12083
|
+
this.cronJobs.set(task.id, job);
|
|
12084
|
+
}
|
|
12085
|
+
unschedule(taskId) {
|
|
12086
|
+
this.cronJobs.get(taskId)?.stop();
|
|
12087
|
+
this.cronJobs.delete(taskId);
|
|
12088
|
+
}
|
|
12089
|
+
add(task) {
|
|
12090
|
+
this.store.saveScheduledTask(task);
|
|
12091
|
+
if (task.enabled) this.schedule(task);
|
|
12092
|
+
}
|
|
12093
|
+
remove(taskId) {
|
|
12094
|
+
this.unschedule(taskId);
|
|
12095
|
+
this.store.deleteScheduledTask(taskId);
|
|
12096
|
+
}
|
|
12097
|
+
list() {
|
|
12098
|
+
return this.store.listScheduledTasks();
|
|
12099
|
+
}
|
|
12100
|
+
isRunning(taskId) {
|
|
12101
|
+
return this.cronJobs.has(taskId);
|
|
12102
|
+
}
|
|
12103
|
+
static validateCron(expression) {
|
|
12104
|
+
return cron.validate(expression);
|
|
12105
|
+
}
|
|
12106
|
+
};
|
|
12107
|
+
|
|
12108
|
+
// src/dashboard/cost-stats.ts
|
|
12109
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
12110
|
+
function utcDateKey(d) {
|
|
12111
|
+
return d.toISOString().slice(0, 10);
|
|
12112
|
+
}
|
|
12113
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
12114
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
12115
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
12116
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12117
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
12118
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
12119
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
12120
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
12121
|
+
}
|
|
12122
|
+
let totalCostUsd = 0;
|
|
12123
|
+
let totalTokens = 0;
|
|
12124
|
+
let totalRuns = 0;
|
|
12125
|
+
for (const s of sessions) {
|
|
12126
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
12127
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
12128
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
12129
|
+
totalCostUsd += cost;
|
|
12130
|
+
totalTokens += tokens;
|
|
12131
|
+
totalRuns += runs;
|
|
12132
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
12133
|
+
if (!Number.isNaN(when.getTime())) {
|
|
12134
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
12135
|
+
if (bucket) {
|
|
12136
|
+
bucket.costUsd += cost;
|
|
12137
|
+
bucket.tokens += tokens;
|
|
12138
|
+
bucket.runs += runs;
|
|
12139
|
+
}
|
|
12140
|
+
}
|
|
12141
|
+
}
|
|
12142
|
+
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) => ({
|
|
12143
|
+
sessionId: s.id,
|
|
12144
|
+
title: s.title,
|
|
12145
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
12146
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
12147
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
12148
|
+
updatedAt: s.updatedAt
|
|
12149
|
+
}));
|
|
12150
|
+
return {
|
|
12151
|
+
totalCostUsd,
|
|
12152
|
+
totalTokens,
|
|
12153
|
+
totalSessions: sessions.length,
|
|
12154
|
+
totalRuns,
|
|
12155
|
+
perDay: [...buckets.values()],
|
|
12156
|
+
topSessions
|
|
12157
|
+
};
|
|
12158
|
+
}
|
|
12159
|
+
|
|
12160
|
+
// src/dashboard/server.ts
|
|
12161
|
+
var __dirname$1 = path21.dirname(fileURLToPath(import.meta.url));
|
|
11944
12162
|
var DashboardServer = class {
|
|
11945
12163
|
app;
|
|
11946
12164
|
httpServer;
|
|
@@ -11965,6 +12183,14 @@ var DashboardServer = class {
|
|
|
11965
12183
|
* map is how that answer reaches the run that's blocked on it.
|
|
11966
12184
|
*/
|
|
11967
12185
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
12186
|
+
/**
|
|
12187
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
12188
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
12189
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
12190
|
+
*/
|
|
12191
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
12192
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
12193
|
+
scheduler;
|
|
11968
12194
|
port;
|
|
11969
12195
|
host;
|
|
11970
12196
|
workspacePath;
|
|
@@ -11982,6 +12208,7 @@ var DashboardServer = class {
|
|
|
11982
12208
|
secret: this.dashboardSecret,
|
|
11983
12209
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
11984
12210
|
});
|
|
12211
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
11985
12212
|
this.setupMiddleware();
|
|
11986
12213
|
this.setupRoutes();
|
|
11987
12214
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -12045,6 +12272,9 @@ var DashboardServer = class {
|
|
|
12045
12272
|
cascade.on("peer:message", (e) => {
|
|
12046
12273
|
this.socket.emitPeerMessage(e);
|
|
12047
12274
|
});
|
|
12275
|
+
cascade.on("plan:approval-required", (e) => {
|
|
12276
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
12277
|
+
});
|
|
12048
12278
|
try {
|
|
12049
12279
|
const result = await cascade.run({
|
|
12050
12280
|
prompt: runPrompt,
|
|
@@ -12052,7 +12282,8 @@ var DashboardServer = class {
|
|
|
12052
12282
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12053
12283
|
});
|
|
12054
12284
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12055
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
12285
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12286
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12056
12287
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
12057
12288
|
this.socket.broadcast("cost:update", {
|
|
12058
12289
|
sessionId,
|
|
@@ -12062,6 +12293,7 @@ var DashboardServer = class {
|
|
|
12062
12293
|
this.throttledBroadcast("workspace");
|
|
12063
12294
|
} catch (err) {
|
|
12064
12295
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12296
|
+
this.captureWhy(sessionId, cascade);
|
|
12065
12297
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
12066
12298
|
sessionId,
|
|
12067
12299
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12072,9 +12304,17 @@ var DashboardServer = class {
|
|
|
12072
12304
|
this.denyPendingApprovals(sessionId);
|
|
12073
12305
|
}
|
|
12074
12306
|
});
|
|
12307
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
12308
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
12309
|
+
approved,
|
|
12310
|
+
note,
|
|
12311
|
+
editedPlan
|
|
12312
|
+
);
|
|
12313
|
+
});
|
|
12075
12314
|
this.socket.onSessionHalt((sessionId) => {
|
|
12076
12315
|
this.activeControllers.get(sessionId)?.abort();
|
|
12077
12316
|
this.denyPendingApprovals(sessionId);
|
|
12317
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
12078
12318
|
});
|
|
12079
12319
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
12080
12320
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -12107,12 +12347,21 @@ var DashboardServer = class {
|
|
|
12107
12347
|
resolve();
|
|
12108
12348
|
});
|
|
12109
12349
|
});
|
|
12350
|
+
try {
|
|
12351
|
+
this.scheduler.start();
|
|
12352
|
+
} catch (err) {
|
|
12353
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
12354
|
+
}
|
|
12110
12355
|
}
|
|
12111
12356
|
async stop() {
|
|
12112
12357
|
if (this.broadcastTimer) {
|
|
12113
12358
|
clearTimeout(this.broadcastTimer);
|
|
12114
12359
|
this.broadcastTimer = null;
|
|
12115
12360
|
}
|
|
12361
|
+
try {
|
|
12362
|
+
this.scheduler.stop();
|
|
12363
|
+
} catch {
|
|
12364
|
+
}
|
|
12116
12365
|
this.socket.close();
|
|
12117
12366
|
try {
|
|
12118
12367
|
this.globalStore?.close();
|
|
@@ -12143,12 +12392,17 @@ var DashboardServer = class {
|
|
|
12143
12392
|
*/
|
|
12144
12393
|
persistConfig() {
|
|
12145
12394
|
try {
|
|
12146
|
-
const configPath =
|
|
12147
|
-
|
|
12148
|
-
|
|
12395
|
+
const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
12396
|
+
fs20.mkdirSync(path21.dirname(configPath), { recursive: true });
|
|
12397
|
+
fs20.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
12149
12398
|
} catch (err) {
|
|
12150
12399
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
12151
12400
|
}
|
|
12401
|
+
try {
|
|
12402
|
+
saveGlobalCredentials(path21.join(os6.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
12403
|
+
} catch (err) {
|
|
12404
|
+
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
12405
|
+
}
|
|
12152
12406
|
}
|
|
12153
12407
|
/**
|
|
12154
12408
|
* Produce a stable dashboard JWT signing secret.
|
|
@@ -12160,15 +12414,15 @@ var DashboardServer = class {
|
|
|
12160
12414
|
resolveDashboardSecret() {
|
|
12161
12415
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
12162
12416
|
if (fromConfig) return fromConfig;
|
|
12163
|
-
const secretPath =
|
|
12417
|
+
const secretPath = path21.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
12164
12418
|
try {
|
|
12165
|
-
if (
|
|
12166
|
-
const existing =
|
|
12419
|
+
if (fs20.existsSync(secretPath)) {
|
|
12420
|
+
const existing = fs20.readFileSync(secretPath, "utf-8").trim();
|
|
12167
12421
|
if (existing.length >= 16) return existing;
|
|
12168
12422
|
}
|
|
12169
12423
|
const generated = randomUUID();
|
|
12170
|
-
|
|
12171
|
-
|
|
12424
|
+
fs20.mkdirSync(path21.dirname(secretPath), { recursive: true });
|
|
12425
|
+
fs20.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
12172
12426
|
if (this.config.dashboard.auth) {
|
|
12173
12427
|
console.warn(
|
|
12174
12428
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -12195,7 +12449,7 @@ var DashboardServer = class {
|
|
|
12195
12449
|
// ── Setup ─────────────────────────────────────
|
|
12196
12450
|
getGlobalStore() {
|
|
12197
12451
|
if (!this.globalStore) {
|
|
12198
|
-
const globalDbPath =
|
|
12452
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12199
12453
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
12200
12454
|
}
|
|
12201
12455
|
return this.globalStore;
|
|
@@ -12233,16 +12487,60 @@ var DashboardServer = class {
|
|
|
12233
12487
|
return title;
|
|
12234
12488
|
}
|
|
12235
12489
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
12236
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
12490
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
12237
12491
|
try {
|
|
12238
12492
|
if (reply && reply.trim()) {
|
|
12239
12493
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12240
12494
|
}
|
|
12495
|
+
if (result) {
|
|
12496
|
+
const session = this.store.getSession(sessionId);
|
|
12497
|
+
if (session) {
|
|
12498
|
+
this.store.updateSession(sessionId, {
|
|
12499
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12500
|
+
metadata: {
|
|
12501
|
+
...session.metadata,
|
|
12502
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
12503
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
12504
|
+
taskCount: session.metadata.taskCount + 1
|
|
12505
|
+
}
|
|
12506
|
+
});
|
|
12507
|
+
}
|
|
12508
|
+
}
|
|
12241
12509
|
} catch (err) {
|
|
12242
12510
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
12243
12511
|
}
|
|
12244
12512
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
12245
12513
|
}
|
|
12514
|
+
/**
|
|
12515
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
12516
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
12517
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
12518
|
+
*/
|
|
12519
|
+
captureWhy(sessionId, cascade, result) {
|
|
12520
|
+
try {
|
|
12521
|
+
const stats = cascade.getRouter().getStats();
|
|
12522
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
12523
|
+
const report = {
|
|
12524
|
+
sessionId,
|
|
12525
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12526
|
+
decisions: cascade.getDecisionLog(),
|
|
12527
|
+
savedUsd: savings.savedUsd,
|
|
12528
|
+
savedPct: savings.savedPct,
|
|
12529
|
+
totalCostUsd: stats.totalCostUsd,
|
|
12530
|
+
totalTokens: stats.totalTokens,
|
|
12531
|
+
costByTier: stats.costByTier,
|
|
12532
|
+
durationMs: result?.durationMs
|
|
12533
|
+
};
|
|
12534
|
+
this.whyBySession.set(sessionId, report);
|
|
12535
|
+
if (this.whyBySession.size > 50) {
|
|
12536
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
12537
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
12538
|
+
}
|
|
12539
|
+
this.socket.broadcast("run:why", report);
|
|
12540
|
+
} catch (err) {
|
|
12541
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
12542
|
+
}
|
|
12543
|
+
}
|
|
12246
12544
|
/**
|
|
12247
12545
|
* Route steering text into running Cascade instances. Targets the given
|
|
12248
12546
|
* session, or every active session when none is specified (the desktop
|
|
@@ -12272,6 +12570,52 @@ var DashboardServer = class {
|
|
|
12272
12570
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
12273
12571
|
});
|
|
12274
12572
|
}
|
|
12573
|
+
/**
|
|
12574
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
12575
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
12576
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
12577
|
+
* client so an open desktop sees the run appear live.
|
|
12578
|
+
*/
|
|
12579
|
+
async runScheduledTask(task) {
|
|
12580
|
+
const sessionId = randomUUID();
|
|
12581
|
+
const prompt = task.prompt;
|
|
12582
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
12583
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
12584
|
+
this.activeSessions.set(sessionId, cascade);
|
|
12585
|
+
cascade.on("tier:status", (e) => {
|
|
12586
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
12587
|
+
});
|
|
12588
|
+
cascade.on("peer:message", (e) => {
|
|
12589
|
+
this.socket.emitPeerMessage(e);
|
|
12590
|
+
});
|
|
12591
|
+
try {
|
|
12592
|
+
const result = await cascade.run({
|
|
12593
|
+
prompt,
|
|
12594
|
+
identityId: task.identityId,
|
|
12595
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
12596
|
+
});
|
|
12597
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
12598
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12599
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12600
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
12601
|
+
this.socket.broadcast("cost:update", {
|
|
12602
|
+
sessionId,
|
|
12603
|
+
totalTokens: result.usage.totalTokens,
|
|
12604
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
12605
|
+
});
|
|
12606
|
+
this.throttledBroadcast("workspace");
|
|
12607
|
+
} catch (err) {
|
|
12608
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12609
|
+
this.captureWhy(sessionId, cascade);
|
|
12610
|
+
this.socket.broadcast("session:error", {
|
|
12611
|
+
sessionId,
|
|
12612
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12613
|
+
});
|
|
12614
|
+
throw err;
|
|
12615
|
+
} finally {
|
|
12616
|
+
this.activeSessions.delete(sessionId);
|
|
12617
|
+
}
|
|
12618
|
+
}
|
|
12275
12619
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
12276
12620
|
denyPendingApprovals(sessionId) {
|
|
12277
12621
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -12374,12 +12718,12 @@ ${prompt}`;
|
|
|
12374
12718
|
}
|
|
12375
12719
|
}
|
|
12376
12720
|
watchRuntimeChanges() {
|
|
12377
|
-
const workspaceDbPath =
|
|
12378
|
-
const globalDbPath =
|
|
12721
|
+
const workspaceDbPath = path21.join(this.workspacePath, CASCADE_DB_FILE);
|
|
12722
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12379
12723
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
12380
12724
|
for (const watchPath of watchPaths) {
|
|
12381
|
-
if (!
|
|
12382
|
-
|
|
12725
|
+
if (!fs20.existsSync(watchPath)) continue;
|
|
12726
|
+
fs20.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
12383
12727
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
12384
12728
|
});
|
|
12385
12729
|
}
|
|
@@ -12501,7 +12845,7 @@ ${prompt}`;
|
|
|
12501
12845
|
const sessionId = req.params.id;
|
|
12502
12846
|
this.store.deleteSession(sessionId);
|
|
12503
12847
|
this.store.deleteRuntimeSession(sessionId);
|
|
12504
|
-
const globalDbPath =
|
|
12848
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12505
12849
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12506
12850
|
try {
|
|
12507
12851
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -12594,7 +12938,7 @@ ${prompt}`;
|
|
|
12594
12938
|
});
|
|
12595
12939
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
12596
12940
|
const body = req.body;
|
|
12597
|
-
const globalDbPath =
|
|
12941
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12598
12942
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
12599
12943
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12600
12944
|
try {
|
|
@@ -12617,7 +12961,7 @@ ${prompt}`;
|
|
|
12617
12961
|
});
|
|
12618
12962
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
12619
12963
|
this.store.deleteAllRuntimeNodes();
|
|
12620
|
-
const globalDbPath =
|
|
12964
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12621
12965
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12622
12966
|
try {
|
|
12623
12967
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -12703,12 +13047,12 @@ ${prompt}`;
|
|
|
12703
13047
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
12704
13048
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
12705
13049
|
try {
|
|
12706
|
-
const configPath =
|
|
12707
|
-
const existing =
|
|
13050
|
+
const configPath = path21.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
13051
|
+
const existing = fs20.existsSync(configPath) ? JSON.parse(fs20.readFileSync(configPath, "utf-8")) : {};
|
|
12708
13052
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
12709
13053
|
const tmp = configPath + ".tmp";
|
|
12710
|
-
|
|
12711
|
-
|
|
13054
|
+
fs20.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
13055
|
+
fs20.renameSync(tmp, configPath);
|
|
12712
13056
|
res.json({ ok: true });
|
|
12713
13057
|
} catch (err) {
|
|
12714
13058
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -12736,7 +13080,7 @@ ${prompt}`;
|
|
|
12736
13080
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
12737
13081
|
const scope = req.query["scope"] ?? "workspace";
|
|
12738
13082
|
if (scope === "global") {
|
|
12739
|
-
const globalDbPath =
|
|
13083
|
+
const globalDbPath = path21.join(os6.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12740
13084
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12741
13085
|
try {
|
|
12742
13086
|
res.json({
|
|
@@ -12784,6 +13128,9 @@ ${prompt}`;
|
|
|
12784
13128
|
cascade.on("peer:message", (e) => {
|
|
12785
13129
|
this.socket.emitPeerMessage(e);
|
|
12786
13130
|
});
|
|
13131
|
+
cascade.on("plan:approval-required", (e) => {
|
|
13132
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
13133
|
+
});
|
|
12787
13134
|
try {
|
|
12788
13135
|
const result = await cascade.run({
|
|
12789
13136
|
prompt: runPrompt,
|
|
@@ -12791,7 +13138,8 @@ ${prompt}`;
|
|
|
12791
13138
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12792
13139
|
});
|
|
12793
13140
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12794
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
13141
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
13142
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12795
13143
|
this.socket.broadcast("cost:update", {
|
|
12796
13144
|
sessionId,
|
|
12797
13145
|
totalTokens: result.usage.totalTokens,
|
|
@@ -12801,6 +13149,7 @@ ${prompt}`;
|
|
|
12801
13149
|
this.throttledBroadcast("workspace");
|
|
12802
13150
|
} catch (err) {
|
|
12803
13151
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
13152
|
+
this.captureWhy(sessionId, cascade);
|
|
12804
13153
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
12805
13154
|
sessionId,
|
|
12806
13155
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12822,13 +13171,217 @@ ${prompt}`;
|
|
|
12822
13171
|
}))
|
|
12823
13172
|
});
|
|
12824
13173
|
});
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
13174
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
13175
|
+
const report = this.whyBySession.get(req.params.id);
|
|
13176
|
+
if (!report) {
|
|
13177
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
13178
|
+
return;
|
|
13179
|
+
}
|
|
13180
|
+
res.json(report);
|
|
13181
|
+
});
|
|
13182
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
13183
|
+
const sessionId = req.params.id;
|
|
13184
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13185
|
+
const before = /* @__PURE__ */ new Map();
|
|
13186
|
+
for (const taskId of taskIds) {
|
|
13187
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
13188
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
13189
|
+
}
|
|
13190
|
+
}
|
|
13191
|
+
const { readFile } = await import('fs/promises');
|
|
13192
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
13193
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
13194
|
+
let after = "";
|
|
13195
|
+
let missing = false;
|
|
13196
|
+
try {
|
|
13197
|
+
const buf = await readFile(filePath);
|
|
13198
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
13199
|
+
} catch {
|
|
13200
|
+
missing = true;
|
|
13201
|
+
}
|
|
13202
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
13203
|
+
}));
|
|
13204
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
13205
|
+
});
|
|
13206
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
13207
|
+
const sessionId = req.params.id;
|
|
13208
|
+
const body = req.body;
|
|
13209
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
13210
|
+
res.status(400).json({ error: "filePath is required" });
|
|
13211
|
+
return;
|
|
13212
|
+
}
|
|
13213
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13214
|
+
let content;
|
|
13215
|
+
for (const taskId of taskIds) {
|
|
13216
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
13217
|
+
if (snap) {
|
|
13218
|
+
content = snap.content;
|
|
13219
|
+
break;
|
|
13220
|
+
}
|
|
13221
|
+
}
|
|
13222
|
+
if (content === void 0) {
|
|
13223
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
13224
|
+
return;
|
|
13225
|
+
}
|
|
13226
|
+
try {
|
|
13227
|
+
const { writeFile } = await import('fs/promises');
|
|
13228
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
13229
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
13230
|
+
} catch (err) {
|
|
13231
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13232
|
+
}
|
|
13233
|
+
});
|
|
13234
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
13235
|
+
try {
|
|
13236
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13237
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
13238
|
+
res.json({
|
|
13239
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
13240
|
+
budget: {
|
|
13241
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
13242
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
13243
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
13244
|
+
}
|
|
13245
|
+
});
|
|
13246
|
+
} catch (err) {
|
|
13247
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13248
|
+
}
|
|
13249
|
+
});
|
|
13250
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
13251
|
+
try {
|
|
13252
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
13253
|
+
} catch (err) {
|
|
13254
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13255
|
+
}
|
|
13256
|
+
});
|
|
13257
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
13258
|
+
const body = req.body;
|
|
13259
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
13260
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
13261
|
+
return;
|
|
13262
|
+
}
|
|
13263
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
13264
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13265
|
+
return;
|
|
13266
|
+
}
|
|
13267
|
+
const task = {
|
|
13268
|
+
id: randomUUID(),
|
|
13269
|
+
name: body.name.trim(),
|
|
13270
|
+
cronExpression: body.cronExpression.trim(),
|
|
13271
|
+
prompt: body.prompt.trim(),
|
|
13272
|
+
workspacePath: this.workspacePath,
|
|
13273
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13274
|
+
enabled: body.enabled !== false
|
|
13275
|
+
};
|
|
13276
|
+
try {
|
|
13277
|
+
this.scheduler.add(task);
|
|
13278
|
+
res.json(task);
|
|
13279
|
+
} catch (err) {
|
|
13280
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13281
|
+
}
|
|
13282
|
+
});
|
|
13283
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13284
|
+
const id = req.params.id;
|
|
13285
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
13286
|
+
if (!existing) {
|
|
13287
|
+
res.status(404).json({ error: "Not found" });
|
|
13288
|
+
return;
|
|
13289
|
+
}
|
|
13290
|
+
const body = req.body;
|
|
13291
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
13292
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13293
|
+
return;
|
|
13294
|
+
}
|
|
13295
|
+
const updated = {
|
|
13296
|
+
...existing,
|
|
13297
|
+
name: body.name?.trim() || existing.name,
|
|
13298
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
13299
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
13300
|
+
enabled: body.enabled ?? existing.enabled
|
|
13301
|
+
};
|
|
13302
|
+
try {
|
|
13303
|
+
this.scheduler.add(updated);
|
|
13304
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
13305
|
+
res.json(updated);
|
|
13306
|
+
} catch (err) {
|
|
13307
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13308
|
+
}
|
|
13309
|
+
});
|
|
13310
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13311
|
+
try {
|
|
13312
|
+
this.scheduler.remove(req.params.id);
|
|
13313
|
+
res.json({ ok: true });
|
|
13314
|
+
} catch (err) {
|
|
13315
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13316
|
+
}
|
|
13317
|
+
});
|
|
13318
|
+
this.app.get("/api/knowledge", auth, (_req, res) => {
|
|
13319
|
+
try {
|
|
13320
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13321
|
+
try {
|
|
13322
|
+
const facts = ws.getAllFacts();
|
|
13323
|
+
res.json({ total: facts.length, facts });
|
|
13324
|
+
} finally {
|
|
13325
|
+
ws.close();
|
|
13326
|
+
}
|
|
13327
|
+
} catch (err) {
|
|
13328
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13329
|
+
}
|
|
13330
|
+
});
|
|
13331
|
+
this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
|
|
13332
|
+
const body = req.body;
|
|
13333
|
+
if (!body.entity || !body.relation) {
|
|
13334
|
+
res.status(400).json({ error: "entity and relation are required" });
|
|
13335
|
+
return;
|
|
13336
|
+
}
|
|
13337
|
+
try {
|
|
13338
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13339
|
+
try {
|
|
13340
|
+
res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
|
|
13341
|
+
} finally {
|
|
13342
|
+
ws.close();
|
|
13343
|
+
}
|
|
13344
|
+
} catch (err) {
|
|
13345
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13346
|
+
}
|
|
13347
|
+
});
|
|
13348
|
+
this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
|
|
13349
|
+
try {
|
|
13350
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13351
|
+
try {
|
|
13352
|
+
res.json({ ok: true, deleted: ws.clearFacts() });
|
|
13353
|
+
} finally {
|
|
13354
|
+
ws.close();
|
|
13355
|
+
}
|
|
13356
|
+
} catch (err) {
|
|
13357
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13358
|
+
}
|
|
13359
|
+
});
|
|
13360
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
13361
|
+
try {
|
|
13362
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
13363
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
13364
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
13365
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
13366
|
+
try {
|
|
13367
|
+
const all = logger.getAllLogs();
|
|
13368
|
+
const total = all.length;
|
|
13369
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
13370
|
+
res.json({ total, offset, entries });
|
|
13371
|
+
} finally {
|
|
13372
|
+
logger.close();
|
|
13373
|
+
}
|
|
13374
|
+
} catch (err) {
|
|
13375
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13376
|
+
}
|
|
13377
|
+
});
|
|
13378
|
+
const prodPath = path21.resolve(__dirname$1, "../web/dist");
|
|
13379
|
+
const devPath = path21.resolve(__dirname$1, "../../web/dist");
|
|
13380
|
+
const webDistPath = fs20.existsSync(prodPath) ? prodPath : devPath;
|
|
13381
|
+
if (fs20.existsSync(webDistPath)) {
|
|
12829
13382
|
this.app.use(express.static(webDistPath));
|
|
12830
13383
|
this.app.get("*", (_req, res) => {
|
|
12831
|
-
res.sendFile(
|
|
13384
|
+
res.sendFile(path21.join(webDistPath, "index.html"));
|
|
12832
13385
|
});
|
|
12833
13386
|
} else {
|
|
12834
13387
|
this.app.get("/", (_req, res) => {
|
|
@@ -12843,68 +13396,6 @@ ${prompt}`;
|
|
|
12843
13396
|
}
|
|
12844
13397
|
}
|
|
12845
13398
|
};
|
|
12846
|
-
var TaskScheduler = class {
|
|
12847
|
-
cronJobs = /* @__PURE__ */ new Map();
|
|
12848
|
-
store;
|
|
12849
|
-
runner;
|
|
12850
|
-
constructor(store, runner) {
|
|
12851
|
-
this.store = store;
|
|
12852
|
-
this.runner = runner;
|
|
12853
|
-
}
|
|
12854
|
-
start() {
|
|
12855
|
-
const tasks = this.store.listScheduledTasks();
|
|
12856
|
-
for (const task of tasks) {
|
|
12857
|
-
if (task.enabled) this.schedule(task);
|
|
12858
|
-
}
|
|
12859
|
-
}
|
|
12860
|
-
stop() {
|
|
12861
|
-
for (const job of this.cronJobs.values()) job.stop();
|
|
12862
|
-
this.cronJobs.clear();
|
|
12863
|
-
}
|
|
12864
|
-
schedule(task) {
|
|
12865
|
-
if (!cron.validate(task.cronExpression)) {
|
|
12866
|
-
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12867
|
-
}
|
|
12868
|
-
const existing = this.cronJobs.get(task.id);
|
|
12869
|
-
if (existing) {
|
|
12870
|
-
try {
|
|
12871
|
-
existing.stop();
|
|
12872
|
-
} catch {
|
|
12873
|
-
}
|
|
12874
|
-
}
|
|
12875
|
-
const job = cron.schedule(task.cronExpression, async () => {
|
|
12876
|
-
try {
|
|
12877
|
-
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12878
|
-
this.store.saveScheduledTask(task);
|
|
12879
|
-
await this.runner(task);
|
|
12880
|
-
} catch (err) {
|
|
12881
|
-
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12882
|
-
}
|
|
12883
|
-
}, { timezone: "UTC" });
|
|
12884
|
-
this.cronJobs.set(task.id, job);
|
|
12885
|
-
}
|
|
12886
|
-
unschedule(taskId) {
|
|
12887
|
-
this.cronJobs.get(taskId)?.stop();
|
|
12888
|
-
this.cronJobs.delete(taskId);
|
|
12889
|
-
}
|
|
12890
|
-
add(task) {
|
|
12891
|
-
this.store.saveScheduledTask(task);
|
|
12892
|
-
if (task.enabled) this.schedule(task);
|
|
12893
|
-
}
|
|
12894
|
-
remove(taskId) {
|
|
12895
|
-
this.unschedule(taskId);
|
|
12896
|
-
this.store.deleteScheduledTask(taskId);
|
|
12897
|
-
}
|
|
12898
|
-
list() {
|
|
12899
|
-
return this.store.listScheduledTasks();
|
|
12900
|
-
}
|
|
12901
|
-
isRunning(taskId) {
|
|
12902
|
-
return this.cronJobs.has(taskId);
|
|
12903
|
-
}
|
|
12904
|
-
static validateCron(expression) {
|
|
12905
|
-
return cron.validate(expression);
|
|
12906
|
-
}
|
|
12907
|
-
};
|
|
12908
13399
|
var execFileAsync2 = promisify(execFile);
|
|
12909
13400
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
12910
13401
|
function sanitizeEnvValue(v) {
|
|
@@ -12963,6 +13454,6 @@ var HooksRunner = class {
|
|
|
12963
13454
|
}
|
|
12964
13455
|
};
|
|
12965
13456
|
|
|
12966
|
-
export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
|
|
13457
|
+
export { AZURE_BASE_URL_TEMPLATE, AuditLogger, CASCADE_AUDIT_FILE, CASCADE_CONFIG_DIR, CASCADE_CONFIG_FILE, CASCADE_DASHBOARD_SECRET_FILE, CASCADE_DB_FILE, CASCADE_IGNORE_FILE, CASCADE_KEYSTORE_FILE, CASCADE_MD_FILE, CASCADE_VERSION, COMPLEXITY_T2_COUNT, Cascade, CascadeCancelledError, CascadeIgnore, CascadeRouter, CascadeToolError, ConfigManager, DEFAULT_API_PORT, DEFAULT_APPROVAL_REQUIRED, DEFAULT_AUTO_SUMMARIZE_AT, DEFAULT_CONTEXT_LIMIT, DEFAULT_DASHBOARD_PORT, DEFAULT_MAX_SESSION_MESSAGES, DEFAULT_RETENTION_DAYS, DEFAULT_THEME, DashboardServer, GLOBAL_CONFIG_DIR, GLOBAL_CREDENTIALS_FILE, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, HooksRunner, Keystore, LM_STUDIO_BASE_URL, MODELS, McpClient, MemoryStore, OLLAMA_BASE_URL, PROVIDER_DISPLAY_NAMES, T1Administrator, T1_MODEL_PRIORITY, T2Manager, T2_MODEL_PRIORITY, T3Worker, T3_MODEL_PRIORITY, THEME_NAMES, TOOL_NAMES, TaskScheduler, Telemetry, ToolRegistry, VISION_MODEL_PRIORITY, createCascade, nodeHttpFetch, preferIpv4Host, runCascade, streamCascade };
|
|
12967
13458
|
//# sourceMappingURL=index.js.map
|
|
12968
13459
|
//# sourceMappingURL=index.js.map
|