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/cli.js
CHANGED
|
@@ -7,8 +7,8 @@ import https from 'https';
|
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
9
|
import Database2 from 'better-sqlite3';
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path26 from 'path';
|
|
11
|
+
import fs24 from 'fs';
|
|
12
12
|
import os8 from 'os';
|
|
13
13
|
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
14
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
@@ -17,7 +17,7 @@ import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo }
|
|
|
17
17
|
import chalk9 from 'chalk';
|
|
18
18
|
import dotenv from 'dotenv';
|
|
19
19
|
import { fileURLToPath } from 'url';
|
|
20
|
-
import
|
|
20
|
+
import fs10 from 'fs/promises';
|
|
21
21
|
import * as _ignoreModule from 'ignore';
|
|
22
22
|
import _ignoreModule__default from 'ignore';
|
|
23
23
|
import { z } from 'zod';
|
|
@@ -42,6 +42,7 @@ import bcrypt from 'bcryptjs';
|
|
|
42
42
|
import { Server } from 'socket.io';
|
|
43
43
|
import parser from 'socket.io-msgpack-parser';
|
|
44
44
|
import jwt from 'jsonwebtoken';
|
|
45
|
+
import cron from 'node-cron';
|
|
45
46
|
|
|
46
47
|
// Cascade AI — Multi-tier AI Orchestration System
|
|
47
48
|
var __defProp = Object.defineProperty;
|
|
@@ -55,16 +56,17 @@ var __export = (target, all) => {
|
|
|
55
56
|
};
|
|
56
57
|
|
|
57
58
|
// src/constants.ts
|
|
58
|
-
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
59
|
+
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_CREDENTIALS_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
59
60
|
var init_constants = __esm({
|
|
60
61
|
"src/constants.ts"() {
|
|
61
|
-
CASCADE_VERSION = "0.
|
|
62
|
+
CASCADE_VERSION = "0.18.0";
|
|
62
63
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
63
64
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
64
65
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
65
66
|
GLOBAL_CONFIG_DIR = ".cascade-ai";
|
|
66
67
|
GLOBAL_DB_FILE = "memory.db";
|
|
67
68
|
GLOBAL_KEYSTORE_FILE = "keystore.enc";
|
|
69
|
+
GLOBAL_CREDENTIALS_FILE = "credentials.json";
|
|
68
70
|
GLOBAL_RUNTIME_DB_FILE = "runtime.db";
|
|
69
71
|
DEFAULT_DASHBOARD_PORT = 4891;
|
|
70
72
|
DEFAULT_CONTEXT_LIMIT = 2e5;
|
|
@@ -1579,12 +1581,12 @@ var init_audit_logger = __esm({
|
|
|
1579
1581
|
constructor(workspacePath, debugMode = false) {
|
|
1580
1582
|
this.workspacePath = workspacePath;
|
|
1581
1583
|
this.debugMode = debugMode;
|
|
1582
|
-
const cascadeDir =
|
|
1583
|
-
if (!
|
|
1584
|
-
|
|
1584
|
+
const cascadeDir = path26.join(workspacePath, ".cascade");
|
|
1585
|
+
if (!fs24.existsSync(cascadeDir)) {
|
|
1586
|
+
fs24.mkdirSync(cascadeDir, { recursive: true });
|
|
1585
1587
|
}
|
|
1586
|
-
this.keyPath =
|
|
1587
|
-
this.dbPath =
|
|
1588
|
+
this.keyPath = path26.join(cascadeDir, "audit_log.key");
|
|
1589
|
+
this.dbPath = path26.join(cascadeDir, "audit_log.db");
|
|
1588
1590
|
this.initEncryptionKey();
|
|
1589
1591
|
this.db = new Database2(this.dbPath);
|
|
1590
1592
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -1614,11 +1616,11 @@ var init_audit_logger = __esm({
|
|
|
1614
1616
|
return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
1615
1617
|
}
|
|
1616
1618
|
initEncryptionKey() {
|
|
1617
|
-
if (
|
|
1618
|
-
this.encryptionKey =
|
|
1619
|
+
if (fs24.existsSync(this.keyPath)) {
|
|
1620
|
+
this.encryptionKey = fs24.readFileSync(this.keyPath);
|
|
1619
1621
|
} else {
|
|
1620
1622
|
this.encryptionKey = crypto.randomBytes(32);
|
|
1621
|
-
|
|
1623
|
+
fs24.writeFileSync(this.keyPath, this.encryptionKey);
|
|
1622
1624
|
}
|
|
1623
1625
|
}
|
|
1624
1626
|
encrypt(text) {
|
|
@@ -1696,9 +1698,9 @@ var init_audit_logger = __esm({
|
|
|
1696
1698
|
dumpDebugIfNeeded() {
|
|
1697
1699
|
if (!this.debugMode) return;
|
|
1698
1700
|
try {
|
|
1699
|
-
const dumpPath =
|
|
1701
|
+
const dumpPath = path26.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
|
|
1700
1702
|
const entries = this.getAllLogs();
|
|
1701
|
-
|
|
1703
|
+
fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
1702
1704
|
} catch (err) {
|
|
1703
1705
|
console.error("Failed to dump debug audit logs", err);
|
|
1704
1706
|
}
|
|
@@ -1753,7 +1755,7 @@ var Keystore = class {
|
|
|
1753
1755
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
1754
1756
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
1755
1757
|
this.backend = "keytar";
|
|
1756
|
-
if (password &&
|
|
1758
|
+
if (password && fs24.existsSync(this.storePath)) {
|
|
1757
1759
|
try {
|
|
1758
1760
|
const fileEntries = this.decryptFile(password);
|
|
1759
1761
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -1772,7 +1774,7 @@ var Keystore = class {
|
|
|
1772
1774
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
1773
1775
|
);
|
|
1774
1776
|
}
|
|
1775
|
-
if (!
|
|
1777
|
+
if (!fs24.existsSync(this.storePath)) {
|
|
1776
1778
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1777
1779
|
this.masterKey = this.deriveKey(password, salt);
|
|
1778
1780
|
this.writeWithSalt({}, salt);
|
|
@@ -1786,7 +1788,7 @@ var Keystore = class {
|
|
|
1786
1788
|
}
|
|
1787
1789
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
1788
1790
|
unlockSync(password) {
|
|
1789
|
-
if (!
|
|
1791
|
+
if (!fs24.existsSync(this.storePath)) {
|
|
1790
1792
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1791
1793
|
this.masterKey = this.deriveKey(password, salt);
|
|
1792
1794
|
this.writeWithSalt({}, salt);
|
|
@@ -1844,7 +1846,7 @@ var Keystore = class {
|
|
|
1844
1846
|
}
|
|
1845
1847
|
}
|
|
1846
1848
|
decryptFile(password, knownSalt) {
|
|
1847
|
-
if (!
|
|
1849
|
+
if (!fs24.existsSync(this.storePath)) return {};
|
|
1848
1850
|
try {
|
|
1849
1851
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
1850
1852
|
const useSalt = knownSalt ?? salt;
|
|
@@ -1866,8 +1868,8 @@ var Keystore = class {
|
|
|
1866
1868
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1867
1869
|
const tag = cipher.getAuthTag();
|
|
1868
1870
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
+
fs24.mkdirSync(path26.dirname(this.storePath), { recursive: true });
|
|
1872
|
+
fs24.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1871
1873
|
}
|
|
1872
1874
|
writeWithSalt(data, salt) {
|
|
1873
1875
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -1877,11 +1879,11 @@ var Keystore = class {
|
|
|
1877
1879
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1878
1880
|
const tag = cipher.getAuthTag();
|
|
1879
1881
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
+
fs24.mkdirSync(path26.dirname(this.storePath), { recursive: true });
|
|
1883
|
+
fs24.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1882
1884
|
}
|
|
1883
1885
|
readRaw() {
|
|
1884
|
-
const buf =
|
|
1886
|
+
const buf = fs24.readFileSync(this.storePath);
|
|
1885
1887
|
let offset = 0;
|
|
1886
1888
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
1887
1889
|
offset += SALT_LEN;
|
|
@@ -1914,9 +1916,9 @@ var CascadeIgnore = class {
|
|
|
1914
1916
|
]);
|
|
1915
1917
|
}
|
|
1916
1918
|
async load(workspacePath) {
|
|
1917
|
-
const filePath =
|
|
1919
|
+
const filePath = path26.join(workspacePath, ".cascadeignore");
|
|
1918
1920
|
try {
|
|
1919
|
-
const content = await
|
|
1921
|
+
const content = await fs10.readFile(filePath, "utf-8");
|
|
1920
1922
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
1921
1923
|
this.ig.add(lines);
|
|
1922
1924
|
this.loaded = true;
|
|
@@ -1925,7 +1927,7 @@ var CascadeIgnore = class {
|
|
|
1925
1927
|
}
|
|
1926
1928
|
isIgnored(filePath, workspacePath) {
|
|
1927
1929
|
try {
|
|
1928
|
-
const relative = workspacePath ?
|
|
1930
|
+
const relative = workspacePath ? path26.relative(workspacePath, filePath) : filePath;
|
|
1929
1931
|
return this.ig.ignores(relative);
|
|
1930
1932
|
} catch {
|
|
1931
1933
|
return false;
|
|
@@ -1936,7 +1938,7 @@ var CascadeIgnore = class {
|
|
|
1936
1938
|
}
|
|
1937
1939
|
};
|
|
1938
1940
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
1939
|
-
const filePath =
|
|
1941
|
+
const filePath = path26.join(workspacePath, ".cascadeignore");
|
|
1940
1942
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
1941
1943
|
# Syntax identical to .gitignore
|
|
1942
1944
|
|
|
@@ -1963,12 +1965,12 @@ build/
|
|
|
1963
1965
|
.DS_Store
|
|
1964
1966
|
Thumbs.db
|
|
1965
1967
|
`;
|
|
1966
|
-
await
|
|
1968
|
+
await fs10.writeFile(filePath, content, "utf-8");
|
|
1967
1969
|
}
|
|
1968
1970
|
async function loadCascadeMd(workspacePath) {
|
|
1969
|
-
const filePath =
|
|
1971
|
+
const filePath = path26.join(workspacePath, "CASCADE.md");
|
|
1970
1972
|
try {
|
|
1971
|
-
const raw = await
|
|
1973
|
+
const raw = await fs10.readFile(filePath, "utf-8");
|
|
1972
1974
|
return parseCascadeMd(raw);
|
|
1973
1975
|
} catch {
|
|
1974
1976
|
return null;
|
|
@@ -1995,7 +1997,7 @@ ${raw.trim()}`;
|
|
|
1995
1997
|
return { raw, sections, systemPrompt };
|
|
1996
1998
|
}
|
|
1997
1999
|
async function createDefaultCascadeMd(workspacePath) {
|
|
1998
|
-
const filePath =
|
|
2000
|
+
const filePath = path26.join(workspacePath, "CASCADE.md");
|
|
1999
2001
|
const content = `# Cascade Project Instructions
|
|
2000
2002
|
|
|
2001
2003
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -2025,12 +2027,12 @@ All tools are allowed unless specified otherwise.
|
|
|
2025
2027
|
|
|
2026
2028
|
List any areas the agent should not touch.
|
|
2027
2029
|
`;
|
|
2028
|
-
await
|
|
2030
|
+
await fs10.writeFile(filePath, content, "utf-8");
|
|
2029
2031
|
}
|
|
2030
2032
|
var MemoryStore = class _MemoryStore {
|
|
2031
2033
|
db;
|
|
2032
2034
|
constructor(dbPath) {
|
|
2033
|
-
|
|
2035
|
+
fs24.mkdirSync(path26.dirname(dbPath), { recursive: true });
|
|
2034
2036
|
try {
|
|
2035
2037
|
this.db = new Database2(dbPath, { timeout: 5e3 });
|
|
2036
2038
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -3177,6 +3179,63 @@ function validateConfig(raw) {
|
|
|
3177
3179
|
return result.data;
|
|
3178
3180
|
}
|
|
3179
3181
|
|
|
3182
|
+
// src/config/global-credentials.ts
|
|
3183
|
+
init_constants();
|
|
3184
|
+
function credentialsPath(globalDir) {
|
|
3185
|
+
return path26.join(globalDir, GLOBAL_CREDENTIALS_FILE);
|
|
3186
|
+
}
|
|
3187
|
+
function providerKey(p) {
|
|
3188
|
+
if (p.type === "azure") {
|
|
3189
|
+
return `azure:${p.deploymentName ?? p.baseUrl ?? p.label ?? ""}`;
|
|
3190
|
+
}
|
|
3191
|
+
return p.type;
|
|
3192
|
+
}
|
|
3193
|
+
function isPersistable(p) {
|
|
3194
|
+
return Boolean(p.apiKey || p.authToken || p.type === "azure" || p.baseUrl);
|
|
3195
|
+
}
|
|
3196
|
+
function loadGlobalCredentials(globalDir) {
|
|
3197
|
+
try {
|
|
3198
|
+
const raw = fs24.readFileSync(credentialsPath(globalDir), "utf-8");
|
|
3199
|
+
const parsed = JSON.parse(raw);
|
|
3200
|
+
if (!Array.isArray(parsed.providers)) return [];
|
|
3201
|
+
return parsed.providers.filter(
|
|
3202
|
+
(p) => Boolean(p) && typeof p.type === "string"
|
|
3203
|
+
);
|
|
3204
|
+
} catch {
|
|
3205
|
+
return [];
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
function saveGlobalCredentials(globalDir, providers) {
|
|
3209
|
+
const filePath = credentialsPath(globalDir);
|
|
3210
|
+
fs24.mkdirSync(globalDir, { recursive: true, mode: 448 });
|
|
3211
|
+
const body = { version: 1, providers: providers.filter(isPersistable) };
|
|
3212
|
+
const tmp = `${filePath}.tmp`;
|
|
3213
|
+
fs24.writeFileSync(tmp, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
|
|
3214
|
+
fs24.renameSync(tmp, filePath);
|
|
3215
|
+
try {
|
|
3216
|
+
fs24.chmodSync(filePath, 384);
|
|
3217
|
+
} catch {
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
function mergeGlobalCredentials(workspaceProviders, globalProviders) {
|
|
3221
|
+
const merged = [...workspaceProviders];
|
|
3222
|
+
const byKey = new Map(merged.map((p) => [providerKey(p), p]));
|
|
3223
|
+
for (const g of globalProviders) {
|
|
3224
|
+
const existing = byKey.get(providerKey(g));
|
|
3225
|
+
if (!existing) {
|
|
3226
|
+
merged.push({ ...g });
|
|
3227
|
+
byKey.set(providerKey(g), merged[merged.length - 1]);
|
|
3228
|
+
continue;
|
|
3229
|
+
}
|
|
3230
|
+
if (!existing.apiKey && g.apiKey) existing.apiKey = g.apiKey;
|
|
3231
|
+
if (!existing.authToken && g.authToken) existing.authToken = g.authToken;
|
|
3232
|
+
if (!existing.baseUrl && g.baseUrl) existing.baseUrl = g.baseUrl;
|
|
3233
|
+
if (!existing.apiVersion && g.apiVersion) existing.apiVersion = g.apiVersion;
|
|
3234
|
+
if (!existing.label && g.label) existing.label = g.label;
|
|
3235
|
+
}
|
|
3236
|
+
return merged;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3180
3239
|
// src/config/index.ts
|
|
3181
3240
|
init_constants();
|
|
3182
3241
|
var KEY_OPTIONAL_PROVIDER_TYPES = /* @__PURE__ */ new Set(["ollama", "openai-compatible"]);
|
|
@@ -3192,18 +3251,20 @@ var ConfigManager = class {
|
|
|
3192
3251
|
cascadeMd = null;
|
|
3193
3252
|
workspacePath;
|
|
3194
3253
|
globalDir;
|
|
3195
|
-
|
|
3254
|
+
/** `globalDirOverride` exists for tests — never point it at the real home dir there. */
|
|
3255
|
+
constructor(workspacePath = process.cwd(), globalDirOverride) {
|
|
3196
3256
|
this.workspacePath = workspacePath;
|
|
3197
|
-
this.globalDir =
|
|
3257
|
+
this.globalDir = globalDirOverride ?? path26.join(os8.homedir(), GLOBAL_CONFIG_DIR);
|
|
3198
3258
|
}
|
|
3199
3259
|
async load() {
|
|
3200
3260
|
this.config = await this.loadConfig();
|
|
3201
3261
|
this.ignore = new CascadeIgnore();
|
|
3202
3262
|
await this.ignore.load(this.workspacePath);
|
|
3203
3263
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
3204
|
-
this.keystore = new Keystore(
|
|
3205
|
-
this.store = new MemoryStore(
|
|
3264
|
+
this.keystore = new Keystore(path26.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
3265
|
+
this.store = new MemoryStore(path26.join(this.workspacePath, CASCADE_DB_FILE));
|
|
3206
3266
|
await this.injectEnvKeys();
|
|
3267
|
+
this.config.providers = mergeGlobalCredentials(this.config.providers, loadGlobalCredentials(this.globalDir));
|
|
3207
3268
|
await this.ensureDefaultIdentity();
|
|
3208
3269
|
}
|
|
3209
3270
|
getConfig() {
|
|
@@ -3225,9 +3286,14 @@ var ConfigManager = class {
|
|
|
3225
3286
|
return this.workspacePath;
|
|
3226
3287
|
}
|
|
3227
3288
|
async save() {
|
|
3228
|
-
const configPath =
|
|
3229
|
-
await
|
|
3230
|
-
await
|
|
3289
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
3290
|
+
await fs10.mkdir(path26.dirname(configPath), { recursive: true });
|
|
3291
|
+
await fs10.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
3292
|
+
try {
|
|
3293
|
+
saveGlobalCredentials(this.globalDir, this.config.providers);
|
|
3294
|
+
} catch (err) {
|
|
3295
|
+
console.warn(`Failed to sync credentials to global store: ${err instanceof Error ? err.message : String(err)}`);
|
|
3296
|
+
}
|
|
3231
3297
|
}
|
|
3232
3298
|
async updateConfig(updates) {
|
|
3233
3299
|
this.config = validateConfig({ ...this.config, ...updates });
|
|
@@ -3250,9 +3316,9 @@ var ConfigManager = class {
|
|
|
3250
3316
|
return configProvider?.apiKey;
|
|
3251
3317
|
}
|
|
3252
3318
|
async loadConfig() {
|
|
3253
|
-
const configPath =
|
|
3319
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
3254
3320
|
try {
|
|
3255
|
-
const raw = await
|
|
3321
|
+
const raw = await fs10.readFile(configPath, "utf-8");
|
|
3256
3322
|
return validateConfig(JSON.parse(raw));
|
|
3257
3323
|
} catch (err) {
|
|
3258
3324
|
if (err.code === "ENOENT") {
|
|
@@ -4129,7 +4195,7 @@ init_constants();
|
|
|
4129
4195
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
4130
4196
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
4131
4197
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
4132
|
-
var DEFAULT_CACHE_FILE =
|
|
4198
|
+
var DEFAULT_CACHE_FILE = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
4133
4199
|
function normalizeModelId(id) {
|
|
4134
4200
|
let s = id.toLowerCase();
|
|
4135
4201
|
const slash = s.lastIndexOf("/");
|
|
@@ -4162,7 +4228,7 @@ var LiveDataProvider = class {
|
|
|
4162
4228
|
if (this.loaded) return;
|
|
4163
4229
|
this.loaded = true;
|
|
4164
4230
|
try {
|
|
4165
|
-
const raw = await
|
|
4231
|
+
const raw = await fs10.readFile(this.opts.cacheFile, "utf-8");
|
|
4166
4232
|
const cache = JSON.parse(raw);
|
|
4167
4233
|
if (cache.snapshot?.families) {
|
|
4168
4234
|
this.snapshot = cache.snapshot;
|
|
@@ -4274,14 +4340,14 @@ var LiveDataProvider = class {
|
|
|
4274
4340
|
}
|
|
4275
4341
|
async saveCache() {
|
|
4276
4342
|
try {
|
|
4277
|
-
await
|
|
4343
|
+
await fs10.mkdir(path26.dirname(this.opts.cacheFile), { recursive: true });
|
|
4278
4344
|
const cache = {
|
|
4279
4345
|
fetchedAt: this.fetchedAt,
|
|
4280
4346
|
snapshot: this.snapshot ?? void 0,
|
|
4281
4347
|
prices: Object.fromEntries(this.prices),
|
|
4282
4348
|
capabilities: Object.fromEntries(this.capabilities)
|
|
4283
4349
|
};
|
|
4284
|
-
await
|
|
4350
|
+
await fs10.writeFile(this.opts.cacheFile, JSON.stringify(cache, null, 2), "utf-8");
|
|
4285
4351
|
} catch {
|
|
4286
4352
|
}
|
|
4287
4353
|
}
|
|
@@ -6178,8 +6244,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
6178
6244
|
tierId: this.id,
|
|
6179
6245
|
sessionId: this.taskId,
|
|
6180
6246
|
requireApproval: false,
|
|
6181
|
-
saveSnapshot: async (
|
|
6182
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
6247
|
+
saveSnapshot: async (path31, content) => {
|
|
6248
|
+
this.store?.addFileSnapshot(this.taskId, path31, content);
|
|
6183
6249
|
},
|
|
6184
6250
|
sendPeerSync: (to, syncType, content) => {
|
|
6185
6251
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -6349,9 +6415,9 @@ ${assignment.expectedOutput}`;
|
|
|
6349
6415
|
const { promisify: promisify4 } = await import('util');
|
|
6350
6416
|
const execAsync3 = promisify4(exec3);
|
|
6351
6417
|
for (const artifactPath of artifactPaths) {
|
|
6352
|
-
const absolutePath =
|
|
6418
|
+
const absolutePath = path26.resolve(process.cwd(), artifactPath);
|
|
6353
6419
|
try {
|
|
6354
|
-
const stat = await
|
|
6420
|
+
const stat = await fs10.stat(absolutePath);
|
|
6355
6421
|
if (!stat.isFile()) {
|
|
6356
6422
|
issues.push(`Expected artifact is not a file: ${artifactPath}`);
|
|
6357
6423
|
continue;
|
|
@@ -6361,7 +6427,7 @@ ${assignment.expectedOutput}`;
|
|
|
6361
6427
|
continue;
|
|
6362
6428
|
}
|
|
6363
6429
|
if (!/\.pdf$/i.test(artifactPath)) {
|
|
6364
|
-
const content = await
|
|
6430
|
+
const content = await fs10.readFile(absolutePath, "utf-8");
|
|
6365
6431
|
if (!content.trim()) {
|
|
6366
6432
|
issues.push(`Artifact content is empty: ${artifactPath}`);
|
|
6367
6433
|
continue;
|
|
@@ -6370,7 +6436,7 @@ ${assignment.expectedOutput}`;
|
|
|
6370
6436
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
6371
6437
|
continue;
|
|
6372
6438
|
}
|
|
6373
|
-
const ext =
|
|
6439
|
+
const ext = path26.extname(absolutePath).toLowerCase();
|
|
6374
6440
|
try {
|
|
6375
6441
|
if (ext === ".ts" || ext === ".tsx") {
|
|
6376
6442
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -8461,16 +8527,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
8461
8527
|
if (typeof input !== "string" || input.length === 0) {
|
|
8462
8528
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
8463
8529
|
}
|
|
8464
|
-
const root =
|
|
8465
|
-
const abs =
|
|
8466
|
-
const rel =
|
|
8467
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
8530
|
+
const root = path26.resolve(workspaceRoot);
|
|
8531
|
+
const abs = path26.isAbsolute(input) ? path26.resolve(input) : path26.resolve(root, input);
|
|
8532
|
+
const rel = path26.relative(root, abs);
|
|
8533
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path26.isAbsolute(rel)) {
|
|
8468
8534
|
throw new WorkspaceSandboxError(input, root);
|
|
8469
8535
|
}
|
|
8470
8536
|
try {
|
|
8471
|
-
const real =
|
|
8472
|
-
const realRel =
|
|
8473
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
8537
|
+
const real = fs24.realpathSync(abs);
|
|
8538
|
+
const realRel = path26.relative(root, real);
|
|
8539
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path26.isAbsolute(realRel))) {
|
|
8474
8540
|
throw new WorkspaceSandboxError(input, root);
|
|
8475
8541
|
}
|
|
8476
8542
|
} catch (e) {
|
|
@@ -8497,7 +8563,7 @@ var FileReadTool = class extends BaseTool {
|
|
|
8497
8563
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
8498
8564
|
const offset = input["offset"] ?? 1;
|
|
8499
8565
|
const limit = input["limit"];
|
|
8500
|
-
const content = await
|
|
8566
|
+
const content = await fs10.readFile(absPath, "utf-8");
|
|
8501
8567
|
const lines = content.split("\n");
|
|
8502
8568
|
const start = Math.max(0, offset - 1);
|
|
8503
8569
|
const end = limit ? start + limit : lines.length;
|
|
@@ -8526,13 +8592,13 @@ var FileWriteTool = class extends BaseTool {
|
|
|
8526
8592
|
const content = input["content"];
|
|
8527
8593
|
if (options.saveSnapshot) {
|
|
8528
8594
|
try {
|
|
8529
|
-
const oldContent = await
|
|
8595
|
+
const oldContent = await fs10.readFile(absPath, "utf-8");
|
|
8530
8596
|
await options.saveSnapshot(absPath, oldContent);
|
|
8531
8597
|
} catch {
|
|
8532
8598
|
}
|
|
8533
8599
|
}
|
|
8534
|
-
await
|
|
8535
|
-
await
|
|
8600
|
+
await fs10.mkdir(path26.dirname(absPath), { recursive: true });
|
|
8601
|
+
await fs10.writeFile(absPath, content, "utf-8");
|
|
8536
8602
|
return `Written ${content.length} characters to ${filePath}`;
|
|
8537
8603
|
}
|
|
8538
8604
|
};
|
|
@@ -8558,7 +8624,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
8558
8624
|
const oldString = input["old_string"];
|
|
8559
8625
|
const newString = input["new_string"];
|
|
8560
8626
|
const replaceAll = input["replace_all"] ?? false;
|
|
8561
|
-
const rawContent = await
|
|
8627
|
+
const rawContent = await fs10.readFile(absPath, "utf-8");
|
|
8562
8628
|
if (options.saveSnapshot) {
|
|
8563
8629
|
await options.saveSnapshot(absPath, rawContent);
|
|
8564
8630
|
}
|
|
@@ -8570,7 +8636,7 @@ var FileEditTool = class extends BaseTool {
|
|
|
8570
8636
|
);
|
|
8571
8637
|
}
|
|
8572
8638
|
const updated = replaceAll ? content.split(normalizedOld).join(newString) : content.replace(normalizedOld, newString);
|
|
8573
|
-
await
|
|
8639
|
+
await fs10.writeFile(absPath, updated, "utf-8");
|
|
8574
8640
|
const count = replaceAll ? content.split(normalizedOld).length - 1 : 1;
|
|
8575
8641
|
return `Replaced ${count} occurrence(s) in ${filePath}`;
|
|
8576
8642
|
}
|
|
@@ -8593,12 +8659,12 @@ var FileDeleteTool = class extends BaseTool {
|
|
|
8593
8659
|
const absPath = resolveInWorkspace(this.workspaceRoot, filePath);
|
|
8594
8660
|
if (options.saveSnapshot) {
|
|
8595
8661
|
try {
|
|
8596
|
-
const oldContent = await
|
|
8662
|
+
const oldContent = await fs10.readFile(absPath, "utf-8");
|
|
8597
8663
|
await options.saveSnapshot(absPath, oldContent);
|
|
8598
8664
|
} catch {
|
|
8599
8665
|
}
|
|
8600
8666
|
}
|
|
8601
|
-
await
|
|
8667
|
+
await fs10.rm(absPath, { recursive: false });
|
|
8602
8668
|
return `Deleted ${filePath}`;
|
|
8603
8669
|
}
|
|
8604
8670
|
};
|
|
@@ -8615,7 +8681,7 @@ var FileListTool = class extends BaseTool {
|
|
|
8615
8681
|
async execute(input, _options) {
|
|
8616
8682
|
const inputPath = input["path"] || ".";
|
|
8617
8683
|
const absPath = resolveInWorkspace(this.workspaceRoot, inputPath);
|
|
8618
|
-
const entries = await
|
|
8684
|
+
const entries = await fs10.readdir(absPath, { withFileTypes: true });
|
|
8619
8685
|
return entries.map((e) => `${e.isDirectory() ? "[DIR] " : " "}${e.name}`).join("\n") || "(empty directory)";
|
|
8620
8686
|
}
|
|
8621
8687
|
};
|
|
@@ -9028,8 +9094,8 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
9028
9094
|
}
|
|
9029
9095
|
};
|
|
9030
9096
|
async function fileToImageAttachment(filePath) {
|
|
9031
|
-
const data = await
|
|
9032
|
-
const ext =
|
|
9097
|
+
const data = await fs10.readFile(filePath);
|
|
9098
|
+
const ext = path26.extname(filePath).toLowerCase();
|
|
9033
9099
|
const mimeMap = {
|
|
9034
9100
|
".jpg": "image/jpeg",
|
|
9035
9101
|
".jpeg": "image/jpeg",
|
|
@@ -9063,14 +9129,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
9063
9129
|
const filePath = input["path"];
|
|
9064
9130
|
const content = input["content"];
|
|
9065
9131
|
const title = input["title"];
|
|
9066
|
-
const dir =
|
|
9067
|
-
if (!
|
|
9068
|
-
|
|
9132
|
+
const dir = path26.dirname(filePath);
|
|
9133
|
+
if (!fs24.existsSync(dir)) {
|
|
9134
|
+
fs24.mkdirSync(dir, { recursive: true });
|
|
9069
9135
|
}
|
|
9070
9136
|
return new Promise((resolve, reject) => {
|
|
9071
9137
|
try {
|
|
9072
9138
|
const doc = new PDFDocument({ margin: 50 });
|
|
9073
|
-
const stream =
|
|
9139
|
+
const stream = fs24.createWriteStream(filePath);
|
|
9074
9140
|
doc.pipe(stream);
|
|
9075
9141
|
if (title) {
|
|
9076
9142
|
doc.info["Title"] = title;
|
|
@@ -9148,22 +9214,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
9148
9214
|
}
|
|
9149
9215
|
cmdPrefix = NODE_CMD;
|
|
9150
9216
|
}
|
|
9151
|
-
const tmpDir =
|
|
9152
|
-
if (!
|
|
9153
|
-
|
|
9217
|
+
const tmpDir = path26.join(this.workspaceRoot, ".cascade", "tmp");
|
|
9218
|
+
if (!fs24.existsSync(tmpDir)) {
|
|
9219
|
+
fs24.mkdirSync(tmpDir, { recursive: true });
|
|
9154
9220
|
}
|
|
9155
9221
|
const extension = language === "python" ? "py" : "js";
|
|
9156
9222
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
9157
|
-
const filePath =
|
|
9158
|
-
|
|
9223
|
+
const filePath = path26.join(tmpDir, fileName);
|
|
9224
|
+
fs24.writeFileSync(filePath, code, "utf-8");
|
|
9159
9225
|
const execArgs = [filePath, ...args];
|
|
9160
9226
|
return new Promise((resolve) => {
|
|
9161
9227
|
const startMs = Date.now();
|
|
9162
9228
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
9163
9229
|
const duration = Date.now() - startMs;
|
|
9164
9230
|
try {
|
|
9165
|
-
if (
|
|
9166
|
-
|
|
9231
|
+
if (fs24.existsSync(filePath)) {
|
|
9232
|
+
fs24.unlinkSync(filePath);
|
|
9167
9233
|
}
|
|
9168
9234
|
} catch (cleanupErr) {
|
|
9169
9235
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -9442,7 +9508,7 @@ var GlobTool = class extends BaseTool {
|
|
|
9442
9508
|
};
|
|
9443
9509
|
async execute(input, _options) {
|
|
9444
9510
|
const pattern = input["pattern"];
|
|
9445
|
-
const searchPath = input["path"] ?
|
|
9511
|
+
const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
9446
9512
|
const matches = await glob(pattern, {
|
|
9447
9513
|
cwd: searchPath,
|
|
9448
9514
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -9455,7 +9521,7 @@ var GlobTool = class extends BaseTool {
|
|
|
9455
9521
|
const withMtime = await Promise.all(
|
|
9456
9522
|
matches.map(async (rel) => {
|
|
9457
9523
|
try {
|
|
9458
|
-
const stat = await
|
|
9524
|
+
const stat = await fs10.stat(path26.join(searchPath, rel));
|
|
9459
9525
|
return { rel, mtime: stat.mtimeMs };
|
|
9460
9526
|
} catch {
|
|
9461
9527
|
return { rel, mtime: 0 };
|
|
@@ -9504,7 +9570,7 @@ var GrepTool = class extends BaseTool {
|
|
|
9504
9570
|
};
|
|
9505
9571
|
async execute(input, _options) {
|
|
9506
9572
|
const pattern = input["pattern"];
|
|
9507
|
-
const searchPath = input["path"] ?
|
|
9573
|
+
const searchPath = input["path"] ? path26.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
9508
9574
|
const globPattern = input["glob"];
|
|
9509
9575
|
const outputMode = input["output_mode"] ?? "content";
|
|
9510
9576
|
const context = input["context"] ?? 0;
|
|
@@ -9558,15 +9624,15 @@ var GrepTool = class extends BaseTool {
|
|
|
9558
9624
|
nodir: true
|
|
9559
9625
|
});
|
|
9560
9626
|
} catch {
|
|
9561
|
-
files = [
|
|
9627
|
+
files = [path26.relative(searchPath, searchPath) || "."];
|
|
9562
9628
|
}
|
|
9563
9629
|
const results = [];
|
|
9564
9630
|
let totalCount = 0;
|
|
9565
9631
|
for (const rel of files) {
|
|
9566
|
-
const abs =
|
|
9632
|
+
const abs = path26.join(searchPath, rel);
|
|
9567
9633
|
let content;
|
|
9568
9634
|
try {
|
|
9569
|
-
content = await
|
|
9635
|
+
content = await fs10.readFile(abs, "utf-8");
|
|
9570
9636
|
} catch {
|
|
9571
9637
|
continue;
|
|
9572
9638
|
}
|
|
@@ -9925,10 +9991,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
9925
9991
|
}
|
|
9926
9992
|
isIgnored(filePath) {
|
|
9927
9993
|
if (!filePath) return false;
|
|
9928
|
-
const abs =
|
|
9929
|
-
const rel =
|
|
9930
|
-
if (!rel || rel.startsWith("..") ||
|
|
9931
|
-
const posixRel = rel.split(
|
|
9994
|
+
const abs = path26.resolve(this.workspaceRoot, filePath);
|
|
9995
|
+
const rel = path26.relative(this.workspaceRoot, abs);
|
|
9996
|
+
if (!rel || rel.startsWith("..") || path26.isAbsolute(rel)) return true;
|
|
9997
|
+
const posixRel = rel.split(path26.sep).join("/");
|
|
9932
9998
|
return this.ignoreMatcher.ignores(posixRel);
|
|
9933
9999
|
}
|
|
9934
10000
|
};
|
|
@@ -10451,7 +10517,7 @@ var TaskAnalyzer = class {
|
|
|
10451
10517
|
analysisCache.clear();
|
|
10452
10518
|
}
|
|
10453
10519
|
};
|
|
10454
|
-
var DEFAULT_STATS_FILE =
|
|
10520
|
+
var DEFAULT_STATS_FILE = path26.join(os8.homedir(), ".cascade", "model-perf.json");
|
|
10455
10521
|
var ModelPerformanceTracker = class {
|
|
10456
10522
|
stats = /* @__PURE__ */ new Map();
|
|
10457
10523
|
featureStats = /* @__PURE__ */ new Map();
|
|
@@ -10464,7 +10530,7 @@ var ModelPerformanceTracker = class {
|
|
|
10464
10530
|
if (this.loaded) return;
|
|
10465
10531
|
this.loaded = true;
|
|
10466
10532
|
try {
|
|
10467
|
-
const raw = await
|
|
10533
|
+
const raw = await fs10.readFile(this.statsFile, "utf-8");
|
|
10468
10534
|
const parsed = JSON.parse(raw);
|
|
10469
10535
|
if (parsed.models) {
|
|
10470
10536
|
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
@@ -10483,12 +10549,12 @@ var ModelPerformanceTracker = class {
|
|
|
10483
10549
|
}
|
|
10484
10550
|
async save() {
|
|
10485
10551
|
try {
|
|
10486
|
-
await
|
|
10552
|
+
await fs10.mkdir(path26.dirname(this.statsFile), { recursive: true });
|
|
10487
10553
|
const modelsObj = {};
|
|
10488
10554
|
const featuresObj = {};
|
|
10489
10555
|
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
10490
10556
|
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
10491
|
-
await
|
|
10557
|
+
await fs10.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
10492
10558
|
} catch {
|
|
10493
10559
|
}
|
|
10494
10560
|
}
|
|
@@ -11022,9 +11088,9 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
11022
11088
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
11023
11089
|
async loadPersistedTools() {
|
|
11024
11090
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
11025
|
-
const file =
|
|
11091
|
+
const file = path26.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
11026
11092
|
try {
|
|
11027
|
-
const raw = await
|
|
11093
|
+
const raw = await fs10.readFile(file, "utf-8");
|
|
11028
11094
|
const specs = JSON.parse(raw);
|
|
11029
11095
|
if (!Array.isArray(specs)) return;
|
|
11030
11096
|
let loaded = 0;
|
|
@@ -11046,11 +11112,11 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
11046
11112
|
}
|
|
11047
11113
|
async persist() {
|
|
11048
11114
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
11049
|
-
const dir =
|
|
11050
|
-
const file =
|
|
11115
|
+
const dir = path26.join(this.workspacePath, ".cascade");
|
|
11116
|
+
const file = path26.join(dir, DYNAMIC_TOOLS_FILE);
|
|
11051
11117
|
try {
|
|
11052
|
-
await
|
|
11053
|
-
await
|
|
11118
|
+
await fs10.mkdir(dir, { recursive: true });
|
|
11119
|
+
await fs10.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
11054
11120
|
} catch (err) {
|
|
11055
11121
|
this.log(`[tool-creator] Failed to persist tools: ${err instanceof Error ? err.message : String(err)}`);
|
|
11056
11122
|
}
|
|
@@ -11067,12 +11133,12 @@ var WorldStateDB = class {
|
|
|
11067
11133
|
constructor(workspacePath, debugMode = false) {
|
|
11068
11134
|
this.workspacePath = workspacePath;
|
|
11069
11135
|
this.debugMode = debugMode;
|
|
11070
|
-
const cascadeDir =
|
|
11071
|
-
if (!
|
|
11072
|
-
|
|
11136
|
+
const cascadeDir = path26.join(workspacePath, ".cascade");
|
|
11137
|
+
if (!fs24.existsSync(cascadeDir)) {
|
|
11138
|
+
fs24.mkdirSync(cascadeDir, { recursive: true });
|
|
11073
11139
|
}
|
|
11074
|
-
this.keyPath =
|
|
11075
|
-
this.dbPath =
|
|
11140
|
+
this.keyPath = path26.join(cascadeDir, "world_state.key");
|
|
11141
|
+
this.dbPath = path26.join(cascadeDir, "world_state.db");
|
|
11076
11142
|
this.initEncryptionKey();
|
|
11077
11143
|
this.db = new Database2(this.dbPath);
|
|
11078
11144
|
this.db.pragma("journal_mode = WAL");
|
|
@@ -11102,11 +11168,11 @@ var WorldStateDB = class {
|
|
|
11102
11168
|
dbPath;
|
|
11103
11169
|
encryptionKey;
|
|
11104
11170
|
initEncryptionKey() {
|
|
11105
|
-
if (
|
|
11106
|
-
this.encryptionKey =
|
|
11171
|
+
if (fs24.existsSync(this.keyPath)) {
|
|
11172
|
+
this.encryptionKey = fs24.readFileSync(this.keyPath);
|
|
11107
11173
|
} else {
|
|
11108
11174
|
this.encryptionKey = crypto.randomBytes(32);
|
|
11109
|
-
|
|
11175
|
+
fs24.writeFileSync(this.keyPath, this.encryptionKey);
|
|
11110
11176
|
}
|
|
11111
11177
|
}
|
|
11112
11178
|
encrypt(text) {
|
|
@@ -11247,6 +11313,22 @@ var WorldStateDB = class {
|
|
|
11247
11313
|
}
|
|
11248
11314
|
return selected.slice(0, limit).map((f) => `- ${f.entity} ${f.relation} ${f.value}`).join("\n");
|
|
11249
11315
|
}
|
|
11316
|
+
/**
|
|
11317
|
+
* Delete one fact by its (normalized) entity + relation key. Returns whether
|
|
11318
|
+
* a row was actually removed. Powers the desktop Knowledge tab's per-fact
|
|
11319
|
+
* delete — users can prune what the planner remembers about their project.
|
|
11320
|
+
*/
|
|
11321
|
+
deleteFact(entity, relation) {
|
|
11322
|
+
const e = normalizeKey(entity);
|
|
11323
|
+
const r = normalizeKey(relation);
|
|
11324
|
+
if (!e || !r) return false;
|
|
11325
|
+
const result = this.db.prepare("DELETE FROM facts WHERE entity = ? AND relation = ?").run(e, r);
|
|
11326
|
+
return result.changes > 0;
|
|
11327
|
+
}
|
|
11328
|
+
/** Delete every fact. Returns how many were removed. */
|
|
11329
|
+
clearFacts() {
|
|
11330
|
+
return this.db.prepare("DELETE FROM facts").run().changes;
|
|
11331
|
+
}
|
|
11250
11332
|
// ── Export / Import ──────────────────────────
|
|
11251
11333
|
//
|
|
11252
11334
|
// Knowledge travels DECRYPTED in the export bundle (a portable plaintext
|
|
@@ -11295,9 +11377,9 @@ var WorldStateDB = class {
|
|
|
11295
11377
|
dumpDebugIfNeeded() {
|
|
11296
11378
|
if (!this.debugMode) return;
|
|
11297
11379
|
try {
|
|
11298
|
-
const dumpPath =
|
|
11380
|
+
const dumpPath = path26.join(os8.tmpdir(), "cascade_world_state_debug.json");
|
|
11299
11381
|
const entries = this.getAllEntries();
|
|
11300
|
-
|
|
11382
|
+
fs24.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
11301
11383
|
} catch (err) {
|
|
11302
11384
|
console.error("Failed to dump debug world state", err);
|
|
11303
11385
|
}
|
|
@@ -13715,10 +13797,10 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
13715
13797
|
}
|
|
13716
13798
|
} catch {
|
|
13717
13799
|
}
|
|
13718
|
-
const configPath =
|
|
13800
|
+
const configPath = path26.join(workspacePath, ".cascade", "config.json");
|
|
13719
13801
|
try {
|
|
13720
|
-
await
|
|
13721
|
-
await
|
|
13802
|
+
await fs10.mkdir(path26.dirname(configPath), { recursive: true });
|
|
13803
|
+
await fs10.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
13722
13804
|
} catch (err) {
|
|
13723
13805
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13724
13806
|
dispatch({
|
|
@@ -13769,9 +13851,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
13769
13851
|
if (msg.includes("non-text parts")) return;
|
|
13770
13852
|
originalLog(...args);
|
|
13771
13853
|
};
|
|
13772
|
-
const store = new MemoryStore(
|
|
13854
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
13773
13855
|
storeRef.current = store;
|
|
13774
|
-
globalStoreRef.current = new MemoryStore(
|
|
13856
|
+
globalStoreRef.current = new MemoryStore(path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
13775
13857
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
13776
13858
|
setIdentities(identityRows);
|
|
13777
13859
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -13834,14 +13916,14 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
13834
13916
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
13835
13917
|
onExport: async (fmt) => {
|
|
13836
13918
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13837
|
-
const exportPath =
|
|
13919
|
+
const exportPath = path26.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
|
|
13838
13920
|
if (fmt === "json") {
|
|
13839
|
-
await
|
|
13921
|
+
await fs10.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
13840
13922
|
} else {
|
|
13841
13923
|
const markdown = state.messages.map((msg) => `## ${msg.role.toUpperCase()} \u2014 ${msg.timestamp}
|
|
13842
13924
|
|
|
13843
13925
|
${msg.content}`).join("\n\n");
|
|
13844
|
-
await
|
|
13926
|
+
await fs10.writeFile(exportPath, markdown, "utf-8");
|
|
13845
13927
|
}
|
|
13846
13928
|
},
|
|
13847
13929
|
onRollback: async () => {
|
|
@@ -13851,7 +13933,7 @@ ${msg.content}`).join("\n\n");
|
|
|
13851
13933
|
if (!snapshots.length) return "No file snapshots found for this session.";
|
|
13852
13934
|
for (const { filePath, content } of snapshots) {
|
|
13853
13935
|
try {
|
|
13854
|
-
await
|
|
13936
|
+
await fs10.writeFile(filePath, content, "utf-8");
|
|
13855
13937
|
} catch (err) {
|
|
13856
13938
|
console.error(`Restore failed: ${filePath}`, err);
|
|
13857
13939
|
}
|
|
@@ -14730,9 +14812,9 @@ function stringifySlashOutput(val) {
|
|
|
14730
14812
|
}
|
|
14731
14813
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
14732
14814
|
if (!query) return "Usage: /search <query>";
|
|
14733
|
-
const dbPath =
|
|
14815
|
+
const dbPath = path26.join(workspacePath, CASCADE_DB_FILE);
|
|
14734
14816
|
try {
|
|
14735
|
-
await
|
|
14817
|
+
await fs10.access(dbPath);
|
|
14736
14818
|
} catch {
|
|
14737
14819
|
return "No database found. Start a conversation first.";
|
|
14738
14820
|
}
|
|
@@ -14764,7 +14846,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
14764
14846
|
async function diagnoseRuntime(config, workspacePath) {
|
|
14765
14847
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
14766
14848
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
14767
|
-
const store = new MemoryStore(
|
|
14849
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
14768
14850
|
try {
|
|
14769
14851
|
const sessions = store.listSessions(void 0, 3);
|
|
14770
14852
|
return [
|
|
@@ -14783,7 +14865,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
14783
14865
|
}
|
|
14784
14866
|
async function showRecentLogs(args, workspacePath) {
|
|
14785
14867
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
14786
|
-
const store = new MemoryStore(
|
|
14868
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
14787
14869
|
try {
|
|
14788
14870
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
14789
14871
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -14795,7 +14877,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
14795
14877
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
14796
14878
|
const sessionId = args[0];
|
|
14797
14879
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
14798
|
-
const store = new MemoryStore(
|
|
14880
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
14799
14881
|
try {
|
|
14800
14882
|
const session = store.getSession(sessionId);
|
|
14801
14883
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -15215,10 +15297,10 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
15215
15297
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
15216
15298
|
};
|
|
15217
15299
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
15218
|
-
const configDir =
|
|
15219
|
-
await
|
|
15220
|
-
const configPath =
|
|
15221
|
-
await
|
|
15300
|
+
const configDir = path26.join(workspacePath, ".cascade");
|
|
15301
|
+
await fs10.mkdir(configDir, { recursive: true });
|
|
15302
|
+
const configPath = path26.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
15303
|
+
await fs10.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
15222
15304
|
savedConfigRef.current = config;
|
|
15223
15305
|
dispatchRef.current({ type: "GO_DONE" });
|
|
15224
15306
|
} catch (err) {
|
|
@@ -15558,14 +15640,14 @@ function printTelemetryBanner() {
|
|
|
15558
15640
|
async function initCommand(workspacePath = process.cwd()) {
|
|
15559
15641
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
15560
15642
|
try {
|
|
15561
|
-
const configDir =
|
|
15562
|
-
await
|
|
15563
|
-
const mdPath =
|
|
15643
|
+
const configDir = path26.join(workspacePath, ".cascade");
|
|
15644
|
+
await fs10.mkdir(configDir, { recursive: true });
|
|
15645
|
+
const mdPath = path26.join(workspacePath, "CASCADE.md");
|
|
15564
15646
|
if (!await fileExists(mdPath)) {
|
|
15565
15647
|
await createDefaultCascadeMd(workspacePath);
|
|
15566
15648
|
spin.succeed(chalk9.green("Created CASCADE.md"));
|
|
15567
15649
|
}
|
|
15568
|
-
const ignorePath =
|
|
15650
|
+
const ignorePath = path26.join(workspacePath, ".cascadeignore");
|
|
15569
15651
|
if (!await fileExists(ignorePath)) {
|
|
15570
15652
|
await createDefaultIgnoreFile(workspacePath);
|
|
15571
15653
|
spin.succeed(chalk9.green("Created .cascadeignore"));
|
|
@@ -15574,7 +15656,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
15574
15656
|
console.log();
|
|
15575
15657
|
console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
15576
15658
|
console.log();
|
|
15577
|
-
const configPath =
|
|
15659
|
+
const configPath = path26.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
15578
15660
|
if (await fileExists(configPath)) {
|
|
15579
15661
|
console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
15580
15662
|
console.log();
|
|
@@ -15593,7 +15675,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
15593
15675
|
}
|
|
15594
15676
|
async function fileExists(p) {
|
|
15595
15677
|
try {
|
|
15596
|
-
await
|
|
15678
|
+
await fs10.access(p);
|
|
15597
15679
|
return true;
|
|
15598
15680
|
} catch {
|
|
15599
15681
|
return false;
|
|
@@ -15605,7 +15687,7 @@ init_constants();
|
|
|
15605
15687
|
var TOS_WARNING = "Reusing this subscription token outside its own CLI may violate the vendor\u2019s terms of service.";
|
|
15606
15688
|
async function readJson(file) {
|
|
15607
15689
|
try {
|
|
15608
|
-
const raw = await
|
|
15690
|
+
const raw = await fs10.readFile(file, "utf-8");
|
|
15609
15691
|
return JSON.parse(raw);
|
|
15610
15692
|
} catch {
|
|
15611
15693
|
return null;
|
|
@@ -15632,7 +15714,7 @@ function fromEnv(env) {
|
|
|
15632
15714
|
return out;
|
|
15633
15715
|
}
|
|
15634
15716
|
async function fromClaudeCode(home) {
|
|
15635
|
-
const file =
|
|
15717
|
+
const file = path26.join(home, ".claude", ".credentials.json");
|
|
15636
15718
|
const data = await readJson(file);
|
|
15637
15719
|
if (!data) return [];
|
|
15638
15720
|
const oauth = data["claudeAiOauth"];
|
|
@@ -15655,7 +15737,7 @@ async function fromClaudeCode(home) {
|
|
|
15655
15737
|
return [];
|
|
15656
15738
|
}
|
|
15657
15739
|
async function fromCodex(home) {
|
|
15658
|
-
const file =
|
|
15740
|
+
const file = path26.join(home, ".codex", "auth.json");
|
|
15659
15741
|
const data = await readJson(file);
|
|
15660
15742
|
if (!data) return [];
|
|
15661
15743
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -15678,7 +15760,7 @@ async function fromCodex(home) {
|
|
|
15678
15760
|
return [];
|
|
15679
15761
|
}
|
|
15680
15762
|
async function fromGemini(home) {
|
|
15681
|
-
const file =
|
|
15763
|
+
const file = path26.join(home, ".gemini", "oauth_creds.json");
|
|
15682
15764
|
const data = await readJson(file);
|
|
15683
15765
|
const accessToken = str(data?.["access_token"]);
|
|
15684
15766
|
if (!accessToken) return [];
|
|
@@ -15694,7 +15776,7 @@ async function fromGemini(home) {
|
|
|
15694
15776
|
}
|
|
15695
15777
|
async function fromCopilot(home) {
|
|
15696
15778
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
15697
|
-
const file =
|
|
15779
|
+
const file = path26.join(home, ".config", "github-copilot", name);
|
|
15698
15780
|
const data = await readJson(file);
|
|
15699
15781
|
if (!data) continue;
|
|
15700
15782
|
for (const value of Object.values(data)) {
|
|
@@ -15748,7 +15830,7 @@ async function doctorCommand() {
|
|
|
15748
15830
|
checks.push({
|
|
15749
15831
|
label: "Cascade config",
|
|
15750
15832
|
ok: true,
|
|
15751
|
-
detail: `Loaded ${
|
|
15833
|
+
detail: `Loaded ${path26.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
15752
15834
|
});
|
|
15753
15835
|
const providers = [
|
|
15754
15836
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -15904,6 +15986,11 @@ function authMiddleware(secret, required = true) {
|
|
|
15904
15986
|
}
|
|
15905
15987
|
const user = verifyToken(token, secret);
|
|
15906
15988
|
if (!user) {
|
|
15989
|
+
if (!required) {
|
|
15990
|
+
req.user = void 0;
|
|
15991
|
+
next();
|
|
15992
|
+
return;
|
|
15993
|
+
}
|
|
15907
15994
|
res.status(401).json({ error: "Invalid or expired token" });
|
|
15908
15995
|
return;
|
|
15909
15996
|
}
|
|
@@ -16036,6 +16123,25 @@ var DashboardSocket = class {
|
|
|
16036
16123
|
});
|
|
16037
16124
|
});
|
|
16038
16125
|
}
|
|
16126
|
+
/**
|
|
16127
|
+
* Boardroom plan decisions from a connected client. The desktop shows a
|
|
16128
|
+
* plan-review modal on `plan:approval-required` and answers here; the
|
|
16129
|
+
* server routes the decision into the paused run via resolvePlanApproval.
|
|
16130
|
+
*/
|
|
16131
|
+
onPlanDecision(callback) {
|
|
16132
|
+
this.io.on("connection", (socket) => {
|
|
16133
|
+
socket.on("plan:decision", (payload) => {
|
|
16134
|
+
if (typeof payload?.sessionId === "string" && typeof payload?.approved === "boolean") {
|
|
16135
|
+
callback({
|
|
16136
|
+
sessionId: payload.sessionId,
|
|
16137
|
+
approved: payload.approved,
|
|
16138
|
+
note: typeof payload.note === "string" && payload.note.trim() ? payload.note.trim() : void 0,
|
|
16139
|
+
editedPlan: payload.editedPlan
|
|
16140
|
+
});
|
|
16141
|
+
}
|
|
16142
|
+
});
|
|
16143
|
+
});
|
|
16144
|
+
}
|
|
16039
16145
|
onSessionSteer(callback) {
|
|
16040
16146
|
this.io.on("connection", (socket) => {
|
|
16041
16147
|
socket.on("session:steer", (payload) => {
|
|
@@ -16080,7 +16186,123 @@ var DashboardSocket = class {
|
|
|
16080
16186
|
|
|
16081
16187
|
// src/dashboard/server.ts
|
|
16082
16188
|
init_constants();
|
|
16083
|
-
var
|
|
16189
|
+
var TaskScheduler = class {
|
|
16190
|
+
cronJobs = /* @__PURE__ */ new Map();
|
|
16191
|
+
store;
|
|
16192
|
+
runner;
|
|
16193
|
+
constructor(store, runner) {
|
|
16194
|
+
this.store = store;
|
|
16195
|
+
this.runner = runner;
|
|
16196
|
+
}
|
|
16197
|
+
start() {
|
|
16198
|
+
const tasks = this.store.listScheduledTasks();
|
|
16199
|
+
for (const task of tasks) {
|
|
16200
|
+
if (task.enabled) this.schedule(task);
|
|
16201
|
+
}
|
|
16202
|
+
}
|
|
16203
|
+
stop() {
|
|
16204
|
+
for (const job of this.cronJobs.values()) job.stop();
|
|
16205
|
+
this.cronJobs.clear();
|
|
16206
|
+
}
|
|
16207
|
+
schedule(task) {
|
|
16208
|
+
if (!cron.validate(task.cronExpression)) {
|
|
16209
|
+
throw new Error(`Invalid cron expression: ${task.cronExpression}`);
|
|
16210
|
+
}
|
|
16211
|
+
const existing = this.cronJobs.get(task.id);
|
|
16212
|
+
if (existing) {
|
|
16213
|
+
try {
|
|
16214
|
+
existing.stop();
|
|
16215
|
+
} catch {
|
|
16216
|
+
}
|
|
16217
|
+
}
|
|
16218
|
+
const job = cron.schedule(task.cronExpression, async () => {
|
|
16219
|
+
try {
|
|
16220
|
+
task.lastRun = (/* @__PURE__ */ new Date()).toISOString();
|
|
16221
|
+
this.store.saveScheduledTask(task);
|
|
16222
|
+
await this.runner(task);
|
|
16223
|
+
} catch (err) {
|
|
16224
|
+
console.error(`[scheduler] Task "${task.id}" (${task.cronExpression}) failed:`, err);
|
|
16225
|
+
}
|
|
16226
|
+
}, { timezone: "UTC" });
|
|
16227
|
+
this.cronJobs.set(task.id, job);
|
|
16228
|
+
}
|
|
16229
|
+
unschedule(taskId) {
|
|
16230
|
+
this.cronJobs.get(taskId)?.stop();
|
|
16231
|
+
this.cronJobs.delete(taskId);
|
|
16232
|
+
}
|
|
16233
|
+
add(task) {
|
|
16234
|
+
this.store.saveScheduledTask(task);
|
|
16235
|
+
if (task.enabled) this.schedule(task);
|
|
16236
|
+
}
|
|
16237
|
+
remove(taskId) {
|
|
16238
|
+
this.unschedule(taskId);
|
|
16239
|
+
this.store.deleteScheduledTask(taskId);
|
|
16240
|
+
}
|
|
16241
|
+
list() {
|
|
16242
|
+
return this.store.listScheduledTasks();
|
|
16243
|
+
}
|
|
16244
|
+
isRunning(taskId) {
|
|
16245
|
+
return this.cronJobs.has(taskId);
|
|
16246
|
+
}
|
|
16247
|
+
static validateCron(expression) {
|
|
16248
|
+
return cron.validate(expression);
|
|
16249
|
+
}
|
|
16250
|
+
};
|
|
16251
|
+
|
|
16252
|
+
// src/dashboard/cost-stats.ts
|
|
16253
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
16254
|
+
function utcDateKey(d) {
|
|
16255
|
+
return d.toISOString().slice(0, 10);
|
|
16256
|
+
}
|
|
16257
|
+
function aggregateCostStats(sessions, opts = {}) {
|
|
16258
|
+
const days = Math.max(1, opts.days ?? 30);
|
|
16259
|
+
const topN = Math.max(1, opts.topN ?? 8);
|
|
16260
|
+
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
16261
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
16262
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
16263
|
+
const key = utcDateKey(new Date(now.getTime() - i * DAY_MS));
|
|
16264
|
+
buckets.set(key, { date: key, costUsd: 0, tokens: 0, runs: 0 });
|
|
16265
|
+
}
|
|
16266
|
+
let totalCostUsd = 0;
|
|
16267
|
+
let totalTokens = 0;
|
|
16268
|
+
let totalRuns = 0;
|
|
16269
|
+
for (const s of sessions) {
|
|
16270
|
+
const cost = s.metadata?.totalCostUsd ?? 0;
|
|
16271
|
+
const tokens = s.metadata?.totalTokens ?? 0;
|
|
16272
|
+
const runs = s.metadata?.taskCount ?? 0;
|
|
16273
|
+
totalCostUsd += cost;
|
|
16274
|
+
totalTokens += tokens;
|
|
16275
|
+
totalRuns += runs;
|
|
16276
|
+
const when = new Date(s.updatedAt || s.createdAt);
|
|
16277
|
+
if (!Number.isNaN(when.getTime())) {
|
|
16278
|
+
const bucket = buckets.get(utcDateKey(when));
|
|
16279
|
+
if (bucket) {
|
|
16280
|
+
bucket.costUsd += cost;
|
|
16281
|
+
bucket.tokens += tokens;
|
|
16282
|
+
bucket.runs += runs;
|
|
16283
|
+
}
|
|
16284
|
+
}
|
|
16285
|
+
}
|
|
16286
|
+
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) => ({
|
|
16287
|
+
sessionId: s.id,
|
|
16288
|
+
title: s.title,
|
|
16289
|
+
costUsd: s.metadata?.totalCostUsd ?? 0,
|
|
16290
|
+
tokens: s.metadata?.totalTokens ?? 0,
|
|
16291
|
+
runs: s.metadata?.taskCount ?? 0,
|
|
16292
|
+
updatedAt: s.updatedAt
|
|
16293
|
+
}));
|
|
16294
|
+
return {
|
|
16295
|
+
totalCostUsd,
|
|
16296
|
+
totalTokens,
|
|
16297
|
+
totalSessions: sessions.length,
|
|
16298
|
+
totalRuns,
|
|
16299
|
+
perDay: [...buckets.values()],
|
|
16300
|
+
topSessions
|
|
16301
|
+
};
|
|
16302
|
+
}
|
|
16303
|
+
|
|
16304
|
+
// src/dashboard/server.ts
|
|
16305
|
+
var __dirname$1 = path26.dirname(fileURLToPath(import.meta.url));
|
|
16084
16306
|
var DashboardServer = class {
|
|
16085
16307
|
app;
|
|
16086
16308
|
httpServer;
|
|
@@ -16105,6 +16327,14 @@ var DashboardServer = class {
|
|
|
16105
16327
|
* map is how that answer reaches the run that's blocked on it.
|
|
16106
16328
|
*/
|
|
16107
16329
|
pendingApprovals = /* @__PURE__ */ new Map();
|
|
16330
|
+
/**
|
|
16331
|
+
* The orchestration decision trail ("why") of each session's most recent
|
|
16332
|
+
* run — captured when the run ends so the desktop's Why panel can show it
|
|
16333
|
+
* after the fact. Bounded: oldest entry evicted past 50 sessions.
|
|
16334
|
+
*/
|
|
16335
|
+
whyBySession = /* @__PURE__ */ new Map();
|
|
16336
|
+
/** Cron scheduler for the Schedules UI — runs tasks while this server is up. */
|
|
16337
|
+
scheduler;
|
|
16108
16338
|
port;
|
|
16109
16339
|
host;
|
|
16110
16340
|
workspacePath;
|
|
@@ -16122,6 +16352,7 @@ var DashboardServer = class {
|
|
|
16122
16352
|
secret: this.dashboardSecret,
|
|
16123
16353
|
corsOrigin: config.dashboard.auth ? [`http://localhost:${this.port}`, `http://127.0.0.1:${this.port}`] : "*"
|
|
16124
16354
|
});
|
|
16355
|
+
this.scheduler = new TaskScheduler(store, (task) => this.runScheduledTask(task));
|
|
16125
16356
|
this.setupMiddleware();
|
|
16126
16357
|
this.setupRoutes();
|
|
16127
16358
|
this.socket.onSessionRate((sessionId, rating) => {
|
|
@@ -16185,6 +16416,9 @@ var DashboardServer = class {
|
|
|
16185
16416
|
cascade.on("peer:message", (e) => {
|
|
16186
16417
|
this.socket.emitPeerMessage(e);
|
|
16187
16418
|
});
|
|
16419
|
+
cascade.on("plan:approval-required", (e) => {
|
|
16420
|
+
this.socket.emitToSocket(socketId, "plan:approval-required", { sessionId, ...e });
|
|
16421
|
+
});
|
|
16188
16422
|
try {
|
|
16189
16423
|
const result = await cascade.run({
|
|
16190
16424
|
prompt: runPrompt,
|
|
@@ -16192,7 +16426,8 @@ var DashboardServer = class {
|
|
|
16192
16426
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16193
16427
|
});
|
|
16194
16428
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16195
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
16429
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16430
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16196
16431
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
16197
16432
|
this.socket.broadcast("cost:update", {
|
|
16198
16433
|
sessionId,
|
|
@@ -16202,6 +16437,7 @@ var DashboardServer = class {
|
|
|
16202
16437
|
this.throttledBroadcast("workspace");
|
|
16203
16438
|
} catch (err) {
|
|
16204
16439
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16440
|
+
this.captureWhy(sessionId, cascade);
|
|
16205
16441
|
this.socket.emitToSocket(socketId, "session:error", {
|
|
16206
16442
|
sessionId,
|
|
16207
16443
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16212,9 +16448,17 @@ var DashboardServer = class {
|
|
|
16212
16448
|
this.denyPendingApprovals(sessionId);
|
|
16213
16449
|
}
|
|
16214
16450
|
});
|
|
16451
|
+
this.socket.onPlanDecision(({ sessionId, approved, note, editedPlan }) => {
|
|
16452
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(
|
|
16453
|
+
approved,
|
|
16454
|
+
note,
|
|
16455
|
+
editedPlan
|
|
16456
|
+
);
|
|
16457
|
+
});
|
|
16215
16458
|
this.socket.onSessionHalt((sessionId) => {
|
|
16216
16459
|
this.activeControllers.get(sessionId)?.abort();
|
|
16217
16460
|
this.denyPendingApprovals(sessionId);
|
|
16461
|
+
this.activeSessions.get(sessionId)?.resolvePlanApproval(false);
|
|
16218
16462
|
});
|
|
16219
16463
|
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
16220
16464
|
const pending = this.pendingApprovals.get(requestId);
|
|
@@ -16247,12 +16491,21 @@ var DashboardServer = class {
|
|
|
16247
16491
|
resolve();
|
|
16248
16492
|
});
|
|
16249
16493
|
});
|
|
16494
|
+
try {
|
|
16495
|
+
this.scheduler.start();
|
|
16496
|
+
} catch (err) {
|
|
16497
|
+
console.warn("[dashboard] failed to start task scheduler:", err);
|
|
16498
|
+
}
|
|
16250
16499
|
}
|
|
16251
16500
|
async stop() {
|
|
16252
16501
|
if (this.broadcastTimer) {
|
|
16253
16502
|
clearTimeout(this.broadcastTimer);
|
|
16254
16503
|
this.broadcastTimer = null;
|
|
16255
16504
|
}
|
|
16505
|
+
try {
|
|
16506
|
+
this.scheduler.stop();
|
|
16507
|
+
} catch {
|
|
16508
|
+
}
|
|
16256
16509
|
this.socket.close();
|
|
16257
16510
|
try {
|
|
16258
16511
|
this.globalStore?.close();
|
|
@@ -16283,12 +16536,17 @@ var DashboardServer = class {
|
|
|
16283
16536
|
*/
|
|
16284
16537
|
persistConfig() {
|
|
16285
16538
|
try {
|
|
16286
|
-
const configPath =
|
|
16287
|
-
|
|
16288
|
-
|
|
16539
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
16540
|
+
fs24.mkdirSync(path26.dirname(configPath), { recursive: true });
|
|
16541
|
+
fs24.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
16289
16542
|
} catch (err) {
|
|
16290
16543
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
16291
16544
|
}
|
|
16545
|
+
try {
|
|
16546
|
+
saveGlobalCredentials(path26.join(os8.homedir(), GLOBAL_CONFIG_DIR), this.config.providers ?? []);
|
|
16547
|
+
} catch (err) {
|
|
16548
|
+
console.warn(`[dashboard] Failed to sync global credentials: ${err instanceof Error ? err.message : String(err)}`);
|
|
16549
|
+
}
|
|
16292
16550
|
}
|
|
16293
16551
|
/**
|
|
16294
16552
|
* Produce a stable dashboard JWT signing secret.
|
|
@@ -16300,15 +16558,15 @@ var DashboardServer = class {
|
|
|
16300
16558
|
resolveDashboardSecret() {
|
|
16301
16559
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
16302
16560
|
if (fromConfig) return fromConfig;
|
|
16303
|
-
const secretPath =
|
|
16561
|
+
const secretPath = path26.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
16304
16562
|
try {
|
|
16305
|
-
if (
|
|
16306
|
-
const existing =
|
|
16563
|
+
if (fs24.existsSync(secretPath)) {
|
|
16564
|
+
const existing = fs24.readFileSync(secretPath, "utf-8").trim();
|
|
16307
16565
|
if (existing.length >= 16) return existing;
|
|
16308
16566
|
}
|
|
16309
16567
|
const generated = randomUUID();
|
|
16310
|
-
|
|
16311
|
-
|
|
16568
|
+
fs24.mkdirSync(path26.dirname(secretPath), { recursive: true });
|
|
16569
|
+
fs24.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
16312
16570
|
if (this.config.dashboard.auth) {
|
|
16313
16571
|
console.warn(
|
|
16314
16572
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -16335,7 +16593,7 @@ var DashboardServer = class {
|
|
|
16335
16593
|
// ── Setup ─────────────────────────────────────
|
|
16336
16594
|
getGlobalStore() {
|
|
16337
16595
|
if (!this.globalStore) {
|
|
16338
|
-
const globalDbPath =
|
|
16596
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16339
16597
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
16340
16598
|
}
|
|
16341
16599
|
return this.globalStore;
|
|
@@ -16373,16 +16631,60 @@ var DashboardServer = class {
|
|
|
16373
16631
|
return title;
|
|
16374
16632
|
}
|
|
16375
16633
|
/** Record run end: the assistant reply (when there is one) and the runtime row's final status. */
|
|
16376
|
-
persistRunEnd(sessionId, title, latestPrompt, reply, status) {
|
|
16634
|
+
persistRunEnd(sessionId, title, latestPrompt, reply, status, result) {
|
|
16377
16635
|
try {
|
|
16378
16636
|
if (reply && reply.trim()) {
|
|
16379
16637
|
this.store.addMessage({ id: randomUUID(), sessionId, role: "assistant", content: reply, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16380
16638
|
}
|
|
16639
|
+
if (result) {
|
|
16640
|
+
const session = this.store.getSession(sessionId);
|
|
16641
|
+
if (session) {
|
|
16642
|
+
this.store.updateSession(sessionId, {
|
|
16643
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16644
|
+
metadata: {
|
|
16645
|
+
...session.metadata,
|
|
16646
|
+
totalTokens: session.metadata.totalTokens + result.usage.totalTokens,
|
|
16647
|
+
totalCostUsd: session.metadata.totalCostUsd + result.usage.estimatedCostUsd,
|
|
16648
|
+
taskCount: session.metadata.taskCount + 1
|
|
16649
|
+
}
|
|
16650
|
+
});
|
|
16651
|
+
}
|
|
16652
|
+
}
|
|
16381
16653
|
} catch (err) {
|
|
16382
16654
|
console.warn("[dashboard] failed to persist run end:", err);
|
|
16383
16655
|
}
|
|
16384
16656
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
16385
16657
|
}
|
|
16658
|
+
/**
|
|
16659
|
+
* Capture the run's decision trail + router economics ("why") and broadcast
|
|
16660
|
+
* it so the desktop's Why panel updates live; kept per-session for the
|
|
16661
|
+
* GET /api/sessions/:id/why fallback (panel opened after the run).
|
|
16662
|
+
*/
|
|
16663
|
+
captureWhy(sessionId, cascade, result) {
|
|
16664
|
+
try {
|
|
16665
|
+
const stats = cascade.getRouter().getStats();
|
|
16666
|
+
const savings = cascade.getRouter().getDelegationSavings();
|
|
16667
|
+
const report = {
|
|
16668
|
+
sessionId,
|
|
16669
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16670
|
+
decisions: cascade.getDecisionLog(),
|
|
16671
|
+
savedUsd: savings.savedUsd,
|
|
16672
|
+
savedPct: savings.savedPct,
|
|
16673
|
+
totalCostUsd: stats.totalCostUsd,
|
|
16674
|
+
totalTokens: stats.totalTokens,
|
|
16675
|
+
costByTier: stats.costByTier,
|
|
16676
|
+
durationMs: result?.durationMs
|
|
16677
|
+
};
|
|
16678
|
+
this.whyBySession.set(sessionId, report);
|
|
16679
|
+
if (this.whyBySession.size > 50) {
|
|
16680
|
+
const oldest = this.whyBySession.keys().next().value;
|
|
16681
|
+
if (oldest) this.whyBySession.delete(oldest);
|
|
16682
|
+
}
|
|
16683
|
+
this.socket.broadcast("run:why", report);
|
|
16684
|
+
} catch (err) {
|
|
16685
|
+
console.warn("[dashboard] failed to capture decision trail:", err);
|
|
16686
|
+
}
|
|
16687
|
+
}
|
|
16386
16688
|
/**
|
|
16387
16689
|
* Route steering text into running Cascade instances. Targets the given
|
|
16388
16690
|
* session, or every active session when none is specified (the desktop
|
|
@@ -16412,6 +16714,52 @@ var DashboardServer = class {
|
|
|
16412
16714
|
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
16413
16715
|
});
|
|
16414
16716
|
}
|
|
16717
|
+
/**
|
|
16718
|
+
* Execute one scheduled task firing. Runs headless (like `cascade run -p`):
|
|
16719
|
+
* tool approvals are auto-granted since nobody may be watching when the cron
|
|
16720
|
+
* fires — the Schedules UI states this. Events broadcast to every connected
|
|
16721
|
+
* client so an open desktop sees the run appear live.
|
|
16722
|
+
*/
|
|
16723
|
+
async runScheduledTask(task) {
|
|
16724
|
+
const sessionId = randomUUID();
|
|
16725
|
+
const prompt = task.prompt;
|
|
16726
|
+
const title = this.persistRunStart(sessionId, `[${task.name}] ${prompt}`);
|
|
16727
|
+
const cascade = new Cascade(this.config, task.workspacePath ?? this.workspacePath, this.store);
|
|
16728
|
+
this.activeSessions.set(sessionId, cascade);
|
|
16729
|
+
cascade.on("tier:status", (e) => {
|
|
16730
|
+
this.socket.broadcast("tier:status", { sessionId, ...e });
|
|
16731
|
+
});
|
|
16732
|
+
cascade.on("peer:message", (e) => {
|
|
16733
|
+
this.socket.emitPeerMessage(e);
|
|
16734
|
+
});
|
|
16735
|
+
try {
|
|
16736
|
+
const result = await cascade.run({
|
|
16737
|
+
prompt,
|
|
16738
|
+
identityId: task.identityId,
|
|
16739
|
+
approvalCallback: async () => ({ approved: true, always: false })
|
|
16740
|
+
});
|
|
16741
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
16742
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
16743
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16744
|
+
this.socket.broadcast("session:complete", { sessionId, result });
|
|
16745
|
+
this.socket.broadcast("cost:update", {
|
|
16746
|
+
sessionId,
|
|
16747
|
+
totalTokens: result.usage.totalTokens,
|
|
16748
|
+
totalCostUsd: result.usage.estimatedCostUsd
|
|
16749
|
+
});
|
|
16750
|
+
this.throttledBroadcast("workspace");
|
|
16751
|
+
} catch (err) {
|
|
16752
|
+
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
16753
|
+
this.captureWhy(sessionId, cascade);
|
|
16754
|
+
this.socket.broadcast("session:error", {
|
|
16755
|
+
sessionId,
|
|
16756
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16757
|
+
});
|
|
16758
|
+
throw err;
|
|
16759
|
+
} finally {
|
|
16760
|
+
this.activeSessions.delete(sessionId);
|
|
16761
|
+
}
|
|
16762
|
+
}
|
|
16415
16763
|
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
16416
16764
|
denyPendingApprovals(sessionId) {
|
|
16417
16765
|
for (const [id, pending] of this.pendingApprovals) {
|
|
@@ -16514,12 +16862,12 @@ ${prompt}`;
|
|
|
16514
16862
|
}
|
|
16515
16863
|
}
|
|
16516
16864
|
watchRuntimeChanges() {
|
|
16517
|
-
const workspaceDbPath =
|
|
16518
|
-
const globalDbPath =
|
|
16865
|
+
const workspaceDbPath = path26.join(this.workspacePath, CASCADE_DB_FILE);
|
|
16866
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16519
16867
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
16520
16868
|
for (const watchPath of watchPaths) {
|
|
16521
|
-
if (!
|
|
16522
|
-
|
|
16869
|
+
if (!fs24.existsSync(watchPath)) continue;
|
|
16870
|
+
fs24.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
16523
16871
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
16524
16872
|
});
|
|
16525
16873
|
}
|
|
@@ -16641,7 +16989,7 @@ ${prompt}`;
|
|
|
16641
16989
|
const sessionId = req.params.id;
|
|
16642
16990
|
this.store.deleteSession(sessionId);
|
|
16643
16991
|
this.store.deleteRuntimeSession(sessionId);
|
|
16644
|
-
const globalDbPath =
|
|
16992
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16645
16993
|
const globalStore = new MemoryStore(globalDbPath);
|
|
16646
16994
|
try {
|
|
16647
16995
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -16734,7 +17082,7 @@ ${prompt}`;
|
|
|
16734
17082
|
});
|
|
16735
17083
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
16736
17084
|
const body = req.body;
|
|
16737
|
-
const globalDbPath =
|
|
17085
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16738
17086
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
16739
17087
|
const globalStore = new MemoryStore(globalDbPath);
|
|
16740
17088
|
try {
|
|
@@ -16757,7 +17105,7 @@ ${prompt}`;
|
|
|
16757
17105
|
});
|
|
16758
17106
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
16759
17107
|
this.store.deleteAllRuntimeNodes();
|
|
16760
|
-
const globalDbPath =
|
|
17108
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16761
17109
|
const globalStore = new MemoryStore(globalDbPath);
|
|
16762
17110
|
try {
|
|
16763
17111
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -16843,12 +17191,12 @@ ${prompt}`;
|
|
|
16843
17191
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
16844
17192
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
16845
17193
|
try {
|
|
16846
|
-
const configPath =
|
|
16847
|
-
const existing =
|
|
17194
|
+
const configPath = path26.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
17195
|
+
const existing = fs24.existsSync(configPath) ? JSON.parse(fs24.readFileSync(configPath, "utf-8")) : {};
|
|
16848
17196
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
16849
17197
|
const tmp = configPath + ".tmp";
|
|
16850
|
-
|
|
16851
|
-
|
|
17198
|
+
fs24.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
17199
|
+
fs24.renameSync(tmp, configPath);
|
|
16852
17200
|
res.json({ ok: true });
|
|
16853
17201
|
} catch (err) {
|
|
16854
17202
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -16876,7 +17224,7 @@ ${prompt}`;
|
|
|
16876
17224
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
16877
17225
|
const scope = req.query["scope"] ?? "workspace";
|
|
16878
17226
|
if (scope === "global") {
|
|
16879
|
-
const globalDbPath =
|
|
17227
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
16880
17228
|
const globalStore = new MemoryStore(globalDbPath);
|
|
16881
17229
|
try {
|
|
16882
17230
|
res.json({
|
|
@@ -16924,6 +17272,9 @@ ${prompt}`;
|
|
|
16924
17272
|
cascade.on("peer:message", (e) => {
|
|
16925
17273
|
this.socket.emitPeerMessage(e);
|
|
16926
17274
|
});
|
|
17275
|
+
cascade.on("plan:approval-required", (e) => {
|
|
17276
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "plan:approval-required", { sessionId, ...e });
|
|
17277
|
+
});
|
|
16927
17278
|
try {
|
|
16928
17279
|
const result = await cascade.run({
|
|
16929
17280
|
prompt: runPrompt,
|
|
@@ -16931,7 +17282,8 @@ ${prompt}`;
|
|
|
16931
17282
|
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16932
17283
|
});
|
|
16933
17284
|
this.recordSessionTask(sessionId, result.taskId);
|
|
16934
|
-
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
17285
|
+
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED", result);
|
|
17286
|
+
this.captureWhy(sessionId, cascade, result);
|
|
16935
17287
|
this.socket.broadcast("cost:update", {
|
|
16936
17288
|
sessionId,
|
|
16937
17289
|
totalTokens: result.usage.totalTokens,
|
|
@@ -16941,6 +17293,7 @@ ${prompt}`;
|
|
|
16941
17293
|
this.throttledBroadcast("workspace");
|
|
16942
17294
|
} catch (err) {
|
|
16943
17295
|
this.persistRunEnd(sessionId, title, prompt, void 0, "FAILED");
|
|
17296
|
+
this.captureWhy(sessionId, cascade);
|
|
16944
17297
|
this.socket.broadcastToRoom(`session:${sessionId}`, "session:error", {
|
|
16945
17298
|
sessionId,
|
|
16946
17299
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -16962,13 +17315,217 @@ ${prompt}`;
|
|
|
16962
17315
|
}))
|
|
16963
17316
|
});
|
|
16964
17317
|
});
|
|
16965
|
-
|
|
16966
|
-
|
|
16967
|
-
|
|
16968
|
-
|
|
17318
|
+
this.app.get("/api/sessions/:id/why", auth, (req, res) => {
|
|
17319
|
+
const report = this.whyBySession.get(req.params.id);
|
|
17320
|
+
if (!report) {
|
|
17321
|
+
res.status(404).json({ error: "No decision trail recorded for this session in the current app run." });
|
|
17322
|
+
return;
|
|
17323
|
+
}
|
|
17324
|
+
res.json(report);
|
|
17325
|
+
});
|
|
17326
|
+
this.app.get("/api/sessions/:id/changes", auth, async (req, res) => {
|
|
17327
|
+
const sessionId = req.params.id;
|
|
17328
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17329
|
+
const before = /* @__PURE__ */ new Map();
|
|
17330
|
+
for (const taskId of taskIds) {
|
|
17331
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
17332
|
+
if (!before.has(filePath)) before.set(filePath, content);
|
|
17333
|
+
}
|
|
17334
|
+
}
|
|
17335
|
+
const { readFile } = await import('fs/promises');
|
|
17336
|
+
const MAX_DIFF_BYTES = 2 * 1024 * 1024;
|
|
17337
|
+
const changes = await Promise.all([...before.entries()].map(async ([filePath, beforeContent]) => {
|
|
17338
|
+
let after = "";
|
|
17339
|
+
let missing = false;
|
|
17340
|
+
try {
|
|
17341
|
+
const buf = await readFile(filePath);
|
|
17342
|
+
after = buf.length > MAX_DIFF_BYTES ? `[file too large to diff \u2014 ${buf.length} bytes]` : buf.toString("utf-8");
|
|
17343
|
+
} catch {
|
|
17344
|
+
missing = true;
|
|
17345
|
+
}
|
|
17346
|
+
return { filePath, before: beforeContent, after, missing, changed: missing || after !== beforeContent };
|
|
17347
|
+
}));
|
|
17348
|
+
res.json({ sessionId, changes: changes.filter((c) => c.changed) });
|
|
17349
|
+
});
|
|
17350
|
+
this.app.post("/api/sessions/:id/revert-file", auth, mutationLimiter, async (req, res) => {
|
|
17351
|
+
const sessionId = req.params.id;
|
|
17352
|
+
const body = req.body;
|
|
17353
|
+
if (!body.filePath || typeof body.filePath !== "string") {
|
|
17354
|
+
res.status(400).json({ error: "filePath is required" });
|
|
17355
|
+
return;
|
|
17356
|
+
}
|
|
17357
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
17358
|
+
let content;
|
|
17359
|
+
for (const taskId of taskIds) {
|
|
17360
|
+
const snap = this.store.getLatestFileSnapshots(taskId).find((s) => s.filePath === body.filePath);
|
|
17361
|
+
if (snap) {
|
|
17362
|
+
content = snap.content;
|
|
17363
|
+
break;
|
|
17364
|
+
}
|
|
17365
|
+
}
|
|
17366
|
+
if (content === void 0) {
|
|
17367
|
+
res.status(404).json({ error: "No snapshot recorded for that file in this session." });
|
|
17368
|
+
return;
|
|
17369
|
+
}
|
|
17370
|
+
try {
|
|
17371
|
+
const { writeFile } = await import('fs/promises');
|
|
17372
|
+
await writeFile(body.filePath, content, "utf-8");
|
|
17373
|
+
res.json({ ok: true, filePath: body.filePath });
|
|
17374
|
+
} catch (err) {
|
|
17375
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17376
|
+
}
|
|
17377
|
+
});
|
|
17378
|
+
this.app.get("/api/costs", auth, (_req, res) => {
|
|
17379
|
+
try {
|
|
17380
|
+
const sessions = this.store.listSessions(void 0, 1e3);
|
|
17381
|
+
const budget = this.config.budget ?? { warnAtPct: 80 };
|
|
17382
|
+
res.json({
|
|
17383
|
+
...aggregateCostStats(sessions, { days: 30, topN: 8 }),
|
|
17384
|
+
budget: {
|
|
17385
|
+
dailyBudgetUsd: budget.dailyBudgetUsd,
|
|
17386
|
+
sessionBudgetUsd: budget.sessionBudgetUsd,
|
|
17387
|
+
maxCostPerRunUsd: budget.maxCostPerRunUsd
|
|
17388
|
+
}
|
|
17389
|
+
});
|
|
17390
|
+
} catch (err) {
|
|
17391
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17392
|
+
}
|
|
17393
|
+
});
|
|
17394
|
+
this.app.get("/api/schedules", auth, (_req, res) => {
|
|
17395
|
+
try {
|
|
17396
|
+
res.json(this.scheduler.list().map((t) => ({ ...t, armed: this.scheduler.isRunning(t.id) })));
|
|
17397
|
+
} catch (err) {
|
|
17398
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17399
|
+
}
|
|
17400
|
+
});
|
|
17401
|
+
this.app.post("/api/schedules", auth, mutationLimiter, (req, res) => {
|
|
17402
|
+
const body = req.body;
|
|
17403
|
+
if (!body.name?.trim() || !body.cronExpression?.trim() || !body.prompt?.trim()) {
|
|
17404
|
+
res.status(400).json({ error: "name, cronExpression, and prompt are required" });
|
|
17405
|
+
return;
|
|
17406
|
+
}
|
|
17407
|
+
if (!TaskScheduler.validateCron(body.cronExpression.trim())) {
|
|
17408
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17409
|
+
return;
|
|
17410
|
+
}
|
|
17411
|
+
const task = {
|
|
17412
|
+
id: randomUUID(),
|
|
17413
|
+
name: body.name.trim(),
|
|
17414
|
+
cronExpression: body.cronExpression.trim(),
|
|
17415
|
+
prompt: body.prompt.trim(),
|
|
17416
|
+
workspacePath: this.workspacePath,
|
|
17417
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17418
|
+
enabled: body.enabled !== false
|
|
17419
|
+
};
|
|
17420
|
+
try {
|
|
17421
|
+
this.scheduler.add(task);
|
|
17422
|
+
res.json(task);
|
|
17423
|
+
} catch (err) {
|
|
17424
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17425
|
+
}
|
|
17426
|
+
});
|
|
17427
|
+
this.app.put("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17428
|
+
const id = req.params.id;
|
|
17429
|
+
const existing = this.scheduler.list().find((t) => t.id === id);
|
|
17430
|
+
if (!existing) {
|
|
17431
|
+
res.status(404).json({ error: "Not found" });
|
|
17432
|
+
return;
|
|
17433
|
+
}
|
|
17434
|
+
const body = req.body;
|
|
17435
|
+
if (body.cronExpression !== void 0 && !TaskScheduler.validateCron(body.cronExpression)) {
|
|
17436
|
+
res.status(400).json({ error: `Invalid cron expression: ${body.cronExpression}` });
|
|
17437
|
+
return;
|
|
17438
|
+
}
|
|
17439
|
+
const updated = {
|
|
17440
|
+
...existing,
|
|
17441
|
+
name: body.name?.trim() || existing.name,
|
|
17442
|
+
cronExpression: body.cronExpression?.trim() || existing.cronExpression,
|
|
17443
|
+
prompt: body.prompt?.trim() || existing.prompt,
|
|
17444
|
+
enabled: body.enabled ?? existing.enabled
|
|
17445
|
+
};
|
|
17446
|
+
try {
|
|
17447
|
+
this.scheduler.add(updated);
|
|
17448
|
+
if (!updated.enabled) this.scheduler.unschedule(id);
|
|
17449
|
+
res.json(updated);
|
|
17450
|
+
} catch (err) {
|
|
17451
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17452
|
+
}
|
|
17453
|
+
});
|
|
17454
|
+
this.app.delete("/api/schedules/:id", auth, mutationLimiter, (req, res) => {
|
|
17455
|
+
try {
|
|
17456
|
+
this.scheduler.remove(req.params.id);
|
|
17457
|
+
res.json({ ok: true });
|
|
17458
|
+
} catch (err) {
|
|
17459
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17460
|
+
}
|
|
17461
|
+
});
|
|
17462
|
+
this.app.get("/api/knowledge", auth, (_req, res) => {
|
|
17463
|
+
try {
|
|
17464
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
17465
|
+
try {
|
|
17466
|
+
const facts = ws.getAllFacts();
|
|
17467
|
+
res.json({ total: facts.length, facts });
|
|
17468
|
+
} finally {
|
|
17469
|
+
ws.close();
|
|
17470
|
+
}
|
|
17471
|
+
} catch (err) {
|
|
17472
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17473
|
+
}
|
|
17474
|
+
});
|
|
17475
|
+
this.app.delete("/api/knowledge/fact", auth, mutationLimiter, (req, res) => {
|
|
17476
|
+
const body = req.body;
|
|
17477
|
+
if (!body.entity || !body.relation) {
|
|
17478
|
+
res.status(400).json({ error: "entity and relation are required" });
|
|
17479
|
+
return;
|
|
17480
|
+
}
|
|
17481
|
+
try {
|
|
17482
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
17483
|
+
try {
|
|
17484
|
+
res.json({ ok: true, deleted: ws.deleteFact(body.entity, body.relation) });
|
|
17485
|
+
} finally {
|
|
17486
|
+
ws.close();
|
|
17487
|
+
}
|
|
17488
|
+
} catch (err) {
|
|
17489
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17490
|
+
}
|
|
17491
|
+
});
|
|
17492
|
+
this.app.delete("/api/knowledge", auth, mutationLimiter, (_req, res) => {
|
|
17493
|
+
try {
|
|
17494
|
+
const ws = new WorldStateDB(this.workspacePath);
|
|
17495
|
+
try {
|
|
17496
|
+
res.json({ ok: true, deleted: ws.clearFacts() });
|
|
17497
|
+
} finally {
|
|
17498
|
+
ws.close();
|
|
17499
|
+
}
|
|
17500
|
+
} catch (err) {
|
|
17501
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17502
|
+
}
|
|
17503
|
+
});
|
|
17504
|
+
this.app.get("/api/audit-chain", auth, async (req, res) => {
|
|
17505
|
+
try {
|
|
17506
|
+
const limit = Math.min(500, Math.max(1, parseInt(req.query["limit"] || "200", 10) || 200));
|
|
17507
|
+
const offset = Math.max(0, parseInt(req.query["offset"] || "0", 10) || 0);
|
|
17508
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
17509
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
17510
|
+
try {
|
|
17511
|
+
const all = logger.getAllLogs();
|
|
17512
|
+
const total = all.length;
|
|
17513
|
+
const entries = all.reverse().slice(offset, offset + limit);
|
|
17514
|
+
res.json({ total, offset, entries });
|
|
17515
|
+
} finally {
|
|
17516
|
+
logger.close();
|
|
17517
|
+
}
|
|
17518
|
+
} catch (err) {
|
|
17519
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
17520
|
+
}
|
|
17521
|
+
});
|
|
17522
|
+
const prodPath = path26.resolve(__dirname$1, "../web/dist");
|
|
17523
|
+
const devPath = path26.resolve(__dirname$1, "../../web/dist");
|
|
17524
|
+
const webDistPath = fs24.existsSync(prodPath) ? prodPath : devPath;
|
|
17525
|
+
if (fs24.existsSync(webDistPath)) {
|
|
16969
17526
|
this.app.use(express.static(webDistPath));
|
|
16970
17527
|
this.app.get("*", (_req, res) => {
|
|
16971
|
-
res.sendFile(
|
|
17528
|
+
res.sendFile(path26.join(webDistPath, "index.html"));
|
|
16972
17529
|
});
|
|
16973
17530
|
} else {
|
|
16974
17531
|
this.app.get("/", (_req, res) => {
|
|
@@ -16989,7 +17546,7 @@ init_constants();
|
|
|
16989
17546
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
16990
17547
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
16991
17548
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
16992
|
-
const store = new MemoryStore(
|
|
17549
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
16993
17550
|
const server = new DashboardServer(config, store, workspacePath);
|
|
16994
17551
|
server.watchRuntimeChanges();
|
|
16995
17552
|
const gThis = globalThis;
|
|
@@ -17021,7 +17578,7 @@ init_constants();
|
|
|
17021
17578
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
17022
17579
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
17023
17580
|
identity.command("list").description("List all available identities").action(() => {
|
|
17024
|
-
const store = new MemoryStore(
|
|
17581
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
17025
17582
|
try {
|
|
17026
17583
|
const identities = store.listIdentities();
|
|
17027
17584
|
console.log(chalk9.bold("\n Identities:"));
|
|
@@ -17041,7 +17598,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
17041
17598
|
}
|
|
17042
17599
|
});
|
|
17043
17600
|
identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
|
|
17044
|
-
const store = new MemoryStore(
|
|
17601
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
17045
17602
|
try {
|
|
17046
17603
|
if (options.default) {
|
|
17047
17604
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -17067,7 +17624,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
17067
17624
|
}
|
|
17068
17625
|
});
|
|
17069
17626
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
17070
|
-
const store = new MemoryStore(
|
|
17627
|
+
const store = new MemoryStore(path26.join(workspacePath, CASCADE_DB_FILE));
|
|
17071
17628
|
try {
|
|
17072
17629
|
const identities = store.listIdentities();
|
|
17073
17630
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -17195,11 +17752,11 @@ async function exportCommand(options = {}) {
|
|
|
17195
17752
|
let store;
|
|
17196
17753
|
try {
|
|
17197
17754
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
17198
|
-
const workspaceDbPath =
|
|
17199
|
-
const globalDbPath =
|
|
17755
|
+
const workspaceDbPath = path26.join(workspacePath, CASCADE_DB_FILE);
|
|
17756
|
+
const globalDbPath = path26.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
17200
17757
|
let dbPath = globalDbPath;
|
|
17201
17758
|
try {
|
|
17202
|
-
await
|
|
17759
|
+
await fs10.access(workspaceDbPath);
|
|
17203
17760
|
dbPath = workspaceDbPath;
|
|
17204
17761
|
} catch {
|
|
17205
17762
|
}
|
|
@@ -17238,8 +17795,8 @@ async function exportCommand(options = {}) {
|
|
|
17238
17795
|
const ext = format === "json" ? ".json" : ".md";
|
|
17239
17796
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
17240
17797
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
17241
|
-
const outPath = options.output ?
|
|
17242
|
-
await
|
|
17798
|
+
const outPath = options.output ? path26.resolve(options.output) : path26.join(process.cwd(), defaultFile);
|
|
17799
|
+
await fs10.writeFile(outPath, content, "utf-8");
|
|
17243
17800
|
spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
|
|
17244
17801
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
17245
17802
|
console.log();
|
|
@@ -17466,11 +18023,11 @@ async function statsCommand() {
|
|
|
17466
18023
|
dotenv.config();
|
|
17467
18024
|
function warnIfBuildIsStale() {
|
|
17468
18025
|
try {
|
|
17469
|
-
const here =
|
|
18026
|
+
const here = path26.dirname(fileURLToPath(import.meta.url));
|
|
17470
18027
|
for (const rel of ["..", "../.."]) {
|
|
17471
|
-
const pkgPath =
|
|
17472
|
-
if (!
|
|
17473
|
-
const pkg = JSON.parse(
|
|
18028
|
+
const pkgPath = path26.join(here, rel, "package.json");
|
|
18029
|
+
if (!fs24.existsSync(pkgPath)) continue;
|
|
18030
|
+
const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
|
|
17474
18031
|
if (pkg.name !== "cascade-ai") continue;
|
|
17475
18032
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
17476
18033
|
console.error(
|