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.js
CHANGED
|
@@ -6,20 +6,20 @@ import http, { createServer } from 'http';
|
|
|
6
6
|
import https from 'https';
|
|
7
7
|
import zlib from 'zlib';
|
|
8
8
|
import { Readable } from 'stream';
|
|
9
|
+
import Database2 from 'better-sqlite3';
|
|
10
|
+
import path25 from 'path';
|
|
11
|
+
import fs23 from 'fs';
|
|
12
|
+
import os8 from 'os';
|
|
13
|
+
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
9
14
|
import { render, Box, Text, useStdout, useApp, useInput, Static } from 'ink';
|
|
10
15
|
import { Command } from 'commander';
|
|
11
16
|
import React2, { useState, useReducer, useRef, useCallback, useEffect, useMemo } from 'react';
|
|
12
17
|
import chalk9 from 'chalk';
|
|
13
18
|
import dotenv from 'dotenv';
|
|
14
|
-
import fs21 from 'fs';
|
|
15
|
-
import path23 from 'path';
|
|
16
19
|
import { fileURLToPath } from 'url';
|
|
17
20
|
import fs9 from 'fs/promises';
|
|
18
|
-
import os6 from 'os';
|
|
19
|
-
import crypto, { randomUUID, timingSafeEqual } from 'crypto';
|
|
20
21
|
import * as _ignoreModule from 'ignore';
|
|
21
22
|
import _ignoreModule__default from 'ignore';
|
|
22
|
-
import Database from 'better-sqlite3';
|
|
23
23
|
import { z } from 'zod';
|
|
24
24
|
import { exec, execFile, execSync, spawnSync } from 'child_process';
|
|
25
25
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
@@ -58,7 +58,7 @@ var __export = (target, all) => {
|
|
|
58
58
|
var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
|
|
59
59
|
var init_constants = __esm({
|
|
60
60
|
"src/constants.ts"() {
|
|
61
|
-
CASCADE_VERSION = "0.
|
|
61
|
+
CASCADE_VERSION = "0.13.2";
|
|
62
62
|
CASCADE_CONFIG_FILE = ".cascade/config.json";
|
|
63
63
|
CASCADE_DB_FILE = ".cascade/memory.db";
|
|
64
64
|
CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
|
|
@@ -1525,6 +1525,149 @@ var init_openai_compatible = __esm({
|
|
|
1525
1525
|
}
|
|
1526
1526
|
});
|
|
1527
1527
|
|
|
1528
|
+
// src/core/audit/audit-logger.ts
|
|
1529
|
+
var audit_logger_exports = {};
|
|
1530
|
+
__export(audit_logger_exports, {
|
|
1531
|
+
AuditLogger: () => AuditLogger2
|
|
1532
|
+
});
|
|
1533
|
+
var AuditLogger2;
|
|
1534
|
+
var init_audit_logger = __esm({
|
|
1535
|
+
"src/core/audit/audit-logger.ts"() {
|
|
1536
|
+
AuditLogger2 = class {
|
|
1537
|
+
constructor(workspacePath, debugMode = false) {
|
|
1538
|
+
this.workspacePath = workspacePath;
|
|
1539
|
+
this.debugMode = debugMode;
|
|
1540
|
+
const cascadeDir = path25.join(workspacePath, ".cascade");
|
|
1541
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
1542
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
1543
|
+
}
|
|
1544
|
+
this.keyPath = path25.join(cascadeDir, "audit_log.key");
|
|
1545
|
+
this.dbPath = path25.join(cascadeDir, "audit_log.db");
|
|
1546
|
+
this.initEncryptionKey();
|
|
1547
|
+
this.db = new Database2(this.dbPath);
|
|
1548
|
+
this.db.pragma("journal_mode = WAL");
|
|
1549
|
+
this.db.exec(`
|
|
1550
|
+
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
1551
|
+
id TEXT PRIMARY KEY,
|
|
1552
|
+
timestamp TEXT NOT NULL,
|
|
1553
|
+
event_type TEXT NOT NULL,
|
|
1554
|
+
tier_id TEXT NOT NULL,
|
|
1555
|
+
encrypted_payload TEXT NOT NULL,
|
|
1556
|
+
prev_hash TEXT NOT NULL DEFAULT '',
|
|
1557
|
+
hash TEXT NOT NULL DEFAULT ''
|
|
1558
|
+
)
|
|
1559
|
+
`);
|
|
1560
|
+
const cols = this.db.pragma("table_info(audit_logs)").map((c) => c.name);
|
|
1561
|
+
if (!cols.includes("prev_hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN prev_hash TEXT NOT NULL DEFAULT ''`);
|
|
1562
|
+
if (!cols.includes("hash")) this.db.exec(`ALTER TABLE audit_logs ADD COLUMN hash TEXT NOT NULL DEFAULT ''`);
|
|
1563
|
+
}
|
|
1564
|
+
workspacePath;
|
|
1565
|
+
debugMode;
|
|
1566
|
+
db;
|
|
1567
|
+
keyPath;
|
|
1568
|
+
dbPath;
|
|
1569
|
+
encryptionKey;
|
|
1570
|
+
/** SHA-256 link over everything that identifies an entry, chained to the previous entry's hash. */
|
|
1571
|
+
chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload) {
|
|
1572
|
+
return crypto.createHash("sha256").update(`${prevHash}|${timestamp}|${eventType}|${tierId}|${encryptedPayload}`).digest("hex");
|
|
1573
|
+
}
|
|
1574
|
+
initEncryptionKey() {
|
|
1575
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
1576
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
1577
|
+
} else {
|
|
1578
|
+
this.encryptionKey = crypto.randomBytes(32);
|
|
1579
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
encrypt(text) {
|
|
1583
|
+
const iv = crypto.randomBytes(16);
|
|
1584
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1585
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
1586
|
+
encrypted += cipher.final("hex");
|
|
1587
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
1588
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
1589
|
+
}
|
|
1590
|
+
decrypt(text) {
|
|
1591
|
+
const parts = text.split(":");
|
|
1592
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
1593
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
1594
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
1595
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
1596
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
1597
|
+
decipher.setAuthTag(authTag);
|
|
1598
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
1599
|
+
decrypted += decipher.final("utf8");
|
|
1600
|
+
return decrypted;
|
|
1601
|
+
}
|
|
1602
|
+
logEvent(eventType, tierId, payloadObj) {
|
|
1603
|
+
const id = crypto.randomUUID();
|
|
1604
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1605
|
+
const payload = JSON.stringify(payloadObj);
|
|
1606
|
+
const encryptedPayload = this.encrypt(payload);
|
|
1607
|
+
const last = this.db.prepare("SELECT hash FROM audit_logs ORDER BY rowid DESC LIMIT 1").get();
|
|
1608
|
+
const prevHash = last?.hash ?? "";
|
|
1609
|
+
const hash = this.chainHash(prevHash, timestamp, eventType, tierId, encryptedPayload);
|
|
1610
|
+
const stmt = this.db.prepare("INSERT INTO audit_logs (id, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
|
1611
|
+
stmt.run(id, timestamp, eventType, tierId, encryptedPayload, prevHash, hash);
|
|
1612
|
+
this.dumpDebugIfNeeded();
|
|
1613
|
+
return id;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Recompute the whole hash chain in insertion (rowid) order. Any edited,
|
|
1617
|
+
* removed, or reordered row makes every later hash mismatch. Rows written
|
|
1618
|
+
* before the chain existed (empty hash) fail verification too — integrity
|
|
1619
|
+
* can only be claimed for chained history.
|
|
1620
|
+
*/
|
|
1621
|
+
verifyChain() {
|
|
1622
|
+
const rows = this.db.prepare(
|
|
1623
|
+
"SELECT rowid, timestamp, event_type, tier_id, encrypted_payload, prev_hash, hash FROM audit_logs ORDER BY rowid ASC"
|
|
1624
|
+
).all();
|
|
1625
|
+
let prevHash = "";
|
|
1626
|
+
for (const row of rows) {
|
|
1627
|
+
const expected = this.chainHash(prevHash, row.timestamp, row.event_type, row.tier_id, row.encrypted_payload);
|
|
1628
|
+
if (row.prev_hash !== prevHash || row.hash !== expected) {
|
|
1629
|
+
return { ok: false, entries: rows.length, firstBadRow: row.rowid };
|
|
1630
|
+
}
|
|
1631
|
+
prevHash = row.hash;
|
|
1632
|
+
}
|
|
1633
|
+
return { ok: true, entries: rows.length };
|
|
1634
|
+
}
|
|
1635
|
+
getAllLogs() {
|
|
1636
|
+
const stmt = this.db.prepare("SELECT id, timestamp, event_type, tier_id, encrypted_payload FROM audit_logs ORDER BY timestamp ASC");
|
|
1637
|
+
const rows = stmt.all();
|
|
1638
|
+
return rows.map((row) => {
|
|
1639
|
+
let payload = "";
|
|
1640
|
+
try {
|
|
1641
|
+
payload = this.decrypt(row.encrypted_payload);
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
payload = '{"error":"[Decryption Failed - Payload Corrupted]"}';
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
id: row.id,
|
|
1647
|
+
timestamp: row.timestamp,
|
|
1648
|
+
eventType: row.event_type,
|
|
1649
|
+
tierId: row.tier_id,
|
|
1650
|
+
payload
|
|
1651
|
+
};
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
dumpDebugIfNeeded() {
|
|
1655
|
+
if (!this.debugMode) return;
|
|
1656
|
+
try {
|
|
1657
|
+
const dumpPath = path25.join(os8.tmpdir(), "cascade_audit_logs_debug.json");
|
|
1658
|
+
const entries = this.getAllLogs();
|
|
1659
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
console.error("Failed to dump debug audit logs", err);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
close() {
|
|
1665
|
+
this.db.close();
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1528
1671
|
// src/cli/index.ts
|
|
1529
1672
|
init_constants();
|
|
1530
1673
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -1568,7 +1711,7 @@ var Keystore = class {
|
|
|
1568
1711
|
const creds = await this.keytar.findCredentials(KEYTAR_SERVICE);
|
|
1569
1712
|
this.cache = Object.fromEntries(creds.map((c) => [c.account, c.password]));
|
|
1570
1713
|
this.backend = "keytar";
|
|
1571
|
-
if (password &&
|
|
1714
|
+
if (password && fs23.existsSync(this.storePath)) {
|
|
1572
1715
|
try {
|
|
1573
1716
|
const fileEntries = this.decryptFile(password);
|
|
1574
1717
|
for (const [k, v] of Object.entries(fileEntries)) {
|
|
@@ -1587,7 +1730,7 @@ var Keystore = class {
|
|
|
1587
1730
|
"Keystore unlock requires a password because the OS keychain (keytar) is not available on this system."
|
|
1588
1731
|
);
|
|
1589
1732
|
}
|
|
1590
|
-
if (!
|
|
1733
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
1591
1734
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1592
1735
|
this.masterKey = this.deriveKey(password, salt);
|
|
1593
1736
|
this.writeWithSalt({}, salt);
|
|
@@ -1601,7 +1744,7 @@ var Keystore = class {
|
|
|
1601
1744
|
}
|
|
1602
1745
|
/** Synchronous legacy unlock kept for AES-only environments. */
|
|
1603
1746
|
unlockSync(password) {
|
|
1604
|
-
if (!
|
|
1747
|
+
if (!fs23.existsSync(this.storePath)) {
|
|
1605
1748
|
const salt = crypto.randomBytes(SALT_LEN);
|
|
1606
1749
|
this.masterKey = this.deriveKey(password, salt);
|
|
1607
1750
|
this.writeWithSalt({}, salt);
|
|
@@ -1659,7 +1802,7 @@ var Keystore = class {
|
|
|
1659
1802
|
}
|
|
1660
1803
|
}
|
|
1661
1804
|
decryptFile(password, knownSalt) {
|
|
1662
|
-
if (!
|
|
1805
|
+
if (!fs23.existsSync(this.storePath)) return {};
|
|
1663
1806
|
try {
|
|
1664
1807
|
const { salt, ciphertext, iv, tag } = this.readRaw();
|
|
1665
1808
|
const useSalt = knownSalt ?? salt;
|
|
@@ -1681,8 +1824,8 @@ var Keystore = class {
|
|
|
1681
1824
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1682
1825
|
const tag = cipher.getAuthTag();
|
|
1683
1826
|
const out = Buffer.concat([raw.salt, iv, tag, ciphertext]);
|
|
1684
|
-
|
|
1685
|
-
|
|
1827
|
+
fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
|
|
1828
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1686
1829
|
}
|
|
1687
1830
|
writeWithSalt(data, salt) {
|
|
1688
1831
|
if (!this.masterKey) throw new Error("writeWithSalt called before masterKey was set");
|
|
@@ -1692,11 +1835,11 @@ var Keystore = class {
|
|
|
1692
1835
|
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1693
1836
|
const tag = cipher.getAuthTag();
|
|
1694
1837
|
const out = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
1695
|
-
|
|
1696
|
-
|
|
1838
|
+
fs23.mkdirSync(path25.dirname(this.storePath), { recursive: true });
|
|
1839
|
+
fs23.writeFileSync(this.storePath, out, { mode: 384 });
|
|
1697
1840
|
}
|
|
1698
1841
|
readRaw() {
|
|
1699
|
-
const buf =
|
|
1842
|
+
const buf = fs23.readFileSync(this.storePath);
|
|
1700
1843
|
let offset = 0;
|
|
1701
1844
|
const salt = buf.subarray(offset, offset + SALT_LEN);
|
|
1702
1845
|
offset += SALT_LEN;
|
|
@@ -1729,7 +1872,7 @@ var CascadeIgnore = class {
|
|
|
1729
1872
|
]);
|
|
1730
1873
|
}
|
|
1731
1874
|
async load(workspacePath) {
|
|
1732
|
-
const filePath =
|
|
1875
|
+
const filePath = path25.join(workspacePath, ".cascadeignore");
|
|
1733
1876
|
try {
|
|
1734
1877
|
const content = await fs9.readFile(filePath, "utf-8");
|
|
1735
1878
|
const lines = content.split("\n").filter((l) => l.trim() && !l.startsWith("#"));
|
|
@@ -1740,7 +1883,7 @@ var CascadeIgnore = class {
|
|
|
1740
1883
|
}
|
|
1741
1884
|
isIgnored(filePath, workspacePath) {
|
|
1742
1885
|
try {
|
|
1743
|
-
const relative = workspacePath ?
|
|
1886
|
+
const relative = workspacePath ? path25.relative(workspacePath, filePath) : filePath;
|
|
1744
1887
|
return this.ig.ignores(relative);
|
|
1745
1888
|
} catch {
|
|
1746
1889
|
return false;
|
|
@@ -1751,7 +1894,7 @@ var CascadeIgnore = class {
|
|
|
1751
1894
|
}
|
|
1752
1895
|
};
|
|
1753
1896
|
async function createDefaultIgnoreFile(workspacePath) {
|
|
1754
|
-
const filePath =
|
|
1897
|
+
const filePath = path25.join(workspacePath, ".cascadeignore");
|
|
1755
1898
|
const content = `# .cascadeignore \u2014 Files Cascade agents cannot read or modify
|
|
1756
1899
|
# Syntax identical to .gitignore
|
|
1757
1900
|
|
|
@@ -1781,7 +1924,7 @@ Thumbs.db
|
|
|
1781
1924
|
await fs9.writeFile(filePath, content, "utf-8");
|
|
1782
1925
|
}
|
|
1783
1926
|
async function loadCascadeMd(workspacePath) {
|
|
1784
|
-
const filePath =
|
|
1927
|
+
const filePath = path25.join(workspacePath, "CASCADE.md");
|
|
1785
1928
|
try {
|
|
1786
1929
|
const raw = await fs9.readFile(filePath, "utf-8");
|
|
1787
1930
|
return parseCascadeMd(raw);
|
|
@@ -1810,7 +1953,7 @@ ${raw.trim()}`;
|
|
|
1810
1953
|
return { raw, sections, systemPrompt };
|
|
1811
1954
|
}
|
|
1812
1955
|
async function createDefaultCascadeMd(workspacePath) {
|
|
1813
|
-
const filePath =
|
|
1956
|
+
const filePath = path25.join(workspacePath, "CASCADE.md");
|
|
1814
1957
|
const content = `# Cascade Project Instructions
|
|
1815
1958
|
|
|
1816
1959
|
This file contains project-specific instructions for the Cascade AI agent.
|
|
@@ -1845,9 +1988,9 @@ List any areas the agent should not touch.
|
|
|
1845
1988
|
var MemoryStore = class _MemoryStore {
|
|
1846
1989
|
db;
|
|
1847
1990
|
constructor(dbPath) {
|
|
1848
|
-
|
|
1991
|
+
fs23.mkdirSync(path25.dirname(dbPath), { recursive: true });
|
|
1849
1992
|
try {
|
|
1850
|
-
this.db = new
|
|
1993
|
+
this.db = new Database2(dbPath, { timeout: 5e3 });
|
|
1851
1994
|
this.db.pragma("journal_mode = WAL");
|
|
1852
1995
|
this.db.pragma("foreign_keys = ON");
|
|
1853
1996
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -2692,7 +2835,8 @@ var WorkspaceConfigSchema = z.object({
|
|
|
2692
2835
|
cascadeMdPath: z.string().default("CASCADE.md"),
|
|
2693
2836
|
configPath: z.string().default(".cascade/config.json"),
|
|
2694
2837
|
keystorePath: z.string().default(".cascade/keystore.enc"),
|
|
2695
|
-
auditLogPath: z.string().default(".cascade/audit.log")
|
|
2838
|
+
auditLogPath: z.string().default(".cascade/audit.log"),
|
|
2839
|
+
debugWorldState: z.boolean().default(false)
|
|
2696
2840
|
});
|
|
2697
2841
|
var CascadeConfigSchema = z.object({
|
|
2698
2842
|
version: z.literal("1.0").default("1.0"),
|
|
@@ -2837,6 +2981,16 @@ var CascadeConfigSchema = z.object({
|
|
|
2837
2981
|
* 'parallel' / 'sequential' — force it.
|
|
2838
2982
|
*/
|
|
2839
2983
|
t3Execution: z.enum(["auto", "parallel", "sequential"]).default("auto"),
|
|
2984
|
+
/**
|
|
2985
|
+
* Per-path privacy tiers. A subtask touching a `local-only` path is forced
|
|
2986
|
+
* onto LOCAL models (never cloud) and its raw output is withheld from the
|
|
2987
|
+
* tiers above. Patterns use .gitignore syntax, like .cascadeignore.
|
|
2988
|
+
*/
|
|
2989
|
+
privacy: z.object({
|
|
2990
|
+
paths: z.array(z.object({ pattern: z.string().min(1), policy: z.enum(["local-only"]) })).default([])
|
|
2991
|
+
}).optional(),
|
|
2992
|
+
/** Routing controls — forceTier pins the root tier, bypassing the classifier. */
|
|
2993
|
+
routing: z.object({ forceTier: z.enum(["auto", "T1", "T2", "T3"]).default("auto") }).optional(),
|
|
2840
2994
|
/**
|
|
2841
2995
|
* T3→T2 reinforcement: when enabled, a worker that discovers its subtask should
|
|
2842
2996
|
* fan out can call the `request_workers` tool to have its T2 manager spawn
|
|
@@ -2886,15 +3040,15 @@ var ConfigManager = class {
|
|
|
2886
3040
|
globalDir;
|
|
2887
3041
|
constructor(workspacePath = process.cwd()) {
|
|
2888
3042
|
this.workspacePath = workspacePath;
|
|
2889
|
-
this.globalDir =
|
|
3043
|
+
this.globalDir = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR);
|
|
2890
3044
|
}
|
|
2891
3045
|
async load() {
|
|
2892
3046
|
this.config = await this.loadConfig();
|
|
2893
3047
|
this.ignore = new CascadeIgnore();
|
|
2894
3048
|
await this.ignore.load(this.workspacePath);
|
|
2895
3049
|
this.cascadeMd = await loadCascadeMd(this.workspacePath);
|
|
2896
|
-
this.keystore = new Keystore(
|
|
2897
|
-
this.store = new MemoryStore(
|
|
3050
|
+
this.keystore = new Keystore(path25.join(this.globalDir, GLOBAL_KEYSTORE_FILE));
|
|
3051
|
+
this.store = new MemoryStore(path25.join(this.workspacePath, CASCADE_DB_FILE));
|
|
2898
3052
|
await this.injectEnvKeys();
|
|
2899
3053
|
await this.ensureDefaultIdentity();
|
|
2900
3054
|
}
|
|
@@ -2917,8 +3071,8 @@ var ConfigManager = class {
|
|
|
2917
3071
|
return this.workspacePath;
|
|
2918
3072
|
}
|
|
2919
3073
|
async save() {
|
|
2920
|
-
const configPath =
|
|
2921
|
-
await fs9.mkdir(
|
|
3074
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
3075
|
+
await fs9.mkdir(path25.dirname(configPath), { recursive: true });
|
|
2922
3076
|
await fs9.writeFile(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
2923
3077
|
}
|
|
2924
3078
|
async updateConfig(updates) {
|
|
@@ -2942,7 +3096,7 @@ var ConfigManager = class {
|
|
|
2942
3096
|
return configProvider?.apiKey;
|
|
2943
3097
|
}
|
|
2944
3098
|
async loadConfig() {
|
|
2945
|
-
const configPath =
|
|
3099
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
2946
3100
|
try {
|
|
2947
3101
|
const raw = await fs9.readFile(configPath, "utf-8");
|
|
2948
3102
|
return validateConfig(JSON.parse(raw));
|
|
@@ -3821,7 +3975,7 @@ init_constants();
|
|
|
3821
3975
|
var DEFAULT_SNAPSHOT_URL = "https://raw.githubusercontent.com/Varun-SV/Cascade-AI/main/src/core/router/benchmark-data.json";
|
|
3822
3976
|
var OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
|
|
3823
3977
|
var FETCH_TIMEOUT_MS = 8e3;
|
|
3824
|
-
var DEFAULT_CACHE_FILE =
|
|
3978
|
+
var DEFAULT_CACHE_FILE = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, "benchmarks-cache.json");
|
|
3825
3979
|
function normalizeModelId(id) {
|
|
3826
3980
|
let s = id.toLowerCase();
|
|
3827
3981
|
const slash = s.lastIndexOf("/");
|
|
@@ -3936,7 +4090,7 @@ var LiveDataProvider = class {
|
|
|
3936
4090
|
}
|
|
3937
4091
|
async saveCache() {
|
|
3938
4092
|
try {
|
|
3939
|
-
await fs9.mkdir(
|
|
4093
|
+
await fs9.mkdir(path25.dirname(this.opts.cacheFile), { recursive: true });
|
|
3940
4094
|
const cache = {
|
|
3941
4095
|
fetchedAt: this.fetchedAt,
|
|
3942
4096
|
snapshot: this.snapshot ?? void 0,
|
|
@@ -4066,7 +4220,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4066
4220
|
costByTier: {},
|
|
4067
4221
|
tokensByTier: {},
|
|
4068
4222
|
inputTokensByTier: {},
|
|
4069
|
-
outputTokensByTier: {}
|
|
4223
|
+
outputTokensByTier: {},
|
|
4224
|
+
costByFeature: {}
|
|
4070
4225
|
};
|
|
4071
4226
|
tierModels = /* @__PURE__ */ new Map();
|
|
4072
4227
|
config;
|
|
@@ -4088,6 +4243,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4088
4243
|
tpmLimiter;
|
|
4089
4244
|
localQueue;
|
|
4090
4245
|
taskAnalyzer;
|
|
4246
|
+
worldStateDB;
|
|
4247
|
+
privacyPaths;
|
|
4248
|
+
guidanceQueue;
|
|
4091
4249
|
liveData;
|
|
4092
4250
|
/** Snapshot of configured/default tier models, taken before Cascade Auto overrides them. */
|
|
4093
4251
|
originalTierModels;
|
|
@@ -4245,7 +4403,17 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4245
4403
|
if (options.model && !requireVision) {
|
|
4246
4404
|
this.ensureProvider(options.model, this.config.providers);
|
|
4247
4405
|
}
|
|
4248
|
-
|
|
4406
|
+
let model = requireVision ? this.selector.selectVisionModel() : options.model ?? this.tierModels.get(tier);
|
|
4407
|
+
if (options.forceLocal && model && !this.isPrivateModel(model)) {
|
|
4408
|
+
const localModel = this.selector.getAllAvailableModels().find((m) => this.isPrivateModel(m));
|
|
4409
|
+
if (!localModel) {
|
|
4410
|
+
throw new Error(
|
|
4411
|
+
"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."
|
|
4412
|
+
);
|
|
4413
|
+
}
|
|
4414
|
+
this.ensureProvider(localModel, this.config.providers);
|
|
4415
|
+
model = localModel;
|
|
4416
|
+
}
|
|
4249
4417
|
if (!model) throw new Error(`No model available for tier ${tier}`);
|
|
4250
4418
|
const provider = this.getProvider(model);
|
|
4251
4419
|
if (!provider) throw new Error(`No provider for model ${model.id}`);
|
|
@@ -4317,7 +4485,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4317
4485
|
if (!result || typeof result.content !== "string" || !result.usage) {
|
|
4318
4486
|
throw new Error(`Provider ${model.provider}:${model.id} returned an invalid generation result.`);
|
|
4319
4487
|
}
|
|
4320
|
-
this.recordStats(tier, model, result.usage);
|
|
4488
|
+
this.recordStats(tier, model, result.usage, options.featureTag);
|
|
4321
4489
|
this.failover.recordSuccess(model.provider);
|
|
4322
4490
|
return result;
|
|
4323
4491
|
} catch (err) {
|
|
@@ -4422,6 +4590,42 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4422
4590
|
setTaskAnalyzer(analyzer) {
|
|
4423
4591
|
this.taskAnalyzer = analyzer;
|
|
4424
4592
|
}
|
|
4593
|
+
setWorldStateDB(db) {
|
|
4594
|
+
this.worldStateDB = db;
|
|
4595
|
+
}
|
|
4596
|
+
getWorldStateDB() {
|
|
4597
|
+
return this.worldStateDB;
|
|
4598
|
+
}
|
|
4599
|
+
setPrivacyPaths(paths) {
|
|
4600
|
+
this.privacyPaths = paths;
|
|
4601
|
+
}
|
|
4602
|
+
getPrivacyPaths() {
|
|
4603
|
+
return this.privacyPaths;
|
|
4604
|
+
}
|
|
4605
|
+
setGuidanceQueue(queue) {
|
|
4606
|
+
this.guidanceQueue = queue;
|
|
4607
|
+
}
|
|
4608
|
+
getGuidanceQueue() {
|
|
4609
|
+
return this.guidanceQueue;
|
|
4610
|
+
}
|
|
4611
|
+
/**
|
|
4612
|
+
* "Private" = inference never leaves the user's machine/network: Ollama
|
|
4613
|
+
* models (isLocal), or an OpenAI-compatible endpoint (e.g. llama.cpp, vLLM,
|
|
4614
|
+
* LM Studio) whose configured host is loopback or a private range. A cloud
|
|
4615
|
+
* OpenAI-compatible endpoint (public host) does NOT qualify.
|
|
4616
|
+
*/
|
|
4617
|
+
isPrivateModel(model) {
|
|
4618
|
+
if (model.isLocal) return true;
|
|
4619
|
+
if (model.provider !== "openai-compatible") return false;
|
|
4620
|
+
const baseUrl = this.config?.providers?.find((p) => p.type === "openai-compatible")?.baseUrl;
|
|
4621
|
+
if (!baseUrl) return false;
|
|
4622
|
+
try {
|
|
4623
|
+
const host = new URL(baseUrl).hostname.toLowerCase();
|
|
4624
|
+
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");
|
|
4625
|
+
} catch {
|
|
4626
|
+
return false;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4425
4629
|
/**
|
|
4426
4630
|
* Cascade Auto per-subtask routing: pick the benchmark-best model for a
|
|
4427
4631
|
* specific subtask's text, scoped to the tier's eligible candidates. Returns
|
|
@@ -4445,7 +4649,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4445
4649
|
costByTier: { ...this.stats.costByTier },
|
|
4446
4650
|
tokensByTier: { ...this.stats.tokensByTier },
|
|
4447
4651
|
inputTokensByTier: { ...this.stats.inputTokensByTier },
|
|
4448
|
-
outputTokensByTier: { ...this.stats.outputTokensByTier }
|
|
4652
|
+
outputTokensByTier: { ...this.stats.outputTokensByTier },
|
|
4653
|
+
costByFeature: { ...this.stats.costByFeature }
|
|
4449
4654
|
};
|
|
4450
4655
|
}
|
|
4451
4656
|
/**
|
|
@@ -4495,7 +4700,8 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4495
4700
|
costByTier: {},
|
|
4496
4701
|
tokensByTier: {},
|
|
4497
4702
|
inputTokensByTier: {},
|
|
4498
|
-
outputTokensByTier: {}
|
|
4703
|
+
outputTokensByTier: {},
|
|
4704
|
+
costByFeature: {}
|
|
4499
4705
|
};
|
|
4500
4706
|
this.sessionCostUsd = 0;
|
|
4501
4707
|
this.budgetState = "ok";
|
|
@@ -4661,7 +4867,7 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4661
4867
|
}
|
|
4662
4868
|
return void 0;
|
|
4663
4869
|
}
|
|
4664
|
-
recordStats(tier, model, usage) {
|
|
4870
|
+
recordStats(tier, model, usage, featureTag) {
|
|
4665
4871
|
this.stats.totalTokens += usage.totalTokens;
|
|
4666
4872
|
this.stats.totalCostUsd += usage.estimatedCostUsd;
|
|
4667
4873
|
this.sessionCostUsd += usage.estimatedCostUsd;
|
|
@@ -4671,6 +4877,9 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
|
|
|
4671
4877
|
this.stats.tokensByTier[tier] = (this.stats.tokensByTier[tier] ?? 0) + usage.totalTokens;
|
|
4672
4878
|
this.stats.inputTokensByTier[tier] = (this.stats.inputTokensByTier[tier] ?? 0) + usage.inputTokens;
|
|
4673
4879
|
this.stats.outputTokensByTier[tier] = (this.stats.outputTokensByTier[tier] ?? 0) + usage.outputTokens;
|
|
4880
|
+
if (featureTag) {
|
|
4881
|
+
this.stats.costByFeature[featureTag] = (this.stats.costByFeature[featureTag] ?? 0) + usage.estimatedCostUsd;
|
|
4882
|
+
}
|
|
4674
4883
|
this.runTokens += usage.totalTokens;
|
|
4675
4884
|
this.runCostUsd += usage.estimatedCostUsd;
|
|
4676
4885
|
this.updateBudgetState();
|
|
@@ -4767,6 +4976,13 @@ var BaseTier = class extends EventEmitter {
|
|
|
4767
4976
|
hierarchyContext = "";
|
|
4768
4977
|
/** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
|
|
4769
4978
|
signal;
|
|
4979
|
+
/**
|
|
4980
|
+
* True for the run's ROOT tier (T3 for Simple, T2 for Moderate, T1 for
|
|
4981
|
+
* Complex). Its own synthesis stream is the user-facing answer and is
|
|
4982
|
+
* tagged `primary` so the desktop renders it live — background workers,
|
|
4983
|
+
* which would interleave, are not tagged.
|
|
4984
|
+
*/
|
|
4985
|
+
isPresenter = false;
|
|
4770
4986
|
constructor(role, id, parentId) {
|
|
4771
4987
|
super();
|
|
4772
4988
|
this.role = role;
|
|
@@ -4774,6 +4990,10 @@ var BaseTier = class extends EventEmitter {
|
|
|
4774
4990
|
this.parentId = parentId;
|
|
4775
4991
|
this.label = this.id;
|
|
4776
4992
|
}
|
|
4993
|
+
/** Mark this tier as the run's presenter (root tier). */
|
|
4994
|
+
setPresenter(on = true) {
|
|
4995
|
+
this.isPresenter = on;
|
|
4996
|
+
}
|
|
4777
4997
|
getStatus() {
|
|
4778
4998
|
return this.status;
|
|
4779
4999
|
}
|
|
@@ -5208,6 +5428,8 @@ var T3Worker = class extends BaseTier {
|
|
|
5208
5428
|
toolRegistry;
|
|
5209
5429
|
context;
|
|
5210
5430
|
assignment;
|
|
5431
|
+
/** True when this subtask matched a privacy.paths local-only pattern. */
|
|
5432
|
+
localOnlyMatch = false;
|
|
5211
5433
|
peerSyncBuffer = [];
|
|
5212
5434
|
store;
|
|
5213
5435
|
audit;
|
|
@@ -5263,6 +5485,11 @@ var T3Worker = class extends BaseTier {
|
|
|
5263
5485
|
this.taskId = taskId;
|
|
5264
5486
|
this.setLabel(assignment.subtaskTitle);
|
|
5265
5487
|
this.setStatus("ACTIVE");
|
|
5488
|
+
const privacy = this.router.getPrivacyPaths?.();
|
|
5489
|
+
this.localOnlyMatch = !!privacy?.hasPolicies() && privacy.anyLocalOnly(this.extractArtifactPaths(assignment));
|
|
5490
|
+
if (this.localOnlyMatch) {
|
|
5491
|
+
this.log("Privacy: subtask touches a local-only path \u2014 forcing a private model; raw output will be withheld upstream.");
|
|
5492
|
+
}
|
|
5266
5493
|
this.tools = this.toolRegistry.getToolDefinitions();
|
|
5267
5494
|
if (this.reinforcementDepth === 0 && this.router.getReinforcementsConfig?.()?.enabled) {
|
|
5268
5495
|
this.tools = [...this.tools, {
|
|
@@ -5393,9 +5620,17 @@ Now execute your subtask using this context where relevant.`
|
|
|
5393
5620
|
}
|
|
5394
5621
|
const reflectCfg = this.router.getReflectionConfig?.() ?? { enabled: false, maxRounds: 1 };
|
|
5395
5622
|
if (reflectCfg.enabled) {
|
|
5396
|
-
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output", status: "IN_PROGRESS" });
|
|
5623
|
+
this.sendStatusUpdate({ progressPct: 85, currentAction: "Reflecting on output via T2-Critic", status: "IN_PROGRESS" });
|
|
5397
5624
|
output = await this.reflectAndImprove(assignment, output, reflectCfg.maxRounds);
|
|
5398
5625
|
}
|
|
5626
|
+
const db = this.router.getWorldStateDB?.();
|
|
5627
|
+
if (db) {
|
|
5628
|
+
try {
|
|
5629
|
+
db.addEntry(this.id, `Completed: ${assignment.subtaskTitle}. Output length: ${output.length} chars.`);
|
|
5630
|
+
} catch (e) {
|
|
5631
|
+
this.log("Failed to write to World State DB");
|
|
5632
|
+
}
|
|
5633
|
+
}
|
|
5399
5634
|
this.setStatus("COMPLETED", output);
|
|
5400
5635
|
this.sendStatusUpdate({ progressPct: 100, currentAction: "Subtask complete", status: "IN_PROGRESS", output });
|
|
5401
5636
|
this.peerBus?.publish(this.id, assignment.subtaskId, output, "COMPLETED");
|
|
@@ -5468,6 +5703,16 @@ Now execute your subtask using this context where relevant.`
|
|
|
5468
5703
|
while (iterations < MAX_ITERATIONS) {
|
|
5469
5704
|
iterations++;
|
|
5470
5705
|
this.throwIfCancelled();
|
|
5706
|
+
const guidance = this.router.getGuidanceQueue?.()?.drain(this.id) ?? [];
|
|
5707
|
+
for (const g of guidance) {
|
|
5708
|
+
this.log(`User intervention received: ${g.text}`);
|
|
5709
|
+
this.sendStatusUpdate({ progressPct: 50, currentAction: "Applying user intervention", status: "IN_PROGRESS" });
|
|
5710
|
+
await this.context.addMessage({
|
|
5711
|
+
role: "user",
|
|
5712
|
+
content: `USER INTERVENTION (mid-run steering \u2014 follow this over prior instructions where they conflict):
|
|
5713
|
+
${g.text}`
|
|
5714
|
+
});
|
|
5715
|
+
}
|
|
5471
5716
|
const options = {
|
|
5472
5717
|
messages: this.context.getMessages(),
|
|
5473
5718
|
systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
|
|
@@ -5476,13 +5721,15 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5476
5721
|
// Don't pass tools array when model can't use them natively
|
|
5477
5722
|
tools: useTextTools ? void 0 : tools.length ? tools : void 0,
|
|
5478
5723
|
maxTokens: 4096,
|
|
5479
|
-
...subtaskModel ? { model: subtaskModel } : {}
|
|
5724
|
+
...subtaskModel ? { model: subtaskModel } : {},
|
|
5725
|
+
featureTag: this.assignment?.sectionTitle,
|
|
5726
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5480
5727
|
};
|
|
5481
5728
|
const result = await this.router.generate(
|
|
5482
5729
|
"T3",
|
|
5483
5730
|
options,
|
|
5484
5731
|
(chunk) => {
|
|
5485
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
5732
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
5486
5733
|
}
|
|
5487
5734
|
);
|
|
5488
5735
|
let effectiveToolCalls = result.toolCalls ?? [];
|
|
@@ -5640,8 +5887,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : "") + textToolSuffix,
|
|
|
5640
5887
|
tierId: this.id,
|
|
5641
5888
|
sessionId: this.taskId,
|
|
5642
5889
|
requireApproval: false,
|
|
5643
|
-
saveSnapshot: async (
|
|
5644
|
-
this.store?.addFileSnapshot(this.taskId,
|
|
5890
|
+
saveSnapshot: async (path30, content) => {
|
|
5891
|
+
this.store?.addFileSnapshot(this.taskId, path30, content);
|
|
5645
5892
|
},
|
|
5646
5893
|
sendPeerSync: (to, syncType, content) => {
|
|
5647
5894
|
this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
|
|
@@ -5811,7 +6058,7 @@ ${assignment.expectedOutput}`;
|
|
|
5811
6058
|
const { promisify: promisify4 } = await import('util');
|
|
5812
6059
|
const execAsync3 = promisify4(exec3);
|
|
5813
6060
|
for (const artifactPath of artifactPaths) {
|
|
5814
|
-
const absolutePath =
|
|
6061
|
+
const absolutePath = path25.resolve(process.cwd(), artifactPath);
|
|
5815
6062
|
try {
|
|
5816
6063
|
const stat = await fs9.stat(absolutePath);
|
|
5817
6064
|
if (!stat.isFile()) {
|
|
@@ -5832,7 +6079,7 @@ ${assignment.expectedOutput}`;
|
|
|
5832
6079
|
issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
|
|
5833
6080
|
continue;
|
|
5834
6081
|
}
|
|
5835
|
-
const ext =
|
|
6082
|
+
const ext = path25.extname(absolutePath).toLowerCase();
|
|
5836
6083
|
try {
|
|
5837
6084
|
if (ext === ".ts" || ext === ".tsx") {
|
|
5838
6085
|
await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
|
|
@@ -5861,30 +6108,35 @@ ${stdout}`);
|
|
|
5861
6108
|
* needed. Best-effort: any parse/error just keeps the current output.
|
|
5862
6109
|
*/
|
|
5863
6110
|
async reflectAndImprove(assignment, output, maxRounds) {
|
|
5864
|
-
const sys = this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5865
|
-
|
|
5866
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "");
|
|
5867
6111
|
let current = output;
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
const
|
|
6112
|
+
try {
|
|
6113
|
+
for (let round = 0; round < Math.max(1, maxRounds); round++) {
|
|
6114
|
+
const verdictResult = await this.router.generate("T2", {
|
|
5871
6115
|
messages: [{
|
|
5872
6116
|
role: "user",
|
|
5873
|
-
content: `
|
|
6117
|
+
content: `You are an independent critic reviewing another worker's output against its assignment.
|
|
5874
6118
|
|
|
5875
|
-
Goal
|
|
6119
|
+
Goal: ${assignment.expectedOutput}
|
|
5876
6120
|
Subtask: ${assignment.description}
|
|
5877
|
-
|
|
5878
|
-
Output:
|
|
6121
|
+
Current Output:
|
|
5879
6122
|
${current}
|
|
5880
6123
|
|
|
5881
|
-
|
|
6124
|
+
Is this output sufficient and correct? Respond with ONLY a JSON object:
|
|
6125
|
+
{"sufficient": true|false, "notes": "what is wrong or missing if false"}`
|
|
5882
6126
|
}],
|
|
5883
|
-
systemPrompt:
|
|
5884
|
-
maxTokens: 400
|
|
6127
|
+
systemPrompt: "You are a T2-Critic reviewing a T3 Worker's output. Judge strictly against the stated goal.",
|
|
6128
|
+
maxTokens: 400,
|
|
6129
|
+
signal: this.signal,
|
|
6130
|
+
featureTag: assignment.sectionTitle,
|
|
6131
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5885
6132
|
});
|
|
5886
|
-
const
|
|
5887
|
-
|
|
6133
|
+
const match = /\{[\s\S]*\}/.exec(verdictResult.content);
|
|
6134
|
+
const parsed = match ? JSON.parse(match[0]) : { sufficient: true };
|
|
6135
|
+
if (parsed.sufficient !== false) {
|
|
6136
|
+
this.log("T2-Critic approved output.");
|
|
6137
|
+
break;
|
|
6138
|
+
}
|
|
6139
|
+
this.log(`T2-Critic rejected output: ${parsed.notes}`);
|
|
5888
6140
|
const improved = await this.router.generate("T3", {
|
|
5889
6141
|
messages: [{
|
|
5890
6142
|
role: "user",
|
|
@@ -5896,16 +6148,20 @@ Goal / expected: ${assignment.expectedOutput}
|
|
|
5896
6148
|
Current output:
|
|
5897
6149
|
${current}`
|
|
5898
6150
|
}],
|
|
5899
|
-
systemPrompt:
|
|
5900
|
-
|
|
6151
|
+
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
6152
|
+
|
|
6153
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6154
|
+
maxTokens: 4096,
|
|
6155
|
+
featureTag: assignment.sectionTitle,
|
|
6156
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5901
6157
|
});
|
|
5902
6158
|
const next = (improved.content ?? "").trim();
|
|
5903
6159
|
if (!next) break;
|
|
5904
6160
|
current = next;
|
|
5905
6161
|
this.log("Reflection: revised output for better goal alignment.");
|
|
5906
|
-
} catch {
|
|
5907
|
-
break;
|
|
5908
6162
|
}
|
|
6163
|
+
} catch (e) {
|
|
6164
|
+
this.log(`T2-Critic reflection failed: ${e}`);
|
|
5909
6165
|
}
|
|
5910
6166
|
return current;
|
|
5911
6167
|
}
|
|
@@ -5926,7 +6182,9 @@ Reply with JSON: { "completeness": "pass"|"fail", "correctness": "pass"|"fail",
|
|
|
5926
6182
|
maxTokens: 500,
|
|
5927
6183
|
systemPrompt: this.systemPromptOverride + (this.hierarchyContext ? `
|
|
5928
6184
|
|
|
5929
|
-
HIERARCHY CONTEXT: ${this.hierarchyContext}` : "")
|
|
6185
|
+
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6186
|
+
featureTag: assignment.sectionTitle,
|
|
6187
|
+
...this.localOnlyMatch ? { forceLocal: true } : {}
|
|
5930
6188
|
});
|
|
5931
6189
|
try {
|
|
5932
6190
|
const jsonMatch = /\{[\s\S]*\}/.exec(testResult.content);
|
|
@@ -6021,6 +6279,7 @@ Begin execution now.`;
|
|
|
6021
6279
|
issues,
|
|
6022
6280
|
peerSyncsUsed: this.peerSyncBuffer.map((m) => m.fromId),
|
|
6023
6281
|
correctionAttempts,
|
|
6282
|
+
localOnly: this.localOnlyMatch || void 0,
|
|
6024
6283
|
reinforcements: this.pendingReinforcements.length ? this.pendingReinforcements : void 0
|
|
6025
6284
|
};
|
|
6026
6285
|
}
|
|
@@ -6312,6 +6571,42 @@ var PeerBus = class extends EventEmitter {
|
|
|
6312
6571
|
}
|
|
6313
6572
|
};
|
|
6314
6573
|
|
|
6574
|
+
// src/core/audit/redaction.ts
|
|
6575
|
+
var RedactionLayer = class {
|
|
6576
|
+
// Regexes for common secrets/PII
|
|
6577
|
+
static RULES = [
|
|
6578
|
+
// IPv4 addresses (basic approximation)
|
|
6579
|
+
{ pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g, replacement: "[REDACTED_IP]" },
|
|
6580
|
+
// Generic API keys/Secrets (looks like a long random hex or b64 string preceded by key/secret/token)
|
|
6581
|
+
{ 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]" },
|
|
6582
|
+
// Email addresses
|
|
6583
|
+
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b/g, replacement: "[REDACTED_EMAIL]" },
|
|
6584
|
+
// Phone numbers (simplistic)
|
|
6585
|
+
{ pattern: /\b(?:\+\d{1,3}[- ]?)?\(?\d{3}\)?[- ]?\d{3}[- ]?\d{4}\b/g, replacement: "[REDACTED_PHONE]" },
|
|
6586
|
+
// AWS Access Key ID
|
|
6587
|
+
{ pattern: /\b(AKIA[0-9A-Z]{16})\b/g, replacement: "[REDACTED_AWS_AK]" }
|
|
6588
|
+
];
|
|
6589
|
+
/**
|
|
6590
|
+
* Applies all redaction rules to the input string.
|
|
6591
|
+
*/
|
|
6592
|
+
static redact(text) {
|
|
6593
|
+
if (!text) return text;
|
|
6594
|
+
let redacted = text;
|
|
6595
|
+
for (const rule of this.RULES) {
|
|
6596
|
+
if (rule.pattern.test(redacted)) {
|
|
6597
|
+
rule.pattern.lastIndex = 0;
|
|
6598
|
+
redacted = redacted.replace(rule.pattern, (match, p1, offset, str2) => {
|
|
6599
|
+
if (p1 && match.includes(p1) && p1 !== match) {
|
|
6600
|
+
return match.replace(p1, rule.replacement.replace("$1", ""));
|
|
6601
|
+
}
|
|
6602
|
+
return rule.replacement;
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
return redacted;
|
|
6607
|
+
}
|
|
6608
|
+
};
|
|
6609
|
+
|
|
6315
6610
|
// src/core/tiers/t2-manager.ts
|
|
6316
6611
|
var T2_SYSTEM_PROMPT = `You are a T2 Manager agent in the Cascade AI system.
|
|
6317
6612
|
Your role is to analyze a section of a task and decompose it into 2-5 discrete subtasks for T3 Workers.
|
|
@@ -6540,7 +6835,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6540
6835
|
try {
|
|
6541
6836
|
const jsonMatch = /\[[\s\S]*\]/.exec(result.content);
|
|
6542
6837
|
if (!jsonMatch) throw new Error("No JSON array found");
|
|
6543
|
-
|
|
6838
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
6839
|
+
return parsed.map((a) => ({ ...a, sectionTitle: assignment.sectionTitle }));
|
|
6544
6840
|
} catch {
|
|
6545
6841
|
return [{
|
|
6546
6842
|
subtaskId: randomUUID(),
|
|
@@ -6550,6 +6846,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6550
6846
|
constraints: assignment.constraints,
|
|
6551
6847
|
peerT3Ids: [],
|
|
6552
6848
|
parentT2: this.id,
|
|
6849
|
+
sectionTitle: assignment.sectionTitle,
|
|
6850
|
+
dependsOn: [],
|
|
6553
6851
|
executionMode: "parallel"
|
|
6554
6852
|
}];
|
|
6555
6853
|
}
|
|
@@ -6669,6 +6967,14 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6669
6967
|
const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
|
|
6670
6968
|
const worker = workerMap.get(id);
|
|
6671
6969
|
const result = await worker.execute(assignment, taskId, waveSignal);
|
|
6970
|
+
if (result.localOnly) {
|
|
6971
|
+
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}]`;
|
|
6972
|
+
} else {
|
|
6973
|
+
if (typeof result.output === "string" && result.output) {
|
|
6974
|
+
result.output = RedactionLayer.redact(result.output);
|
|
6975
|
+
}
|
|
6976
|
+
}
|
|
6977
|
+
if (result.issues) result.issues = result.issues.map((i) => RedactionLayer.redact(i));
|
|
6672
6978
|
resultMap.set(id, result);
|
|
6673
6979
|
return result;
|
|
6674
6980
|
};
|
|
@@ -6884,6 +7190,7 @@ ${peerOutputs}` : "";
|
|
|
6884
7190
|
chunkEnd++;
|
|
6885
7191
|
}
|
|
6886
7192
|
i = chunkEnd;
|
|
7193
|
+
const isLastChunk = chunkEnd >= completed.length;
|
|
6887
7194
|
const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
|
|
6888
7195
|
${currentSummary ? `
|
|
6889
7196
|
PREVIOUS SUMMARY SO FAR:
|
|
@@ -6893,6 +7200,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
6893
7200
|
` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
|
|
6894
7201
|
const messages = [{ role: "user", content: prompt }];
|
|
6895
7202
|
try {
|
|
7203
|
+
const streamFinal = isLastChunk && this.isPresenter ? (chunk) => this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: true }) : void 0;
|
|
6896
7204
|
const result = await this.router.generate("T2", {
|
|
6897
7205
|
messages,
|
|
6898
7206
|
systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
|
|
@@ -6900,7 +7208,7 @@ NEW OUTPUTS TO INTEGRATE:
|
|
|
6900
7208
|
HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
6901
7209
|
maxTokens: 500,
|
|
6902
7210
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
6903
|
-
});
|
|
7211
|
+
}, streamFinal);
|
|
6904
7212
|
currentSummary = result.content;
|
|
6905
7213
|
} catch (err) {
|
|
6906
7214
|
this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -6952,14 +7260,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
|
|
|
6952
7260
|
...this.sectionModel ? { model: this.sectionModel } : {}
|
|
6953
7261
|
});
|
|
6954
7262
|
const answer = result.content.trim().toUpperCase();
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
}
|
|
6958
|
-
if (answer.includes("NO")) {
|
|
6959
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T2", reasoning: "T2 LLM evaluated: inconsistent with section goal" };
|
|
6960
|
-
}
|
|
7263
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7264
|
+
(req.trail ??= []).push({ tier: "T2", verdict, reason: `T2: ${verdict === "approve" ? "consistent with section goal" : verdict === "deny" ? "inconsistent with section goal" : "unsure"}` });
|
|
6961
7265
|
return null;
|
|
6962
7266
|
} catch {
|
|
7267
|
+
(req.trail ??= []).push({ tier: "T2", verdict: "unsure", reason: "T2 evaluation failed" });
|
|
6963
7268
|
return null;
|
|
6964
7269
|
}
|
|
6965
7270
|
}
|
|
@@ -7301,10 +7606,15 @@ In 3-5 terse bullets, flag the most important RISKS, GAPS, or over-/under-decomp
|
|
|
7301
7606
|
}
|
|
7302
7607
|
}
|
|
7303
7608
|
async decomposeTask(prompt, systemContext) {
|
|
7609
|
+
const db = this.router.getWorldStateDB?.();
|
|
7610
|
+
const worldStateContext = db ? `
|
|
7611
|
+
|
|
7612
|
+
PROJECT WORLD STATE (Current architecture and recent changes):
|
|
7613
|
+
${db.getFormattedState()}` : "";
|
|
7304
7614
|
const contextSection = systemContext ? `
|
|
7305
7615
|
Project context:
|
|
7306
7616
|
${systemContext}` : "";
|
|
7307
|
-
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}
|
|
7617
|
+
const decompositionPrompt = `Analyze this task and create an execution plan.${contextSection}${worldStateContext}
|
|
7308
7618
|
|
|
7309
7619
|
Task: ${prompt}
|
|
7310
7620
|
|
|
@@ -7636,7 +7946,7 @@ Instructions:
|
|
|
7636
7946
|
systemPrompt: this.systemPromptOverride + "You are a final output compiler. Summarize and format the task results clearly.",
|
|
7637
7947
|
maxTokens: 8e3
|
|
7638
7948
|
}, (chunk) => {
|
|
7639
|
-
this.emit("stream:token", { tierId: this.id, text: chunk.text });
|
|
7949
|
+
this.emit("stream:token", { tierId: this.id, text: chunk.text, primary: this.isPresenter });
|
|
7640
7950
|
});
|
|
7641
7951
|
return result.content;
|
|
7642
7952
|
}
|
|
@@ -7665,14 +7975,11 @@ Reply with exactly one word: YES, NO, or UNSURE.
|
|
|
7665
7975
|
temperature: 0
|
|
7666
7976
|
});
|
|
7667
7977
|
const answer = result.content.trim().toUpperCase();
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
}
|
|
7671
|
-
if (answer.includes("NO")) {
|
|
7672
|
-
return { requestId: req.id, approved: false, always: true, decidedBy: "T1", reasoning: "T1 evaluated: not consistent with overall task goal" };
|
|
7673
|
-
}
|
|
7978
|
+
const verdict = answer.includes("YES") ? "approve" : answer.includes("NO") ? "deny" : "unsure";
|
|
7979
|
+
(req.trail ??= []).push({ tier: "T1", verdict, reason: `T1: ${verdict === "approve" ? "consistent with overall task goal" : verdict === "deny" ? "not consistent with overall task goal" : "unsure"}` });
|
|
7674
7980
|
return null;
|
|
7675
7981
|
} catch {
|
|
7982
|
+
(req.trail ??= []).push({ tier: "T1", verdict: "unsure", reason: "T1 evaluation failed" });
|
|
7676
7983
|
return null;
|
|
7677
7984
|
}
|
|
7678
7985
|
}
|
|
@@ -7792,16 +8099,16 @@ function resolveInWorkspace(workspaceRoot, input) {
|
|
|
7792
8099
|
if (typeof input !== "string" || input.length === 0) {
|
|
7793
8100
|
throw new WorkspaceSandboxError(String(input), workspaceRoot);
|
|
7794
8101
|
}
|
|
7795
|
-
const root =
|
|
7796
|
-
const abs =
|
|
7797
|
-
const rel =
|
|
7798
|
-
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") ||
|
|
8102
|
+
const root = path25.resolve(workspaceRoot);
|
|
8103
|
+
const abs = path25.isAbsolute(input) ? path25.resolve(input) : path25.resolve(root, input);
|
|
8104
|
+
const rel = path25.relative(root, abs);
|
|
8105
|
+
if (rel === "" || rel === ".") ; else if (rel.startsWith("..") || path25.isAbsolute(rel)) {
|
|
7799
8106
|
throw new WorkspaceSandboxError(input, root);
|
|
7800
8107
|
}
|
|
7801
8108
|
try {
|
|
7802
|
-
const real =
|
|
7803
|
-
const realRel =
|
|
7804
|
-
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") ||
|
|
8109
|
+
const real = fs23.realpathSync(abs);
|
|
8110
|
+
const realRel = path25.relative(root, real);
|
|
8111
|
+
if (realRel !== "" && realRel !== "." && (realRel.startsWith("..") || path25.isAbsolute(realRel))) {
|
|
7805
8112
|
throw new WorkspaceSandboxError(input, root);
|
|
7806
8113
|
}
|
|
7807
8114
|
} catch (e) {
|
|
@@ -7862,7 +8169,7 @@ var FileWriteTool = class extends BaseTool {
|
|
|
7862
8169
|
} catch {
|
|
7863
8170
|
}
|
|
7864
8171
|
}
|
|
7865
|
-
await fs9.mkdir(
|
|
8172
|
+
await fs9.mkdir(path25.dirname(absPath), { recursive: true });
|
|
7866
8173
|
await fs9.writeFile(absPath, content, "utf-8");
|
|
7867
8174
|
return `Written ${content.length} characters to ${filePath}`;
|
|
7868
8175
|
}
|
|
@@ -8360,7 +8667,7 @@ var ImageAnalyzeTool = class extends BaseTool {
|
|
|
8360
8667
|
};
|
|
8361
8668
|
async function fileToImageAttachment(filePath) {
|
|
8362
8669
|
const data = await fs9.readFile(filePath);
|
|
8363
|
-
const ext =
|
|
8670
|
+
const ext = path25.extname(filePath).toLowerCase();
|
|
8364
8671
|
const mimeMap = {
|
|
8365
8672
|
".jpg": "image/jpeg",
|
|
8366
8673
|
".jpeg": "image/jpeg",
|
|
@@ -8394,14 +8701,14 @@ var PDFCreateTool = class extends BaseTool {
|
|
|
8394
8701
|
const filePath = input["path"];
|
|
8395
8702
|
const content = input["content"];
|
|
8396
8703
|
const title = input["title"];
|
|
8397
|
-
const dir =
|
|
8398
|
-
if (!
|
|
8399
|
-
|
|
8704
|
+
const dir = path25.dirname(filePath);
|
|
8705
|
+
if (!fs23.existsSync(dir)) {
|
|
8706
|
+
fs23.mkdirSync(dir, { recursive: true });
|
|
8400
8707
|
}
|
|
8401
8708
|
return new Promise((resolve, reject) => {
|
|
8402
8709
|
try {
|
|
8403
8710
|
const doc = new PDFDocument({ margin: 50 });
|
|
8404
|
-
const stream =
|
|
8711
|
+
const stream = fs23.createWriteStream(filePath);
|
|
8405
8712
|
doc.pipe(stream);
|
|
8406
8713
|
if (title) {
|
|
8407
8714
|
doc.info["Title"] = title;
|
|
@@ -8479,22 +8786,22 @@ var CodeInterpreterTool = class extends BaseTool {
|
|
|
8479
8786
|
}
|
|
8480
8787
|
cmdPrefix = NODE_CMD;
|
|
8481
8788
|
}
|
|
8482
|
-
const tmpDir =
|
|
8483
|
-
if (!
|
|
8484
|
-
|
|
8789
|
+
const tmpDir = path25.join(this.workspaceRoot, ".cascade", "tmp");
|
|
8790
|
+
if (!fs23.existsSync(tmpDir)) {
|
|
8791
|
+
fs23.mkdirSync(tmpDir, { recursive: true });
|
|
8485
8792
|
}
|
|
8486
8793
|
const extension = language === "python" ? "py" : "js";
|
|
8487
8794
|
const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
|
|
8488
|
-
const filePath =
|
|
8489
|
-
|
|
8795
|
+
const filePath = path25.join(tmpDir, fileName);
|
|
8796
|
+
fs23.writeFileSync(filePath, code, "utf-8");
|
|
8490
8797
|
const execArgs = [filePath, ...args];
|
|
8491
8798
|
return new Promise((resolve) => {
|
|
8492
8799
|
const startMs = Date.now();
|
|
8493
8800
|
execFile(cmdPrefix, execArgs, { cwd: this.workspaceRoot, timeout: 3e4 }, (error, stdout, stderr) => {
|
|
8494
8801
|
const duration = Date.now() - startMs;
|
|
8495
8802
|
try {
|
|
8496
|
-
if (
|
|
8497
|
-
|
|
8803
|
+
if (fs23.existsSync(filePath)) {
|
|
8804
|
+
fs23.unlinkSync(filePath);
|
|
8498
8805
|
}
|
|
8499
8806
|
} catch (cleanupErr) {
|
|
8500
8807
|
console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
|
|
@@ -8773,7 +9080,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8773
9080
|
};
|
|
8774
9081
|
async execute(input, _options) {
|
|
8775
9082
|
const pattern = input["pattern"];
|
|
8776
|
-
const searchPath = input["path"] ?
|
|
9083
|
+
const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8777
9084
|
const matches = await glob(pattern, {
|
|
8778
9085
|
cwd: searchPath,
|
|
8779
9086
|
ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
|
|
@@ -8786,7 +9093,7 @@ var GlobTool = class extends BaseTool {
|
|
|
8786
9093
|
const withMtime = await Promise.all(
|
|
8787
9094
|
matches.map(async (rel) => {
|
|
8788
9095
|
try {
|
|
8789
|
-
const stat = await fs9.stat(
|
|
9096
|
+
const stat = await fs9.stat(path25.join(searchPath, rel));
|
|
8790
9097
|
return { rel, mtime: stat.mtimeMs };
|
|
8791
9098
|
} catch {
|
|
8792
9099
|
return { rel, mtime: 0 };
|
|
@@ -8835,7 +9142,7 @@ var GrepTool = class extends BaseTool {
|
|
|
8835
9142
|
};
|
|
8836
9143
|
async execute(input, _options) {
|
|
8837
9144
|
const pattern = input["pattern"];
|
|
8838
|
-
const searchPath = input["path"] ?
|
|
9145
|
+
const searchPath = input["path"] ? path25.resolve(this.workspaceRoot, input["path"]) : this.workspaceRoot;
|
|
8839
9146
|
const globPattern = input["glob"];
|
|
8840
9147
|
const outputMode = input["output_mode"] ?? "content";
|
|
8841
9148
|
const context = input["context"] ?? 0;
|
|
@@ -8889,12 +9196,12 @@ var GrepTool = class extends BaseTool {
|
|
|
8889
9196
|
nodir: true
|
|
8890
9197
|
});
|
|
8891
9198
|
} catch {
|
|
8892
|
-
files = [
|
|
9199
|
+
files = [path25.relative(searchPath, searchPath) || "."];
|
|
8893
9200
|
}
|
|
8894
9201
|
const results = [];
|
|
8895
9202
|
let totalCount = 0;
|
|
8896
9203
|
for (const rel of files) {
|
|
8897
|
-
const abs =
|
|
9204
|
+
const abs = path25.join(searchPath, rel);
|
|
8898
9205
|
let content;
|
|
8899
9206
|
try {
|
|
8900
9207
|
content = await fs9.readFile(abs, "utf-8");
|
|
@@ -9256,10 +9563,10 @@ var ToolRegistry = class extends EventEmitter {
|
|
|
9256
9563
|
}
|
|
9257
9564
|
isIgnored(filePath) {
|
|
9258
9565
|
if (!filePath) return false;
|
|
9259
|
-
const abs =
|
|
9260
|
-
const rel =
|
|
9261
|
-
if (!rel || rel.startsWith("..") ||
|
|
9262
|
-
const posixRel = rel.split(
|
|
9566
|
+
const abs = path25.resolve(this.workspaceRoot, filePath);
|
|
9567
|
+
const rel = path25.relative(this.workspaceRoot, abs);
|
|
9568
|
+
if (!rel || rel.startsWith("..") || path25.isAbsolute(rel)) return true;
|
|
9569
|
+
const posixRel = rel.split(path25.sep).join("/");
|
|
9263
9570
|
return this.ignoreMatcher.ignores(posixRel);
|
|
9264
9571
|
}
|
|
9265
9572
|
};
|
|
@@ -9376,6 +9683,9 @@ var McpClient = class _McpClient {
|
|
|
9376
9683
|
return this.clients.has(serverName);
|
|
9377
9684
|
}
|
|
9378
9685
|
};
|
|
9686
|
+
|
|
9687
|
+
// src/core/cascade.ts
|
|
9688
|
+
init_audit_logger();
|
|
9379
9689
|
var SAFE_TOOLS = /* @__PURE__ */ new Set([
|
|
9380
9690
|
"file_read",
|
|
9381
9691
|
"file_list",
|
|
@@ -9759,9 +10069,10 @@ var TaskAnalyzer = class {
|
|
|
9759
10069
|
analysisCache.clear();
|
|
9760
10070
|
}
|
|
9761
10071
|
};
|
|
9762
|
-
var DEFAULT_STATS_FILE =
|
|
10072
|
+
var DEFAULT_STATS_FILE = path25.join(os8.homedir(), ".cascade", "model-perf.json");
|
|
9763
10073
|
var ModelPerformanceTracker = class {
|
|
9764
10074
|
stats = /* @__PURE__ */ new Map();
|
|
10075
|
+
featureStats = /* @__PURE__ */ new Map();
|
|
9765
10076
|
statsFile;
|
|
9766
10077
|
loaded = false;
|
|
9767
10078
|
constructor(statsFile = DEFAULT_STATS_FILE) {
|
|
@@ -9773,18 +10084,29 @@ var ModelPerformanceTracker = class {
|
|
|
9773
10084
|
try {
|
|
9774
10085
|
const raw = await fs9.readFile(this.statsFile, "utf-8");
|
|
9775
10086
|
const parsed = JSON.parse(raw);
|
|
9776
|
-
|
|
9777
|
-
this.stats.set(key, stat);
|
|
10087
|
+
if (parsed.models) {
|
|
10088
|
+
for (const [key, stat] of Object.entries(parsed.models)) this.stats.set(key, stat);
|
|
10089
|
+
} else {
|
|
10090
|
+
for (const [key, stat] of Object.entries(parsed)) {
|
|
10091
|
+
if (stat && typeof stat === "object" && typeof stat.successCount === "number") {
|
|
10092
|
+
this.stats.set(key, stat);
|
|
10093
|
+
}
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
if (parsed.features) {
|
|
10097
|
+
for (const [key, stat] of Object.entries(parsed.features)) this.featureStats.set(key, stat);
|
|
9778
10098
|
}
|
|
9779
10099
|
} catch {
|
|
9780
10100
|
}
|
|
9781
10101
|
}
|
|
9782
10102
|
async save() {
|
|
9783
10103
|
try {
|
|
9784
|
-
await fs9.mkdir(
|
|
9785
|
-
const
|
|
9786
|
-
|
|
9787
|
-
|
|
10104
|
+
await fs9.mkdir(path25.dirname(this.statsFile), { recursive: true });
|
|
10105
|
+
const modelsObj = {};
|
|
10106
|
+
const featuresObj = {};
|
|
10107
|
+
for (const [key, stat] of this.stats) modelsObj[key] = stat;
|
|
10108
|
+
for (const [key, stat] of this.featureStats) featuresObj[key] = stat;
|
|
10109
|
+
await fs9.writeFile(this.statsFile, JSON.stringify({ models: modelsObj, features: featuresObj }, null, 2), "utf-8");
|
|
9788
10110
|
} catch {
|
|
9789
10111
|
}
|
|
9790
10112
|
}
|
|
@@ -9805,6 +10127,13 @@ var ModelPerformanceTracker = class {
|
|
|
9805
10127
|
sampleCount: s.sampleCount + 1
|
|
9806
10128
|
});
|
|
9807
10129
|
}
|
|
10130
|
+
recordFeatureCost(featureTag, costUsd) {
|
|
10131
|
+
const s = this.featureStats.get(featureTag) ?? { totalCostUsd: 0, runCount: 0 };
|
|
10132
|
+
this.featureStats.set(featureTag, {
|
|
10133
|
+
totalCostUsd: s.totalCostUsd + costUsd,
|
|
10134
|
+
runCount: s.runCount + 1
|
|
10135
|
+
});
|
|
10136
|
+
}
|
|
9808
10137
|
/**
|
|
9809
10138
|
* Record an explicit user rating (good/bad). Counts as 3 automatic samples
|
|
9810
10139
|
* so user feedback carries significantly more weight than auto-detected outcomes.
|
|
@@ -9819,6 +10148,9 @@ var ModelPerformanceTracker = class {
|
|
|
9819
10148
|
getAll() {
|
|
9820
10149
|
return new Map(this.stats);
|
|
9821
10150
|
}
|
|
10151
|
+
getAllFeatures() {
|
|
10152
|
+
return new Map(this.featureStats);
|
|
10153
|
+
}
|
|
9822
10154
|
/**
|
|
9823
10155
|
* Returns 0.05–1.0; defaults to 0.5 (neutral prior) when no history exists.
|
|
9824
10156
|
* High retry counts penalise the score.
|
|
@@ -10183,7 +10515,7 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10183
10515
|
* any dangerous action, so a silently-reloaded tool can't act without approval. */
|
|
10184
10516
|
async loadPersistedTools() {
|
|
10185
10517
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10186
|
-
const file =
|
|
10518
|
+
const file = path25.join(this.workspacePath, ".cascade", DYNAMIC_TOOLS_FILE);
|
|
10187
10519
|
try {
|
|
10188
10520
|
const raw = await fs9.readFile(file, "utf-8");
|
|
10189
10521
|
const specs = JSON.parse(raw);
|
|
@@ -10207,8 +10539,8 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10207
10539
|
}
|
|
10208
10540
|
async persist() {
|
|
10209
10541
|
if (!this.workspacePath || !this.persistEnabled) return;
|
|
10210
|
-
const dir =
|
|
10211
|
-
const file =
|
|
10542
|
+
const dir = path25.join(this.workspacePath, ".cascade");
|
|
10543
|
+
const file = path25.join(dir, DYNAMIC_TOOLS_FILE);
|
|
10212
10544
|
try {
|
|
10213
10545
|
await fs9.mkdir(dir, { recursive: true });
|
|
10214
10546
|
await fs9.writeFile(file, JSON.stringify(Array.from(this.specs.values()), null, 2), "utf-8");
|
|
@@ -10221,6 +10553,166 @@ Required capability: ${description.slice(0, 300)}`;
|
|
|
10221
10553
|
return Array.from(this.specs.keys());
|
|
10222
10554
|
}
|
|
10223
10555
|
};
|
|
10556
|
+
var WorldStateDB = class {
|
|
10557
|
+
constructor(workspacePath, debugMode = false) {
|
|
10558
|
+
this.workspacePath = workspacePath;
|
|
10559
|
+
this.debugMode = debugMode;
|
|
10560
|
+
const cascadeDir = path25.join(workspacePath, ".cascade");
|
|
10561
|
+
if (!fs23.existsSync(cascadeDir)) {
|
|
10562
|
+
fs23.mkdirSync(cascadeDir, { recursive: true });
|
|
10563
|
+
}
|
|
10564
|
+
this.keyPath = path25.join(cascadeDir, "world_state.key");
|
|
10565
|
+
this.dbPath = path25.join(cascadeDir, "world_state.db");
|
|
10566
|
+
this.initEncryptionKey();
|
|
10567
|
+
this.db = new Database2(this.dbPath);
|
|
10568
|
+
this.db.pragma("journal_mode = WAL");
|
|
10569
|
+
this.db.exec(`
|
|
10570
|
+
CREATE TABLE IF NOT EXISTS world_state (
|
|
10571
|
+
id TEXT PRIMARY KEY,
|
|
10572
|
+
timestamp TEXT NOT NULL,
|
|
10573
|
+
worker_id TEXT NOT NULL,
|
|
10574
|
+
encrypted_payload TEXT NOT NULL
|
|
10575
|
+
)
|
|
10576
|
+
`);
|
|
10577
|
+
}
|
|
10578
|
+
workspacePath;
|
|
10579
|
+
debugMode;
|
|
10580
|
+
db;
|
|
10581
|
+
keyPath;
|
|
10582
|
+
dbPath;
|
|
10583
|
+
encryptionKey;
|
|
10584
|
+
initEncryptionKey() {
|
|
10585
|
+
if (fs23.existsSync(this.keyPath)) {
|
|
10586
|
+
this.encryptionKey = fs23.readFileSync(this.keyPath);
|
|
10587
|
+
} else {
|
|
10588
|
+
this.encryptionKey = crypto.randomBytes(32);
|
|
10589
|
+
fs23.writeFileSync(this.keyPath, this.encryptionKey);
|
|
10590
|
+
}
|
|
10591
|
+
}
|
|
10592
|
+
encrypt(text) {
|
|
10593
|
+
const iv = crypto.randomBytes(16);
|
|
10594
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10595
|
+
let encrypted = cipher.update(text, "utf8", "hex");
|
|
10596
|
+
encrypted += cipher.final("hex");
|
|
10597
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
10598
|
+
return `${iv.toString("hex")}:${authTag}:${encrypted}`;
|
|
10599
|
+
}
|
|
10600
|
+
decrypt(text) {
|
|
10601
|
+
const parts = text.split(":");
|
|
10602
|
+
if (parts.length !== 3) throw new Error("Invalid encrypted payload format");
|
|
10603
|
+
const [ivHex, authTagHex, encryptedHex] = parts;
|
|
10604
|
+
const iv = Buffer.from(ivHex, "hex");
|
|
10605
|
+
const authTag = Buffer.from(authTagHex, "hex");
|
|
10606
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", this.encryptionKey, iv);
|
|
10607
|
+
decipher.setAuthTag(authTag);
|
|
10608
|
+
let decrypted = decipher.update(encryptedHex, "hex", "utf8");
|
|
10609
|
+
decrypted += decipher.final("utf8");
|
|
10610
|
+
return decrypted;
|
|
10611
|
+
}
|
|
10612
|
+
addEntry(workerId, summary) {
|
|
10613
|
+
const id = crypto.randomUUID();
|
|
10614
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10615
|
+
const payload = JSON.stringify({ summary });
|
|
10616
|
+
const encryptedPayload = this.encrypt(payload);
|
|
10617
|
+
const stmt = this.db.prepare("INSERT INTO world_state (id, timestamp, worker_id, encrypted_payload) VALUES (?, ?, ?, ?)");
|
|
10618
|
+
stmt.run(id, timestamp, workerId, encryptedPayload);
|
|
10619
|
+
this.dumpDebugIfNeeded();
|
|
10620
|
+
return id;
|
|
10621
|
+
}
|
|
10622
|
+
getAllEntries() {
|
|
10623
|
+
const stmt = this.db.prepare("SELECT id, timestamp, worker_id, encrypted_payload FROM world_state ORDER BY timestamp ASC");
|
|
10624
|
+
const rows = stmt.all();
|
|
10625
|
+
return rows.map((row) => {
|
|
10626
|
+
try {
|
|
10627
|
+
const decrypted = this.decrypt(row.encrypted_payload);
|
|
10628
|
+
const parsed = JSON.parse(decrypted);
|
|
10629
|
+
return {
|
|
10630
|
+
id: row.id,
|
|
10631
|
+
timestamp: row.timestamp,
|
|
10632
|
+
workerId: row.worker_id,
|
|
10633
|
+
summary: parsed.summary
|
|
10634
|
+
};
|
|
10635
|
+
} catch (err) {
|
|
10636
|
+
return {
|
|
10637
|
+
id: row.id,
|
|
10638
|
+
timestamp: row.timestamp,
|
|
10639
|
+
workerId: row.worker_id,
|
|
10640
|
+
summary: "[Decryption Failed - Payload Corrupted]"
|
|
10641
|
+
};
|
|
10642
|
+
}
|
|
10643
|
+
});
|
|
10644
|
+
}
|
|
10645
|
+
getFormattedState() {
|
|
10646
|
+
const entries = this.getAllEntries();
|
|
10647
|
+
if (entries.length === 0) return "World State is currently empty.";
|
|
10648
|
+
return entries.map((e, idx) => `[${e.timestamp}] Step ${idx + 1} (${e.workerId}): ${e.summary}`).join("\n");
|
|
10649
|
+
}
|
|
10650
|
+
dumpDebugIfNeeded() {
|
|
10651
|
+
if (!this.debugMode) return;
|
|
10652
|
+
try {
|
|
10653
|
+
const dumpPath = path25.join(os8.tmpdir(), "cascade_world_state_debug.json");
|
|
10654
|
+
const entries = this.getAllEntries();
|
|
10655
|
+
fs23.writeFileSync(dumpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
10656
|
+
} catch (err) {
|
|
10657
|
+
console.error("Failed to dump debug world state", err);
|
|
10658
|
+
}
|
|
10659
|
+
}
|
|
10660
|
+
close() {
|
|
10661
|
+
this.db.close();
|
|
10662
|
+
}
|
|
10663
|
+
};
|
|
10664
|
+
var ignore3 = _ignoreModule.default ?? _ignoreModule;
|
|
10665
|
+
var PrivacyPaths = class {
|
|
10666
|
+
localOnly;
|
|
10667
|
+
hasRules;
|
|
10668
|
+
constructor(policies = []) {
|
|
10669
|
+
this.localOnly = ignore3();
|
|
10670
|
+
const patterns = policies.filter((p) => p.policy === "local-only").map((p) => p.pattern);
|
|
10671
|
+
if (patterns.length) this.localOnly.add(patterns);
|
|
10672
|
+
this.hasRules = patterns.length > 0;
|
|
10673
|
+
}
|
|
10674
|
+
/** True when any privacy rules are configured at all (cheap short-circuit). */
|
|
10675
|
+
hasPolicies() {
|
|
10676
|
+
return this.hasRules;
|
|
10677
|
+
}
|
|
10678
|
+
/** True when the given workspace-relative path falls under a local-only policy. */
|
|
10679
|
+
isLocalOnly(relativePath) {
|
|
10680
|
+
if (!this.hasRules || !relativePath) return false;
|
|
10681
|
+
try {
|
|
10682
|
+
return this.localOnly.ignores(relativePath.replace(/^\.?\//, ""));
|
|
10683
|
+
} catch {
|
|
10684
|
+
return false;
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
/** True when ANY of the given paths falls under a local-only policy. */
|
|
10688
|
+
anyLocalOnly(relativePaths) {
|
|
10689
|
+
return relativePaths.some((p) => this.isLocalOnly(p));
|
|
10690
|
+
}
|
|
10691
|
+
};
|
|
10692
|
+
|
|
10693
|
+
// src/core/steering/guidance.ts
|
|
10694
|
+
var GuidanceQueue = class {
|
|
10695
|
+
entries = [];
|
|
10696
|
+
/** Per-consumer read cursor so a broadcast entry reaches each worker once. */
|
|
10697
|
+
cursors = /* @__PURE__ */ new Map();
|
|
10698
|
+
push(text, nodeId) {
|
|
10699
|
+
const entry = { text, nodeId, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10700
|
+
this.entries.push(entry);
|
|
10701
|
+
return entry;
|
|
10702
|
+
}
|
|
10703
|
+
/**
|
|
10704
|
+
* New entries for this consumer since its last drain, filtered to entries
|
|
10705
|
+
* that target it (or target everyone). Advances the consumer's cursor.
|
|
10706
|
+
*/
|
|
10707
|
+
drain(consumerId) {
|
|
10708
|
+
const from = this.cursors.get(consumerId) ?? 0;
|
|
10709
|
+
this.cursors.set(consumerId, this.entries.length);
|
|
10710
|
+
return this.entries.slice(from).filter((e) => !e.nodeId || consumerId === e.nodeId || consumerId.startsWith(e.nodeId));
|
|
10711
|
+
}
|
|
10712
|
+
get size() {
|
|
10713
|
+
return this.entries.length;
|
|
10714
|
+
}
|
|
10715
|
+
};
|
|
10224
10716
|
|
|
10225
10717
|
// src/core/cascade.ts
|
|
10226
10718
|
var Cascade = class _Cascade extends EventEmitter {
|
|
@@ -10240,6 +10732,9 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10240
10732
|
taskAnalyzer;
|
|
10241
10733
|
perfTracker;
|
|
10242
10734
|
toolCreator;
|
|
10735
|
+
worldStateDB;
|
|
10736
|
+
encryptedAuditLogger;
|
|
10737
|
+
guidanceQueue;
|
|
10243
10738
|
workspacePath;
|
|
10244
10739
|
constructor(config, workspacePath, store) {
|
|
10245
10740
|
super();
|
|
@@ -10261,6 +10756,23 @@ var Cascade = class _Cascade extends EventEmitter {
|
|
|
10261
10756
|
});
|
|
10262
10757
|
this.toolRegistry = new ToolRegistry(this.config.tools, workspacePath);
|
|
10263
10758
|
this.telemetry = config.telemetry?.enabled ? new Telemetry(config.telemetry, config.telemetry.distinctId ?? "anonymous") : noopTelemetry;
|
|
10759
|
+
this.worldStateDB = new WorldStateDB(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10760
|
+
this.router.setWorldStateDB(this.worldStateDB);
|
|
10761
|
+
this.encryptedAuditLogger = new AuditLogger2(this.workspacePath, this.config.workspace?.debugWorldState ?? false);
|
|
10762
|
+
const privacyPolicies = this.config.privacy?.paths ?? [];
|
|
10763
|
+
if (privacyPolicies.length) this.router.setPrivacyPaths(new PrivacyPaths(privacyPolicies));
|
|
10764
|
+
this.guidanceQueue = new GuidanceQueue();
|
|
10765
|
+
this.router.setGuidanceQueue(this.guidanceQueue);
|
|
10766
|
+
}
|
|
10767
|
+
/**
|
|
10768
|
+
* Live intervention: inject a user correction into the running hierarchy.
|
|
10769
|
+
* Every active T3 worker (or only the one matching `nodeId`) picks it up at
|
|
10770
|
+
* the top of its next agent-loop iteration as a USER INTERVENTION message.
|
|
10771
|
+
*/
|
|
10772
|
+
injectGuidance(text, nodeId) {
|
|
10773
|
+
const entry = this.guidanceQueue.push(text, nodeId);
|
|
10774
|
+
this.encryptedAuditLogger?.logEvent("user_guidance", nodeId ?? "*", { text });
|
|
10775
|
+
this.emit("guidance:injected", entry);
|
|
10264
10776
|
}
|
|
10265
10777
|
initOptionalFeatures() {
|
|
10266
10778
|
if (this.config.cascadeAuto === true) {
|
|
@@ -10423,6 +10935,12 @@ ${last.partialOutput}` : "");
|
|
|
10423
10935
|
if (!prompt) return null;
|
|
10424
10936
|
return this.run({ prompt });
|
|
10425
10937
|
}
|
|
10938
|
+
getWorkspacePath() {
|
|
10939
|
+
return this.workspacePath;
|
|
10940
|
+
}
|
|
10941
|
+
getWorldStateDB() {
|
|
10942
|
+
return this.worldStateDB;
|
|
10943
|
+
}
|
|
10426
10944
|
/**
|
|
10427
10945
|
* Record an explicit user rating for the last completed run.
|
|
10428
10946
|
* Explicit ratings carry 3× the weight of auto-detected outcomes so user
|
|
@@ -10509,6 +11027,11 @@ ${last.partialOutput}` : "");
|
|
|
10509
11027
|
}
|
|
10510
11028
|
this.initOptionalFeatures();
|
|
10511
11029
|
if (this.toolCreator) await this.toolCreator.loadPersistedTools();
|
|
11030
|
+
if (this.encryptedAuditLogger) {
|
|
11031
|
+
this.on("tool:call", (e) => this.encryptedAuditLogger.logEvent("tool_call", e.tierId || "unknown", e));
|
|
11032
|
+
this.on("tool:result", (e) => this.encryptedAuditLogger.logEvent("tool_result", e.tierId || "unknown", e));
|
|
11033
|
+
this.on("tier:status", (e) => this.encryptedAuditLogger.logEvent("tier_status", e.tierId || "unknown", e));
|
|
11034
|
+
}
|
|
10512
11035
|
this.initialized = true;
|
|
10513
11036
|
})();
|
|
10514
11037
|
try {
|
|
@@ -10552,6 +11075,21 @@ ${last.partialOutput}` : "");
|
|
|
10552
11075
|
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);
|
|
10553
11076
|
return inquiry && !producesArtifact;
|
|
10554
11077
|
}
|
|
11078
|
+
/**
|
|
11079
|
+
* Strong, explicit signals that a task needs the full hierarchy (planning +
|
|
11080
|
+
* multiple sections/workers). Deliberately conservative: requires a
|
|
11081
|
+
* build/implementation verb AND either an app/system-scale noun or an
|
|
11082
|
+
* explicit multi-part structure, so ordinary single-file asks (handled as
|
|
11083
|
+
* Simple/Moderate) don't get over-escalated.
|
|
11084
|
+
*/
|
|
11085
|
+
looksClearlyComplex(prompt) {
|
|
11086
|
+
const p = prompt.trim();
|
|
11087
|
+
if (p.length < 24) return false;
|
|
11088
|
+
const buildVerb = /\b(?:build|implement|create|develop|design|scaffold|refactor|migrate|architect|set up|integrate)\b/i.test(p);
|
|
11089
|
+
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);
|
|
11090
|
+
const multiPart = /(?:\b(?:and|then|also|plus|as well as)\b.*\b(?:and|then|also)\b)|(?:^|\n)\s*(?:[-*]|\d+[.)])\s+/i.test(p);
|
|
11091
|
+
return buildVerb && (scaleNoun || multiPart);
|
|
11092
|
+
}
|
|
10555
11093
|
// Cache glob scan results per workspace path to avoid repeated I/O.
|
|
10556
11094
|
static globCache = /* @__PURE__ */ new Map();
|
|
10557
11095
|
async countWorkspaceFiles(workspacePath) {
|
|
@@ -10635,10 +11173,16 @@ ${prompt}` : prompt;
|
|
|
10635
11173
|
let verdict;
|
|
10636
11174
|
if (match) {
|
|
10637
11175
|
verdict = match[1] === "simple" ? "Simple" : match[1] === "moderate" ? "Moderate" : "Complex";
|
|
10638
|
-
|
|
11176
|
+
if (verdict !== "Complex" && this.looksClearlyComplex(prompt)) {
|
|
11177
|
+
this.recordDecision("complexity", `Complex \u2014 heuristic floor over classifier "${verdict}": explicit multi-step build/implementation signals (T1 engaged)`);
|
|
11178
|
+
verdict = "Complex";
|
|
11179
|
+
} else {
|
|
11180
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier: ${reason || "no reason given"}`);
|
|
11181
|
+
}
|
|
10639
11182
|
} else {
|
|
10640
|
-
|
|
10641
|
-
|
|
11183
|
+
const words = prompt.trim().split(/\s+/).length;
|
|
11184
|
+
verdict = words <= 12 ? "Simple" : words >= 40 || this.looksClearlyComplex(prompt) ? "Complex" : "Moderate";
|
|
11185
|
+
this.recordDecision("complexity", `${verdict} \u2014 classifier output unparseable; defaulted by length/signals`);
|
|
10642
11186
|
}
|
|
10643
11187
|
return verdict;
|
|
10644
11188
|
} catch {
|
|
@@ -10690,7 +11234,15 @@ ${prompt}` : prompt;
|
|
|
10690
11234
|
}
|
|
10691
11235
|
escalator.resolveUserDecision(req.id, approved, always);
|
|
10692
11236
|
});
|
|
10693
|
-
const
|
|
11237
|
+
const forceTier = this.config.routing?.forceTier;
|
|
11238
|
+
const forced = forceTier === "T1" ? "Complex" : forceTier === "T2" ? "Moderate" : forceTier === "T3" ? "Simple" : void 0;
|
|
11239
|
+
let complexity;
|
|
11240
|
+
if (forced) {
|
|
11241
|
+
complexity = forced;
|
|
11242
|
+
this.recordDecision("complexity", `${forced} \u2014 manually forced via routing.forceTier=${forceTier}`);
|
|
11243
|
+
} else {
|
|
11244
|
+
complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
|
|
11245
|
+
}
|
|
10694
11246
|
this.telemetry.capture("cascade:session_start", {
|
|
10695
11247
|
complexity,
|
|
10696
11248
|
providerCount: this.config.providers.length,
|
|
@@ -10770,6 +11322,7 @@ ${prompt}` : prompt;
|
|
|
10770
11322
|
try {
|
|
10771
11323
|
if (complexity === "Simple") {
|
|
10772
11324
|
const t3 = new T3Worker(this.router, this.toolRegistry, "root");
|
|
11325
|
+
t3.setPresenter(true);
|
|
10773
11326
|
t3.setHierarchyContext("You are the DIRECT worker for this task. There is no T1 Administrator or T2 Manager involved in this run.");
|
|
10774
11327
|
if (identityPrompt) {
|
|
10775
11328
|
t3.setSystemPromptOverride(identityPrompt);
|
|
@@ -10794,6 +11347,7 @@ ${prompt}` : prompt;
|
|
|
10794
11347
|
this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
|
|
10795
11348
|
} else if (complexity === "Moderate") {
|
|
10796
11349
|
const t2 = new T2Manager(this.router, this.toolRegistry, "root");
|
|
11350
|
+
t2.setPresenter(true);
|
|
10797
11351
|
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.");
|
|
10798
11352
|
if (identityPrompt) {
|
|
10799
11353
|
t2.setSystemPromptOverride(identityPrompt);
|
|
@@ -10843,6 +11397,7 @@ ${prompt}` : prompt;
|
|
|
10843
11397
|
}
|
|
10844
11398
|
} else {
|
|
10845
11399
|
const t1 = new T1Administrator(this.router, this.toolRegistry, this.config);
|
|
11400
|
+
t1.setPresenter(true);
|
|
10846
11401
|
t1.setHierarchyContext("You are the top-level Administrator. You are responsible for the overall plan and supervising multiple T2 Managers.");
|
|
10847
11402
|
if (identityPrompt) {
|
|
10848
11403
|
t1.setSystemPromptOverride(identityPrompt);
|
|
@@ -10934,6 +11489,7 @@ ${prompt}` : prompt;
|
|
|
10934
11489
|
durationMs,
|
|
10935
11490
|
costByTier: stats.costByTier,
|
|
10936
11491
|
tokensByTier: stats.tokensByTier,
|
|
11492
|
+
costByFeature: stats.costByFeature,
|
|
10937
11493
|
costPercentByTier: this.router.getTierCostPercentages()
|
|
10938
11494
|
};
|
|
10939
11495
|
}
|
|
@@ -11183,6 +11739,30 @@ var SlashCommandRegistry = class {
|
|
|
11183
11739
|
return { output: out || "File changes rolled back.", handled: true };
|
|
11184
11740
|
}
|
|
11185
11741
|
});
|
|
11742
|
+
this.register({
|
|
11743
|
+
command: "/steer",
|
|
11744
|
+
description: "Steer a running task: /steer <correction for the active workers>",
|
|
11745
|
+
args: ["<guidance>"],
|
|
11746
|
+
handler: async (args, ctx) => {
|
|
11747
|
+
const output = await ctx.onSteer(args);
|
|
11748
|
+
return { output, handled: true };
|
|
11749
|
+
}
|
|
11750
|
+
});
|
|
11751
|
+
this.register({
|
|
11752
|
+
command: "/audit",
|
|
11753
|
+
description: "Verify the tamper-evident audit log (hash chain integrity)",
|
|
11754
|
+
handler: async (_args, ctx) => {
|
|
11755
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
11756
|
+
const logger = new AuditLogger3(ctx.workspacePath);
|
|
11757
|
+
try {
|
|
11758
|
+
const v = logger.verifyChain();
|
|
11759
|
+
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.`;
|
|
11760
|
+
return { output, handled: true };
|
|
11761
|
+
} finally {
|
|
11762
|
+
logger.close();
|
|
11763
|
+
}
|
|
11764
|
+
}
|
|
11765
|
+
});
|
|
11186
11766
|
this.register({
|
|
11187
11767
|
command: "/branch",
|
|
11188
11768
|
description: "Fork current session into parallel branches",
|
|
@@ -11830,7 +12410,8 @@ function CostTracker({
|
|
|
11830
12410
|
tokensByTier,
|
|
11831
12411
|
compact = false,
|
|
11832
12412
|
savedUsd = 0,
|
|
11833
|
-
savedPct = 0
|
|
12413
|
+
savedPct = 0,
|
|
12414
|
+
costByFeature
|
|
11834
12415
|
}) {
|
|
11835
12416
|
const hasTierCost = costByTier && Object.keys(costByTier).length > 0;
|
|
11836
12417
|
const savingsLine = savedUsd > 0 ? `Saved $${savedUsd.toFixed(4)} (${savedPct}%) vs. running every call on T1` : null;
|
|
@@ -11929,6 +12510,16 @@ function CostTracker({
|
|
|
11929
12510
|
] }, tier);
|
|
11930
12511
|
})
|
|
11931
12512
|
] })
|
|
12513
|
+
] }),
|
|
12514
|
+
costByFeature && Object.keys(costByFeature).length > 0 && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
12515
|
+
/* @__PURE__ */ jsx(Text, { color: theme.colors.muted, children: "By feature/section:" }),
|
|
12516
|
+
Object.entries(costByFeature).map(([feature, cost]) => /* @__PURE__ */ jsxs(Box, { marginLeft: 2, children: [
|
|
12517
|
+
/* @__PURE__ */ jsx(Box, { width: 30, children: /* @__PURE__ */ jsx(Text, { color: theme.colors.secondary, wrap: "truncate-end", children: feature }) }),
|
|
12518
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.colors.success, children: [
|
|
12519
|
+
"$",
|
|
12520
|
+
cost.toFixed(6)
|
|
12521
|
+
] })
|
|
12522
|
+
] }, feature))
|
|
11932
12523
|
] })
|
|
11933
12524
|
] });
|
|
11934
12525
|
}
|
|
@@ -12342,7 +12933,7 @@ function replReducer(state, action) {
|
|
|
12342
12933
|
case "SET_TREE":
|
|
12343
12934
|
return { ...state, agentTree: action.tree };
|
|
12344
12935
|
case "UPDATE_COST":
|
|
12345
|
-
return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, savedUsd: action.savedUsd, savedPct: action.savedPct };
|
|
12936
|
+
return { ...state, totalTokens: action.tokens, totalCostUsd: action.costUsd, callsByProvider: action.byProvider, callsByTier: action.byTier, costByTier: action.costByTier, tokensByTier: action.tokensByTier, costByFeature: action.costByFeature, savedUsd: action.savedUsd, savedPct: action.savedPct };
|
|
12346
12937
|
case "SET_APPROVAL":
|
|
12347
12938
|
return { ...state, approvalRequest: action.request };
|
|
12348
12939
|
case "SET_EXECUTING":
|
|
@@ -12356,7 +12947,7 @@ function replReducer(state, action) {
|
|
|
12356
12947
|
case "TOGGLE_COMMS":
|
|
12357
12948
|
return { ...state, showComms: !state.showComms };
|
|
12358
12949
|
case "CLEAR":
|
|
12359
|
-
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
12950
|
+
return { ...state, messages: [], agentTree: null, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], activeTool: null };
|
|
12360
12951
|
case "CLEAR_TREE":
|
|
12361
12952
|
return { ...state, agentTree: null };
|
|
12362
12953
|
case "TOGGLE_COST":
|
|
@@ -12405,7 +12996,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12405
12996
|
const [slashIndex, setSlashIndex] = useState(0);
|
|
12406
12997
|
const [identities, setIdentities] = useState([]);
|
|
12407
12998
|
const [currentIdentityId, setCurrentIdentityId] = useState(config.defaultIdentityId);
|
|
12408
|
-
const [state, dispatch] = useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
|
|
12999
|
+
const [state, dispatch] = useReducer(replReducer, { messages: [], agentTree: null, isStreaming: false, isExecuting: false, streamBuffer: "", totalTokens: 0, totalCostUsd: 0, callsByProvider: {}, callsByTier: {}, costByTier: {}, tokensByTier: {}, costByFeature: {}, savedUsd: 0, savedPct: 0, peerEvents: [], showComms: true, approvalRequest: null, showCost: false, showDetails: false, error: null, activeTool: null });
|
|
12409
13000
|
const [isShowingModels, setIsShowingModels] = useState(false);
|
|
12410
13001
|
const [planApprovalRequest, setPlanApprovalRequest] = useState(null);
|
|
12411
13002
|
const [historyOffset, setHistoryOffset] = useState(0);
|
|
@@ -12474,9 +13065,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12474
13065
|
}
|
|
12475
13066
|
} catch {
|
|
12476
13067
|
}
|
|
12477
|
-
const configPath =
|
|
13068
|
+
const configPath = path25.join(workspacePath, ".cascade", "config.json");
|
|
12478
13069
|
try {
|
|
12479
|
-
await fs9.mkdir(
|
|
13070
|
+
await fs9.mkdir(path25.dirname(configPath), { recursive: true });
|
|
12480
13071
|
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
12481
13072
|
} catch (err) {
|
|
12482
13073
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -12528,9 +13119,9 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12528
13119
|
if (msg.includes("non-text parts")) return;
|
|
12529
13120
|
originalLog(...args);
|
|
12530
13121
|
};
|
|
12531
|
-
const store = new MemoryStore(
|
|
13122
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
12532
13123
|
storeRef.current = store;
|
|
12533
|
-
globalStoreRef.current = new MemoryStore(
|
|
13124
|
+
globalStoreRef.current = new MemoryStore(path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
|
|
12534
13125
|
const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
|
|
12535
13126
|
setIdentities(identityRows);
|
|
12536
13127
|
let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
|
|
@@ -12593,7 +13184,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName, a
|
|
|
12593
13184
|
onThemeChange: (name) => setTheme(getTheme(name)),
|
|
12594
13185
|
onExport: async (fmt) => {
|
|
12595
13186
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
12596
|
-
const exportPath =
|
|
13187
|
+
const exportPath = path25.join(workspacePath, `cascade-export-${stamp}.${fmt === "json" ? "json" : "md"}`);
|
|
12597
13188
|
if (fmt === "json") {
|
|
12598
13189
|
await fs9.writeFile(exportPath, JSON.stringify({ sessionId: sessionIdRef.current, messages: state.messages }, null, 2), "utf-8");
|
|
12599
13190
|
} else {
|
|
@@ -12617,6 +13208,14 @@ ${msg.content}`).join("\n\n");
|
|
|
12617
13208
|
}
|
|
12618
13209
|
return `Restored ${snapshots.length} files to their initial session state.`;
|
|
12619
13210
|
},
|
|
13211
|
+
onSteer: (args) => {
|
|
13212
|
+
const text = args.join(" ").trim();
|
|
13213
|
+
if (!text) return "Usage: /steer <correction for the active workers>";
|
|
13214
|
+
const cascade = cascadeRef.current;
|
|
13215
|
+
if (!cascade) return "No active Cascade instance.";
|
|
13216
|
+
cascade.injectGuidance(text);
|
|
13217
|
+
return "Guidance queued \u2014 active workers apply it on their next step. (No task running? It applies to the next run's workers.)";
|
|
13218
|
+
},
|
|
12620
13219
|
onBranch: async () => {
|
|
12621
13220
|
const store = storeRef.current;
|
|
12622
13221
|
if (!store) return;
|
|
@@ -12935,7 +13534,7 @@ ${lastUser.content}`;
|
|
|
12935
13534
|
if (lastTool !== null) dispatch({ type: "SET_ACTIVE_TOOL", toolName: lastTool });
|
|
12936
13535
|
const stats = cascade.getRouter().getStats();
|
|
12937
13536
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
12938
|
-
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13537
|
+
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
12939
13538
|
statusThrottleTimeout = null;
|
|
12940
13539
|
};
|
|
12941
13540
|
cascade.on("tier:root", onRoot);
|
|
@@ -13027,7 +13626,7 @@ ${lastUser.content}`;
|
|
|
13027
13626
|
flushPeerEvents();
|
|
13028
13627
|
const stats = cascade.getRouter().getStats();
|
|
13029
13628
|
const savings = cascade.getRouter().getDelegationSavings();
|
|
13030
|
-
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13629
|
+
dispatch({ type: "UPDATE_COST", tokens: stats.totalTokens, costUsd: stats.totalCostUsd, byProvider: stats.callsByProvider, byTier: stats.callsByTier, costByTier: stats.costByTier, tokensByTier: stats.tokensByTier, costByFeature: stats.costByFeature, savedUsd: savings.savedUsd, savedPct: savings.savedPct });
|
|
13031
13630
|
dispatch({ type: "COMMIT_STREAM", finalText: result.output, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
13032
13631
|
persistMessage("assistant", result.output, (/* @__PURE__ */ new Date()).toISOString());
|
|
13033
13632
|
const receipt = formatRunReceipt(result, stats.totalCostUsd, savings);
|
|
@@ -13312,7 +13911,7 @@ ${lastUser.content}`;
|
|
|
13312
13911
|
),
|
|
13313
13912
|
adaptiveMode === "wide" && state.showComms && liveBudget.commsMaxEvents > 0 && /* @__PURE__ */ jsx(PeerFeed, { events: state.peerEvents, theme, maxRows: liveBudget.commsMaxEvents }),
|
|
13314
13913
|
adaptiveMode === "wide" && state.showDetails && liveBudget.showTimeline && /* @__PURE__ */ jsx(TimelinePanel, { nodes: [...treeNodesRef.current.values()], theme, currentIndex: timelineIndex, onChangeIndex: setTimelineIndex }),
|
|
13315
|
-
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
|
|
13914
|
+
adaptiveMode !== "narrow" && state.showCost && /* @__PURE__ */ jsx(CostTracker, { theme, totalTokens: state.totalTokens, totalCostUsd: state.totalCostUsd, callsByProvider: state.callsByProvider, callsByTier: state.callsByTier, costByTier: state.costByTier, tokensByTier: state.tokensByTier, costByFeature: state.costByFeature, compact: liveBudget.costCompact, savedUsd: state.savedUsd, savedPct: state.savedPct }),
|
|
13316
13915
|
liveBudget.collapsed && (state.showCost || state.showDetails || state.agentTree != null) && /* @__PURE__ */ jsx(Text, { color: theme.colors.muted, dimColor: true, children: " \u25B8 panels collapsed (small terminal)" }),
|
|
13317
13916
|
state.approvalRequest && /* @__PURE__ */ jsx(ApprovalPrompt, { request: state.approvalRequest, theme, onDecision: (decision) => {
|
|
13318
13917
|
dispatch({ type: "SET_APPROVAL", request: null });
|
|
@@ -13481,7 +14080,7 @@ function stringifySlashOutput(val) {
|
|
|
13481
14080
|
}
|
|
13482
14081
|
async function searchSessionsAndMessages(query, workspacePath) {
|
|
13483
14082
|
if (!query) return "Usage: /search <query>";
|
|
13484
|
-
const dbPath =
|
|
14083
|
+
const dbPath = path25.join(workspacePath, CASCADE_DB_FILE);
|
|
13485
14084
|
try {
|
|
13486
14085
|
await fs9.access(dbPath);
|
|
13487
14086
|
} catch {
|
|
@@ -13515,7 +14114,7 @@ async function searchSessionsAndMessages(query, workspacePath) {
|
|
|
13515
14114
|
async function diagnoseRuntime(config, workspacePath) {
|
|
13516
14115
|
const providers = config.providers.map((p) => `${p.type}${p.apiKey ? " (key set)" : " (no key)"}`).join("\n");
|
|
13517
14116
|
const models = [`T1: ${config.models.t1 ?? "default"}`, `T2: ${config.models.t2 ?? "default"}`, `T3: ${config.models.t3 ?? "default"}`].join("\n");
|
|
13518
|
-
const store = new MemoryStore(
|
|
14117
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13519
14118
|
try {
|
|
13520
14119
|
const sessions = store.listSessions(void 0, 3);
|
|
13521
14120
|
return [
|
|
@@ -13534,7 +14133,7 @@ async function diagnoseRuntime(config, workspacePath) {
|
|
|
13534
14133
|
}
|
|
13535
14134
|
async function showRecentLogs(args, workspacePath) {
|
|
13536
14135
|
const limit = Number.parseInt(args[0] ?? "10", 10) || 10;
|
|
13537
|
-
const store = new MemoryStore(
|
|
14136
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13538
14137
|
try {
|
|
13539
14138
|
const logs = store.listRuntimeNodeLogs(void 0, void 0, limit);
|
|
13540
14139
|
if (!logs.length) return "No recent runtime logs.";
|
|
@@ -13546,7 +14145,7 @@ async function showRecentLogs(args, workspacePath) {
|
|
|
13546
14145
|
async function loadSessionSnapshot(args, workspacePath) {
|
|
13547
14146
|
const sessionId = args[0];
|
|
13548
14147
|
if (!sessionId) return "Usage: /resume <sessionId>";
|
|
13549
|
-
const store = new MemoryStore(
|
|
14148
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
13550
14149
|
try {
|
|
13551
14150
|
const session = store.getSession(sessionId);
|
|
13552
14151
|
if (!session) return `Session not found: ${sessionId}`;
|
|
@@ -13966,9 +14565,9 @@ function SetupWizard({ workspacePath, onComplete }) {
|
|
|
13966
14565
|
...anyAuto ? { cascadeAuto: true } : {}
|
|
13967
14566
|
};
|
|
13968
14567
|
const config = CascadeConfigSchema.parse(rawConfig);
|
|
13969
|
-
const configDir =
|
|
14568
|
+
const configDir = path25.join(workspacePath, ".cascade");
|
|
13970
14569
|
await fs9.mkdir(configDir, { recursive: true });
|
|
13971
|
-
const configPath =
|
|
14570
|
+
const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
13972
14571
|
await fs9.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
13973
14572
|
savedConfigRef.current = config;
|
|
13974
14573
|
dispatchRef.current({ type: "GO_DONE" });
|
|
@@ -14290,14 +14889,14 @@ function printTelemetryBanner() {
|
|
|
14290
14889
|
async function initCommand(workspacePath = process.cwd()) {
|
|
14291
14890
|
const spin = ora({ text: "Initializing Cascade project\u2026", color: "magenta" }).start();
|
|
14292
14891
|
try {
|
|
14293
|
-
const configDir =
|
|
14892
|
+
const configDir = path25.join(workspacePath, ".cascade");
|
|
14294
14893
|
await fs9.mkdir(configDir, { recursive: true });
|
|
14295
|
-
const mdPath =
|
|
14894
|
+
const mdPath = path25.join(workspacePath, "CASCADE.md");
|
|
14296
14895
|
if (!await fileExists(mdPath)) {
|
|
14297
14896
|
await createDefaultCascadeMd(workspacePath);
|
|
14298
14897
|
spin.succeed(chalk9.green("Created CASCADE.md"));
|
|
14299
14898
|
}
|
|
14300
|
-
const ignorePath =
|
|
14899
|
+
const ignorePath = path25.join(workspacePath, ".cascadeignore");
|
|
14301
14900
|
if (!await fileExists(ignorePath)) {
|
|
14302
14901
|
await createDefaultIgnoreFile(workspacePath);
|
|
14303
14902
|
spin.succeed(chalk9.green("Created .cascadeignore"));
|
|
@@ -14306,7 +14905,7 @@ async function initCommand(workspacePath = process.cwd()) {
|
|
|
14306
14905
|
console.log();
|
|
14307
14906
|
console.log(chalk9.magenta(" \u25C8 Cascade AI \u2014 Project initialized"));
|
|
14308
14907
|
console.log();
|
|
14309
|
-
const configPath =
|
|
14908
|
+
const configPath = path25.join(workspacePath, CASCADE_CONFIG_FILE);
|
|
14310
14909
|
if (await fileExists(configPath)) {
|
|
14311
14910
|
console.log(chalk9.yellow(" .cascade/config.json already exists \u2014 launching wizard to reconfigure."));
|
|
14312
14911
|
console.log();
|
|
@@ -14364,7 +14963,7 @@ function fromEnv(env) {
|
|
|
14364
14963
|
return out;
|
|
14365
14964
|
}
|
|
14366
14965
|
async function fromClaudeCode(home) {
|
|
14367
|
-
const file =
|
|
14966
|
+
const file = path25.join(home, ".claude", ".credentials.json");
|
|
14368
14967
|
const data = await readJson(file);
|
|
14369
14968
|
if (!data) return [];
|
|
14370
14969
|
const oauth = data["claudeAiOauth"];
|
|
@@ -14387,7 +14986,7 @@ async function fromClaudeCode(home) {
|
|
|
14387
14986
|
return [];
|
|
14388
14987
|
}
|
|
14389
14988
|
async function fromCodex(home) {
|
|
14390
|
-
const file =
|
|
14989
|
+
const file = path25.join(home, ".codex", "auth.json");
|
|
14391
14990
|
const data = await readJson(file);
|
|
14392
14991
|
if (!data) return [];
|
|
14393
14992
|
const apiKey = str(data["OPENAI_API_KEY"]);
|
|
@@ -14410,7 +15009,7 @@ async function fromCodex(home) {
|
|
|
14410
15009
|
return [];
|
|
14411
15010
|
}
|
|
14412
15011
|
async function fromGemini(home) {
|
|
14413
|
-
const file =
|
|
15012
|
+
const file = path25.join(home, ".gemini", "oauth_creds.json");
|
|
14414
15013
|
const data = await readJson(file);
|
|
14415
15014
|
const accessToken = str(data?.["access_token"]);
|
|
14416
15015
|
if (!accessToken) return [];
|
|
@@ -14426,7 +15025,7 @@ async function fromGemini(home) {
|
|
|
14426
15025
|
}
|
|
14427
15026
|
async function fromCopilot(home) {
|
|
14428
15027
|
for (const name of ["apps.json", "hosts.json"]) {
|
|
14429
|
-
const file =
|
|
15028
|
+
const file = path25.join(home, ".config", "github-copilot", name);
|
|
14430
15029
|
const data = await readJson(file);
|
|
14431
15030
|
if (!data) continue;
|
|
14432
15031
|
for (const value of Object.values(data)) {
|
|
@@ -14447,7 +15046,7 @@ async function fromCopilot(home) {
|
|
|
14447
15046
|
return [];
|
|
14448
15047
|
}
|
|
14449
15048
|
async function discoverCredentials(opts = {}) {
|
|
14450
|
-
const home = opts.homeDir ??
|
|
15049
|
+
const home = opts.homeDir ?? os8.homedir();
|
|
14451
15050
|
const env = opts.env ?? process.env;
|
|
14452
15051
|
const groups = await Promise.all([
|
|
14453
15052
|
Promise.resolve(fromEnv(env)),
|
|
@@ -14480,7 +15079,7 @@ async function doctorCommand() {
|
|
|
14480
15079
|
checks.push({
|
|
14481
15080
|
label: "Cascade config",
|
|
14482
15081
|
ok: true,
|
|
14483
|
-
detail: `Loaded ${
|
|
15082
|
+
detail: `Loaded ${path25.join(process.cwd(), CASCADE_CONFIG_FILE)}`
|
|
14484
15083
|
});
|
|
14485
15084
|
const providers = [
|
|
14486
15085
|
{ type: "anthropic", name: "Anthropic" },
|
|
@@ -14759,6 +15358,24 @@ var DashboardSocket = class {
|
|
|
14759
15358
|
});
|
|
14760
15359
|
});
|
|
14761
15360
|
}
|
|
15361
|
+
onSessionHalt(callback) {
|
|
15362
|
+
this.io.on("connection", (socket) => {
|
|
15363
|
+
socket.on("session:halt", (payload) => {
|
|
15364
|
+
if (typeof payload?.sessionId === "string") {
|
|
15365
|
+
callback(payload.sessionId);
|
|
15366
|
+
}
|
|
15367
|
+
});
|
|
15368
|
+
});
|
|
15369
|
+
}
|
|
15370
|
+
onSessionSteer(callback) {
|
|
15371
|
+
this.io.on("connection", (socket) => {
|
|
15372
|
+
socket.on("session:steer", (payload) => {
|
|
15373
|
+
if (typeof payload?.message === "string" && payload.message.trim()) {
|
|
15374
|
+
callback(payload.message.trim(), payload.sessionId, payload.nodeId);
|
|
15375
|
+
}
|
|
15376
|
+
});
|
|
15377
|
+
});
|
|
15378
|
+
}
|
|
14762
15379
|
onConfigUpdate(callback) {
|
|
14763
15380
|
this.io.on("connection", (socket) => {
|
|
14764
15381
|
socket.on("config:update", (payload) => {
|
|
@@ -14781,7 +15398,8 @@ var DashboardSocket = class {
|
|
|
14781
15398
|
socket.on("cascade:run", (payload) => {
|
|
14782
15399
|
if (typeof payload?.prompt === "string" && payload.prompt.trim()) {
|
|
14783
15400
|
const sessionId = typeof payload.sessionId === "string" && payload.sessionId.trim() ? payload.sessionId.trim() : void 0;
|
|
14784
|
-
|
|
15401
|
+
const forceTier = ["T1", "T2", "T3"].includes(payload.forceTier) ? payload.forceTier : void 0;
|
|
15402
|
+
callback(payload.prompt.trim(), payload.model ?? "auto", socket.id, sessionId, forceTier);
|
|
14785
15403
|
}
|
|
14786
15404
|
});
|
|
14787
15405
|
});
|
|
@@ -14793,7 +15411,7 @@ var DashboardSocket = class {
|
|
|
14793
15411
|
|
|
14794
15412
|
// src/dashboard/server.ts
|
|
14795
15413
|
init_constants();
|
|
14796
|
-
var __dirname$1 =
|
|
15414
|
+
var __dirname$1 = path25.dirname(fileURLToPath(import.meta.url));
|
|
14797
15415
|
var DashboardServer = class {
|
|
14798
15416
|
app;
|
|
14799
15417
|
httpServer;
|
|
@@ -14804,6 +15422,20 @@ var DashboardServer = class {
|
|
|
14804
15422
|
globalStore = null;
|
|
14805
15423
|
broadcastTimer = null;
|
|
14806
15424
|
activeSessions = /* @__PURE__ */ new Map();
|
|
15425
|
+
activeControllers = /* @__PURE__ */ new Map();
|
|
15426
|
+
/**
|
|
15427
|
+
* Run taskIds per chat session — file snapshots are keyed by the run's
|
|
15428
|
+
* taskId (see t3-worker saveSnapshot), so session rollback needs the list
|
|
15429
|
+
* of runs the session performed in this server's lifetime.
|
|
15430
|
+
*/
|
|
15431
|
+
sessionTaskIds = /* @__PURE__ */ new Map();
|
|
15432
|
+
/**
|
|
15433
|
+
* Tool-approval requests awaiting a user decision from a connected client,
|
|
15434
|
+
* keyed by the request's uuid. The desktop shows a modal on
|
|
15435
|
+
* `permission:user-required` and answers with `permission:decision`; this
|
|
15436
|
+
* map is how that answer reaches the run that's blocked on it.
|
|
15437
|
+
*/
|
|
15438
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
14807
15439
|
port;
|
|
14808
15440
|
host;
|
|
14809
15441
|
workspacePath;
|
|
@@ -14862,15 +15494,18 @@ var DashboardServer = class {
|
|
|
14862
15494
|
}
|
|
14863
15495
|
this.persistConfig();
|
|
14864
15496
|
});
|
|
14865
|
-
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId) => {
|
|
15497
|
+
this.socket.onCascadeRun(async (prompt, model, socketId, requestedSessionId, forceTier) => {
|
|
14866
15498
|
const sessionId = requestedSessionId ?? randomUUID();
|
|
15499
|
+
const abortController = new AbortController();
|
|
15500
|
+
this.activeControllers.set(sessionId, abortController);
|
|
14867
15501
|
const runPrompt = requestedSessionId ? this.buildContinuationPrompt(sessionId, prompt) : prompt;
|
|
14868
15502
|
const title = this.persistRunStart(sessionId, prompt);
|
|
14869
|
-
|
|
15503
|
+
let cfg = model !== "auto" ? { ...this.config, models: { ...this.config.models, t1: model } } : this.config;
|
|
15504
|
+
if (forceTier) cfg = { ...cfg, routing: { ...cfg.routing, forceTier } };
|
|
14870
15505
|
const cascade = new Cascade(cfg, this.workspacePath, this.store);
|
|
14871
15506
|
this.activeSessions.set(sessionId, cascade);
|
|
14872
15507
|
cascade.on("stream:token", (e) => {
|
|
14873
|
-
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
15508
|
+
this.socket.emitToSocket(socketId, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
14874
15509
|
});
|
|
14875
15510
|
cascade.on("tier:status", (e) => {
|
|
14876
15511
|
this.socket.emitToSocket(socketId, "tier:status", { sessionId, ...e });
|
|
@@ -14882,7 +15517,12 @@ var DashboardServer = class {
|
|
|
14882
15517
|
this.socket.emitPeerMessage(e);
|
|
14883
15518
|
});
|
|
14884
15519
|
try {
|
|
14885
|
-
const result = await cascade.run({
|
|
15520
|
+
const result = await cascade.run({
|
|
15521
|
+
prompt: runPrompt,
|
|
15522
|
+
signal: abortController.signal,
|
|
15523
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
15524
|
+
});
|
|
15525
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
14886
15526
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
14887
15527
|
this.socket.emitToSocket(socketId, "session:complete", { sessionId, result });
|
|
14888
15528
|
this.socket.broadcast("cost:update", {
|
|
@@ -14899,6 +15539,24 @@ var DashboardServer = class {
|
|
|
14899
15539
|
});
|
|
14900
15540
|
} finally {
|
|
14901
15541
|
this.activeSessions.delete(sessionId);
|
|
15542
|
+
this.activeControllers.delete(sessionId);
|
|
15543
|
+
this.denyPendingApprovals(sessionId);
|
|
15544
|
+
}
|
|
15545
|
+
});
|
|
15546
|
+
this.socket.onSessionHalt((sessionId) => {
|
|
15547
|
+
this.activeControllers.get(sessionId)?.abort();
|
|
15548
|
+
this.denyPendingApprovals(sessionId);
|
|
15549
|
+
});
|
|
15550
|
+
this.socket.onApprovalResponse(({ requestId, approved, always }) => {
|
|
15551
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
15552
|
+
if (!pending) return;
|
|
15553
|
+
this.pendingApprovals.delete(requestId);
|
|
15554
|
+
pending.resolve({ approved: !!approved, always: !!always });
|
|
15555
|
+
});
|
|
15556
|
+
this.socket.onSessionSteer((message, sessionId, nodeId) => {
|
|
15557
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15558
|
+
if (steered > 0) {
|
|
15559
|
+
this.socket.broadcast("session:message-injected", { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
14902
15560
|
}
|
|
14903
15561
|
});
|
|
14904
15562
|
}
|
|
@@ -14946,9 +15604,9 @@ var DashboardServer = class {
|
|
|
14946
15604
|
*/
|
|
14947
15605
|
persistConfig() {
|
|
14948
15606
|
try {
|
|
14949
|
-
const configPath =
|
|
14950
|
-
|
|
14951
|
-
|
|
15607
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
15608
|
+
fs23.mkdirSync(path25.dirname(configPath), { recursive: true });
|
|
15609
|
+
fs23.writeFileSync(configPath, JSON.stringify(this.config, null, 2), "utf-8");
|
|
14952
15610
|
} catch (err) {
|
|
14953
15611
|
console.warn(`[dashboard] Failed to persist config: ${err instanceof Error ? err.message : String(err)}`);
|
|
14954
15612
|
}
|
|
@@ -14963,15 +15621,15 @@ var DashboardServer = class {
|
|
|
14963
15621
|
resolveDashboardSecret() {
|
|
14964
15622
|
const fromConfig = this.config.dashboard.secret ?? process.env["CASCADE_DASHBOARD_SECRET"];
|
|
14965
15623
|
if (fromConfig) return fromConfig;
|
|
14966
|
-
const secretPath =
|
|
15624
|
+
const secretPath = path25.join(this.workspacePath, CASCADE_DASHBOARD_SECRET_FILE);
|
|
14967
15625
|
try {
|
|
14968
|
-
if (
|
|
14969
|
-
const existing =
|
|
15626
|
+
if (fs23.existsSync(secretPath)) {
|
|
15627
|
+
const existing = fs23.readFileSync(secretPath, "utf-8").trim();
|
|
14970
15628
|
if (existing.length >= 16) return existing;
|
|
14971
15629
|
}
|
|
14972
15630
|
const generated = randomUUID();
|
|
14973
|
-
|
|
14974
|
-
|
|
15631
|
+
fs23.mkdirSync(path25.dirname(secretPath), { recursive: true });
|
|
15632
|
+
fs23.writeFileSync(secretPath, generated, { encoding: "utf-8", mode: 384 });
|
|
14975
15633
|
if (this.config.dashboard.auth) {
|
|
14976
15634
|
console.warn(
|
|
14977
15635
|
`Dashboard auth enabled with no secret configured; persisted a generated secret to ${secretPath}. Set CASCADE_DASHBOARD_SECRET or config.dashboard.secret to override.`
|
|
@@ -14998,7 +15656,7 @@ var DashboardServer = class {
|
|
|
14998
15656
|
// ── Setup ─────────────────────────────────────
|
|
14999
15657
|
getGlobalStore() {
|
|
15000
15658
|
if (!this.globalStore) {
|
|
15001
|
-
const globalDbPath =
|
|
15659
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15002
15660
|
this.globalStore = new MemoryStore(globalDbPath);
|
|
15003
15661
|
}
|
|
15004
15662
|
return this.globalStore;
|
|
@@ -15046,6 +15704,44 @@ var DashboardServer = class {
|
|
|
15046
15704
|
}
|
|
15047
15705
|
this.persistRuntimeRow(sessionId, title, status, latestPrompt);
|
|
15048
15706
|
}
|
|
15707
|
+
/**
|
|
15708
|
+
* Route steering text into running Cascade instances. Targets the given
|
|
15709
|
+
* session, or every active session when none is specified (the desktop
|
|
15710
|
+
* usually has exactly one run in flight). Returns how many were reached.
|
|
15711
|
+
*/
|
|
15712
|
+
steerSessions(message, sessionId, nodeId) {
|
|
15713
|
+
const targets = sessionId ? [this.activeSessions.get(sessionId)].filter((c) => !!c) : [...this.activeSessions.values()];
|
|
15714
|
+
for (const cascade of targets) cascade.injectGuidance(message, nodeId);
|
|
15715
|
+
return targets.length;
|
|
15716
|
+
}
|
|
15717
|
+
recordSessionTask(sessionId, taskId) {
|
|
15718
|
+
const list = this.sessionTaskIds.get(sessionId) ?? [];
|
|
15719
|
+
list.push(taskId);
|
|
15720
|
+
this.sessionTaskIds.set(sessionId, list);
|
|
15721
|
+
}
|
|
15722
|
+
/**
|
|
15723
|
+
* Approval bridge: cascade calls this when a dangerous tool escalates to the
|
|
15724
|
+
* user. The request itself was already pushed to the client via the
|
|
15725
|
+
* `permission:user-required` forward; here we just park a resolver keyed by
|
|
15726
|
+
* the request id and wait for the client's `permission:decision` (handled in
|
|
15727
|
+
* onApprovalResponse). Never auto-approves — an unanswered request stays
|
|
15728
|
+
* pending until the client answers, the run ends, or the escalator's own
|
|
15729
|
+
* timeout denies it.
|
|
15730
|
+
*/
|
|
15731
|
+
makeApprovalCallback(sessionId) {
|
|
15732
|
+
return (request) => new Promise((resolve) => {
|
|
15733
|
+
this.pendingApprovals.set(request.id, { resolve, sessionId });
|
|
15734
|
+
});
|
|
15735
|
+
}
|
|
15736
|
+
/** Deny + clear any approvals still pending for a session (run end / abort). */
|
|
15737
|
+
denyPendingApprovals(sessionId) {
|
|
15738
|
+
for (const [id, pending] of this.pendingApprovals) {
|
|
15739
|
+
if (pending.sessionId === sessionId) {
|
|
15740
|
+
this.pendingApprovals.delete(id);
|
|
15741
|
+
pending.resolve({ approved: false, always: false });
|
|
15742
|
+
}
|
|
15743
|
+
}
|
|
15744
|
+
}
|
|
15049
15745
|
persistRuntimeRow(sessionId, title, status, latestPrompt) {
|
|
15050
15746
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
15051
15747
|
const row = { sessionId, title, workspacePath: this.workspacePath, status, startedAt: now, updatedAt: now, latestPrompt, isGlobal: false };
|
|
@@ -15139,12 +15835,12 @@ ${prompt}`;
|
|
|
15139
15835
|
}
|
|
15140
15836
|
}
|
|
15141
15837
|
watchRuntimeChanges() {
|
|
15142
|
-
const workspaceDbPath =
|
|
15143
|
-
const globalDbPath =
|
|
15838
|
+
const workspaceDbPath = path25.join(this.workspacePath, CASCADE_DB_FILE);
|
|
15839
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15144
15840
|
const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
|
|
15145
15841
|
for (const watchPath of watchPaths) {
|
|
15146
|
-
if (!
|
|
15147
|
-
|
|
15842
|
+
if (!fs23.existsSync(watchPath)) continue;
|
|
15843
|
+
fs23.watchFile(watchPath, { interval: 3e3 }, () => {
|
|
15148
15844
|
this.throttledBroadcast(watchPath === globalDbPath ? "global" : "workspace");
|
|
15149
15845
|
});
|
|
15150
15846
|
}
|
|
@@ -15234,15 +15930,6 @@ ${prompt}`;
|
|
|
15234
15930
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:halt", payload);
|
|
15235
15931
|
res.json({ success: true, ...payload });
|
|
15236
15932
|
});
|
|
15237
|
-
this.app.post("/api/approve", auth, mutationLimiter, (req, res) => {
|
|
15238
|
-
const body = req.body;
|
|
15239
|
-
const sessionId = typeof body["sessionId"] === "string" ? body["sessionId"] : void 0;
|
|
15240
|
-
const nodeId = typeof body["nodeId"] === "string" ? body["nodeId"] : void 0;
|
|
15241
|
-
const payload = { sessionId, nodeId, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15242
|
-
this.socket.broadcast("session:approve", payload);
|
|
15243
|
-
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:approve", payload);
|
|
15244
|
-
res.json({ success: true, ...payload });
|
|
15245
|
-
});
|
|
15246
15933
|
this.app.post("/api/inject", auth, mutationLimiter, (req, res) => {
|
|
15247
15934
|
const body = req.body;
|
|
15248
15935
|
const message = typeof body["message"] === "string" ? body["message"] : void 0;
|
|
@@ -15252,7 +15939,8 @@ ${prompt}`;
|
|
|
15252
15939
|
res.status(400).json({ error: "message is required and must be a string" });
|
|
15253
15940
|
return;
|
|
15254
15941
|
}
|
|
15255
|
-
const
|
|
15942
|
+
const steered = this.steerSessions(message, sessionId, nodeId);
|
|
15943
|
+
const payload = { sessionId, nodeId, message, steered, requestedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
15256
15944
|
this.socket.broadcast("session:message-injected", payload);
|
|
15257
15945
|
if (sessionId) this.socket.broadcastToRoom(`session:${sessionId}`, "session:message-injected", payload);
|
|
15258
15946
|
res.json({ success: true, ...payload });
|
|
@@ -15274,7 +15962,7 @@ ${prompt}`;
|
|
|
15274
15962
|
const sessionId = req.params.id;
|
|
15275
15963
|
this.store.deleteSession(sessionId);
|
|
15276
15964
|
this.store.deleteRuntimeSession(sessionId);
|
|
15277
|
-
const globalDbPath =
|
|
15965
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15278
15966
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15279
15967
|
try {
|
|
15280
15968
|
globalStore.deleteRuntimeSession(sessionId);
|
|
@@ -15286,9 +15974,34 @@ ${prompt}`;
|
|
|
15286
15974
|
this.socket.broadcast("runtime:refresh", { scope: "global" });
|
|
15287
15975
|
res.json({ ok: true });
|
|
15288
15976
|
});
|
|
15977
|
+
this.app.post("/api/sessions/:id/rollback", auth, mutationLimiter, async (req, res) => {
|
|
15978
|
+
const sessionId = req.params.id;
|
|
15979
|
+
const taskIds = this.sessionTaskIds.get(sessionId) ?? [];
|
|
15980
|
+
if (!taskIds.length) {
|
|
15981
|
+
res.json({ ok: true, restored: 0, message: "No file snapshots recorded for this session in the current app run." });
|
|
15982
|
+
return;
|
|
15983
|
+
}
|
|
15984
|
+
const toRestore = /* @__PURE__ */ new Map();
|
|
15985
|
+
for (const taskId of taskIds) {
|
|
15986
|
+
for (const { filePath, content } of this.store.getLatestFileSnapshots(taskId)) {
|
|
15987
|
+
if (!toRestore.has(filePath)) toRestore.set(filePath, content);
|
|
15988
|
+
}
|
|
15989
|
+
}
|
|
15990
|
+
const { writeFile } = await import('fs/promises');
|
|
15991
|
+
let restored = 0;
|
|
15992
|
+
for (const [filePath, content] of toRestore) {
|
|
15993
|
+
try {
|
|
15994
|
+
await writeFile(filePath, content, "utf-8");
|
|
15995
|
+
restored++;
|
|
15996
|
+
} catch (err) {
|
|
15997
|
+
console.warn(`[dashboard] rollback restore failed: ${filePath}`, err);
|
|
15998
|
+
}
|
|
15999
|
+
}
|
|
16000
|
+
res.json({ ok: true, restored });
|
|
16001
|
+
});
|
|
15289
16002
|
this.app.delete("/api/sessions", auth, (req, res) => {
|
|
15290
16003
|
const body = req.body;
|
|
15291
|
-
const globalDbPath =
|
|
16004
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15292
16005
|
if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
|
|
15293
16006
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15294
16007
|
try {
|
|
@@ -15311,7 +16024,7 @@ ${prompt}`;
|
|
|
15311
16024
|
});
|
|
15312
16025
|
this.app.delete("/api/runtime", auth, (_req, res) => {
|
|
15313
16026
|
this.store.deleteAllRuntimeNodes();
|
|
15314
|
-
const globalDbPath =
|
|
16027
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15315
16028
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15316
16029
|
try {
|
|
15317
16030
|
globalStore.deleteAllRuntimeNodes();
|
|
@@ -15362,6 +16075,19 @@ ${prompt}`;
|
|
|
15362
16075
|
this.store.deleteIdentity(req.params.id);
|
|
15363
16076
|
res.json({ ok: true });
|
|
15364
16077
|
});
|
|
16078
|
+
this.app.get("/api/audit/verify", auth, async (_req, res) => {
|
|
16079
|
+
try {
|
|
16080
|
+
const { AuditLogger: AuditLogger3 } = await Promise.resolve().then(() => (init_audit_logger(), audit_logger_exports));
|
|
16081
|
+
const logger = new AuditLogger3(this.workspacePath);
|
|
16082
|
+
try {
|
|
16083
|
+
res.json(logger.verifyChain());
|
|
16084
|
+
} finally {
|
|
16085
|
+
logger.close();
|
|
16086
|
+
}
|
|
16087
|
+
} catch (err) {
|
|
16088
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
16089
|
+
}
|
|
16090
|
+
});
|
|
15365
16091
|
this.app.get("/api/audit/:sessionId", auth, (req, res) => {
|
|
15366
16092
|
const log = this.store.getAuditLog(req.params.sessionId);
|
|
15367
16093
|
res.json(log);
|
|
@@ -15384,12 +16110,12 @@ ${prompt}`;
|
|
|
15384
16110
|
if (body["tierLimits"]) this.config.tierLimits = { ...this.config.tierLimits, ...body["tierLimits"] };
|
|
15385
16111
|
if (body["budget"]) this.config.budget = { ...this.config.budget, ...body["budget"] };
|
|
15386
16112
|
try {
|
|
15387
|
-
const configPath =
|
|
15388
|
-
const existing =
|
|
16113
|
+
const configPath = path25.join(this.workspacePath, CASCADE_CONFIG_FILE);
|
|
16114
|
+
const existing = fs23.existsSync(configPath) ? JSON.parse(fs23.readFileSync(configPath, "utf-8")) : {};
|
|
15389
16115
|
const updated = { ...existing, tierLimits: this.config.tierLimits, budget: this.config.budget };
|
|
15390
16116
|
const tmp = configPath + ".tmp";
|
|
15391
|
-
|
|
15392
|
-
|
|
16117
|
+
fs23.writeFileSync(tmp, JSON.stringify(updated, null, 2), "utf-8");
|
|
16118
|
+
fs23.renameSync(tmp, configPath);
|
|
15393
16119
|
res.json({ ok: true });
|
|
15394
16120
|
} catch (err) {
|
|
15395
16121
|
res.status(500).json({ error: `Failed to save config: ${err instanceof Error ? err.message : String(err)}` });
|
|
@@ -15417,7 +16143,7 @@ ${prompt}`;
|
|
|
15417
16143
|
this.app.get("/api/runtime", auth, (req, res) => {
|
|
15418
16144
|
const scope = req.query["scope"] ?? "workspace";
|
|
15419
16145
|
if (scope === "global") {
|
|
15420
|
-
const globalDbPath =
|
|
16146
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
|
|
15421
16147
|
const globalStore = new MemoryStore(globalDbPath);
|
|
15422
16148
|
try {
|
|
15423
16149
|
res.json({
|
|
@@ -15454,7 +16180,7 @@ ${prompt}`;
|
|
|
15454
16180
|
const cascade = new Cascade(this.config, this.workspacePath, this.store);
|
|
15455
16181
|
this.activeSessions.set(sessionId, cascade);
|
|
15456
16182
|
cascade.on("stream:token", (e) => {
|
|
15457
|
-
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text });
|
|
16183
|
+
this.socket.broadcastToRoom(`session:${sessionId}`, "stream:token", { sessionId, tierId: e.tierId, text: e.text, primary: e.primary });
|
|
15458
16184
|
});
|
|
15459
16185
|
cascade.on("tier:status", (e) => {
|
|
15460
16186
|
this.socket.broadcastToRoom(`session:${sessionId}`, "tier:status", { sessionId, ...e });
|
|
@@ -15466,7 +16192,12 @@ ${prompt}`;
|
|
|
15466
16192
|
this.socket.emitPeerMessage(e);
|
|
15467
16193
|
});
|
|
15468
16194
|
try {
|
|
15469
|
-
const result = await cascade.run({
|
|
16195
|
+
const result = await cascade.run({
|
|
16196
|
+
prompt: runPrompt,
|
|
16197
|
+
identityId: body.identityId,
|
|
16198
|
+
approvalCallback: this.makeApprovalCallback(sessionId)
|
|
16199
|
+
});
|
|
16200
|
+
this.recordSessionTask(sessionId, result.taskId);
|
|
15470
16201
|
this.persistRunEnd(sessionId, title, prompt, result.output, "COMPLETED");
|
|
15471
16202
|
this.socket.broadcast("cost:update", {
|
|
15472
16203
|
sessionId,
|
|
@@ -15483,6 +16214,7 @@ ${prompt}`;
|
|
|
15483
16214
|
});
|
|
15484
16215
|
} finally {
|
|
15485
16216
|
this.activeSessions.delete(sessionId);
|
|
16217
|
+
this.denyPendingApprovals(sessionId);
|
|
15486
16218
|
}
|
|
15487
16219
|
})();
|
|
15488
16220
|
});
|
|
@@ -15497,13 +16229,13 @@ ${prompt}`;
|
|
|
15497
16229
|
}))
|
|
15498
16230
|
});
|
|
15499
16231
|
});
|
|
15500
|
-
const prodPath =
|
|
15501
|
-
const devPath =
|
|
15502
|
-
const webDistPath =
|
|
15503
|
-
if (
|
|
16232
|
+
const prodPath = path25.resolve(__dirname$1, "../web/dist");
|
|
16233
|
+
const devPath = path25.resolve(__dirname$1, "../../web/dist");
|
|
16234
|
+
const webDistPath = fs23.existsSync(prodPath) ? prodPath : devPath;
|
|
16235
|
+
if (fs23.existsSync(webDistPath)) {
|
|
15504
16236
|
this.app.use(express.static(webDistPath));
|
|
15505
16237
|
this.app.get("*", (_req, res) => {
|
|
15506
|
-
res.sendFile(
|
|
16238
|
+
res.sendFile(path25.join(webDistPath, "index.html"));
|
|
15507
16239
|
});
|
|
15508
16240
|
} else {
|
|
15509
16241
|
this.app.get("/", (_req, res) => {
|
|
@@ -15524,7 +16256,7 @@ init_constants();
|
|
|
15524
16256
|
async function dashboardCommand(config, workspacePath = process.cwd()) {
|
|
15525
16257
|
const port = config.dashboard.port ?? DEFAULT_DASHBOARD_PORT;
|
|
15526
16258
|
const spin = ora({ text: `Starting dashboard on port ${port}\u2026`, color: "magenta" }).start();
|
|
15527
|
-
const store = new MemoryStore(
|
|
16259
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15528
16260
|
const server = new DashboardServer(config, store, workspacePath);
|
|
15529
16261
|
server.watchRuntimeChanges();
|
|
15530
16262
|
const gThis = globalThis;
|
|
@@ -15556,7 +16288,7 @@ init_constants();
|
|
|
15556
16288
|
function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
15557
16289
|
const identity = new Command("identity").alias("id").description("Manage Cascade identities");
|
|
15558
16290
|
identity.command("list").description("List all available identities").action(() => {
|
|
15559
|
-
const store = new MemoryStore(
|
|
16291
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15560
16292
|
try {
|
|
15561
16293
|
const identities = store.listIdentities();
|
|
15562
16294
|
console.log(chalk9.bold("\n Identities:"));
|
|
@@ -15576,7 +16308,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15576
16308
|
}
|
|
15577
16309
|
});
|
|
15578
16310
|
identity.command("create <name>").description("Create a new identity").option("-d, --desc <text>", "Description of the identity").option("-s, --system <text>", "System prompt").option("--default", "Set as default identity").action((name, options) => {
|
|
15579
|
-
const store = new MemoryStore(
|
|
16311
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15580
16312
|
try {
|
|
15581
16313
|
if (options.default) {
|
|
15582
16314
|
const existingDefault = store.getDefaultIdentity();
|
|
@@ -15602,7 +16334,7 @@ function makeIdentityCommand(workspacePath = process.cwd()) {
|
|
|
15602
16334
|
}
|
|
15603
16335
|
});
|
|
15604
16336
|
identity.command("set-default <name>").description("Set an identity as default by name or ID").action((query) => {
|
|
15605
|
-
const store = new MemoryStore(
|
|
16337
|
+
const store = new MemoryStore(path25.join(workspacePath, CASCADE_DB_FILE));
|
|
15606
16338
|
try {
|
|
15607
16339
|
const identities = store.listIdentities();
|
|
15608
16340
|
const match = identities.find((i) => i.id === query || i.name.toLowerCase() === query.toLowerCase());
|
|
@@ -15730,8 +16462,8 @@ async function exportCommand(options = {}) {
|
|
|
15730
16462
|
let store;
|
|
15731
16463
|
try {
|
|
15732
16464
|
const workspacePath = options.workspacePath ?? process.cwd();
|
|
15733
|
-
const workspaceDbPath =
|
|
15734
|
-
const globalDbPath =
|
|
16465
|
+
const workspaceDbPath = path25.join(workspacePath, CASCADE_DB_FILE);
|
|
16466
|
+
const globalDbPath = path25.join(os8.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
|
|
15735
16467
|
let dbPath = globalDbPath;
|
|
15736
16468
|
try {
|
|
15737
16469
|
await fs9.access(workspaceDbPath);
|
|
@@ -15773,7 +16505,7 @@ async function exportCommand(options = {}) {
|
|
|
15773
16505
|
const ext = format === "json" ? ".json" : ".md";
|
|
15774
16506
|
const safeName = session.title.replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").slice(0, 60);
|
|
15775
16507
|
const defaultFile = `cascade-export-${safeName}${ext}`;
|
|
15776
|
-
const outPath = options.output ?
|
|
16508
|
+
const outPath = options.output ? path25.resolve(options.output) : path25.join(process.cwd(), defaultFile);
|
|
15777
16509
|
await fs9.writeFile(outPath, content, "utf-8");
|
|
15778
16510
|
spin.succeed(chalk9.green(`Exported to ${chalk9.white(outPath)}`));
|
|
15779
16511
|
const messageCount = Array.isArray(session.messages) ? session.messages.length : 0;
|
|
@@ -16001,11 +16733,11 @@ async function statsCommand() {
|
|
|
16001
16733
|
dotenv.config();
|
|
16002
16734
|
function warnIfBuildIsStale() {
|
|
16003
16735
|
try {
|
|
16004
|
-
const here =
|
|
16736
|
+
const here = path25.dirname(fileURLToPath(import.meta.url));
|
|
16005
16737
|
for (const rel of ["..", "../.."]) {
|
|
16006
|
-
const pkgPath =
|
|
16007
|
-
if (!
|
|
16008
|
-
const pkg = JSON.parse(
|
|
16738
|
+
const pkgPath = path25.join(here, rel, "package.json");
|
|
16739
|
+
if (!fs23.existsSync(pkgPath)) continue;
|
|
16740
|
+
const pkg = JSON.parse(fs23.readFileSync(pkgPath, "utf-8"));
|
|
16009
16741
|
if (pkg.name !== "cascade-ai") continue;
|
|
16010
16742
|
if (pkg.version && pkg.version !== CASCADE_VERSION) {
|
|
16011
16743
|
console.error(
|