cascade-ai 0.12.23 → 0.13.1
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/README.md +13 -2
- package/dist/cli.cjs +828 -177
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +823 -172
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1504 -893
- package/dist/index.cjs +779 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +139 -0
- package/dist/index.d.ts +139 -0
- package/dist/index.js +756 -140
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -6,20 +6,20 @@ import http, { createServer } from 'http';
|
|
|
6
6
|
import https from 'https';
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
|
+
import Database2 from 'better-sqlite3';
|
|
10
|
+
import path25 from 'path';
|
|
11
|
+
import fs23 from 'fs';
|
|
12
|
+
import os8 from 'os';
|
|
13
|
+
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
9
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
10
15
|
import { Command } from 'commander';
|
|
11
16
|
import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
12
17
|
import chalk9 from 'chalk';
|
|
13
18
|
import dotenv from 'dotenv';
|
|
14
|
-
import fs21 from 'fs';
|
|
15
|
-
import path23 from 'path';
|
|
16
19
|
import { fileURLToPath } from 'url';
|
|
17
20
|
import fs9 from 'fs/promises';
|
|
18
|
-
import os6 from 'os';
|
|
19
|
-
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
20
21
|
import * as _ignoreModule from 'ignore';
|
|
21
22
|
import _ignoreModule__default from 'ignore';
|
|
22
|
-
import Database from 'better-sqlite3';
|
|
23
23
|
import { z } from 'zod';
|
|
24
24
|
import { exec, execFile, execSync, spawnSync } from 'child_process';
|
|
25
25
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -58,7 +58,7 @@ var __export = (target, all) => {
|
|
|
58
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
59
|
var init_constants = __esm({
|
|
60
60
|
"src/constants.ts"() {
|
|
61
|
-
CASCADE_VERSION = "0.
|
|
61
|
+
CASCADE_VERSION = "0.13.1";
|
|
62
62
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
63
63
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
64
64
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -1525,6 +1525,149 @@ var init_openai_compatible = __esm({
|
|
|
1525
1525
|
}
|
|
1526
1526
|
});
|
|
1527
1527
|
|
|
1528
|
+
// src/core/audit/audit-logger.ts
|
|
1529
|
+
var audit_logger_exports = {};
|
|
1530
|
+
__export(audit_logger_exports, {
|
|
1531
|
+
AuditLogger: () => AuditLogger2
|
|
1532
|
+
});
|
|
1533
|
+
var AuditLogger2;
|
|
1534
|
+
var init_audit_logger = __esm({
|
|
1535
|
+
"src/core/audit/audit-logger.ts"() {
|
|
1536
|
+
AuditLogger2 = class {
|
|
1537
|
+
constructor(workspacePath, debugMode = false) {
|
|
1538
|
+
this.workspacePath = workspacePath;
|
|
1539
|
+
this.debugMode = debugMode;
|
|
1540
|
+
const cascadeDir = path25.join(workspacePath, ".cascade");
|
|
1541
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
1542
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
1543
|
+
}
|
|
1544
|
+
this.keyPath = path25.join(cascadeDir, "audit_log.key");
|
|
1545
|
+
this.dbPath = path25.join(cascadeDir, "audit_log.db");
|
|
1546
|
+
this.initEncryptionKey();
|
|
1547
|
+
this.db = new Database2(this.dbPath);
|
|
1548
|
+
this.db.pragma("journal_mode = WAL");
|
|
1549
|
+
this.db.exec(`
|
|
1550
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
1551
|
+
id TEXT PRIMARY KEY,
|
|
1552
|
+
timestamp TEXT NOT NULL,
|
|
1553
|
+
event_type TEXT NOT NULL,
|
|
1554
|
+
tier_id TEXT NOT NULL,
|
|
1555
|
+
encrypted_payload TEXT NOT NULL,
|
|
1556
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
1557
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
1558
|
+
)
|
|
1559
|
+
`);
|
|
1560
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
1561
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
1562
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
1563
|
+
}
|
|
1564
|
+
workspacePath;
|
|
1565
|
+
debugMode;
|
|
1566
|
+
db;
|
|
1567
|
+
keyPath;
|
|
1568
|
+
dbPath;
|
|
1569
|
+
encryptionKey;
|
|
1570
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
1571
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
1572
|
+
return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
1573
|
+
}
|
|
1574
|
+
initEncryptionKey() {
|
|
1575
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
1576
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
1577
|
+
} else {
|
|
1578
|
+
this.encryptionKey = crypto.randomBytes(32);
|
|
1579
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
encrypt(text) {
|
|
1583
|
+
const iv = crypto.randomBytes(16);
|
|
1584
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1585
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
1586
|
+
encrypted += cipher.final("hex");
|
|
1587
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
1588
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
1589
|
+
}
|
|
1590
|
+
decrypt(text) {
|
|
1591
|
+
const parts = text.split(":");
|
|
1592
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
1593
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
1594
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
1595
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
1596
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1597
|
+
decipher.setAuthTag(authTag);
|
|
1598
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
1599
|
+
decrypted += decipher.final("utf8");
|
|
1600
|
+
return decrypted;
|
|
1601
|
+
}
|
|
1602
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
1603
|
+
const id = crypto.randomUUID();
|
|
1604
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1605
|
+
const payload = JSON.stringify(payloadObj);
|
|
1606
|
+
const encryptedPayload = this.encrypt(payload);
|
|
1607
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
1608
|
+
const prevHash = last?.hash ?? "";
|
|
1609
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
1610
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
1611
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
1612
|
+
this.dumpDebugIfNeeded();
|
|
1613
|
+
return id;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
1617
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
1618
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
1619
|
+
* can only be claimed for chained history.
|
|
1620
|
+
*/
|
|
1621
|
+
verifyChain() {
|
|
1622
|
+
const rows = this.db.prepare(
|
|
1623
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
1624
|
+
).all();
|
|
1625
|
+
let prevHash = "";
|
|
1626
|
+
for (const row of rows) {
|
|
1627
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
1628
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
1629
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
1630
|
+
}
|
|
1631
|
+
prevHash = row.hash;
|
|
1632
|
+
}
|
|
1633
|
+
return { ok: true, entries: rows.length };
|
|
1634
|
+
}
|
|
1635
|
+
getAllLogs() {
|
|
1636
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
1637
|
+
const rows = stmt.all();
|
|
1638
|
+
return rows.map((row) => {
|
|
1639
|
+
let payload = "";
|
|
1640
|
+
try {
|
|
1641
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
id: row.id,
|
|
1647
|
+
timestamp: row.timestamp,
|
|
1648
|
+
eventType: row.event_type,
|
|
1649
|
+
tierId: row.tier_id,
|
|
1650
|
+
payload
|
|
1651
|
+
};
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
dumpDebugIfNeeded() {
|
|
1655
|
+
if (!this.debugMode) return;
|
|
1656
|
+
try {
|
|
1657
|
+
const dumpPath = path25.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
|
|
1658
|
+
const entries = this.getAllLogs();
|
|
1659
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
console.error("Failed to dump debug audit logs", err);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
close() {
|
|
1665
|
+
this.db.close();
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1528
1671
|
// src/cli/index.ts
|
|
1529
1672
|
init_constants();
|
|
1530
1673
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -1568,7 +1711,7 @@ var Keystore = class {
|
|
|
1568
1711
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
1569
1712
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
1570
1713
|
this.backend = "keytar";
|
|
1571
|
-
if (password &&
|
|
1714
|
+
if (password && fs23.existsSync(this.storePath)) {
|
|
1572
1715
|
try {
|
|
1573
1716
|
const fileEntries = this.decryptFile(password);
|
|
1574
1717
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -1587,7 +1730,7 @@ var Keystore = class {
|
|
|
1587
1730
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
1588
1731
|
);
|
|
1589
1732
|
}
|
|
1590
|
-
if (!
|
|
1733
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
1591
1734
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1592
1735
|
this.masterKey = this.deriveKey(password, salt);
|
|
1593
1736
|
this.writeWithSalt({}, salt);
|
|
@@ -1601,7 +1744,7 @@ var Keystore = class {
|
|
|
1601
1744
|
}
|
|
1602
1745
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
1603
1746
|
unlockSync(password) {
|
|
1604
|
-
if (!
|
|
1747
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
1605
1748
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1606
1749
|
this.masterKey = this.deriveKey(password, salt);
|
|
1607
1750
|
this.writeWithSalt({}, salt);
|
|
@@ -1659,7 +1802,7 @@ var Keystore = class {
|
|
|
1659
1802
|
}
|
|
1660
1803
|
}
|
|
1661
1804
|
decryptFile(password, knownSalt) {
|
|
1662
|
-
if (!
|
|
1805
|
+
if (!fs23.existsSync(this.storePath)) return {};
|
|
1663
1806
|
try {
|
|
1664
1807
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
1665
1808
|
const useSalt = knownSalt ?? salt;
|
|
@@ -1681,8 +1824,8 @@ var Keystore = class {
|
|
|
1681
1824
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1682
1825
|
const tag = cipher.getAuthTag();
|
|
1683
1826
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
1684
|
-
|
|
1685
|
-
|
|
1827
|
+
fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
|
|
1828
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1686
1829
|
}
|
|
1687
1830
|
writeWithSalt(data, salt) {
|
|
1688
1831
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -1692,11 +1835,11 @@ var Keystore = class {
|
|
|
1692
1835
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1693
1836
|
const tag = cipher.getAuthTag();
|
|
1694
1837
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
1695
|
-
|
|
1696
|
-
|
|
1838
|
+
fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
|
|
1839
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1697
1840
|
}
|
|
1698
1841
|
readRaw() {
|
|
1699
|
-
const buf =
|
|
1842
|
+
const buf = fs23.readFileSync(this.storePath);
|
|
1700
1843
|
let offset = 0;
|
|
1701
1844
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
1702
1845
|
offset += SALT_LEN;
|
|
@@ -1729,7 +1872,7 @@ var CascadeIgnore = class {
|
|
|
1729
1872
|
]);
|
|
1730
1873
|
}
|
|
1731
1874
|
async load(workspacePath) {
|
|
1732
|
-
const filePath =
|
|
1875
|
+
const filePath = path25.join(workspacePath, ".cascadeignore");
|
|
1733
1876
|
try {
|
|
1734
1877
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
1735
1878
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -1740,7 +1883,7 @@ var CascadeIgnore = class {
|
|
|
1740
1883
|
}
|
|
1741
1884
|
isIgnored(filePath, workspacePath) {
|
|
1742
1885
|
try {
|
|
1743
|
-
const relative = workspacePath ?
|
|
1886
|
+
const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
|
|
1744
1887
|
return this.ig.ignores(relative);
|
|
1745
1888
|
} catch {
|
|
1746
1889
|
return false;
|
|
@@ -1751,7 +1894,7 @@ var CascadeIgnore = class {
|
|
|
1751
1894
|
}
|
|
1752
1895
|
};
|
|
1753
1896
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
1754
|
-
const filePath =
|
|
1897
|
+
const filePath = path25.join(workspacePath, ".cascadeignore");
|
|
1755
1898
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
1756
1899
|
# Syntax identical to .gitignore
|
|
1757
1900
|
|
|
@@ -1781,7 +1924,7 @@ Thumbs.db
|
|
|
1781
1924
|
await fs9.writeFile(filePath, content, "utf-8");
|
|
1782
1925
|
}
|
|
1783
1926
|
async function loadCascadeMd(workspacePath) {
|
|
1784
|
-
const filePath =
|
|
1927
|
+
const filePath = path25.join(workspacePath, "CASCADE.md");
|
|
1785
1928
|
try {
|
|
1786
1929
|
const raw = await fs9.readFile(filePath, "utf-8");
|
|
1787
1930
|
return parseCascadeMd(raw);
|
|
@@ -1810,7 +1953,7 @@ ${raw.trim()}`;
|
|
|
1810
1953
|
return { raw, sections, systemPrompt };
|
|
1811
1954
|
}
|
|
1812
1955
|
async function createDefaultCascadeMd(workspacePath) {
|
|
1813
|
-
const filePath =
|
|
1956
|
+
const filePath = path25.join(workspacePath, "CASCADE.md");
|
|
1814
1957
|
const content = `# Cascade Project Instructions
|
|
1815
1958
|
|
|
1816
1959
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -1845,9 +1988,9 @@ List any areas the agent should not touch.
|
|
|
1845
1988
|
var MemoryStore = class _MemoryStore {
|
|
1846
1989
|
db;
|
|
1847
1990
|
constructor(dbPath) {
|
|
1848
|
-
|
|
1991
|
+
fs23.mkdirSync(path25.dirname(dbPath), { recursive: true });
|
|
1849
1992
|
try {
|
|
1850
|
-
this.db = new
|
|
1993
|
+
this.db = new Database2(dbPath, { timeout: 5e3 });
|
|
1851
1994
|
this.db.pragma("journal_mode = WAL");
|
|
1852
1995
|
this.db.pragma("foreign_keys = ON");
|
|
1853
1996
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -2692,7 +2835,8 @@ var WorkspaceConfigSchema = z.object({
|
|
|
2692
2835
|
cascadeMdPath: z.string().default("CASCADE.md"),
|
|
2693
2836
|
configPath: z.string().default(".cascade/config.json"),
|
|
2694
2837
|
keystorePath: z.string().default(".cascade/keystore.enc"),
|
|
2695
|
-
auditLogPath: z.string().default(".cascade/audit.log")
|
|
2838
|
+
auditLogPath: z.string().default(".cascade/audit.log"),
|
|
2839
|
+
debugWorldState: z.boolean().default(false)
|
|
2696
2840
|
});
|
|
2697
2841
|
var CascadeConfigSchema = z.object({
|
|
2698
2842
|
version: z.literal("1.0").default("1.0"),
|
|
@@ -2837,6 +2981,14 @@ var CascadeConfigSchema = z.object({
|
|
|
2837
2981
|
* 'parallel' / 'sequential' — force it.
|
|
2838
2982
|
*/
|
|
2839
2983
|
t3Execution: z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
2984
|
+
/**
|
|
2985
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
2986
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
2987
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
2988
|
+
*/
|
|
2989
|
+
privacy: z.object({
|
|
2990
|
+
paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
|
|
2991
|
+
}).optional(),
|
|
2840
2992
|
/**
|
|
2841
2993
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
2842
2994
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -2886,15 +3038,15 @@ var ConfigManager = class {
|
|
|
2886
3038
|
globalDir;
|
|
2887
3039
|
constructor(workspacePath = process.cwd()) {
|
|
2888
3040
|
this.workspacePath = workspacePath;
|
|
2889
|
-
this.globalDir =
|
|
3041
|
+
this.globalDir = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR);
|
|
2890
3042
|
}
|
|
2891
3043
|
async load() {
|
|
2892
3044
|
this.config = await this.loadConfig();
|
|
2893
3045
|
this.ignore = new CascadeIgnore();
|
|
2894
3046
|
await this.ignore.load(this.workspacePath);
|
|
2895
3047
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
2896
|
-
this.keystore = new Keystore(
|
|
2897
|
-
this.store = new MemoryStore(
|
|
3048
|
+
this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
3049
|
+
this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
|
|
2898
3050
|
await this.injectEnvKeys();
|
|
2899
3051
|
await this.ensureDefaultIdentity();
|
|
2900
3052
|
}
|
|
@@ -2917,8 +3069,8 @@ var ConfigManager = class {
|
|
|
2917
3069
|
return this.workspacePath;
|
|
2918
3070
|
}
|
|
2919
3071
|
async save() {
|
|
2920
|
-
const configPath =
|
|
2921
|
-
await fs9.mkdir(
|
|
3072
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
3073
|
+
await fs9.mkdir(path25.dirname(configPath), { recursive: true });
|
|
2922
3074
|
await fs9.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
2923
3075
|
}
|
|
2924
3076
|
async updateConfig(updates) {
|
|
@@ -2942,7 +3094,7 @@ var ConfigManager = class {
|
|
|
2942
3094
|
return configProvider?.apiKey;
|
|
2943
3095
|
}
|
|
2944
3096
|
async loadConfig() {
|
|
2945
|
-
const configPath =
|
|
3097
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
2946
3098
|
try {
|
|
2947
3099
|
const raw = await fs9.readFile(configPath, "utf-8");
|
|
2948
3100
|
return validateConfig(JSON.parse(raw));
|
|
@@ -3821,7 +3973,7 @@ init_constants();
|
|
|
3821
3973
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
3822
3974
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
3823
3975
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
3824
|
-
var DEFAULT_CACHE_FILE =
|
|
3976
|
+
var DEFAULT_CACHE_FILE = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
3825
3977
|
function normalizeModelId(id) {
|
|
3826
3978
|
let s = id.toLowerCase();
|
|
3827
3979
|
const slash = s.lastIndexOf("/");
|
|
@@ -3936,7 +4088,7 @@ var LiveDataProvider = class {
|
|
|
3936
4088
|
}
|
|
3937
4089
|
async saveCache() {
|
|
3938
4090
|
try {
|
|
3939
|
-
await fs9.mkdir(
|
|
4091
|
+
await fs9.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
|
|
3940
4092
|
const cache = {
|
|
3941
4093
|
fetchedAt: this.fetchedAt,
|
|
3942
4094
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -4066,7 +4218,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4066
4218
|
costByTier: {},
|
|
4067
4219
|
tokensByTier: {},
|
|
4068
4220
|
inputTokensByTier: {},
|
|
4069
|
-
outputTokensByTier: {}
|
|
4221
|
+
outputTokensByTier: {},
|
|
4222
|
+
costByFeature: {}
|
|
4070
4223
|
};
|
|
4071
4224
|
tierModels = /* @__PURE__ */ new Map();
|
|
4072
4225
|
config;
|
|
@@ -4088,6 +4241,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4088
4241
|
tpmLimiter;
|
|
4089
4242
|
localQueue;
|
|
4090
4243
|
taskAnalyzer;
|
|
4244
|
+
worldStateDB;
|
|
4245
|
+
privacyPaths;
|
|
4246
|
+
guidanceQueue;
|
|
4091
4247
|
liveData;
|
|
4092
4248
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
4093
4249
|
originalTierModels;
|
|
@@ -4245,7 +4401,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4245
4401
|
if (options.model && !requireVision) {
|
|
4246
4402
|
this.ensureProvider(options.model, this.config.providers);
|
|
4247
4403
|
}
|
|
4248
|
-
|
|
4404
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
4405
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
4406
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
4407
|
+
if (!localModel) {
|
|
4408
|
+
throw new Error(
|
|
4409
|
+
"privacy.paths: this subtask touches a local-only path but no LOCAL model is available. Configure Ollama or an OpenAI-compatible endpoint on a loopback/private host, or remove the privacy policy."
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
4413
|
+
model = localModel;
|
|
4414
|
+
}
|
|
4249
4415
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
4250
4416
|
const provider = this.getProvider(model);
|
|
4251
4417
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -4317,7 +4483,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4317
4483
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
4318
4484
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
4319
4485
|
}
|
|
4320
|
-
this.recordStats(tier, model, result.usage);
|
|
4486
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
4321
4487
|
this.failover.recordSuccess(model.provider);
|
|
4322
4488
|
return result;
|
|
4323
4489
|
} catch (err) {
|
|
@@ -4422,6 +4588,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4422
4588
|
setTaskAnalyzer(analyzer) {
|
|
4423
4589
|
this.taskAnalyzer = analyzer;
|
|
4424
4590
|
}
|
|
4591
|
+
setWorldStateDB(db) {
|
|
4592
|
+
this.worldStateDB = db;
|
|
4593
|
+
}
|
|
4594
|
+
getWorldStateDB() {
|
|
4595
|
+
return this.worldStateDB;
|
|
4596
|
+
}
|
|
4597
|
+
setPrivacyPaths(paths) {
|
|
4598
|
+
this.privacyPaths = paths;
|
|
4599
|
+
}
|
|
4600
|
+
getPrivacyPaths() {
|
|
4601
|
+
return this.privacyPaths;
|
|
4602
|
+
}
|
|
4603
|
+
setGuidanceQueue(queue) {
|
|
4604
|
+
this.guidanceQueue = queue;
|
|
4605
|
+
}
|
|
4606
|
+
getGuidanceQueue() {
|
|
4607
|
+
return this.guidanceQueue;
|
|
4608
|
+
}
|
|
4609
|
+
/**
|
|
4610
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
4611
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
4612
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
4613
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
4614
|
+
*/
|
|
4615
|
+
isPrivateModel(model) {
|
|
4616
|
+
if (model.isLocal) return true;
|
|
4617
|
+
if (model.provider !== "openai-compatible") return false;
|
|
4618
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
4619
|
+
if (!baseUrl) return false;
|
|
4620
|
+
try {
|
|
4621
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
4622
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]" || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host) || host.endsWith(".local");
|
|
4623
|
+
} catch {
|
|
4624
|
+
return false;
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4425
4627
|
/**
|
|
4426
4628
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
4427
4629
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -4445,7 +4647,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4445
4647
|
costByTier: { ...this.stats.costByTier },
|
|
4446
4648
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
4447
4649
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
4448
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
4650
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
4651
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
4449
4652
|
};
|
|
4450
4653
|
}
|
|
4451
4654
|
/**
|
|
@@ -4495,7 +4698,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4495
4698
|
costByTier: {},
|
|
4496
4699
|
tokensByTier: {},
|
|
4497
4700
|
inputTokensByTier: {},
|
|
4498
|
-
outputTokensByTier: {}
|
|
4701
|
+
outputTokensByTier: {},
|
|
4702
|
+
costByFeature: {}
|
|
4499
4703
|
};
|
|
4500
4704
|
this.sessionCostUsd = 0;
|
|
4501
4705
|
this.budgetState = "ok";
|
|
@@ -4661,7 +4865,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4661
4865
|
}
|
|
4662
4866
|
return void 0;
|
|
4663
4867
|
}
|
|
4664
|
-
recordStats(tier, model, usage) {
|
|
4868
|
+
recordStats(tier, model, usage, featureTag) {
|
|
4665
4869
|
this.stats.totalTokens += usage.totalTokens;
|
|
4666
4870
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
4667
4871
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -4671,6 +4875,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4671
4875
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
4672
4876
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
4673
4877
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
4878
|
+
if (featureTag) {
|
|
4879
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
4880
|
+
}
|
|
4674
4881
|
this.runTokens += usage.totalTokens;
|
|
4675
4882
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
4676
4883
|
this.updateBudgetState();
|
|
@@ -5208,6 +5415,8 @@ var T3Worker = class extends BaseTier {
|
|
|
5208
5415
|
toolRegistry;
|
|
5209
5416
|
context;
|
|
5210
5417
|
assignment;
|
|
5418
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
5419
|
+
localOnlyMatch = false;
|
|
5211
5420
|
peerSyncBuffer = [];
|
|
5212
5421
|
store;
|
|
5213
5422
|
audit;
|
|
@@ -5263,6 +5472,11 @@ var T3Worker = class extends BaseTier {
|
|
|
5263
5472
|
this.taskId = taskId;
|
|
5264
5473
|
this.setLabel(assignment.subtaskTitle);
|
|
5265
5474
|
this.setStatus("ACTIVE");
|
|
5475
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
5476
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
5477
|
+
if (this.localOnlyMatch) {
|
|
5478
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
5479
|
+
}
|
|
5266
5480
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
5267
5481
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
5268
5482
|
this.tools = [...this.tools, {
|
|
@@ -5393,9 +5607,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5393
5607
|
}
|
|
5394
5608
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
5395
5609
|
if (reflectCfg.enabled) {
|
|
5396
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
5610
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
5397
5611
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
5398
5612
|
}
|
|
5613
|
+
const db = this.router.getWorldStateDB?.();
|
|
5614
|
+
if (db) {
|
|
5615
|
+
try {
|
|
5616
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
5617
|
+
} catch (e) {
|
|
5618
|
+
this.log("Failed to write to World State DB");
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5399
5621
|
this.setStatus("COMPLETED", output);
|
|
5400
5622
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
5401
5623
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -5468,6 +5690,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
5468
5690
|
while (iterations < MAX_ITERATIONS) {
|
|
5469
5691
|
iterations++;
|
|
5470
5692
|
this.throwIfCancelled();
|
|
5693
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
5694
|
+
for (const g of guidance) {
|
|
5695
|
+
this.log(`User intervention received: ${g.text}`);
|
|
5696
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
5697
|
+
await this.context.addMessage({
|
|
5698
|
+
role: "user",
|
|
5699
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
5700
|
+
${g.text}`
|
|
5701
|
+
});
|
|
5702
|
+
}
|
|
5471
5703
|
const options = {
|
|
5472
5704
|
messages: this.context.getMessages(),
|
|
5473
5705
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -5476,7 +5708,9 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5476
5708
|
// Don't pass tools array when model can't use them natively
|
|
5477
5709
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
5478
5710
|
maxTokens: 4096,
|
|
5479
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
5711
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
5712
|
+
featureTag: this.assignment?.sectionTitle,
|
|
5713
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5480
5714
|
};
|
|
5481
5715
|
const result = await this.router.generate(
|
|
5482
5716
|
"T3",
|
|
@@ -5640,8 +5874,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5640
5874
|
tierId: this.id,
|
|
5641
5875
|
sessionId: this.taskId,
|
|
5642
5876
|
requireApproval: false,
|
|
5643
|
-
saveSnapshot: async (
|
|
5644
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
5877
|
+
saveSnapshot: async (path30, content) => {
|
|
5878
|
+
this.store?.addFileSnapshot(this.taskId, path30, content);
|
|
5645
5879
|
},
|
|
5646
5880
|
sendPeerSync: (to, syncType, content) => {
|
|
5647
5881
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -5811,7 +6045,7 @@ ${assignment.expectedOutput}`;
|
|
|
5811
6045
|
const { promisify: promisify4 } = await import('util');
|
|
5812
6046
|
const execAsync3 = promisify4(exec3);
|
|
5813
6047
|
for (const artifactPath of artifactPaths) {
|
|
5814
|
-
const absolutePath =
|
|
6048
|
+
const absolutePath = path25.resolve(process.cwd(), artifactPath);
|
|
5815
6049
|
try {
|
|
5816
6050
|
const stat = await fs9.stat(absolutePath);
|
|
5817
6051
|
if (!stat.isFile()) {
|
|
@@ -5832,7 +6066,7 @@ ${assignment.expectedOutput}`;
|
|
|
5832
6066
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
5833
6067
|
continue;
|
|
5834
6068
|
}
|
|
5835
|
-
const ext =
|
|
6069
|
+
const ext = path25.extname(absolutePath).toLowerCase();
|
|
5836
6070
|
try {
|
|
5837
6071
|
if (ext === ".ts" || ext === ".tsx") {
|
|
5838
6072
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -5861,30 +6095,35 @@ ${stdout}`);
|
|
|
5861
6095
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
5862
6096
|
*/
|
|
5863
6097
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
5864
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5865
|
-
|
|
5866
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
5867
6098
|
let current = output;
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
const
|
|
6099
|
+
try {
|
|
6100
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
6101
|
+
const verdictResult = await this.router.generate("T2", {
|
|
5871
6102
|
messages: [{
|
|
5872
6103
|
role: "user",
|
|
5873
|
-
content: `
|
|
6104
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
5874
6105
|
|
|
5875
|
-
Goal
|
|
6106
|
+
Goal: ${assignment.expectedOutput}
|
|
5876
6107
|
Subtask: ${assignment.description}
|
|
5877
|
-
|
|
5878
|
-
Output:
|
|
6108
|
+
Current Output:
|
|
5879
6109
|
${current}
|
|
5880
6110
|
|
|
5881
|
-
|
|
6111
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
6112
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
5882
6113
|
}],
|
|
5883
|
-
systemPrompt:
|
|
5884
|
-
maxTokens: 400
|
|
6114
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
6115
|
+
maxTokens: 400,
|
|
6116
|
+
signal: this.signal,
|
|
6117
|
+
featureTag: assignment.sectionTitle,
|
|
6118
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5885
6119
|
});
|
|
5886
|
-
const
|
|
5887
|
-
|
|
6120
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
6121
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
6122
|
+
if (parsed.sufficient !== false) {
|
|
6123
|
+
this.log("T2-Critic approved output.");
|
|
6124
|
+
break;
|
|
6125
|
+
}
|
|
6126
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
5888
6127
|
const improved = await this.router.generate("T3", {
|
|
5889
6128
|
messages: [{
|
|
5890
6129
|
role: "user",
|
|
@@ -5896,16 +6135,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
5896
6135
|
Current output:
|
|
5897
6136
|
${current}`
|
|
5898
6137
|
}],
|
|
5899
|
-
systemPrompt:
|
|
5900
|
-
|
|
6138
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
6139
|
+
|
|
6140
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6141
|
+
maxTokens: 4096,
|
|
6142
|
+
featureTag: assignment.sectionTitle,
|
|
6143
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5901
6144
|
});
|
|
5902
6145
|
const next = (improved.content ?? "").trim();
|
|
5903
6146
|
if (!next) break;
|
|
5904
6147
|
current = next;
|
|
5905
6148
|
this.log("Reflection: revised output for better goal alignment.");
|
|
5906
|
-
} catch {
|
|
5907
|
-
break;
|
|
5908
6149
|
}
|
|
6150
|
+
} catch (e) {
|
|
6151
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
5909
6152
|
}
|
|
5910
6153
|
return current;
|
|
5911
6154
|
}
|
|
@@ -5926,7 +6169,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
5926
6169
|
maxTokens: 500,
|
|
5927
6170
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5928
6171
|
|
|
5929
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
6172
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6173
|
+
featureTag: assignment.sectionTitle,
|
|
6174
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5930
6175
|
});
|
|
5931
6176
|
try {
|
|
5932
6177
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -6021,6 +6266,7 @@ Begin execution now.`;
|
|
|
6021
6266
|
issues,
|
|
6022
6267
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
6023
6268
|
correctionAttempts,
|
|
6269
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
6024
6270
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
6025
6271
|
};
|
|
6026
6272
|
}
|
|
@@ -6312,6 +6558,42 @@ var PeerBus = class extends EventEmitter {
|
|
|
6312
6558
|
}
|
|
6313
6559
|
};
|
|
6314
6560
|
|
|
6561
|
+
// src/core/audit/redaction.ts
|
|
6562
|
+
var RedactionLayer = class {
|
|
6563
|
+
// Regexes for common secrets/PII
|
|
6564
|
+
static RULES = [
|
|
6565
|
+
// IPv4 addresses (basic approximation)
|
|
6566
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
6567
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
6568
|
+
{ pattern: /(?:\b(?:api_key|apikey|secret|token|password|bearer|auth|authorization)\b[^a-zA-Z0-9_]{1,4})([a-zA-Z0-9_\-\.]{16,})/gi, replacement: "$1[REDACTED_SECRET]" },
|
|
6569
|
+
// Email addresses
|
|
6570
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
6571
|
+
// Phone numbers (simplistic)
|
|
6572
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
6573
|
+
// AWS Access Key ID
|
|
6574
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
6575
|
+
];
|
|
6576
|
+
/**
|
|
6577
|
+
* Applies all redaction rules to the input string.
|
|
6578
|
+
*/
|
|
6579
|
+
static redact(text) {
|
|
6580
|
+
if (!text) return text;
|
|
6581
|
+
let redacted = text;
|
|
6582
|
+
for (const rule of this.RULES) {
|
|
6583
|
+
if (rule.pattern.test(redacted)) {
|
|
6584
|
+
rule.pattern.lastIndex = 0;
|
|
6585
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
|
|
6586
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
6587
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
6588
|
+
}
|
|
6589
|
+
return rule.replacement;
|
|
6590
|
+
});
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
return redacted;
|
|
6594
|
+
}
|
|
6595
|
+
};
|
|
6596
|
+
|
|
6315
6597
|
// src/core/tiers/t2-manager.ts
|
|
6316
6598
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
6317
6599
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -6540,7 +6822,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6540
6822
|
try {
|
|
6541
6823
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
6542
6824
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
6543
|
-
|
|
6825
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
6826
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
6544
6827
|
} catch {
|
|
6545
6828
|
return [{
|
|
6546
6829
|
subtaskId: randomUUID(),
|
|
@@ -6550,6 +6833,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6550
6833
|
constraints: assignment.constraints,
|
|
6551
6834
|
peerT3Ids: [],
|
|
6552
6835
|
parentT2: this.id,
|
|
6836
|
+
sectionTitle: assignment.sectionTitle,
|
|
6837
|
+
dependsOn: [],
|
|
6553
6838
|
executionMode: "parallel"
|
|
6554
6839
|
}];
|
|
6555
6840
|
}
|
|
@@ -6669,6 +6954,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6669
6954
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
6670
6955
|
const worker = workerMap.get(id);
|
|
6671
6956
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
6957
|
+
if (result.localOnly) {
|
|
6958
|
+
result.output = `[local-only path \u2014 output withheld by privacy policy; status: ${result.status}; checks passed: ${result.testResults.passed.length}/${result.testResults.checksRun.length || 0}]`;
|
|
6959
|
+
} else {
|
|
6960
|
+
if (typeof result.output === "string" && result.output) {
|
|
6961
|
+
result.output = RedactionLayer.redact(result.output);
|
|
6962
|
+
}
|
|
6963
|
+
}
|
|
6964
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
6672
6965
|
resultMap.set(id, result);
|
|
6673
6966
|
return result;
|
|
6674
6967
|
};
|
|
@@ -7301,10 +7594,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7301
7594
|
}
|
|
7302
7595
|
}
|
|
7303
7596
|
async decomposeTask(prompt, systemContext) {
|
|
7597
|
+
const db = this.router.getWorldStateDB?.();
|
|
7598
|
+
const worldStateContext = db ? `
|
|
7599
|
+
|
|
7600
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7601
|
+
${db.getFormattedState()}` : "";
|
|
7304
7602
|
const contextSection = systemContext ? `
|
|
7305
7603
|
Project context:
|
|
7306
7604
|
${systemContext}` : "";
|
|
7307
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
7605
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
7308
7606
|
|
|
7309
7607
|
Task: ${prompt}
|
|
7310
7608
|
|
|
@@ -7792,16 +8090,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
7792
8090
|
if (typeof input !== "string" || input.length === 0) {
|
|
7793
8091
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
7794
8092
|
}
|
|
7795
|
-
const root =
|
|
7796
|
-
const abs =
|
|
7797
|
-
const rel =
|
|
7798
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
8093
|
+
const root = path25.resolve(workspaceRoot);
|
|
8094
|
+
const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
|
|
8095
|
+
const rel = path25.relative(root, abs);
|
|
8096
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
7799
8097
|
throw new WorkspaceSandboxError(input, root);
|
|
7800
8098
|
}
|
|
7801
8099
|
try {
|
|
7802
|
-
const real =
|
|
7803
|
-
const realRel =
|
|
7804
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
8100
|
+
const real = fs23.realpathSync(abs);
|
|
8101
|
+
const realRel = path25.relative(root, real);
|
|
8102
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
|
|
7805
8103
|
throw new WorkspaceSandboxError(input, root);
|
|
7806
8104
|
}
|
|
7807
8105
|
} catch (e) {
|
|
@@ -7862,7 +8160,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
7862
8160
|
} catch {
|
|
7863
8161
|
}
|
|
7864
8162
|
}
|
|
7865
|
-
await fs9.mkdir(
|
|
8163
|
+
await fs9.mkdir(path25.dirname(absPath), { recursive: true });
|
|
7866
8164
|
await fs9.writeFile(absPath, content, "utf-8");
|
|
7867
8165
|
return `Written ${content.length} characters to ${filePath}`;
|
|
7868
8166
|
}
|
|
@@ -8360,7 +8658,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
8360
8658
|
};
|
|
8361
8659
|
async function fileToImageAttachment(filePath) {
|
|
8362
8660
|
const data = await fs9.readFile(filePath);
|
|
8363
|
-
const ext =
|
|
8661
|
+
const ext = path25.extname(filePath).toLowerCase();
|
|
8364
8662
|
const mimeMap = {
|
|
8365
8663
|
".jpg": "image/jpeg",
|
|
8366
8664
|
".jpeg": "image/jpeg",
|
|
@@ -8394,14 +8692,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
8394
8692
|
const filePath = input["path"];
|
|
8395
8693
|
const content = input["content"];
|
|
8396
8694
|
const title = input["title"];
|
|
8397
|
-
const dir =
|
|
8398
|
-
if (!
|
|
8399
|
-
|
|
8695
|
+
const dir = path25.dirname(filePath);
|
|
8696
|
+
if (!fs23.existsSync(dir)) {
|
|
8697
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
8400
8698
|
}
|
|
8401
8699
|
return new Promise((resolve, reject) => {
|
|
8402
8700
|
try {
|
|
8403
8701
|
const doc = new PDFDocument({ margin: 50 });
|
|
8404
|
-
const stream =
|
|
8702
|
+
const stream = fs23.createWriteStream(filePath);
|
|
8405
8703
|
doc.pipe(stream);
|
|
8406
8704
|
if (title) {
|
|
8407
8705
|
doc.info["Title"] = title;
|
|
@@ -8479,22 +8777,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
8479
8777
|
}
|
|
8480
8778
|
cmdPrefix = NODE_CMD;
|
|
8481
8779
|
}
|
|
8482
|
-
const tmpDir =
|
|
8483
|
-
if (!
|
|
8484
|
-
|
|
8780
|
+
const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
|
|
8781
|
+
if (!fs23.existsSync(tmpDir)) {
|
|
8782
|
+
fs23.mkdirSync(tmpDir, { recursive: true });
|
|
8485
8783
|
}
|
|
8486
8784
|
const extension = language === "python" ? "py" : "js";
|
|
8487
8785
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
8488
|
-
const filePath =
|
|
8489
|
-
|
|
8786
|
+
const filePath = path25.join(tmpDir, fileName);
|
|
8787
|
+
fs23.writeFileSync(filePath, code, "utf-8");
|
|
8490
8788
|
const execArgs = [filePath, ...args];
|
|
8491
8789
|
return new Promise((resolve) => {
|
|
8492
8790
|
const startMs = Date.now();
|
|
8493
8791
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
8494
8792
|
const duration = Date.now() - startMs;
|
|
8495
8793
|
try {
|
|
8496
|
-
if (
|
|
8497
|
-
|
|
8794
|
+
if (fs23.existsSync(filePath)) {
|
|
8795
|
+
fs23.unlinkSync(filePath);
|
|
8498
8796
|
}
|
|
8499
8797
|
} catch (cleanupErr) {
|
|
8500
8798
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -8773,7 +9071,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8773
9071
|
};
|
|
8774
9072
|
async execute(input, _options) {
|
|
8775
9073
|
const pattern = input["pattern"];
|
|
8776
|
-
const searchPath = input["path"] ?
|
|
9074
|
+
const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8777
9075
|
const matches = await glob(pattern, {
|
|
8778
9076
|
cwd: searchPath,
|
|
8779
9077
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -8786,7 +9084,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8786
9084
|
const withMtime = await Promise.all(
|
|
8787
9085
|
matches.map(async (rel) => {
|
|
8788
9086
|
try {
|
|
8789
|
-
const stat = await fs9.stat(
|
|
9087
|
+
const stat = await fs9.stat(path25.join(searchPath, rel));
|
|
8790
9088
|
return { rel, mtime: stat.mtimeMs };
|
|
8791
9089
|
} catch {
|
|
8792
9090
|
return { rel, mtime: 0 };
|
|
@@ -8835,7 +9133,7 @@ var GrepTool = class extends BaseTool {
|
|
|
8835
9133
|
};
|
|
8836
9134
|
async execute(input, _options) {
|
|
8837
9135
|
const pattern = input["pattern"];
|
|
8838
|
-
const searchPath = input["path"] ?
|
|
9136
|
+
const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8839
9137
|
const globPattern = input["glob"];
|
|
8840
9138
|
const outputMode = input["output_mode"] ?? "content";
|
|
8841
9139
|
const context = input["context"] ?? 0;
|
|
@@ -8889,12 +9187,12 @@ var GrepTool = class extends BaseTool {
|
|
|
8889
9187
|
nodir: true
|
|
8890
9188
|
});
|
|
8891
9189
|
} catch {
|
|
8892
|
-
files = [
|
|
9190
|
+
files = [path25.relative(searchPath, searchPath) || "."];
|
|
8893
9191
|
}
|
|
8894
9192
|
const results = [];
|
|
8895
9193
|
let totalCount = 0;
|
|
8896
9194
|
for (const rel of files) {
|
|
8897
|
-
const abs =
|
|
9195
|
+
const abs = path25.join(searchPath, rel);
|
|
8898
9196
|
let content;
|
|
8899
9197
|
try {
|
|
8900
9198
|
content = await fs9.readFile(abs, "utf-8");
|
|
@@ -9256,10 +9554,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
9256
9554
|
}
|
|
9257
9555
|
isIgnored(filePath) {
|
|
9258
9556
|
if (!filePath) return false;
|
|
9259
|
-
const abs =
|
|
9260
|
-
const rel =
|
|
9261
|
-
if (!rel || rel.startsWith("..") ||
|
|
9262
|
-
const posixRel = rel.split(
|
|
9557
|
+
const abs = path25.resolve(this.workspaceRoot, filePath);
|
|
9558
|
+
const rel = path25.relative(this.workspaceRoot, abs);
|
|
9559
|
+
if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
|
|
9560
|
+
const posixRel = rel.split(path25.sep).join("/");
|
|
9263
9561
|
return this.ignoreMatcher.ignores(posixRel);
|
|
9264
9562
|
}
|
|
9265
9563
|
};
|
|
@@ -9376,6 +9674,9 @@ var McpClient = class _McpClient {
|
|
|
9376
9674
|
return this.clients.has(serverName);
|
|
9377
9675
|
}
|
|
9378
9676
|
};
|
|
9677
|
+
|
|
9678
|
+
// src/core/cascade.ts
|
|
9679
|
+
init_audit_logger();
|
|
9379
9680
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
9380
9681
|
"file_read",
|
|
9381
9682
|
"file_list",
|
|
@@ -9759,9 +10060,10 @@ var TaskAnalyzer = class {
|
|
|
9759
10060
|
analysisCache.clear();
|
|
9760
10061
|
}
|
|
9761
10062
|
};
|
|
9762
|
-
var DEFAULT_STATS_FILE =
|
|
10063
|
+
var DEFAULT_STATS_FILE = path25.join(os8.homedir(), ".cascade", "model-perf.json");
|
|
9763
10064
|
var ModelPerformanceTracker = class {
|
|
9764
10065
|
stats = /* @__PURE__ */ new Map();
|
|
10066
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
9765
10067
|
statsFile;
|
|
9766
10068
|
loaded = false;
|
|
9767
10069
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -9773,18 +10075,29 @@ var ModelPerformanceTracker = class {
|
|
|
9773
10075
|
try {
|
|
9774
10076
|
const raw = await fs9.readFile(this.statsFile, "utf-8");
|
|
9775
10077
|
const parsed = JSON.parse(raw);
|
|
9776
|
-
|
|
9777
|
-
this.stats.set(key, stat);
|
|
10078
|
+
if (parsed.models) {
|
|
10079
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
10080
|
+
} else {
|
|
10081
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
10082
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
10083
|
+
this.stats.set(key, stat);
|
|
10084
|
+
}
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
if (parsed.features) {
|
|
10088
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
9778
10089
|
}
|
|
9779
10090
|
} catch {
|
|
9780
10091
|
}
|
|
9781
10092
|
}
|
|
9782
10093
|
async save() {
|
|
9783
10094
|
try {
|
|
9784
|
-
await fs9.mkdir(
|
|
9785
|
-
const
|
|
9786
|
-
|
|
9787
|
-
|
|
10095
|
+
await fs9.mkdir(path25.dirname(this.statsFile), { recursive: true });
|
|
10096
|
+
const modelsObj = {};
|
|
10097
|
+
const featuresObj = {};
|
|
10098
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
10099
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
10100
|
+
await fs9.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
9788
10101
|
} catch {
|
|
9789
10102
|
}
|
|
9790
10103
|
}
|
|
@@ -9805,6 +10118,13 @@ var ModelPerformanceTracker = class {
|
|
|
9805
10118
|
sampleCount: s.sampleCount + 1
|
|
9806
10119
|
});
|
|
9807
10120
|
}
|
|
10121
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
10122
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
10123
|
+
this.featureStats.set(featureTag, {
|
|
10124
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
10125
|
+
runCount: s.runCount + 1
|
|
10126
|
+
});
|
|
10127
|
+
}
|
|
9808
10128
|
/**
|
|
9809
10129
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
9810
10130
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -9819,6 +10139,9 @@ var ModelPerformanceTracker = class {
|
|
|
9819
10139
|
getAll() {
|
|
9820
10140
|
return new Map(this.stats);
|
|
9821
10141
|
}
|
|
10142
|
+
getAllFeatures() {
|
|
10143
|
+
return new Map(this.featureStats);
|
|
10144
|
+
}
|
|
9822
10145
|
/**
|
|
9823
10146
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
9824
10147
|
* High retry counts penalise the score.
|
|
@@ -10183,7 +10506,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10183
10506
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
10184
10507
|
async loadPersistedTools() {
|
|
10185
10508
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10186
|
-
const file =
|
|
10509
|
+
const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
10187
10510
|
try {
|
|
10188
10511
|
const raw = await fs9.readFile(file, "utf-8");
|
|
10189
10512
|
const specs = JSON.parse(raw);
|
|
@@ -10207,8 +10530,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10207
10530
|
}
|
|
10208
10531
|
async persist() {
|
|
10209
10532
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10210
|
-
const dir =
|
|
10211
|
-
const file =
|
|
10533
|
+
const dir = path25.join(this.workspacePath, ".cascade");
|
|
10534
|
+
const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
|
|
10212
10535
|
try {
|
|
10213
10536
|
await fs9.mkdir(dir, { recursive: true });
|
|
10214
10537
|
await fs9.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -10221,6 +10544,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10221
10544
|
return Array.from(this.specs.keys());
|
|
10222
10545
|
}
|
|
10223
10546
|
};
|
|
10547
|
+
var WorldStateDB = class {
|
|
10548
|
+
constructor(workspacePath, debugMode = false) {
|
|
10549
|
+
this.workspacePath = workspacePath;
|
|
10550
|
+
this.debugMode = debugMode;
|
|
10551
|
+
const cascadeDir = path25.join(workspacePath, ".cascade");
|
|
10552
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
10553
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
10554
|
+
}
|
|
10555
|
+
this.keyPath = path25.join(cascadeDir, "world_state.key");
|
|
10556
|
+
this.dbPath = path25.join(cascadeDir, "world_state.db");
|
|
10557
|
+
this.initEncryptionKey();
|
|
10558
|
+
this.db = new Database2(this.dbPath);
|
|
10559
|
+
this.db.pragma("journal_mode = WAL");
|
|
10560
|
+
this.db.exec(`
|
|
10561
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
10562
|
+
id TEXT PRIMARY KEY,
|
|
10563
|
+
timestamp TEXT NOT NULL,
|
|
10564
|
+
worker_id TEXT NOT NULL,
|
|
10565
|
+
encrypted_payload TEXT NOT NULL
|
|
10566
|
+
)
|
|
10567
|
+
`);
|
|
10568
|
+
}
|
|
10569
|
+
workspacePath;
|
|
10570
|
+
debugMode;
|
|
10571
|
+
db;
|
|
10572
|
+
keyPath;
|
|
10573
|
+
dbPath;
|
|
10574
|
+
encryptionKey;
|
|
10575
|
+
initEncryptionKey() {
|
|
10576
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
10577
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
10578
|
+
} else {
|
|
10579
|
+
this.encryptionKey = crypto.randomBytes(32);
|
|
10580
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
10581
|
+
}
|
|
10582
|
+
}
|
|
10583
|
+
encrypt(text) {
|
|
10584
|
+
const iv = crypto.randomBytes(16);
|
|
10585
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10586
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
10587
|
+
encrypted += cipher.final("hex");
|
|
10588
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
10589
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
10590
|
+
}
|
|
10591
|
+
decrypt(text) {
|
|
10592
|
+
const parts = text.split(":");
|
|
10593
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
10594
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
10595
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
10596
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
10597
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10598
|
+
decipher.setAuthTag(authTag);
|
|
10599
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
10600
|
+
decrypted += decipher.final("utf8");
|
|
10601
|
+
return decrypted;
|
|
10602
|
+
}
|
|
10603
|
+
addEntry(workerId, summary) {
|
|
10604
|
+
const id = crypto.randomUUID();
|
|
10605
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10606
|
+
const payload = JSON.stringify({ summary });
|
|
10607
|
+
const encryptedPayload = this.encrypt(payload);
|
|
10608
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
10609
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
10610
|
+
this.dumpDebugIfNeeded();
|
|
10611
|
+
return id;
|
|
10612
|
+
}
|
|
10613
|
+
getAllEntries() {
|
|
10614
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
10615
|
+
const rows = stmt.all();
|
|
10616
|
+
return rows.map((row) => {
|
|
10617
|
+
try {
|
|
10618
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
10619
|
+
const parsed = JSON.parse(decrypted);
|
|
10620
|
+
return {
|
|
10621
|
+
id: row.id,
|
|
10622
|
+
timestamp: row.timestamp,
|
|
10623
|
+
workerId: row.worker_id,
|
|
10624
|
+
summary: parsed.summary
|
|
10625
|
+
};
|
|
10626
|
+
} catch (err) {
|
|
10627
|
+
return {
|
|
10628
|
+
id: row.id,
|
|
10629
|
+
timestamp: row.timestamp,
|
|
10630
|
+
workerId: row.worker_id,
|
|
10631
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
10632
|
+
};
|
|
10633
|
+
}
|
|
10634
|
+
});
|
|
10635
|
+
}
|
|
10636
|
+
getFormattedState() {
|
|
10637
|
+
const entries = this.getAllEntries();
|
|
10638
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
10639
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10640
|
+
}
|
|
10641
|
+
dumpDebugIfNeeded() {
|
|
10642
|
+
if (!this.debugMode) return;
|
|
10643
|
+
try {
|
|
10644
|
+
const dumpPath = path25.join(os8.tmpdir(), "cascade_world_state_debug.json");
|
|
10645
|
+
const entries = this.getAllEntries();
|
|
10646
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
10647
|
+
} catch (err) {
|
|
10648
|
+
console.error("Failed to dump debug world state", err);
|
|
10649
|
+
}
|
|
10650
|
+
}
|
|
10651
|
+
close() {
|
|
10652
|
+
this.db.close();
|
|
10653
|
+
}
|
|
10654
|
+
};
|
|
10655
|
+
var ignore3 = _ignoreModule.default ?? _ignoreModule;
|
|
10656
|
+
var PrivacyPaths = class {
|
|
10657
|
+
localOnly;
|
|
10658
|
+
hasRules;
|
|
10659
|
+
constructor(policies = []) {
|
|
10660
|
+
this.localOnly = ignore3();
|
|
10661
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
10662
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
10663
|
+
this.hasRules = patterns.length > 0;
|
|
10664
|
+
}
|
|
10665
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
10666
|
+
hasPolicies() {
|
|
10667
|
+
return this.hasRules;
|
|
10668
|
+
}
|
|
10669
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
10670
|
+
isLocalOnly(relativePath) {
|
|
10671
|
+
if (!this.hasRules || !relativePath) return false;
|
|
10672
|
+
try {
|
|
10673
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
10674
|
+
} catch {
|
|
10675
|
+
return false;
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
10679
|
+
anyLocalOnly(relativePaths) {
|
|
10680
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
10681
|
+
}
|
|
10682
|
+
};
|
|
10683
|
+
|
|
10684
|
+
// src/core/steering/guidance.ts
|
|
10685
|
+
var GuidanceQueue = class {
|
|
10686
|
+
entries = [];
|
|
10687
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
10688
|
+
cursors = /* @__PURE__ */ new Map();
|
|
10689
|
+
push(text, nodeId) {
|
|
10690
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10691
|
+
this.entries.push(entry);
|
|
10692
|
+
return entry;
|
|
10693
|
+
}
|
|
10694
|
+
/**
|
|
10695
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
10696
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
10697
|
+
*/
|
|
10698
|
+
drain(consumerId) {
|
|
10699
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
10700
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
10701
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
10702
|
+
}
|
|
10703
|
+
get size() {
|
|
10704
|
+
return this.entries.length;
|
|
10705
|
+
}
|
|
10706
|
+
};
|
|
10224
10707
|
|
|
10225
10708
|
// src/core/cascade.ts
|
|
10226
10709
|
var Cascade = class _Cascade extends EventEmitter {
|
|
@@ -10240,6 +10723,9 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10240
10723
|
taskAnalyzer;
|
|
10241
10724
|
perfTracker;
|
|
10242
10725
|
toolCreator;
|
|
10726
|
+
worldStateDB;
|
|
10727
|
+
encryptedAuditLogger;
|
|
10728
|
+
guidanceQueue;
|
|
10243
10729
|
workspacePath;
|
|
10244
10730
|
constructor(config, workspacePath, store) {
|
|
10245
10731
|
super();
|
|
@@ -10261,6 +10747,23 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10261
10747
|
});
|
|
10262
10748
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
10263
10749
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
10750
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10751
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
10752
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10753
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
10754
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
10755
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
10756
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
10757
|
+
}
|
|
10758
|
+
/**
|
|
10759
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
10760
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
10761
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
10762
|
+
*/
|
|
10763
|
+
injectGuidance(text, nodeId) {
|
|
10764
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
10765
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
10766
|
+
this.emit("guidance:injected", entry);
|
|
10264
10767
|
}
|
|
10265
10768
|
initOptionalFeatures() {
|
|
10266
10769
|
if (this.config.cascadeAuto === true) {
|
|
@@ -10423,6 +10926,12 @@ ${last.partialOutput}` : "");
|
|
|
10423
10926
|
if (!prompt) return null;
|
|
10424
10927
|
return this.run({ prompt });
|
|
10425
10928
|
}
|
|
10929
|
+
getWorkspacePath() {
|
|
10930
|
+
return this.workspacePath;
|
|
10931
|
+
}
|
|
10932
|
+
getWorldStateDB() {
|
|
10933
|
+
return this.worldStateDB;
|
|
10934
|
+
}
|
|
10426
10935
|
/**
|
|
10427
10936
|
* Record an explicit user rating for the last completed run.
|
|
10428
10937
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -10509,6 +11018,11 @@ ${last.partialOutput}` : "");
|
|
|
10509
11018
|
}
|
|
10510
11019
|
this.initOptionalFeatures();
|
|
10511
11020
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
11021
|
+
if (this.encryptedAuditLogger) {
|
|
11022
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
11023
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
11024
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
11025
|
+
}
|
|
10512
11026
|
this.initialized = true;
|
|
10513
11027
|
})();
|
|
10514
11028
|
try {
|
|
@@ -10934,6 +11448,7 @@ ${prompt}` : prompt;
|
|
|
10934
11448
|
durationMs,
|
|
10935
11449
|
costByTier: stats.costByTier,
|
|
10936
11450
|
tokensByTier: stats.tokensByTier,
|
|
11451
|
+
costByFeature: stats.costByFeature,
|
|
10937
11452
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
10938
11453
|
};
|
|
10939
11454
|
}
|
|
@@ -11183,6 +11698,30 @@ var SlashCommandRegistry = class {
|
|
|
11183
11698
|
return { output: out || "File changes rolled back.", handled: true };
|
|
11184
11699
|
}
|
|
11185
11700
|
});
|
|
11701
|
+
this.register({
|
|
11702
|
+
command: "/steer",
|
|
11703
|
+
description: "Steer a running task: /steer <correction for the active workers>",
|
|
11704
|
+
args: ["<guidance>"],
|
|
11705
|
+
handler: async (args, ctx) => {
|
|
11706
|
+
const output = await ctx.onSteer(args);
|
|
11707
|
+
return { output, handled: true };
|
|
11708
|
+
}
|
|
11709
|
+
});
|
|
11710
|
+
this.register({
|
|
11711
|
+
command: "/audit",
|
|
11712
|
+
description: "Verify the tamper-evident audit log (hash chain integrity)",
|
|
11713
|
+
handler: async (_args, ctx) => {
|
|
11714
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
11715
|
+
const logger = new AuditLogger3(ctx.workspacePath);
|
|
11716
|
+
try {
|
|
11717
|
+
const v = logger.verifyChain();
|
|
11718
|
+
const output = v.ok ? `\u2714 Audit log intact \u2014 ${v.entries} entr${v.entries === 1 ? "y" : "ies"}, hash chain verified.` : `\u2718 Audit log FAILED verification at row ${v.firstBadRow} of ${v.entries} \u2014 entries at or after that row were modified, removed, or predate the hash chain.`;
|
|
11719
|
+
return { output, handled: true };
|
|
11720
|
+
} finally {
|
|
11721
|
+
logger.close();
|
|
11722
|
+
}
|
|
11723
|
+
}
|
|
11724
|
+
});
|
|
11186
11725
|
this.register({
|
|
11187
11726
|
command: "/branch",
|
|
11188
11727
|
description: "Fork current session into parallel branches",
|
|
@@ -11830,7 +12369,8 @@ function CostTracker({
|
|
|
11830
12369
|
tokensByTier,
|
|
11831
12370
|
compact = false,
|
|
11832
12371
|
savedUsd = 0,
|
|
11833
|
-
savedPct = 0
|
|
12372
|
+
savedPct = 0,
|
|
12373
|
+
costByFeature
|
|
11834
12374
|
}) {
|
|
11835
12375
|
const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
|
|
11836
12376
|
const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
|
|
@@ -11929,6 +12469,16 @@ function CostTracker({
|
|
|
11929
12469
|
] }, tier);
|
|
11930
12470
|
})
|
|
11931
12471
|
] })
|
|
12472
|
+
] }),
|
|
12473
|
+
costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
12474
|
+
/* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "By feature/section:" }),
|
|
12475
|
+
Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
|
|
12476
|
+
/* @__PURE__ */ jsx(Box, { width: 30, children: /* @__PURE__ */ jsx(Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
|
|
12477
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.colors.success, children: [
|
|
12478
|
+
"$",
|
|
12479
|
+
cost.toFixed(6)
|
|
12480
|
+
] })
|
|
12481
|
+
] }, feature))
|
|
11932
12482
|
] })
|
|
11933
12483
|
] });
|
|
11934
12484
|
}
|
|
@@ -12342,7 +12892,7 @@ function replReducer(state, action) {
|
|
|
12342
12892
|
case "SET_TREE":
|
|
12343
12893
|
return { ...state, agentTree: action.tree };
|
|
12344
12894
|
case "UPDATE_COST":
|
|
12345
|
-
return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, savedUsd: action.savedUsd, savedPct: action.savedPct };
|
|
12895
|
+
return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, costByFeature: action.costByFeature, savedUsd: action.savedUsd, savedPct: action.savedPct };
|
|
12346
12896
|
case "SET_APPROVAL":
|
|
12347
12897
|
return { ...state, approvalRequest: action.request };
|
|
12348
12898
|
case "SET_EXECUTING":
|
|
@@ -12356,7 +12906,7 @@ function replReducer(state, action) {
|
|
|
12356
12906
|
case "TOGGLE_COMMS":
|
|
12357
12907
|
return { ...state, showComms: !state.showComms };
|
|
12358
12908
|
case "CLEAR":
|
|
12359
|
-
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
12909
|
+
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
12360
12910
|
case "CLEAR_TREE":
|
|
12361
12911
|
return { ...state, agentTree: null };
|
|
12362
12912
|
case "TOGGLE_COST":
|
|
@@ -12405,7 +12955,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12405
12955
|
const [slashIndex, setSlashIndex] = useState(0);
|
|
12406
12956
|
const [identities, setIdentities] = useState([]);
|
|
12407
12957
|
const [currentIdentityId, setCurrentIdentityId] = useState(config.defaultIdentityId);
|
|
12408
|
-
const [state, dispatch] = useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
|
|
12958
|
+
const [state, dispatch] = useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
|
|
12409
12959
|
const [isShowingModels, setIsShowingModels] = useState(false);
|
|
12410
12960
|
const [planApprovalRequest, setPlanApprovalRequest] = useState(null);
|
|
12411
12961
|
const [historyOffset, setHistoryOffset] = useState(0);
|
|
@@ -12474,9 +13024,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12474
13024
|
}
|
|
12475
13025
|
} catch {
|
|
12476
13026
|
}
|
|
12477
|
-
const configPath =
|
|
13027
|
+
const configPath = path25.join(workspacePath, ".cascade", "config.json");
|
|
12478
13028
|
try {
|
|
12479
|
-
await fs9.mkdir(
|
|
13029
|
+
await fs9.mkdir(path25.dirname(configPath), { recursive: true });
|
|
12480
13030
|
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
12481
13031
|
} catch (err) {
|
|
12482
13032
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -12528,9 +13078,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12528
13078
|
if (msg.includes("non-text parts")) return;
|
|
12529
13079
|
originalLog(...args);
|
|
12530
13080
|
};
|
|
12531
|
-
const store = new MemoryStore(
|
|
13081
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
12532
13082
|
storeRef.current = store;
|
|
12533
|
-
globalStoreRef.current = new MemoryStore(
|
|
13083
|
+
globalStoreRef.current = new MemoryStore(path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
12534
13084
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
12535
13085
|
setIdentities(identityRows);
|
|
12536
13086
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -12593,7 +13143,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12593
13143
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
12594
13144
|
onExport: async (fmt) => {
|
|
12595
13145
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12596
|
-
const exportPath =
|
|
13146
|
+
const exportPath = path25.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
|
|
12597
13147
|
if (fmt === "json") {
|
|
12598
13148
|
await fs9.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
12599
13149
|
} else {
|
|
@@ -12617,6 +13167,14 @@ ${msg.content}`).join("\n\n");
|
|
|
12617
13167
|
}
|
|
12618
13168
|
return `Restored ${snapshots.length} files to their initial session state.`;
|
|
12619
13169
|
},
|
|
13170
|
+
onSteer: (args) => {
|
|
13171
|
+
const text = args.join(" ").trim();
|
|
13172
|
+
if (!text) return "Usage: /steer <correction for the active workers>";
|
|
13173
|
+
const cascade = cascadeRef.current;
|
|
13174
|
+
if (!cascade) return "No active Cascade instance.";
|
|
13175
|
+
cascade.injectGuidance(text);
|
|
13176
|
+
return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
|
|
13177
|
+
},
|
|
12620
13178
|
onBranch: async () => {
|
|
12621
13179
|
const store = storeRef.current;
|
|
12622
13180
|
if (!store) return;
|
|
@@ -12935,7 +13493,7 @@ ${lastUser.content}`;
|
|
|
12935
13493
|
if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
|
|
12936
13494
|
const stats = cascade.getRouter().getStats();
|
|
12937
13495
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
12938
|
-
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13496
|
+
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
12939
13497
|
statusThrottleTimeout = null;
|
|
12940
13498
|
};
|
|
12941
13499
|
cascade.on("tier:root", onRoot);
|
|
@@ -13027,7 +13585,7 @@ ${lastUser.content}`;
|
|
|
13027
13585
|
flushPeerEvents();
|
|
13028
13586
|
const stats = cascade.getRouter().getStats();
|
|
13029
13587
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
13030
|
-
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13588
|
+
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13031
13589
|
dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13032
13590
|
persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
|
|
13033
13591
|
const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
|
|
@@ -13312,7 +13870,7 @@ ${lastUser.content}`;
|
|
|
13312
13870
|
),
|
|
13313
13871
|
adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
|
|
13314
13872
|
adaptiveMode === "wide" && state.showDetails && liveBudget.showTimeline && /* @__PURE__ */ jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
|
|
13315
|
-
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
|
|
13873
|
+
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, costByFeature: state.costByFeature, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
|
|
13316
13874
|
liveBudget.collapsed && (state.showCost || state.showDetails || state.agentTree != null) && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u25B8 panels collapsed (small terminal)" }),
|
|
13317
13875
|
state.approvalRequest && /* @__PURE__ */ jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
|
|
13318
13876
|
dispatch({ type: "SET_APPROVAL", request: null });
|
|
@@ -13481,7 +14039,7 @@ function stringifySlashOutput(val) {
|
|
|
13481
14039
|
}
|
|
13482
14040
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
13483
14041
|
if (!query) return "Usage: /search <query>";
|
|
13484
|
-
const dbPath =
|
|
14042
|
+
const dbPath = path25.join(workspacePath, CASCADE_DB_FILE);
|
|
13485
14043
|
try {
|
|
13486
14044
|
await fs9.access(dbPath);
|
|
13487
14045
|
} catch {
|
|
@@ -13515,7 +14073,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
13515
14073
|
async function diagnoseRuntime(config, workspacePath) {
|
|
13516
14074
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
13517
14075
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
13518
|
-
const store = new MemoryStore(
|
|
14076
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13519
14077
|
try {
|
|
13520
14078
|
const sessions = store.listSessions(void 0, 3);
|
|
13521
14079
|
return [
|
|
@@ -13534,7 +14092,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
13534
14092
|
}
|
|
13535
14093
|
async function showRecentLogs(args, workspacePath) {
|
|
13536
14094
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
13537
|
-
const store = new MemoryStore(
|
|
14095
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13538
14096
|
try {
|
|
13539
14097
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
13540
14098
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -13546,7 +14104,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
13546
14104
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
13547
14105
|
const sessionId = args[0];
|
|
13548
14106
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
13549
|
-
const store = new MemoryStore(
|
|
14107
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13550
14108
|
try {
|
|
13551
14109
|
const session = store.getSession(sessionId);
|
|
13552
14110
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -13966,9 +14524,9 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
13966
14524
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
13967
14525
|
};
|
|
13968
14526
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
13969
|
-
const configDir =
|
|
14527
|
+
const configDir = path25.join(workspacePath, ".cascade");
|
|
13970
14528
|
await fs9.mkdir(configDir, { recursive: true });
|
|
13971
|
-
const configPath =
|
|
14529
|
+
const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
13972
14530
|
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
13973
14531
|
savedConfigRef.current = config;
|
|
13974
14532
|
dispatchRef.current({ type: "GO_DONE" });
|
|
@@ -14290,14 +14848,14 @@ function printTelemetryBanner() {
|
|
|
14290
14848
|
async function initCommand(workspacePath = process.cwd()) {
|
|
14291
14849
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
14292
14850
|
try {
|
|
14293
|
-
const configDir =
|
|
14851
|
+
const configDir = path25.join(workspacePath, ".cascade");
|
|
14294
14852
|
await fs9.mkdir(configDir, { recursive: true });
|
|
14295
|
-
const mdPath =
|
|
14853
|
+
const mdPath = path25.join(workspacePath, "CASCADE.md");
|
|
14296
14854
|
if (!await fileExists(mdPath)) {
|
|
14297
14855
|
await createDefaultCascadeMd(workspacePath);
|
|
14298
14856
|
spin.succeed(chalk9.green("Created CASCADE.md"));
|
|
14299
14857
|
}
|
|
14300
|
-
const ignorePath =
|
|
14858
|
+
const ignorePath = path25.join(workspacePath, ".cascadeignore");
|
|
14301
14859
|
if (!await fileExists(ignorePath)) {
|
|
14302
14860
|
await createDefaultIgnoreFile(workspacePath);
|
|
14303
14861
|
spin.succeed(chalk9.green("Created .cascadeignore"));
|
|
@@ -14306,7 +14864,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
14306
14864
|
console.log();
|
|
14307
14865
|
console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
14308
14866
|
console.log();
|
|
14309
|
-
const configPath =
|
|
14867
|
+
const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
14310
14868
|
if (await fileExists(configPath)) {
|
|
14311
14869
|
console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
14312
14870
|
console.log();
|
|
@@ -14364,7 +14922,7 @@ function fromEnv(env) {
|
|
|
14364
14922
|
return out;
|
|
14365
14923
|
}
|
|
14366
14924
|
async function fromClaudeCode(home) {
|
|
14367
|
-
const file =
|
|
14925
|
+
const file = path25.join(home, ".claude", ".credentials.json");
|
|
14368
14926
|
const data = await readJson(file);
|
|
14369
14927
|
if (!data) return [];
|
|
14370
14928
|
const oauth = data["claudeAiOauth"];
|
|
@@ -14387,7 +14945,7 @@ async function fromClaudeCode(home) {
|
|
|
14387
14945
|
return [];
|
|
14388
14946
|
}
|
|
14389
14947
|
async function fromCodex(home) {
|
|
14390
|
-
const file =
|
|
14948
|
+
const file = path25.join(home, ".codex", "auth.json");
|
|
14391
14949
|
const data = await readJson(file);
|
|
14392
14950
|
if (!data) return [];
|
|
14393
14951
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -14410,7 +14968,7 @@ async function fromCodex(home) {
|
|
|
14410
14968
|
return [];
|
|
14411
14969
|
}
|
|
14412
14970
|
async function fromGemini(home) {
|
|
14413
|
-
const file =
|
|
14971
|
+
const file = path25.join(home, ".gemini", "oauth_creds.json");
|
|
14414
14972
|
const data = await readJson(file);
|
|
14415
14973
|
const accessToken = str(data?.["access_token"]);
|
|
14416
14974
|
if (!accessToken) return [];
|
|
@@ -14426,7 +14984,7 @@ async function fromGemini(home) {
|
|
|
14426
14984
|
}
|
|
14427
14985
|
async function fromCopilot(home) {
|
|
14428
14986
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
14429
|
-
const file =
|
|
14987
|
+
const file = path25.join(home, ".config", "github-copilot", name);
|
|
14430
14988
|
const data = await readJson(file);
|
|
14431
14989
|
if (!data) continue;
|
|
14432
14990
|
for (const value of Object.values(data)) {
|
|
@@ -14447,7 +15005,7 @@ async function fromCopilot(home) {
|
|
|
14447
15005
|
return [];
|
|
14448
15006
|
}
|
|
14449
15007
|
async function discoverCredentials(opts = {}) {
|
|
14450
|
-
const home = opts.homeDir ??
|
|
15008
|
+
const home = opts.homeDir ?? os8.homedir();
|
|
14451
15009
|
const env = opts.env ?? process.env;
|
|
14452
15010
|
const groups = await Promise.all([
|
|
14453
15011
|
Promise.resolve(fromEnv(env)),
|
|
@@ -14480,7 +15038,7 @@ async function doctorCommand() {
|
|
|
14480
15038
|
checks.push({
|
|
14481
15039
|
label: "Cascade config",
|
|
14482
15040
|
ok: true,
|
|
14483
|
-
detail: `Loaded ${
|
|
15041
|
+
detail: `Loaded ${path25.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
14484
15042
|
});
|
|
14485
15043
|
const providers = [
|
|
14486
15044
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -14759,6 +15317,24 @@ var DashboardSocket = class {
|
|
|
14759
15317
|
});
|
|
14760
15318
|
});
|
|
14761
15319
|
}
|
|
15320
|
+
onSessionHalt(callback) {
|
|
15321
|
+
this.io.on("connection", (socket) => {
|
|
15322
|
+
socket.on("session:halt", (payload) => {
|
|
15323
|
+
if (typeof payload?.sessionId === "string") {
|
|
15324
|
+
callback(payload.sessionId);
|
|
15325
|
+
}
|
|
15326
|
+
});
|
|
15327
|
+
});
|
|
15328
|
+
}
|
|
15329
|
+
onSessionSteer(callback) {
|
|
15330
|
+
this.io.on("connection", (socket) => {
|
|
15331
|
+
socket.on("session:steer", (payload) => {
|
|
15332
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
15333
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
15334
|
+
}
|
|
15335
|
+
});
|
|
15336
|
+
});
|
|
15337
|
+
}
|
|
14762
15338
|
onConfigUpdate(callback) {
|
|
14763
15339
|
this.io.on("connection", (socket) => {
|
|
14764
15340
|
socket.on("config:update", (payload) => {
|
|
@@ -14793,7 +15369,7 @@ var DashboardSocket = class {
|
|
|
14793
15369
|
|
|
14794
15370
|
// src/dashboard/server.ts
|
|
14795
15371
|
init_constants();
|
|
14796
|
-
var __dirname$1 =
|
|
15372
|
+
var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
|
|
14797
15373
|
var DashboardServer = class {
|
|
14798
15374
|
app;
|
|
14799
15375
|
httpServer;
|
|
@@ -14804,6 +15380,13 @@ var DashboardServer = class {
|
|
|
14804
15380
|
globalStore = null;
|
|
14805
15381
|
broadcastTimer = null;
|
|
14806
15382
|
activeSessions = /* @__PURE__ */ new Map();
|
|
15383
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
15384
|
+
/**
|
|
15385
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
15386
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
15387
|
+
* of runs the session performed in this server's lifetime.
|
|
15388
|
+
*/
|
|
15389
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
14807
15390
|
port;
|
|
14808
15391
|
host;
|
|
14809
15392
|
workspacePath;
|
|
@@ -14864,6 +15447,8 @@ var DashboardServer = class {
|
|
|
14864
15447
|
});
|
|
14865
15448
|
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
14866
15449
|
const sessionId = requestedSessionId ?? randomUUID();
|
|
15450
|
+
const abortController = new AbortController();
|
|
15451
|
+
this.activeControllers.set(sessionId, abortController);
|
|
14867
15452
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
14868
15453
|
const title = this.persistRunStart(sessionId, prompt);
|
|
14869
15454
|
const cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
@@ -14882,7 +15467,8 @@ var DashboardServer = class {
|
|
|
14882
15467
|
this.socket.emitPeerMessage(e);
|
|
14883
15468
|
});
|
|
14884
15469
|
try {
|
|
14885
|
-
const result = await cascade.run({ prompt: runPrompt });
|
|
15470
|
+
const result = await cascade.run({ prompt: runPrompt, signal: abortController.signal });
|
|
15471
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
14886
15472
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
14887
15473
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
14888
15474
|
this.socket.broadcast("cost:update", {
|
|
@@ -14899,6 +15485,16 @@ var DashboardServer = class {
|
|
|
14899
15485
|
});
|
|
14900
15486
|
} finally {
|
|
14901
15487
|
this.activeSessions.delete(sessionId);
|
|
15488
|
+
this.activeControllers.delete(sessionId);
|
|
15489
|
+
}
|
|
15490
|
+
});
|
|
15491
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
15492
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
15493
|
+
});
|
|
15494
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15495
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15496
|
+
if (steered > 0) {
|
|
15497
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
14902
15498
|
}
|
|
14903
15499
|
});
|
|
14904
15500
|
}
|
|
@@ -14946,9 +15542,9 @@ var DashboardServer = class {
|
|
|
14946
15542
|
*/
|
|
14947
15543
|
persistConfig() {
|
|
14948
15544
|
try {
|
|
14949
|
-
const configPath =
|
|
14950
|
-
|
|
14951
|
-
|
|
15545
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
15546
|
+
fs23.mkdirSync(path25.dirname(configPath), { recursive: true });
|
|
15547
|
+
fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
14952
15548
|
} catch (err) {
|
|
14953
15549
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
14954
15550
|
}
|
|
@@ -14963,15 +15559,15 @@ var DashboardServer = class {
|
|
|
14963
15559
|
resolveDashboardSecret() {
|
|
14964
15560
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
14965
15561
|
if (fromConfig) return fromConfig;
|
|
14966
|
-
const secretPath =
|
|
15562
|
+
const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
14967
15563
|
try {
|
|
14968
|
-
if (
|
|
14969
|
-
const existing =
|
|
15564
|
+
if (fs23.existsSync(secretPath)) {
|
|
15565
|
+
const existing = fs23.readFileSync(secretPath, "utf-8").trim();
|
|
14970
15566
|
if (existing.length >= 16) return existing;
|
|
14971
15567
|
}
|
|
14972
15568
|
const generated = randomUUID();
|
|
14973
|
-
|
|
14974
|
-
|
|
15569
|
+
fs23.mkdirSync(path25.dirname(secretPath), { recursive: true });
|
|
15570
|
+
fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
14975
15571
|
if (this.config.dashboard.auth) {
|
|
14976
15572
|
console.warn(
|
|
14977
15573
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -14998,7 +15594,7 @@ var DashboardServer = class {
|
|
|
14998
15594
|
// ── Setup ─────────────────────────────────────
|
|
14999
15595
|
getGlobalStore() {
|
|
15000
15596
|
if (!this.globalStore) {
|
|
15001
|
-
const globalDbPath =
|
|
15597
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15002
15598
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
15003
15599
|
}
|
|
15004
15600
|
return this.globalStore;
|
|
@@ -15046,6 +15642,21 @@ var DashboardServer = class {
|
|
|
15046
15642
|
}
|
|
15047
15643
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
15048
15644
|
}
|
|
15645
|
+
/**
|
|
15646
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
15647
|
+
* session, or every active session when none is specified (the desktop
|
|
15648
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
15649
|
+
*/
|
|
15650
|
+
steerSessions(message, sessionId, nodeId) {
|
|
15651
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
15652
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
15653
|
+
return targets.length;
|
|
15654
|
+
}
|
|
15655
|
+
recordSessionTask(sessionId, taskId) {
|
|
15656
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
15657
|
+
list.push(taskId);
|
|
15658
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
15659
|
+
}
|
|
15049
15660
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15050
15661
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15051
15662
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15139,12 +15750,12 @@ ${prompt}`;
|
|
|
15139
15750
|
}
|
|
15140
15751
|
}
|
|
15141
15752
|
watchRuntimeChanges() {
|
|
15142
|
-
const workspaceDbPath =
|
|
15143
|
-
const globalDbPath =
|
|
15753
|
+
const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
|
|
15754
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15144
15755
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
15145
15756
|
for (const watchPath of watchPaths) {
|
|
15146
|
-
if (!
|
|
15147
|
-
|
|
15757
|
+
if (!fs23.existsSync(watchPath)) continue;
|
|
15758
|
+
fs23.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
15148
15759
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
15149
15760
|
});
|
|
15150
15761
|
}
|
|
@@ -15252,7 +15863,8 @@ ${prompt}`;
|
|
|
15252
15863
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
15253
15864
|
return;
|
|
15254
15865
|
}
|
|
15255
|
-
const
|
|
15866
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15867
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15256
15868
|
this.socket.broadcast("session:message-injected", payload);
|
|
15257
15869
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
15258
15870
|
res.json({ success: true, ...payload });
|
|
@@ -15274,7 +15886,7 @@ ${prompt}`;
|
|
|
15274
15886
|
const sessionId = req.params.id;
|
|
15275
15887
|
this.store.deleteSession(sessionId);
|
|
15276
15888
|
this.store.deleteRuntimeSession(sessionId);
|
|
15277
|
-
const globalDbPath =
|
|
15889
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15278
15890
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15279
15891
|
try {
|
|
15280
15892
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -15286,9 +15898,34 @@ ${prompt}`;
|
|
|
15286
15898
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15287
15899
|
res.json({ ok: true });
|
|
15288
15900
|
});
|
|
15901
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
15902
|
+
const sessionId = req.params.id;
|
|
15903
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
15904
|
+
if (!taskIds.length) {
|
|
15905
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
15906
|
+
return;
|
|
15907
|
+
}
|
|
15908
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
15909
|
+
for (const taskId of taskIds) {
|
|
15910
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
15911
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
15912
|
+
}
|
|
15913
|
+
}
|
|
15914
|
+
const { writeFile } = await import('fs/promises');
|
|
15915
|
+
let restored = 0;
|
|
15916
|
+
for (const [filePath, content] of toRestore) {
|
|
15917
|
+
try {
|
|
15918
|
+
await writeFile(filePath, content, "utf-8");
|
|
15919
|
+
restored++;
|
|
15920
|
+
} catch (err) {
|
|
15921
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
15922
|
+
}
|
|
15923
|
+
}
|
|
15924
|
+
res.json({ ok: true, restored });
|
|
15925
|
+
});
|
|
15289
15926
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
15290
15927
|
const body = req.body;
|
|
15291
|
-
const globalDbPath =
|
|
15928
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15292
15929
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
15293
15930
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15294
15931
|
try {
|
|
@@ -15311,7 +15948,7 @@ ${prompt}`;
|
|
|
15311
15948
|
});
|
|
15312
15949
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
15313
15950
|
this.store.deleteAllRuntimeNodes();
|
|
15314
|
-
const globalDbPath =
|
|
15951
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15315
15952
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15316
15953
|
try {
|
|
15317
15954
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -15362,6 +15999,19 @@ ${prompt}`;
|
|
|
15362
15999
|
this.store.deleteIdentity(req.params.id);
|
|
15363
16000
|
res.json({ ok: true });
|
|
15364
16001
|
});
|
|
16002
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
16003
|
+
try {
|
|
16004
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
16005
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
16006
|
+
try {
|
|
16007
|
+
res.json(logger.verifyChain());
|
|
16008
|
+
} finally {
|
|
16009
|
+
logger.close();
|
|
16010
|
+
}
|
|
16011
|
+
} catch (err) {
|
|
16012
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16013
|
+
}
|
|
16014
|
+
});
|
|
15365
16015
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
15366
16016
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
15367
16017
|
res.json(log);
|
|
@@ -15384,12 +16034,12 @@ ${prompt}`;
|
|
|
15384
16034
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
15385
16035
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
15386
16036
|
try {
|
|
15387
|
-
const configPath =
|
|
15388
|
-
const existing =
|
|
16037
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
16038
|
+
const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
|
|
15389
16039
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
15390
16040
|
const tmp = configPath + ".tmp";
|
|
15391
|
-
|
|
15392
|
-
|
|
16041
|
+
fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
16042
|
+
fs23.renameSync(tmp, configPath);
|
|
15393
16043
|
res.json({ ok: true });
|
|
15394
16044
|
} catch (err) {
|
|
15395
16045
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -15417,7 +16067,7 @@ ${prompt}`;
|
|
|
15417
16067
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
15418
16068
|
const scope = req.query["scope"] ?? "workspace";
|
|
15419
16069
|
if (scope === "global") {
|
|
15420
|
-
const globalDbPath =
|
|
16070
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15421
16071
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15422
16072
|
try {
|
|
15423
16073
|
res.json({
|
|
@@ -15467,6 +16117,7 @@ ${prompt}`;
|
|
|
15467
16117
|
});
|
|
15468
16118
|
try {
|
|
15469
16119
|
const result = await cascade.run({ prompt: runPrompt, identityId: body.identityId });
|
|
16120
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
15470
16121
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15471
16122
|
this.socket.broadcast("cost:update", {
|
|
15472
16123
|
sessionId,
|
|
@@ -15497,13 +16148,13 @@ ${prompt}`;
|
|
|
15497
16148
|
}))
|
|
15498
16149
|
});
|
|
15499
16150
|
});
|
|
15500
|
-
const prodPath =
|
|
15501
|
-
const devPath =
|
|
15502
|
-
const webDistPath =
|
|
15503
|
-
if (
|
|
16151
|
+
const prodPath = path25.resolve(__dirname$1, "../web/dist");
|
|
16152
|
+
const devPath = path25.resolve(__dirname$1, "../../web/dist");
|
|
16153
|
+
const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
|
|
16154
|
+
if (fs23.existsSync(webDistPath)) {
|
|
15504
16155
|
this.app.use(express.static(webDistPath));
|
|
15505
16156
|
this.app.get("*", (_req, res) => {
|
|
15506
|
-
res.sendFile(
|
|
16157
|
+
res.sendFile(path25.join(webDistPath, "index.html"));
|
|
15507
16158
|
});
|
|
15508
16159
|
} else {
|
|
15509
16160
|
this.app.get("/", (_req, res) => {
|
|
@@ -15524,7 +16175,7 @@ init_constants();
|
|
|
15524
16175
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
15525
16176
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
15526
16177
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
15527
|
-
const store = new MemoryStore(
|
|
16178
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15528
16179
|
const server = new DashboardServer(config, store, workspacePath);
|
|
15529
16180
|
server.watchRuntimeChanges();
|
|
15530
16181
|
const gThis = globalThis;
|
|
@@ -15556,7 +16207,7 @@ init_constants();
|
|
|
15556
16207
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
15557
16208
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
15558
16209
|
identity.command("list").description("List all available identities").action(() => {
|
|
15559
|
-
const store = new MemoryStore(
|
|
16210
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15560
16211
|
try {
|
|
15561
16212
|
const identities = store.listIdentities();
|
|
15562
16213
|
console.log(chalk9.bold("\n Identities:"));
|
|
@@ -15576,7 +16227,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15576
16227
|
}
|
|
15577
16228
|
});
|
|
15578
16229
|
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) => {
|
|
15579
|
-
const store = new MemoryStore(
|
|
16230
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15580
16231
|
try {
|
|
15581
16232
|
if (options.default) {
|
|
15582
16233
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -15602,7 +16253,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15602
16253
|
}
|
|
15603
16254
|
});
|
|
15604
16255
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
15605
|
-
const store = new MemoryStore(
|
|
16256
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15606
16257
|
try {
|
|
15607
16258
|
const identities = store.listIdentities();
|
|
15608
16259
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -15730,8 +16381,8 @@ async function exportCommand(options = {}) {
|
|
|
15730
16381
|
let store;
|
|
15731
16382
|
try {
|
|
15732
16383
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
15733
|
-
const workspaceDbPath =
|
|
15734
|
-
const globalDbPath =
|
|
16384
|
+
const workspaceDbPath = path25.join(workspacePath, CASCADE_DB_FILE);
|
|
16385
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
15735
16386
|
let dbPath = globalDbPath;
|
|
15736
16387
|
try {
|
|
15737
16388
|
await fs9.access(workspaceDbPath);
|
|
@@ -15773,7 +16424,7 @@ async function exportCommand(options = {}) {
|
|
|
15773
16424
|
const ext = format === "json" ? ".json" : ".md";
|
|
15774
16425
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
15775
16426
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
15776
|
-
const outPath = options.output ?
|
|
16427
|
+
const outPath = options.output ? path25.resolve(options.output) : path25.join(process.cwd(), defaultFile);
|
|
15777
16428
|
await fs9.writeFile(outPath, content, "utf-8");
|
|
15778
16429
|
spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
|
|
15779
16430
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
@@ -16001,11 +16652,11 @@ async function statsCommand() {
|
|
|
16001
16652
|
dotenv.config();
|
|
16002
16653
|
function warnIfBuildIsStale() {
|
|
16003
16654
|
try {
|
|
16004
|
-
const here =
|
|
16655
|
+
const here = path25.dirname(fileURLToPath(import.meta.url));
|
|
16005
16656
|
for (const rel of ["..", "../.."]) {
|
|
16006
|
-
const pkgPath =
|
|
16007
|
-
if (!
|
|
16008
|
-
const pkg = JSON.parse(
|
|
16657
|
+
const pkgPath = path25.join(here, rel, "package.json");
|
|
16658
|
+
if (!fs23.existsSync(pkgPath)) continue;
|
|
16659
|
+
const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
|
|
16009
16660
|
if (pkg.name !== "cascade-ai") continue;
|
|
16010
16661
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
16011
16662
|
console.error(
|