cascade-ai 0.2.1 → 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.0";
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);
@@ -3854,14 +4027,13 @@ Now execute your subtask using this context where relevant.`
3854
4027
  await this.peerBus.barrier(this.id, barrierName, total);
3855
4028
  }
3856
4029
  receivePeerSync(fromId, content) {
3857
- const existing = this.peerSyncBuffer.find((p) => p.fromId === fromId);
3858
- if (existing) {
3859
- existing.content = content;
3860
- existing.timestamp = (/* @__PURE__ */ new Date()).toISOString();
3861
- } else {
3862
- this.peerSyncBuffer.push({ fromId, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3863
- }
4030
+ this.peerSyncBuffer.push({ fromId, content, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3864
4031
  this.emit("peer-sync-received", { fromId, content });
4032
+ this.context.addMessage({
4033
+ role: "user",
4034
+ content: `[SYSTEM_NOTIFICATION]: You received a new peer message from ${fromId}. Use the "peer_message" tool with action="receive" to read it.`
4035
+ }).catch(() => {
4036
+ });
3865
4037
  }
3866
4038
  // ── Private ──────────────────────────────────
3867
4039
  async runAgentLoop(systemPrompt, tools) {
@@ -3873,6 +4045,7 @@ Now execute your subtask using this context where relevant.`
3873
4045
  tools = [...tools];
3874
4046
  while (iterations < MAX_ITERATIONS) {
3875
4047
  iterations++;
4048
+ this.throwIfCancelled();
3876
4049
  const options = {
3877
4050
  messages: this.context.getMessages(),
3878
4051
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -3893,21 +4066,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3893
4066
  if (requiresArtifact) {
3894
4067
  stalledArtifactIterations += 1;
3895
4068
  if (stalledArtifactIterations >= 2) {
3896
- if (this.toolCreator && stalledArtifactIterations === 2) {
3897
- const toolName = await this.toolCreator.createTool(
3898
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
3899
- this.assignment?.description ?? ""
3900
- );
3901
- if (toolName) {
3902
- tools = this.toolRegistry.getToolDefinitions();
3903
- this.sendStatusUpdate({
3904
- progressPct: 50,
3905
- currentAction: `Dynamic tool created: ${toolName}`,
3906
- status: "IN_PROGRESS"
3907
- });
3908
- this.emit("tool:created", { tierId: this.id, toolName });
3909
- continue;
3910
- }
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"}`);
3911
4071
  }
3912
4072
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
3913
4073
  }
@@ -3992,7 +4152,11 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3992
4152
  sendPeerSync: (to, syncType, content) => {
3993
4153
  this.peerBus?.send(this.id, to, syncType, this.assignment?.subtaskId ?? "", content);
3994
4154
  },
3995
- getPeerMessages: () => [...this.peerSyncBuffer]
4155
+ getPeerMessages: () => {
4156
+ const msgs = [...this.peerSyncBuffer];
4157
+ this.peerSyncBuffer = [];
4158
+ return msgs;
4159
+ }
3996
4160
  });
3997
4161
  if (this.audit) {
3998
4162
  this.audit.toolCall(this.id, tc.name, tc.input);
@@ -4063,6 +4227,9 @@ ${assignment.expectedOutput}`;
4063
4227
  const artifactPaths = this.extractArtifactPaths(assignment);
4064
4228
  if (!artifactPaths.length) return { ok: true, issues: [] };
4065
4229
  const issues = [];
4230
+ const { exec: exec4 } = await import('child_process');
4231
+ const { promisify: promisify3 } = await import('util');
4232
+ const execAsync3 = promisify3(exec4);
4066
4233
  for (const artifactPath of artifactPaths) {
4067
4234
  const absolutePath = path17__default.default.resolve(process.cwd(), artifactPath);
4068
4235
  try {
@@ -4079,9 +4246,27 @@ ${assignment.expectedOutput}`;
4079
4246
  const content = await fs7__default.default.readFile(absolutePath, "utf-8");
4080
4247
  if (!content.trim()) {
4081
4248
  issues.push(`Artifact content is empty: ${artifactPath}`);
4249
+ continue;
4082
4250
  }
4083
4251
  } else if (stat.size < 100) {
4084
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}`);
4085
4270
  }
4086
4271
  } catch {
4087
4272
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -4478,7 +4663,8 @@ var T2Manager = class extends BaseTier {
4478
4663
  });
4479
4664
  this.emit("peer-sync-received", { fromId, content });
4480
4665
  }
4481
- async execute(assignment, taskId) {
4666
+ async execute(assignment, taskId, signal) {
4667
+ this.signal = signal;
4482
4668
  this.assignment = assignment;
4483
4669
  this.taskId = taskId;
4484
4670
  this.setLabel(assignment.sectionTitle);
@@ -4490,12 +4676,14 @@ var T2Manager = class extends BaseTier {
4490
4676
  });
4491
4677
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
4492
4678
  try {
4679
+ this.throwIfCancelled();
4493
4680
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
4494
4681
  this.sendStatusUpdate({
4495
4682
  progressPct: 20,
4496
4683
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
4497
4684
  status: "IN_PROGRESS"
4498
4685
  });
4686
+ this.throwIfCancelled();
4499
4687
  const t3Results = await this.executeSubtasks(subtasks, taskId);
4500
4688
  this.sendStatusUpdate({
4501
4689
  progressPct: 90,
@@ -4534,13 +4722,17 @@ var T2Manager = class extends BaseTier {
4534
4722
  }
4535
4723
  // ── Private ──────────────────────────────────
4536
4724
  async decomposeSection(assignment) {
4725
+ const peerPlans = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_PLAN_ANNOUNCEMENT").map((p) => `[Peer ${p.fromId} Plan]: ${p.content.sectionTitle} - ${p.content.subtaskTitles?.join(", ")}`).join("\n");
4537
4726
  const prompt = `Decompose this section into 2-5 concrete subtasks for T3 workers.
4538
4727
 
4539
4728
  Section: ${assignment.sectionTitle}
4540
4729
  Description: ${assignment.description}
4541
4730
  Expected output: ${assignment.expectedOutput}
4542
4731
  Constraints: ${assignment.constraints.join("; ")}
4543
-
4732
+ ${peerPlans ? `
4733
+ Context from sibling T2 plans (use this to align execution and avoid overlaps):
4734
+ ${peerPlans}
4735
+ ` : ""}
4544
4736
  Return a JSON array of subtask objects, each with:
4545
4737
  - subtaskId: string (unique)
4546
4738
  - subtaskTitle: string
@@ -4658,11 +4850,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4658
4850
  ).join(", ")}`,
4659
4851
  status: "IN_PROGRESS"
4660
4852
  });
4853
+ this.throwIfCancelled();
4661
4854
  const waveResults = await Promise.allSettled(
4662
4855
  runnableIds.map(async (id) => {
4663
4856
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4664
4857
  const worker = workerMap.get(id);
4665
- const result = await worker.execute(assignment, taskId);
4858
+ const result = await worker.execute(assignment, taskId, this.signal);
4666
4859
  resultMap.set(id, result);
4667
4860
  return result;
4668
4861
  })
@@ -4676,6 +4869,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4676
4869
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4677
4870
  const retried = await this.retryT3(assignment, taskId);
4678
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
+ }
4679
4926
  }
4680
4927
  for (const dependent of adj.get(id) ?? []) {
4681
4928
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -4739,7 +4986,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4739
4986
  }));
4740
4987
  return worker.execute(
4741
4988
  { ...assignment, description: `[RETRY] ${assignment.description}` },
4742
- taskId
4989
+ taskId,
4990
+ this.signal
4743
4991
  );
4744
4992
  }
4745
4993
  publishSectionOutput(result) {
@@ -4753,24 +5001,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4753
5001
  async aggregateResults(assignment, results) {
4754
5002
  const completed = results.filter((r) => r.status === "COMPLETED");
4755
5003
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
4756
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
4757
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
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");
5005
+ const peerContext = peerOutputs ? `
5006
+
5007
+ Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
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}
4758
5017
 
4759
- ${outputs}`;
4760
- const messages = [{ role: "user", content: prompt }];
4761
- try {
4762
- const result = await this.router.generate("T2", {
4763
- messages,
4764
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
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 ? `
4765
5038
 
4766
5039
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4767
- maxTokens: 300
4768
- });
4769
- return result.content;
4770
- } catch (err) {
4771
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
4772
- 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
+ }
4773
5047
  }
5048
+ return currentSummary;
4774
5049
  }
4775
5050
  determineStatus(results) {
4776
5051
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -4898,10 +5173,10 @@ Rules:
4898
5173
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
4899
5174
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
4900
5175
 
4901
- EXECUTION MODE GUIDANCE:
4902
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
4903
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
4904
- - 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.
4905
5180
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
4906
5181
 
4907
5182
  QUALITY RULES:
@@ -4940,7 +5215,8 @@ var T1Administrator = class extends BaseTier {
4940
5215
  setToolCreator(creator) {
4941
5216
  this.toolCreator = creator;
4942
5217
  }
4943
- async execute(userPrompt, images, systemContext) {
5218
+ async execute(userPrompt, images, systemContext, signal) {
5219
+ this.signal = signal;
4944
5220
  this.taskId = crypto.randomUUID();
4945
5221
  this.setLabel("Administrator");
4946
5222
  this.setStatus("ACTIVE");
@@ -4951,10 +5227,12 @@ var T1Administrator = class extends BaseTier {
4951
5227
  status: "IN_PROGRESS"
4952
5228
  });
4953
5229
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
5230
+ this.throwIfCancelled();
4954
5231
  let enrichedPrompt = userPrompt;
4955
5232
  if (images?.length) {
4956
5233
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
4957
5234
  }
5235
+ this.throwIfCancelled();
4958
5236
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
4959
5237
  this.sendStatusUpdate({
4960
5238
  progressPct: 10,
@@ -4962,21 +5240,83 @@ var T1Administrator = class extends BaseTier {
4962
5240
  status: "IN_PROGRESS"
4963
5241
  });
4964
5242
  this.emit("plan", { taskId: this.taskId, plan });
4965
- 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
+ }
4966
5269
  this.sendStatusUpdate({
4967
5270
  progressPct: 95,
4968
5271
  currentAction: "Compiling final output",
4969
5272
  status: "IN_PROGRESS"
4970
5273
  });
4971
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
5274
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
4972
5275
  this.setStatus("COMPLETED");
4973
5276
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
4974
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
5277
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
4975
5278
  }
4976
5279
  getEscalations() {
4977
5280
  return [...this.escalations];
4978
5281
  }
4979
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
+ }
4980
5320
  async analyzeImages(prompt, images) {
4981
5321
  const visionModel = this.router.getModelForTier("T1");
4982
5322
  if (!visionModel?.isVisionCapable) return prompt;
@@ -5005,29 +5345,35 @@ ${systemContext}` : "";
5005
5345
  Example: if asked to create files "inside python_exclusive", every subtask that
5006
5346
  creates a file must use "python_exclusive/filename.ext" as the path.
5007
5347
 
5008
- Return JSON where subtasks can declare dependencies:
5348
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
5009
5349
  {
5010
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
5011
5357
  "t3Subtasks": [{
5012
5358
  "subtaskId": "t1",
5013
- "subtaskTitle": "Generate Source Code",
5014
- "dependsOn": [], // \u2190 empty = runs immediately
5015
- "executionMode": "parallel"
5016
- }, {
5017
- "subtaskId": "t2",
5018
- "subtaskTitle": "Save Code to File",
5019
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
5020
- "executionMode": "parallel"
5021
- }, {
5022
- "subtaskId": "t3",
5023
- "subtaskTitle": "Execute and Verify",
5024
- "dependsOn": ["t2"], // \u2190 waits for t2
5025
- "executionMode": "parallel"
5359
+ "subtaskTitle": "Init NPM",
5360
+ "description": "Run npm init",
5361
+ "expectedOutput": "package.json created",
5362
+ "constraints": [],
5363
+ "dependsOn": []
5026
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": [...]
5027
5373
  }]
5028
5374
  }
5029
- Use dependsOn when a subtask needs the output of a previous one.
5030
- 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.`;
5031
5377
  const messages = [{ role: "user", content: decompositionPrompt }];
5032
5378
  const result = await this.router.generate("T1", {
5033
5379
  messages,
@@ -5155,92 +5501,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5155
5501
  ].filter(Boolean).join(" ");
5156
5502
  m.setHierarchyContext(context);
5157
5503
  });
5158
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
5159
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
5160
- for (const section of sections) {
5161
- if (overlapSections.has(section.sectionId)) {
5162
- 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]];
5163
5511
  }
5164
5512
  }
5165
5513
  }
5166
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
5167
- const isSequential = sections.some((s) => s.executionMode === "sequential");
5168
5514
  const t2Results = [];
5169
5515
  try {
5170
- if (isSequential) {
5171
- this.log("Dispatching T2 managers sequentially");
5172
- for (let i = 0; i < managers.length; i++) {
5173
- const m = managers[i];
5174
- this.sendStatusUpdate({
5175
- progressPct: pct(i),
5176
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
5177
- status: "IN_PROGRESS"
5178
- });
5179
- try {
5180
- const result = await m.execute(sections[i], this.taskId);
5181
- t2Results.push(result);
5182
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
5183
- if (result.status === "ESCALATED") {
5184
- this.escalations.push({
5185
- raisedBy: `T2_${sections[i].sectionId}`,
5186
- sectionId: sections[i].sectionId,
5187
- attempted: result.issues,
5188
- blocker: result.issues.join("; "),
5189
- needs: "Human review required"
5190
- });
5191
- }
5192
- } catch (err) {
5193
- t2Results.push({
5194
- sectionId: sections[i].sectionId,
5195
- sectionTitle: sections[i].sectionTitle,
5196
- status: "FAILED",
5197
- t3Results: [],
5198
- sectionSummary: "",
5199
- issues: [err instanceof Error ? err.message : String(err)]
5200
- });
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);
5201
5563
  }
5564
+ s.dependsOn = safeDeps;
5202
5565
  }
5203
- } else {
5204
- const results = await Promise.allSettled(
5205
- managers.map((m, i) => {
5206
- this.sendStatusUpdate({
5207
- progressPct: pct(i),
5208
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
5209
- status: "IN_PROGRESS"
5210
- });
5211
- return m.execute(sections[i], this.taskId);
5212
- })
5213
- );
5214
- for (let i = 0; i < results.length; i++) {
5215
- const r = results[i];
5216
- if (r.status === "fulfilled") {
5217
- t2Results.push(r.value);
5218
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
5219
- if (r.value.status === "ESCALATED") {
5220
- this.escalations.push({
5221
- raisedBy: `T2_${sections[i].sectionId}`,
5222
- sectionId: sections[i].sectionId,
5223
- attempted: r.value.issues,
5224
- blocker: r.value.issues.join("; "),
5225
- needs: "Human review required"
5226
- });
5227
- }
5228
- } else {
5229
- t2Results.push({
5230
- sectionId: sections[i].sectionId,
5231
- sectionTitle: sections[i].sectionTitle,
5232
- status: "FAILED",
5233
- t3Results: [],
5234
- sectionSummary: "",
5235
- 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"
5236
5601
  });
5237
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
+ };
5238
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();
5239
5621
  }
5240
- } finally {
5241
- cleanup();
5242
- }
5243
- return t2Results;
5622
+ };
5623
+ await executeWave();
5624
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
5244
5625
  }
5245
5626
  async compileFinalOutput(originalPrompt, plan, t2Results) {
5246
5627
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -5690,13 +6071,47 @@ var GitHubTool = class extends BaseTool {
5690
6071
  }
5691
6072
  async execute(input, _options) {
5692
6073
  const platform = input["platform"] ?? "github";
5693
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
5694
6074
  const operation = input["operation"];
5695
6075
  const repo = input["repo"];
5696
- if (platform === "github") {
5697
- 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)}`;
5698
6114
  }
5699
- return this.executeGitLab(operation, repo, token, input);
5700
6115
  }
5701
6116
  async executeGitHub(operation, repo, token, input) {
5702
6117
  const headers = {
@@ -5783,6 +6198,7 @@ ${response.data.description}`;
5783
6198
  };
5784
6199
 
5785
6200
  // src/tools/browser.ts
6201
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
5786
6202
  var BrowserTool = class extends BaseTool {
5787
6203
  name = "browser";
5788
6204
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -5791,7 +6207,7 @@ var BrowserTool = class extends BaseTool {
5791
6207
  properties: {
5792
6208
  action: {
5793
6209
  type: "string",
5794
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
6210
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
5795
6211
  },
5796
6212
  url: { type: "string", description: "URL to navigate to" },
5797
6213
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -5811,53 +6227,86 @@ var BrowserTool = class extends BaseTool {
5811
6227
  try {
5812
6228
  playwright = await import('playwright');
5813
6229
  } catch {
5814
- 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";
5815
6231
  }
5816
- if (!this.browser) {
5817
- const pw = playwright;
5818
- this.browser = await pw.chromium.launch({ headless: true });
5819
- const b = this.browser;
5820
- this.page = await b.newPage();
5821
- }
5822
- const page = this.page;
5823
6232
  const action = input["action"];
5824
6233
  const timeout = input["timeout"] ?? 1e4;
5825
- switch (action) {
5826
- case "navigate": {
5827
- await page.goto(input["url"], { timeout });
5828
- return `Navigated to ${input["url"]}`;
5829
- }
5830
- case "click": {
5831
- await page.click(input["selector"], { timeout });
5832
- return `Clicked ${input["selector"]}`;
5833
- }
5834
- case "fill": {
5835
- await page.fill(input["selector"], input["value"]);
5836
- return `Filled ${input["selector"]} with value`;
5837
- }
5838
- case "screenshot": {
5839
- const buf = await page.screenshot({ type: "png" });
5840
- return `data:image/png;base64,${buf.toString("base64")}`;
5841
- }
5842
- case "evaluate": {
5843
- const result = await page.evaluate(input["script"]);
5844
- 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)}`;
5845
6251
  }
5846
- case "extract_text": {
5847
- const text = await page.locator("body").innerText();
5848
- 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`;
5849
6287
  }
5850
- case "wait": {
5851
- await page.waitForTimeout(timeout);
5852
- 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}`;
5853
6293
  }
5854
- default:
5855
- throw new Error(`Unknown browser action: ${action}`);
6294
+ return `Browser action "${action}" failed: ${errMsg}`;
5856
6295
  }
5857
6296
  }
5858
6297
  async close() {
5859
- if (this.browser) {
5860
- 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 {
5861
6310
  this.browser = null;
5862
6311
  this.page = null;
5863
6312
  }
@@ -5954,6 +6403,19 @@ var PDFCreateTool = class extends BaseTool {
5954
6403
  });
5955
6404
  }
5956
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"]);
5957
6419
  var CodeInterpreterTool = class extends BaseTool {
5958
6420
  name = "run_code";
5959
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.";
@@ -5969,10 +6431,30 @@ var CodeInterpreterTool = class extends BaseTool {
5969
6431
  isDangerous() {
5970
6432
  return true;
5971
6433
  }
5972
- async execute(input, options) {
6434
+ async execute(input, _options) {
5973
6435
  const language = input["language"];
5974
6436
  const code = input["code"];
5975
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
+ }
5976
6458
  const tmpDir = path17__default.default.join(process.cwd(), ".cascade", "tmp");
5977
6459
  if (!fs14__default.default.existsSync(tmpDir)) {
5978
6460
  fs14__default.default.mkdirSync(tmpDir, { recursive: true });
@@ -5981,8 +6463,9 @@ var CodeInterpreterTool = class extends BaseTool {
5981
6463
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
5982
6464
  const filePath = path17__default.default.join(tmpDir, fileName);
5983
6465
  fs14__default.default.writeFileSync(filePath, code, "utf-8");
5984
- const cmdPrefix = language === "python" ? "python3" : "node";
5985
- 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 : ""}`;
5986
6469
  return new Promise((resolve) => {
5987
6470
  const startMs = Date.now();
5988
6471
  child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -5995,10 +6478,17 @@ var CodeInterpreterTool = class extends BaseTool {
5995
6478
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
5996
6479
  }
5997
6480
  if (error) {
5998
- 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):
5999
6488
  Error: ${error.message}
6000
6489
  Stderr: ${stderr}
6001
6490
  Stdout: ${stdout}`);
6491
+ }
6002
6492
  } else {
6003
6493
  resolve(`Execution successful (${duration}ms):
6004
6494
  Stdout: ${stdout}
@@ -6063,6 +6553,186 @@ ${formatted}`;
6063
6553
  }
6064
6554
  };
6065
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
+
6066
6736
  // src/tools/mcp.ts
6067
6737
  var McpToolWrapper = class extends BaseTool {
6068
6738
  name;
@@ -6184,7 +6854,8 @@ var ToolRegistry = class {
6184
6854
  new ImageAnalyzeTool(),
6185
6855
  new PDFCreateTool(),
6186
6856
  new CodeInterpreterTool(),
6187
- new PeerCommunicationTool()
6857
+ new PeerCommunicationTool(),
6858
+ new WebSearchTool(this.config.webSearch)
6188
6859
  ];
6189
6860
  for (const tool of tools) {
6190
6861
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -6208,8 +6879,23 @@ var ToolRegistry = class {
6208
6879
  return this.ignoreMatcher.ignores(posixRel);
6209
6880
  }
6210
6881
  };
6211
- 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
+ }
6212
6897
  clients = /* @__PURE__ */ new Map();
6898
+ transports = /* @__PURE__ */ new Map();
6213
6899
  tools = /* @__PURE__ */ new Map();
6214
6900
  trustedServers;
6215
6901
  approvalCallback;
@@ -6238,6 +6924,8 @@ var McpClient = class {
6238
6924
  );
6239
6925
  await client.connect(transport);
6240
6926
  this.clients.set(server.name, client);
6927
+ this.transports.set(server.name, transport);
6928
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
6241
6929
  const toolsResult = await client.listTools();
6242
6930
  for (const tool of toolsResult.tools) {
6243
6931
  for (const existing of this.tools.values()) {
@@ -6259,8 +6947,11 @@ var McpClient = class {
6259
6947
  async disconnect(serverName) {
6260
6948
  const client = this.clients.get(serverName);
6261
6949
  if (client) {
6950
+ const transport = this.transports.get(serverName);
6951
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
6262
6952
  await client.close();
6263
6953
  this.clients.delete(serverName);
6954
+ this.transports.delete(serverName);
6264
6955
  for (const key of this.tools.keys()) {
6265
6956
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
6266
6957
  }
@@ -6288,6 +6979,13 @@ var McpClient = class {
6288
6979
  getConnectedServers() {
6289
6980
  return Array.from(this.clients.keys());
6290
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
+ }
6291
6989
  isConnected(serverName) {
6292
6990
  return this.clients.has(serverName);
6293
6991
  }
@@ -6856,12 +7554,25 @@ var Cascade = class extends EventEmitter__default.default {
6856
7554
  looksLikeSimpleArtifactTask(prompt) {
6857
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);
6858
7556
  }
6859
- async determineComplexity(prompt, conversationHistory = []) {
7557
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
6860
7558
  if (this.looksLikeSimpleArtifactTask(prompt)) {
6861
7559
  return "Simple";
6862
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
+ }
6863
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.
6864
7573
 
7574
+ ${workspaceContext}
7575
+
6865
7576
  Classification:
6866
7577
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
6867
7578
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -6936,7 +7647,7 @@ ${prompt}` : prompt;
6936
7647
  }
6937
7648
  escalator.resolveUserDecision(req.id, approved, always);
6938
7649
  });
6939
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
7650
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
6940
7651
  this.telemetry.capture("cascade:session_start", {
6941
7652
  complexity,
6942
7653
  providerCount: this.config.providers.length,
@@ -7016,7 +7727,7 @@ ${prompt}` : prompt;
7016
7727
  peerT3Ids: [],
7017
7728
  parentT2: "root"
7018
7729
  };
7019
- const t3Result = await t3.execute(assignment, taskId);
7730
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
7020
7731
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
7021
7732
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
7022
7733
  } else if (complexity === "Moderate") {
@@ -7039,7 +7750,7 @@ ${prompt}` : prompt;
7039
7750
  constraints: [],
7040
7751
  t3Subtasks: []
7041
7752
  };
7042
- const t2Result = await t2.execute(assignment, taskId);
7753
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
7043
7754
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
7044
7755
  t2Results = [t2Result];
7045
7756
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -7061,13 +7772,22 @@ ${prompt}` : prompt;
7061
7772
  if (toolCreator) t1.setToolCreator(toolCreator);
7062
7773
  bindTierEvents(t1);
7063
7774
  t1.on("plan", (e) => this.emit("plan", e));
7064
- const result = await t1.execute(options.prompt, options.images);
7775
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
7065
7776
  finalOutput = result.output;
7066
7777
  t2Results = result.t2Results;
7067
7778
  }
7068
7779
  } catch (err) {
7069
- runError = err;
7070
- 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
+ }
7071
7791
  } finally {
7072
7792
  try {
7073
7793
  escalator.cancelAllPending();
@@ -7802,6 +8522,9 @@ var ModelsDisplay = ({
7802
8522
  });
7803
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}`;
7804
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);
7805
8528
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7806
8529
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { justifyContent: "space-between", children: [
7807
8530
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, color: "cyan", children: title }),
@@ -7811,16 +8534,29 @@ var ModelsDisplay = ({
7811
8534
  breadcrumb,
7812
8535
  " \xB7 \u2191/\u2193 navigate \xB7 1\u20139 jump"
7813
8536
  ] }) }),
7814
- 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) => {
7815
- const focused = i === cursor;
7816
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
7817
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
7818
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
7819
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
7820
- item.sublabel && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
7821
- ] })
7822
- ] }, `${step}-${item.value}-${i}`);
7823
- }) })
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
+ ] })
7824
8560
  ] });
7825
8561
  };
7826
8562
  function CostTracker({
@@ -8014,13 +8750,18 @@ function replReducer(state, action) {
8014
8750
  async function refreshModelCache(store, providers) {
8015
8751
  for (const provider of providers) {
8016
8752
  try {
8017
- 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 };
8018
8755
  let instance;
8019
8756
  if (provider.type === "openai") instance = new OpenAIProvider(provider, dummyModel);
8020
8757
  else if (provider.type === "gemini") instance = new GeminiProvider(provider, dummyModel);
8021
8758
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
8022
8759
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
8023
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
+ }
8024
8765
  if (instance) {
8025
8766
  const fetched = await instance.listModels();
8026
8767
  for (const m of fetched) store.upsertCachedModel(m);
@@ -8158,7 +8899,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName })
8158
8899
  };
8159
8900
  const store = new MemoryStore(path17__default.default.join(workspacePath, CASCADE_DB_FILE));
8160
8901
  storeRef.current = store;
8161
- 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));
8162
8903
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
8163
8904
  setIdentities(identityRows);
8164
8905
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -8643,14 +9384,29 @@ Use /identity <name|id> to switch.`;
8643
9384
  const isAutoScrollingRef = React5.useRef(true);
8644
9385
  const width = stdout?.columns ?? 100;
8645
9386
  const height = stdout?.rows ?? 24;
8646
- 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;
8647
9404
  const costHeight = state.showCost ? 6 : 0;
8648
9405
  const approvalHeight = state.approvalRequest ? 12 : 0;
8649
9406
  const slashVisibleCount = Math.min(SLASH_PAGE_SIZE, slashEntries.length);
8650
9407
  const slashHeight = slashVisibleCount > 0 ? slashVisibleCount + 2 : 0;
8651
9408
  const chromeHeight = statusHeight + costHeight + approvalHeight + slashHeight + 7;
8652
- const totalCap = Math.floor(height * 0.7);
8653
- const availableHeight = Math.max(4, totalCap - chromeHeight);
9409
+ const availableHeight = Math.max(4, height - chromeHeight);
8654
9410
  const allLines = formatToLines(
8655
9411
  state.isStreaming ? [...state.messages, { id: "stream", role: "assistant", content: state.streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }] : state.messages,
8656
9412
  width - 4,
@@ -9031,10 +9787,14 @@ function wizardReducer(state, action) {
9031
9787
  return { ...state, currentEntryIdx: next };
9032
9788
  }
9033
9789
  case "ADD_AZURE": {
9790
+ const prevAzure = state.entries.find((e) => e.type === "azure");
9034
9791
  const newEntry = {
9035
9792
  id: crypto.randomUUID(),
9036
9793
  type: "azure",
9037
- 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
9038
9798
  };
9039
9799
  return {
9040
9800
  ...state,
@@ -9143,8 +9903,9 @@ function SetupWizard({ workspacePath, onComplete }) {
9143
9903
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
9144
9904
  } else if (type === "azure") {
9145
9905
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
9146
- const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9147
- 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);
9148
9909
  const fetched = await p.listModels();
9149
9910
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
9150
9911
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
@@ -9165,7 +9926,8 @@ function SetupWizard({ workspacePath, onComplete }) {
9165
9926
  type: e.type,
9166
9927
  ...e.apiKey ? { apiKey: e.apiKey } : {},
9167
9928
  ...e.baseUrl ? { baseUrl: e.baseUrl } : {},
9168
- ...e.deploymentName ? { deploymentName: e.deploymentName } : {}
9929
+ ...e.deploymentName ? { deploymentName: e.deploymentName } : {},
9930
+ ...e.apiVersion ? { apiVersion: e.apiVersion } : {}
9169
9931
  }));
9170
9932
  const models = {};
9171
9933
  if (state.tierT1 !== "auto") models["t1"] = state.tierT1;
@@ -9195,17 +9957,17 @@ function SetupWizard({ workspacePath, onComplete }) {
9195
9957
  if (key.return) {
9196
9958
  if (state.selectedTypes.size === 0) return;
9197
9959
  dispatch({ type: "CONFIRM_PROVIDERS" });
9198
- setFieldStage("apiKey");
9960
+ const firstType = [...state.selectedTypes][0];
9961
+ setFieldStage(firstType === "azure" ? "deploymentName" : firstType === "openai-compatible" ? "label" : firstType === "ollama" ? "baseUrl" : "apiKey");
9199
9962
  setFieldBuffer("");
9200
9963
  }
9201
9964
  }
9202
9965
  if (state.step === "TIER_ASSIGN") {
9203
- if (key.tab || key.downArrow) {
9966
+ if (key.tab || key.rightArrow) {
9204
9967
  const order = ["T1", "T2", "T3"];
9205
9968
  const idx = order.indexOf(state.tierSelectFocus);
9206
9969
  dispatch({ type: "SET_TIER_FOCUS", tier: order[(idx + 1) % 3] });
9207
9970
  }
9208
- if (key.return) dispatch({ type: "GO_SAVE" });
9209
9971
  }
9210
9972
  });
9211
9973
  const currentEntry = state.entries[state.currentEntryIdx];
@@ -9215,7 +9977,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9215
9977
  if (fieldStage === "deploymentName") {
9216
9978
  dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
9217
9979
  setFieldBuffer("");
9218
- setFieldStage("baseUrl");
9980
+ if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
9981
+ setFieldStage("askMore");
9982
+ } else {
9983
+ setFieldStage("baseUrl");
9984
+ }
9219
9985
  } else if (fieldStage === "baseUrl") {
9220
9986
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
9221
9987
  setFieldBuffer("");
@@ -9223,6 +9989,10 @@ function SetupWizard({ workspacePath, onComplete }) {
9223
9989
  } else if (fieldStage === "apiKey") {
9224
9990
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9225
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("");
9226
9996
  setFieldStage("askMore");
9227
9997
  }
9228
9998
  } else if (currentEntry.type === "openai-compatible") {
@@ -9242,13 +10012,19 @@ function SetupWizard({ workspacePath, onComplete }) {
9242
10012
  } else if (currentEntry.type === "ollama") {
9243
10013
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
9244
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
+ }
9245
10019
  dispatch({ type: "NEXT_ENTRY" });
9246
- setFieldStage("apiKey");
9247
10020
  } else {
9248
10021
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9249
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
+ }
9250
10027
  dispatch({ type: "NEXT_ENTRY" });
9251
- setFieldStage("apiKey");
9252
10028
  }
9253
10029
  }, [currentEntry, fieldStage]);
9254
10030
  if (state.step === "PROVIDER_SELECT") {
@@ -9299,8 +10075,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9299
10075
  setFieldStage(isAzure ? "deploymentName" : "label");
9300
10076
  setFieldBuffer("");
9301
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
+ }
9302
10082
  dispatch({ type: "NEXT_ENTRY" });
9303
- setFieldStage("apiKey");
9304
10083
  setFieldBuffer("");
9305
10084
  }
9306
10085
  }
@@ -9381,7 +10160,16 @@ function SetupWizard({ workspacePath, onComplete }) {
9381
10160
  SelectInput__default.default,
9382
10161
  {
9383
10162
  items: modelOptions,
9384
- 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
+ },
9385
10173
  indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9386
10174
  itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSelected ? "magenta" : "white", children: label })
9387
10175
  }
@@ -9859,7 +10647,7 @@ var DashboardServer = class {
9859
10647
  // ── Setup ─────────────────────────────────────
9860
10648
  getGlobalStore() {
9861
10649
  if (!this.globalStore) {
9862
- 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);
9863
10651
  this.globalStore = new MemoryStore(globalDbPath);
9864
10652
  }
9865
10653
  return this.globalStore;
@@ -9921,7 +10709,7 @@ var DashboardServer = class {
9921
10709
  }
9922
10710
  watchRuntimeChanges() {
9923
10711
  const workspaceDbPath = path17__default.default.join(this.workspacePath, CASCADE_DB_FILE);
9924
- 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);
9925
10713
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
9926
10714
  for (const watchPath of watchPaths) {
9927
10715
  if (!fs14__default.default.existsSync(watchPath)) continue;
@@ -10029,7 +10817,7 @@ var DashboardServer = class {
10029
10817
  const sessionId = req.params.id;
10030
10818
  this.store.deleteSession(sessionId);
10031
10819
  this.store.deleteRuntimeSession(sessionId);
10032
- 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);
10033
10821
  const globalStore = new MemoryStore(globalDbPath);
10034
10822
  try {
10035
10823
  globalStore.deleteRuntimeSession(sessionId);
@@ -10043,7 +10831,7 @@ var DashboardServer = class {
10043
10831
  });
10044
10832
  this.app.delete("/api/sessions", auth, (req, res) => {
10045
10833
  const body = req.body;
10046
- 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);
10047
10835
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
10048
10836
  const globalStore = new MemoryStore(globalDbPath);
10049
10837
  try {
@@ -10066,7 +10854,7 @@ var DashboardServer = class {
10066
10854
  });
10067
10855
  this.app.delete("/api/runtime", auth, (_req, res) => {
10068
10856
  this.store.deleteAllRuntimeNodes();
10069
- 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);
10070
10858
  const globalStore = new MemoryStore(globalDbPath);
10071
10859
  try {
10072
10860
  globalStore.deleteAllRuntimeNodes();
@@ -10162,7 +10950,7 @@ var DashboardServer = class {
10162
10950
  this.app.get("/api/runtime", auth, (req, res) => {
10163
10951
  const scope = req.query["scope"] ?? "workspace";
10164
10952
  if (scope === "global") {
10165
- 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);
10166
10954
  const globalStore = new MemoryStore(globalDbPath);
10167
10955
  try {
10168
10956
  res.json({
@@ -10412,7 +11200,15 @@ async function exportCommand(options = {}) {
10412
11200
  const spin = ora__default.default({ text: "Loading sessions\u2026", color: "magenta" }).start();
10413
11201
  let store;
10414
11202
  try {
10415
- 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
+ }
10416
11212
  store = new MemoryStore(dbPath);
10417
11213
  } catch (err) {
10418
11214
  spin.fail(chalk8__default.default.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
@@ -10551,6 +11347,15 @@ async function telemetryCommand(action) {
10551
11347
 
10552
11348
  // src/cli/index.ts
10553
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
+ });
10554
11359
  var program = new commander.Command();
10555
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) => {
10556
11361
  await startRepl(options);