cascade-ai 0.2.2 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,16 +9,17 @@ import chalk8 from 'chalk';
9
9
  import dotenv from 'dotenv';
10
10
  import fs7 from 'fs/promises';
11
11
  import path17 from 'path';
12
- import os from 'os';
12
+ import os3 from 'os';
13
13
  import crypto, { randomUUID, timingSafeEqual } from 'crypto';
14
14
  import fs14 from 'fs';
15
15
  import * as _ignoreModule from 'ignore';
16
16
  import _ignoreModule__default from 'ignore';
17
17
  import Database from 'better-sqlite3';
18
18
  import { z } from 'zod';
19
- import { exec, spawnSync } from 'child_process';
19
+ import { exec, execSync, spawnSync } from 'child_process';
20
20
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
21
21
  import EventEmitter from 'events';
22
+ import { glob } from 'glob';
22
23
  import { promisify } from 'util';
23
24
  import { simpleGit } from 'simple-git';
24
25
  import PDFDocument from 'pdfkit';
@@ -82,7 +83,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
82
83
  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;
83
84
  var init_constants = __esm({
84
85
  "src/constants.ts"() {
85
- CASCADE_VERSION = "0.2.2";
86
+ CASCADE_VERSION = "0.2.11";
86
87
  CASCADE_CONFIG_FILE = ".cascade/config.json";
87
88
  CASCADE_DB_FILE = ".cascade/memory.db";
88
89
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -377,7 +378,8 @@ var init_constants = __esm({
377
378
  IMAGE_ANALYZE: "image_analyze",
378
379
  PDF_CREATE: "pdf_create",
379
380
  RUN_CODE: "run_code",
380
- PEER_MESSAGE: "peer_message"
381
+ PEER_MESSAGE: "peer_message",
382
+ WEB_SEARCH: "web_search"
381
383
  };
382
384
  DEFAULT_APPROVAL_REQUIRED = [
383
385
  TOOL_NAMES.SHELL,
@@ -591,33 +593,61 @@ var init_anthropic = __esm({
591
593
  }
592
594
  }
593
595
  convertMessages(messages) {
594
- return messages.filter((m) => m.role !== "system").map((m) => {
595
- if (typeof m.content === "string") {
596
- return { role: m.role, content: m.content };
596
+ const result = [];
597
+ for (const m of messages) {
598
+ if (m.role === "system") continue;
599
+ if (m.role === "tool") {
600
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
601
+ result.push({
602
+ role: "user",
603
+ content: [{
604
+ type: "tool_result",
605
+ tool_use_id: m.toolCallId ?? "",
606
+ content: toolContent
607
+ }]
608
+ });
609
+ continue;
597
610
  }
598
- const content = m.content.map((block) => {
599
- if (block.type === "text") return { type: "text", text: block.text };
600
- if (block.type === "image") {
601
- const img = block.image;
602
- if (img.type === "base64") {
603
- return {
604
- type: "image",
605
- source: {
606
- type: "base64",
607
- media_type: img.mimeType,
608
- data: img.data
611
+ if (m.role === "assistant") {
612
+ const content = [];
613
+ const text = typeof m.content === "string" ? m.content : "";
614
+ if (text) content.push({ type: "text", text });
615
+ for (const tc of m.toolCalls ?? []) {
616
+ content.push({
617
+ type: "tool_use",
618
+ id: tc.id,
619
+ name: tc.name,
620
+ input: tc.input
621
+ });
622
+ }
623
+ if (content.length > 0) {
624
+ result.push({ role: "assistant", content });
625
+ }
626
+ continue;
627
+ }
628
+ if (m.role === "user") {
629
+ if (typeof m.content === "string") {
630
+ result.push({ role: "user", content: m.content });
631
+ } else {
632
+ const content = m.content.map((block) => {
633
+ if (block.type === "text") return { type: "text", text: block.text };
634
+ if (block.type === "image") {
635
+ const img = block.image;
636
+ if (img.type === "base64") {
637
+ return {
638
+ type: "image",
639
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
640
+ };
609
641
  }
610
- };
611
- }
612
- return {
613
- type: "image",
614
- source: { type: "url", url: img.data }
615
- };
642
+ return { type: "image", source: { type: "url", url: img.data } };
643
+ }
644
+ return { type: "text", text: "" };
645
+ });
646
+ result.push({ role: "user", content });
616
647
  }
617
- return { type: "text", text: "" };
618
- });
619
- return { role: m.role, content };
620
- });
648
+ }
649
+ }
650
+ return result;
621
651
  }
622
652
  };
623
653
  }
@@ -915,7 +945,7 @@ var init_gemini = __esm({
915
945
  for (const part of candidate?.content?.parts ?? []) {
916
946
  if (part.functionCall) {
917
947
  toolCalls.push({
918
- id: `gemini-tool-${Date.now()}-${toolCalls.length}`,
948
+ id: part.functionCall.name,
919
949
  name: part.functionCall.name,
920
950
  input: part.functionCall.args ?? {}
921
951
  });
@@ -1003,10 +1033,70 @@ var init_gemini = __esm({
1003
1033
  }
1004
1034
  // ── Private ──────────────────────────────────
1005
1035
  buildContents(messages, extraImages) {
1006
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
1007
- role: m.role === "assistant" ? "model" : "user",
1008
- parts: typeof m.content === "string" ? [{ text: m.content }] : this.convertMessageContent(m, extraImages)
1009
- }));
1036
+ const contents = [];
1037
+ for (const m of messages) {
1038
+ if (m.role === "system") {
1039
+ const text = typeof m.content === "string" ? m.content : "";
1040
+ if (!text.trim()) continue;
1041
+ const prev = contents[contents.length - 1];
1042
+ if (prev?.role === "user") {
1043
+ prev.parts.unshift({ text: `[System context]: ${text}
1044
+
1045
+ ` });
1046
+ } else {
1047
+ contents.push({ role: "user", parts: [{ text: `[System context]: ${text}` }] });
1048
+ }
1049
+ continue;
1050
+ }
1051
+ if (m.role === "tool") {
1052
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1053
+ const functionName = m.toolCallId ?? "unknown_function";
1054
+ contents.push({
1055
+ role: "user",
1056
+ parts: [{
1057
+ functionResponse: {
1058
+ name: functionName,
1059
+ response: { output: toolContent }
1060
+ }
1061
+ }]
1062
+ });
1063
+ continue;
1064
+ }
1065
+ if (m.role === "assistant") {
1066
+ const parts = [];
1067
+ const textContent = typeof m.content === "string" ? m.content : "";
1068
+ if (textContent) parts.push({ text: textContent });
1069
+ for (const tc of m.toolCalls ?? []) {
1070
+ parts.push({
1071
+ functionCall: {
1072
+ name: tc.name,
1073
+ args: tc.input
1074
+ }
1075
+ });
1076
+ }
1077
+ if (parts.length > 0) {
1078
+ contents.push({ role: "model", parts });
1079
+ }
1080
+ continue;
1081
+ }
1082
+ if (m.role === "user") {
1083
+ const parts = this.convertMessageContent(m, contents.length === 0 ? extraImages : void 0);
1084
+ if (extraImages?.length && contents.length > 0) {
1085
+ const isLastUser = !messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
1086
+ if (isLastUser) {
1087
+ for (const img of extraImages) {
1088
+ if (img.type === "base64") {
1089
+ parts.push({ inlineData: { mimeType: img.mimeType, data: img.data } });
1090
+ }
1091
+ }
1092
+ }
1093
+ }
1094
+ if (parts.length > 0) {
1095
+ contents.push({ role: "user", parts });
1096
+ }
1097
+ }
1098
+ }
1099
+ return contents;
1010
1100
  }
1011
1101
  convertMessageContent(msg, extraImages) {
1012
1102
  const parts = [];
@@ -1568,9 +1658,10 @@ var MemoryStore = class _MemoryStore {
1568
1658
  constructor(dbPath) {
1569
1659
  fs14.mkdirSync(path17.dirname(dbPath), { recursive: true });
1570
1660
  try {
1571
- this.db = new Database(dbPath);
1661
+ this.db = new Database(dbPath, { timeout: 5e3 });
1572
1662
  this.db.pragma("journal_mode = WAL");
1573
1663
  this.db.pragma("foreign_keys = ON");
1664
+ this.db.pragma("synchronous = NORMAL");
1574
1665
  this.migrate();
1575
1666
  } catch (err) {
1576
1667
  if (err instanceof Error && err.message.includes("Could not locate the bindings file")) {
@@ -1584,6 +1675,38 @@ Original error: ${err.message}`
1584
1675
  throw err;
1585
1676
  }
1586
1677
  }
1678
+ // ── Async Write Queue ─────────────────────────
1679
+ writeQueue = [];
1680
+ isProcessingQueue = false;
1681
+ async processQueue() {
1682
+ if (this.isProcessingQueue) return;
1683
+ this.isProcessingQueue = true;
1684
+ while (this.writeQueue.length > 0) {
1685
+ const op = this.writeQueue.shift();
1686
+ if (op) {
1687
+ let attempts = 0;
1688
+ while (attempts < 5) {
1689
+ try {
1690
+ op();
1691
+ break;
1692
+ } catch (err) {
1693
+ if (err instanceof Error && err.code === "SQLITE_BUSY") {
1694
+ attempts++;
1695
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempts)));
1696
+ } else {
1697
+ console.error("Cascade AI: DB Write Error:", err);
1698
+ break;
1699
+ }
1700
+ }
1701
+ }
1702
+ }
1703
+ }
1704
+ this.isProcessingQueue = false;
1705
+ }
1706
+ enqueueWrite(op) {
1707
+ this.writeQueue.push(op);
1708
+ this.processQueue().catch(console.error);
1709
+ }
1587
1710
  // ── Sessions ──────────────────────────────────
1588
1711
  createSession(session) {
1589
1712
  this.db.prepare(`
@@ -1670,26 +1793,28 @@ Original error: ${err.message}`
1670
1793
  }
1671
1794
  // ── Runtime Sessions / Nodes ─────────────────
1672
1795
  upsertRuntimeSession(session) {
1673
- this.db.prepare(`
1674
- INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1675
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1676
- ON CONFLICT(session_id) DO UPDATE SET
1677
- title = excluded.title,
1678
- workspace_path = excluded.workspace_path,
1679
- status = excluded.status,
1680
- updated_at = excluded.updated_at,
1681
- latest_prompt = excluded.latest_prompt,
1682
- is_global = excluded.is_global
1683
- `).run(
1684
- session.sessionId,
1685
- session.title,
1686
- session.workspacePath,
1687
- session.status,
1688
- session.startedAt,
1689
- session.updatedAt,
1690
- session.latestPrompt ?? null,
1691
- session.isGlobal ? 1 : 0
1692
- );
1796
+ this.enqueueWrite(() => {
1797
+ this.db.prepare(`
1798
+ INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1799
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1800
+ ON CONFLICT(session_id) DO UPDATE SET
1801
+ title = excluded.title,
1802
+ workspace_path = excluded.workspace_path,
1803
+ status = excluded.status,
1804
+ updated_at = excluded.updated_at,
1805
+ latest_prompt = excluded.latest_prompt,
1806
+ is_global = excluded.is_global
1807
+ `).run(
1808
+ session.sessionId,
1809
+ session.title,
1810
+ session.workspacePath,
1811
+ session.status,
1812
+ session.startedAt,
1813
+ session.updatedAt,
1814
+ session.latestPrompt ?? null,
1815
+ session.isGlobal ? 1 : 0
1816
+ );
1817
+ });
1693
1818
  }
1694
1819
  listRuntimeSessions(limit = 100) {
1695
1820
  const rows = this.db.prepare(`
@@ -1707,33 +1832,35 @@ Original error: ${err.message}`
1707
1832
  }));
1708
1833
  }
1709
1834
  upsertRuntimeNode(node) {
1710
- this.db.prepare(`
1711
- INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1712
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1713
- ON CONFLICT(tier_id) DO UPDATE SET
1714
- session_id = excluded.session_id,
1715
- parent_id = excluded.parent_id,
1716
- role = excluded.role,
1717
- label = excluded.label,
1718
- status = excluded.status,
1719
- current_action = excluded.current_action,
1720
- progress_pct = excluded.progress_pct,
1721
- updated_at = excluded.updated_at,
1722
- workspace_path = excluded.workspace_path,
1723
- is_global = excluded.is_global
1724
- `).run(
1725
- node.tierId,
1726
- node.sessionId,
1727
- node.parentId ?? null,
1728
- node.role,
1729
- node.label,
1730
- node.status,
1731
- node.currentAction ?? null,
1732
- node.progressPct ?? null,
1733
- node.updatedAt,
1734
- node.workspacePath ?? null,
1735
- node.isGlobal ? 1 : 0
1736
- );
1835
+ this.enqueueWrite(() => {
1836
+ this.db.prepare(`
1837
+ INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1838
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1839
+ ON CONFLICT(tier_id) DO UPDATE SET
1840
+ session_id = excluded.session_id,
1841
+ parent_id = excluded.parent_id,
1842
+ role = excluded.role,
1843
+ label = excluded.label,
1844
+ status = excluded.status,
1845
+ current_action = excluded.current_action,
1846
+ progress_pct = excluded.progress_pct,
1847
+ updated_at = excluded.updated_at,
1848
+ workspace_path = excluded.workspace_path,
1849
+ is_global = excluded.is_global
1850
+ `).run(
1851
+ node.tierId,
1852
+ node.sessionId,
1853
+ node.parentId ?? null,
1854
+ node.role,
1855
+ node.label,
1856
+ node.status,
1857
+ node.currentAction ?? null,
1858
+ node.progressPct ?? null,
1859
+ node.updatedAt,
1860
+ node.workspacePath ?? null,
1861
+ node.isGlobal ? 1 : 0
1862
+ );
1863
+ });
1737
1864
  }
1738
1865
  listRuntimeNodes(sessionId, limit = 500) {
1739
1866
  const rows = sessionId ? this.db.prepare(`
@@ -1756,30 +1883,32 @@ Original error: ${err.message}`
1756
1883
  }));
1757
1884
  }
1758
1885
  addRuntimeNodeLog(log) {
1759
- this.db.prepare(`
1760
- INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1761
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1762
- `).run(
1763
- log.id,
1764
- log.sessionId,
1765
- log.tierId,
1766
- log.role,
1767
- log.label,
1768
- log.status,
1769
- log.currentAction ?? null,
1770
- log.progressPct ?? null,
1771
- log.timestamp,
1772
- log.workspacePath ?? null,
1773
- log.isGlobal ? 1 : 0
1774
- );
1775
- this.db.prepare(`
1776
- DELETE FROM runtime_node_logs
1777
- WHERE id NOT IN (
1778
- SELECT id FROM runtime_node_logs
1779
- ORDER BY timestamp DESC
1780
- LIMIT 2000
1781
- )
1782
- `).run();
1886
+ this.enqueueWrite(() => {
1887
+ this.db.prepare(`
1888
+ INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1889
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1890
+ `).run(
1891
+ log.id,
1892
+ log.sessionId,
1893
+ log.tierId,
1894
+ log.role,
1895
+ log.label,
1896
+ log.status,
1897
+ log.currentAction ?? null,
1898
+ log.progressPct ?? null,
1899
+ log.timestamp,
1900
+ log.workspacePath ?? null,
1901
+ log.isGlobal ? 1 : 0
1902
+ );
1903
+ this.db.prepare(`
1904
+ DELETE FROM runtime_node_logs
1905
+ WHERE id NOT IN (
1906
+ SELECT id FROM runtime_node_logs
1907
+ ORDER BY timestamp DESC
1908
+ LIMIT 2000
1909
+ )
1910
+ `).run();
1911
+ });
1783
1912
  }
1784
1913
  listRuntimeNodeLogs(sessionId, tierId, limit = 200) {
1785
1914
  let rows;
@@ -1817,19 +1946,21 @@ Original error: ${err.message}`
1817
1946
  }
1818
1947
  // ── Messages ──────────────────────────────────
1819
1948
  addMessage(message) {
1820
- this.db.prepare(`
1821
- INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
1822
- VALUES (?, ?, ?, ?, ?, ?, ?)
1823
- `).run(
1824
- message.id,
1825
- message.sessionId,
1826
- message.role,
1827
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
1828
- message.timestamp,
1829
- message.tokens ? JSON.stringify(message.tokens) : null,
1830
- message.agentMessages ? JSON.stringify(message.agentMessages) : null
1831
- );
1832
- this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
1949
+ this.enqueueWrite(() => {
1950
+ this.db.prepare(`
1951
+ INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
1952
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1953
+ `).run(
1954
+ message.id,
1955
+ message.sessionId,
1956
+ message.role,
1957
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
1958
+ message.timestamp,
1959
+ message.tokens ? JSON.stringify(message.tokens) : null,
1960
+ message.agentMessages ? JSON.stringify(message.agentMessages) : null
1961
+ );
1962
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
1963
+ });
1833
1964
  }
1834
1965
  getSessionMessages(sessionId) {
1835
1966
  const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
@@ -1926,10 +2057,12 @@ Original error: ${err.message}`
1926
2057
  }
1927
2058
  // ── Audit Log ─────────────────────────────────
1928
2059
  addAuditEntry(entry) {
1929
- this.db.prepare(`
1930
- INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
1931
- VALUES (?, ?, ?, ?, ?, ?)
1932
- `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2060
+ this.enqueueWrite(() => {
2061
+ this.db.prepare(`
2062
+ INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
2063
+ VALUES (?, ?, ?, ?, ?, ?)
2064
+ `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2065
+ });
1933
2066
  }
1934
2067
  getAuditLog(sessionId, limit = 100) {
1935
2068
  const rows = this.db.prepare("SELECT * FROM audit_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
@@ -1944,10 +2077,12 @@ Original error: ${err.message}`
1944
2077
  }
1945
2078
  // ── File Snapshots ────────────────────────────
1946
2079
  addFileSnapshot(sessionId, filePath, content) {
1947
- this.db.prepare(`
1948
- INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
1949
- VALUES (?, ?, ?, ?, ?)
1950
- `).run(randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2080
+ this.enqueueWrite(() => {
2081
+ this.db.prepare(`
2082
+ INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
2083
+ VALUES (?, ?, ?, ?, ?)
2084
+ `).run(randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2085
+ });
1951
2086
  }
1952
2087
  getLatestFileSnapshots(sessionId) {
1953
2088
  const rows = this.db.prepare(`
@@ -2251,12 +2386,24 @@ var McpServerConfigSchema = z.object({
2251
2386
  args: z.array(z.string()).optional(),
2252
2387
  env: z.record(z.string()).optional()
2253
2388
  });
2389
+ var WebSearchConfigSchema = z.object({
2390
+ /** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
2391
+ searxngUrl: z.string().optional(),
2392
+ /** Brave Search API key — get one at https://api.search.brave.com */
2393
+ braveApiKey: z.string().optional(),
2394
+ /** Tavily API key — get one at https://tavily.com */
2395
+ tavilyApiKey: z.string().optional(),
2396
+ /** Max results per search (default 5) */
2397
+ maxResults: z.number().default(5)
2398
+ });
2254
2399
  var ToolsConfigSchema = z.object({
2255
2400
  shellAllowlist: z.array(z.string()).default([]),
2256
2401
  shellBlocklist: z.array(z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
2257
2402
  requireApprovalFor: z.array(z.string()).default([]),
2258
2403
  browserEnabled: z.boolean().default(false),
2259
- mcpServers: z.array(McpServerConfigSchema).optional()
2404
+ mcpServers: z.array(McpServerConfigSchema).optional(),
2405
+ /** Web search backends — at least one should be configured for best results */
2406
+ webSearch: WebSearchConfigSchema.optional()
2260
2407
  });
2261
2408
  var HookDefinitionSchema = z.object({
2262
2409
  command: z.string(),
@@ -2361,7 +2508,7 @@ var ConfigManager = class {
2361
2508
  globalDir;
2362
2509
  constructor(workspacePath = process.cwd()) {
2363
2510
  this.workspacePath = workspacePath;
2364
- this.globalDir = path17.join(os.homedir(), GLOBAL_CONFIG_DIR);
2511
+ this.globalDir = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR);
2365
2512
  }
2366
2513
  async load() {
2367
2514
  this.config = await this.loadConfig();
@@ -3360,6 +3507,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter {
3360
3507
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
3361
3508
  }
3362
3509
  };
3510
+
3511
+ // src/utils/retry.ts
3512
+ var CascadeCancelledError = class extends Error {
3513
+ constructor(reason) {
3514
+ super(reason ?? "Run was cancelled via AbortSignal");
3515
+ this.name = "CascadeCancelledError";
3516
+ }
3517
+ };
3518
+
3519
+ // src/core/tiers/base.ts
3363
3520
  var BaseTier = class extends EventEmitter {
3364
3521
  id;
3365
3522
  role;
@@ -3369,6 +3526,8 @@ var BaseTier = class extends EventEmitter {
3369
3526
  label;
3370
3527
  systemPromptOverride = "";
3371
3528
  hierarchyContext = "";
3529
+ /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
3530
+ signal;
3372
3531
  constructor(role, id, parentId) {
3373
3532
  super();
3374
3533
  this.role = role;
@@ -3431,6 +3590,18 @@ var BaseTier = class extends EventEmitter {
3431
3590
  log(message, data) {
3432
3591
  this.emit("log", { tierId: this.id, role: this.role, message, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3433
3592
  }
3593
+ /**
3594
+ * Throws `CascadeCancelledError` if the run's `AbortSignal` has fired.
3595
+ * Call this at safe checkpoints (before LLM calls, between T3 dispatches)
3596
+ * to provide a fast, clean cancellation path.
3597
+ */
3598
+ throwIfCancelled() {
3599
+ if (this.signal?.aborted) {
3600
+ throw new CascadeCancelledError(
3601
+ typeof this.signal.reason === "string" ? this.signal.reason : "Run cancelled by caller"
3602
+ );
3603
+ }
3604
+ }
3434
3605
  };
3435
3606
 
3436
3607
  // src/core/context/manager.ts
@@ -3632,6 +3803,7 @@ Rules:
3632
3803
  - Execute the subtask completely \u2014 do not stop partway through.
3633
3804
  - Use tools when needed. Ask for approval only when the tool registry requires it.
3634
3805
  - 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.
3806
+ - Use the "web_search" tool to find current information, documentation, news, or general web data.
3635
3807
  - Use the "pdf_create" tool for PDF requests.
3636
3808
  - 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.
3637
3809
  - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
@@ -3675,7 +3847,8 @@ var T3Worker = class extends BaseTier {
3675
3847
  this.store = store;
3676
3848
  this.audit = new AuditLogger(store, sessionId);
3677
3849
  }
3678
- async execute(assignment, taskId) {
3850
+ async execute(assignment, taskId, signal) {
3851
+ this.signal = signal;
3679
3852
  this.assignment = assignment;
3680
3853
  this.taskId = taskId;
3681
3854
  this.setLabel(assignment.subtaskTitle);
@@ -3825,6 +3998,7 @@ Now execute your subtask using this context where relevant.`
3825
3998
  tools = [...tools];
3826
3999
  while (iterations < MAX_ITERATIONS) {
3827
4000
  iterations++;
4001
+ this.throwIfCancelled();
3828
4002
  const options = {
3829
4003
  messages: this.context.getMessages(),
3830
4004
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -3845,21 +4019,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3845
4019
  if (requiresArtifact) {
3846
4020
  stalledArtifactIterations += 1;
3847
4021
  if (stalledArtifactIterations >= 2) {
3848
- if (this.toolCreator && stalledArtifactIterations === 2) {
3849
- const toolName = await this.toolCreator.createTool(
3850
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
3851
- this.assignment?.description ?? ""
3852
- );
3853
- if (toolName) {
3854
- tools = this.toolRegistry.getToolDefinitions();
3855
- this.sendStatusUpdate({
3856
- progressPct: 50,
3857
- currentAction: `Dynamic tool created: ${toolName}`,
3858
- status: "IN_PROGRESS"
3859
- });
3860
- this.emit("tool:created", { tierId: this.id, toolName });
3861
- continue;
3862
- }
4022
+ if (stalledArtifactIterations === 2) {
4023
+ throw new Error(`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`);
3863
4024
  }
3864
4025
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
3865
4026
  }
@@ -4019,6 +4180,9 @@ ${assignment.expectedOutput}`;
4019
4180
  const artifactPaths = this.extractArtifactPaths(assignment);
4020
4181
  if (!artifactPaths.length) return { ok: true, issues: [] };
4021
4182
  const issues = [];
4183
+ const { exec: exec4 } = await import('child_process');
4184
+ const { promisify: promisify3 } = await import('util');
4185
+ const execAsync3 = promisify3(exec4);
4022
4186
  for (const artifactPath of artifactPaths) {
4023
4187
  const absolutePath = path17.resolve(process.cwd(), artifactPath);
4024
4188
  try {
@@ -4035,9 +4199,27 @@ ${assignment.expectedOutput}`;
4035
4199
  const content = await fs7.readFile(absolutePath, "utf-8");
4036
4200
  if (!content.trim()) {
4037
4201
  issues.push(`Artifact content is empty: ${artifactPath}`);
4202
+ continue;
4038
4203
  }
4039
4204
  } else if (stat.size < 100) {
4040
4205
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4206
+ continue;
4207
+ }
4208
+ const ext = path17.extname(absolutePath).toLowerCase();
4209
+ try {
4210
+ if (ext === ".ts" || ext === ".tsx") {
4211
+ await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
4212
+ } else if (ext === ".js" || ext === ".jsx") {
4213
+ await execAsync3(`node --check ${absolutePath}`, { timeout: 1e4 });
4214
+ } else if (ext === ".py") {
4215
+ await execAsync3(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
4216
+ }
4217
+ } catch (err) {
4218
+ const stderr = err?.stderr || String(err);
4219
+ const stdout = err?.stdout || "";
4220
+ issues.push(`Semantic error in ${artifactPath}:
4221
+ ${stderr}
4222
+ ${stdout}`);
4041
4223
  }
4042
4224
  } catch {
4043
4225
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -4434,7 +4616,8 @@ var T2Manager = class extends BaseTier {
4434
4616
  });
4435
4617
  this.emit("peer-sync-received", { fromId, content });
4436
4618
  }
4437
- async execute(assignment, taskId) {
4619
+ async execute(assignment, taskId, signal) {
4620
+ this.signal = signal;
4438
4621
  this.assignment = assignment;
4439
4622
  this.taskId = taskId;
4440
4623
  this.setLabel(assignment.sectionTitle);
@@ -4446,12 +4629,14 @@ var T2Manager = class extends BaseTier {
4446
4629
  });
4447
4630
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
4448
4631
  try {
4632
+ this.throwIfCancelled();
4449
4633
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
4450
4634
  this.sendStatusUpdate({
4451
4635
  progressPct: 20,
4452
4636
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
4453
4637
  status: "IN_PROGRESS"
4454
4638
  });
4639
+ this.throwIfCancelled();
4455
4640
  const t3Results = await this.executeSubtasks(subtasks, taskId);
4456
4641
  this.sendStatusUpdate({
4457
4642
  progressPct: 90,
@@ -4618,11 +4803,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4618
4803
  ).join(", ")}`,
4619
4804
  status: "IN_PROGRESS"
4620
4805
  });
4806
+ this.throwIfCancelled();
4621
4807
  const waveResults = await Promise.allSettled(
4622
4808
  runnableIds.map(async (id) => {
4623
4809
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4624
4810
  const worker = workerMap.get(id);
4625
- const result = await worker.execute(assignment, taskId);
4811
+ const result = await worker.execute(assignment, taskId, this.signal);
4626
4812
  resultMap.set(id, result);
4627
4813
  return result;
4628
4814
  })
@@ -4636,6 +4822,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4636
4822
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4637
4823
  const retried = await this.retryT3(assignment, taskId);
4638
4824
  resultMap.set(id, retried);
4825
+ } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
4826
+ const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4827
+ if (this.toolCreator) {
4828
+ this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
4829
+ this.sendStatusUpdate({
4830
+ progressPct: 50,
4831
+ currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
4832
+ status: "IN_PROGRESS"
4833
+ });
4834
+ const toolName = await this.toolCreator.createTool(
4835
+ `Help complete: ${assignment.subtaskTitle}`,
4836
+ assignment.description
4837
+ );
4838
+ if (toolName) {
4839
+ this.log(`T2 verifying new tool: ${toolName}`);
4840
+ this.sendStatusUpdate({
4841
+ progressPct: 60,
4842
+ currentAction: `T2 Verifying new tool: ${toolName}`,
4843
+ status: "IN_PROGRESS"
4844
+ });
4845
+ try {
4846
+ const verifyResult = await this.router.generate("T2", {
4847
+ 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".` }],
4848
+ systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
4849
+ maxTokens: 50
4850
+ });
4851
+ if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
4852
+ this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
4853
+ const retried = await this.retryT3({
4854
+ ...assignment,
4855
+ description: `${assignment.description}
4856
+
4857
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
4858
+ }, taskId);
4859
+ resultMap.set(id, retried);
4860
+ } else {
4861
+ this.log(`T2 rejected the dynamic tool: ${toolName}`);
4862
+ resultMap.set(id, r.value);
4863
+ }
4864
+ } catch {
4865
+ const retried = await this.retryT3({
4866
+ ...assignment,
4867
+ description: `${assignment.description}
4868
+
4869
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
4870
+ }, taskId);
4871
+ resultMap.set(id, retried);
4872
+ }
4873
+ } else {
4874
+ resultMap.set(id, r.value);
4875
+ }
4876
+ } else {
4877
+ resultMap.set(id, r.value);
4878
+ }
4639
4879
  }
4640
4880
  for (const dependent of adj.get(id) ?? []) {
4641
4881
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -4699,7 +4939,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4699
4939
  }));
4700
4940
  return worker.execute(
4701
4941
  { ...assignment, description: `[RETRY] ${assignment.description}` },
4702
- taskId
4942
+ taskId,
4943
+ this.signal
4703
4944
  );
4704
4945
  }
4705
4946
  publishSectionOutput(result) {
@@ -4713,29 +4954,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4713
4954
  async aggregateResults(assignment, results) {
4714
4955
  const completed = results.filter((r) => r.status === "COMPLETED");
4715
4956
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
4716
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
4717
4957
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
4718
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
4719
-
4720
- ${outputs}
4721
- ${peerOutputs ? `
4958
+ const peerContext = peerOutputs ? `
4722
4959
 
4723
4960
  Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
4724
- ${peerOutputs}` : ""}`;
4725
- const messages = [{ role: "user", content: prompt }];
4726
- try {
4727
- const result = await this.router.generate("T2", {
4728
- messages,
4729
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
4961
+ ${peerOutputs}` : "";
4962
+ const MAX_CHUNK_LENGTH = 15e3;
4963
+ let currentSummary = "";
4964
+ let i = 0;
4965
+ while (i < completed.length) {
4966
+ let chunkText = "";
4967
+ let chunkEnd = i;
4968
+ while (chunkEnd < completed.length) {
4969
+ const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
4970
+
4971
+ `;
4972
+ if (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
4973
+ break;
4974
+ }
4975
+ chunkText += nextOutput;
4976
+ chunkEnd++;
4977
+ }
4978
+ i = chunkEnd;
4979
+ const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
4980
+ ${currentSummary ? `
4981
+ PREVIOUS SUMMARY SO FAR:
4982
+ ${currentSummary}
4983
+
4984
+ NEW OUTPUTS TO INTEGRATE:
4985
+ ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
4986
+ const messages = [{ role: "user", content: prompt }];
4987
+ try {
4988
+ const result = await this.router.generate("T2", {
4989
+ messages,
4990
+ systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
4730
4991
 
4731
4992
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4732
- maxTokens: 300
4733
- });
4734
- return result.content;
4735
- } catch (err) {
4736
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
4737
- return outputs;
4993
+ maxTokens: 500
4994
+ });
4995
+ currentSummary = result.content;
4996
+ } catch (err) {
4997
+ this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
4998
+ return currentSummary + "\n\n" + chunkText;
4999
+ }
4738
5000
  }
5001
+ return currentSummary;
4739
5002
  }
4740
5003
  determineStatus(results) {
4741
5004
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -4863,10 +5126,10 @@ Rules:
4863
5126
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
4864
5127
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
4865
5128
 
4866
- EXECUTION MODE GUIDANCE:
4867
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
4868
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
4869
- - Prefer parallel execution: it is significantly faster and reduces total wall-clock time.
5129
+ DEPENDENCY GUIDANCE:
5130
+ - Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
5131
+ - 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).
5132
+ - Prefer empty dependencies (parallel execution): it is significantly faster and reduces total wall-clock time.
4870
5133
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
4871
5134
 
4872
5135
  QUALITY RULES:
@@ -4905,7 +5168,8 @@ var T1Administrator = class extends BaseTier {
4905
5168
  setToolCreator(creator) {
4906
5169
  this.toolCreator = creator;
4907
5170
  }
4908
- async execute(userPrompt, images, systemContext) {
5171
+ async execute(userPrompt, images, systemContext, signal) {
5172
+ this.signal = signal;
4909
5173
  this.taskId = randomUUID();
4910
5174
  this.setLabel("Administrator");
4911
5175
  this.setStatus("ACTIVE");
@@ -4916,10 +5180,12 @@ var T1Administrator = class extends BaseTier {
4916
5180
  status: "IN_PROGRESS"
4917
5181
  });
4918
5182
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
5183
+ this.throwIfCancelled();
4919
5184
  let enrichedPrompt = userPrompt;
4920
5185
  if (images?.length) {
4921
5186
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
4922
5187
  }
5188
+ this.throwIfCancelled();
4923
5189
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
4924
5190
  this.sendStatusUpdate({
4925
5191
  progressPct: 10,
@@ -4927,21 +5193,83 @@ var T1Administrator = class extends BaseTier {
4927
5193
  status: "IN_PROGRESS"
4928
5194
  });
4929
5195
  this.emit("plan", { taskId: this.taskId, plan });
4930
- const t2Results = await this.dispatchT2Managers(plan.sections);
5196
+ this.throwIfCancelled();
5197
+ let allT2Results = await this.dispatchT2Managers(plan.sections);
5198
+ let pass = 1;
5199
+ const MAX_REPLAN_PASSES = 2;
5200
+ while (pass <= MAX_REPLAN_PASSES) {
5201
+ const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
5202
+ if (reviewResult.approved) {
5203
+ this.log("T1 Review passed.");
5204
+ break;
5205
+ }
5206
+ this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
5207
+ this.sendStatusUpdate({
5208
+ progressPct: 80 + pass * 5,
5209
+ currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
5210
+ status: "IN_PROGRESS"
5211
+ });
5212
+ const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
5213
+ Review reason: ${reviewResult.reason}
5214
+
5215
+ Original goal: ${enrichedPrompt}
5216
+
5217
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
5218
+ const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
5219
+ allT2Results = [...allT2Results, ...correctionResults];
5220
+ pass++;
5221
+ }
4931
5222
  this.sendStatusUpdate({
4932
5223
  progressPct: 95,
4933
5224
  currentAction: "Compiling final output",
4934
5225
  status: "IN_PROGRESS"
4935
5226
  });
4936
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
5227
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
4937
5228
  this.setStatus("COMPLETED");
4938
5229
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
4939
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
5230
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
4940
5231
  }
4941
5232
  getEscalations() {
4942
5233
  return [...this.escalations];
4943
5234
  }
4944
5235
  // ── Private ──────────────────────────────────
5236
+ async reviewT2Outputs(originalPrompt, plan, t2Results) {
5237
+ const failedSections = t2Results.filter((r) => r.status === "FAILED");
5238
+ if (failedSections.length > 0) {
5239
+ return {
5240
+ approved: false,
5241
+ reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
5242
+ };
5243
+ }
5244
+ const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
5245
+ ${r.sectionSummary}`).join("\n\n");
5246
+ const prompt = `You are a strict QA Reviewer for the Cascade AI system.
5247
+ Review the following execution outputs against the original user prompt.
5248
+
5249
+ Original Request: ${originalPrompt}
5250
+
5251
+ T2 Manager Summaries:
5252
+ ${sectionsText}
5253
+
5254
+ Does the current state of the workspace and the outputs fully satisfy the user's request?
5255
+ If yes, reply with exactly: "APPROVED".
5256
+ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
5257
+ try {
5258
+ const result = await this.router.generate("T1", {
5259
+ messages: [{ role: "user", content: prompt }],
5260
+ systemPrompt: this.systemPromptOverride + "You are a QA Reviewer.",
5261
+ maxTokens: 500,
5262
+ temperature: 0
5263
+ });
5264
+ const response = result.content.trim();
5265
+ if (response.toUpperCase().startsWith("APPROVED")) {
5266
+ return { approved: true };
5267
+ }
5268
+ return { approved: false, reason: response.replace(/^REJECTED:\s*/i, "") };
5269
+ } catch {
5270
+ return { approved: true };
5271
+ }
5272
+ }
4945
5273
  async analyzeImages(prompt, images) {
4946
5274
  const visionModel = this.router.getModelForTier("T1");
4947
5275
  if (!visionModel?.isVisionCapable) return prompt;
@@ -4970,29 +5298,35 @@ ${systemContext}` : "";
4970
5298
  Example: if asked to create files "inside python_exclusive", every subtask that
4971
5299
  creates a file must use "python_exclusive/filename.ext" as the path.
4972
5300
 
4973
- Return JSON where subtasks can declare dependencies:
5301
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
4974
5302
  {
4975
5303
  "sections": [{
5304
+ "sectionId": "s1",
5305
+ "sectionTitle": "Setup Project",
5306
+ "description": "Initialize the project",
5307
+ "expectedOutput": "Basic structure created",
5308
+ "constraints": [],
5309
+ "dependsOn": [], // \u2190 empty = runs immediately
4976
5310
  "t3Subtasks": [{
4977
5311
  "subtaskId": "t1",
4978
- "subtaskTitle": "Generate Source Code",
4979
- "dependsOn": [], // \u2190 empty = runs immediately
4980
- "executionMode": "parallel"
4981
- }, {
4982
- "subtaskId": "t2",
4983
- "subtaskTitle": "Save Code to File",
4984
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
4985
- "executionMode": "parallel"
4986
- }, {
4987
- "subtaskId": "t3",
4988
- "subtaskTitle": "Execute and Verify",
4989
- "dependsOn": ["t2"], // \u2190 waits for t2
4990
- "executionMode": "parallel"
5312
+ "subtaskTitle": "Init NPM",
5313
+ "description": "Run npm init",
5314
+ "expectedOutput": "package.json created",
5315
+ "constraints": [],
5316
+ "dependsOn": []
4991
5317
  }]
5318
+ }, {
5319
+ "sectionId": "s2",
5320
+ "sectionTitle": "Write Tests",
5321
+ "description": "Write tests for the project",
5322
+ "expectedOutput": "Tests passing",
5323
+ "constraints": [],
5324
+ "dependsOn": ["s1"], // \u2190 waits for section s1 to complete first
5325
+ "t3Subtasks": [...]
4992
5326
  }]
4993
5327
  }
4994
- Use dependsOn when a subtask needs the output of a previous one.
4995
- Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5328
+ Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
5329
+ Leave dependsOn empty for sections that can run immediately in parallel.`;
4996
5330
  const messages = [{ role: "user", content: decompositionPrompt }];
4997
5331
  const result = await this.router.generate("T1", {
4998
5332
  messages,
@@ -5120,92 +5454,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5120
5454
  ].filter(Boolean).join(" ");
5121
5455
  m.setHierarchyContext(context);
5122
5456
  });
5123
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
5124
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
5125
- for (const section of sections) {
5126
- if (overlapSections.has(section.sectionId)) {
5127
- section.executionMode = "sequential";
5457
+ if (overlapSections.size > 0) {
5458
+ this.log("Overlap detected \u2014 adding sequential dependencies for conflicting sections to prevent race conditions");
5459
+ const overlapArray = Array.from(overlapSections);
5460
+ for (let i = 1; i < overlapArray.length; i++) {
5461
+ const section = sections.find((s) => s.sectionId === overlapArray[i]);
5462
+ if (section) {
5463
+ section.dependsOn = [...section.dependsOn || [], overlapArray[i - 1]];
5128
5464
  }
5129
5465
  }
5130
5466
  }
5131
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
5132
- const isSequential = sections.some((s) => s.executionMode === "sequential");
5133
5467
  const t2Results = [];
5134
5468
  try {
5135
- if (isSequential) {
5136
- this.log("Dispatching T2 managers sequentially");
5137
- for (let i = 0; i < managers.length; i++) {
5138
- const m = managers[i];
5139
- this.sendStatusUpdate({
5140
- progressPct: pct(i),
5141
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
5142
- status: "IN_PROGRESS"
5143
- });
5144
- try {
5145
- const result = await m.execute(sections[i], this.taskId);
5146
- t2Results.push(result);
5147
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
5148
- if (result.status === "ESCALATED") {
5149
- this.escalations.push({
5150
- raisedBy: `T2_${sections[i].sectionId}`,
5151
- sectionId: sections[i].sectionId,
5152
- attempted: result.issues,
5153
- blocker: result.issues.join("; "),
5154
- needs: "Human review required"
5155
- });
5156
- }
5157
- } catch (err) {
5158
- t2Results.push({
5159
- sectionId: sections[i].sectionId,
5160
- sectionTitle: sections[i].sectionTitle,
5161
- status: "FAILED",
5162
- t3Results: [],
5163
- sectionSummary: "",
5164
- issues: [err instanceof Error ? err.message : String(err)]
5165
- });
5469
+ t2Results.push(...await this.runT2sWithDependencies(sections, managers, this.taskId));
5470
+ } finally {
5471
+ cleanup();
5472
+ }
5473
+ return t2Results;
5474
+ }
5475
+ /**
5476
+ * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
5477
+ */
5478
+ async runT2sWithDependencies(sections, managers, taskId) {
5479
+ const adj = /* @__PURE__ */ new Map();
5480
+ const inDegree = /* @__PURE__ */ new Map();
5481
+ const resultMap = /* @__PURE__ */ new Map();
5482
+ const allKeys = new Set(sections.map((s) => s.sectionId));
5483
+ for (const s of sections) {
5484
+ if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
5485
+ inDegree.set(s.sectionId, 0);
5486
+ s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
5487
+ }
5488
+ for (const s of sections) {
5489
+ for (const dep of s.dependsOn ?? []) {
5490
+ adj.get(dep).add(s.sectionId);
5491
+ inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
5492
+ }
5493
+ }
5494
+ const queue = [];
5495
+ const degree = new Map(inDegree);
5496
+ for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
5497
+ const visited = /* @__PURE__ */ new Set();
5498
+ while (queue.length > 0) {
5499
+ const u = queue.shift();
5500
+ visited.add(u);
5501
+ for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
5502
+ const newDeg = (degree.get(v) ?? 1) - 1;
5503
+ degree.set(v, newDeg);
5504
+ if (newDeg === 0) queue.push(v);
5505
+ }
5506
+ }
5507
+ const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
5508
+ if (cycleNodes.length > 0) {
5509
+ this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
5510
+ for (const s of sections) {
5511
+ if (cycleNodes.includes(s.sectionId)) {
5512
+ const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
5513
+ for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
5514
+ inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
5515
+ adj.get(removed)?.delete(s.sectionId);
5166
5516
  }
5517
+ s.dependsOn = safeDeps;
5167
5518
  }
5168
- } else {
5169
- const results = await Promise.allSettled(
5170
- managers.map((m, i) => {
5171
- this.sendStatusUpdate({
5172
- progressPct: pct(i),
5173
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
5174
- status: "IN_PROGRESS"
5175
- });
5176
- return m.execute(sections[i], this.taskId);
5177
- })
5178
- );
5179
- for (let i = 0; i < results.length; i++) {
5180
- const r = results[i];
5181
- if (r.status === "fulfilled") {
5182
- t2Results.push(r.value);
5183
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
5184
- if (r.value.status === "ESCALATED") {
5185
- this.escalations.push({
5186
- raisedBy: `T2_${sections[i].sectionId}`,
5187
- sectionId: sections[i].sectionId,
5188
- attempted: r.value.issues,
5189
- blocker: r.value.issues.join("; "),
5190
- needs: "Human review required"
5191
- });
5192
- }
5193
- } else {
5194
- t2Results.push({
5195
- sectionId: sections[i].sectionId,
5196
- sectionTitle: sections[i].sectionTitle,
5197
- status: "FAILED",
5198
- t3Results: [],
5199
- sectionSummary: "",
5200
- issues: [r.reason instanceof Error ? r.reason.message : String(r.reason)]
5519
+ }
5520
+ }
5521
+ const totalSections = sections.length;
5522
+ let completedSections = 0;
5523
+ const executeWave = async () => {
5524
+ const readyIds = [];
5525
+ for (const [id, deg] of inDegree.entries()) {
5526
+ if (deg === 0 && !resultMap.has(id)) {
5527
+ readyIds.push(id);
5528
+ }
5529
+ }
5530
+ if (readyIds.length === 0) return;
5531
+ await Promise.all(readyIds.map(async (id) => {
5532
+ resultMap.set(id, null);
5533
+ const index = sections.findIndex((s) => s.sectionId === id);
5534
+ const section = sections[index];
5535
+ const manager = managers[index];
5536
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
5537
+ this.sendStatusUpdate({
5538
+ progressPct,
5539
+ currentAction: `T2 working on: ${section.sectionTitle}`,
5540
+ status: "IN_PROGRESS"
5541
+ });
5542
+ this.throwIfCancelled();
5543
+ let result;
5544
+ try {
5545
+ result = await manager.execute(section, taskId, this.signal);
5546
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
5547
+ if (result.status === "ESCALATED") {
5548
+ this.escalations.push({
5549
+ raisedBy: `T2_${section.sectionId}`,
5550
+ sectionId: section.sectionId,
5551
+ attempted: result.issues,
5552
+ blocker: result.issues.join("; "),
5553
+ needs: "Human review required"
5201
5554
  });
5202
5555
  }
5556
+ } catch (err) {
5557
+ result = {
5558
+ sectionId: section.sectionId,
5559
+ sectionTitle: section.sectionTitle,
5560
+ status: "FAILED",
5561
+ t3Results: [],
5562
+ sectionSummary: "",
5563
+ issues: [err instanceof Error ? err.message : String(err)]
5564
+ };
5203
5565
  }
5566
+ resultMap.set(id, result);
5567
+ completedSections++;
5568
+ for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
5569
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
5570
+ }
5571
+ }));
5572
+ if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
5573
+ await executeWave();
5204
5574
  }
5205
- } finally {
5206
- cleanup();
5207
- }
5208
- return t2Results;
5575
+ };
5576
+ await executeWave();
5577
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
5209
5578
  }
5210
5579
  async compileFinalOutput(originalPrompt, plan, t2Results) {
5211
5580
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -5655,13 +6024,47 @@ var GitHubTool = class extends BaseTool {
5655
6024
  }
5656
6025
  async execute(input, _options) {
5657
6026
  const platform = input["platform"] ?? "github";
5658
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
5659
6027
  const operation = input["operation"];
5660
6028
  const repo = input["repo"];
5661
- if (platform === "github") {
5662
- return this.executeGitHub(operation, repo, token, input);
6029
+ let token = input["token"];
6030
+ if (!token) {
6031
+ if (platform === "github") {
6032
+ token = process.env["GITHUB_TOKEN"];
6033
+ } else {
6034
+ token = process.env["GITLAB_TOKEN"];
6035
+ }
6036
+ }
6037
+ if (!token) {
6038
+ const envName = platform === "github" ? "GITHUB_TOKEN" : "GITLAB_TOKEN";
6039
+ return `Error: No ${platform} token provided. Set the ${envName} environment variable or pass a "token" field in the input.`;
6040
+ }
6041
+ try {
6042
+ if (platform === "github") {
6043
+ return await this.executeGitHub(operation, repo, token, input);
6044
+ }
6045
+ return await this.executeGitLab(operation, repo, token, input);
6046
+ } catch (err) {
6047
+ const axiosErr = err;
6048
+ if (axiosErr?.response?.status) {
6049
+ const status = axiosErr.response.status;
6050
+ const msg = axiosErr.response.data?.message ?? "";
6051
+ switch (status) {
6052
+ case 401:
6053
+ return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
6054
+ case 403:
6055
+ return `Permission denied: Your ${platform} token lacks the required scopes for this operation. Needed: repo or workflow.`;
6056
+ case 404:
6057
+ return `Not found: Repository "${repo}" does not exist, or your token cannot access it.`;
6058
+ case 422:
6059
+ return `Validation error from ${platform}: ${msg || "Check your input parameters (branch names, base/head refs, etc.)."}`;
6060
+ case 429:
6061
+ return `Rate limited by ${platform}. Please wait a moment before trying again.`;
6062
+ default:
6063
+ return `${platform} API error (${status}): ${msg || (axiosErr.message ?? "Unknown error")}`;
6064
+ }
6065
+ }
6066
+ return `${platform} request failed: ${axiosErr.message ?? String(err)}`;
5663
6067
  }
5664
- return this.executeGitLab(operation, repo, token, input);
5665
6068
  }
5666
6069
  async executeGitHub(operation, repo, token, input) {
5667
6070
  const headers = {
@@ -5748,6 +6151,7 @@ ${response.data.description}`;
5748
6151
  };
5749
6152
 
5750
6153
  // src/tools/browser.ts
6154
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
5751
6155
  var BrowserTool = class extends BaseTool {
5752
6156
  name = "browser";
5753
6157
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -5756,7 +6160,7 @@ var BrowserTool = class extends BaseTool {
5756
6160
  properties: {
5757
6161
  action: {
5758
6162
  type: "string",
5759
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
6163
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
5760
6164
  },
5761
6165
  url: { type: "string", description: "URL to navigate to" },
5762
6166
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -5776,53 +6180,86 @@ var BrowserTool = class extends BaseTool {
5776
6180
  try {
5777
6181
  playwright = await import('playwright');
5778
6182
  } catch {
5779
- throw new Error("Playwright is not installed. Run: npm install playwright && npx playwright install chromium");
6183
+ return "Error: Playwright is not installed. Run: npm install playwright && npx playwright install chromium";
5780
6184
  }
5781
- if (!this.browser) {
5782
- const pw = playwright;
5783
- this.browser = await pw.chromium.launch({ headless: true });
5784
- const b = this.browser;
5785
- this.page = await b.newPage();
5786
- }
5787
- const page = this.page;
5788
6185
  const action = input["action"];
5789
6186
  const timeout = input["timeout"] ?? 1e4;
5790
- switch (action) {
5791
- case "navigate": {
5792
- await page.goto(input["url"], { timeout });
5793
- return `Navigated to ${input["url"]}`;
5794
- }
5795
- case "click": {
5796
- await page.click(input["selector"], { timeout });
5797
- return `Clicked ${input["selector"]}`;
5798
- }
5799
- case "fill": {
5800
- await page.fill(input["selector"], input["value"]);
5801
- return `Filled ${input["selector"]} with value`;
5802
- }
5803
- case "screenshot": {
5804
- const buf = await page.screenshot({ type: "png" });
5805
- return `data:image/png;base64,${buf.toString("base64")}`;
5806
- }
5807
- case "evaluate": {
5808
- const result = await page.evaluate(input["script"]);
5809
- return JSON.stringify(result);
6187
+ if (action === "close") {
6188
+ await this.close();
6189
+ return "Browser closed.";
6190
+ }
6191
+ if (!this.browser || !this.page) {
6192
+ await this.close();
6193
+ const launchPromise = playwright.chromium.launch({ headless: true });
6194
+ const timeoutPromise = new Promise(
6195
+ (_, 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)
6196
+ );
6197
+ try {
6198
+ this.browser = await Promise.race([launchPromise, timeoutPromise]);
6199
+ this.page = await this.browser.newPage();
6200
+ } catch (err) {
6201
+ this.browser = null;
6202
+ this.page = null;
6203
+ return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
5810
6204
  }
5811
- case "extract_text": {
5812
- const text = await page.locator("body").innerText();
5813
- return text.slice(0, 1e4);
6205
+ }
6206
+ const page = this.page;
6207
+ try {
6208
+ switch (action) {
6209
+ case "navigate": {
6210
+ await page.goto(input["url"], { timeout });
6211
+ const title = await page.title();
6212
+ return `Navigated to ${input["url"]} (title: "${title}")`;
6213
+ }
6214
+ case "click": {
6215
+ await page.click(input["selector"], { timeout });
6216
+ return `Clicked ${input["selector"]}`;
6217
+ }
6218
+ case "fill": {
6219
+ await page.fill(input["selector"], input["value"]);
6220
+ return `Filled ${input["selector"]} with value`;
6221
+ }
6222
+ case "screenshot": {
6223
+ const buf = await page.screenshot({ type: "png" });
6224
+ return `data:image/png;base64,${buf.toString("base64")}`;
6225
+ }
6226
+ case "evaluate": {
6227
+ const result = await page.evaluate(input["script"]);
6228
+ return JSON.stringify(result);
6229
+ }
6230
+ case "extract_text": {
6231
+ const text = await page.locator("body").innerText();
6232
+ return text.slice(0, 1e4);
6233
+ }
6234
+ case "wait": {
6235
+ await page.waitForTimeout(timeout);
6236
+ return `Waited ${timeout}ms`;
6237
+ }
6238
+ default:
6239
+ return `Unknown browser action: ${action}. Supported: navigate, click, fill, screenshot, evaluate, extract_text, wait, close`;
5814
6240
  }
5815
- case "wait": {
5816
- await page.waitForTimeout(timeout);
5817
- return `Waited ${timeout}ms`;
6241
+ } catch (err) {
6242
+ const errMsg = err instanceof Error ? err.message : String(err);
6243
+ if (/Target closed|Page crashed|Navigation failed/i.test(errMsg)) {
6244
+ await this.close();
6245
+ return `Browser error (page reset): ${errMsg}`;
5818
6246
  }
5819
- default:
5820
- throw new Error(`Unknown browser action: ${action}`);
6247
+ return `Browser action "${action}" failed: ${errMsg}`;
5821
6248
  }
5822
6249
  }
5823
6250
  async close() {
5824
- if (this.browser) {
5825
- await this.browser.close();
6251
+ try {
6252
+ if (this.page) {
6253
+ await this.page.close().catch(() => {
6254
+ });
6255
+ this.page = null;
6256
+ }
6257
+ if (this.browser) {
6258
+ await this.browser.close().catch(() => {
6259
+ });
6260
+ this.browser = null;
6261
+ }
6262
+ } catch {
5826
6263
  this.browser = null;
5827
6264
  this.page = null;
5828
6265
  }
@@ -5919,6 +6356,19 @@ var PDFCreateTool = class extends BaseTool {
5919
6356
  });
5920
6357
  }
5921
6358
  };
6359
+ function detectCommand(candidates2) {
6360
+ for (const cmd of candidates2) {
6361
+ try {
6362
+ const which = process.platform === "win32" ? "where" : "which";
6363
+ execSync(`${which} ${cmd}`, { stdio: "ignore" });
6364
+ return cmd;
6365
+ } catch {
6366
+ }
6367
+ }
6368
+ return null;
6369
+ }
6370
+ var PYTHON_CMD = detectCommand(["python3", "python"]);
6371
+ var NODE_CMD = detectCommand(["node"]);
5922
6372
  var CodeInterpreterTool = class extends BaseTool {
5923
6373
  name = "run_code";
5924
6374
  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.";
@@ -5934,10 +6384,30 @@ var CodeInterpreterTool = class extends BaseTool {
5934
6384
  isDangerous() {
5935
6385
  return true;
5936
6386
  }
5937
- async execute(input, options) {
6387
+ async execute(input, _options) {
5938
6388
  const language = input["language"];
5939
6389
  const code = input["code"];
5940
6390
  const args = input["args"] ?? [];
6391
+ let cmdPrefix;
6392
+ if (language === "python") {
6393
+ if (!PYTHON_CMD) {
6394
+ return [
6395
+ "Error: Python interpreter not found.",
6396
+ "Please install Python and ensure it is in your PATH.",
6397
+ "Tried: python3, python"
6398
+ ].join("\n");
6399
+ }
6400
+ cmdPrefix = PYTHON_CMD;
6401
+ } else {
6402
+ if (!NODE_CMD) {
6403
+ return [
6404
+ "Error: Node.js interpreter not found.",
6405
+ "Please install Node.js and ensure it is in your PATH.",
6406
+ "Tried: node"
6407
+ ].join("\n");
6408
+ }
6409
+ cmdPrefix = NODE_CMD;
6410
+ }
5941
6411
  const tmpDir = path17.join(process.cwd(), ".cascade", "tmp");
5942
6412
  if (!fs14.existsSync(tmpDir)) {
5943
6413
  fs14.mkdirSync(tmpDir, { recursive: true });
@@ -5946,8 +6416,9 @@ var CodeInterpreterTool = class extends BaseTool {
5946
6416
  const fileName = `intp_${randomUUID().slice(0, 8)}.${extension}`;
5947
6417
  const filePath = path17.join(tmpDir, fileName);
5948
6418
  fs14.writeFileSync(filePath, code, "utf-8");
5949
- const cmdPrefix = language === "python" ? "python3" : "node";
5950
- const fullCmd = `${cmdPrefix} "${filePath}" ${args.map((a) => `"${a}"`).join(" ")}`;
6419
+ const quotedPath = `"${filePath}"`;
6420
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
6421
+ const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
5951
6422
  return new Promise((resolve) => {
5952
6423
  const startMs = Date.now();
5953
6424
  exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -5960,10 +6431,17 @@ var CodeInterpreterTool = class extends BaseTool {
5960
6431
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
5961
6432
  }
5962
6433
  if (error) {
5963
- resolve(`Execution failed (${duration}ms):
6434
+ const timedOut = error.killed && duration >= 3e4;
6435
+ if (timedOut) {
6436
+ resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
6437
+ Partial stdout: ${stdout}
6438
+ Stderr: ${stderr}`);
6439
+ } else {
6440
+ resolve(`Execution failed (${duration}ms):
5964
6441
  Error: ${error.message}
5965
6442
  Stderr: ${stderr}
5966
6443
  Stdout: ${stdout}`);
6444
+ }
5967
6445
  } else {
5968
6446
  resolve(`Execution successful (${duration}ms):
5969
6447
  Stdout: ${stdout}
@@ -6028,6 +6506,186 @@ ${formatted}`;
6028
6506
  }
6029
6507
  };
6030
6508
 
6509
+ // src/tools/web-search.ts
6510
+ async function searchSearXNG(query, baseUrl, maxResults) {
6511
+ const url = new URL("/search", baseUrl);
6512
+ url.searchParams.set("q", query);
6513
+ url.searchParams.set("format", "json");
6514
+ url.searchParams.set("categories", "general");
6515
+ url.searchParams.set("engines", "google,bing,duckduckgo");
6516
+ const resp = await fetch(url.toString(), {
6517
+ headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
6518
+ signal: AbortSignal.timeout(1e4)
6519
+ });
6520
+ if (!resp.ok) {
6521
+ throw new Error(`SearXNG returned HTTP ${resp.status}`);
6522
+ }
6523
+ const data = await resp.json();
6524
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6525
+ title: r.title ?? "",
6526
+ url: r.url ?? "",
6527
+ snippet: r.content ?? "",
6528
+ engine: `searxng(${r.engine ?? "unknown"})`
6529
+ }));
6530
+ }
6531
+ async function searchBrave(query, apiKey, maxResults) {
6532
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}&safesearch=off`;
6533
+ const resp = await fetch(url, {
6534
+ headers: {
6535
+ "Accept": "application/json",
6536
+ "Accept-Encoding": "gzip",
6537
+ "X-Subscription-Token": apiKey
6538
+ },
6539
+ signal: AbortSignal.timeout(1e4)
6540
+ });
6541
+ if (!resp.ok) {
6542
+ throw new Error(`Brave Search returned HTTP ${resp.status}`);
6543
+ }
6544
+ const data = await resp.json();
6545
+ return (data.web?.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6546
+ title: r.title ?? "",
6547
+ url: r.url ?? "",
6548
+ snippet: r.description ?? "",
6549
+ engine: "brave"
6550
+ }));
6551
+ }
6552
+ async function searchTavily(query, apiKey, maxResults) {
6553
+ const resp = await fetch("https://api.tavily.com/search", {
6554
+ method: "POST",
6555
+ headers: {
6556
+ "Content-Type": "application/json",
6557
+ "Authorization": `Bearer ${apiKey}`
6558
+ },
6559
+ body: JSON.stringify({
6560
+ query,
6561
+ max_results: maxResults,
6562
+ search_depth: "basic",
6563
+ include_answer: false,
6564
+ include_raw_content: false
6565
+ }),
6566
+ signal: AbortSignal.timeout(15e3)
6567
+ });
6568
+ if (!resp.ok) {
6569
+ throw new Error(`Tavily returned HTTP ${resp.status}`);
6570
+ }
6571
+ const data = await resp.json();
6572
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6573
+ title: r.title ?? "",
6574
+ url: r.url ?? "",
6575
+ snippet: r.content ?? "",
6576
+ engine: "tavily"
6577
+ }));
6578
+ }
6579
+ async function searchDuckDuckGoLite(query, maxResults) {
6580
+ const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
6581
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
6582
+ signal: AbortSignal.timeout(1e4)
6583
+ });
6584
+ if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
6585
+ const html = await resp.text();
6586
+ const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
6587
+ const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
6588
+ const links = [];
6589
+ const snippets = [];
6590
+ let m;
6591
+ while ((m = linkPattern.exec(html)) !== null) {
6592
+ links.push({ url: m[1], title: m[2].trim() });
6593
+ }
6594
+ while ((m = snippetPattern.exec(html)) !== null) {
6595
+ snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
6596
+ }
6597
+ return links.slice(0, maxResults).map((link, i) => ({
6598
+ title: link.title,
6599
+ url: link.url,
6600
+ snippet: snippets[i] ?? "",
6601
+ engine: "duckduckgo-lite"
6602
+ }));
6603
+ }
6604
+ var WebSearchTool = class extends BaseTool {
6605
+ name = "web_search";
6606
+ description = "Search the web for current information, news, documentation, or any topic. Returns a list of relevant results with titles, URLs, and snippets.";
6607
+ inputSchema = {
6608
+ type: "object",
6609
+ properties: {
6610
+ query: { type: "string", description: "The search query" },
6611
+ maxResults: { type: "number", description: "Number of results to return (default: 5, max: 10)" }
6612
+ },
6613
+ required: ["query"]
6614
+ };
6615
+ config;
6616
+ constructor(config = {}) {
6617
+ super();
6618
+ this.config = {
6619
+ searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
6620
+ braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
6621
+ tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
6622
+ maxResults: config.maxResults ?? 5
6623
+ };
6624
+ }
6625
+ async execute(input, _options) {
6626
+ const query = input["query"];
6627
+ if (!query?.trim()) return "Error: query is required and must be non-empty.";
6628
+ const maxResults = Math.min(
6629
+ input["maxResults"] ?? this.config.maxResults ?? 5,
6630
+ 10
6631
+ );
6632
+ const errors = [];
6633
+ let results = [];
6634
+ if (this.config.searxngUrl) {
6635
+ try {
6636
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
6637
+ if (results.length > 0) return this.formatResults(query, results);
6638
+ errors.push("SearXNG: returned 0 results");
6639
+ } catch (err) {
6640
+ errors.push(`SearXNG: ${err instanceof Error ? err.message : String(err)}`);
6641
+ }
6642
+ }
6643
+ if (this.config.braveApiKey) {
6644
+ try {
6645
+ results = await searchBrave(query, this.config.braveApiKey, maxResults);
6646
+ if (results.length > 0) return this.formatResults(query, results);
6647
+ errors.push("Brave: returned 0 results");
6648
+ } catch (err) {
6649
+ errors.push(`Brave: ${err instanceof Error ? err.message : String(err)}`);
6650
+ }
6651
+ }
6652
+ if (this.config.tavilyApiKey) {
6653
+ try {
6654
+ results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
6655
+ if (results.length > 0) return this.formatResults(query, results);
6656
+ errors.push("Tavily: returned 0 results");
6657
+ } catch (err) {
6658
+ errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
6659
+ }
6660
+ }
6661
+ try {
6662
+ results = await searchDuckDuckGoLite(query, maxResults);
6663
+ if (results.length > 0) return this.formatResults(query, results);
6664
+ errors.push("DuckDuckGo Lite: returned 0 results");
6665
+ } catch (err) {
6666
+ errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
6667
+ }
6668
+ 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" : "";
6669
+ return [
6670
+ `Web search for "${query}" failed across all backends:`,
6671
+ ...errors.map((e) => ` \u2022 ${e}`),
6672
+ configHint
6673
+ ].join("\n");
6674
+ }
6675
+ formatResults(query, results) {
6676
+ const lines = [`Web search results for: "${query}"`, ""];
6677
+ for (let i = 0; i < results.length; i++) {
6678
+ const r = results[i];
6679
+ lines.push(`[${i + 1}] ${r.title}`);
6680
+ lines.push(` URL: ${r.url}`);
6681
+ if (r.snippet) lines.push(` ${r.snippet.slice(0, 300)}`);
6682
+ if (r.engine) lines.push(` Source: ${r.engine}`);
6683
+ lines.push("");
6684
+ }
6685
+ return lines.join("\n");
6686
+ }
6687
+ };
6688
+
6031
6689
  // src/tools/mcp.ts
6032
6690
  var McpToolWrapper = class extends BaseTool {
6033
6691
  name;
@@ -6149,7 +6807,8 @@ var ToolRegistry = class {
6149
6807
  new ImageAnalyzeTool(),
6150
6808
  new PDFCreateTool(),
6151
6809
  new CodeInterpreterTool(),
6152
- new PeerCommunicationTool()
6810
+ new PeerCommunicationTool(),
6811
+ new WebSearchTool(this.config.webSearch)
6153
6812
  ];
6154
6813
  for (const tool of tools) {
6155
6814
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -6173,8 +6832,23 @@ var ToolRegistry = class {
6173
6832
  return this.ignoreMatcher.ignores(posixRel);
6174
6833
  }
6175
6834
  };
6176
- var McpClient = class {
6835
+ var McpClient = class _McpClient {
6836
+ static activeProcessPids = /* @__PURE__ */ new Set();
6837
+ /**
6838
+ * Forcefully kills all known MCP child processes.
6839
+ * Call this from global process exit handlers to prevent zombie processes.
6840
+ */
6841
+ static killAllProcesses() {
6842
+ for (const pid of _McpClient.activeProcessPids) {
6843
+ try {
6844
+ process.kill(pid, "SIGKILL");
6845
+ } catch {
6846
+ }
6847
+ }
6848
+ _McpClient.activeProcessPids.clear();
6849
+ }
6177
6850
  clients = /* @__PURE__ */ new Map();
6851
+ transports = /* @__PURE__ */ new Map();
6178
6852
  tools = /* @__PURE__ */ new Map();
6179
6853
  trustedServers;
6180
6854
  approvalCallback;
@@ -6203,6 +6877,8 @@ var McpClient = class {
6203
6877
  );
6204
6878
  await client.connect(transport);
6205
6879
  this.clients.set(server.name, client);
6880
+ this.transports.set(server.name, transport);
6881
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
6206
6882
  const toolsResult = await client.listTools();
6207
6883
  for (const tool of toolsResult.tools) {
6208
6884
  for (const existing of this.tools.values()) {
@@ -6224,8 +6900,11 @@ var McpClient = class {
6224
6900
  async disconnect(serverName) {
6225
6901
  const client = this.clients.get(serverName);
6226
6902
  if (client) {
6903
+ const transport = this.transports.get(serverName);
6904
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
6227
6905
  await client.close();
6228
6906
  this.clients.delete(serverName);
6907
+ this.transports.delete(serverName);
6229
6908
  for (const key of this.tools.keys()) {
6230
6909
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
6231
6910
  }
@@ -6253,6 +6932,13 @@ var McpClient = class {
6253
6932
  getConnectedServers() {
6254
6933
  return Array.from(this.clients.keys());
6255
6934
  }
6935
+ getActivePids() {
6936
+ const pids = [];
6937
+ for (const transport of this.transports.values()) {
6938
+ if (transport.pid) pids.push(transport.pid);
6939
+ }
6940
+ return pids;
6941
+ }
6256
6942
  isConnected(serverName) {
6257
6943
  return this.clients.has(serverName);
6258
6944
  }
@@ -6821,12 +7507,25 @@ var Cascade = class extends EventEmitter {
6821
7507
  looksLikeSimpleArtifactTask(prompt) {
6822
7508
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
6823
7509
  }
6824
- async determineComplexity(prompt, conversationHistory = []) {
7510
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
6825
7511
  if (this.looksLikeSimpleArtifactTask(prompt)) {
6826
7512
  return "Simple";
6827
7513
  }
7514
+ let workspaceContext = "";
7515
+ try {
7516
+ const files = await glob("**/*.*", {
7517
+ cwd: workspacePath,
7518
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
7519
+ nodir: true
7520
+ });
7521
+ workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
7522
+ } catch {
7523
+ workspaceContext = "Workspace Scout: Could not scan workspace.";
7524
+ }
6828
7525
  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.
6829
7526
 
7527
+ ${workspaceContext}
7528
+
6830
7529
  Classification:
6831
7530
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
6832
7531
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -6901,7 +7600,7 @@ ${prompt}` : prompt;
6901
7600
  }
6902
7601
  escalator.resolveUserDecision(req.id, approved, always);
6903
7602
  });
6904
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
7603
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
6905
7604
  this.telemetry.capture("cascade:session_start", {
6906
7605
  complexity,
6907
7606
  providerCount: this.config.providers.length,
@@ -6981,7 +7680,7 @@ ${prompt}` : prompt;
6981
7680
  peerT3Ids: [],
6982
7681
  parentT2: "root"
6983
7682
  };
6984
- const t3Result = await t3.execute(assignment, taskId);
7683
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
6985
7684
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
6986
7685
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
6987
7686
  } else if (complexity === "Moderate") {
@@ -7004,7 +7703,7 @@ ${prompt}` : prompt;
7004
7703
  constraints: [],
7005
7704
  t3Subtasks: []
7006
7705
  };
7007
- const t2Result = await t2.execute(assignment, taskId);
7706
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
7008
7707
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
7009
7708
  t2Results = [t2Result];
7010
7709
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -7026,13 +7725,22 @@ ${prompt}` : prompt;
7026
7725
  if (toolCreator) t1.setToolCreator(toolCreator);
7027
7726
  bindTierEvents(t1);
7028
7727
  t1.on("plan", (e) => this.emit("plan", e));
7029
- const result = await t1.execute(options.prompt, options.images);
7728
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
7030
7729
  finalOutput = result.output;
7031
7730
  t2Results = result.t2Results;
7032
7731
  }
7033
7732
  } catch (err) {
7034
- runError = err;
7035
- throw err;
7733
+ if (err instanceof CascadeCancelledError) {
7734
+ this.emit("run:cancelled", {
7735
+ taskId,
7736
+ reason: err.message,
7737
+ partialOutput: finalOutput || ""
7738
+ });
7739
+ runError = null;
7740
+ } else {
7741
+ runError = err;
7742
+ throw err;
7743
+ }
7036
7744
  } finally {
7037
7745
  try {
7038
7746
  escalator.cancelAllPending();
@@ -7767,6 +8475,9 @@ var ModelsDisplay = ({
7767
8475
  });
7768
8476
  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}`;
7769
8477
  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}`;
8478
+ const PAGE_SIZE = 8;
8479
+ const viewStart = Math.max(0, Math.min(cursor - Math.floor(PAGE_SIZE / 2), currentItems.length - PAGE_SIZE));
8480
+ const visibleItems = currentItems.slice(viewStart, viewStart + PAGE_SIZE);
7770
8481
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7771
8482
  /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
7772
8483
  /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: title }),
@@ -7776,16 +8487,29 @@ var ModelsDisplay = ({
7776
8487
  breadcrumb,
7777
8488
  " \xB7 \u2191/\u2193 navigate \xB7 1\u20139 jump"
7778
8489
  ] }) }),
7779
- currentItems.length === 0 ? /* @__PURE__ */ jsx(Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: currentItems.map((item, i) => {
7780
- const focused = i === cursor;
7781
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
7782
- /* @__PURE__ */ jsx(Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
7783
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
7784
- /* @__PURE__ */ jsx(Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
7785
- item.sublabel && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
7786
- ] })
7787
- ] }, `${step}-${item.value}-${i}`);
7788
- }) })
8490
+ currentItems.length === 0 ? /* @__PURE__ */ jsx(Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
8491
+ viewStart > 0 && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
8492
+ " \u2191 ",
8493
+ viewStart,
8494
+ " more above"
8495
+ ] }),
8496
+ visibleItems.map((item, i) => {
8497
+ const globalIdx = viewStart + i;
8498
+ const focused = globalIdx === cursor;
8499
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "row", children: [
8500
+ /* @__PURE__ */ jsx(Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
8501
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
8502
+ /* @__PURE__ */ jsx(Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
8503
+ item.sublabel && /* @__PURE__ */ jsx(Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
8504
+ ] })
8505
+ ] }, `${step}-${item.value}-${globalIdx}`);
8506
+ }),
8507
+ viewStart + PAGE_SIZE < currentItems.length && /* @__PURE__ */ jsxs(Text, { color: "gray", dimColor: true, children: [
8508
+ " \u2193 ",
8509
+ currentItems.length - viewStart - PAGE_SIZE,
8510
+ " more below"
8511
+ ] })
8512
+ ] })
7789
8513
  ] });
7790
8514
  };
7791
8515
  function CostTracker({
@@ -7979,13 +8703,18 @@ function replReducer(state, action) {
7979
8703
  async function refreshModelCache(store, providers) {
7980
8704
  for (const provider of providers) {
7981
8705
  try {
7982
- const dummyModel = { id: "dummy", name: "dummy", provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8706
+ const dummyId = provider.type === "azure" ? provider.deploymentName || "azure-model" : "dummy";
8707
+ const dummyModel = { id: dummyId, name: dummyId, provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
7983
8708
  let instance;
7984
8709
  if (provider.type === "openai") instance = new OpenAIProvider(provider, dummyModel);
7985
8710
  else if (provider.type === "gemini") instance = new GeminiProvider(provider, dummyModel);
7986
8711
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
7987
8712
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
7988
8713
  else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
8714
+ else if (provider.type === "azure") {
8715
+ const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
8716
+ instance = new AzureOpenAIProvider2(provider, dummyModel);
8717
+ }
7989
8718
  if (instance) {
7990
8719
  const fetched = await instance.listModels();
7991
8720
  for (const m of fetched) store.upsertCachedModel(m);
@@ -8123,7 +8852,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName })
8123
8852
  };
8124
8853
  const store = new MemoryStore(path17.join(workspacePath, CASCADE_DB_FILE));
8125
8854
  storeRef.current = store;
8126
- globalStoreRef.current = new MemoryStore(path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8855
+ globalStoreRef.current = new MemoryStore(path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8127
8856
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
8128
8857
  setIdentities(identityRows);
8129
8858
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -8608,14 +9337,29 @@ Use /identity <name|id> to switch.`;
8608
9337
  const isAutoScrollingRef = useRef(true);
8609
9338
  const width = stdout?.columns ?? 100;
8610
9339
  const height = stdout?.rows ?? 24;
8611
- const statusHeight = state.showDetails ? state.agentTree ? 10 : 4 : 3;
9340
+ const hasActiveOrFailed2 = (node) => {
9341
+ if (node.status === "ACTIVE" || node.status === "FAILED") return true;
9342
+ return node.children?.some(hasActiveOrFailed2) ?? false;
9343
+ };
9344
+ let agentTreeHeight = 0;
9345
+ if (state.agentTree && hasActiveOrFailed2(state.agentTree)) {
9346
+ agentTreeHeight = 1;
9347
+ const childrenCount = state.agentTree.children?.length ?? 0;
9348
+ agentTreeHeight += Math.min(childrenCount, 6);
9349
+ if (childrenCount > 6) agentTreeHeight += 1;
9350
+ }
9351
+ let timelineHeight = 0;
9352
+ if (state.showDetails && treeNodesRef.current.size > 0) {
9353
+ timelineHeight = 1;
9354
+ timelineHeight += Math.min(3, treeNodesRef.current.size);
9355
+ }
9356
+ const statusHeight = agentTreeHeight + timelineHeight;
8612
9357
  const costHeight = state.showCost ? 6 : 0;
8613
9358
  const approvalHeight = state.approvalRequest ? 12 : 0;
8614
9359
  const slashVisibleCount = Math.min(SLASH_PAGE_SIZE, slashEntries.length);
8615
9360
  const slashHeight = slashVisibleCount > 0 ? slashVisibleCount + 2 : 0;
8616
9361
  const chromeHeight = statusHeight + costHeight + approvalHeight + slashHeight + 7;
8617
- const totalCap = Math.floor(height * 0.7);
8618
- const availableHeight = Math.max(4, totalCap - chromeHeight);
9362
+ const availableHeight = Math.max(4, height - chromeHeight);
8619
9363
  const allLines = formatToLines(
8620
9364
  state.isStreaming ? [...state.messages, { id: "stream", role: "assistant", content: state.streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }] : state.messages,
8621
9365
  width - 4,
@@ -8996,10 +9740,14 @@ function wizardReducer(state, action) {
8996
9740
  return { ...state, currentEntryIdx: next };
8997
9741
  }
8998
9742
  case "ADD_AZURE": {
9743
+ const prevAzure = state.entries.find((e) => e.type === "azure");
8999
9744
  const newEntry = {
9000
9745
  id: randomUUID(),
9001
9746
  type: "azure",
9002
- label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`
9747
+ label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`,
9748
+ baseUrl: prevAzure?.baseUrl,
9749
+ apiKey: prevAzure?.apiKey,
9750
+ apiVersion: prevAzure?.apiVersion
9003
9751
  };
9004
9752
  return {
9005
9753
  ...state,
@@ -9108,8 +9856,9 @@ function SetupWizard({ workspacePath, onComplete }) {
9108
9856
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
9109
9857
  } else if (type === "azure") {
9110
9858
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
9111
- const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9112
- const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName }, dummyModel);
9859
+ const actualModelId = deploymentName || `azure-${entry.id}`;
9860
+ const dummyModel = { id: actualModelId, name: actualModelId, provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9861
+ const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName, apiVersion: entry.apiVersion }, dummyModel);
9113
9862
  const fetched = await p.listModels();
9114
9863
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
9115
9864
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
@@ -9130,7 +9879,8 @@ function SetupWizard({ workspacePath, onComplete }) {
9130
9879
  type: e.type,
9131
9880
  ...e.apiKey ? { apiKey: e.apiKey } : {},
9132
9881
  ...e.baseUrl ? { baseUrl: e.baseUrl } : {},
9133
- ...e.deploymentName ? { deploymentName: e.deploymentName } : {}
9882
+ ...e.deploymentName ? { deploymentName: e.deploymentName } : {},
9883
+ ...e.apiVersion ? { apiVersion: e.apiVersion } : {}
9134
9884
  }));
9135
9885
  const models = {};
9136
9886
  if (state.tierT1 !== "auto") models["t1"] = state.tierT1;
@@ -9160,17 +9910,17 @@ function SetupWizard({ workspacePath, onComplete }) {
9160
9910
  if (key.return) {
9161
9911
  if (state.selectedTypes.size === 0) return;
9162
9912
  dispatch({ type: "CONFIRM_PROVIDERS" });
9163
- setFieldStage("apiKey");
9913
+ const firstType = [...state.selectedTypes][0];
9914
+ setFieldStage(firstType === "azure" ? "deploymentName" : firstType === "openai-compatible" ? "label" : firstType === "ollama" ? "baseUrl" : "apiKey");
9164
9915
  setFieldBuffer("");
9165
9916
  }
9166
9917
  }
9167
9918
  if (state.step === "TIER_ASSIGN") {
9168
- if (key.tab || key.downArrow) {
9919
+ if (key.tab || key.rightArrow) {
9169
9920
  const order = ["T1", "T2", "T3"];
9170
9921
  const idx = order.indexOf(state.tierSelectFocus);
9171
9922
  dispatch({ type: "SET_TIER_FOCUS", tier: order[(idx + 1) % 3] });
9172
9923
  }
9173
- if (key.return) dispatch({ type: "GO_SAVE" });
9174
9924
  }
9175
9925
  });
9176
9926
  const currentEntry = state.entries[state.currentEntryIdx];
@@ -9180,7 +9930,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9180
9930
  if (fieldStage === "deploymentName") {
9181
9931
  dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
9182
9932
  setFieldBuffer("");
9183
- setFieldStage("baseUrl");
9933
+ if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
9934
+ setFieldStage("askMore");
9935
+ } else {
9936
+ setFieldStage("baseUrl");
9937
+ }
9184
9938
  } else if (fieldStage === "baseUrl") {
9185
9939
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
9186
9940
  setFieldBuffer("");
@@ -9188,6 +9942,10 @@ function SetupWizard({ workspacePath, onComplete }) {
9188
9942
  } else if (fieldStage === "apiKey") {
9189
9943
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9190
9944
  setFieldBuffer("");
9945
+ setFieldStage("apiVersion");
9946
+ } else if (fieldStage === "apiVersion") {
9947
+ dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
9948
+ setFieldBuffer("");
9191
9949
  setFieldStage("askMore");
9192
9950
  }
9193
9951
  } else if (currentEntry.type === "openai-compatible") {
@@ -9207,13 +9965,19 @@ function SetupWizard({ workspacePath, onComplete }) {
9207
9965
  } else if (currentEntry.type === "ollama") {
9208
9966
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
9209
9967
  setFieldBuffer("");
9968
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
9969
+ if (nextEntry) {
9970
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
9971
+ }
9210
9972
  dispatch({ type: "NEXT_ENTRY" });
9211
- setFieldStage("apiKey");
9212
9973
  } else {
9213
9974
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9214
9975
  setFieldBuffer("");
9976
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
9977
+ if (nextEntry) {
9978
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
9979
+ }
9215
9980
  dispatch({ type: "NEXT_ENTRY" });
9216
- setFieldStage("apiKey");
9217
9981
  }
9218
9982
  }, [currentEntry, fieldStage]);
9219
9983
  if (state.step === "PROVIDER_SELECT") {
@@ -9264,8 +10028,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9264
10028
  setFieldStage(isAzure ? "deploymentName" : "label");
9265
10029
  setFieldBuffer("");
9266
10030
  } else {
10031
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10032
+ if (nextEntry) {
10033
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10034
+ }
9267
10035
  dispatch({ type: "NEXT_ENTRY" });
9268
- setFieldStage("apiKey");
9269
10036
  setFieldBuffer("");
9270
10037
  }
9271
10038
  }
@@ -9346,7 +10113,16 @@ function SetupWizard({ workspacePath, onComplete }) {
9346
10113
  SelectInput,
9347
10114
  {
9348
10115
  items: modelOptions,
9349
- onSelect: (item) => dispatch({ type: "SET_TIER", tier, value: item.value }),
10116
+ onSelect: (item) => {
10117
+ dispatch({ type: "SET_TIER", tier, value: item.value });
10118
+ const order = ["T1", "T2", "T3"];
10119
+ const idx = order.indexOf(tier);
10120
+ if (idx < 2) {
10121
+ dispatch({ type: "SET_TIER_FOCUS", tier: order[idx + 1] });
10122
+ } else {
10123
+ dispatch({ type: "GO_SAVE" });
10124
+ }
10125
+ },
9350
10126
  indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsx(Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9351
10127
  itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsx(Text, { color: isSelected ? "magenta" : "white", children: label })
9352
10128
  }
@@ -9824,7 +10600,7 @@ var DashboardServer = class {
9824
10600
  // ── Setup ─────────────────────────────────────
9825
10601
  getGlobalStore() {
9826
10602
  if (!this.globalStore) {
9827
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10603
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9828
10604
  this.globalStore = new MemoryStore(globalDbPath);
9829
10605
  }
9830
10606
  return this.globalStore;
@@ -9886,7 +10662,7 @@ var DashboardServer = class {
9886
10662
  }
9887
10663
  watchRuntimeChanges() {
9888
10664
  const workspaceDbPath = path17.join(this.workspacePath, CASCADE_DB_FILE);
9889
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10665
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9890
10666
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
9891
10667
  for (const watchPath of watchPaths) {
9892
10668
  if (!fs14.existsSync(watchPath)) continue;
@@ -9994,7 +10770,7 @@ var DashboardServer = class {
9994
10770
  const sessionId = req.params.id;
9995
10771
  this.store.deleteSession(sessionId);
9996
10772
  this.store.deleteRuntimeSession(sessionId);
9997
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10773
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9998
10774
  const globalStore = new MemoryStore(globalDbPath);
9999
10775
  try {
10000
10776
  globalStore.deleteRuntimeSession(sessionId);
@@ -10008,7 +10784,7 @@ var DashboardServer = class {
10008
10784
  });
10009
10785
  this.app.delete("/api/sessions", auth, (req, res) => {
10010
10786
  const body = req.body;
10011
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10787
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10012
10788
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
10013
10789
  const globalStore = new MemoryStore(globalDbPath);
10014
10790
  try {
@@ -10031,7 +10807,7 @@ var DashboardServer = class {
10031
10807
  });
10032
10808
  this.app.delete("/api/runtime", auth, (_req, res) => {
10033
10809
  this.store.deleteAllRuntimeNodes();
10034
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10810
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10035
10811
  const globalStore = new MemoryStore(globalDbPath);
10036
10812
  try {
10037
10813
  globalStore.deleteAllRuntimeNodes();
@@ -10127,7 +10903,7 @@ var DashboardServer = class {
10127
10903
  this.app.get("/api/runtime", auth, (req, res) => {
10128
10904
  const scope = req.query["scope"] ?? "workspace";
10129
10905
  if (scope === "global") {
10130
- const globalDbPath = path17.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10906
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10131
10907
  const globalStore = new MemoryStore(globalDbPath);
10132
10908
  try {
10133
10909
  res.json({
@@ -10377,7 +11153,15 @@ async function exportCommand(options = {}) {
10377
11153
  const spin = ora({ text: "Loading sessions\u2026", color: "magenta" }).start();
10378
11154
  let store;
10379
11155
  try {
10380
- const dbPath = path17.join(process.env["HOME"] ?? "~", GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11156
+ const workspacePath = options.workspacePath ?? process.cwd();
11157
+ const workspaceDbPath = path17.join(workspacePath, CASCADE_DB_FILE);
11158
+ const globalDbPath = path17.join(os3.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11159
+ let dbPath = globalDbPath;
11160
+ try {
11161
+ await fs7.access(workspaceDbPath);
11162
+ dbPath = workspaceDbPath;
11163
+ } catch {
11164
+ }
10381
11165
  store = new MemoryStore(dbPath);
10382
11166
  } catch (err) {
10383
11167
  spin.fail(chalk8.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
@@ -10516,6 +11300,15 @@ async function telemetryCommand(action) {
10516
11300
 
10517
11301
  // src/cli/index.ts
10518
11302
  dotenv.config();
11303
+ process.on("exit", () => McpClient.killAllProcesses());
11304
+ process.on("SIGINT", () => {
11305
+ McpClient.killAllProcesses();
11306
+ process.exit(0);
11307
+ });
11308
+ process.on("SIGTERM", () => {
11309
+ McpClient.killAllProcesses();
11310
+ process.exit(0);
11311
+ });
10519
11312
  var program = new Command();
10520
11313
  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) => {
10521
11314
  await startRepl(options);