cascade-ai 0.12.24 → 0.13.2
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 +19 -2
- package/dist/cli.cjs +943 -211
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +938 -206
- package/dist/cli.js.map +1 -1
- package/dist/desktop-core.cjs +1619 -927
- package/dist/index.cjs +894 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -2
- package/dist/index.d.ts +198 -2
- package/dist/index.js +871 -174
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.cjs
CHANGED
|
@@ -8,19 +8,19 @@ var http = require('http');
|
|
|
8
8
|
var https = require('https');
|
|
9
9
|
var zlib = require('zlib');
|
|
10
10
|
var stream = require('stream');
|
|
11
|
+
var Database2 = require('better-sqlite3');
|
|
12
|
+
var path25 = require('path');
|
|
13
|
+
var fs23 = require('fs');
|
|
14
|
+
var os8 = require('os');
|
|
15
|
+
var crypto = require('crypto');
|
|
11
16
|
var ink = require('ink');
|
|
12
17
|
var commander = require('commander');
|
|
13
18
|
var React2 = require('react');
|
|
14
19
|
var chalk9 = require('chalk');
|
|
15
20
|
var dotenv = require('dotenv');
|
|
16
|
-
var fs21 = require('fs');
|
|
17
|
-
var path23 = require('path');
|
|
18
21
|
var url = require('url');
|
|
19
22
|
var fs9 = require('fs/promises');
|
|
20
|
-
var os6 = require('os');
|
|
21
|
-
var crypto = require('crypto');
|
|
22
23
|
var _ignoreModule = require('ignore');
|
|
23
|
-
var Database = require('better-sqlite3');
|
|
24
24
|
var zod = require('zod');
|
|
25
25
|
var child_process = require('child_process');
|
|
26
26
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -71,16 +71,16 @@ var dns__default = /*#__PURE__*/_interopDefault(dns);
|
|
|
71
71
|
var http__default = /*#__PURE__*/_interopDefault(http);
|
|
72
72
|
var https__default = /*#__PURE__*/_interopDefault(https);
|
|
73
73
|
var zlib__default = /*#__PURE__*/_interopDefault(zlib);
|
|
74
|
+
var Database2__default = /*#__PURE__*/_interopDefault(Database2);
|
|
75
|
+
var path25__default = /*#__PURE__*/_interopDefault(path25);
|
|
76
|
+
var fs23__default = /*#__PURE__*/_interopDefault(fs23);
|
|
77
|
+
var os8__default = /*#__PURE__*/_interopDefault(os8);
|
|
78
|
+
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
74
79
|
var React2__default = /*#__PURE__*/_interopDefault(React2);
|
|
75
80
|
var chalk9__default = /*#__PURE__*/_interopDefault(chalk9);
|
|
76
81
|
var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
|
|
77
|
-
var fs21__default = /*#__PURE__*/_interopDefault(fs21);
|
|
78
|
-
var path23__default = /*#__PURE__*/_interopDefault(path23);
|
|
79
82
|
var fs9__default = /*#__PURE__*/_interopDefault(fs9);
|
|
80
|
-
var os6__default = /*#__PURE__*/_interopDefault(os6);
|
|
81
|
-
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
|
|
82
83
|
var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
|
|
83
|
-
var Database__default = /*#__PURE__*/_interopDefault(Database);
|
|
84
84
|
var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter);
|
|
85
85
|
var PDFDocument__default = /*#__PURE__*/_interopDefault(PDFDocument);
|
|
86
86
|
var dns2__default = /*#__PURE__*/_interopDefault(dns2);
|
|
@@ -109,7 +109,7 @@ var __export = (target, all) => {
|
|
|
109
109
|
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
110
110
|
var init_constants = __esm({
|
|
111
111
|
"src/constants.ts"() {
|
|
112
|
-
CASCADE_VERSION = "0.
|
|
112
|
+
CASCADE_VERSION = "0.13.2";
|
|
113
113
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
114
114
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
115
115
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -1576,6 +1576,149 @@ var init_openai_compatible = __esm({
|
|
|
1576
1576
|
}
|
|
1577
1577
|
});
|
|
1578
1578
|
|
|
1579
|
+
// src/core/audit/audit-logger.ts
|
|
1580
|
+
var audit_logger_exports = {};
|
|
1581
|
+
__export(audit_logger_exports, {
|
|
1582
|
+
AuditLogger: () => AuditLogger2
|
|
1583
|
+
});
|
|
1584
|
+
var AuditLogger2;
|
|
1585
|
+
var init_audit_logger = __esm({
|
|
1586
|
+
"src/core/audit/audit-logger.ts"() {
|
|
1587
|
+
AuditLogger2 = class {
|
|
1588
|
+
constructor(workspacePath, debugMode = false) {
|
|
1589
|
+
this.workspacePath = workspacePath;
|
|
1590
|
+
this.debugMode = debugMode;
|
|
1591
|
+
const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
|
|
1592
|
+
if (!fs23__default.default.existsSync(cascadeDir)) {
|
|
1593
|
+
fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
1594
|
+
}
|
|
1595
|
+
this.keyPath = path25__default.default.join(cascadeDir, "audit_log.key");
|
|
1596
|
+
this.dbPath = path25__default.default.join(cascadeDir, "audit_log.db");
|
|
1597
|
+
this.initEncryptionKey();
|
|
1598
|
+
this.db = new Database2__default.default(this.dbPath);
|
|
1599
|
+
this.db.pragma("journal_mode = WAL");
|
|
1600
|
+
this.db.exec(`
|
|
1601
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
1602
|
+
id TEXT PRIMARY KEY,
|
|
1603
|
+
timestamp TEXT NOT NULL,
|
|
1604
|
+
event_type TEXT NOT NULL,
|
|
1605
|
+
tier_id TEXT NOT NULL,
|
|
1606
|
+
encrypted_payload TEXT NOT NULL,
|
|
1607
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
1608
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
1609
|
+
)
|
|
1610
|
+
`);
|
|
1611
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
1612
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
1613
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
1614
|
+
}
|
|
1615
|
+
workspacePath;
|
|
1616
|
+
debugMode;
|
|
1617
|
+
db;
|
|
1618
|
+
keyPath;
|
|
1619
|
+
dbPath;
|
|
1620
|
+
encryptionKey;
|
|
1621
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
1622
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
1623
|
+
return crypto__default.default.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
1624
|
+
}
|
|
1625
|
+
initEncryptionKey() {
|
|
1626
|
+
if (fs23__default.default.existsSync(this.keyPath)) {
|
|
1627
|
+
this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
|
|
1628
|
+
} else {
|
|
1629
|
+
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
1630
|
+
fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
encrypt(text) {
|
|
1634
|
+
const iv = crypto__default.default.randomBytes(16);
|
|
1635
|
+
const cipher = crypto__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1636
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
1637
|
+
encrypted += cipher.final("hex");
|
|
1638
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
1639
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
1640
|
+
}
|
|
1641
|
+
decrypt(text) {
|
|
1642
|
+
const parts = text.split(":");
|
|
1643
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
1644
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
1645
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
1646
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
1647
|
+
const decipher = crypto__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1648
|
+
decipher.setAuthTag(authTag);
|
|
1649
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
1650
|
+
decrypted += decipher.final("utf8");
|
|
1651
|
+
return decrypted;
|
|
1652
|
+
}
|
|
1653
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
1654
|
+
const id = crypto__default.default.randomUUID();
|
|
1655
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1656
|
+
const payload = JSON.stringify(payloadObj);
|
|
1657
|
+
const encryptedPayload = this.encrypt(payload);
|
|
1658
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
1659
|
+
const prevHash = last?.hash ?? "";
|
|
1660
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
1661
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
1662
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
1663
|
+
this.dumpDebugIfNeeded();
|
|
1664
|
+
return id;
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
1668
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
1669
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
1670
|
+
* can only be claimed for chained history.
|
|
1671
|
+
*/
|
|
1672
|
+
verifyChain() {
|
|
1673
|
+
const rows = this.db.prepare(
|
|
1674
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
1675
|
+
).all();
|
|
1676
|
+
let prevHash = "";
|
|
1677
|
+
for (const row of rows) {
|
|
1678
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
1679
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
1680
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
1681
|
+
}
|
|
1682
|
+
prevHash = row.hash;
|
|
1683
|
+
}
|
|
1684
|
+
return { ok: true, entries: rows.length };
|
|
1685
|
+
}
|
|
1686
|
+
getAllLogs() {
|
|
1687
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
1688
|
+
const rows = stmt.all();
|
|
1689
|
+
return rows.map((row) => {
|
|
1690
|
+
let payload = "";
|
|
1691
|
+
try {
|
|
1692
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
1695
|
+
}
|
|
1696
|
+
return {
|
|
1697
|
+
id: row.id,
|
|
1698
|
+
timestamp: row.timestamp,
|
|
1699
|
+
eventType: row.event_type,
|
|
1700
|
+
tierId: row.tier_id,
|
|
1701
|
+
payload
|
|
1702
|
+
};
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
dumpDebugIfNeeded() {
|
|
1706
|
+
if (!this.debugMode) return;
|
|
1707
|
+
try {
|
|
1708
|
+
const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_audit_logs_debug.json");
|
|
1709
|
+
const entries = this.getAllLogs();
|
|
1710
|
+
fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
1711
|
+
} catch (err) {
|
|
1712
|
+
console.error("Failed to dump debug audit logs", err);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
close() {
|
|
1716
|
+
this.db.close();
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1579
1722
|
// src/cli/index.ts
|
|
1580
1723
|
init_constants();
|
|
1581
1724
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -1619,7 +1762,7 @@ var Keystore = class {
|
|
|
1619
1762
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
1620
1763
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
1621
1764
|
this.backend = "keytar";
|
|
1622
|
-
if (password &&
|
|
1765
|
+
if (password && fs23__default.default.existsSync(this.storePath)) {
|
|
1623
1766
|
try {
|
|
1624
1767
|
const fileEntries = this.decryptFile(password);
|
|
1625
1768
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -1638,7 +1781,7 @@ var Keystore = class {
|
|
|
1638
1781
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
1639
1782
|
);
|
|
1640
1783
|
}
|
|
1641
|
-
if (!
|
|
1784
|
+
if (!fs23__default.default.existsSync(this.storePath)) {
|
|
1642
1785
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
1643
1786
|
this.masterKey = this.deriveKey(password, salt);
|
|
1644
1787
|
this.writeWithSalt({}, salt);
|
|
@@ -1652,7 +1795,7 @@ var Keystore = class {
|
|
|
1652
1795
|
}
|
|
1653
1796
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
1654
1797
|
unlockSync(password) {
|
|
1655
|
-
if (!
|
|
1798
|
+
if (!fs23__default.default.existsSync(this.storePath)) {
|
|
1656
1799
|
const salt = crypto__default.default.randomBytes(SALT_LEN);
|
|
1657
1800
|
this.masterKey = this.deriveKey(password, salt);
|
|
1658
1801
|
this.writeWithSalt({}, salt);
|
|
@@ -1710,7 +1853,7 @@ var Keystore = class {
|
|
|
1710
1853
|
}
|
|
1711
1854
|
}
|
|
1712
1855
|
decryptFile(password, knownSalt) {
|
|
1713
|
-
if (!
|
|
1856
|
+
if (!fs23__default.default.existsSync(this.storePath)) return {};
|
|
1714
1857
|
try {
|
|
1715
1858
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
1716
1859
|
const useSalt = knownSalt ?? salt;
|
|
@@ -1732,8 +1875,8 @@ var Keystore = class {
|
|
|
1732
1875
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1733
1876
|
const tag = cipher.getAuthTag();
|
|
1734
1877
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
1735
|
-
|
|
1736
|
-
|
|
1878
|
+
fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
|
|
1879
|
+
fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1737
1880
|
}
|
|
1738
1881
|
writeWithSalt(data, salt) {
|
|
1739
1882
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -1743,11 +1886,11 @@ var Keystore = class {
|
|
|
1743
1886
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1744
1887
|
const tag = cipher.getAuthTag();
|
|
1745
1888
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
1746
|
-
|
|
1747
|
-
|
|
1889
|
+
fs23__default.default.mkdirSync(path25__default.default.dirname(this.storePath), { recursive: true });
|
|
1890
|
+
fs23__default.default.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1748
1891
|
}
|
|
1749
1892
|
readRaw() {
|
|
1750
|
-
const buf =
|
|
1893
|
+
const buf = fs23__default.default.readFileSync(this.storePath);
|
|
1751
1894
|
let offset = 0;
|
|
1752
1895
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
1753
1896
|
offset += SALT_LEN;
|
|
@@ -1780,7 +1923,7 @@ var CascadeIgnore = class {
|
|
|
1780
1923
|
]);
|
|
1781
1924
|
}
|
|
1782
1925
|
async load(workspacePath) {
|
|
1783
|
-
const filePath =
|
|
1926
|
+
const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
|
|
1784
1927
|
try {
|
|
1785
1928
|
const content = await fs9__default.default.readFile(filePath, "utf-8");
|
|
1786
1929
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -1791,7 +1934,7 @@ var CascadeIgnore = class {
|
|
|
1791
1934
|
}
|
|
1792
1935
|
isIgnored(filePath, workspacePath) {
|
|
1793
1936
|
try {
|
|
1794
|
-
const relative = workspacePath ?
|
|
1937
|
+
const relative = workspacePath ? path25__default.default.relative(workspacePath, filePath) : filePath;
|
|
1795
1938
|
return this.ig.ignores(relative);
|
|
1796
1939
|
} catch {
|
|
1797
1940
|
return false;
|
|
@@ -1802,7 +1945,7 @@ var CascadeIgnore = class {
|
|
|
1802
1945
|
}
|
|
1803
1946
|
};
|
|
1804
1947
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
1805
|
-
const filePath =
|
|
1948
|
+
const filePath = path25__default.default.join(workspacePath, ".cascadeignore");
|
|
1806
1949
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
1807
1950
|
# Syntax identical to .gitignore
|
|
1808
1951
|
|
|
@@ -1832,7 +1975,7 @@ Thumbs.db
|
|
|
1832
1975
|
await fs9__default.default.writeFile(filePath, content, "utf-8");
|
|
1833
1976
|
}
|
|
1834
1977
|
async function loadCascadeMd(workspacePath) {
|
|
1835
|
-
const filePath =
|
|
1978
|
+
const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
|
|
1836
1979
|
try {
|
|
1837
1980
|
const raw = await fs9__default.default.readFile(filePath, "utf-8");
|
|
1838
1981
|
return parseCascadeMd(raw);
|
|
@@ -1861,7 +2004,7 @@ ${raw.trim()}`;
|
|
|
1861
2004
|
return { raw, sections, systemPrompt };
|
|
1862
2005
|
}
|
|
1863
2006
|
async function createDefaultCascadeMd(workspacePath) {
|
|
1864
|
-
const filePath =
|
|
2007
|
+
const filePath = path25__default.default.join(workspacePath, "CASCADE.md");
|
|
1865
2008
|
const content = `# Cascade Project Instructions
|
|
1866
2009
|
|
|
1867
2010
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -1896,9 +2039,9 @@ List any areas the agent should not touch.
|
|
|
1896
2039
|
var MemoryStore = class _MemoryStore {
|
|
1897
2040
|
db;
|
|
1898
2041
|
constructor(dbPath) {
|
|
1899
|
-
|
|
2042
|
+
fs23__default.default.mkdirSync(path25__default.default.dirname(dbPath), { recursive: true });
|
|
1900
2043
|
try {
|
|
1901
|
-
this.db = new
|
|
2044
|
+
this.db = new Database2__default.default(dbPath, { timeout: 5e3 });
|
|
1902
2045
|
this.db.pragma("journal_mode = WAL");
|
|
1903
2046
|
this.db.pragma("foreign_keys = ON");
|
|
1904
2047
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -2743,7 +2886,8 @@ var WorkspaceConfigSchema = zod.z.object({
|
|
|
2743
2886
|
cascadeMdPath: zod.z.string().default("CASCADE.md"),
|
|
2744
2887
|
configPath: zod.z.string().default(".cascade/config.json"),
|
|
2745
2888
|
keystorePath: zod.z.string().default(".cascade/keystore.enc"),
|
|
2746
|
-
auditLogPath: zod.z.string().default(".cascade/audit.log")
|
|
2889
|
+
auditLogPath: zod.z.string().default(".cascade/audit.log"),
|
|
2890
|
+
debugWorldState: zod.z.boolean().default(false)
|
|
2747
2891
|
});
|
|
2748
2892
|
var CascadeConfigSchema = zod.z.object({
|
|
2749
2893
|
version: zod.z.literal("1.0").default("1.0"),
|
|
@@ -2888,6 +3032,16 @@ var CascadeConfigSchema = zod.z.object({
|
|
|
2888
3032
|
* 'parallel' / 'sequential' — force it.
|
|
2889
3033
|
*/
|
|
2890
3034
|
t3Execution: zod.z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
3035
|
+
/**
|
|
3036
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
3037
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
3038
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
3039
|
+
*/
|
|
3040
|
+
privacy: zod.z.object({
|
|
3041
|
+
paths: zod.z.array(zod.z.object({ pattern: zod.z.string().min(1), policy: zod.z.enum(["local-only"]) })).default([])
|
|
3042
|
+
}).optional(),
|
|
3043
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
3044
|
+
routing: zod.z.object({ forceTier: zod.z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
2891
3045
|
/**
|
|
2892
3046
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
2893
3047
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -2937,15 +3091,15 @@ var ConfigManager = class {
|
|
|
2937
3091
|
globalDir;
|
|
2938
3092
|
constructor(workspacePath = process.cwd()) {
|
|
2939
3093
|
this.workspacePath = workspacePath;
|
|
2940
|
-
this.globalDir =
|
|
3094
|
+
this.globalDir = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR);
|
|
2941
3095
|
}
|
|
2942
3096
|
async load() {
|
|
2943
3097
|
this.config = await this.loadConfig();
|
|
2944
3098
|
this.ignore = new CascadeIgnore();
|
|
2945
3099
|
await this.ignore.load(this.workspacePath);
|
|
2946
3100
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
2947
|
-
this.keystore = new Keystore(
|
|
2948
|
-
this.store = new MemoryStore(
|
|
3101
|
+
this.keystore = new Keystore(path25__default.default.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
3102
|
+
this.store = new MemoryStore(path25__default.default.join(this.workspacePath, CASCADE_DB_FILE));
|
|
2949
3103
|
await this.injectEnvKeys();
|
|
2950
3104
|
await this.ensureDefaultIdentity();
|
|
2951
3105
|
}
|
|
@@ -2968,8 +3122,8 @@ var ConfigManager = class {
|
|
|
2968
3122
|
return this.workspacePath;
|
|
2969
3123
|
}
|
|
2970
3124
|
async save() {
|
|
2971
|
-
const configPath =
|
|
2972
|
-
await fs9__default.default.mkdir(
|
|
3125
|
+
const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
3126
|
+
await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
|
|
2973
3127
|
await fs9__default.default.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
2974
3128
|
}
|
|
2975
3129
|
async updateConfig(updates) {
|
|
@@ -2993,7 +3147,7 @@ var ConfigManager = class {
|
|
|
2993
3147
|
return configProvider?.apiKey;
|
|
2994
3148
|
}
|
|
2995
3149
|
async loadConfig() {
|
|
2996
|
-
const configPath =
|
|
3150
|
+
const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
2997
3151
|
try {
|
|
2998
3152
|
const raw = await fs9__default.default.readFile(configPath, "utf-8");
|
|
2999
3153
|
return validateConfig(JSON.parse(raw));
|
|
@@ -3872,7 +4026,7 @@ init_constants();
|
|
|
3872
4026
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
3873
4027
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
3874
4028
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
3875
|
-
var DEFAULT_CACHE_FILE =
|
|
4029
|
+
var DEFAULT_CACHE_FILE = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
3876
4030
|
function normalizeModelId(id) {
|
|
3877
4031
|
let s = id.toLowerCase();
|
|
3878
4032
|
const slash = s.lastIndexOf("/");
|
|
@@ -3987,7 +4141,7 @@ var LiveDataProvider = class {
|
|
|
3987
4141
|
}
|
|
3988
4142
|
async saveCache() {
|
|
3989
4143
|
try {
|
|
3990
|
-
await fs9__default.default.mkdir(
|
|
4144
|
+
await fs9__default.default.mkdir(path25__default.default.dirname(this.opts.cacheFile), { recursive: true });
|
|
3991
4145
|
const cache = {
|
|
3992
4146
|
fetchedAt: this.fetchedAt,
|
|
3993
4147
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -4117,7 +4271,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4117
4271
|
costByTier: {},
|
|
4118
4272
|
tokensByTier: {},
|
|
4119
4273
|
inputTokensByTier: {},
|
|
4120
|
-
outputTokensByTier: {}
|
|
4274
|
+
outputTokensByTier: {},
|
|
4275
|
+
costByFeature: {}
|
|
4121
4276
|
};
|
|
4122
4277
|
tierModels = /* @__PURE__ */ new Map();
|
|
4123
4278
|
config;
|
|
@@ -4139,6 +4294,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4139
4294
|
tpmLimiter;
|
|
4140
4295
|
localQueue;
|
|
4141
4296
|
taskAnalyzer;
|
|
4297
|
+
worldStateDB;
|
|
4298
|
+
privacyPaths;
|
|
4299
|
+
guidanceQueue;
|
|
4142
4300
|
liveData;
|
|
4143
4301
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
4144
4302
|
originalTierModels;
|
|
@@ -4296,7 +4454,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4296
4454
|
if (options.model && !requireVision) {
|
|
4297
4455
|
this.ensureProvider(options.model, this.config.providers);
|
|
4298
4456
|
}
|
|
4299
|
-
|
|
4457
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
4458
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
4459
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
4460
|
+
if (!localModel) {
|
|
4461
|
+
throw new Error(
|
|
4462
|
+
"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."
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
4466
|
+
model = localModel;
|
|
4467
|
+
}
|
|
4300
4468
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
4301
4469
|
const provider = this.getProvider(model);
|
|
4302
4470
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -4368,7 +4536,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4368
4536
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
4369
4537
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
4370
4538
|
}
|
|
4371
|
-
this.recordStats(tier, model, result.usage);
|
|
4539
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
4372
4540
|
this.failover.recordSuccess(model.provider);
|
|
4373
4541
|
return result;
|
|
4374
4542
|
} catch (err) {
|
|
@@ -4473,6 +4641,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4473
4641
|
setTaskAnalyzer(analyzer) {
|
|
4474
4642
|
this.taskAnalyzer = analyzer;
|
|
4475
4643
|
}
|
|
4644
|
+
setWorldStateDB(db) {
|
|
4645
|
+
this.worldStateDB = db;
|
|
4646
|
+
}
|
|
4647
|
+
getWorldStateDB() {
|
|
4648
|
+
return this.worldStateDB;
|
|
4649
|
+
}
|
|
4650
|
+
setPrivacyPaths(paths) {
|
|
4651
|
+
this.privacyPaths = paths;
|
|
4652
|
+
}
|
|
4653
|
+
getPrivacyPaths() {
|
|
4654
|
+
return this.privacyPaths;
|
|
4655
|
+
}
|
|
4656
|
+
setGuidanceQueue(queue) {
|
|
4657
|
+
this.guidanceQueue = queue;
|
|
4658
|
+
}
|
|
4659
|
+
getGuidanceQueue() {
|
|
4660
|
+
return this.guidanceQueue;
|
|
4661
|
+
}
|
|
4662
|
+
/**
|
|
4663
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
4664
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
4665
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
4666
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
4667
|
+
*/
|
|
4668
|
+
isPrivateModel(model) {
|
|
4669
|
+
if (model.isLocal) return true;
|
|
4670
|
+
if (model.provider !== "openai-compatible") return false;
|
|
4671
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
4672
|
+
if (!baseUrl) return false;
|
|
4673
|
+
try {
|
|
4674
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
4675
|
+
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");
|
|
4676
|
+
} catch {
|
|
4677
|
+
return false;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4476
4680
|
/**
|
|
4477
4681
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
4478
4682
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -4496,7 +4700,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4496
4700
|
costByTier: { ...this.stats.costByTier },
|
|
4497
4701
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
4498
4702
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
4499
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
4703
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
4704
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
4500
4705
|
};
|
|
4501
4706
|
}
|
|
4502
4707
|
/**
|
|
@@ -4546,7 +4751,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4546
4751
|
costByTier: {},
|
|
4547
4752
|
tokensByTier: {},
|
|
4548
4753
|
inputTokensByTier: {},
|
|
4549
|
-
outputTokensByTier: {}
|
|
4754
|
+
outputTokensByTier: {},
|
|
4755
|
+
costByFeature: {}
|
|
4550
4756
|
};
|
|
4551
4757
|
this.sessionCostUsd = 0;
|
|
4552
4758
|
this.budgetState = "ok";
|
|
@@ -4712,7 +4918,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4712
4918
|
}
|
|
4713
4919
|
return void 0;
|
|
4714
4920
|
}
|
|
4715
|
-
recordStats(tier, model, usage) {
|
|
4921
|
+
recordStats(tier, model, usage, featureTag) {
|
|
4716
4922
|
this.stats.totalTokens += usage.totalTokens;
|
|
4717
4923
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
4718
4924
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -4722,6 +4928,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
|
|
|
4722
4928
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
4723
4929
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
4724
4930
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
4931
|
+
if (featureTag) {
|
|
4932
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
4933
|
+
}
|
|
4725
4934
|
this.runTokens += usage.totalTokens;
|
|
4726
4935
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
4727
4936
|
this.updateBudgetState();
|
|
@@ -4818,6 +5027,13 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
4818
5027
|
hierarchyContext = "";
|
|
4819
5028
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
4820
5029
|
signal;
|
|
5030
|
+
/**
|
|
5031
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
5032
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
5033
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
5034
|
+
* which would interleave, are not tagged.
|
|
5035
|
+
*/
|
|
5036
|
+
isPresenter = false;
|
|
4821
5037
|
constructor(role, id, parentId) {
|
|
4822
5038
|
super();
|
|
4823
5039
|
this.role = role;
|
|
@@ -4825,6 +5041,10 @@ var BaseTier = class extends EventEmitter__default.default {
|
|
|
4825
5041
|
this.parentId = parentId;
|
|
4826
5042
|
this.label = this.id;
|
|
4827
5043
|
}
|
|
5044
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
5045
|
+
setPresenter(on = true) {
|
|
5046
|
+
this.isPresenter = on;
|
|
5047
|
+
}
|
|
4828
5048
|
getStatus() {
|
|
4829
5049
|
return this.status;
|
|
4830
5050
|
}
|
|
@@ -5259,6 +5479,8 @@ var T3Worker = class extends BaseTier {
|
|
|
5259
5479
|
toolRegistry;
|
|
5260
5480
|
context;
|
|
5261
5481
|
assignment;
|
|
5482
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
5483
|
+
localOnlyMatch = false;
|
|
5262
5484
|
peerSyncBuffer = [];
|
|
5263
5485
|
store;
|
|
5264
5486
|
audit;
|
|
@@ -5314,6 +5536,11 @@ var T3Worker = class extends BaseTier {
|
|
|
5314
5536
|
this.taskId = taskId;
|
|
5315
5537
|
this.setLabel(assignment.subtaskTitle);
|
|
5316
5538
|
this.setStatus("ACTIVE");
|
|
5539
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
5540
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
5541
|
+
if (this.localOnlyMatch) {
|
|
5542
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
5543
|
+
}
|
|
5317
5544
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
5318
5545
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
5319
5546
|
this.tools = [...this.tools, {
|
|
@@ -5444,9 +5671,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5444
5671
|
}
|
|
5445
5672
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
5446
5673
|
if (reflectCfg.enabled) {
|
|
5447
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
5674
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
5448
5675
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
5449
5676
|
}
|
|
5677
|
+
const db = this.router.getWorldStateDB?.();
|
|
5678
|
+
if (db) {
|
|
5679
|
+
try {
|
|
5680
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
5681
|
+
} catch (e) {
|
|
5682
|
+
this.log("Failed to write to World State DB");
|
|
5683
|
+
}
|
|
5684
|
+
}
|
|
5450
5685
|
this.setStatus("COMPLETED", output);
|
|
5451
5686
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
5452
5687
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -5519,6 +5754,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
5519
5754
|
while (iterations < MAX_ITERATIONS) {
|
|
5520
5755
|
iterations++;
|
|
5521
5756
|
this.throwIfCancelled();
|
|
5757
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
5758
|
+
for (const g of guidance) {
|
|
5759
|
+
this.log(`User intervention received: ${g.text}`);
|
|
5760
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
5761
|
+
await this.context.addMessage({
|
|
5762
|
+
role: "user",
|
|
5763
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
5764
|
+
${g.text}`
|
|
5765
|
+
});
|
|
5766
|
+
}
|
|
5522
5767
|
const options = {
|
|
5523
5768
|
messages: this.context.getMessages(),
|
|
5524
5769
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -5527,13 +5772,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5527
5772
|
// Don't pass tools array when model can't use them natively
|
|
5528
5773
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
5529
5774
|
maxTokens: 4096,
|
|
5530
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
5775
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
5776
|
+
featureTag: this.assignment?.sectionTitle,
|
|
5777
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5531
5778
|
};
|
|
5532
5779
|
const result = await this.router.generate(
|
|
5533
5780
|
"T3",
|
|
5534
5781
|
options,
|
|
5535
5782
|
(chunk) => {
|
|
5536
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
5783
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5537
5784
|
}
|
|
5538
5785
|
);
|
|
5539
5786
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -5691,8 +5938,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5691
5938
|
tierId: this.id,
|
|
5692
5939
|
sessionId: this.taskId,
|
|
5693
5940
|
requireApproval: false,
|
|
5694
|
-
saveSnapshot: async (
|
|
5695
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
5941
|
+
saveSnapshot: async (path30, content) => {
|
|
5942
|
+
this.store?.addFileSnapshot(this.taskId, path30, content);
|
|
5696
5943
|
},
|
|
5697
5944
|
sendPeerSync: (to, syncType, content) => {
|
|
5698
5945
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -5862,7 +6109,7 @@ ${assignment.expectedOutput}`;
|
|
|
5862
6109
|
const { promisify: promisify4 } = await import('util');
|
|
5863
6110
|
const execAsync3 = promisify4(exec3);
|
|
5864
6111
|
for (const artifactPath of artifactPaths) {
|
|
5865
|
-
const absolutePath =
|
|
6112
|
+
const absolutePath = path25__default.default.resolve(process.cwd(), artifactPath);
|
|
5866
6113
|
try {
|
|
5867
6114
|
const stat = await fs9__default.default.stat(absolutePath);
|
|
5868
6115
|
if (!stat.isFile()) {
|
|
@@ -5883,7 +6130,7 @@ ${assignment.expectedOutput}`;
|
|
|
5883
6130
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
5884
6131
|
continue;
|
|
5885
6132
|
}
|
|
5886
|
-
const ext =
|
|
6133
|
+
const ext = path25__default.default.extname(absolutePath).toLowerCase();
|
|
5887
6134
|
try {
|
|
5888
6135
|
if (ext === ".ts" || ext === ".tsx") {
|
|
5889
6136
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -5912,30 +6159,35 @@ ${stdout}`);
|
|
|
5912
6159
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
5913
6160
|
*/
|
|
5914
6161
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
5915
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5916
|
-
|
|
5917
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
5918
6162
|
let current = output;
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
const
|
|
6163
|
+
try {
|
|
6164
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
6165
|
+
const verdictResult = await this.router.generate("T2", {
|
|
5922
6166
|
messages: [{
|
|
5923
6167
|
role: "user",
|
|
5924
|
-
content: `
|
|
6168
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
5925
6169
|
|
|
5926
|
-
Goal
|
|
6170
|
+
Goal: ${assignment.expectedOutput}
|
|
5927
6171
|
Subtask: ${assignment.description}
|
|
5928
|
-
|
|
5929
|
-
Output:
|
|
6172
|
+
Current Output:
|
|
5930
6173
|
${current}
|
|
5931
6174
|
|
|
5932
|
-
|
|
6175
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
6176
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
5933
6177
|
}],
|
|
5934
|
-
systemPrompt:
|
|
5935
|
-
maxTokens: 400
|
|
6178
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
6179
|
+
maxTokens: 400,
|
|
6180
|
+
signal: this.signal,
|
|
6181
|
+
featureTag: assignment.sectionTitle,
|
|
6182
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5936
6183
|
});
|
|
5937
|
-
const
|
|
5938
|
-
|
|
6184
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
6185
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
6186
|
+
if (parsed.sufficient !== false) {
|
|
6187
|
+
this.log("T2-Critic approved output.");
|
|
6188
|
+
break;
|
|
6189
|
+
}
|
|
6190
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
5939
6191
|
const improved = await this.router.generate("T3", {
|
|
5940
6192
|
messages: [{
|
|
5941
6193
|
role: "user",
|
|
@@ -5947,16 +6199,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
5947
6199
|
Current output:
|
|
5948
6200
|
${current}`
|
|
5949
6201
|
}],
|
|
5950
|
-
systemPrompt:
|
|
5951
|
-
|
|
6202
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
6203
|
+
|
|
6204
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6205
|
+
maxTokens: 4096,
|
|
6206
|
+
featureTag: assignment.sectionTitle,
|
|
6207
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5952
6208
|
});
|
|
5953
6209
|
const next = (improved.content ?? "").trim();
|
|
5954
6210
|
if (!next) break;
|
|
5955
6211
|
current = next;
|
|
5956
6212
|
this.log("Reflection: revised output for better goal alignment.");
|
|
5957
|
-
} catch {
|
|
5958
|
-
break;
|
|
5959
6213
|
}
|
|
6214
|
+
} catch (e) {
|
|
6215
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
5960
6216
|
}
|
|
5961
6217
|
return current;
|
|
5962
6218
|
}
|
|
@@ -5977,7 +6233,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
5977
6233
|
maxTokens: 500,
|
|
5978
6234
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5979
6235
|
|
|
5980
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
6236
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6237
|
+
featureTag: assignment.sectionTitle,
|
|
6238
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5981
6239
|
});
|
|
5982
6240
|
try {
|
|
5983
6241
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -6072,6 +6330,7 @@ Begin execution now.`;
|
|
|
6072
6330
|
issues,
|
|
6073
6331
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
6074
6332
|
correctionAttempts,
|
|
6333
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
6075
6334
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
6076
6335
|
};
|
|
6077
6336
|
}
|
|
@@ -6363,6 +6622,42 @@ var PeerBus = class extends EventEmitter__default.default {
|
|
|
6363
6622
|
}
|
|
6364
6623
|
};
|
|
6365
6624
|
|
|
6625
|
+
// src/core/audit/redaction.ts
|
|
6626
|
+
var RedactionLayer = class {
|
|
6627
|
+
// Regexes for common secrets/PII
|
|
6628
|
+
static RULES = [
|
|
6629
|
+
// IPv4 addresses (basic approximation)
|
|
6630
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
6631
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
6632
|
+
{ 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]" },
|
|
6633
|
+
// Email addresses
|
|
6634
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
6635
|
+
// Phone numbers (simplistic)
|
|
6636
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
6637
|
+
// AWS Access Key ID
|
|
6638
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
6639
|
+
];
|
|
6640
|
+
/**
|
|
6641
|
+
* Applies all redaction rules to the input string.
|
|
6642
|
+
*/
|
|
6643
|
+
static redact(text) {
|
|
6644
|
+
if (!text) return text;
|
|
6645
|
+
let redacted = text;
|
|
6646
|
+
for (const rule of this.RULES) {
|
|
6647
|
+
if (rule.pattern.test(redacted)) {
|
|
6648
|
+
rule.pattern.lastIndex = 0;
|
|
6649
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
|
|
6650
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
6651
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
6652
|
+
}
|
|
6653
|
+
return rule.replacement;
|
|
6654
|
+
});
|
|
6655
|
+
}
|
|
6656
|
+
}
|
|
6657
|
+
return redacted;
|
|
6658
|
+
}
|
|
6659
|
+
};
|
|
6660
|
+
|
|
6366
6661
|
// src/core/tiers/t2-manager.ts
|
|
6367
6662
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
6368
6663
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -6591,7 +6886,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6591
6886
|
try {
|
|
6592
6887
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
6593
6888
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
6594
|
-
|
|
6889
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
6890
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
6595
6891
|
} catch {
|
|
6596
6892
|
return [{
|
|
6597
6893
|
subtaskId: crypto.randomUUID(),
|
|
@@ -6601,6 +6897,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6601
6897
|
constraints: assignment.constraints,
|
|
6602
6898
|
peerT3Ids: [],
|
|
6603
6899
|
parentT2: this.id,
|
|
6900
|
+
sectionTitle: assignment.sectionTitle,
|
|
6901
|
+
dependsOn: [],
|
|
6604
6902
|
executionMode: "parallel"
|
|
6605
6903
|
}];
|
|
6606
6904
|
}
|
|
@@ -6720,6 +7018,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6720
7018
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
6721
7019
|
const worker = workerMap.get(id);
|
|
6722
7020
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
7021
|
+
if (result.localOnly) {
|
|
7022
|
+
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}]`;
|
|
7023
|
+
} else {
|
|
7024
|
+
if (typeof result.output === "string" && result.output) {
|
|
7025
|
+
result.output = RedactionLayer.redact(result.output);
|
|
7026
|
+
}
|
|
7027
|
+
}
|
|
7028
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
6723
7029
|
resultMap.set(id, result);
|
|
6724
7030
|
return result;
|
|
6725
7031
|
};
|
|
@@ -6935,6 +7241,7 @@ ${peerOutputs}` : "";
|
|
|
6935
7241
|
chunkEnd++;
|
|
6936
7242
|
}
|
|
6937
7243
|
i = chunkEnd;
|
|
7244
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
6938
7245
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
6939
7246
|
${currentSummary ? `
|
|
6940
7247
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -6944,6 +7251,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
6944
7251
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
6945
7252
|
const messages = [{ role: "user", content: prompt }];
|
|
6946
7253
|
try {
|
|
7254
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
6947
7255
|
const result = await this.router.generate("T2", {
|
|
6948
7256
|
messages,
|
|
6949
7257
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -6951,7 +7259,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
6951
7259
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6952
7260
|
maxTokens: 500,
|
|
6953
7261
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
6954
|
-
});
|
|
7262
|
+
}, streamFinal);
|
|
6955
7263
|
currentSummary = result.content;
|
|
6956
7264
|
} catch (err) {
|
|
6957
7265
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -7003,14 +7311,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
7003
7311
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
7004
7312
|
});
|
|
7005
7313
|
const answer = result.content.trim().toUpperCase();
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
}
|
|
7009
|
-
if (answer.includes("NO")) {
|
|
7010
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
7011
|
-
}
|
|
7314
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7315
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
7012
7316
|
return null;
|
|
7013
7317
|
} catch {
|
|
7318
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
7014
7319
|
return null;
|
|
7015
7320
|
}
|
|
7016
7321
|
}
|
|
@@ -7352,10 +7657,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7352
7657
|
}
|
|
7353
7658
|
}
|
|
7354
7659
|
async decomposeTask(prompt, systemContext) {
|
|
7660
|
+
const db = this.router.getWorldStateDB?.();
|
|
7661
|
+
const worldStateContext = db ? `
|
|
7662
|
+
|
|
7663
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7664
|
+
${db.getFormattedState()}` : "";
|
|
7355
7665
|
const contextSection = systemContext ? `
|
|
7356
7666
|
Project context:
|
|
7357
7667
|
${systemContext}` : "";
|
|
7358
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
7668
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
7359
7669
|
|
|
7360
7670
|
Task: ${prompt}
|
|
7361
7671
|
|
|
@@ -7687,7 +7997,7 @@ Instructions:
|
|
|
7687
7997
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
7688
7998
|
maxTokens: 8e3
|
|
7689
7999
|
}, (chunk) => {
|
|
7690
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
8000
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
7691
8001
|
});
|
|
7692
8002
|
return result.content;
|
|
7693
8003
|
}
|
|
@@ -7716,14 +8026,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
7716
8026
|
temperature: 0
|
|
7717
8027
|
});
|
|
7718
8028
|
const answer = result.content.trim().toUpperCase();
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
}
|
|
7722
|
-
if (answer.includes("NO")) {
|
|
7723
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
7724
|
-
}
|
|
8029
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
8030
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
7725
8031
|
return null;
|
|
7726
8032
|
} catch {
|
|
8033
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
7727
8034
|
return null;
|
|
7728
8035
|
}
|
|
7729
8036
|
}
|
|
@@ -7843,16 +8150,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
7843
8150
|
if (typeof input !== "string" || input.length === 0) {
|
|
7844
8151
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
7845
8152
|
}
|
|
7846
|
-
const root =
|
|
7847
|
-
const abs =
|
|
7848
|
-
const rel =
|
|
7849
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
8153
|
+
const root = path25__default.default.resolve(workspaceRoot);
|
|
8154
|
+
const abs = path25__default.default.isAbsolute(input) ? path25__default.default.resolve(input) : path25__default.default.resolve(root, input);
|
|
8155
|
+
const rel = path25__default.default.relative(root, abs);
|
|
8156
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25__default.default.isAbsolute(rel)) {
|
|
7850
8157
|
throw new WorkspaceSandboxError(input, root);
|
|
7851
8158
|
}
|
|
7852
8159
|
try {
|
|
7853
|
-
const real =
|
|
7854
|
-
const realRel =
|
|
7855
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
8160
|
+
const real = fs23__default.default.realpathSync(abs);
|
|
8161
|
+
const realRel = path25__default.default.relative(root, real);
|
|
8162
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25__default.default.isAbsolute(realRel))) {
|
|
7856
8163
|
throw new WorkspaceSandboxError(input, root);
|
|
7857
8164
|
}
|
|
7858
8165
|
} catch (e) {
|
|
@@ -7913,7 +8220,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
7913
8220
|
} catch {
|
|
7914
8221
|
}
|
|
7915
8222
|
}
|
|
7916
|
-
await fs9__default.default.mkdir(
|
|
8223
|
+
await fs9__default.default.mkdir(path25__default.default.dirname(absPath), { recursive: true });
|
|
7917
8224
|
await fs9__default.default.writeFile(absPath, content, "utf-8");
|
|
7918
8225
|
return `Written ${content.length} characters to ${filePath}`;
|
|
7919
8226
|
}
|
|
@@ -8411,7 +8718,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
8411
8718
|
};
|
|
8412
8719
|
async function fileToImageAttachment(filePath) {
|
|
8413
8720
|
const data = await fs9__default.default.readFile(filePath);
|
|
8414
|
-
const ext =
|
|
8721
|
+
const ext = path25__default.default.extname(filePath).toLowerCase();
|
|
8415
8722
|
const mimeMap = {
|
|
8416
8723
|
".jpg": "image/jpeg",
|
|
8417
8724
|
".jpeg": "image/jpeg",
|
|
@@ -8445,14 +8752,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
8445
8752
|
const filePath = input["path"];
|
|
8446
8753
|
const content = input["content"];
|
|
8447
8754
|
const title = input["title"];
|
|
8448
|
-
const dir =
|
|
8449
|
-
if (!
|
|
8450
|
-
|
|
8755
|
+
const dir = path25__default.default.dirname(filePath);
|
|
8756
|
+
if (!fs23__default.default.existsSync(dir)) {
|
|
8757
|
+
fs23__default.default.mkdirSync(dir, { recursive: true });
|
|
8451
8758
|
}
|
|
8452
8759
|
return new Promise((resolve, reject) => {
|
|
8453
8760
|
try {
|
|
8454
8761
|
const doc = new PDFDocument__default.default({ margin: 50 });
|
|
8455
|
-
const stream =
|
|
8762
|
+
const stream = fs23__default.default.createWriteStream(filePath);
|
|
8456
8763
|
doc.pipe(stream);
|
|
8457
8764
|
if (title) {
|
|
8458
8765
|
doc.info["Title"] = title;
|
|
@@ -8530,22 +8837,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
8530
8837
|
}
|
|
8531
8838
|
cmdPrefix = NODE_CMD;
|
|
8532
8839
|
}
|
|
8533
|
-
const tmpDir =
|
|
8534
|
-
if (!
|
|
8535
|
-
|
|
8840
|
+
const tmpDir = path25__default.default.join(this.workspaceRoot, ".cascade", "tmp");
|
|
8841
|
+
if (!fs23__default.default.existsSync(tmpDir)) {
|
|
8842
|
+
fs23__default.default.mkdirSync(tmpDir, { recursive: true });
|
|
8536
8843
|
}
|
|
8537
8844
|
const extension = language === "python" ? "py" : "js";
|
|
8538
8845
|
const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
|
|
8539
|
-
const filePath =
|
|
8540
|
-
|
|
8846
|
+
const filePath = path25__default.default.join(tmpDir, fileName);
|
|
8847
|
+
fs23__default.default.writeFileSync(filePath, code, "utf-8");
|
|
8541
8848
|
const execArgs = [filePath, ...args];
|
|
8542
8849
|
return new Promise((resolve) => {
|
|
8543
8850
|
const startMs = Date.now();
|
|
8544
8851
|
child_process.execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
8545
8852
|
const duration = Date.now() - startMs;
|
|
8546
8853
|
try {
|
|
8547
|
-
if (
|
|
8548
|
-
|
|
8854
|
+
if (fs23__default.default.existsSync(filePath)) {
|
|
8855
|
+
fs23__default.default.unlinkSync(filePath);
|
|
8549
8856
|
}
|
|
8550
8857
|
} catch (cleanupErr) {
|
|
8551
8858
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -8824,7 +9131,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8824
9131
|
};
|
|
8825
9132
|
async execute(input, _options) {
|
|
8826
9133
|
const pattern = input["pattern"];
|
|
8827
|
-
const searchPath = input["path"] ?
|
|
9134
|
+
const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8828
9135
|
const matches = await glob.glob(pattern, {
|
|
8829
9136
|
cwd: searchPath,
|
|
8830
9137
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -8837,7 +9144,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8837
9144
|
const withMtime = await Promise.all(
|
|
8838
9145
|
matches.map(async (rel) => {
|
|
8839
9146
|
try {
|
|
8840
|
-
const stat = await fs9__default.default.stat(
|
|
9147
|
+
const stat = await fs9__default.default.stat(path25__default.default.join(searchPath, rel));
|
|
8841
9148
|
return { rel, mtime: stat.mtimeMs };
|
|
8842
9149
|
} catch {
|
|
8843
9150
|
return { rel, mtime: 0 };
|
|
@@ -8886,7 +9193,7 @@ var GrepTool = class extends BaseTool {
|
|
|
8886
9193
|
};
|
|
8887
9194
|
async execute(input, _options) {
|
|
8888
9195
|
const pattern = input["pattern"];
|
|
8889
|
-
const searchPath = input["path"] ?
|
|
9196
|
+
const searchPath = input["path"] ? path25__default.default.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8890
9197
|
const globPattern = input["glob"];
|
|
8891
9198
|
const outputMode = input["output_mode"] ?? "content";
|
|
8892
9199
|
const context = input["context"] ?? 0;
|
|
@@ -8940,12 +9247,12 @@ var GrepTool = class extends BaseTool {
|
|
|
8940
9247
|
nodir: true
|
|
8941
9248
|
});
|
|
8942
9249
|
} catch {
|
|
8943
|
-
files = [
|
|
9250
|
+
files = [path25__default.default.relative(searchPath, searchPath) || "."];
|
|
8944
9251
|
}
|
|
8945
9252
|
const results = [];
|
|
8946
9253
|
let totalCount = 0;
|
|
8947
9254
|
for (const rel of files) {
|
|
8948
|
-
const abs =
|
|
9255
|
+
const abs = path25__default.default.join(searchPath, rel);
|
|
8949
9256
|
let content;
|
|
8950
9257
|
try {
|
|
8951
9258
|
content = await fs9__default.default.readFile(abs, "utf-8");
|
|
@@ -9307,10 +9614,10 @@ var ToolRegistry = class extends EventEmitter__default.default {
|
|
|
9307
9614
|
}
|
|
9308
9615
|
isIgnored(filePath) {
|
|
9309
9616
|
if (!filePath) return false;
|
|
9310
|
-
const abs =
|
|
9311
|
-
const rel =
|
|
9312
|
-
if (!rel || rel.startsWith("..") ||
|
|
9313
|
-
const posixRel = rel.split(
|
|
9617
|
+
const abs = path25__default.default.resolve(this.workspaceRoot, filePath);
|
|
9618
|
+
const rel = path25__default.default.relative(this.workspaceRoot, abs);
|
|
9619
|
+
if (!rel || rel.startsWith("..") || path25__default.default.isAbsolute(rel)) return true;
|
|
9620
|
+
const posixRel = rel.split(path25__default.default.sep).join("/");
|
|
9314
9621
|
return this.ignoreMatcher.ignores(posixRel);
|
|
9315
9622
|
}
|
|
9316
9623
|
};
|
|
@@ -9427,6 +9734,9 @@ var McpClient = class _McpClient {
|
|
|
9427
9734
|
return this.clients.has(serverName);
|
|
9428
9735
|
}
|
|
9429
9736
|
};
|
|
9737
|
+
|
|
9738
|
+
// src/core/cascade.ts
|
|
9739
|
+
init_audit_logger();
|
|
9430
9740
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
9431
9741
|
"file_read",
|
|
9432
9742
|
"file_list",
|
|
@@ -9810,9 +10120,10 @@ var TaskAnalyzer = class {
|
|
|
9810
10120
|
analysisCache.clear();
|
|
9811
10121
|
}
|
|
9812
10122
|
};
|
|
9813
|
-
var DEFAULT_STATS_FILE =
|
|
10123
|
+
var DEFAULT_STATS_FILE = path25__default.default.join(os8__default.default.homedir(), ".cascade", "model-perf.json");
|
|
9814
10124
|
var ModelPerformanceTracker = class {
|
|
9815
10125
|
stats = /* @__PURE__ */ new Map();
|
|
10126
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
9816
10127
|
statsFile;
|
|
9817
10128
|
loaded = false;
|
|
9818
10129
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -9824,18 +10135,29 @@ var ModelPerformanceTracker = class {
|
|
|
9824
10135
|
try {
|
|
9825
10136
|
const raw = await fs9__default.default.readFile(this.statsFile, "utf-8");
|
|
9826
10137
|
const parsed = JSON.parse(raw);
|
|
9827
|
-
|
|
9828
|
-
this.stats.set(key, stat);
|
|
10138
|
+
if (parsed.models) {
|
|
10139
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
10140
|
+
} else {
|
|
10141
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
10142
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
10143
|
+
this.stats.set(key, stat);
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
10146
|
+
}
|
|
10147
|
+
if (parsed.features) {
|
|
10148
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
9829
10149
|
}
|
|
9830
10150
|
} catch {
|
|
9831
10151
|
}
|
|
9832
10152
|
}
|
|
9833
10153
|
async save() {
|
|
9834
10154
|
try {
|
|
9835
|
-
await fs9__default.default.mkdir(
|
|
9836
|
-
const
|
|
9837
|
-
|
|
9838
|
-
|
|
10155
|
+
await fs9__default.default.mkdir(path25__default.default.dirname(this.statsFile), { recursive: true });
|
|
10156
|
+
const modelsObj = {};
|
|
10157
|
+
const featuresObj = {};
|
|
10158
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
10159
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
10160
|
+
await fs9__default.default.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
9839
10161
|
} catch {
|
|
9840
10162
|
}
|
|
9841
10163
|
}
|
|
@@ -9856,6 +10178,13 @@ var ModelPerformanceTracker = class {
|
|
|
9856
10178
|
sampleCount: s.sampleCount + 1
|
|
9857
10179
|
});
|
|
9858
10180
|
}
|
|
10181
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
10182
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
10183
|
+
this.featureStats.set(featureTag, {
|
|
10184
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
10185
|
+
runCount: s.runCount + 1
|
|
10186
|
+
});
|
|
10187
|
+
}
|
|
9859
10188
|
/**
|
|
9860
10189
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
9861
10190
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -9870,6 +10199,9 @@ var ModelPerformanceTracker = class {
|
|
|
9870
10199
|
getAll() {
|
|
9871
10200
|
return new Map(this.stats);
|
|
9872
10201
|
}
|
|
10202
|
+
getAllFeatures() {
|
|
10203
|
+
return new Map(this.featureStats);
|
|
10204
|
+
}
|
|
9873
10205
|
/**
|
|
9874
10206
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
9875
10207
|
* High retry counts penalise the score.
|
|
@@ -10234,7 +10566,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10234
10566
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
10235
10567
|
async loadPersistedTools() {
|
|
10236
10568
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10237
|
-
const file =
|
|
10569
|
+
const file = path25__default.default.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
10238
10570
|
try {
|
|
10239
10571
|
const raw = await fs9__default.default.readFile(file, "utf-8");
|
|
10240
10572
|
const specs = JSON.parse(raw);
|
|
@@ -10258,8 +10590,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10258
10590
|
}
|
|
10259
10591
|
async persist() {
|
|
10260
10592
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10261
|
-
const dir =
|
|
10262
|
-
const file =
|
|
10593
|
+
const dir = path25__default.default.join(this.workspacePath, ".cascade");
|
|
10594
|
+
const file = path25__default.default.join(dir, DYNAMIC_TOOLS_FILE);
|
|
10263
10595
|
try {
|
|
10264
10596
|
await fs9__default.default.mkdir(dir, { recursive: true });
|
|
10265
10597
|
await fs9__default.default.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -10272,6 +10604,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10272
10604
|
return Array.from(this.specs.keys());
|
|
10273
10605
|
}
|
|
10274
10606
|
};
|
|
10607
|
+
var WorldStateDB = class {
|
|
10608
|
+
constructor(workspacePath, debugMode = false) {
|
|
10609
|
+
this.workspacePath = workspacePath;
|
|
10610
|
+
this.debugMode = debugMode;
|
|
10611
|
+
const cascadeDir = path25__default.default.join(workspacePath, ".cascade");
|
|
10612
|
+
if (!fs23__default.default.existsSync(cascadeDir)) {
|
|
10613
|
+
fs23__default.default.mkdirSync(cascadeDir, { recursive: true });
|
|
10614
|
+
}
|
|
10615
|
+
this.keyPath = path25__default.default.join(cascadeDir, "world_state.key");
|
|
10616
|
+
this.dbPath = path25__default.default.join(cascadeDir, "world_state.db");
|
|
10617
|
+
this.initEncryptionKey();
|
|
10618
|
+
this.db = new Database2__default.default(this.dbPath);
|
|
10619
|
+
this.db.pragma("journal_mode = WAL");
|
|
10620
|
+
this.db.exec(`
|
|
10621
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
10622
|
+
id TEXT PRIMARY KEY,
|
|
10623
|
+
timestamp TEXT NOT NULL,
|
|
10624
|
+
worker_id TEXT NOT NULL,
|
|
10625
|
+
encrypted_payload TEXT NOT NULL
|
|
10626
|
+
)
|
|
10627
|
+
`);
|
|
10628
|
+
}
|
|
10629
|
+
workspacePath;
|
|
10630
|
+
debugMode;
|
|
10631
|
+
db;
|
|
10632
|
+
keyPath;
|
|
10633
|
+
dbPath;
|
|
10634
|
+
encryptionKey;
|
|
10635
|
+
initEncryptionKey() {
|
|
10636
|
+
if (fs23__default.default.existsSync(this.keyPath)) {
|
|
10637
|
+
this.encryptionKey = fs23__default.default.readFileSync(this.keyPath);
|
|
10638
|
+
} else {
|
|
10639
|
+
this.encryptionKey = crypto__default.default.randomBytes(32);
|
|
10640
|
+
fs23__default.default.writeFileSync(this.keyPath, this.encryptionKey);
|
|
10641
|
+
}
|
|
10642
|
+
}
|
|
10643
|
+
encrypt(text) {
|
|
10644
|
+
const iv = crypto__default.default.randomBytes(16);
|
|
10645
|
+
const cipher = crypto__default.default.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10646
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
10647
|
+
encrypted += cipher.final("hex");
|
|
10648
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
10649
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
10650
|
+
}
|
|
10651
|
+
decrypt(text) {
|
|
10652
|
+
const parts = text.split(":");
|
|
10653
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
10654
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
10655
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
10656
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
10657
|
+
const decipher = crypto__default.default.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10658
|
+
decipher.setAuthTag(authTag);
|
|
10659
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
10660
|
+
decrypted += decipher.final("utf8");
|
|
10661
|
+
return decrypted;
|
|
10662
|
+
}
|
|
10663
|
+
addEntry(workerId, summary) {
|
|
10664
|
+
const id = crypto__default.default.randomUUID();
|
|
10665
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10666
|
+
const payload = JSON.stringify({ summary });
|
|
10667
|
+
const encryptedPayload = this.encrypt(payload);
|
|
10668
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
10669
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
10670
|
+
this.dumpDebugIfNeeded();
|
|
10671
|
+
return id;
|
|
10672
|
+
}
|
|
10673
|
+
getAllEntries() {
|
|
10674
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
10675
|
+
const rows = stmt.all();
|
|
10676
|
+
return rows.map((row) => {
|
|
10677
|
+
try {
|
|
10678
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
10679
|
+
const parsed = JSON.parse(decrypted);
|
|
10680
|
+
return {
|
|
10681
|
+
id: row.id,
|
|
10682
|
+
timestamp: row.timestamp,
|
|
10683
|
+
workerId: row.worker_id,
|
|
10684
|
+
summary: parsed.summary
|
|
10685
|
+
};
|
|
10686
|
+
} catch (err) {
|
|
10687
|
+
return {
|
|
10688
|
+
id: row.id,
|
|
10689
|
+
timestamp: row.timestamp,
|
|
10690
|
+
workerId: row.worker_id,
|
|
10691
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
10692
|
+
};
|
|
10693
|
+
}
|
|
10694
|
+
});
|
|
10695
|
+
}
|
|
10696
|
+
getFormattedState() {
|
|
10697
|
+
const entries = this.getAllEntries();
|
|
10698
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
10699
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10700
|
+
}
|
|
10701
|
+
dumpDebugIfNeeded() {
|
|
10702
|
+
if (!this.debugMode) return;
|
|
10703
|
+
try {
|
|
10704
|
+
const dumpPath = path25__default.default.join(os8__default.default.tmpdir(), "cascade_world_state_debug.json");
|
|
10705
|
+
const entries = this.getAllEntries();
|
|
10706
|
+
fs23__default.default.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
10707
|
+
} catch (err) {
|
|
10708
|
+
console.error("Failed to dump debug world state", err);
|
|
10709
|
+
}
|
|
10710
|
+
}
|
|
10711
|
+
close() {
|
|
10712
|
+
this.db.close();
|
|
10713
|
+
}
|
|
10714
|
+
};
|
|
10715
|
+
var ignore3 = _ignoreModule__namespace.default ?? _ignoreModule__namespace;
|
|
10716
|
+
var PrivacyPaths = class {
|
|
10717
|
+
localOnly;
|
|
10718
|
+
hasRules;
|
|
10719
|
+
constructor(policies = []) {
|
|
10720
|
+
this.localOnly = ignore3();
|
|
10721
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
10722
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
10723
|
+
this.hasRules = patterns.length > 0;
|
|
10724
|
+
}
|
|
10725
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
10726
|
+
hasPolicies() {
|
|
10727
|
+
return this.hasRules;
|
|
10728
|
+
}
|
|
10729
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
10730
|
+
isLocalOnly(relativePath) {
|
|
10731
|
+
if (!this.hasRules || !relativePath) return false;
|
|
10732
|
+
try {
|
|
10733
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
10734
|
+
} catch {
|
|
10735
|
+
return false;
|
|
10736
|
+
}
|
|
10737
|
+
}
|
|
10738
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
10739
|
+
anyLocalOnly(relativePaths) {
|
|
10740
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
10741
|
+
}
|
|
10742
|
+
};
|
|
10743
|
+
|
|
10744
|
+
// src/core/steering/guidance.ts
|
|
10745
|
+
var GuidanceQueue = class {
|
|
10746
|
+
entries = [];
|
|
10747
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
10748
|
+
cursors = /* @__PURE__ */ new Map();
|
|
10749
|
+
push(text, nodeId) {
|
|
10750
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10751
|
+
this.entries.push(entry);
|
|
10752
|
+
return entry;
|
|
10753
|
+
}
|
|
10754
|
+
/**
|
|
10755
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
10756
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
10757
|
+
*/
|
|
10758
|
+
drain(consumerId) {
|
|
10759
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
10760
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
10761
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
10762
|
+
}
|
|
10763
|
+
get size() {
|
|
10764
|
+
return this.entries.length;
|
|
10765
|
+
}
|
|
10766
|
+
};
|
|
10275
10767
|
|
|
10276
10768
|
// src/core/cascade.ts
|
|
10277
10769
|
var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
@@ -10291,6 +10783,9 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
10291
10783
|
taskAnalyzer;
|
|
10292
10784
|
perfTracker;
|
|
10293
10785
|
toolCreator;
|
|
10786
|
+
worldStateDB;
|
|
10787
|
+
encryptedAuditLogger;
|
|
10788
|
+
guidanceQueue;
|
|
10294
10789
|
workspacePath;
|
|
10295
10790
|
constructor(config, workspacePath, store) {
|
|
10296
10791
|
super();
|
|
@@ -10312,6 +10807,23 @@ var Cascade = class _Cascade extends EventEmitter__default.default {
|
|
|
10312
10807
|
});
|
|
10313
10808
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
10314
10809
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
10810
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10811
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
10812
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10813
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
10814
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
10815
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
10816
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
10817
|
+
}
|
|
10818
|
+
/**
|
|
10819
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
10820
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
10821
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
10822
|
+
*/
|
|
10823
|
+
injectGuidance(text, nodeId) {
|
|
10824
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
10825
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
10826
|
+
this.emit("guidance:injected", entry);
|
|
10315
10827
|
}
|
|
10316
10828
|
initOptionalFeatures() {
|
|
10317
10829
|
if (this.config.cascadeAuto === true) {
|
|
@@ -10474,6 +10986,12 @@ ${last.partialOutput}` : "");
|
|
|
10474
10986
|
if (!prompt) return null;
|
|
10475
10987
|
return this.run({ prompt });
|
|
10476
10988
|
}
|
|
10989
|
+
getWorkspacePath() {
|
|
10990
|
+
return this.workspacePath;
|
|
10991
|
+
}
|
|
10992
|
+
getWorldStateDB() {
|
|
10993
|
+
return this.worldStateDB;
|
|
10994
|
+
}
|
|
10477
10995
|
/**
|
|
10478
10996
|
* Record an explicit user rating for the last completed run.
|
|
10479
10997
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -10560,6 +11078,11 @@ ${last.partialOutput}` : "");
|
|
|
10560
11078
|
}
|
|
10561
11079
|
this.initOptionalFeatures();
|
|
10562
11080
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
11081
|
+
if (this.encryptedAuditLogger) {
|
|
11082
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
11083
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
11084
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
11085
|
+
}
|
|
10563
11086
|
this.initialized = true;
|
|
10564
11087
|
})();
|
|
10565
11088
|
try {
|
|
@@ -10603,6 +11126,21 @@ ${last.partialOutput}` : "");
|
|
|
10603
11126
|
const producesArtifact = /\b(?:create|build|implement|generate|write|refactor|rewrite|add|fix|deploy|install|migrate|scaffold|set up|save (?:a|the)|report|\.(?:pdf|md|txt|json|csv|py|js|ts|tsx|jsx|html|docx?))\b/i.test(p);
|
|
10604
11127
|
return inquiry && !producesArtifact;
|
|
10605
11128
|
}
|
|
11129
|
+
/**
|
|
11130
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
11131
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
11132
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
11133
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11134
|
+
* Simple/Moderate) don't get over-escalated.
|
|
11135
|
+
*/
|
|
11136
|
+
looksClearlyComplex(prompt) {
|
|
11137
|
+
const p = prompt.trim();
|
|
11138
|
+
if (p.length < 24) return false;
|
|
11139
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11140
|
+
const scaleNoun = /\b(?:app(?:lication)?|system|platform|service|api|backend|frontend|full[- ]?stack|website|dashboard|pipeline|microservices?|database schema|authentication|end[- ]to[- ]end|codebase|project|multiple files|several (?:files|modules|components)|test suite)\b/i.test(p);
|
|
11141
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
11142
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
11143
|
+
}
|
|
10606
11144
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
10607
11145
|
static globCache = /* @__PURE__ */ new Map();
|
|
10608
11146
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -10686,10 +11224,16 @@ ${prompt}` : prompt;
|
|
|
10686
11224
|
let verdict;
|
|
10687
11225
|
if (match) {
|
|
10688
11226
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
10689
|
-
|
|
11227
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11228
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11229
|
+
verdict = "Complex";
|
|
11230
|
+
} else {
|
|
11231
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11232
|
+
}
|
|
10690
11233
|
} else {
|
|
10691
|
-
|
|
10692
|
-
|
|
11234
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
11235
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
11236
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
10693
11237
|
}
|
|
10694
11238
|
return verdict;
|
|
10695
11239
|
} catch {
|
|
@@ -10741,7 +11285,15 @@ ${prompt}` : prompt;
|
|
|
10741
11285
|
}
|
|
10742
11286
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
10743
11287
|
});
|
|
10744
|
-
const
|
|
11288
|
+
const forceTier = this.config.routing?.forceTier;
|
|
11289
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
11290
|
+
let complexity;
|
|
11291
|
+
if (forced) {
|
|
11292
|
+
complexity = forced;
|
|
11293
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
11294
|
+
} else {
|
|
11295
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
11296
|
+
}
|
|
10745
11297
|
this.telemetry.capture("cascade:session_start", {
|
|
10746
11298
|
complexity,
|
|
10747
11299
|
providerCount: this.config.providers.length,
|
|
@@ -10821,6 +11373,7 @@ ${prompt}` : prompt;
|
|
|
10821
11373
|
try {
|
|
10822
11374
|
if (complexity === "Simple") {
|
|
10823
11375
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
11376
|
+
t3.setPresenter(true);
|
|
10824
11377
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
10825
11378
|
if (identityPrompt) {
|
|
10826
11379
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -10845,6 +11398,7 @@ ${prompt}` : prompt;
|
|
|
10845
11398
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
10846
11399
|
} else if (complexity === "Moderate") {
|
|
10847
11400
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
11401
|
+
t2.setPresenter(true);
|
|
10848
11402
|
t2.setHierarchyContext("You are the ROOT Manager for this task. There is no T1 Administrator involved in this run. You are responsible for decomposing the task and managing T3 workers directly.");
|
|
10849
11403
|
if (identityPrompt) {
|
|
10850
11404
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -10894,6 +11448,7 @@ ${prompt}` : prompt;
|
|
|
10894
11448
|
}
|
|
10895
11449
|
} else {
|
|
10896
11450
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
11451
|
+
t1.setPresenter(true);
|
|
10897
11452
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
10898
11453
|
if (identityPrompt) {
|
|
10899
11454
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -10985,6 +11540,7 @@ ${prompt}` : prompt;
|
|
|
10985
11540
|
durationMs,
|
|
10986
11541
|
costByTier: stats.costByTier,
|
|
10987
11542
|
tokensByTier: stats.tokensByTier,
|
|
11543
|
+
costByFeature: stats.costByFeature,
|
|
10988
11544
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
10989
11545
|
};
|
|
10990
11546
|
}
|
|
@@ -11234,6 +11790,30 @@ var SlashCommandRegistry = class {
|
|
|
11234
11790
|
return { output: out || "File changes rolled back.", handled: true };
|
|
11235
11791
|
}
|
|
11236
11792
|
});
|
|
11793
|
+
this.register({
|
|
11794
|
+
command: "/steer",
|
|
11795
|
+
description: "Steer a running task: /steer <correction for the active workers>",
|
|
11796
|
+
args: ["<guidance>"],
|
|
11797
|
+
handler: async (args, ctx) => {
|
|
11798
|
+
const output = await ctx.onSteer(args);
|
|
11799
|
+
return { output, handled: true };
|
|
11800
|
+
}
|
|
11801
|
+
});
|
|
11802
|
+
this.register({
|
|
11803
|
+
command: "/audit",
|
|
11804
|
+
description: "Verify the tamper-evident audit log (hash chain integrity)",
|
|
11805
|
+
handler: async (_args, ctx) => {
|
|
11806
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
11807
|
+
const logger = new AuditLogger3(ctx.workspacePath);
|
|
11808
|
+
try {
|
|
11809
|
+
const v = logger.verifyChain();
|
|
11810
|
+
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.`;
|
|
11811
|
+
return { output, handled: true };
|
|
11812
|
+
} finally {
|
|
11813
|
+
logger.close();
|
|
11814
|
+
}
|
|
11815
|
+
}
|
|
11816
|
+
});
|
|
11237
11817
|
this.register({
|
|
11238
11818
|
command: "/branch",
|
|
11239
11819
|
description: "Fork current session into parallel branches",
|
|
@@ -11881,7 +12461,8 @@ function CostTracker({
|
|
|
11881
12461
|
tokensByTier,
|
|
11882
12462
|
compact = false,
|
|
11883
12463
|
savedUsd = 0,
|
|
11884
|
-
savedPct = 0
|
|
12464
|
+
savedPct = 0,
|
|
12465
|
+
costByFeature
|
|
11885
12466
|
}) {
|
|
11886
12467
|
const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
|
|
11887
12468
|
const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
|
|
@@ -11980,6 +12561,16 @@ function CostTracker({
|
|
|
11980
12561
|
] }, tier);
|
|
11981
12562
|
})
|
|
11982
12563
|
] })
|
|
12564
|
+
] }),
|
|
12565
|
+
costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginTop: 1, children: [
|
|
12566
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, children: "By feature/section:" }),
|
|
12567
|
+
Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginLeft: 2, children: [
|
|
12568
|
+
/* @__PURE__ */ jsxRuntime.jsx(ink.Box, { width: 30, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
|
|
12569
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: theme.colors.success, children: [
|
|
12570
|
+
"$",
|
|
12571
|
+
cost.toFixed(6)
|
|
12572
|
+
] })
|
|
12573
|
+
] }, feature))
|
|
11983
12574
|
] })
|
|
11984
12575
|
] });
|
|
11985
12576
|
}
|
|
@@ -12393,7 +12984,7 @@ function replReducer(state, action) {
|
|
|
12393
12984
|
case "SET_TREE":
|
|
12394
12985
|
return { ...state, agentTree: action.tree };
|
|
12395
12986
|
case "UPDATE_COST":
|
|
12396
|
-
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 };
|
|
12987
|
+
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 };
|
|
12397
12988
|
case "SET_APPROVAL":
|
|
12398
12989
|
return { ...state, approvalRequest: action.request };
|
|
12399
12990
|
case "SET_EXECUTING":
|
|
@@ -12407,7 +12998,7 @@ function replReducer(state, action) {
|
|
|
12407
12998
|
case "TOGGLE_COMMS":
|
|
12408
12999
|
return { ...state, showComms: !state.showComms };
|
|
12409
13000
|
case "CLEAR":
|
|
12410
|
-
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
13001
|
+
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
12411
13002
|
case "CLEAR_TREE":
|
|
12412
13003
|
return { ...state, agentTree: null };
|
|
12413
13004
|
case "TOGGLE_COST":
|
|
@@ -12456,7 +13047,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12456
13047
|
const [slashIndex, setSlashIndex] = React2.useState(0);
|
|
12457
13048
|
const [identities, setIdentities] = React2.useState([]);
|
|
12458
13049
|
const [currentIdentityId, setCurrentIdentityId] = React2.useState(config.defaultIdentityId);
|
|
12459
|
-
const [state, dispatch] = React2.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 });
|
|
13050
|
+
const [state, dispatch] = React2.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 });
|
|
12460
13051
|
const [isShowingModels, setIsShowingModels] = React2.useState(false);
|
|
12461
13052
|
const [planApprovalRequest, setPlanApprovalRequest] = React2.useState(null);
|
|
12462
13053
|
const [historyOffset, setHistoryOffset] = React2.useState(0);
|
|
@@ -12525,9 +13116,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12525
13116
|
}
|
|
12526
13117
|
} catch {
|
|
12527
13118
|
}
|
|
12528
|
-
const configPath =
|
|
13119
|
+
const configPath = path25__default.default.join(workspacePath, ".cascade", "config.json");
|
|
12529
13120
|
try {
|
|
12530
|
-
await fs9__default.default.mkdir(
|
|
13121
|
+
await fs9__default.default.mkdir(path25__default.default.dirname(configPath), { recursive: true });
|
|
12531
13122
|
await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
12532
13123
|
} catch (err) {
|
|
12533
13124
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -12579,9 +13170,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12579
13170
|
if (msg.includes("non-text parts")) return;
|
|
12580
13171
|
originalLog(...args);
|
|
12581
13172
|
};
|
|
12582
|
-
const store = new MemoryStore(
|
|
13173
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
12583
13174
|
storeRef.current = store;
|
|
12584
|
-
globalStoreRef.current = new MemoryStore(
|
|
13175
|
+
globalStoreRef.current = new MemoryStore(path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
12585
13176
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
12586
13177
|
setIdentities(identityRows);
|
|
12587
13178
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -12644,7 +13235,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12644
13235
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
12645
13236
|
onExport: async (fmt) => {
|
|
12646
13237
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12647
|
-
const exportPath =
|
|
13238
|
+
const exportPath = path25__default.default.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
|
|
12648
13239
|
if (fmt === "json") {
|
|
12649
13240
|
await fs9__default.default.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
12650
13241
|
} else {
|
|
@@ -12668,6 +13259,14 @@ ${msg.content}`).join("\n\n");
|
|
|
12668
13259
|
}
|
|
12669
13260
|
return `Restored ${snapshots.length} files to their initial session state.`;
|
|
12670
13261
|
},
|
|
13262
|
+
onSteer: (args) => {
|
|
13263
|
+
const text = args.join(" ").trim();
|
|
13264
|
+
if (!text) return "Usage: /steer <correction for the active workers>";
|
|
13265
|
+
const cascade = cascadeRef.current;
|
|
13266
|
+
if (!cascade) return "No active Cascade instance.";
|
|
13267
|
+
cascade.injectGuidance(text);
|
|
13268
|
+
return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
|
|
13269
|
+
},
|
|
12671
13270
|
onBranch: async () => {
|
|
12672
13271
|
const store = storeRef.current;
|
|
12673
13272
|
if (!store) return;
|
|
@@ -12986,7 +13585,7 @@ ${lastUser.content}`;
|
|
|
12986
13585
|
if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
|
|
12987
13586
|
const stats = cascade.getRouter().getStats();
|
|
12988
13587
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
12989
|
-
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 });
|
|
12990
13589
|
statusThrottleTimeout = null;
|
|
12991
13590
|
};
|
|
12992
13591
|
cascade.on("tier:root", onRoot);
|
|
@@ -13078,7 +13677,7 @@ ${lastUser.content}`;
|
|
|
13078
13677
|
flushPeerEvents();
|
|
13079
13678
|
const stats = cascade.getRouter().getStats();
|
|
13080
13679
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
13081
|
-
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 });
|
|
13680
|
+
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 });
|
|
13082
13681
|
dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13083
13682
|
persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
|
|
13084
13683
|
const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
|
|
@@ -13363,7 +13962,7 @@ ${lastUser.content}`;
|
|
|
13363
13962
|
),
|
|
13364
13963
|
adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsxRuntime.jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
|
|
13365
13964
|
adaptiveMode === "wide" && state.showDetails && liveBudget.showTimeline && /* @__PURE__ */ jsxRuntime.jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
|
|
13366
|
-
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsxRuntime.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 }),
|
|
13965
|
+
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsxRuntime.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 }),
|
|
13367
13966
|
liveBudget.collapsed && (state.showCost || state.showDetails || state.agentTree != null) && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: theme.colors.muted, dimColor: true, children: " \u25B8 panels collapsed (small terminal)" }),
|
|
13368
13967
|
state.approvalRequest && /* @__PURE__ */ jsxRuntime.jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
|
|
13369
13968
|
dispatch({ type: "SET_APPROVAL", request: null });
|
|
@@ -13532,7 +14131,7 @@ function stringifySlashOutput(val) {
|
|
|
13532
14131
|
}
|
|
13533
14132
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
13534
14133
|
if (!query) return "Usage: /search <query>";
|
|
13535
|
-
const dbPath =
|
|
14134
|
+
const dbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
13536
14135
|
try {
|
|
13537
14136
|
await fs9__default.default.access(dbPath);
|
|
13538
14137
|
} catch {
|
|
@@ -13566,7 +14165,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
13566
14165
|
async function diagnoseRuntime(config, workspacePath) {
|
|
13567
14166
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
13568
14167
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
13569
|
-
const store = new MemoryStore(
|
|
14168
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
13570
14169
|
try {
|
|
13571
14170
|
const sessions = store.listSessions(void 0, 3);
|
|
13572
14171
|
return [
|
|
@@ -13585,7 +14184,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
13585
14184
|
}
|
|
13586
14185
|
async function showRecentLogs(args, workspacePath) {
|
|
13587
14186
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
13588
|
-
const store = new MemoryStore(
|
|
14187
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
13589
14188
|
try {
|
|
13590
14189
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
13591
14190
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -13597,7 +14196,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
13597
14196
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
13598
14197
|
const sessionId = args[0];
|
|
13599
14198
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
13600
|
-
const store = new MemoryStore(
|
|
14199
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
13601
14200
|
try {
|
|
13602
14201
|
const session = store.getSession(sessionId);
|
|
13603
14202
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -14017,9 +14616,9 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
14017
14616
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
14018
14617
|
};
|
|
14019
14618
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
14020
|
-
const configDir =
|
|
14619
|
+
const configDir = path25__default.default.join(workspacePath, ".cascade");
|
|
14021
14620
|
await fs9__default.default.mkdir(configDir, { recursive: true });
|
|
14022
|
-
const configPath =
|
|
14621
|
+
const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
14023
14622
|
await fs9__default.default.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
14024
14623
|
savedConfigRef.current = config;
|
|
14025
14624
|
dispatchRef.current({ type: "GO_DONE" });
|
|
@@ -14341,14 +14940,14 @@ function printTelemetryBanner() {
|
|
|
14341
14940
|
async function initCommand(workspacePath = process.cwd()) {
|
|
14342
14941
|
const spin = ora__default.default({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
14343
14942
|
try {
|
|
14344
|
-
const configDir =
|
|
14943
|
+
const configDir = path25__default.default.join(workspacePath, ".cascade");
|
|
14345
14944
|
await fs9__default.default.mkdir(configDir, { recursive: true });
|
|
14346
|
-
const mdPath =
|
|
14945
|
+
const mdPath = path25__default.default.join(workspacePath, "CASCADE.md");
|
|
14347
14946
|
if (!await fileExists(mdPath)) {
|
|
14348
14947
|
await createDefaultCascadeMd(workspacePath);
|
|
14349
14948
|
spin.succeed(chalk9__default.default.green("Created CASCADE.md"));
|
|
14350
14949
|
}
|
|
14351
|
-
const ignorePath =
|
|
14950
|
+
const ignorePath = path25__default.default.join(workspacePath, ".cascadeignore");
|
|
14352
14951
|
if (!await fileExists(ignorePath)) {
|
|
14353
14952
|
await createDefaultIgnoreFile(workspacePath);
|
|
14354
14953
|
spin.succeed(chalk9__default.default.green("Created .cascadeignore"));
|
|
@@ -14357,7 +14956,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
14357
14956
|
console.log();
|
|
14358
14957
|
console.log(chalk9__default.default.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
14359
14958
|
console.log();
|
|
14360
|
-
const configPath =
|
|
14959
|
+
const configPath = path25__default.default.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
14361
14960
|
if (await fileExists(configPath)) {
|
|
14362
14961
|
console.log(chalk9__default.default.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
14363
14962
|
console.log();
|
|
@@ -14415,7 +15014,7 @@ function fromEnv(env) {
|
|
|
14415
15014
|
return out;
|
|
14416
15015
|
}
|
|
14417
15016
|
async function fromClaudeCode(home) {
|
|
14418
|
-
const file =
|
|
15017
|
+
const file = path25__default.default.join(home, ".claude", ".credentials.json");
|
|
14419
15018
|
const data = await readJson(file);
|
|
14420
15019
|
if (!data) return [];
|
|
14421
15020
|
const oauth = data["claudeAiOauth"];
|
|
@@ -14438,7 +15037,7 @@ async function fromClaudeCode(home) {
|
|
|
14438
15037
|
return [];
|
|
14439
15038
|
}
|
|
14440
15039
|
async function fromCodex(home) {
|
|
14441
|
-
const file =
|
|
15040
|
+
const file = path25__default.default.join(home, ".codex", "auth.json");
|
|
14442
15041
|
const data = await readJson(file);
|
|
14443
15042
|
if (!data) return [];
|
|
14444
15043
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -14461,7 +15060,7 @@ async function fromCodex(home) {
|
|
|
14461
15060
|
return [];
|
|
14462
15061
|
}
|
|
14463
15062
|
async function fromGemini(home) {
|
|
14464
|
-
const file =
|
|
15063
|
+
const file = path25__default.default.join(home, ".gemini", "oauth_creds.json");
|
|
14465
15064
|
const data = await readJson(file);
|
|
14466
15065
|
const accessToken = str(data?.["access_token"]);
|
|
14467
15066
|
if (!accessToken) return [];
|
|
@@ -14477,7 +15076,7 @@ async function fromGemini(home) {
|
|
|
14477
15076
|
}
|
|
14478
15077
|
async function fromCopilot(home) {
|
|
14479
15078
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
14480
|
-
const file =
|
|
15079
|
+
const file = path25__default.default.join(home, ".config", "github-copilot", name);
|
|
14481
15080
|
const data = await readJson(file);
|
|
14482
15081
|
if (!data) continue;
|
|
14483
15082
|
for (const value of Object.values(data)) {
|
|
@@ -14498,7 +15097,7 @@ async function fromCopilot(home) {
|
|
|
14498
15097
|
return [];
|
|
14499
15098
|
}
|
|
14500
15099
|
async function discoverCredentials(opts = {}) {
|
|
14501
|
-
const home = opts.homeDir ??
|
|
15100
|
+
const home = opts.homeDir ?? os8__default.default.homedir();
|
|
14502
15101
|
const env = opts.env ?? process.env;
|
|
14503
15102
|
const groups = await Promise.all([
|
|
14504
15103
|
Promise.resolve(fromEnv(env)),
|
|
@@ -14531,7 +15130,7 @@ async function doctorCommand() {
|
|
|
14531
15130
|
checks.push({
|
|
14532
15131
|
label: "Cascade config",
|
|
14533
15132
|
ok: true,
|
|
14534
|
-
detail: `Loaded ${
|
|
15133
|
+
detail: `Loaded ${path25__default.default.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
14535
15134
|
});
|
|
14536
15135
|
const providers = [
|
|
14537
15136
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -14810,6 +15409,24 @@ var DashboardSocket = class {
|
|
|
14810
15409
|
});
|
|
14811
15410
|
});
|
|
14812
15411
|
}
|
|
15412
|
+
onSessionHalt(callback) {
|
|
15413
|
+
this.io.on("connection", (socket) => {
|
|
15414
|
+
socket.on("session:halt", (payload) => {
|
|
15415
|
+
if (typeof payload?.sessionId === "string") {
|
|
15416
|
+
callback(payload.sessionId);
|
|
15417
|
+
}
|
|
15418
|
+
});
|
|
15419
|
+
});
|
|
15420
|
+
}
|
|
15421
|
+
onSessionSteer(callback) {
|
|
15422
|
+
this.io.on("connection", (socket) => {
|
|
15423
|
+
socket.on("session:steer", (payload) => {
|
|
15424
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
15425
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
15426
|
+
}
|
|
15427
|
+
});
|
|
15428
|
+
});
|
|
15429
|
+
}
|
|
14813
15430
|
onConfigUpdate(callback) {
|
|
14814
15431
|
this.io.on("connection", (socket) => {
|
|
14815
15432
|
socket.on("config:update", (payload) => {
|
|
@@ -14832,7 +15449,8 @@ var DashboardSocket = class {
|
|
|
14832
15449
|
socket.on("cascade:run", (payload) => {
|
|
14833
15450
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
14834
15451
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
14835
|
-
|
|
15452
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
15453
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
14836
15454
|
}
|
|
14837
15455
|
});
|
|
14838
15456
|
});
|
|
@@ -14844,7 +15462,7 @@ var DashboardSocket = class {
|
|
|
14844
15462
|
|
|
14845
15463
|
// src/dashboard/server.ts
|
|
14846
15464
|
init_constants();
|
|
14847
|
-
var __dirname$1 =
|
|
15465
|
+
var __dirname$1 = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
14848
15466
|
var DashboardServer = class {
|
|
14849
15467
|
app;
|
|
14850
15468
|
httpServer;
|
|
@@ -14855,6 +15473,20 @@ var DashboardServer = class {
|
|
|
14855
15473
|
globalStore = null;
|
|
14856
15474
|
broadcastTimer = null;
|
|
14857
15475
|
activeSessions = /* @__PURE__ */ new Map();
|
|
15476
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
15477
|
+
/**
|
|
15478
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
15479
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
15480
|
+
* of runs the session performed in this server's lifetime.
|
|
15481
|
+
*/
|
|
15482
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
15483
|
+
/**
|
|
15484
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
15485
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
15486
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
15487
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
15488
|
+
*/
|
|
15489
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
14858
15490
|
port;
|
|
14859
15491
|
host;
|
|
14860
15492
|
workspacePath;
|
|
@@ -14913,15 +15545,18 @@ var DashboardServer = class {
|
|
|
14913
15545
|
}
|
|
14914
15546
|
this.persistConfig();
|
|
14915
15547
|
});
|
|
14916
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
15548
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
14917
15549
|
const sessionId = requestedSessionId ?? crypto.randomUUID();
|
|
15550
|
+
const abortController = new AbortController();
|
|
15551
|
+
this.activeControllers.set(sessionId, abortController);
|
|
14918
15552
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
14919
15553
|
const title = this.persistRunStart(sessionId, prompt);
|
|
14920
|
-
|
|
15554
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
15555
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
14921
15556
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
14922
15557
|
this.activeSessions.set(sessionId, cascade);
|
|
14923
15558
|
cascade.on("stream:token", (e) => {
|
|
14924
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
15559
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
14925
15560
|
});
|
|
14926
15561
|
cascade.on("tier:status", (e) => {
|
|
14927
15562
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -14933,7 +15568,12 @@ var DashboardServer = class {
|
|
|
14933
15568
|
this.socket.emitPeerMessage(e);
|
|
14934
15569
|
});
|
|
14935
15570
|
try {
|
|
14936
|
-
const result = await cascade.run({
|
|
15571
|
+
const result = await cascade.run({
|
|
15572
|
+
prompt: runPrompt,
|
|
15573
|
+
signal: abortController.signal,
|
|
15574
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
15575
|
+
});
|
|
15576
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
14937
15577
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
14938
15578
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
14939
15579
|
this.socket.broadcast("cost:update", {
|
|
@@ -14950,6 +15590,24 @@ var DashboardServer = class {
|
|
|
14950
15590
|
});
|
|
14951
15591
|
} finally {
|
|
14952
15592
|
this.activeSessions.delete(sessionId);
|
|
15593
|
+
this.activeControllers.delete(sessionId);
|
|
15594
|
+
this.denyPendingApprovals(sessionId);
|
|
15595
|
+
}
|
|
15596
|
+
});
|
|
15597
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
15598
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
15599
|
+
this.denyPendingApprovals(sessionId);
|
|
15600
|
+
});
|
|
15601
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
15602
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
15603
|
+
if (!pending) return;
|
|
15604
|
+
this.pendingApprovals.delete(requestId);
|
|
15605
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
15606
|
+
});
|
|
15607
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15608
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15609
|
+
if (steered > 0) {
|
|
15610
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
14953
15611
|
}
|
|
14954
15612
|
});
|
|
14955
15613
|
}
|
|
@@ -14997,9 +15655,9 @@ var DashboardServer = class {
|
|
|
14997
15655
|
*/
|
|
14998
15656
|
persistConfig() {
|
|
14999
15657
|
try {
|
|
15000
|
-
const configPath =
|
|
15001
|
-
|
|
15002
|
-
|
|
15658
|
+
const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
15659
|
+
fs23__default.default.mkdirSync(path25__default.default.dirname(configPath), { recursive: true });
|
|
15660
|
+
fs23__default.default.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
15003
15661
|
} catch (err) {
|
|
15004
15662
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
15005
15663
|
}
|
|
@@ -15014,15 +15672,15 @@ var DashboardServer = class {
|
|
|
15014
15672
|
resolveDashboardSecret() {
|
|
15015
15673
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
15016
15674
|
if (fromConfig) return fromConfig;
|
|
15017
|
-
const secretPath =
|
|
15675
|
+
const secretPath = path25__default.default.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
15018
15676
|
try {
|
|
15019
|
-
if (
|
|
15020
|
-
const existing =
|
|
15677
|
+
if (fs23__default.default.existsSync(secretPath)) {
|
|
15678
|
+
const existing = fs23__default.default.readFileSync(secretPath, "utf-8").trim();
|
|
15021
15679
|
if (existing.length >= 16) return existing;
|
|
15022
15680
|
}
|
|
15023
15681
|
const generated = crypto.randomUUID();
|
|
15024
|
-
|
|
15025
|
-
|
|
15682
|
+
fs23__default.default.mkdirSync(path25__default.default.dirname(secretPath), { recursive: true });
|
|
15683
|
+
fs23__default.default.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
15026
15684
|
if (this.config.dashboard.auth) {
|
|
15027
15685
|
console.warn(
|
|
15028
15686
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -15049,7 +15707,7 @@ var DashboardServer = class {
|
|
|
15049
15707
|
// ── Setup ─────────────────────────────────────
|
|
15050
15708
|
getGlobalStore() {
|
|
15051
15709
|
if (!this.globalStore) {
|
|
15052
|
-
const globalDbPath =
|
|
15710
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15053
15711
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
15054
15712
|
}
|
|
15055
15713
|
return this.globalStore;
|
|
@@ -15097,6 +15755,44 @@ var DashboardServer = class {
|
|
|
15097
15755
|
}
|
|
15098
15756
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
15099
15757
|
}
|
|
15758
|
+
/**
|
|
15759
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
15760
|
+
* session, or every active session when none is specified (the desktop
|
|
15761
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
15762
|
+
*/
|
|
15763
|
+
steerSessions(message, sessionId, nodeId) {
|
|
15764
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
15765
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
15766
|
+
return targets.length;
|
|
15767
|
+
}
|
|
15768
|
+
recordSessionTask(sessionId, taskId) {
|
|
15769
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
15770
|
+
list.push(taskId);
|
|
15771
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
15772
|
+
}
|
|
15773
|
+
/**
|
|
15774
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
15775
|
+
* user. The request itself was already pushed to the client via the
|
|
15776
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
15777
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
15778
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
15779
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
15780
|
+
* timeout denies it.
|
|
15781
|
+
*/
|
|
15782
|
+
makeApprovalCallback(sessionId) {
|
|
15783
|
+
return (request) => new Promise((resolve) => {
|
|
15784
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
15785
|
+
});
|
|
15786
|
+
}
|
|
15787
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
15788
|
+
denyPendingApprovals(sessionId) {
|
|
15789
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
15790
|
+
if (pending.sessionId === sessionId) {
|
|
15791
|
+
this.pendingApprovals.delete(id);
|
|
15792
|
+
pending.resolve({ approved: false, always: false });
|
|
15793
|
+
}
|
|
15794
|
+
}
|
|
15795
|
+
}
|
|
15100
15796
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15101
15797
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15102
15798
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15190,12 +15886,12 @@ ${prompt}`;
|
|
|
15190
15886
|
}
|
|
15191
15887
|
}
|
|
15192
15888
|
watchRuntimeChanges() {
|
|
15193
|
-
const workspaceDbPath =
|
|
15194
|
-
const globalDbPath =
|
|
15889
|
+
const workspaceDbPath = path25__default.default.join(this.workspacePath, CASCADE_DB_FILE);
|
|
15890
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15195
15891
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
15196
15892
|
for (const watchPath of watchPaths) {
|
|
15197
|
-
if (!
|
|
15198
|
-
|
|
15893
|
+
if (!fs23__default.default.existsSync(watchPath)) continue;
|
|
15894
|
+
fs23__default.default.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
15199
15895
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
15200
15896
|
});
|
|
15201
15897
|
}
|
|
@@ -15285,15 +15981,6 @@ ${prompt}`;
|
|
|
15285
15981
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
15286
15982
|
res.json({ success: true, ...payload });
|
|
15287
15983
|
});
|
|
15288
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
15289
|
-
const body = req.body;
|
|
15290
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
15291
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
15292
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15293
|
-
this.socket.broadcast("session:approve", payload);
|
|
15294
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
15295
|
-
res.json({ success: true, ...payload });
|
|
15296
|
-
});
|
|
15297
15984
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
15298
15985
|
const body = req.body;
|
|
15299
15986
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -15303,7 +15990,8 @@ ${prompt}`;
|
|
|
15303
15990
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
15304
15991
|
return;
|
|
15305
15992
|
}
|
|
15306
|
-
const
|
|
15993
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15994
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15307
15995
|
this.socket.broadcast("session:message-injected", payload);
|
|
15308
15996
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
15309
15997
|
res.json({ success: true, ...payload });
|
|
@@ -15325,7 +16013,7 @@ ${prompt}`;
|
|
|
15325
16013
|
const sessionId = req.params.id;
|
|
15326
16014
|
this.store.deleteSession(sessionId);
|
|
15327
16015
|
this.store.deleteRuntimeSession(sessionId);
|
|
15328
|
-
const globalDbPath =
|
|
16016
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15329
16017
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15330
16018
|
try {
|
|
15331
16019
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -15337,9 +16025,34 @@ ${prompt}`;
|
|
|
15337
16025
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15338
16026
|
res.json({ ok: true });
|
|
15339
16027
|
});
|
|
16028
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
16029
|
+
const sessionId = req.params.id;
|
|
16030
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
16031
|
+
if (!taskIds.length) {
|
|
16032
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
16033
|
+
return;
|
|
16034
|
+
}
|
|
16035
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
16036
|
+
for (const taskId of taskIds) {
|
|
16037
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
16038
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
16039
|
+
}
|
|
16040
|
+
}
|
|
16041
|
+
const { writeFile } = await import('fs/promises');
|
|
16042
|
+
let restored = 0;
|
|
16043
|
+
for (const [filePath, content] of toRestore) {
|
|
16044
|
+
try {
|
|
16045
|
+
await writeFile(filePath, content, "utf-8");
|
|
16046
|
+
restored++;
|
|
16047
|
+
} catch (err) {
|
|
16048
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
16049
|
+
}
|
|
16050
|
+
}
|
|
16051
|
+
res.json({ ok: true, restored });
|
|
16052
|
+
});
|
|
15340
16053
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
15341
16054
|
const body = req.body;
|
|
15342
|
-
const globalDbPath =
|
|
16055
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15343
16056
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
15344
16057
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15345
16058
|
try {
|
|
@@ -15362,7 +16075,7 @@ ${prompt}`;
|
|
|
15362
16075
|
});
|
|
15363
16076
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
15364
16077
|
this.store.deleteAllRuntimeNodes();
|
|
15365
|
-
const globalDbPath =
|
|
16078
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15366
16079
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15367
16080
|
try {
|
|
15368
16081
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -15413,6 +16126,19 @@ ${prompt}`;
|
|
|
15413
16126
|
this.store.deleteIdentity(req.params.id);
|
|
15414
16127
|
res.json({ ok: true });
|
|
15415
16128
|
});
|
|
16129
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
16130
|
+
try {
|
|
16131
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
16132
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
16133
|
+
try {
|
|
16134
|
+
res.json(logger.verifyChain());
|
|
16135
|
+
} finally {
|
|
16136
|
+
logger.close();
|
|
16137
|
+
}
|
|
16138
|
+
} catch (err) {
|
|
16139
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16140
|
+
}
|
|
16141
|
+
});
|
|
15416
16142
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
15417
16143
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
15418
16144
|
res.json(log);
|
|
@@ -15435,12 +16161,12 @@ ${prompt}`;
|
|
|
15435
16161
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
15436
16162
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
15437
16163
|
try {
|
|
15438
|
-
const configPath =
|
|
15439
|
-
const existing =
|
|
16164
|
+
const configPath = path25__default.default.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
16165
|
+
const existing = fs23__default.default.existsSync(configPath) ? JSON.parse(fs23__default.default.readFileSync(configPath, "utf-8")) : {};
|
|
15440
16166
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
15441
16167
|
const tmp = configPath + ".tmp";
|
|
15442
|
-
|
|
15443
|
-
|
|
16168
|
+
fs23__default.default.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
16169
|
+
fs23__default.default.renameSync(tmp, configPath);
|
|
15444
16170
|
res.json({ ok: true });
|
|
15445
16171
|
} catch (err) {
|
|
15446
16172
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -15468,7 +16194,7 @@ ${prompt}`;
|
|
|
15468
16194
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
15469
16195
|
const scope = req.query["scope"] ?? "workspace";
|
|
15470
16196
|
if (scope === "global") {
|
|
15471
|
-
const globalDbPath =
|
|
16197
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15472
16198
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15473
16199
|
try {
|
|
15474
16200
|
res.json({
|
|
@@ -15505,7 +16231,7 @@ ${prompt}`;
|
|
|
15505
16231
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
15506
16232
|
this.activeSessions.set(sessionId, cascade);
|
|
15507
16233
|
cascade.on("stream:token", (e) => {
|
|
15508
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16234
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
15509
16235
|
});
|
|
15510
16236
|
cascade.on("tier:status", (e) => {
|
|
15511
16237
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -15517,7 +16243,12 @@ ${prompt}`;
|
|
|
15517
16243
|
this.socket.emitPeerMessage(e);
|
|
15518
16244
|
});
|
|
15519
16245
|
try {
|
|
15520
|
-
const result = await cascade.run({
|
|
16246
|
+
const result = await cascade.run({
|
|
16247
|
+
prompt: runPrompt,
|
|
16248
|
+
identityId: body.identityId,
|
|
16249
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16250
|
+
});
|
|
16251
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
15521
16252
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15522
16253
|
this.socket.broadcast("cost:update", {
|
|
15523
16254
|
sessionId,
|
|
@@ -15534,6 +16265,7 @@ ${prompt}`;
|
|
|
15534
16265
|
});
|
|
15535
16266
|
} finally {
|
|
15536
16267
|
this.activeSessions.delete(sessionId);
|
|
16268
|
+
this.denyPendingApprovals(sessionId);
|
|
15537
16269
|
}
|
|
15538
16270
|
})();
|
|
15539
16271
|
});
|
|
@@ -15548,13 +16280,13 @@ ${prompt}`;
|
|
|
15548
16280
|
}))
|
|
15549
16281
|
});
|
|
15550
16282
|
});
|
|
15551
|
-
const prodPath =
|
|
15552
|
-
const devPath =
|
|
15553
|
-
const webDistPath =
|
|
15554
|
-
if (
|
|
16283
|
+
const prodPath = path25__default.default.resolve(__dirname$1, "../web/dist");
|
|
16284
|
+
const devPath = path25__default.default.resolve(__dirname$1, "../../web/dist");
|
|
16285
|
+
const webDistPath = fs23__default.default.existsSync(prodPath) ? prodPath : devPath;
|
|
16286
|
+
if (fs23__default.default.existsSync(webDistPath)) {
|
|
15555
16287
|
this.app.use(express__default.default.static(webDistPath));
|
|
15556
16288
|
this.app.get("*", (_req, res) => {
|
|
15557
|
-
res.sendFile(
|
|
16289
|
+
res.sendFile(path25__default.default.join(webDistPath, "index.html"));
|
|
15558
16290
|
});
|
|
15559
16291
|
} else {
|
|
15560
16292
|
this.app.get("/", (_req, res) => {
|
|
@@ -15575,7 +16307,7 @@ init_constants();
|
|
|
15575
16307
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
15576
16308
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
15577
16309
|
const spin = ora__default.default({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
15578
|
-
const store = new MemoryStore(
|
|
16310
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
15579
16311
|
const server = new DashboardServer(config, store, workspacePath);
|
|
15580
16312
|
server.watchRuntimeChanges();
|
|
15581
16313
|
const gThis = globalThis;
|
|
@@ -15607,7 +16339,7 @@ init_constants();
|
|
|
15607
16339
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
15608
16340
|
const identity = new commander.Command("identity").alias("id").description("Manage Cascade identities");
|
|
15609
16341
|
identity.command("list").description("List all available identities").action(() => {
|
|
15610
|
-
const store = new MemoryStore(
|
|
16342
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
15611
16343
|
try {
|
|
15612
16344
|
const identities = store.listIdentities();
|
|
15613
16345
|
console.log(chalk9__default.default.bold("\n Identities:"));
|
|
@@ -15627,7 +16359,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15627
16359
|
}
|
|
15628
16360
|
});
|
|
15629
16361
|
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) => {
|
|
15630
|
-
const store = new MemoryStore(
|
|
16362
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
15631
16363
|
try {
|
|
15632
16364
|
if (options.default) {
|
|
15633
16365
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -15653,7 +16385,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15653
16385
|
}
|
|
15654
16386
|
});
|
|
15655
16387
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
15656
|
-
const store = new MemoryStore(
|
|
16388
|
+
const store = new MemoryStore(path25__default.default.join(workspacePath, CASCADE_DB_FILE));
|
|
15657
16389
|
try {
|
|
15658
16390
|
const identities = store.listIdentities();
|
|
15659
16391
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -15781,8 +16513,8 @@ async function exportCommand(options = {}) {
|
|
|
15781
16513
|
let store;
|
|
15782
16514
|
try {
|
|
15783
16515
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
15784
|
-
const workspaceDbPath =
|
|
15785
|
-
const globalDbPath =
|
|
16516
|
+
const workspaceDbPath = path25__default.default.join(workspacePath, CASCADE_DB_FILE);
|
|
16517
|
+
const globalDbPath = path25__default.default.join(os8__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
15786
16518
|
let dbPath = globalDbPath;
|
|
15787
16519
|
try {
|
|
15788
16520
|
await fs9__default.default.access(workspaceDbPath);
|
|
@@ -15824,7 +16556,7 @@ async function exportCommand(options = {}) {
|
|
|
15824
16556
|
const ext = format === "json" ? ".json" : ".md";
|
|
15825
16557
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
15826
16558
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
15827
|
-
const outPath = options.output ?
|
|
16559
|
+
const outPath = options.output ? path25__default.default.resolve(options.output) : path25__default.default.join(process.cwd(), defaultFile);
|
|
15828
16560
|
await fs9__default.default.writeFile(outPath, content, "utf-8");
|
|
15829
16561
|
spin.succeed(chalk9__default.default.green(`Exported to ${chalk9__default.default.white(outPath)}`));
|
|
15830
16562
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
@@ -16052,11 +16784,11 @@ async function statsCommand() {
|
|
|
16052
16784
|
dotenv__default.default.config();
|
|
16053
16785
|
function warnIfBuildIsStale() {
|
|
16054
16786
|
try {
|
|
16055
|
-
const here =
|
|
16787
|
+
const here = path25__default.default.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href))));
|
|
16056
16788
|
for (const rel of ["..", "../.."]) {
|
|
16057
|
-
const pkgPath =
|
|
16058
|
-
if (!
|
|
16059
|
-
const pkg = JSON.parse(
|
|
16789
|
+
const pkgPath = path25__default.default.join(here, rel, "package.json");
|
|
16790
|
+
if (!fs23__default.default.existsSync(pkgPath)) continue;
|
|
16791
|
+
const pkg = JSON.parse(fs23__default.default.readFileSync(pkgPath, "utf-8"));
|
|
16060
16792
|
if (pkg.name !== "cascade-ai") continue;
|
|
16061
16793
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
16062
16794
|
console.error(
|