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.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var Database = require('better-sqlite3');
|
|
4
|
-
var
|
|
5
|
-
var
|
|
4
|
+
var path21 = require('path');
|
|
5
|
+
var fs20 = require('fs');
|
|
6
6
|
var os6 = require('os');
|
|
7
7
|
var crypto3 = require('crypto');
|
|
8
8
|
var EventEmitter = require('events');
|
|
@@ -58,8 +58,8 @@ function _interopNamespace(e) {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
61
|
-
var
|
|
62
|
-
var
|
|
61
|
+
var path21__default = /*#__PURE__*/_interopDefault(path21);
|
|
62
|
+
var fs20__default = /*#__PURE__*/_interopDefault(fs20);
|
|
63
63
|
var os6__default = /*#__PURE__*/_interopDefault(os6);
|
|
64
64
|
var crypto3__default = /*#__PURE__*/_interopDefault(crypto3);
|
|
65
65
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
@@ -104,12 +104,12 @@ var init_audit_logger = __esm({
|
|
|
104
104
|
constructor(workspacePath, debugMode = false) {
|
|
105
105
|
this.workspacePath = workspacePath;
|
|
106
106
|
this.debugMode = debugMode;
|
|
107
|
-
const cascadeDir =
|
|
108
|
-
if (!
|
|
109
|
-
|
|
107
|
+
const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
|
|
108
|
+
if (!fs20__default.default.existsSync(cascadeDir)) {
|
|
109
|
+
fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
110
110
|
}
|
|
111
|
-
this.keyPath =
|
|
112
|
-
this.dbPath =
|
|
111
|
+
this.keyPath = path21__default.default.join(cascadeDir, "audit_log.key");
|
|
112
|
+
this.dbPath = path21__default.default.join(cascadeDir, "audit_log.db");
|
|
113
113
|
this.initEncryptionKey();
|
|
114
114
|
this.db = new Database__default.default(this.dbPath);
|
|
115
115
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -139,11 +139,11 @@ var init_audit_logger = __esm({
|
|
|
139
139
|
return crypto3__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
140
140
|
}
|
|
141
141
|
initEncryptionKey() {
|
|
142
|
-
if (
|
|
143
|
-
this.encryptionKey =
|
|
142
|
+
if (fs20__default.default.existsSync(this.keyPath)) {
|
|
143
|
+
this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
|
|
144
144
|
} else {
|
|
145
145
|
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
146
|
-
|
|
146
|
+
fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
encrypt(text) {
|
|
@@ -221,9 +221,9 @@ var init_audit_logger = __esm({
|
|
|
221
221
|
dumpDebugIfNeeded() {
|
|
222
222
|
if (!this.debugMode) return;
|
|
223
223
|
try {
|
|
224
|
-
const dumpPath =
|
|
224
|
+
const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
225
225
|
const entries = this.getAllLogs();
|
|
226
|
-
|
|
226
|
+
fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
227
227
|
} catch (err) {
|
|
228
228
|
console.error("Failed to dump debug audit logs", err);
|
|
229
229
|
}
|
|
@@ -236,7 +236,7 @@ var init_audit_logger = __esm({
|
|
|
236
236
|
});
|
|
237
237
|
|
|
238
238
|
// src/constants.ts
|
|
239
|
-
var CASCADE_VERSION = "0.
|
|
239
|
+
var CASCADE_VERSION = "0.18.0";
|
|
240
240
|
var CASCADE_CONFIG_DIR = ".cascade";
|
|
241
241
|
var CASCADE_MD_FILE = "CASCADE.md";
|
|
242
242
|
var CASCADE_IGNORE_FILE = ".cascadeignore";
|
|
@@ -248,6 +248,7 @@ var CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
|
248
248
|
var GLOBAL_CONFIG_DIR = ".cascade-ai";
|
|
249
249
|
var GLOBAL_DB_FILE = "memory.db";
|
|
250
250
|
var GLOBAL_KEYSTORE_FILE = "keystore.enc";
|
|
251
|
+
var GLOBAL_CREDENTIALS_FILE = "credentials.json";
|
|
251
252
|
var GLOBAL_RUNTIME_DB_FILE = "runtime.db";
|
|
252
253
|
var DEFAULT_DASHBOARD_PORT = 4891;
|
|
253
254
|
var DEFAULT_API_PORT = 4892;
|
|
@@ -2210,7 +2211,7 @@ function computeDelegationSavings(stats, t1Model) {
|
|
|
2210
2211
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
2211
2212
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
2212
2213
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
2213
|
-
var DEFAULT_CACHE_FILE =
|
|
2214
|
+
var DEFAULT_CACHE_FILE = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
2214
2215
|
function normalizeModelId(id) {
|
|
2215
2216
|
let s = id.toLowerCase();
|
|
2216
2217
|
const slash = s.lastIndexOf("/");
|
|
@@ -2355,7 +2356,7 @@ var LiveDataProvider = class {
|
|
|
2355
2356
|
}
|
|
2356
2357
|
async saveCache() {
|
|
2357
2358
|
try {
|
|
2358
|
-
await fs4__default.default.mkdir(
|
|
2359
|
+
await fs4__default.default.mkdir(path21__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
2359
2360
|
const cache = {
|
|
2360
2361
|
fetchedAt: this.fetchedAt,
|
|
2361
2362
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -4258,8 +4259,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
4258
4259
|
tierId: this.id,
|
|
4259
4260
|
sessionId: this.taskId,
|
|
4260
4261
|
requireApproval: false,
|
|
4261
|
-
saveSnapshot: async (
|
|
4262
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
4262
|
+
saveSnapshot: async (path22, content) => {
|
|
4263
|
+
this.store?.addFileSnapshot(this.taskId, path22, content);
|
|
4263
4264
|
},
|
|
4264
4265
|
sendPeerSync: (to, syncType, content) => {
|
|
4265
4266
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -4429,7 +4430,7 @@ ${assignment.expectedOutput}`;
|
|
|
4429
4430
|
const { promisify: promisify4 } = await import('util');
|
|
4430
4431
|
const execAsync2 = promisify4(exec2);
|
|
4431
4432
|
for (const artifactPath of artifactPaths) {
|
|
4432
|
-
const absolutePath =
|
|
4433
|
+
const absolutePath = path21__default.default.resolve(process.cwd(), artifactPath);
|
|
4433
4434
|
try {
|
|
4434
4435
|
const stat = await fs4__default.default.stat(absolutePath);
|
|
4435
4436
|
if (!stat.isFile()) {
|
|
@@ -4450,7 +4451,7 @@ ${assignment.expectedOutput}`;
|
|
|
4450
4451
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
4451
4452
|
continue;
|
|
4452
4453
|
}
|
|
4453
|
-
const ext =
|
|
4454
|
+
const ext = path21__default.default.extname(absolutePath).toLowerCase();
|
|
4454
4455
|
try {
|
|
4455
4456
|
if (ext === ".ts" || ext === ".tsx") {
|
|
4456
4457
|
await execAsync2(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -6532,16 +6533,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
6532
6533
|
if (typeof input !== "string" || input.length === 0) {
|
|
6533
6534
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
6534
6535
|
}
|
|
6535
|
-
const root =
|
|
6536
|
-
const abs =
|
|
6537
|
-
const rel =
|
|
6538
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
6536
|
+
const root = path21__default.default.resolve(workspaceRoot);
|
|
6537
|
+
const abs = path21__default.default.isAbsolute(input) ? path21__default.default.resolve(input) : path21__default.default.resolve(root, input);
|
|
6538
|
+
const rel = path21__default.default.relative(root, abs);
|
|
6539
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path21__default.default.isAbsolute(rel)) {
|
|
6539
6540
|
throw new WorkspaceSandboxError(input, root);
|
|
6540
6541
|
}
|
|
6541
6542
|
try {
|
|
6542
|
-
const real =
|
|
6543
|
-
const realRel =
|
|
6544
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
6543
|
+
const real = fs20__default.default.realpathSync(abs);
|
|
6544
|
+
const realRel = path21__default.default.relative(root, real);
|
|
6545
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path21__default.default.isAbsolute(realRel))) {
|
|
6545
6546
|
throw new WorkspaceSandboxError(input, root);
|
|
6546
6547
|
}
|
|
6547
6548
|
} catch (e) {
|
|
@@ -6602,7 +6603,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
6602
6603
|
} catch {
|
|
6603
6604
|
}
|
|
6604
6605
|
}
|
|
6605
|
-
await fs4__default.default.mkdir(
|
|
6606
|
+
await fs4__default.default.mkdir(path21__default.default.dirname(absPath), { recursive: true });
|
|
6606
6607
|
await fs4__default.default.writeFile(absPath, content, "utf-8");
|
|
6607
6608
|
return `Written ${content.length} characters to ${filePath}`;
|
|
6608
6609
|
}
|
|
@@ -7100,7 +7101,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
7100
7101
|
};
|
|
7101
7102
|
async function fileToImageAttachment(filePath) {
|
|
7102
7103
|
const data = await fs4__default.default.readFile(filePath);
|
|
7103
|
-
const ext =
|
|
7104
|
+
const ext = path21__default.default.extname(filePath).toLowerCase();
|
|
7104
7105
|
const mimeMap = {
|
|
7105
7106
|
".jpg": "image/jpeg",
|
|
7106
7107
|
".jpeg": "image/jpeg",
|
|
@@ -7134,14 +7135,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
7134
7135
|
const filePath = input["path"];
|
|
7135
7136
|
const content = input["content"];
|
|
7136
7137
|
const title = input["title"];
|
|
7137
|
-
const dir =
|
|
7138
|
-
if (!
|
|
7139
|
-
|
|
7138
|
+
const dir = path21__default.default.dirname(filePath);
|
|
7139
|
+
if (!fs20__default.default.existsSync(dir)) {
|
|
7140
|
+
fs20__default.default.mkdirSync(dir, { recursive: true });
|
|
7140
7141
|
}
|
|
7141
7142
|
return new Promise((resolve, reject) => {
|
|
7142
7143
|
try {
|
|
7143
7144
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
7144
|
-
const stream =
|
|
7145
|
+
const stream = fs20__default.default.createWriteStream(filePath);
|
|
7145
7146
|
doc.pipe(stream);
|
|
7146
7147
|
if (title) {
|
|
7147
7148
|
doc.info["Title"] = title;
|
|
@@ -7219,22 +7220,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
7219
7220
|
}
|
|
7220
7221
|
cmdPrefix = NODE_CMD;
|
|
7221
7222
|
}
|
|
7222
|
-
const tmpDir =
|
|
7223
|
-
if (!
|
|
7224
|
-
|
|
7223
|
+
const tmpDir = path21__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
7224
|
+
if (!fs20__default.default.existsSync(tmpDir)) {
|
|
7225
|
+
fs20__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
7225
7226
|
}
|
|
7226
7227
|
const extension = language === "python" ? "py" : "js";
|
|
7227
7228
|
const fileName = `intp_${crypto3.randomUUID().slice(0, 8)}.${extension}`;
|
|
7228
|
-
const filePath =
|
|
7229
|
-
|
|
7229
|
+
const filePath = path21__default.default.join(tmpDir, fileName);
|
|
7230
|
+
fs20__default.default.writeFileSync(filePath, code, "utf-8");
|
|
7230
7231
|
const execArgs = [filePath, ...args];
|
|
7231
7232
|
return new Promise((resolve) => {
|
|
7232
7233
|
const startMs = Date.now();
|
|
7233
7234
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
7234
7235
|
const duration = Date.now() - startMs;
|
|
7235
7236
|
try {
|
|
7236
|
-
if (
|
|
7237
|
-
|
|
7237
|
+
if (fs20__default.default.existsSync(filePath)) {
|
|
7238
|
+
fs20__default.default.unlinkSync(filePath);
|
|
7238
7239
|
}
|
|
7239
7240
|
} catch (cleanupErr) {
|
|
7240
7241
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -7513,7 +7514,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7513
7514
|
};
|
|
7514
7515
|
async execute(input, _options) {
|
|
7515
7516
|
const pattern = input["pattern"];
|
|
7516
|
-
const searchPath = input["path"] ?
|
|
7517
|
+
const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7517
7518
|
const matches = await glob.glob(pattern, {
|
|
7518
7519
|
cwd: searchPath,
|
|
7519
7520
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -7526,7 +7527,7 @@ var GlobTool = class extends BaseTool {
|
|
|
7526
7527
|
const withMtime = await Promise.all(
|
|
7527
7528
|
matches.map(async (rel) => {
|
|
7528
7529
|
try {
|
|
7529
|
-
const stat = await fs4__default.default.stat(
|
|
7530
|
+
const stat = await fs4__default.default.stat(path21__default.default.join(searchPath, rel));
|
|
7530
7531
|
return { rel, mtime: stat.mtimeMs };
|
|
7531
7532
|
} catch {
|
|
7532
7533
|
return { rel, mtime: 0 };
|
|
@@ -7575,7 +7576,7 @@ var GrepTool = class extends BaseTool {
|
|
|
7575
7576
|
};
|
|
7576
7577
|
async execute(input, _options) {
|
|
7577
7578
|
const pattern = input["pattern"];
|
|
7578
|
-
const searchPath = input["path"] ?
|
|
7579
|
+
const searchPath = input["path"] ? path21__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
7579
7580
|
const globPattern = input["glob"];
|
|
7580
7581
|
const outputMode = input["output_mode"] ?? "content";
|
|
7581
7582
|
const context = input["context"] ?? 0;
|
|
@@ -7629,12 +7630,12 @@ var GrepTool = class extends BaseTool {
|
|
|
7629
7630
|
nodir: true
|
|
7630
7631
|
});
|
|
7631
7632
|
} catch {
|
|
7632
|
-
files = [
|
|
7633
|
+
files = [path21__default.default.relative(searchPath, searchPath) || "."];
|
|
7633
7634
|
}
|
|
7634
7635
|
const results = [];
|
|
7635
7636
|
let totalCount = 0;
|
|
7636
7637
|
for (const rel of files) {
|
|
7637
|
-
const abs =
|
|
7638
|
+
const abs = path21__default.default.join(searchPath, rel);
|
|
7638
7639
|
let content;
|
|
7639
7640
|
try {
|
|
7640
7641
|
content = await fs4__default.default.readFile(abs, "utf-8");
|
|
@@ -7996,10 +7997,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
7996
7997
|
}
|
|
7997
7998
|
isIgnored(filePath) {
|
|
7998
7999
|
if (!filePath) return false;
|
|
7999
|
-
const abs =
|
|
8000
|
-
const rel =
|
|
8001
|
-
if (!rel || rel.startsWith("..") ||
|
|
8002
|
-
const posixRel = rel.split(
|
|
8000
|
+
const abs = path21__default.default.resolve(this.workspaceRoot, filePath);
|
|
8001
|
+
const rel = path21__default.default.relative(this.workspaceRoot, abs);
|
|
8002
|
+
if (!rel || rel.startsWith("..") || path21__default.default.isAbsolute(rel)) return true;
|
|
8003
|
+
const posixRel = rel.split(path21__default.default.sep).join("/");
|
|
8003
8004
|
return this.ignoreMatcher.ignores(posixRel);
|
|
8004
8005
|
}
|
|
8005
8006
|
};
|
|
@@ -8833,7 +8834,7 @@ var TaskAnalyzer = class {
|
|
|
8833
8834
|
analysisCache.clear();
|
|
8834
8835
|
}
|
|
8835
8836
|
};
|
|
8836
|
-
var DEFAULT_STATS_FILE =
|
|
8837
|
+
var DEFAULT_STATS_FILE = path21__default.default.join(os6__default.default.homedir(), ".cascade", "model-perf.json");
|
|
8837
8838
|
var ModelPerformanceTracker = class {
|
|
8838
8839
|
stats = /* @__PURE__ */ new Map();
|
|
8839
8840
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -8865,7 +8866,7 @@ var ModelPerformanceTracker = class {
|
|
|
8865
8866
|
}
|
|
8866
8867
|
async save() {
|
|
8867
8868
|
try {
|
|
8868
|
-
await fs4__default.default.mkdir(
|
|
8869
|
+
await fs4__default.default.mkdir(path21__default.default.dirname(this.statsFile), { recursive: true });
|
|
8869
8870
|
const modelsObj = {};
|
|
8870
8871
|
const featuresObj = {};
|
|
8871
8872
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
@@ -9404,7 +9405,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9404
9405
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
9405
9406
|
async loadPersistedTools() {
|
|
9406
9407
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9407
|
-
const file =
|
|
9408
|
+
const file = path21__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
9408
9409
|
try {
|
|
9409
9410
|
const raw = await fs4__default.default.readFile(file, "utf-8");
|
|
9410
9411
|
const specs = JSON.parse(raw);
|
|
@@ -9428,8 +9429,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
9428
9429
|
}
|
|
9429
9430
|
async persist() {
|
|
9430
9431
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
9431
|
-
const dir =
|
|
9432
|
-
const file =
|
|
9432
|
+
const dir = path21__default.default.join(this.workspacePath, ".cascade");
|
|
9433
|
+
const file = path21__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
9433
9434
|
try {
|
|
9434
9435
|
await fs4__default.default.mkdir(dir, { recursive: true });
|
|
9435
9436
|
await fs4__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -9449,12 +9450,12 @@ var WorldStateDB = class {
|
|
|
9449
9450
|
constructor(workspacePath, debugMode = false) {
|
|
9450
9451
|
this.workspacePath = workspacePath;
|
|
9451
9452
|
this.debugMode = debugMode;
|
|
9452
|
-
const cascadeDir =
|
|
9453
|
-
if (!
|
|
9454
|
-
|
|
9453
|
+
const cascadeDir = path21__default.default.join(workspacePath, ".cascade");
|
|
9454
|
+
if (!fs20__default.default.existsSync(cascadeDir)) {
|
|
9455
|
+
fs20__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
9455
9456
|
}
|
|
9456
|
-
this.keyPath =
|
|
9457
|
-
this.dbPath =
|
|
9457
|
+
this.keyPath = path21__default.default.join(cascadeDir, "world_state.key");
|
|
9458
|
+
this.dbPath = path21__default.default.join(cascadeDir, "world_state.db");
|
|
9458
9459
|
this.initEncryptionKey();
|
|
9459
9460
|
this.db = new Database__default.default(this.dbPath);
|
|
9460
9461
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -9484,11 +9485,11 @@ var WorldStateDB = class {
|
|
|
9484
9485
|
dbPath;
|
|
9485
9486
|
encryptionKey;
|
|
9486
9487
|
initEncryptionKey() {
|
|
9487
|
-
if (
|
|
9488
|
-
this.encryptionKey =
|
|
9488
|
+
if (fs20__default.default.existsSync(this.keyPath)) {
|
|
9489
|
+
this.encryptionKey = fs20__default.default.readFileSync(this.keyPath);
|
|
9489
9490
|
} else {
|
|
9490
9491
|
this.encryptionKey = crypto3__default.default.randomBytes(32);
|
|
9491
|
-
|
|
9492
|
+
fs20__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
9492
9493
|
}
|
|
9493
9494
|
}
|
|
9494
9495
|
encrypt(text) {
|
|
@@ -9629,6 +9630,22 @@ var WorldStateDB = class {
|
|
|
9629
9630
|
}
|
|
9630
9631
|
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
9631
9632
|
}
|
|
9633
|
+
/**
|
|
9634
|
+
* Delete one fact by its (normalized) entity + relation key. Returns whether
|
|
9635
|
+
* a row was actually removed. Powers the desktop Knowledge tab's per-fact
|
|
9636
|
+
* delete — users can prune what the planner remembers about their project.
|
|
9637
|
+
*/
|
|
9638
|
+
deleteFact(entity, relation) {
|
|
9639
|
+
const e = normalizeKey(entity);
|
|
9640
|
+
const r = normalizeKey(relation);
|
|
9641
|
+
if (!e || !r) return false;
|
|
9642
|
+
const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
|
|
9643
|
+
return result.changes > 0;
|
|
9644
|
+
}
|
|
9645
|
+
/** Delete every fact. Returns how many were removed. */
|
|
9646
|
+
clearFacts() {
|
|
9647
|
+
return this.db.prepare("DELETE FROM facts").run().changes;
|
|
9648
|
+
}
|
|
9632
9649
|
// ── Export / Import ──────────────────────────
|
|
9633
9650
|
//
|
|
9634
9651
|
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
@@ -9677,9 +9694,9 @@ var WorldStateDB = class {
|
|
|
9677
9694
|
dumpDebugIfNeeded() {
|
|
9678
9695
|
if (!this.debugMode) return;
|
|
9679
9696
|
try {
|
|
9680
|
-
const dumpPath =
|
|
9697
|
+
const dumpPath = path21__default.default.join(os6__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
9681
9698
|
const entries = this.getAllEntries();
|
|
9682
|
-
|
|
9699
|
+
fs20__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
9683
9700
|
} catch (err) {
|
|
9684
9701
|
console.error("Failed to dump debug world state", err);
|
|
9685
9702
|
}
|
|
@@ -10589,7 +10606,7 @@ var Keystore = class {
|
|
|
10589
10606
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
10590
10607
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
10591
10608
|
this.backend = "keytar";
|
|
10592
|
-
if (password &&
|
|
10609
|
+
if (password && fs20__default.default.existsSync(this.storePath)) {
|
|
10593
10610
|
try {
|
|
10594
10611
|
const fileEntries = this.decryptFile(password);
|
|
10595
10612
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -10608,7 +10625,7 @@ var Keystore = class {
|
|
|
10608
10625
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
10609
10626
|
);
|
|
10610
10627
|
}
|
|
10611
|
-
if (!
|
|
10628
|
+
if (!fs20__default.default.existsSync(this.storePath)) {
|
|
10612
10629
|
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
10613
10630
|
this.masterKey = this.deriveKey(password, salt);
|
|
10614
10631
|
this.writeWithSalt({}, salt);
|
|
@@ -10622,7 +10639,7 @@ var Keystore = class {
|
|
|
10622
10639
|
}
|
|
10623
10640
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
10624
10641
|
unlockSync(password) {
|
|
10625
|
-
if (!
|
|
10642
|
+
if (!fs20__default.default.existsSync(this.storePath)) {
|
|
10626
10643
|
const salt = crypto3__default.default.randomBytes(SALT_LEN);
|
|
10627
10644
|
this.masterKey = this.deriveKey(password, salt);
|
|
10628
10645
|
this.writeWithSalt({}, salt);
|
|
@@ -10680,7 +10697,7 @@ var Keystore = class {
|
|
|
10680
10697
|
}
|
|
10681
10698
|
}
|
|
10682
10699
|
decryptFile(password, knownSalt) {
|
|
10683
|
-
if (!
|
|
10700
|
+
if (!fs20__default.default.existsSync(this.storePath)) return {};
|
|
10684
10701
|
try {
|
|
10685
10702
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
10686
10703
|
const useSalt = knownSalt ?? salt;
|
|
@@ -10702,8 +10719,8 @@ var Keystore = class {
|
|
|
10702
10719
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10703
10720
|
const tag = cipher.getAuthTag();
|
|
10704
10721
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
10705
|
-
|
|
10706
|
-
|
|
10722
|
+
fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
|
|
10723
|
+
fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10707
10724
|
}
|
|
10708
10725
|
writeWithSalt(data, salt) {
|
|
10709
10726
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -10713,11 +10730,11 @@ var Keystore = class {
|
|
|
10713
10730
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
10714
10731
|
const tag = cipher.getAuthTag();
|
|
10715
10732
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
10716
|
-
|
|
10717
|
-
|
|
10733
|
+
fs20__default.default.mkdirSync(path21__default.default.dirname(this.storePath), { recursive: true });
|
|
10734
|
+
fs20__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
10718
10735
|
}
|
|
10719
10736
|
readRaw() {
|
|
10720
|
-
const buf =
|
|
10737
|
+
const buf = fs20__default.default.readFileSync(this.storePath);
|
|
10721
10738
|
let offset = 0;
|
|
10722
10739
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
10723
10740
|
offset += SALT_LEN;
|
|
@@ -10750,7 +10767,7 @@ var CascadeIgnore = class {
|
|
|
10750
10767
|
]);
|
|
10751
10768
|
}
|
|
10752
10769
|
async load(workspacePath) {
|
|
10753
|
-
const filePath =
|
|
10770
|
+
const filePath = path21__default.default.join(workspacePath, ".cascadeignore");
|
|
10754
10771
|
try {
|
|
10755
10772
|
const content = await fs4__default.default.readFile(filePath, "utf-8");
|
|
10756
10773
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -10761,7 +10778,7 @@ var CascadeIgnore = class {
|
|
|
10761
10778
|
}
|
|
10762
10779
|
isIgnored(filePath, workspacePath) {
|
|
10763
10780
|
try {
|
|
10764
|
-
const relative = workspacePath ?
|
|
10781
|
+
const relative = workspacePath ? path21__default.default.relative(workspacePath, filePath) : filePath;
|
|
10765
10782
|
return this.ig.ignores(relative);
|
|
10766
10783
|
} catch {
|
|
10767
10784
|
return false;
|
|
@@ -10772,7 +10789,7 @@ var CascadeIgnore = class {
|
|
|
10772
10789
|
}
|
|
10773
10790
|
};
|
|
10774
10791
|
async function loadCascadeMd(workspacePath) {
|
|
10775
|
-
const filePath =
|
|
10792
|
+
const filePath = path21__default.default.join(workspacePath, "CASCADE.md");
|
|
10776
10793
|
try {
|
|
10777
10794
|
const raw = await fs4__default.default.readFile(filePath, "utf-8");
|
|
10778
10795
|
return parseCascadeMd(raw);
|
|
@@ -10803,7 +10820,7 @@ ${raw.trim()}`;
|
|
|
10803
10820
|
var MemoryStore = class _MemoryStore {
|
|
10804
10821
|
db;
|
|
10805
10822
|
constructor(dbPath) {
|
|
10806
|
-
|
|
10823
|
+
fs20__default.default.mkdirSync(path21__default.default.dirname(dbPath), { recursive: true });
|
|
10807
10824
|
try {
|
|
10808
10825
|
this.db = new Database__default.default(dbPath, { timeout: 5e3 });
|
|
10809
10826
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -11638,6 +11655,60 @@ Original error: ${err.message}`
|
|
|
11638
11655
|
};
|
|
11639
11656
|
}
|
|
11640
11657
|
};
|
|
11658
|
+
function credentialsPath(globalDir) {
|
|
11659
|
+
return path21__default.default.join(globalDir, GLOBAL_CREDENTIALS_FILE);
|
|
11660
|
+
}
|
|
11661
|
+
function providerKey(p) {
|
|
11662
|
+
if (p.type === "azure") {
|
|
11663
|
+
return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
|
|
11664
|
+
}
|
|
11665
|
+
return p.type;
|
|
11666
|
+
}
|
|
11667
|
+
function isPersistable(p) {
|
|
11668
|
+
return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
|
|
11669
|
+
}
|
|
11670
|
+
function loadGlobalCredentials(globalDir) {
|
|
11671
|
+
try {
|
|
11672
|
+
const raw = fs20__default.default.readFileSync(credentialsPath(globalDir), "utf-8");
|
|
11673
|
+
const parsed = JSON.parse(raw);
|
|
11674
|
+
if (!Array.isArray(parsed.providers)) return [];
|
|
11675
|
+
return parsed.providers.filter(
|
|
11676
|
+
(p) => Boolean(p) && typeof p.type === "string"
|
|
11677
|
+
);
|
|
11678
|
+
} catch {
|
|
11679
|
+
return [];
|
|
11680
|
+
}
|
|
11681
|
+
}
|
|
11682
|
+
function saveGlobalCredentials(globalDir, providers) {
|
|
11683
|
+
const filePath = credentialsPath(globalDir);
|
|
11684
|
+
fs20__default.default.mkdirSync(globalDir, { recursive: true, mode: 448 });
|
|
11685
|
+
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
11686
|
+
const tmp = `${filePath}.tmp`;
|
|
11687
|
+
fs20__default.default.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
11688
|
+
fs20__default.default.renameSync(tmp, filePath);
|
|
11689
|
+
try {
|
|
11690
|
+
fs20__default.default.chmodSync(filePath, 384);
|
|
11691
|
+
} catch {
|
|
11692
|
+
}
|
|
11693
|
+
}
|
|
11694
|
+
function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
11695
|
+
const merged = [...workspaceProviders];
|
|
11696
|
+
const byKey = new Map(merged.map((p) => [providerKey(p), p]));
|
|
11697
|
+
for (const g of globalProviders) {
|
|
11698
|
+
const existing = byKey.get(providerKey(g));
|
|
11699
|
+
if (!existing) {
|
|
11700
|
+
merged.push({ ...g });
|
|
11701
|
+
byKey.set(providerKey(g), merged[merged.length - 1]);
|
|
11702
|
+
continue;
|
|
11703
|
+
}
|
|
11704
|
+
if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
|
|
11705
|
+
if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
|
|
11706
|
+
if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
|
|
11707
|
+
if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
|
|
11708
|
+
if (!existing.label && g.label) existing.label = g.label;
|
|
11709
|
+
}
|
|
11710
|
+
return merged;
|
|
11711
|
+
}
|
|
11641
11712
|
|
|
11642
11713
|
// src/config/index.ts
|
|
11643
11714
|
var ConfigManager = class {
|
|
@@ -11648,18 +11719,20 @@ var ConfigManager = class {
|
|
|
11648
11719
|
cascadeMd = null;
|
|
11649
11720
|
workspacePath;
|
|
11650
11721
|
globalDir;
|
|
11651
|
-
|
|
11722
|
+
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
11723
|
+
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
11652
11724
|
this.workspacePath = workspacePath;
|
|
11653
|
-
this.globalDir =
|
|
11725
|
+
this.globalDir = globalDirOverride ?? path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
11654
11726
|
}
|
|
11655
11727
|
async load() {
|
|
11656
11728
|
this.config = await this.loadConfig();
|
|
11657
11729
|
this.ignore = new CascadeIgnore();
|
|
11658
11730
|
await this.ignore.load(this.workspacePath);
|
|
11659
11731
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
11660
|
-
this.keystore = new Keystore(
|
|
11661
|
-
this.store = new MemoryStore(
|
|
11732
|
+
this.keystore = new Keystore(path21__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
11733
|
+
this.store = new MemoryStore(path21__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
11662
11734
|
await this.injectEnvKeys();
|
|
11735
|
+
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
11663
11736
|
await this.ensureDefaultIdentity();
|
|
11664
11737
|
}
|
|
11665
11738
|
getConfig() {
|
|
@@ -11681,9 +11754,14 @@ var ConfigManager = class {
|
|
|
11681
11754
|
return this.workspacePath;
|
|
11682
11755
|
}
|
|
11683
11756
|
async save() {
|
|
11684
|
-
const configPath =
|
|
11685
|
-
await fs4__default.default.mkdir(
|
|
11757
|
+
const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11758
|
+
await fs4__default.default.mkdir(path21__default.default.dirname(configPath), { recursive: true });
|
|
11686
11759
|
await fs4__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
11760
|
+
try {
|
|
11761
|
+
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
11762
|
+
} catch (err) {
|
|
11763
|
+
console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
|
|
11764
|
+
}
|
|
11687
11765
|
}
|
|
11688
11766
|
async updateConfig(updates) {
|
|
11689
11767
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -11706,7 +11784,7 @@ var ConfigManager = class {
|
|
|
11706
11784
|
return configProvider?.apiKey;
|
|
11707
11785
|
}
|
|
11708
11786
|
async loadConfig() {
|
|
11709
|
-
const configPath =
|
|
11787
|
+
const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
11710
11788
|
try {
|
|
11711
11789
|
const raw = await fs4__default.default.readFile(configPath, "utf-8");
|
|
11712
11790
|
return validateConfig(JSON.parse(raw));
|
|
@@ -11813,6 +11891,11 @@ function authMiddleware(secret, required = true) {
|
|
|
11813
11891
|
}
|
|
11814
11892
|
const user = verifyToken(token, secret);
|
|
11815
11893
|
if (!user) {
|
|
11894
|
+
if (!required) {
|
|
11895
|
+
req.user = void 0;
|
|
11896
|
+
next();
|
|
11897
|
+
return;
|
|
11898
|
+
}
|
|
11816
11899
|
res.status(401).json({ error: "Invalid or expired token" });
|
|
11817
11900
|
return;
|
|
11818
11901
|
}
|
|
@@ -11945,6 +12028,25 @@ var DashboardSocket = class {
|
|
|
11945
12028
|
});
|
|
11946
12029
|
});
|
|
11947
12030
|
}
|
|
12031
|
+
/**
|
|
12032
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
12033
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
12034
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
12035
|
+
*/
|
|
12036
|
+
onPlanDecision(callback) {
|
|
12037
|
+
this.io.on("connection", (socket) => {
|
|
12038
|
+
socket.on("plan:decision", (payload) => {
|
|
12039
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
12040
|
+
callback({
|
|
12041
|
+
sessionId: payload.sessionId,
|
|
12042
|
+
approved: payload.approved,
|
|
12043
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
12044
|
+
editedPlan: payload.editedPlan
|
|
12045
|
+
});
|
|
12046
|
+
}
|
|
12047
|
+
});
|
|
12048
|
+
});
|
|
12049
|
+
}
|
|
11948
12050
|
onSessionSteer(callback) {
|
|
11949
12051
|
this.io.on("connection", (socket) => {
|
|
11950
12052
|
socket.on("session:steer", (payload) => {
|
|
@@ -11986,7 +12088,123 @@ var DashboardSocket = class {
|
|
|
11986
12088
|
this.io.close();
|
|
11987
12089
|
}
|
|
11988
12090
|
};
|
|
11989
|
-
var
|
|
12091
|
+
var TaskScheduler = class {
|
|
12092
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
12093
|
+
store;
|
|
12094
|
+
runner;
|
|
12095
|
+
constructor(store, runner) {
|
|
12096
|
+
this.store = store;
|
|
12097
|
+
this.runner = runner;
|
|
12098
|
+
}
|
|
12099
|
+
start() {
|
|
12100
|
+
const tasks = this.store.listScheduledTasks();
|
|
12101
|
+
for (const task of tasks) {
|
|
12102
|
+
if (task.enabled) this.schedule(task);
|
|
12103
|
+
}
|
|
12104
|
+
}
|
|
12105
|
+
stop() {
|
|
12106
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
12107
|
+
this.cronJobs.clear();
|
|
12108
|
+
}
|
|
12109
|
+
schedule(task) {
|
|
12110
|
+
if (!cron__default.default.validate(task.cronExpression)) {
|
|
12111
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12112
|
+
}
|
|
12113
|
+
const existing = this.cronJobs.get(task.id);
|
|
12114
|
+
if (existing) {
|
|
12115
|
+
try {
|
|
12116
|
+
existing.stop();
|
|
12117
|
+
} catch {
|
|
12118
|
+
}
|
|
12119
|
+
}
|
|
12120
|
+
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
12121
|
+
try {
|
|
12122
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12123
|
+
this.store.saveScheduledTask(task);
|
|
12124
|
+
await this.runner(task);
|
|
12125
|
+
} catch (err) {
|
|
12126
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12127
|
+
}
|
|
12128
|
+
}, { timezone: "UTC" });
|
|
12129
|
+
this.cronJobs.set(task.id, job);
|
|
12130
|
+
}
|
|
12131
|
+
unschedule(taskId) {
|
|
12132
|
+
this.cronJobs.get(taskId)?.stop();
|
|
12133
|
+
this.cronJobs.delete(taskId);
|
|
12134
|
+
}
|
|
12135
|
+
add(task) {
|
|
12136
|
+
this.store.saveScheduledTask(task);
|
|
12137
|
+
if (task.enabled) this.schedule(task);
|
|
12138
|
+
}
|
|
12139
|
+
remove(taskId) {
|
|
12140
|
+
this.unschedule(taskId);
|
|
12141
|
+
this.store.deleteScheduledTask(taskId);
|
|
12142
|
+
}
|
|
12143
|
+
list() {
|
|
12144
|
+
return this.store.listScheduledTasks();
|
|
12145
|
+
}
|
|
12146
|
+
isRunning(taskId) {
|
|
12147
|
+
return this.cronJobs.has(taskId);
|
|
12148
|
+
}
|
|
12149
|
+
static validateCron(expression) {
|
|
12150
|
+
return cron__default.default.validate(expression);
|
|
12151
|
+
}
|
|
12152
|
+
};
|
|
12153
|
+
|
|
12154
|
+
// src/dashboard/cost-stats.ts
|
|
12155
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
12156
|
+
function utcDateKey(d) {
|
|
12157
|
+
return d.toISOString().slice(0, 10);
|
|
12158
|
+
}
|
|
12159
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
12160
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
12161
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
12162
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
12163
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
12164
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
12165
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
12166
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
12167
|
+
}
|
|
12168
|
+
let totalCostUsd = 0;
|
|
12169
|
+
let totalTokens = 0;
|
|
12170
|
+
let totalRuns = 0;
|
|
12171
|
+
for (const s of sessions) {
|
|
12172
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
12173
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
12174
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
12175
|
+
totalCostUsd += cost;
|
|
12176
|
+
totalTokens += tokens;
|
|
12177
|
+
totalRuns += runs;
|
|
12178
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
12179
|
+
if (!Number.isNaN(when.getTime())) {
|
|
12180
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
12181
|
+
if (bucket) {
|
|
12182
|
+
bucket.costUsd += cost;
|
|
12183
|
+
bucket.tokens += tokens;
|
|
12184
|
+
bucket.runs += runs;
|
|
12185
|
+
}
|
|
12186
|
+
}
|
|
12187
|
+
}
|
|
12188
|
+
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) => ({
|
|
12189
|
+
sessionId: s.id,
|
|
12190
|
+
title: s.title,
|
|
12191
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
12192
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
12193
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
12194
|
+
updatedAt: s.updatedAt
|
|
12195
|
+
}));
|
|
12196
|
+
return {
|
|
12197
|
+
totalCostUsd,
|
|
12198
|
+
totalTokens,
|
|
12199
|
+
totalSessions: sessions.length,
|
|
12200
|
+
totalRuns,
|
|
12201
|
+
perDay: [...buckets.values()],
|
|
12202
|
+
topSessions
|
|
12203
|
+
};
|
|
12204
|
+
}
|
|
12205
|
+
|
|
12206
|
+
// src/dashboard/server.ts
|
|
12207
|
+
var __dirname$1 = path21__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
|
|
11990
12208
|
var DashboardServer = class {
|
|
11991
12209
|
app;
|
|
11992
12210
|
httpServer;
|
|
@@ -12011,6 +12229,14 @@ var DashboardServer = class {
|
|
|
12011
12229
|
* map is how that answer reaches the run that's blocked on it.
|
|
12012
12230
|
*/
|
|
12013
12231
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
12232
|
+
/**
|
|
12233
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
12234
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
12235
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
12236
|
+
*/
|
|
12237
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
12238
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
12239
|
+
scheduler;
|
|
12014
12240
|
port;
|
|
12015
12241
|
host;
|
|
12016
12242
|
workspacePath;
|
|
@@ -12028,6 +12254,7 @@ var DashboardServer = class {
|
|
|
12028
12254
|
secret: this.dashboardSecret,
|
|
12029
12255
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
12030
12256
|
});
|
|
12257
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
12031
12258
|
this.setupMiddleware();
|
|
12032
12259
|
this.setupRoutes();
|
|
12033
12260
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -12091,6 +12318,9 @@ var DashboardServer = class {
|
|
|
12091
12318
|
cascade.on("peer:message", (e) => {
|
|
12092
12319
|
this.socket.emitPeerMessage(e);
|
|
12093
12320
|
});
|
|
12321
|
+
cascade.on("plan:approval-required", (e) => {
|
|
12322
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
12323
|
+
});
|
|
12094
12324
|
try {
|
|
12095
12325
|
const result = await cascade.run({
|
|
12096
12326
|
prompt: runPrompt,
|
|
@@ -12098,7 +12328,8 @@ var DashboardServer = class {
|
|
|
12098
12328
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12099
12329
|
});
|
|
12100
12330
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12101
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
12331
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12332
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12102
12333
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
12103
12334
|
this.socket.broadcast("cost:update", {
|
|
12104
12335
|
sessionId,
|
|
@@ -12108,6 +12339,7 @@ var DashboardServer = class {
|
|
|
12108
12339
|
this.throttledBroadcast("workspace");
|
|
12109
12340
|
} catch (err) {
|
|
12110
12341
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12342
|
+
this.captureWhy(sessionId, cascade);
|
|
12111
12343
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
12112
12344
|
sessionId,
|
|
12113
12345
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12118,9 +12350,17 @@ var DashboardServer = class {
|
|
|
12118
12350
|
this.denyPendingApprovals(sessionId);
|
|
12119
12351
|
}
|
|
12120
12352
|
});
|
|
12353
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
12354
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
12355
|
+
approved,
|
|
12356
|
+
note,
|
|
12357
|
+
editedPlan
|
|
12358
|
+
);
|
|
12359
|
+
});
|
|
12121
12360
|
this.socket.onSessionHalt((sessionId) => {
|
|
12122
12361
|
this.activeControllers.get(sessionId)?.abort();
|
|
12123
12362
|
this.denyPendingApprovals(sessionId);
|
|
12363
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
12124
12364
|
});
|
|
12125
12365
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
12126
12366
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -12153,12 +12393,21 @@ var DashboardServer = class {
|
|
|
12153
12393
|
resolve();
|
|
12154
12394
|
});
|
|
12155
12395
|
});
|
|
12396
|
+
try {
|
|
12397
|
+
this.scheduler.start();
|
|
12398
|
+
} catch (err) {
|
|
12399
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
12400
|
+
}
|
|
12156
12401
|
}
|
|
12157
12402
|
async stop() {
|
|
12158
12403
|
if (this.broadcastTimer) {
|
|
12159
12404
|
clearTimeout(this.broadcastTimer);
|
|
12160
12405
|
this.broadcastTimer = null;
|
|
12161
12406
|
}
|
|
12407
|
+
try {
|
|
12408
|
+
this.scheduler.stop();
|
|
12409
|
+
} catch {
|
|
12410
|
+
}
|
|
12162
12411
|
this.socket.close();
|
|
12163
12412
|
try {
|
|
12164
12413
|
this.globalStore?.close();
|
|
@@ -12189,12 +12438,17 @@ var DashboardServer = class {
|
|
|
12189
12438
|
*/
|
|
12190
12439
|
persistConfig() {
|
|
12191
12440
|
try {
|
|
12192
|
-
const configPath =
|
|
12193
|
-
|
|
12194
|
-
|
|
12441
|
+
const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
12442
|
+
fs20__default.default.mkdirSync(path21__default.default.dirname(configPath), { recursive: true });
|
|
12443
|
+
fs20__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
12195
12444
|
} catch (err) {
|
|
12196
12445
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
12197
12446
|
}
|
|
12447
|
+
try {
|
|
12448
|
+
saveGlobalCredentials(path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
12449
|
+
} catch (err) {
|
|
12450
|
+
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
12451
|
+
}
|
|
12198
12452
|
}
|
|
12199
12453
|
/**
|
|
12200
12454
|
* Produce a stable dashboard JWT signing secret.
|
|
@@ -12206,15 +12460,15 @@ var DashboardServer = class {
|
|
|
12206
12460
|
resolveDashboardSecret() {
|
|
12207
12461
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
12208
12462
|
if (fromConfig) return fromConfig;
|
|
12209
|
-
const secretPath =
|
|
12463
|
+
const secretPath = path21__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
12210
12464
|
try {
|
|
12211
|
-
if (
|
|
12212
|
-
const existing =
|
|
12465
|
+
if (fs20__default.default.existsSync(secretPath)) {
|
|
12466
|
+
const existing = fs20__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
12213
12467
|
if (existing.length >= 16) return existing;
|
|
12214
12468
|
}
|
|
12215
12469
|
const generated = crypto3.randomUUID();
|
|
12216
|
-
|
|
12217
|
-
|
|
12470
|
+
fs20__default.default.mkdirSync(path21__default.default.dirname(secretPath), { recursive: true });
|
|
12471
|
+
fs20__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
12218
12472
|
if (this.config.dashboard.auth) {
|
|
12219
12473
|
console.warn(
|
|
12220
12474
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -12241,7 +12495,7 @@ var DashboardServer = class {
|
|
|
12241
12495
|
// ── Setup ─────────────────────────────────────
|
|
12242
12496
|
getGlobalStore() {
|
|
12243
12497
|
if (!this.globalStore) {
|
|
12244
|
-
const globalDbPath =
|
|
12498
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12245
12499
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
12246
12500
|
}
|
|
12247
12501
|
return this.globalStore;
|
|
@@ -12279,16 +12533,60 @@ var DashboardServer = class {
|
|
|
12279
12533
|
return title;
|
|
12280
12534
|
}
|
|
12281
12535
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
12282
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
12536
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
12283
12537
|
try {
|
|
12284
12538
|
if (reply && reply.trim()) {
|
|
12285
12539
|
this.store.addMessage({ id: crypto3.randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
12286
12540
|
}
|
|
12541
|
+
if (result) {
|
|
12542
|
+
const session = this.store.getSession(sessionId);
|
|
12543
|
+
if (session) {
|
|
12544
|
+
this.store.updateSession(sessionId, {
|
|
12545
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12546
|
+
metadata: {
|
|
12547
|
+
...session.metadata,
|
|
12548
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
12549
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
12550
|
+
taskCount: session.metadata.taskCount + 1
|
|
12551
|
+
}
|
|
12552
|
+
});
|
|
12553
|
+
}
|
|
12554
|
+
}
|
|
12287
12555
|
} catch (err) {
|
|
12288
12556
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
12289
12557
|
}
|
|
12290
12558
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
12291
12559
|
}
|
|
12560
|
+
/**
|
|
12561
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
12562
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
12563
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
12564
|
+
*/
|
|
12565
|
+
captureWhy(sessionId, cascade, result) {
|
|
12566
|
+
try {
|
|
12567
|
+
const stats = cascade.getRouter().getStats();
|
|
12568
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
12569
|
+
const report = {
|
|
12570
|
+
sessionId,
|
|
12571
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12572
|
+
decisions: cascade.getDecisionLog(),
|
|
12573
|
+
savedUsd: savings.savedUsd,
|
|
12574
|
+
savedPct: savings.savedPct,
|
|
12575
|
+
totalCostUsd: stats.totalCostUsd,
|
|
12576
|
+
totalTokens: stats.totalTokens,
|
|
12577
|
+
costByTier: stats.costByTier,
|
|
12578
|
+
durationMs: result?.durationMs
|
|
12579
|
+
};
|
|
12580
|
+
this.whyBySession.set(sessionId, report);
|
|
12581
|
+
if (this.whyBySession.size > 50) {
|
|
12582
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
12583
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
12584
|
+
}
|
|
12585
|
+
this.socket.broadcast("run:why", report);
|
|
12586
|
+
} catch (err) {
|
|
12587
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
12588
|
+
}
|
|
12589
|
+
}
|
|
12292
12590
|
/**
|
|
12293
12591
|
* Route steering text into running Cascade instances. Targets the given
|
|
12294
12592
|
* session, or every active session when none is specified (the desktop
|
|
@@ -12318,6 +12616,52 @@ var DashboardServer = class {
|
|
|
12318
12616
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
12319
12617
|
});
|
|
12320
12618
|
}
|
|
12619
|
+
/**
|
|
12620
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
12621
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
12622
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
12623
|
+
* client so an open desktop sees the run appear live.
|
|
12624
|
+
*/
|
|
12625
|
+
async runScheduledTask(task) {
|
|
12626
|
+
const sessionId = crypto3.randomUUID();
|
|
12627
|
+
const prompt = task.prompt;
|
|
12628
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
12629
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
12630
|
+
this.activeSessions.set(sessionId, cascade);
|
|
12631
|
+
cascade.on("tier:status", (e) => {
|
|
12632
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
12633
|
+
});
|
|
12634
|
+
cascade.on("peer:message", (e) => {
|
|
12635
|
+
this.socket.emitPeerMessage(e);
|
|
12636
|
+
});
|
|
12637
|
+
try {
|
|
12638
|
+
const result = await cascade.run({
|
|
12639
|
+
prompt,
|
|
12640
|
+
identityId: task.identityId,
|
|
12641
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
12642
|
+
});
|
|
12643
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
12644
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
12645
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12646
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
12647
|
+
this.socket.broadcast("cost:update", {
|
|
12648
|
+
sessionId,
|
|
12649
|
+
totalTokens: result.usage.totalTokens,
|
|
12650
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
12651
|
+
});
|
|
12652
|
+
this.throttledBroadcast("workspace");
|
|
12653
|
+
} catch (err) {
|
|
12654
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
12655
|
+
this.captureWhy(sessionId, cascade);
|
|
12656
|
+
this.socket.broadcast("session:error", {
|
|
12657
|
+
sessionId,
|
|
12658
|
+
error: err instanceof Error ? err.message : String(err)
|
|
12659
|
+
});
|
|
12660
|
+
throw err;
|
|
12661
|
+
} finally {
|
|
12662
|
+
this.activeSessions.delete(sessionId);
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12321
12665
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
12322
12666
|
denyPendingApprovals(sessionId) {
|
|
12323
12667
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -12420,12 +12764,12 @@ ${prompt}`;
|
|
|
12420
12764
|
}
|
|
12421
12765
|
}
|
|
12422
12766
|
watchRuntimeChanges() {
|
|
12423
|
-
const workspaceDbPath =
|
|
12424
|
-
const globalDbPath =
|
|
12767
|
+
const workspaceDbPath = path21__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
12768
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12425
12769
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
12426
12770
|
for (const watchPath of watchPaths) {
|
|
12427
|
-
if (!
|
|
12428
|
-
|
|
12771
|
+
if (!fs20__default.default.existsSync(watchPath)) continue;
|
|
12772
|
+
fs20__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
12429
12773
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
12430
12774
|
});
|
|
12431
12775
|
}
|
|
@@ -12547,7 +12891,7 @@ ${prompt}`;
|
|
|
12547
12891
|
const sessionId = req.params.id;
|
|
12548
12892
|
this.store.deleteSession(sessionId);
|
|
12549
12893
|
this.store.deleteRuntimeSession(sessionId);
|
|
12550
|
-
const globalDbPath =
|
|
12894
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12551
12895
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12552
12896
|
try {
|
|
12553
12897
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -12640,7 +12984,7 @@ ${prompt}`;
|
|
|
12640
12984
|
});
|
|
12641
12985
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
12642
12986
|
const body = req.body;
|
|
12643
|
-
const globalDbPath =
|
|
12987
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12644
12988
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
12645
12989
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12646
12990
|
try {
|
|
@@ -12663,7 +13007,7 @@ ${prompt}`;
|
|
|
12663
13007
|
});
|
|
12664
13008
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
12665
13009
|
this.store.deleteAllRuntimeNodes();
|
|
12666
|
-
const globalDbPath =
|
|
13010
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12667
13011
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12668
13012
|
try {
|
|
12669
13013
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -12749,12 +13093,12 @@ ${prompt}`;
|
|
|
12749
13093
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
12750
13094
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
12751
13095
|
try {
|
|
12752
|
-
const configPath =
|
|
12753
|
-
const existing =
|
|
13096
|
+
const configPath = path21__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
13097
|
+
const existing = fs20__default.default.existsSync(configPath) ? JSON.parse(fs20__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
12754
13098
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
12755
13099
|
const tmp = configPath + ".tmp";
|
|
12756
|
-
|
|
12757
|
-
|
|
13100
|
+
fs20__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
13101
|
+
fs20__default.default.renameSync(tmp, configPath);
|
|
12758
13102
|
res.json({ ok: true });
|
|
12759
13103
|
} catch (err) {
|
|
12760
13104
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -12782,7 +13126,7 @@ ${prompt}`;
|
|
|
12782
13126
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
12783
13127
|
const scope = req.query["scope"] ?? "workspace";
|
|
12784
13128
|
if (scope === "global") {
|
|
12785
|
-
const globalDbPath =
|
|
13129
|
+
const globalDbPath = path21__default.default.join(os6__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
12786
13130
|
const globalStore = new MemoryStore(globalDbPath);
|
|
12787
13131
|
try {
|
|
12788
13132
|
res.json({
|
|
@@ -12830,6 +13174,9 @@ ${prompt}`;
|
|
|
12830
13174
|
cascade.on("peer:message", (e) => {
|
|
12831
13175
|
this.socket.emitPeerMessage(e);
|
|
12832
13176
|
});
|
|
13177
|
+
cascade.on("plan:approval-required", (e) => {
|
|
13178
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
13179
|
+
});
|
|
12833
13180
|
try {
|
|
12834
13181
|
const result = await cascade.run({
|
|
12835
13182
|
prompt: runPrompt,
|
|
@@ -12837,7 +13184,8 @@ ${prompt}`;
|
|
|
12837
13184
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
12838
13185
|
});
|
|
12839
13186
|
this.recordSessionTask(sessionId, result.taskId);
|
|
12840
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
13187
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
13188
|
+
this.captureWhy(sessionId, cascade, result);
|
|
12841
13189
|
this.socket.broadcast("cost:update", {
|
|
12842
13190
|
sessionId,
|
|
12843
13191
|
totalTokens: result.usage.totalTokens,
|
|
@@ -12847,6 +13195,7 @@ ${prompt}`;
|
|
|
12847
13195
|
this.throttledBroadcast("workspace");
|
|
12848
13196
|
} catch (err) {
|
|
12849
13197
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
13198
|
+
this.captureWhy(sessionId, cascade);
|
|
12850
13199
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
12851
13200
|
sessionId,
|
|
12852
13201
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -12868,13 +13217,217 @@ ${prompt}`;
|
|
|
12868
13217
|
}))
|
|
12869
13218
|
});
|
|
12870
13219
|
});
|
|
12871
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
13220
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
13221
|
+
const report = this.whyBySession.get(req.params.id);
|
|
13222
|
+
if (!report) {
|
|
13223
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
13224
|
+
return;
|
|
13225
|
+
}
|
|
13226
|
+
res.json(report);
|
|
13227
|
+
});
|
|
13228
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
13229
|
+
const sessionId = req.params.id;
|
|
13230
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13231
|
+
const before = /* @__PURE__ */ new Map();
|
|
13232
|
+
for (const taskId of taskIds) {
|
|
13233
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
13234
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
13235
|
+
}
|
|
13236
|
+
}
|
|
13237
|
+
const { readFile } = await import('fs/promises');
|
|
13238
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
13239
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
13240
|
+
let after = "";
|
|
13241
|
+
let missing = false;
|
|
13242
|
+
try {
|
|
13243
|
+
const buf = await readFile(filePath);
|
|
13244
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
13245
|
+
} catch {
|
|
13246
|
+
missing = true;
|
|
13247
|
+
}
|
|
13248
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
13249
|
+
}));
|
|
13250
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
13251
|
+
});
|
|
13252
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
13253
|
+
const sessionId = req.params.id;
|
|
13254
|
+
const body = req.body;
|
|
13255
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
13256
|
+
res.status(400).json({ error: "filePath is required" });
|
|
13257
|
+
return;
|
|
13258
|
+
}
|
|
13259
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
13260
|
+
let content;
|
|
13261
|
+
for (const taskId of taskIds) {
|
|
13262
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
13263
|
+
if (snap) {
|
|
13264
|
+
content = snap.content;
|
|
13265
|
+
break;
|
|
13266
|
+
}
|
|
13267
|
+
}
|
|
13268
|
+
if (content === void 0) {
|
|
13269
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
13270
|
+
return;
|
|
13271
|
+
}
|
|
13272
|
+
try {
|
|
13273
|
+
const { writeFile } = await import('fs/promises');
|
|
13274
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
13275
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
13276
|
+
} catch (err) {
|
|
13277
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13278
|
+
}
|
|
13279
|
+
});
|
|
13280
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
13281
|
+
try {
|
|
13282
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
13283
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
13284
|
+
res.json({
|
|
13285
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
13286
|
+
budget: {
|
|
13287
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
13288
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
13289
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
13290
|
+
}
|
|
13291
|
+
});
|
|
13292
|
+
} catch (err) {
|
|
13293
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13294
|
+
}
|
|
13295
|
+
});
|
|
13296
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
13297
|
+
try {
|
|
13298
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
13299
|
+
} catch (err) {
|
|
13300
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13301
|
+
}
|
|
13302
|
+
});
|
|
13303
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
13304
|
+
const body = req.body;
|
|
13305
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
13306
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
13307
|
+
return;
|
|
13308
|
+
}
|
|
13309
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
13310
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13311
|
+
return;
|
|
13312
|
+
}
|
|
13313
|
+
const task = {
|
|
13314
|
+
id: crypto3.randomUUID(),
|
|
13315
|
+
name: body.name.trim(),
|
|
13316
|
+
cronExpression: body.cronExpression.trim(),
|
|
13317
|
+
prompt: body.prompt.trim(),
|
|
13318
|
+
workspacePath: this.workspacePath,
|
|
13319
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13320
|
+
enabled: body.enabled !== false
|
|
13321
|
+
};
|
|
13322
|
+
try {
|
|
13323
|
+
this.scheduler.add(task);
|
|
13324
|
+
res.json(task);
|
|
13325
|
+
} catch (err) {
|
|
13326
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13327
|
+
}
|
|
13328
|
+
});
|
|
13329
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13330
|
+
const id = req.params.id;
|
|
13331
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
13332
|
+
if (!existing) {
|
|
13333
|
+
res.status(404).json({ error: "Not found" });
|
|
13334
|
+
return;
|
|
13335
|
+
}
|
|
13336
|
+
const body = req.body;
|
|
13337
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
13338
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
13339
|
+
return;
|
|
13340
|
+
}
|
|
13341
|
+
const updated = {
|
|
13342
|
+
...existing,
|
|
13343
|
+
name: body.name?.trim() || existing.name,
|
|
13344
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
13345
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
13346
|
+
enabled: body.enabled ?? existing.enabled
|
|
13347
|
+
};
|
|
13348
|
+
try {
|
|
13349
|
+
this.scheduler.add(updated);
|
|
13350
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
13351
|
+
res.json(updated);
|
|
13352
|
+
} catch (err) {
|
|
13353
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13354
|
+
}
|
|
13355
|
+
});
|
|
13356
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
13357
|
+
try {
|
|
13358
|
+
this.scheduler.remove(req.params.id);
|
|
13359
|
+
res.json({ ok: true });
|
|
13360
|
+
} catch (err) {
|
|
13361
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13362
|
+
}
|
|
13363
|
+
});
|
|
13364
|
+
this.app.get("/api/knowledge", auth, (_req, res) => {
|
|
13365
|
+
try {
|
|
13366
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13367
|
+
try {
|
|
13368
|
+
const facts = ws.getAllFacts();
|
|
13369
|
+
res.json({ total: facts.length, facts });
|
|
13370
|
+
} finally {
|
|
13371
|
+
ws.close();
|
|
13372
|
+
}
|
|
13373
|
+
} catch (err) {
|
|
13374
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13375
|
+
}
|
|
13376
|
+
});
|
|
13377
|
+
this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
|
|
13378
|
+
const body = req.body;
|
|
13379
|
+
if (!body.entity || !body.relation) {
|
|
13380
|
+
res.status(400).json({ error: "entity and relation are required" });
|
|
13381
|
+
return;
|
|
13382
|
+
}
|
|
13383
|
+
try {
|
|
13384
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13385
|
+
try {
|
|
13386
|
+
res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
|
|
13387
|
+
} finally {
|
|
13388
|
+
ws.close();
|
|
13389
|
+
}
|
|
13390
|
+
} catch (err) {
|
|
13391
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13392
|
+
}
|
|
13393
|
+
});
|
|
13394
|
+
this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
|
|
13395
|
+
try {
|
|
13396
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
13397
|
+
try {
|
|
13398
|
+
res.json({ ok: true, deleted: ws.clearFacts() });
|
|
13399
|
+
} finally {
|
|
13400
|
+
ws.close();
|
|
13401
|
+
}
|
|
13402
|
+
} catch (err) {
|
|
13403
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13404
|
+
}
|
|
13405
|
+
});
|
|
13406
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
13407
|
+
try {
|
|
13408
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
13409
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
13410
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
13411
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
13412
|
+
try {
|
|
13413
|
+
const all = logger.getAllLogs();
|
|
13414
|
+
const total = all.length;
|
|
13415
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
13416
|
+
res.json({ total, offset, entries });
|
|
13417
|
+
} finally {
|
|
13418
|
+
logger.close();
|
|
13419
|
+
}
|
|
13420
|
+
} catch (err) {
|
|
13421
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
13422
|
+
}
|
|
13423
|
+
});
|
|
13424
|
+
const prodPath = path21__default.default.resolve(__dirname$1, "../web/dist");
|
|
13425
|
+
const devPath = path21__default.default.resolve(__dirname$1, "../../web/dist");
|
|
13426
|
+
const webDistPath = fs20__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
13427
|
+
if (fs20__default.default.existsSync(webDistPath)) {
|
|
12875
13428
|
this.app.use(express__default.default.static(webDistPath));
|
|
12876
13429
|
this.app.get("*", (_req, res) => {
|
|
12877
|
-
res.sendFile(
|
|
13430
|
+
res.sendFile(path21__default.default.join(webDistPath, "index.html"));
|
|
12878
13431
|
});
|
|
12879
13432
|
} else {
|
|
12880
13433
|
this.app.get("/", (_req, res) => {
|
|
@@ -12889,68 +13442,6 @@ ${prompt}`;
|
|
|
12889
13442
|
}
|
|
12890
13443
|
}
|
|
12891
13444
|
};
|
|
12892
|
-
var TaskScheduler = class {
|
|
12893
|
-
cronJobs = /* @__PURE__ */ new Map();
|
|
12894
|
-
store;
|
|
12895
|
-
runner;
|
|
12896
|
-
constructor(store, runner) {
|
|
12897
|
-
this.store = store;
|
|
12898
|
-
this.runner = runner;
|
|
12899
|
-
}
|
|
12900
|
-
start() {
|
|
12901
|
-
const tasks = this.store.listScheduledTasks();
|
|
12902
|
-
for (const task of tasks) {
|
|
12903
|
-
if (task.enabled) this.schedule(task);
|
|
12904
|
-
}
|
|
12905
|
-
}
|
|
12906
|
-
stop() {
|
|
12907
|
-
for (const job of this.cronJobs.values()) job.stop();
|
|
12908
|
-
this.cronJobs.clear();
|
|
12909
|
-
}
|
|
12910
|
-
schedule(task) {
|
|
12911
|
-
if (!cron__default.default.validate(task.cronExpression)) {
|
|
12912
|
-
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
12913
|
-
}
|
|
12914
|
-
const existing = this.cronJobs.get(task.id);
|
|
12915
|
-
if (existing) {
|
|
12916
|
-
try {
|
|
12917
|
-
existing.stop();
|
|
12918
|
-
} catch {
|
|
12919
|
-
}
|
|
12920
|
-
}
|
|
12921
|
-
const job = cron__default.default.schedule(task.cronExpression, async () => {
|
|
12922
|
-
try {
|
|
12923
|
-
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
12924
|
-
this.store.saveScheduledTask(task);
|
|
12925
|
-
await this.runner(task);
|
|
12926
|
-
} catch (err) {
|
|
12927
|
-
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
12928
|
-
}
|
|
12929
|
-
}, { timezone: "UTC" });
|
|
12930
|
-
this.cronJobs.set(task.id, job);
|
|
12931
|
-
}
|
|
12932
|
-
unschedule(taskId) {
|
|
12933
|
-
this.cronJobs.get(taskId)?.stop();
|
|
12934
|
-
this.cronJobs.delete(taskId);
|
|
12935
|
-
}
|
|
12936
|
-
add(task) {
|
|
12937
|
-
this.store.saveScheduledTask(task);
|
|
12938
|
-
if (task.enabled) this.schedule(task);
|
|
12939
|
-
}
|
|
12940
|
-
remove(taskId) {
|
|
12941
|
-
this.unschedule(taskId);
|
|
12942
|
-
this.store.deleteScheduledTask(taskId);
|
|
12943
|
-
}
|
|
12944
|
-
list() {
|
|
12945
|
-
return this.store.listScheduledTasks();
|
|
12946
|
-
}
|
|
12947
|
-
isRunning(taskId) {
|
|
12948
|
-
return this.cronJobs.has(taskId);
|
|
12949
|
-
}
|
|
12950
|
-
static validateCron(expression) {
|
|
12951
|
-
return cron__default.default.validate(expression);
|
|
12952
|
-
}
|
|
12953
|
-
};
|
|
12954
13445
|
var execFileAsync2 = util.promisify(child_process.execFile);
|
|
12955
13446
|
var SAFE_ENV_NAME = /^[A-Z][A-Z0-9_]*$/;
|
|
12956
13447
|
function sanitizeEnvValue(v) {
|
|
@@ -13037,6 +13528,7 @@ exports.DEFAULT_RETENTION_DAYS = DEFAULT_RETENTION_DAYS;
|
|
|
13037
13528
|
exports.DEFAULT_THEME = DEFAULT_THEME;
|
|
13038
13529
|
exports.DashboardServer = DashboardServer;
|
|
13039
13530
|
exports.GLOBAL_CONFIG_DIR = GLOBAL_CONFIG_DIR;
|
|
13531
|
+
exports.GLOBAL_CREDENTIALS_FILE = GLOBAL_CREDENTIALS_FILE;
|
|
13040
13532
|
exports.GLOBAL_DB_FILE = GLOBAL_DB_FILE;
|
|
13041
13533
|
exports.GLOBAL_KEYSTORE_FILE = GLOBAL_KEYSTORE_FILE;
|
|
13042
13534
|
exports.GLOBAL_RUNTIME_DB_FILE = GLOBAL_RUNTIME_DB_FILE;
|