cascade-ai 0.2.2 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -11,7 +11,7 @@ var chalk8 = require('chalk');
11
11
  var dotenv = require('dotenv');
12
12
  var fs7 = require('fs/promises');
13
13
  var path17 = require('path');
14
- var os = require('os');
14
+ var os3 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var fs14 = require('fs');
17
17
  var _ignoreModule = require('ignore');
@@ -20,6 +20,7 @@ var zod = require('zod');
20
20
  var child_process = require('child_process');
21
21
  var jsxRuntime = require('react/jsx-runtime');
22
22
  var EventEmitter = require('events');
23
+ var glob = require('glob');
23
24
  var util = require('util');
24
25
  var simpleGit = require('simple-git');
25
26
  var PDFDocument = require('pdfkit');
@@ -68,7 +69,7 @@ var chalk8__default = /*#__PURE__*/_interopDefault(chalk8);
68
69
  var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
69
70
  var fs7__default = /*#__PURE__*/_interopDefault(fs7);
70
71
  var path17__default = /*#__PURE__*/_interopDefault(path17);
71
- var os__default = /*#__PURE__*/_interopDefault(os);
72
+ var os3__default = /*#__PURE__*/_interopDefault(os3);
72
73
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
73
74
  var fs14__default = /*#__PURE__*/_interopDefault(fs14);
74
75
  var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
@@ -129,7 +130,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
129
130
  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;
130
131
  var init_constants = __esm({
131
132
  "src/constants.ts"() {
132
- CASCADE_VERSION = "0.2.2";
133
+ CASCADE_VERSION = "0.2.11";
133
134
  CASCADE_CONFIG_FILE = ".cascade/config.json";
134
135
  CASCADE_DB_FILE = ".cascade/memory.db";
135
136
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -424,7 +425,8 @@ var init_constants = __esm({
424
425
  IMAGE_ANALYZE: "image_analyze",
425
426
  PDF_CREATE: "pdf_create",
426
427
  RUN_CODE: "run_code",
427
- PEER_MESSAGE: "peer_message"
428
+ PEER_MESSAGE: "peer_message",
429
+ WEB_SEARCH: "web_search"
428
430
  };
429
431
  DEFAULT_APPROVAL_REQUIRED = [
430
432
  TOOL_NAMES.SHELL,
@@ -638,33 +640,61 @@ var init_anthropic = __esm({
638
640
  }
639
641
  }
640
642
  convertMessages(messages) {
641
- return messages.filter((m) => m.role !== "system").map((m) => {
642
- if (typeof m.content === "string") {
643
- return { role: m.role, content: m.content };
643
+ const result = [];
644
+ for (const m of messages) {
645
+ if (m.role === "system") continue;
646
+ if (m.role === "tool") {
647
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
648
+ result.push({
649
+ role: "user",
650
+ content: [{
651
+ type: "tool_result",
652
+ tool_use_id: m.toolCallId ?? "",
653
+ content: toolContent
654
+ }]
655
+ });
656
+ continue;
644
657
  }
645
- const content = m.content.map((block) => {
646
- if (block.type === "text") return { type: "text", text: block.text };
647
- if (block.type === "image") {
648
- const img = block.image;
649
- if (img.type === "base64") {
650
- return {
651
- type: "image",
652
- source: {
653
- type: "base64",
654
- media_type: img.mimeType,
655
- data: img.data
658
+ if (m.role === "assistant") {
659
+ const content = [];
660
+ const text = typeof m.content === "string" ? m.content : "";
661
+ if (text) content.push({ type: "text", text });
662
+ for (const tc of m.toolCalls ?? []) {
663
+ content.push({
664
+ type: "tool_use",
665
+ id: tc.id,
666
+ name: tc.name,
667
+ input: tc.input
668
+ });
669
+ }
670
+ if (content.length > 0) {
671
+ result.push({ role: "assistant", content });
672
+ }
673
+ continue;
674
+ }
675
+ if (m.role === "user") {
676
+ if (typeof m.content === "string") {
677
+ result.push({ role: "user", content: m.content });
678
+ } else {
679
+ const content = m.content.map((block) => {
680
+ if (block.type === "text") return { type: "text", text: block.text };
681
+ if (block.type === "image") {
682
+ const img = block.image;
683
+ if (img.type === "base64") {
684
+ return {
685
+ type: "image",
686
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
687
+ };
656
688
  }
657
- };
658
- }
659
- return {
660
- type: "image",
661
- source: { type: "url", url: img.data }
662
- };
689
+ return { type: "image", source: { type: "url", url: img.data } };
690
+ }
691
+ return { type: "text", text: "" };
692
+ });
693
+ result.push({ role: "user", content });
663
694
  }
664
- return { type: "text", text: "" };
665
- });
666
- return { role: m.role, content };
667
- });
695
+ }
696
+ }
697
+ return result;
668
698
  }
669
699
  };
670
700
  }
@@ -962,7 +992,7 @@ var init_gemini = __esm({
962
992
  for (const part of candidate?.content?.parts ?? []) {
963
993
  if (part.functionCall) {
964
994
  toolCalls.push({
965
- id: `gemini-tool-${Date.now()}-${toolCalls.length}`,
995
+ id: part.functionCall.name,
966
996
  name: part.functionCall.name,
967
997
  input: part.functionCall.args ?? {}
968
998
  });
@@ -1050,10 +1080,70 @@ var init_gemini = __esm({
1050
1080
  }
1051
1081
  // ── Private ──────────────────────────────────
1052
1082
  buildContents(messages, extraImages) {
1053
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
1054
- role: m.role === "assistant" ? "model" : "user",
1055
- parts: typeof m.content === "string" ? [{ text: m.content }] : this.convertMessageContent(m, extraImages)
1056
- }));
1083
+ const contents = [];
1084
+ for (const m of messages) {
1085
+ if (m.role === "system") {
1086
+ const text = typeof m.content === "string" ? m.content : "";
1087
+ if (!text.trim()) continue;
1088
+ const prev = contents[contents.length - 1];
1089
+ if (prev?.role === "user") {
1090
+ prev.parts.unshift({ text: `[System context]: ${text}
1091
+
1092
+ ` });
1093
+ } else {
1094
+ contents.push({ role: "user", parts: [{ text: `[System context]: ${text}` }] });
1095
+ }
1096
+ continue;
1097
+ }
1098
+ if (m.role === "tool") {
1099
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1100
+ const functionName = m.toolCallId ?? "unknown_function";
1101
+ contents.push({
1102
+ role: "user",
1103
+ parts: [{
1104
+ functionResponse: {
1105
+ name: functionName,
1106
+ response: { output: toolContent }
1107
+ }
1108
+ }]
1109
+ });
1110
+ continue;
1111
+ }
1112
+ if (m.role === "assistant") {
1113
+ const parts = [];
1114
+ const textContent = typeof m.content === "string" ? m.content : "";
1115
+ if (textContent) parts.push({ text: textContent });
1116
+ for (const tc of m.toolCalls ?? []) {
1117
+ parts.push({
1118
+ functionCall: {
1119
+ name: tc.name,
1120
+ args: tc.input
1121
+ }
1122
+ });
1123
+ }
1124
+ if (parts.length > 0) {
1125
+ contents.push({ role: "model", parts });
1126
+ }
1127
+ continue;
1128
+ }
1129
+ if (m.role === "user") {
1130
+ const parts = this.convertMessageContent(m, contents.length === 0 ? extraImages : void 0);
1131
+ if (extraImages?.length && contents.length > 0) {
1132
+ const isLastUser = !messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
1133
+ if (isLastUser) {
1134
+ for (const img of extraImages) {
1135
+ if (img.type === "base64") {
1136
+ parts.push({ inlineData: { mimeType: img.mimeType, data: img.data } });
1137
+ }
1138
+ }
1139
+ }
1140
+ }
1141
+ if (parts.length > 0) {
1142
+ contents.push({ role: "user", parts });
1143
+ }
1144
+ }
1145
+ }
1146
+ return contents;
1057
1147
  }
1058
1148
  convertMessageContent(msg, extraImages) {
1059
1149
  const parts = [];
@@ -1615,9 +1705,10 @@ var MemoryStore = class _MemoryStore {
1615
1705
  constructor(dbPath) {
1616
1706
  fs14__default.default.mkdirSync(path17__default.default.dirname(dbPath), { recursive: true });
1617
1707
  try {
1618
- this.db = new Database__default.default(dbPath);
1708
+ this.db = new Database__default.default(dbPath, { timeout: 5e3 });
1619
1709
  this.db.pragma("journal_mode = WAL");
1620
1710
  this.db.pragma("foreign_keys = ON");
1711
+ this.db.pragma("synchronous = NORMAL");
1621
1712
  this.migrate();
1622
1713
  } catch (err) {
1623
1714
  if (err instanceof Error && err.message.includes("Could not locate the bindings file")) {
@@ -1631,6 +1722,38 @@ Original error: ${err.message}`
1631
1722
  throw err;
1632
1723
  }
1633
1724
  }
1725
+ // ── Async Write Queue ─────────────────────────
1726
+ writeQueue = [];
1727
+ isProcessingQueue = false;
1728
+ async processQueue() {
1729
+ if (this.isProcessingQueue) return;
1730
+ this.isProcessingQueue = true;
1731
+ while (this.writeQueue.length > 0) {
1732
+ const op = this.writeQueue.shift();
1733
+ if (op) {
1734
+ let attempts = 0;
1735
+ while (attempts < 5) {
1736
+ try {
1737
+ op();
1738
+ break;
1739
+ } catch (err) {
1740
+ if (err instanceof Error && err.code === "SQLITE_BUSY") {
1741
+ attempts++;
1742
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempts)));
1743
+ } else {
1744
+ console.error("Cascade AI: DB Write Error:", err);
1745
+ break;
1746
+ }
1747
+ }
1748
+ }
1749
+ }
1750
+ }
1751
+ this.isProcessingQueue = false;
1752
+ }
1753
+ enqueueWrite(op) {
1754
+ this.writeQueue.push(op);
1755
+ this.processQueue().catch(console.error);
1756
+ }
1634
1757
  // ── Sessions ──────────────────────────────────
1635
1758
  createSession(session) {
1636
1759
  this.db.prepare(`
@@ -1717,26 +1840,28 @@ Original error: ${err.message}`
1717
1840
  }
1718
1841
  // ── Runtime Sessions / Nodes ─────────────────
1719
1842
  upsertRuntimeSession(session) {
1720
- this.db.prepare(`
1721
- INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1722
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1723
- ON CONFLICT(session_id) DO UPDATE SET
1724
- title = excluded.title,
1725
- workspace_path = excluded.workspace_path,
1726
- status = excluded.status,
1727
- updated_at = excluded.updated_at,
1728
- latest_prompt = excluded.latest_prompt,
1729
- is_global = excluded.is_global
1730
- `).run(
1731
- session.sessionId,
1732
- session.title,
1733
- session.workspacePath,
1734
- session.status,
1735
- session.startedAt,
1736
- session.updatedAt,
1737
- session.latestPrompt ?? null,
1738
- session.isGlobal ? 1 : 0
1739
- );
1843
+ this.enqueueWrite(() => {
1844
+ this.db.prepare(`
1845
+ INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1846
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1847
+ ON CONFLICT(session_id) DO UPDATE SET
1848
+ title = excluded.title,
1849
+ workspace_path = excluded.workspace_path,
1850
+ status = excluded.status,
1851
+ updated_at = excluded.updated_at,
1852
+ latest_prompt = excluded.latest_prompt,
1853
+ is_global = excluded.is_global
1854
+ `).run(
1855
+ session.sessionId,
1856
+ session.title,
1857
+ session.workspacePath,
1858
+ session.status,
1859
+ session.startedAt,
1860
+ session.updatedAt,
1861
+ session.latestPrompt ?? null,
1862
+ session.isGlobal ? 1 : 0
1863
+ );
1864
+ });
1740
1865
  }
1741
1866
  listRuntimeSessions(limit = 100) {
1742
1867
  const rows = this.db.prepare(`
@@ -1754,33 +1879,35 @@ Original error: ${err.message}`
1754
1879
  }));
1755
1880
  }
1756
1881
  upsertRuntimeNode(node) {
1757
- this.db.prepare(`
1758
- INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1759
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1760
- ON CONFLICT(tier_id) DO UPDATE SET
1761
- session_id = excluded.session_id,
1762
- parent_id = excluded.parent_id,
1763
- role = excluded.role,
1764
- label = excluded.label,
1765
- status = excluded.status,
1766
- current_action = excluded.current_action,
1767
- progress_pct = excluded.progress_pct,
1768
- updated_at = excluded.updated_at,
1769
- workspace_path = excluded.workspace_path,
1770
- is_global = excluded.is_global
1771
- `).run(
1772
- node.tierId,
1773
- node.sessionId,
1774
- node.parentId ?? null,
1775
- node.role,
1776
- node.label,
1777
- node.status,
1778
- node.currentAction ?? null,
1779
- node.progressPct ?? null,
1780
- node.updatedAt,
1781
- node.workspacePath ?? null,
1782
- node.isGlobal ? 1 : 0
1783
- );
1882
+ this.enqueueWrite(() => {
1883
+ this.db.prepare(`
1884
+ INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1885
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1886
+ ON CONFLICT(tier_id) DO UPDATE SET
1887
+ session_id = excluded.session_id,
1888
+ parent_id = excluded.parent_id,
1889
+ role = excluded.role,
1890
+ label = excluded.label,
1891
+ status = excluded.status,
1892
+ current_action = excluded.current_action,
1893
+ progress_pct = excluded.progress_pct,
1894
+ updated_at = excluded.updated_at,
1895
+ workspace_path = excluded.workspace_path,
1896
+ is_global = excluded.is_global
1897
+ `).run(
1898
+ node.tierId,
1899
+ node.sessionId,
1900
+ node.parentId ?? null,
1901
+ node.role,
1902
+ node.label,
1903
+ node.status,
1904
+ node.currentAction ?? null,
1905
+ node.progressPct ?? null,
1906
+ node.updatedAt,
1907
+ node.workspacePath ?? null,
1908
+ node.isGlobal ? 1 : 0
1909
+ );
1910
+ });
1784
1911
  }
1785
1912
  listRuntimeNodes(sessionId, limit = 500) {
1786
1913
  const rows = sessionId ? this.db.prepare(`
@@ -1803,30 +1930,32 @@ Original error: ${err.message}`
1803
1930
  }));
1804
1931
  }
1805
1932
  addRuntimeNodeLog(log) {
1806
- this.db.prepare(`
1807
- INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1808
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1809
- `).run(
1810
- log.id,
1811
- log.sessionId,
1812
- log.tierId,
1813
- log.role,
1814
- log.label,
1815
- log.status,
1816
- log.currentAction ?? null,
1817
- log.progressPct ?? null,
1818
- log.timestamp,
1819
- log.workspacePath ?? null,
1820
- log.isGlobal ? 1 : 0
1821
- );
1822
- this.db.prepare(`
1823
- DELETE FROM runtime_node_logs
1824
- WHERE id NOT IN (
1825
- SELECT id FROM runtime_node_logs
1826
- ORDER BY timestamp DESC
1827
- LIMIT 2000
1828
- )
1829
- `).run();
1933
+ this.enqueueWrite(() => {
1934
+ this.db.prepare(`
1935
+ INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1936
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1937
+ `).run(
1938
+ log.id,
1939
+ log.sessionId,
1940
+ log.tierId,
1941
+ log.role,
1942
+ log.label,
1943
+ log.status,
1944
+ log.currentAction ?? null,
1945
+ log.progressPct ?? null,
1946
+ log.timestamp,
1947
+ log.workspacePath ?? null,
1948
+ log.isGlobal ? 1 : 0
1949
+ );
1950
+ this.db.prepare(`
1951
+ DELETE FROM runtime_node_logs
1952
+ WHERE id NOT IN (
1953
+ SELECT id FROM runtime_node_logs
1954
+ ORDER BY timestamp DESC
1955
+ LIMIT 2000
1956
+ )
1957
+ `).run();
1958
+ });
1830
1959
  }
1831
1960
  listRuntimeNodeLogs(sessionId, tierId, limit = 200) {
1832
1961
  let rows;
@@ -1864,19 +1993,21 @@ Original error: ${err.message}`
1864
1993
  }
1865
1994
  // ── Messages ──────────────────────────────────
1866
1995
  addMessage(message) {
1867
- this.db.prepare(`
1868
- INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
1869
- VALUES (?, ?, ?, ?, ?, ?, ?)
1870
- `).run(
1871
- message.id,
1872
- message.sessionId,
1873
- message.role,
1874
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
1875
- message.timestamp,
1876
- message.tokens ? JSON.stringify(message.tokens) : null,
1877
- message.agentMessages ? JSON.stringify(message.agentMessages) : null
1878
- );
1879
- this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
1996
+ this.enqueueWrite(() => {
1997
+ this.db.prepare(`
1998
+ INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
1999
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2000
+ `).run(
2001
+ message.id,
2002
+ message.sessionId,
2003
+ message.role,
2004
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
2005
+ message.timestamp,
2006
+ message.tokens ? JSON.stringify(message.tokens) : null,
2007
+ message.agentMessages ? JSON.stringify(message.agentMessages) : null
2008
+ );
2009
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
2010
+ });
1880
2011
  }
1881
2012
  getSessionMessages(sessionId) {
1882
2013
  const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
@@ -1973,10 +2104,12 @@ Original error: ${err.message}`
1973
2104
  }
1974
2105
  // ── Audit Log ─────────────────────────────────
1975
2106
  addAuditEntry(entry) {
1976
- this.db.prepare(`
1977
- INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
1978
- VALUES (?, ?, ?, ?, ?, ?)
1979
- `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2107
+ this.enqueueWrite(() => {
2108
+ this.db.prepare(`
2109
+ INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
2110
+ VALUES (?, ?, ?, ?, ?, ?)
2111
+ `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2112
+ });
1980
2113
  }
1981
2114
  getAuditLog(sessionId, limit = 100) {
1982
2115
  const rows = this.db.prepare("SELECT * FROM audit_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
@@ -1991,10 +2124,12 @@ Original error: ${err.message}`
1991
2124
  }
1992
2125
  // ── File Snapshots ────────────────────────────
1993
2126
  addFileSnapshot(sessionId, filePath, content) {
1994
- this.db.prepare(`
1995
- INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
1996
- VALUES (?, ?, ?, ?, ?)
1997
- `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2127
+ this.enqueueWrite(() => {
2128
+ this.db.prepare(`
2129
+ INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
2130
+ VALUES (?, ?, ?, ?, ?)
2131
+ `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2132
+ });
1998
2133
  }
1999
2134
  getLatestFileSnapshots(sessionId) {
2000
2135
  const rows = this.db.prepare(`
@@ -2298,12 +2433,24 @@ var McpServerConfigSchema = zod.z.object({
2298
2433
  args: zod.z.array(zod.z.string()).optional(),
2299
2434
  env: zod.z.record(zod.z.string()).optional()
2300
2435
  });
2436
+ var WebSearchConfigSchema = zod.z.object({
2437
+ /** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
2438
+ searxngUrl: zod.z.string().optional(),
2439
+ /** Brave Search API key — get one at https://api.search.brave.com */
2440
+ braveApiKey: zod.z.string().optional(),
2441
+ /** Tavily API key — get one at https://tavily.com */
2442
+ tavilyApiKey: zod.z.string().optional(),
2443
+ /** Max results per search (default 5) */
2444
+ maxResults: zod.z.number().default(5)
2445
+ });
2301
2446
  var ToolsConfigSchema = zod.z.object({
2302
2447
  shellAllowlist: zod.z.array(zod.z.string()).default([]),
2303
2448
  shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
2304
2449
  requireApprovalFor: zod.z.array(zod.z.string()).default([]),
2305
2450
  browserEnabled: zod.z.boolean().default(false),
2306
- mcpServers: zod.z.array(McpServerConfigSchema).optional()
2451
+ mcpServers: zod.z.array(McpServerConfigSchema).optional(),
2452
+ /** Web search backends — at least one should be configured for best results */
2453
+ webSearch: WebSearchConfigSchema.optional()
2307
2454
  });
2308
2455
  var HookDefinitionSchema = zod.z.object({
2309
2456
  command: zod.z.string(),
@@ -2408,7 +2555,7 @@ var ConfigManager = class {
2408
2555
  globalDir;
2409
2556
  constructor(workspacePath = process.cwd()) {
2410
2557
  this.workspacePath = workspacePath;
2411
- this.globalDir = path17__default.default.join(os__default.default.homedir(), GLOBAL_CONFIG_DIR);
2558
+ this.globalDir = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR);
2412
2559
  }
2413
2560
  async load() {
2414
2561
  this.config = await this.loadConfig();
@@ -3407,6 +3554,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
3407
3554
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
3408
3555
  }
3409
3556
  };
3557
+
3558
+ // src/utils/retry.ts
3559
+ var CascadeCancelledError = class extends Error {
3560
+ constructor(reason) {
3561
+ super(reason ?? "Run was cancelled via AbortSignal");
3562
+ this.name = "CascadeCancelledError";
3563
+ }
3564
+ };
3565
+
3566
+ // src/core/tiers/base.ts
3410
3567
  var BaseTier = class extends EventEmitter__default.default {
3411
3568
  id;
3412
3569
  role;
@@ -3416,6 +3573,8 @@ var BaseTier = class extends EventEmitter__default.default {
3416
3573
  label;
3417
3574
  systemPromptOverride = "";
3418
3575
  hierarchyContext = "";
3576
+ /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
3577
+ signal;
3419
3578
  constructor(role, id, parentId) {
3420
3579
  super();
3421
3580
  this.role = role;
@@ -3478,6 +3637,18 @@ var BaseTier = class extends EventEmitter__default.default {
3478
3637
  log(message, data) {
3479
3638
  this.emit("log", { tierId: this.id, role: this.role, message, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3480
3639
  }
3640
+ /**
3641
+ * Throws `CascadeCancelledError` if the run's `AbortSignal` has fired.
3642
+ * Call this at safe checkpoints (before LLM calls, between T3 dispatches)
3643
+ * to provide a fast, clean cancellation path.
3644
+ */
3645
+ throwIfCancelled() {
3646
+ if (this.signal?.aborted) {
3647
+ throw new CascadeCancelledError(
3648
+ typeof this.signal.reason === "string" ? this.signal.reason : "Run cancelled by caller"
3649
+ );
3650
+ }
3651
+ }
3481
3652
  };
3482
3653
 
3483
3654
  // src/core/context/manager.ts
@@ -3679,6 +3850,7 @@ Rules:
3679
3850
  - Execute the subtask completely \u2014 do not stop partway through.
3680
3851
  - Use tools when needed. Ask for approval only when the tool registry requires it.
3681
3852
  - If the task asks for a file or artifact, you must actually create it in the workspace, verify that it exists, and inspect it before claiming success.
3853
+ - Use the "web_search" tool to find current information, documentation, news, or general web data.
3682
3854
  - Use the "pdf_create" tool for PDF requests.
3683
3855
  - Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.
3684
3856
  - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
@@ -3722,7 +3894,8 @@ var T3Worker = class extends BaseTier {
3722
3894
  this.store = store;
3723
3895
  this.audit = new AuditLogger(store, sessionId);
3724
3896
  }
3725
- async execute(assignment, taskId) {
3897
+ async execute(assignment, taskId, signal) {
3898
+ this.signal = signal;
3726
3899
  this.assignment = assignment;
3727
3900
  this.taskId = taskId;
3728
3901
  this.setLabel(assignment.subtaskTitle);
@@ -3872,6 +4045,7 @@ Now execute your subtask using this context where relevant.`
3872
4045
  tools = [...tools];
3873
4046
  while (iterations < MAX_ITERATIONS) {
3874
4047
  iterations++;
4048
+ this.throwIfCancelled();
3875
4049
  const options = {
3876
4050
  messages: this.context.getMessages(),
3877
4051
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -3892,21 +4066,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3892
4066
  if (requiresArtifact) {
3893
4067
  stalledArtifactIterations += 1;
3894
4068
  if (stalledArtifactIterations >= 2) {
3895
- if (this.toolCreator && stalledArtifactIterations === 2) {
3896
- const toolName = await this.toolCreator.createTool(
3897
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
3898
- this.assignment?.description ?? ""
3899
- );
3900
- if (toolName) {
3901
- tools = this.toolRegistry.getToolDefinitions();
3902
- this.sendStatusUpdate({
3903
- progressPct: 50,
3904
- currentAction: `Dynamic tool created: ${toolName}`,
3905
- status: "IN_PROGRESS"
3906
- });
3907
- this.emit("tool:created", { tierId: this.id, toolName });
3908
- continue;
3909
- }
4069
+ if (stalledArtifactIterations === 2) {
4070
+ throw new Error(`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`);
3910
4071
  }
3911
4072
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
3912
4073
  }
@@ -4066,6 +4227,9 @@ ${assignment.expectedOutput}`;
4066
4227
  const artifactPaths = this.extractArtifactPaths(assignment);
4067
4228
  if (!artifactPaths.length) return { ok: true, issues: [] };
4068
4229
  const issues = [];
4230
+ const { exec: exec4 } = await import('child_process');
4231
+ const { promisify: promisify3 } = await import('util');
4232
+ const execAsync3 = promisify3(exec4);
4069
4233
  for (const artifactPath of artifactPaths) {
4070
4234
  const absolutePath = path17__default.default.resolve(process.cwd(), artifactPath);
4071
4235
  try {
@@ -4082,9 +4246,27 @@ ${assignment.expectedOutput}`;
4082
4246
  const content = await fs7__default.default.readFile(absolutePath, "utf-8");
4083
4247
  if (!content.trim()) {
4084
4248
  issues.push(`Artifact content is empty: ${artifactPath}`);
4249
+ continue;
4085
4250
  }
4086
4251
  } else if (stat.size < 100) {
4087
4252
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4253
+ continue;
4254
+ }
4255
+ const ext = path17__default.default.extname(absolutePath).toLowerCase();
4256
+ try {
4257
+ if (ext === ".ts" || ext === ".tsx") {
4258
+ await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
4259
+ } else if (ext === ".js" || ext === ".jsx") {
4260
+ await execAsync3(`node --check ${absolutePath}`, { timeout: 1e4 });
4261
+ } else if (ext === ".py") {
4262
+ await execAsync3(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
4263
+ }
4264
+ } catch (err) {
4265
+ const stderr = err?.stderr || String(err);
4266
+ const stdout = err?.stdout || "";
4267
+ issues.push(`Semantic error in ${artifactPath}:
4268
+ ${stderr}
4269
+ ${stdout}`);
4088
4270
  }
4089
4271
  } catch {
4090
4272
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -4481,7 +4663,8 @@ var T2Manager = class extends BaseTier {
4481
4663
  });
4482
4664
  this.emit("peer-sync-received", { fromId, content });
4483
4665
  }
4484
- async execute(assignment, taskId) {
4666
+ async execute(assignment, taskId, signal) {
4667
+ this.signal = signal;
4485
4668
  this.assignment = assignment;
4486
4669
  this.taskId = taskId;
4487
4670
  this.setLabel(assignment.sectionTitle);
@@ -4493,12 +4676,14 @@ var T2Manager = class extends BaseTier {
4493
4676
  });
4494
4677
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
4495
4678
  try {
4679
+ this.throwIfCancelled();
4496
4680
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
4497
4681
  this.sendStatusUpdate({
4498
4682
  progressPct: 20,
4499
4683
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
4500
4684
  status: "IN_PROGRESS"
4501
4685
  });
4686
+ this.throwIfCancelled();
4502
4687
  const t3Results = await this.executeSubtasks(subtasks, taskId);
4503
4688
  this.sendStatusUpdate({
4504
4689
  progressPct: 90,
@@ -4665,11 +4850,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4665
4850
  ).join(", ")}`,
4666
4851
  status: "IN_PROGRESS"
4667
4852
  });
4853
+ this.throwIfCancelled();
4668
4854
  const waveResults = await Promise.allSettled(
4669
4855
  runnableIds.map(async (id) => {
4670
4856
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4671
4857
  const worker = workerMap.get(id);
4672
- const result = await worker.execute(assignment, taskId);
4858
+ const result = await worker.execute(assignment, taskId, this.signal);
4673
4859
  resultMap.set(id, result);
4674
4860
  return result;
4675
4861
  })
@@ -4683,6 +4869,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4683
4869
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4684
4870
  const retried = await this.retryT3(assignment, taskId);
4685
4871
  resultMap.set(id, retried);
4872
+ } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
4873
+ const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4874
+ if (this.toolCreator) {
4875
+ this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
4876
+ this.sendStatusUpdate({
4877
+ progressPct: 50,
4878
+ currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
4879
+ status: "IN_PROGRESS"
4880
+ });
4881
+ const toolName = await this.toolCreator.createTool(
4882
+ `Help complete: ${assignment.subtaskTitle}`,
4883
+ assignment.description
4884
+ );
4885
+ if (toolName) {
4886
+ this.log(`T2 verifying new tool: ${toolName}`);
4887
+ this.sendStatusUpdate({
4888
+ progressPct: 60,
4889
+ currentAction: `T2 Verifying new tool: ${toolName}`,
4890
+ status: "IN_PROGRESS"
4891
+ });
4892
+ try {
4893
+ const verifyResult = await this.router.generate("T2", {
4894
+ messages: [{ role: "user", content: `A new tool named "${toolName}" was just created dynamically to help with: ${assignment.description}. Based on its name and purpose, does this seem like a valid addition? Reply "VERIFIED" or "REJECTED".` }],
4895
+ systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
4896
+ maxTokens: 50
4897
+ });
4898
+ if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
4899
+ this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
4900
+ const retried = await this.retryT3({
4901
+ ...assignment,
4902
+ description: `${assignment.description}
4903
+
4904
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
4905
+ }, taskId);
4906
+ resultMap.set(id, retried);
4907
+ } else {
4908
+ this.log(`T2 rejected the dynamic tool: ${toolName}`);
4909
+ resultMap.set(id, r.value);
4910
+ }
4911
+ } catch {
4912
+ const retried = await this.retryT3({
4913
+ ...assignment,
4914
+ description: `${assignment.description}
4915
+
4916
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
4917
+ }, taskId);
4918
+ resultMap.set(id, retried);
4919
+ }
4920
+ } else {
4921
+ resultMap.set(id, r.value);
4922
+ }
4923
+ } else {
4924
+ resultMap.set(id, r.value);
4925
+ }
4686
4926
  }
4687
4927
  for (const dependent of adj.get(id) ?? []) {
4688
4928
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -4746,7 +4986,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4746
4986
  }));
4747
4987
  return worker.execute(
4748
4988
  { ...assignment, description: `[RETRY] ${assignment.description}` },
4749
- taskId
4989
+ taskId,
4990
+ this.signal
4750
4991
  );
4751
4992
  }
4752
4993
  publishSectionOutput(result) {
@@ -4760,29 +5001,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4760
5001
  async aggregateResults(assignment, results) {
4761
5002
  const completed = results.filter((r) => r.status === "COMPLETED");
4762
5003
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
4763
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
4764
5004
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
4765
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
4766
-
4767
- ${outputs}
4768
- ${peerOutputs ? `
5005
+ const peerContext = peerOutputs ? `
4769
5006
 
4770
5007
  Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
4771
- ${peerOutputs}` : ""}`;
4772
- const messages = [{ role: "user", content: prompt }];
4773
- try {
4774
- const result = await this.router.generate("T2", {
4775
- messages,
4776
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
5008
+ ${peerOutputs}` : "";
5009
+ const MAX_CHUNK_LENGTH = 15e3;
5010
+ let currentSummary = "";
5011
+ let i = 0;
5012
+ while (i < completed.length) {
5013
+ let chunkText = "";
5014
+ let chunkEnd = i;
5015
+ while (chunkEnd < completed.length) {
5016
+ const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
5017
+
5018
+ `;
5019
+ if (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
5020
+ break;
5021
+ }
5022
+ chunkText += nextOutput;
5023
+ chunkEnd++;
5024
+ }
5025
+ i = chunkEnd;
5026
+ const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
5027
+ ${currentSummary ? `
5028
+ PREVIOUS SUMMARY SO FAR:
5029
+ ${currentSummary}
5030
+
5031
+ NEW OUTPUTS TO INTEGRATE:
5032
+ ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
5033
+ const messages = [{ role: "user", content: prompt }];
5034
+ try {
5035
+ const result = await this.router.generate("T2", {
5036
+ messages,
5037
+ systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
4777
5038
 
4778
5039
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4779
- maxTokens: 300
4780
- });
4781
- return result.content;
4782
- } catch (err) {
4783
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
4784
- return outputs;
5040
+ maxTokens: 500
5041
+ });
5042
+ currentSummary = result.content;
5043
+ } catch (err) {
5044
+ this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
5045
+ return currentSummary + "\n\n" + chunkText;
5046
+ }
4785
5047
  }
5048
+ return currentSummary;
4786
5049
  }
4787
5050
  determineStatus(results) {
4788
5051
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -4910,10 +5173,10 @@ Rules:
4910
5173
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
4911
5174
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
4912
5175
 
4913
- EXECUTION MODE GUIDANCE:
4914
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
4915
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
4916
- - Prefer parallel execution: it is significantly faster and reduces total wall-clock time.
5176
+ DEPENDENCY GUIDANCE:
5177
+ - Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
5178
+ - Populate "dependsOn" with section IDs ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
5179
+ - Prefer empty dependencies (parallel execution): it is significantly faster and reduces total wall-clock time.
4917
5180
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
4918
5181
 
4919
5182
  QUALITY RULES:
@@ -4952,7 +5215,8 @@ var T1Administrator = class extends BaseTier {
4952
5215
  setToolCreator(creator) {
4953
5216
  this.toolCreator = creator;
4954
5217
  }
4955
- async execute(userPrompt, images, systemContext) {
5218
+ async execute(userPrompt, images, systemContext, signal) {
5219
+ this.signal = signal;
4956
5220
  this.taskId = crypto.randomUUID();
4957
5221
  this.setLabel("Administrator");
4958
5222
  this.setStatus("ACTIVE");
@@ -4963,10 +5227,12 @@ var T1Administrator = class extends BaseTier {
4963
5227
  status: "IN_PROGRESS"
4964
5228
  });
4965
5229
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
5230
+ this.throwIfCancelled();
4966
5231
  let enrichedPrompt = userPrompt;
4967
5232
  if (images?.length) {
4968
5233
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
4969
5234
  }
5235
+ this.throwIfCancelled();
4970
5236
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
4971
5237
  this.sendStatusUpdate({
4972
5238
  progressPct: 10,
@@ -4974,21 +5240,83 @@ var T1Administrator = class extends BaseTier {
4974
5240
  status: "IN_PROGRESS"
4975
5241
  });
4976
5242
  this.emit("plan", { taskId: this.taskId, plan });
4977
- const t2Results = await this.dispatchT2Managers(plan.sections);
5243
+ this.throwIfCancelled();
5244
+ let allT2Results = await this.dispatchT2Managers(plan.sections);
5245
+ let pass = 1;
5246
+ const MAX_REPLAN_PASSES = 2;
5247
+ while (pass <= MAX_REPLAN_PASSES) {
5248
+ const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
5249
+ if (reviewResult.approved) {
5250
+ this.log("T1 Review passed.");
5251
+ break;
5252
+ }
5253
+ this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
5254
+ this.sendStatusUpdate({
5255
+ progressPct: 80 + pass * 5,
5256
+ currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
5257
+ status: "IN_PROGRESS"
5258
+ });
5259
+ const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
5260
+ Review reason: ${reviewResult.reason}
5261
+
5262
+ Original goal: ${enrichedPrompt}
5263
+
5264
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
5265
+ const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
5266
+ allT2Results = [...allT2Results, ...correctionResults];
5267
+ pass++;
5268
+ }
4978
5269
  this.sendStatusUpdate({
4979
5270
  progressPct: 95,
4980
5271
  currentAction: "Compiling final output",
4981
5272
  status: "IN_PROGRESS"
4982
5273
  });
4983
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
5274
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
4984
5275
  this.setStatus("COMPLETED");
4985
5276
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
4986
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
5277
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
4987
5278
  }
4988
5279
  getEscalations() {
4989
5280
  return [...this.escalations];
4990
5281
  }
4991
5282
  // ── Private ──────────────────────────────────
5283
+ async reviewT2Outputs(originalPrompt, plan, t2Results) {
5284
+ const failedSections = t2Results.filter((r) => r.status === "FAILED");
5285
+ if (failedSections.length > 0) {
5286
+ return {
5287
+ approved: false,
5288
+ reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
5289
+ };
5290
+ }
5291
+ const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
5292
+ ${r.sectionSummary}`).join("\n\n");
5293
+ const prompt = `You are a strict QA Reviewer for the Cascade AI system.
5294
+ Review the following execution outputs against the original user prompt.
5295
+
5296
+ Original Request: ${originalPrompt}
5297
+
5298
+ T2 Manager Summaries:
5299
+ ${sectionsText}
5300
+
5301
+ Does the current state of the workspace and the outputs fully satisfy the user's request?
5302
+ If yes, reply with exactly: "APPROVED".
5303
+ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
5304
+ try {
5305
+ const result = await this.router.generate("T1", {
5306
+ messages: [{ role: "user", content: prompt }],
5307
+ systemPrompt: this.systemPromptOverride + "You are a QA Reviewer.",
5308
+ maxTokens: 500,
5309
+ temperature: 0
5310
+ });
5311
+ const response = result.content.trim();
5312
+ if (response.toUpperCase().startsWith("APPROVED")) {
5313
+ return { approved: true };
5314
+ }
5315
+ return { approved: false, reason: response.replace(/^REJECTED:\s*/i, "") };
5316
+ } catch {
5317
+ return { approved: true };
5318
+ }
5319
+ }
4992
5320
  async analyzeImages(prompt, images) {
4993
5321
  const visionModel = this.router.getModelForTier("T1");
4994
5322
  if (!visionModel?.isVisionCapable) return prompt;
@@ -5017,29 +5345,35 @@ ${systemContext}` : "";
5017
5345
  Example: if asked to create files "inside python_exclusive", every subtask that
5018
5346
  creates a file must use "python_exclusive/filename.ext" as the path.
5019
5347
 
5020
- Return JSON where subtasks can declare dependencies:
5348
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
5021
5349
  {
5022
5350
  "sections": [{
5351
+ "sectionId": "s1",
5352
+ "sectionTitle": "Setup Project",
5353
+ "description": "Initialize the project",
5354
+ "expectedOutput": "Basic structure created",
5355
+ "constraints": [],
5356
+ "dependsOn": [], // \u2190 empty = runs immediately
5023
5357
  "t3Subtasks": [{
5024
5358
  "subtaskId": "t1",
5025
- "subtaskTitle": "Generate Source Code",
5026
- "dependsOn": [], // \u2190 empty = runs immediately
5027
- "executionMode": "parallel"
5028
- }, {
5029
- "subtaskId": "t2",
5030
- "subtaskTitle": "Save Code to File",
5031
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
5032
- "executionMode": "parallel"
5033
- }, {
5034
- "subtaskId": "t3",
5035
- "subtaskTitle": "Execute and Verify",
5036
- "dependsOn": ["t2"], // \u2190 waits for t2
5037
- "executionMode": "parallel"
5359
+ "subtaskTitle": "Init NPM",
5360
+ "description": "Run npm init",
5361
+ "expectedOutput": "package.json created",
5362
+ "constraints": [],
5363
+ "dependsOn": []
5038
5364
  }]
5365
+ }, {
5366
+ "sectionId": "s2",
5367
+ "sectionTitle": "Write Tests",
5368
+ "description": "Write tests for the project",
5369
+ "expectedOutput": "Tests passing",
5370
+ "constraints": [],
5371
+ "dependsOn": ["s1"], // \u2190 waits for section s1 to complete first
5372
+ "t3Subtasks": [...]
5039
5373
  }]
5040
5374
  }
5041
- Use dependsOn when a subtask needs the output of a previous one.
5042
- Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5375
+ Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
5376
+ Leave dependsOn empty for sections that can run immediately in parallel.`;
5043
5377
  const messages = [{ role: "user", content: decompositionPrompt }];
5044
5378
  const result = await this.router.generate("T1", {
5045
5379
  messages,
@@ -5167,92 +5501,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5167
5501
  ].filter(Boolean).join(" ");
5168
5502
  m.setHierarchyContext(context);
5169
5503
  });
5170
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
5171
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
5172
- for (const section of sections) {
5173
- if (overlapSections.has(section.sectionId)) {
5174
- section.executionMode = "sequential";
5504
+ if (overlapSections.size > 0) {
5505
+ this.log("Overlap detected \u2014 adding sequential dependencies for conflicting sections to prevent race conditions");
5506
+ const overlapArray = Array.from(overlapSections);
5507
+ for (let i = 1; i < overlapArray.length; i++) {
5508
+ const section = sections.find((s) => s.sectionId === overlapArray[i]);
5509
+ if (section) {
5510
+ section.dependsOn = [...section.dependsOn || [], overlapArray[i - 1]];
5175
5511
  }
5176
5512
  }
5177
5513
  }
5178
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
5179
- const isSequential = sections.some((s) => s.executionMode === "sequential");
5180
5514
  const t2Results = [];
5181
5515
  try {
5182
- if (isSequential) {
5183
- this.log("Dispatching T2 managers sequentially");
5184
- for (let i = 0; i < managers.length; i++) {
5185
- const m = managers[i];
5186
- this.sendStatusUpdate({
5187
- progressPct: pct(i),
5188
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
5189
- status: "IN_PROGRESS"
5190
- });
5191
- try {
5192
- const result = await m.execute(sections[i], this.taskId);
5193
- t2Results.push(result);
5194
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
5195
- if (result.status === "ESCALATED") {
5196
- this.escalations.push({
5197
- raisedBy: `T2_${sections[i].sectionId}`,
5198
- sectionId: sections[i].sectionId,
5199
- attempted: result.issues,
5200
- blocker: result.issues.join("; "),
5201
- needs: "Human review required"
5202
- });
5203
- }
5204
- } catch (err) {
5205
- t2Results.push({
5206
- sectionId: sections[i].sectionId,
5207
- sectionTitle: sections[i].sectionTitle,
5208
- status: "FAILED",
5209
- t3Results: [],
5210
- sectionSummary: "",
5211
- issues: [err instanceof Error ? err.message : String(err)]
5212
- });
5516
+ t2Results.push(...await this.runT2sWithDependencies(sections, managers, this.taskId));
5517
+ } finally {
5518
+ cleanup();
5519
+ }
5520
+ return t2Results;
5521
+ }
5522
+ /**
5523
+ * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
5524
+ */
5525
+ async runT2sWithDependencies(sections, managers, taskId) {
5526
+ const adj = /* @__PURE__ */ new Map();
5527
+ const inDegree = /* @__PURE__ */ new Map();
5528
+ const resultMap = /* @__PURE__ */ new Map();
5529
+ const allKeys = new Set(sections.map((s) => s.sectionId));
5530
+ for (const s of sections) {
5531
+ if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
5532
+ inDegree.set(s.sectionId, 0);
5533
+ s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
5534
+ }
5535
+ for (const s of sections) {
5536
+ for (const dep of s.dependsOn ?? []) {
5537
+ adj.get(dep).add(s.sectionId);
5538
+ inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
5539
+ }
5540
+ }
5541
+ const queue = [];
5542
+ const degree = new Map(inDegree);
5543
+ for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
5544
+ const visited = /* @__PURE__ */ new Set();
5545
+ while (queue.length > 0) {
5546
+ const u = queue.shift();
5547
+ visited.add(u);
5548
+ for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
5549
+ const newDeg = (degree.get(v) ?? 1) - 1;
5550
+ degree.set(v, newDeg);
5551
+ if (newDeg === 0) queue.push(v);
5552
+ }
5553
+ }
5554
+ const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
5555
+ if (cycleNodes.length > 0) {
5556
+ this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
5557
+ for (const s of sections) {
5558
+ if (cycleNodes.includes(s.sectionId)) {
5559
+ const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
5560
+ for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
5561
+ inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
5562
+ adj.get(removed)?.delete(s.sectionId);
5213
5563
  }
5564
+ s.dependsOn = safeDeps;
5214
5565
  }
5215
- } else {
5216
- const results = await Promise.allSettled(
5217
- managers.map((m, i) => {
5218
- this.sendStatusUpdate({
5219
- progressPct: pct(i),
5220
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
5221
- status: "IN_PROGRESS"
5222
- });
5223
- return m.execute(sections[i], this.taskId);
5224
- })
5225
- );
5226
- for (let i = 0; i < results.length; i++) {
5227
- const r = results[i];
5228
- if (r.status === "fulfilled") {
5229
- t2Results.push(r.value);
5230
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
5231
- if (r.value.status === "ESCALATED") {
5232
- this.escalations.push({
5233
- raisedBy: `T2_${sections[i].sectionId}`,
5234
- sectionId: sections[i].sectionId,
5235
- attempted: r.value.issues,
5236
- blocker: r.value.issues.join("; "),
5237
- needs: "Human review required"
5238
- });
5239
- }
5240
- } else {
5241
- t2Results.push({
5242
- sectionId: sections[i].sectionId,
5243
- sectionTitle: sections[i].sectionTitle,
5244
- status: "FAILED",
5245
- t3Results: [],
5246
- sectionSummary: "",
5247
- issues: [r.reason instanceof Error ? r.reason.message : String(r.reason)]
5566
+ }
5567
+ }
5568
+ const totalSections = sections.length;
5569
+ let completedSections = 0;
5570
+ const executeWave = async () => {
5571
+ const readyIds = [];
5572
+ for (const [id, deg] of inDegree.entries()) {
5573
+ if (deg === 0 && !resultMap.has(id)) {
5574
+ readyIds.push(id);
5575
+ }
5576
+ }
5577
+ if (readyIds.length === 0) return;
5578
+ await Promise.all(readyIds.map(async (id) => {
5579
+ resultMap.set(id, null);
5580
+ const index = sections.findIndex((s) => s.sectionId === id);
5581
+ const section = sections[index];
5582
+ const manager = managers[index];
5583
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
5584
+ this.sendStatusUpdate({
5585
+ progressPct,
5586
+ currentAction: `T2 working on: ${section.sectionTitle}`,
5587
+ status: "IN_PROGRESS"
5588
+ });
5589
+ this.throwIfCancelled();
5590
+ let result;
5591
+ try {
5592
+ result = await manager.execute(section, taskId, this.signal);
5593
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
5594
+ if (result.status === "ESCALATED") {
5595
+ this.escalations.push({
5596
+ raisedBy: `T2_${section.sectionId}`,
5597
+ sectionId: section.sectionId,
5598
+ attempted: result.issues,
5599
+ blocker: result.issues.join("; "),
5600
+ needs: "Human review required"
5248
5601
  });
5249
5602
  }
5603
+ } catch (err) {
5604
+ result = {
5605
+ sectionId: section.sectionId,
5606
+ sectionTitle: section.sectionTitle,
5607
+ status: "FAILED",
5608
+ t3Results: [],
5609
+ sectionSummary: "",
5610
+ issues: [err instanceof Error ? err.message : String(err)]
5611
+ };
5250
5612
  }
5613
+ resultMap.set(id, result);
5614
+ completedSections++;
5615
+ for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
5616
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
5617
+ }
5618
+ }));
5619
+ if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
5620
+ await executeWave();
5251
5621
  }
5252
- } finally {
5253
- cleanup();
5254
- }
5255
- return t2Results;
5622
+ };
5623
+ await executeWave();
5624
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
5256
5625
  }
5257
5626
  async compileFinalOutput(originalPrompt, plan, t2Results) {
5258
5627
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -5702,13 +6071,47 @@ var GitHubTool = class extends BaseTool {
5702
6071
  }
5703
6072
  async execute(input, _options) {
5704
6073
  const platform = input["platform"] ?? "github";
5705
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
5706
6074
  const operation = input["operation"];
5707
6075
  const repo = input["repo"];
5708
- if (platform === "github") {
5709
- return this.executeGitHub(operation, repo, token, input);
6076
+ let token = input["token"];
6077
+ if (!token) {
6078
+ if (platform === "github") {
6079
+ token = process.env["GITHUB_TOKEN"];
6080
+ } else {
6081
+ token = process.env["GITLAB_TOKEN"];
6082
+ }
6083
+ }
6084
+ if (!token) {
6085
+ const envName = platform === "github" ? "GITHUB_TOKEN" : "GITLAB_TOKEN";
6086
+ return `Error: No ${platform} token provided. Set the ${envName} environment variable or pass a "token" field in the input.`;
6087
+ }
6088
+ try {
6089
+ if (platform === "github") {
6090
+ return await this.executeGitHub(operation, repo, token, input);
6091
+ }
6092
+ return await this.executeGitLab(operation, repo, token, input);
6093
+ } catch (err) {
6094
+ const axiosErr = err;
6095
+ if (axiosErr?.response?.status) {
6096
+ const status = axiosErr.response.status;
6097
+ const msg = axiosErr.response.data?.message ?? "";
6098
+ switch (status) {
6099
+ case 401:
6100
+ return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
6101
+ case 403:
6102
+ return `Permission denied: Your ${platform} token lacks the required scopes for this operation. Needed: repo or workflow.`;
6103
+ case 404:
6104
+ return `Not found: Repository "${repo}" does not exist, or your token cannot access it.`;
6105
+ case 422:
6106
+ return `Validation error from ${platform}: ${msg || "Check your input parameters (branch names, base/head refs, etc.)."}`;
6107
+ case 429:
6108
+ return `Rate limited by ${platform}. Please wait a moment before trying again.`;
6109
+ default:
6110
+ return `${platform} API error (${status}): ${msg || (axiosErr.message ?? "Unknown error")}`;
6111
+ }
6112
+ }
6113
+ return `${platform} request failed: ${axiosErr.message ?? String(err)}`;
5710
6114
  }
5711
- return this.executeGitLab(operation, repo, token, input);
5712
6115
  }
5713
6116
  async executeGitHub(operation, repo, token, input) {
5714
6117
  const headers = {
@@ -5795,6 +6198,7 @@ ${response.data.description}`;
5795
6198
  };
5796
6199
 
5797
6200
  // src/tools/browser.ts
6201
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
5798
6202
  var BrowserTool = class extends BaseTool {
5799
6203
  name = "browser";
5800
6204
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -5803,7 +6207,7 @@ var BrowserTool = class extends BaseTool {
5803
6207
  properties: {
5804
6208
  action: {
5805
6209
  type: "string",
5806
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
6210
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
5807
6211
  },
5808
6212
  url: { type: "string", description: "URL to navigate to" },
5809
6213
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -5823,53 +6227,86 @@ var BrowserTool = class extends BaseTool {
5823
6227
  try {
5824
6228
  playwright = await import('playwright');
5825
6229
  } catch {
5826
- throw new Error("Playwright is not installed. Run: npm install playwright && npx playwright install chromium");
6230
+ return "Error: Playwright is not installed. Run: npm install playwright && npx playwright install chromium";
5827
6231
  }
5828
- if (!this.browser) {
5829
- const pw = playwright;
5830
- this.browser = await pw.chromium.launch({ headless: true });
5831
- const b = this.browser;
5832
- this.page = await b.newPage();
5833
- }
5834
- const page = this.page;
5835
6232
  const action = input["action"];
5836
6233
  const timeout = input["timeout"] ?? 1e4;
5837
- switch (action) {
5838
- case "navigate": {
5839
- await page.goto(input["url"], { timeout });
5840
- return `Navigated to ${input["url"]}`;
5841
- }
5842
- case "click": {
5843
- await page.click(input["selector"], { timeout });
5844
- return `Clicked ${input["selector"]}`;
5845
- }
5846
- case "fill": {
5847
- await page.fill(input["selector"], input["value"]);
5848
- return `Filled ${input["selector"]} with value`;
5849
- }
5850
- case "screenshot": {
5851
- const buf = await page.screenshot({ type: "png" });
5852
- return `data:image/png;base64,${buf.toString("base64")}`;
5853
- }
5854
- case "evaluate": {
5855
- const result = await page.evaluate(input["script"]);
5856
- return JSON.stringify(result);
6234
+ if (action === "close") {
6235
+ await this.close();
6236
+ return "Browser closed.";
6237
+ }
6238
+ if (!this.browser || !this.page) {
6239
+ await this.close();
6240
+ const launchPromise = playwright.chromium.launch({ headless: true });
6241
+ const timeoutPromise = new Promise(
6242
+ (_, reject) => setTimeout(() => reject(new Error(`Browser launch timed out after ${BROWSER_LAUNCH_TIMEOUT_MS}ms. Is Chromium installed? Run: npx playwright install chromium`)), BROWSER_LAUNCH_TIMEOUT_MS)
6243
+ );
6244
+ try {
6245
+ this.browser = await Promise.race([launchPromise, timeoutPromise]);
6246
+ this.page = await this.browser.newPage();
6247
+ } catch (err) {
6248
+ this.browser = null;
6249
+ this.page = null;
6250
+ return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
5857
6251
  }
5858
- case "extract_text": {
5859
- const text = await page.locator("body").innerText();
5860
- return text.slice(0, 1e4);
6252
+ }
6253
+ const page = this.page;
6254
+ try {
6255
+ switch (action) {
6256
+ case "navigate": {
6257
+ await page.goto(input["url"], { timeout });
6258
+ const title = await page.title();
6259
+ return `Navigated to ${input["url"]} (title: "${title}")`;
6260
+ }
6261
+ case "click": {
6262
+ await page.click(input["selector"], { timeout });
6263
+ return `Clicked ${input["selector"]}`;
6264
+ }
6265
+ case "fill": {
6266
+ await page.fill(input["selector"], input["value"]);
6267
+ return `Filled ${input["selector"]} with value`;
6268
+ }
6269
+ case "screenshot": {
6270
+ const buf = await page.screenshot({ type: "png" });
6271
+ return `data:image/png;base64,${buf.toString("base64")}`;
6272
+ }
6273
+ case "evaluate": {
6274
+ const result = await page.evaluate(input["script"]);
6275
+ return JSON.stringify(result);
6276
+ }
6277
+ case "extract_text": {
6278
+ const text = await page.locator("body").innerText();
6279
+ return text.slice(0, 1e4);
6280
+ }
6281
+ case "wait": {
6282
+ await page.waitForTimeout(timeout);
6283
+ return `Waited ${timeout}ms`;
6284
+ }
6285
+ default:
6286
+ return `Unknown browser action: ${action}. Supported: navigate, click, fill, screenshot, evaluate, extract_text, wait, close`;
5861
6287
  }
5862
- case "wait": {
5863
- await page.waitForTimeout(timeout);
5864
- return `Waited ${timeout}ms`;
6288
+ } catch (err) {
6289
+ const errMsg = err instanceof Error ? err.message : String(err);
6290
+ if (/Target closed|Page crashed|Navigation failed/i.test(errMsg)) {
6291
+ await this.close();
6292
+ return `Browser error (page reset): ${errMsg}`;
5865
6293
  }
5866
- default:
5867
- throw new Error(`Unknown browser action: ${action}`);
6294
+ return `Browser action "${action}" failed: ${errMsg}`;
5868
6295
  }
5869
6296
  }
5870
6297
  async close() {
5871
- if (this.browser) {
5872
- await this.browser.close();
6298
+ try {
6299
+ if (this.page) {
6300
+ await this.page.close().catch(() => {
6301
+ });
6302
+ this.page = null;
6303
+ }
6304
+ if (this.browser) {
6305
+ await this.browser.close().catch(() => {
6306
+ });
6307
+ this.browser = null;
6308
+ }
6309
+ } catch {
5873
6310
  this.browser = null;
5874
6311
  this.page = null;
5875
6312
  }
@@ -5966,6 +6403,19 @@ var PDFCreateTool = class extends BaseTool {
5966
6403
  });
5967
6404
  }
5968
6405
  };
6406
+ function detectCommand(candidates2) {
6407
+ for (const cmd of candidates2) {
6408
+ try {
6409
+ const which = process.platform === "win32" ? "where" : "which";
6410
+ child_process.execSync(`${which} ${cmd}`, { stdio: "ignore" });
6411
+ return cmd;
6412
+ } catch {
6413
+ }
6414
+ }
6415
+ return null;
6416
+ }
6417
+ var PYTHON_CMD = detectCommand(["python3", "python"]);
6418
+ var NODE_CMD = detectCommand(["node"]);
5969
6419
  var CodeInterpreterTool = class extends BaseTool {
5970
6420
  name = "run_code";
5971
6421
  description = "Execute a Python or Node.js script to perform complex tasks (data processing, file conversion, etc.). The script is automatically cleaned up after execution.";
@@ -5981,10 +6431,30 @@ var CodeInterpreterTool = class extends BaseTool {
5981
6431
  isDangerous() {
5982
6432
  return true;
5983
6433
  }
5984
- async execute(input, options) {
6434
+ async execute(input, _options) {
5985
6435
  const language = input["language"];
5986
6436
  const code = input["code"];
5987
6437
  const args = input["args"] ?? [];
6438
+ let cmdPrefix;
6439
+ if (language === "python") {
6440
+ if (!PYTHON_CMD) {
6441
+ return [
6442
+ "Error: Python interpreter not found.",
6443
+ "Please install Python and ensure it is in your PATH.",
6444
+ "Tried: python3, python"
6445
+ ].join("\n");
6446
+ }
6447
+ cmdPrefix = PYTHON_CMD;
6448
+ } else {
6449
+ if (!NODE_CMD) {
6450
+ return [
6451
+ "Error: Node.js interpreter not found.",
6452
+ "Please install Node.js and ensure it is in your PATH.",
6453
+ "Tried: node"
6454
+ ].join("\n");
6455
+ }
6456
+ cmdPrefix = NODE_CMD;
6457
+ }
5988
6458
  const tmpDir = path17__default.default.join(process.cwd(), ".cascade", "tmp");
5989
6459
  if (!fs14__default.default.existsSync(tmpDir)) {
5990
6460
  fs14__default.default.mkdirSync(tmpDir, { recursive: true });
@@ -5993,8 +6463,9 @@ var CodeInterpreterTool = class extends BaseTool {
5993
6463
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
5994
6464
  const filePath = path17__default.default.join(tmpDir, fileName);
5995
6465
  fs14__default.default.writeFileSync(filePath, code, "utf-8");
5996
- const cmdPrefix = language === "python" ? "python3" : "node";
5997
- const fullCmd = `${cmdPrefix} "${filePath}" ${args.map((a) => `"${a}"`).join(" ")}`;
6466
+ const quotedPath = `"${filePath}"`;
6467
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
6468
+ const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
5998
6469
  return new Promise((resolve) => {
5999
6470
  const startMs = Date.now();
6000
6471
  child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -6007,10 +6478,17 @@ var CodeInterpreterTool = class extends BaseTool {
6007
6478
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
6008
6479
  }
6009
6480
  if (error) {
6010
- resolve(`Execution failed (${duration}ms):
6481
+ const timedOut = error.killed && duration >= 3e4;
6482
+ if (timedOut) {
6483
+ resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
6484
+ Partial stdout: ${stdout}
6485
+ Stderr: ${stderr}`);
6486
+ } else {
6487
+ resolve(`Execution failed (${duration}ms):
6011
6488
  Error: ${error.message}
6012
6489
  Stderr: ${stderr}
6013
6490
  Stdout: ${stdout}`);
6491
+ }
6014
6492
  } else {
6015
6493
  resolve(`Execution successful (${duration}ms):
6016
6494
  Stdout: ${stdout}
@@ -6075,6 +6553,186 @@ ${formatted}`;
6075
6553
  }
6076
6554
  };
6077
6555
 
6556
+ // src/tools/web-search.ts
6557
+ async function searchSearXNG(query, baseUrl, maxResults) {
6558
+ const url = new URL("/search", baseUrl);
6559
+ url.searchParams.set("q", query);
6560
+ url.searchParams.set("format", "json");
6561
+ url.searchParams.set("categories", "general");
6562
+ url.searchParams.set("engines", "google,bing,duckduckgo");
6563
+ const resp = await fetch(url.toString(), {
6564
+ headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
6565
+ signal: AbortSignal.timeout(1e4)
6566
+ });
6567
+ if (!resp.ok) {
6568
+ throw new Error(`SearXNG returned HTTP ${resp.status}`);
6569
+ }
6570
+ const data = await resp.json();
6571
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6572
+ title: r.title ?? "",
6573
+ url: r.url ?? "",
6574
+ snippet: r.content ?? "",
6575
+ engine: `searxng(${r.engine ?? "unknown"})`
6576
+ }));
6577
+ }
6578
+ async function searchBrave(query, apiKey, maxResults) {
6579
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}&safesearch=off`;
6580
+ const resp = await fetch(url, {
6581
+ headers: {
6582
+ "Accept": "application/json",
6583
+ "Accept-Encoding": "gzip",
6584
+ "X-Subscription-Token": apiKey
6585
+ },
6586
+ signal: AbortSignal.timeout(1e4)
6587
+ });
6588
+ if (!resp.ok) {
6589
+ throw new Error(`Brave Search returned HTTP ${resp.status}`);
6590
+ }
6591
+ const data = await resp.json();
6592
+ return (data.web?.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6593
+ title: r.title ?? "",
6594
+ url: r.url ?? "",
6595
+ snippet: r.description ?? "",
6596
+ engine: "brave"
6597
+ }));
6598
+ }
6599
+ async function searchTavily(query, apiKey, maxResults) {
6600
+ const resp = await fetch("https://api.tavily.com/search", {
6601
+ method: "POST",
6602
+ headers: {
6603
+ "Content-Type": "application/json",
6604
+ "Authorization": `Bearer ${apiKey}`
6605
+ },
6606
+ body: JSON.stringify({
6607
+ query,
6608
+ max_results: maxResults,
6609
+ search_depth: "basic",
6610
+ include_answer: false,
6611
+ include_raw_content: false
6612
+ }),
6613
+ signal: AbortSignal.timeout(15e3)
6614
+ });
6615
+ if (!resp.ok) {
6616
+ throw new Error(`Tavily returned HTTP ${resp.status}`);
6617
+ }
6618
+ const data = await resp.json();
6619
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6620
+ title: r.title ?? "",
6621
+ url: r.url ?? "",
6622
+ snippet: r.content ?? "",
6623
+ engine: "tavily"
6624
+ }));
6625
+ }
6626
+ async function searchDuckDuckGoLite(query, maxResults) {
6627
+ const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
6628
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
6629
+ signal: AbortSignal.timeout(1e4)
6630
+ });
6631
+ if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
6632
+ const html = await resp.text();
6633
+ const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
6634
+ const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
6635
+ const links = [];
6636
+ const snippets = [];
6637
+ let m;
6638
+ while ((m = linkPattern.exec(html)) !== null) {
6639
+ links.push({ url: m[1], title: m[2].trim() });
6640
+ }
6641
+ while ((m = snippetPattern.exec(html)) !== null) {
6642
+ snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
6643
+ }
6644
+ return links.slice(0, maxResults).map((link, i) => ({
6645
+ title: link.title,
6646
+ url: link.url,
6647
+ snippet: snippets[i] ?? "",
6648
+ engine: "duckduckgo-lite"
6649
+ }));
6650
+ }
6651
+ var WebSearchTool = class extends BaseTool {
6652
+ name = "web_search";
6653
+ description = "Search the web for current information, news, documentation, or any topic. Returns a list of relevant results with titles, URLs, and snippets.";
6654
+ inputSchema = {
6655
+ type: "object",
6656
+ properties: {
6657
+ query: { type: "string", description: "The search query" },
6658
+ maxResults: { type: "number", description: "Number of results to return (default: 5, max: 10)" }
6659
+ },
6660
+ required: ["query"]
6661
+ };
6662
+ config;
6663
+ constructor(config = {}) {
6664
+ super();
6665
+ this.config = {
6666
+ searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
6667
+ braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
6668
+ tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
6669
+ maxResults: config.maxResults ?? 5
6670
+ };
6671
+ }
6672
+ async execute(input, _options) {
6673
+ const query = input["query"];
6674
+ if (!query?.trim()) return "Error: query is required and must be non-empty.";
6675
+ const maxResults = Math.min(
6676
+ input["maxResults"] ?? this.config.maxResults ?? 5,
6677
+ 10
6678
+ );
6679
+ const errors = [];
6680
+ let results = [];
6681
+ if (this.config.searxngUrl) {
6682
+ try {
6683
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
6684
+ if (results.length > 0) return this.formatResults(query, results);
6685
+ errors.push("SearXNG: returned 0 results");
6686
+ } catch (err) {
6687
+ errors.push(`SearXNG: ${err instanceof Error ? err.message : String(err)}`);
6688
+ }
6689
+ }
6690
+ if (this.config.braveApiKey) {
6691
+ try {
6692
+ results = await searchBrave(query, this.config.braveApiKey, maxResults);
6693
+ if (results.length > 0) return this.formatResults(query, results);
6694
+ errors.push("Brave: returned 0 results");
6695
+ } catch (err) {
6696
+ errors.push(`Brave: ${err instanceof Error ? err.message : String(err)}`);
6697
+ }
6698
+ }
6699
+ if (this.config.tavilyApiKey) {
6700
+ try {
6701
+ results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
6702
+ if (results.length > 0) return this.formatResults(query, results);
6703
+ errors.push("Tavily: returned 0 results");
6704
+ } catch (err) {
6705
+ errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
6706
+ }
6707
+ }
6708
+ try {
6709
+ results = await searchDuckDuckGoLite(query, maxResults);
6710
+ if (results.length > 0) return this.formatResults(query, results);
6711
+ errors.push("DuckDuckGo Lite: returned 0 results");
6712
+ } catch (err) {
6713
+ errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
6714
+ }
6715
+ const configHint = !this.config.searxngUrl && !this.config.braveApiKey && !this.config.tavilyApiKey ? "\nTip: Configure a search backend for better results:\n \u2022 Self-hosted: set SEARXNG_URL in your environment\n \u2022 Brave Search API: set BRAVE_SEARCH_API_KEY\n \u2022 Tavily API: set TAVILY_API_KEY" : "";
6716
+ return [
6717
+ `Web search for "${query}" failed across all backends:`,
6718
+ ...errors.map((e) => ` \u2022 ${e}`),
6719
+ configHint
6720
+ ].join("\n");
6721
+ }
6722
+ formatResults(query, results) {
6723
+ const lines = [`Web search results for: "${query}"`, ""];
6724
+ for (let i = 0; i < results.length; i++) {
6725
+ const r = results[i];
6726
+ lines.push(`[${i + 1}] ${r.title}`);
6727
+ lines.push(` URL: ${r.url}`);
6728
+ if (r.snippet) lines.push(` ${r.snippet.slice(0, 300)}`);
6729
+ if (r.engine) lines.push(` Source: ${r.engine}`);
6730
+ lines.push("");
6731
+ }
6732
+ return lines.join("\n");
6733
+ }
6734
+ };
6735
+
6078
6736
  // src/tools/mcp.ts
6079
6737
  var McpToolWrapper = class extends BaseTool {
6080
6738
  name;
@@ -6196,7 +6854,8 @@ var ToolRegistry = class {
6196
6854
  new ImageAnalyzeTool(),
6197
6855
  new PDFCreateTool(),
6198
6856
  new CodeInterpreterTool(),
6199
- new PeerCommunicationTool()
6857
+ new PeerCommunicationTool(),
6858
+ new WebSearchTool(this.config.webSearch)
6200
6859
  ];
6201
6860
  for (const tool of tools) {
6202
6861
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -6220,8 +6879,23 @@ var ToolRegistry = class {
6220
6879
  return this.ignoreMatcher.ignores(posixRel);
6221
6880
  }
6222
6881
  };
6223
- var McpClient = class {
6882
+ var McpClient = class _McpClient {
6883
+ static activeProcessPids = /* @__PURE__ */ new Set();
6884
+ /**
6885
+ * Forcefully kills all known MCP child processes.
6886
+ * Call this from global process exit handlers to prevent zombie processes.
6887
+ */
6888
+ static killAllProcesses() {
6889
+ for (const pid of _McpClient.activeProcessPids) {
6890
+ try {
6891
+ process.kill(pid, "SIGKILL");
6892
+ } catch {
6893
+ }
6894
+ }
6895
+ _McpClient.activeProcessPids.clear();
6896
+ }
6224
6897
  clients = /* @__PURE__ */ new Map();
6898
+ transports = /* @__PURE__ */ new Map();
6225
6899
  tools = /* @__PURE__ */ new Map();
6226
6900
  trustedServers;
6227
6901
  approvalCallback;
@@ -6250,6 +6924,8 @@ var McpClient = class {
6250
6924
  );
6251
6925
  await client.connect(transport);
6252
6926
  this.clients.set(server.name, client);
6927
+ this.transports.set(server.name, transport);
6928
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
6253
6929
  const toolsResult = await client.listTools();
6254
6930
  for (const tool of toolsResult.tools) {
6255
6931
  for (const existing of this.tools.values()) {
@@ -6271,8 +6947,11 @@ var McpClient = class {
6271
6947
  async disconnect(serverName) {
6272
6948
  const client = this.clients.get(serverName);
6273
6949
  if (client) {
6950
+ const transport = this.transports.get(serverName);
6951
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
6274
6952
  await client.close();
6275
6953
  this.clients.delete(serverName);
6954
+ this.transports.delete(serverName);
6276
6955
  for (const key of this.tools.keys()) {
6277
6956
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
6278
6957
  }
@@ -6300,6 +6979,13 @@ var McpClient = class {
6300
6979
  getConnectedServers() {
6301
6980
  return Array.from(this.clients.keys());
6302
6981
  }
6982
+ getActivePids() {
6983
+ const pids = [];
6984
+ for (const transport of this.transports.values()) {
6985
+ if (transport.pid) pids.push(transport.pid);
6986
+ }
6987
+ return pids;
6988
+ }
6303
6989
  isConnected(serverName) {
6304
6990
  return this.clients.has(serverName);
6305
6991
  }
@@ -6868,12 +7554,25 @@ var Cascade = class extends EventEmitter__default.default {
6868
7554
  looksLikeSimpleArtifactTask(prompt) {
6869
7555
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
6870
7556
  }
6871
- async determineComplexity(prompt, conversationHistory = []) {
7557
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
6872
7558
  if (this.looksLikeSimpleArtifactTask(prompt)) {
6873
7559
  return "Simple";
6874
7560
  }
7561
+ let workspaceContext = "";
7562
+ try {
7563
+ const files = await glob.glob("**/*.*", {
7564
+ cwd: workspacePath,
7565
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
7566
+ nodir: true
7567
+ });
7568
+ workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
7569
+ } catch {
7570
+ workspaceContext = "Workspace Scout: Could not scan workspace.";
7571
+ }
6875
7572
  const sysPrompt = `You are a routing classifier for a hierarchical AI system. Determine task complexity using BOTH the latest user message and the recent conversation context.
6876
7573
 
7574
+ ${workspaceContext}
7575
+
6877
7576
  Classification:
6878
7577
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
6879
7578
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -6948,7 +7647,7 @@ ${prompt}` : prompt;
6948
7647
  }
6949
7648
  escalator.resolveUserDecision(req.id, approved, always);
6950
7649
  });
6951
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
7650
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
6952
7651
  this.telemetry.capture("cascade:session_start", {
6953
7652
  complexity,
6954
7653
  providerCount: this.config.providers.length,
@@ -7028,7 +7727,7 @@ ${prompt}` : prompt;
7028
7727
  peerT3Ids: [],
7029
7728
  parentT2: "root"
7030
7729
  };
7031
- const t3Result = await t3.execute(assignment, taskId);
7730
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
7032
7731
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
7033
7732
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
7034
7733
  } else if (complexity === "Moderate") {
@@ -7051,7 +7750,7 @@ ${prompt}` : prompt;
7051
7750
  constraints: [],
7052
7751
  t3Subtasks: []
7053
7752
  };
7054
- const t2Result = await t2.execute(assignment, taskId);
7753
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
7055
7754
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
7056
7755
  t2Results = [t2Result];
7057
7756
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -7073,13 +7772,22 @@ ${prompt}` : prompt;
7073
7772
  if (toolCreator) t1.setToolCreator(toolCreator);
7074
7773
  bindTierEvents(t1);
7075
7774
  t1.on("plan", (e) => this.emit("plan", e));
7076
- const result = await t1.execute(options.prompt, options.images);
7775
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
7077
7776
  finalOutput = result.output;
7078
7777
  t2Results = result.t2Results;
7079
7778
  }
7080
7779
  } catch (err) {
7081
- runError = err;
7082
- throw err;
7780
+ if (err instanceof CascadeCancelledError) {
7781
+ this.emit("run:cancelled", {
7782
+ taskId,
7783
+ reason: err.message,
7784
+ partialOutput: finalOutput || ""
7785
+ });
7786
+ runError = null;
7787
+ } else {
7788
+ runError = err;
7789
+ throw err;
7790
+ }
7083
7791
  } finally {
7084
7792
  try {
7085
7793
  escalator.cancelAllPending();
@@ -7814,6 +8522,9 @@ var ModelsDisplay = ({
7814
8522
  });
7815
8523
  const title = step === "PROVIDER" ? "\u25C8 SELECT PROVIDER" : step === "TIER" ? `\u25C8 APPLY ${picked.provider === "auto" ? "AUTO" : String(picked.provider).toUpperCase()} TO WHICH TIER?` : `\u25C8 ${String(picked.provider).toUpperCase()} \u2192 SELECT MODEL FOR ${picked.tier}`;
7816
8524
  const breadcrumb = step === "PROVIDER" ? "Step 1 / 3" : step === "TIER" ? `Step 2 / 3 \xB7 provider: ${picked.provider}` : `Step 3 / 3 \xB7 ${picked.provider} \u2192 ${picked.tier}`;
8525
+ const PAGE_SIZE = 8;
8526
+ const viewStart = Math.max(0, Math.min(cursor - Math.floor(PAGE_SIZE / 2), currentItems.length - PAGE_SIZE));
8527
+ const visibleItems = currentItems.slice(viewStart, viewStart + PAGE_SIZE);
7817
8528
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7818
8529
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { justifyContent: "space-between", children: [
7819
8530
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, color: "cyan", children: title }),
@@ -7823,16 +8534,29 @@ var ModelsDisplay = ({
7823
8534
  breadcrumb,
7824
8535
  " \xB7 \u2191/\u2193 navigate \xB7 1\u20139 jump"
7825
8536
  ] }) }),
7826
- currentItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: currentItems.map((item, i) => {
7827
- const focused = i === cursor;
7828
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
7829
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
7830
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
7831
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
7832
- item.sublabel && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
7833
- ] })
7834
- ] }, `${step}-${item.value}-${i}`);
7835
- }) })
8537
+ currentItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
8538
+ viewStart > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "gray", dimColor: true, children: [
8539
+ " \u2191 ",
8540
+ viewStart,
8541
+ " more above"
8542
+ ] }),
8543
+ visibleItems.map((item, i) => {
8544
+ const globalIdx = viewStart + i;
8545
+ const focused = globalIdx === cursor;
8546
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
8547
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
8548
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
8549
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
8550
+ item.sublabel && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
8551
+ ] })
8552
+ ] }, `${step}-${item.value}-${globalIdx}`);
8553
+ }),
8554
+ viewStart + PAGE_SIZE < currentItems.length && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "gray", dimColor: true, children: [
8555
+ " \u2193 ",
8556
+ currentItems.length - viewStart - PAGE_SIZE,
8557
+ " more below"
8558
+ ] })
8559
+ ] })
7836
8560
  ] });
7837
8561
  };
7838
8562
  function CostTracker({
@@ -8026,13 +8750,18 @@ function replReducer(state, action) {
8026
8750
  async function refreshModelCache(store, providers) {
8027
8751
  for (const provider of providers) {
8028
8752
  try {
8029
- const dummyModel = { id: "dummy", name: "dummy", provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8753
+ const dummyId = provider.type === "azure" ? provider.deploymentName || "azure-model" : "dummy";
8754
+ const dummyModel = { id: dummyId, name: dummyId, provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8030
8755
  let instance;
8031
8756
  if (provider.type === "openai") instance = new OpenAIProvider(provider, dummyModel);
8032
8757
  else if (provider.type === "gemini") instance = new GeminiProvider(provider, dummyModel);
8033
8758
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
8034
8759
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
8035
8760
  else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
8761
+ else if (provider.type === "azure") {
8762
+ const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
8763
+ instance = new AzureOpenAIProvider2(provider, dummyModel);
8764
+ }
8036
8765
  if (instance) {
8037
8766
  const fetched = await instance.listModels();
8038
8767
  for (const m of fetched) store.upsertCachedModel(m);
@@ -8170,7 +8899,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName })
8170
8899
  };
8171
8900
  const store = new MemoryStore(path17__default.default.join(workspacePath, CASCADE_DB_FILE));
8172
8901
  storeRef.current = store;
8173
- globalStoreRef.current = new MemoryStore(path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8902
+ globalStoreRef.current = new MemoryStore(path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8174
8903
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
8175
8904
  setIdentities(identityRows);
8176
8905
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -8655,14 +9384,29 @@ Use /identity <name|id> to switch.`;
8655
9384
  const isAutoScrollingRef = React5.useRef(true);
8656
9385
  const width = stdout?.columns ?? 100;
8657
9386
  const height = stdout?.rows ?? 24;
8658
- const statusHeight = state.showDetails ? state.agentTree ? 10 : 4 : 3;
9387
+ const hasActiveOrFailed2 = (node) => {
9388
+ if (node.status === "ACTIVE" || node.status === "FAILED") return true;
9389
+ return node.children?.some(hasActiveOrFailed2) ?? false;
9390
+ };
9391
+ let agentTreeHeight = 0;
9392
+ if (state.agentTree && hasActiveOrFailed2(state.agentTree)) {
9393
+ agentTreeHeight = 1;
9394
+ const childrenCount = state.agentTree.children?.length ?? 0;
9395
+ agentTreeHeight += Math.min(childrenCount, 6);
9396
+ if (childrenCount > 6) agentTreeHeight += 1;
9397
+ }
9398
+ let timelineHeight = 0;
9399
+ if (state.showDetails && treeNodesRef.current.size > 0) {
9400
+ timelineHeight = 1;
9401
+ timelineHeight += Math.min(3, treeNodesRef.current.size);
9402
+ }
9403
+ const statusHeight = agentTreeHeight + timelineHeight;
8659
9404
  const costHeight = state.showCost ? 6 : 0;
8660
9405
  const approvalHeight = state.approvalRequest ? 12 : 0;
8661
9406
  const slashVisibleCount = Math.min(SLASH_PAGE_SIZE, slashEntries.length);
8662
9407
  const slashHeight = slashVisibleCount > 0 ? slashVisibleCount + 2 : 0;
8663
9408
  const chromeHeight = statusHeight + costHeight + approvalHeight + slashHeight + 7;
8664
- const totalCap = Math.floor(height * 0.7);
8665
- const availableHeight = Math.max(4, totalCap - chromeHeight);
9409
+ const availableHeight = Math.max(4, height - chromeHeight);
8666
9410
  const allLines = formatToLines(
8667
9411
  state.isStreaming ? [...state.messages, { id: "stream", role: "assistant", content: state.streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }] : state.messages,
8668
9412
  width - 4,
@@ -9043,10 +9787,14 @@ function wizardReducer(state, action) {
9043
9787
  return { ...state, currentEntryIdx: next };
9044
9788
  }
9045
9789
  case "ADD_AZURE": {
9790
+ const prevAzure = state.entries.find((e) => e.type === "azure");
9046
9791
  const newEntry = {
9047
9792
  id: crypto.randomUUID(),
9048
9793
  type: "azure",
9049
- label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`
9794
+ label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`,
9795
+ baseUrl: prevAzure?.baseUrl,
9796
+ apiKey: prevAzure?.apiKey,
9797
+ apiVersion: prevAzure?.apiVersion
9050
9798
  };
9051
9799
  return {
9052
9800
  ...state,
@@ -9155,8 +9903,9 @@ function SetupWizard({ workspacePath, onComplete }) {
9155
9903
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
9156
9904
  } else if (type === "azure") {
9157
9905
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
9158
- const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9159
- const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName }, dummyModel);
9906
+ const actualModelId = deploymentName || `azure-${entry.id}`;
9907
+ const dummyModel = { id: actualModelId, name: actualModelId, provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9908
+ const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName, apiVersion: entry.apiVersion }, dummyModel);
9160
9909
  const fetched = await p.listModels();
9161
9910
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
9162
9911
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
@@ -9177,7 +9926,8 @@ function SetupWizard({ workspacePath, onComplete }) {
9177
9926
  type: e.type,
9178
9927
  ...e.apiKey ? { apiKey: e.apiKey } : {},
9179
9928
  ...e.baseUrl ? { baseUrl: e.baseUrl } : {},
9180
- ...e.deploymentName ? { deploymentName: e.deploymentName } : {}
9929
+ ...e.deploymentName ? { deploymentName: e.deploymentName } : {},
9930
+ ...e.apiVersion ? { apiVersion: e.apiVersion } : {}
9181
9931
  }));
9182
9932
  const models = {};
9183
9933
  if (state.tierT1 !== "auto") models["t1"] = state.tierT1;
@@ -9207,17 +9957,17 @@ function SetupWizard({ workspacePath, onComplete }) {
9207
9957
  if (key.return) {
9208
9958
  if (state.selectedTypes.size === 0) return;
9209
9959
  dispatch({ type: "CONFIRM_PROVIDERS" });
9210
- setFieldStage("apiKey");
9960
+ const firstType = [...state.selectedTypes][0];
9961
+ setFieldStage(firstType === "azure" ? "deploymentName" : firstType === "openai-compatible" ? "label" : firstType === "ollama" ? "baseUrl" : "apiKey");
9211
9962
  setFieldBuffer("");
9212
9963
  }
9213
9964
  }
9214
9965
  if (state.step === "TIER_ASSIGN") {
9215
- if (key.tab || key.downArrow) {
9966
+ if (key.tab || key.rightArrow) {
9216
9967
  const order = ["T1", "T2", "T3"];
9217
9968
  const idx = order.indexOf(state.tierSelectFocus);
9218
9969
  dispatch({ type: "SET_TIER_FOCUS", tier: order[(idx + 1) % 3] });
9219
9970
  }
9220
- if (key.return) dispatch({ type: "GO_SAVE" });
9221
9971
  }
9222
9972
  });
9223
9973
  const currentEntry = state.entries[state.currentEntryIdx];
@@ -9227,7 +9977,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9227
9977
  if (fieldStage === "deploymentName") {
9228
9978
  dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
9229
9979
  setFieldBuffer("");
9230
- setFieldStage("baseUrl");
9980
+ if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
9981
+ setFieldStage("askMore");
9982
+ } else {
9983
+ setFieldStage("baseUrl");
9984
+ }
9231
9985
  } else if (fieldStage === "baseUrl") {
9232
9986
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
9233
9987
  setFieldBuffer("");
@@ -9235,6 +9989,10 @@ function SetupWizard({ workspacePath, onComplete }) {
9235
9989
  } else if (fieldStage === "apiKey") {
9236
9990
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9237
9991
  setFieldBuffer("");
9992
+ setFieldStage("apiVersion");
9993
+ } else if (fieldStage === "apiVersion") {
9994
+ dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
9995
+ setFieldBuffer("");
9238
9996
  setFieldStage("askMore");
9239
9997
  }
9240
9998
  } else if (currentEntry.type === "openai-compatible") {
@@ -9254,13 +10012,19 @@ function SetupWizard({ workspacePath, onComplete }) {
9254
10012
  } else if (currentEntry.type === "ollama") {
9255
10013
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
9256
10014
  setFieldBuffer("");
10015
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10016
+ if (nextEntry) {
10017
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10018
+ }
9257
10019
  dispatch({ type: "NEXT_ENTRY" });
9258
- setFieldStage("apiKey");
9259
10020
  } else {
9260
10021
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9261
10022
  setFieldBuffer("");
10023
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10024
+ if (nextEntry) {
10025
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10026
+ }
9262
10027
  dispatch({ type: "NEXT_ENTRY" });
9263
- setFieldStage("apiKey");
9264
10028
  }
9265
10029
  }, [currentEntry, fieldStage]);
9266
10030
  if (state.step === "PROVIDER_SELECT") {
@@ -9311,8 +10075,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9311
10075
  setFieldStage(isAzure ? "deploymentName" : "label");
9312
10076
  setFieldBuffer("");
9313
10077
  } else {
10078
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10079
+ if (nextEntry) {
10080
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10081
+ }
9314
10082
  dispatch({ type: "NEXT_ENTRY" });
9315
- setFieldStage("apiKey");
9316
10083
  setFieldBuffer("");
9317
10084
  }
9318
10085
  }
@@ -9393,7 +10160,16 @@ function SetupWizard({ workspacePath, onComplete }) {
9393
10160
  SelectInput__default.default,
9394
10161
  {
9395
10162
  items: modelOptions,
9396
- onSelect: (item) => dispatch({ type: "SET_TIER", tier, value: item.value }),
10163
+ onSelect: (item) => {
10164
+ dispatch({ type: "SET_TIER", tier, value: item.value });
10165
+ const order = ["T1", "T2", "T3"];
10166
+ const idx = order.indexOf(tier);
10167
+ if (idx < 2) {
10168
+ dispatch({ type: "SET_TIER_FOCUS", tier: order[idx + 1] });
10169
+ } else {
10170
+ dispatch({ type: "GO_SAVE" });
10171
+ }
10172
+ },
9397
10173
  indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9398
10174
  itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSelected ? "magenta" : "white", children: label })
9399
10175
  }
@@ -9871,7 +10647,7 @@ var DashboardServer = class {
9871
10647
  // ── Setup ─────────────────────────────────────
9872
10648
  getGlobalStore() {
9873
10649
  if (!this.globalStore) {
9874
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10650
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9875
10651
  this.globalStore = new MemoryStore(globalDbPath);
9876
10652
  }
9877
10653
  return this.globalStore;
@@ -9933,7 +10709,7 @@ var DashboardServer = class {
9933
10709
  }
9934
10710
  watchRuntimeChanges() {
9935
10711
  const workspaceDbPath = path17__default.default.join(this.workspacePath, CASCADE_DB_FILE);
9936
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10712
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9937
10713
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
9938
10714
  for (const watchPath of watchPaths) {
9939
10715
  if (!fs14__default.default.existsSync(watchPath)) continue;
@@ -10041,7 +10817,7 @@ var DashboardServer = class {
10041
10817
  const sessionId = req.params.id;
10042
10818
  this.store.deleteSession(sessionId);
10043
10819
  this.store.deleteRuntimeSession(sessionId);
10044
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10820
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10045
10821
  const globalStore = new MemoryStore(globalDbPath);
10046
10822
  try {
10047
10823
  globalStore.deleteRuntimeSession(sessionId);
@@ -10055,7 +10831,7 @@ var DashboardServer = class {
10055
10831
  });
10056
10832
  this.app.delete("/api/sessions", auth, (req, res) => {
10057
10833
  const body = req.body;
10058
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10834
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10059
10835
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
10060
10836
  const globalStore = new MemoryStore(globalDbPath);
10061
10837
  try {
@@ -10078,7 +10854,7 @@ var DashboardServer = class {
10078
10854
  });
10079
10855
  this.app.delete("/api/runtime", auth, (_req, res) => {
10080
10856
  this.store.deleteAllRuntimeNodes();
10081
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10857
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10082
10858
  const globalStore = new MemoryStore(globalDbPath);
10083
10859
  try {
10084
10860
  globalStore.deleteAllRuntimeNodes();
@@ -10174,7 +10950,7 @@ var DashboardServer = class {
10174
10950
  this.app.get("/api/runtime", auth, (req, res) => {
10175
10951
  const scope = req.query["scope"] ?? "workspace";
10176
10952
  if (scope === "global") {
10177
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10953
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10178
10954
  const globalStore = new MemoryStore(globalDbPath);
10179
10955
  try {
10180
10956
  res.json({
@@ -10424,7 +11200,15 @@ async function exportCommand(options = {}) {
10424
11200
  const spin = ora__default.default({ text: "Loading sessions\u2026", color: "magenta" }).start();
10425
11201
  let store;
10426
11202
  try {
10427
- const dbPath = path17__default.default.join(process.env["HOME"] ?? "~", GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11203
+ const workspacePath = options.workspacePath ?? process.cwd();
11204
+ const workspaceDbPath = path17__default.default.join(workspacePath, CASCADE_DB_FILE);
11205
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11206
+ let dbPath = globalDbPath;
11207
+ try {
11208
+ await fs7__default.default.access(workspaceDbPath);
11209
+ dbPath = workspaceDbPath;
11210
+ } catch {
11211
+ }
10428
11212
  store = new MemoryStore(dbPath);
10429
11213
  } catch (err) {
10430
11214
  spin.fail(chalk8__default.default.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
@@ -10563,6 +11347,15 @@ async function telemetryCommand(action) {
10563
11347
 
10564
11348
  // src/cli/index.ts
10565
11349
  dotenv__default.default.config();
11350
+ process.on("exit", () => McpClient.killAllProcesses());
11351
+ process.on("SIGINT", () => {
11352
+ McpClient.killAllProcesses();
11353
+ process.exit(0);
11354
+ });
11355
+ process.on("SIGTERM", () => {
11356
+ McpClient.killAllProcesses();
11357
+ process.exit(0);
11358
+ });
10566
11359
  var program = new commander.Command();
10567
11360
  program.name("cascade").description("Multi-tier AI orchestration CLI").version(CASCADE_VERSION, "-v, --version").option("-p, --prompt <text>", "Run a single prompt non-interactively").option("-t, --theme <name>", "Color theme", DEFAULT_THEME).option("-w, --workspace <path>", "Workspace path", process.cwd()).option("-i, --identity <name>", "Identity name or ID").option("--no-color", "Disable colors").action(async (options) => {
10568
11361
  await startRepl(options);