cascade-ai 0.2.2 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -11,7 +11,7 @@ var chalk8 = require('chalk');
11
11
  var dotenv = require('dotenv');
12
12
  var fs7 = require('fs/promises');
13
13
  var path17 = require('path');
14
- var os = require('os');
14
+ var os3 = require('os');
15
15
  var crypto = require('crypto');
16
16
  var fs14 = require('fs');
17
17
  var _ignoreModule = require('ignore');
@@ -20,6 +20,7 @@ var zod = require('zod');
20
20
  var child_process = require('child_process');
21
21
  var jsxRuntime = require('react/jsx-runtime');
22
22
  var EventEmitter = require('events');
23
+ var glob = require('glob');
23
24
  var util = require('util');
24
25
  var simpleGit = require('simple-git');
25
26
  var PDFDocument = require('pdfkit');
@@ -68,7 +69,7 @@ var chalk8__default = /*#__PURE__*/_interopDefault(chalk8);
68
69
  var dotenv__default = /*#__PURE__*/_interopDefault(dotenv);
69
70
  var fs7__default = /*#__PURE__*/_interopDefault(fs7);
70
71
  var path17__default = /*#__PURE__*/_interopDefault(path17);
71
- var os__default = /*#__PURE__*/_interopDefault(os);
72
+ var os3__default = /*#__PURE__*/_interopDefault(os3);
72
73
  var crypto__default = /*#__PURE__*/_interopDefault(crypto);
73
74
  var fs14__default = /*#__PURE__*/_interopDefault(fs14);
74
75
  var _ignoreModule__namespace = /*#__PURE__*/_interopNamespace(_ignoreModule);
@@ -129,7 +130,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
129
130
  var CASCADE_VERSION, CASCADE_CONFIG_FILE, CASCADE_DB_FILE, CASCADE_DASHBOARD_SECRET_FILE, GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE, GLOBAL_KEYSTORE_FILE, GLOBAL_RUNTIME_DB_FILE, DEFAULT_DASHBOARD_PORT, DEFAULT_CONTEXT_LIMIT, DEFAULT_AUTO_SUMMARIZE_AT, MODELS, T1_MODEL_PRIORITY, T2_MODEL_PRIORITY, T3_MODEL_PRIORITY, VISION_MODEL_PRIORITY, COMPLEXITY_T2_COUNT, THEME_NAMES, DEFAULT_THEME, OLLAMA_BASE_URL, LM_STUDIO_BASE_URL, AZURE_BASE_URL_TEMPLATE, TOOL_NAMES, DEFAULT_APPROVAL_REQUIRED;
130
131
  var init_constants = __esm({
131
132
  "src/constants.ts"() {
132
- CASCADE_VERSION = "0.2.2";
133
+ CASCADE_VERSION = "0.2.12";
133
134
  CASCADE_CONFIG_FILE = ".cascade/config.json";
134
135
  CASCADE_DB_FILE = ".cascade/memory.db";
135
136
  CASCADE_DASHBOARD_SECRET_FILE = ".cascade/dashboard-secret";
@@ -424,7 +425,8 @@ var init_constants = __esm({
424
425
  IMAGE_ANALYZE: "image_analyze",
425
426
  PDF_CREATE: "pdf_create",
426
427
  RUN_CODE: "run_code",
427
- PEER_MESSAGE: "peer_message"
428
+ PEER_MESSAGE: "peer_message",
429
+ WEB_SEARCH: "web_search"
428
430
  };
429
431
  DEFAULT_APPROVAL_REQUIRED = [
430
432
  TOOL_NAMES.SHELL,
@@ -567,17 +569,38 @@ var init_anthropic = __esm({
567
569
  messages,
568
570
  tools: tools?.length ? tools : void 0
569
571
  });
572
+ let isThinking = false;
570
573
  for await (const event of stream) {
571
- if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
572
- const text = event.delta.text;
573
- fullContent += text;
574
- onChunk({ text, finishReason: null });
574
+ if (event.type === "content_block_delta") {
575
+ if (event.delta.type === "thinking_delta") {
576
+ if (!isThinking) {
577
+ isThinking = true;
578
+ fullContent += "<think>\n";
579
+ onChunk({ text: "<think>\n", finishReason: null });
580
+ }
581
+ const text = event.delta.thinking;
582
+ fullContent += text;
583
+ onChunk({ text, finishReason: null });
584
+ } else if (event.delta.type === "text_delta") {
585
+ if (isThinking) {
586
+ isThinking = false;
587
+ fullContent += "\n</think>\n\n";
588
+ onChunk({ text: "\n</think>\n\n", finishReason: null });
589
+ }
590
+ const text = event.delta.text;
591
+ fullContent += text;
592
+ onChunk({ text, finishReason: null });
593
+ }
575
594
  } else if (event.type === "message_delta" && event.usage) {
576
595
  outputTokens = event.usage.output_tokens;
577
596
  } else if (event.type === "message_start" && event.message.usage) {
578
597
  inputTokens = event.message.usage.input_tokens;
579
598
  }
580
599
  }
600
+ if (isThinking) {
601
+ fullContent += "\n</think>\n\n";
602
+ onChunk({ text: "\n</think>\n\n", finishReason: null });
603
+ }
581
604
  const finalMessage = await stream.finalMessage();
582
605
  const toolCalls = finalMessage.content.filter((b) => b.type === "tool_use").map((b) => ({
583
606
  id: b.id,
@@ -638,33 +661,61 @@ var init_anthropic = __esm({
638
661
  }
639
662
  }
640
663
  convertMessages(messages) {
641
- return messages.filter((m) => m.role !== "system").map((m) => {
642
- if (typeof m.content === "string") {
643
- return { role: m.role, content: m.content };
664
+ const result = [];
665
+ for (const m of messages) {
666
+ if (m.role === "system") continue;
667
+ if (m.role === "tool") {
668
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
669
+ result.push({
670
+ role: "user",
671
+ content: [{
672
+ type: "tool_result",
673
+ tool_use_id: m.toolCallId ?? "",
674
+ content: toolContent
675
+ }]
676
+ });
677
+ continue;
644
678
  }
645
- const content = m.content.map((block) => {
646
- if (block.type === "text") return { type: "text", text: block.text };
647
- if (block.type === "image") {
648
- const img = block.image;
649
- if (img.type === "base64") {
650
- return {
651
- type: "image",
652
- source: {
653
- type: "base64",
654
- media_type: img.mimeType,
655
- data: img.data
679
+ if (m.role === "assistant") {
680
+ const content = [];
681
+ const text = typeof m.content === "string" ? m.content : "";
682
+ if (text) content.push({ type: "text", text });
683
+ for (const tc of m.toolCalls ?? []) {
684
+ content.push({
685
+ type: "tool_use",
686
+ id: tc.id,
687
+ name: tc.name,
688
+ input: tc.input
689
+ });
690
+ }
691
+ if (content.length > 0) {
692
+ result.push({ role: "assistant", content });
693
+ }
694
+ continue;
695
+ }
696
+ if (m.role === "user") {
697
+ if (typeof m.content === "string") {
698
+ result.push({ role: "user", content: m.content });
699
+ } else {
700
+ const content = m.content.map((block) => {
701
+ if (block.type === "text") return { type: "text", text: block.text };
702
+ if (block.type === "image") {
703
+ const img = block.image;
704
+ if (img.type === "base64") {
705
+ return {
706
+ type: "image",
707
+ source: { type: "base64", media_type: img.mimeType, data: img.data }
708
+ };
656
709
  }
657
- };
658
- }
659
- return {
660
- type: "image",
661
- source: { type: "url", url: img.data }
662
- };
710
+ return { type: "image", source: { type: "url", url: img.data } };
711
+ }
712
+ return { type: "text", text: "" };
713
+ });
714
+ result.push({ role: "user", content });
663
715
  }
664
- return { type: "text", text: "" };
665
- });
666
- return { role: m.role, content };
667
- });
716
+ }
717
+ }
718
+ return result;
668
719
  }
669
720
  };
670
721
  }
@@ -733,9 +784,25 @@ var init_openai = __esm({
733
784
  }
734
785
  }
735
786
  const toolCallsMap = {};
787
+ let isThinking = false;
736
788
  for await (const chunk of stream) {
737
789
  const delta = chunk.choices[0]?.delta;
790
+ const reasoningContent = delta?.reasoning_content;
791
+ if (reasoningContent) {
792
+ if (!isThinking) {
793
+ isThinking = true;
794
+ fullContent += "<think>\n";
795
+ onChunk({ text: "<think>\n", finishReason: null });
796
+ }
797
+ fullContent += reasoningContent;
798
+ onChunk({ text: reasoningContent, finishReason: null });
799
+ }
738
800
  if (delta?.content) {
801
+ if (isThinking) {
802
+ isThinking = false;
803
+ fullContent += "\n</think>\n\n";
804
+ onChunk({ text: "\n</think>\n\n", finishReason: null });
805
+ }
739
806
  fullContent += delta.content;
740
807
  onChunk({ text: delta.content, finishReason: null });
741
808
  }
@@ -758,6 +825,10 @@ var init_openai = __esm({
758
825
  outputTokens = chunk.usage.completion_tokens;
759
826
  }
760
827
  }
828
+ if (isThinking) {
829
+ fullContent += "\n</think>\n\n";
830
+ onChunk({ text: "\n</think>\n\n", finishReason: null });
831
+ }
761
832
  const toolCalls = Object.values(toolCallsMap).map((tc) => {
762
833
  let input = {};
763
834
  try {
@@ -962,7 +1033,7 @@ var init_gemini = __esm({
962
1033
  for (const part of candidate?.content?.parts ?? []) {
963
1034
  if (part.functionCall) {
964
1035
  toolCalls.push({
965
- id: `gemini-tool-${Date.now()}-${toolCalls.length}`,
1036
+ id: part.functionCall.name,
966
1037
  name: part.functionCall.name,
967
1038
  input: part.functionCall.args ?? {}
968
1039
  });
@@ -1050,10 +1121,70 @@ var init_gemini = __esm({
1050
1121
  }
1051
1122
  // ── Private ──────────────────────────────────
1052
1123
  buildContents(messages, extraImages) {
1053
- return messages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({
1054
- role: m.role === "assistant" ? "model" : "user",
1055
- parts: typeof m.content === "string" ? [{ text: m.content }] : this.convertMessageContent(m, extraImages)
1056
- }));
1124
+ const contents = [];
1125
+ for (const m of messages) {
1126
+ if (m.role === "system") {
1127
+ const text = typeof m.content === "string" ? m.content : "";
1128
+ if (!text.trim()) continue;
1129
+ const prev = contents[contents.length - 1];
1130
+ if (prev?.role === "user") {
1131
+ prev.parts.unshift({ text: `[System context]: ${text}
1132
+
1133
+ ` });
1134
+ } else {
1135
+ contents.push({ role: "user", parts: [{ text: `[System context]: ${text}` }] });
1136
+ }
1137
+ continue;
1138
+ }
1139
+ if (m.role === "tool") {
1140
+ const toolContent = typeof m.content === "string" ? m.content : JSON.stringify(m.content);
1141
+ const functionName = m.toolCallId ?? "unknown_function";
1142
+ contents.push({
1143
+ role: "user",
1144
+ parts: [{
1145
+ functionResponse: {
1146
+ name: functionName,
1147
+ response: { output: toolContent }
1148
+ }
1149
+ }]
1150
+ });
1151
+ continue;
1152
+ }
1153
+ if (m.role === "assistant") {
1154
+ const parts = [];
1155
+ const textContent = typeof m.content === "string" ? m.content : "";
1156
+ if (textContent) parts.push({ text: textContent });
1157
+ for (const tc of m.toolCalls ?? []) {
1158
+ parts.push({
1159
+ functionCall: {
1160
+ name: tc.name,
1161
+ args: tc.input
1162
+ }
1163
+ });
1164
+ }
1165
+ if (parts.length > 0) {
1166
+ contents.push({ role: "model", parts });
1167
+ }
1168
+ continue;
1169
+ }
1170
+ if (m.role === "user") {
1171
+ const parts = this.convertMessageContent(m, contents.length === 0 ? extraImages : void 0);
1172
+ if (extraImages?.length && contents.length > 0) {
1173
+ const isLastUser = !messages.slice(messages.indexOf(m) + 1).some((x) => x.role === "user");
1174
+ if (isLastUser) {
1175
+ for (const img of extraImages) {
1176
+ if (img.type === "base64") {
1177
+ parts.push({ inlineData: { mimeType: img.mimeType, data: img.data } });
1178
+ }
1179
+ }
1180
+ }
1181
+ }
1182
+ if (parts.length > 0) {
1183
+ contents.push({ role: "user", parts });
1184
+ }
1185
+ }
1186
+ }
1187
+ return contents;
1057
1188
  }
1058
1189
  convertMessageContent(msg, extraImages) {
1059
1190
  const parts = [];
@@ -1615,9 +1746,10 @@ var MemoryStore = class _MemoryStore {
1615
1746
  constructor(dbPath) {
1616
1747
  fs14__default.default.mkdirSync(path17__default.default.dirname(dbPath), { recursive: true });
1617
1748
  try {
1618
- this.db = new Database__default.default(dbPath);
1749
+ this.db = new Database__default.default(dbPath, { timeout: 5e3 });
1619
1750
  this.db.pragma("journal_mode = WAL");
1620
1751
  this.db.pragma("foreign_keys = ON");
1752
+ this.db.pragma("synchronous = NORMAL");
1621
1753
  this.migrate();
1622
1754
  } catch (err) {
1623
1755
  if (err instanceof Error && err.message.includes("Could not locate the bindings file")) {
@@ -1631,6 +1763,38 @@ Original error: ${err.message}`
1631
1763
  throw err;
1632
1764
  }
1633
1765
  }
1766
+ // ── Async Write Queue ─────────────────────────
1767
+ writeQueue = [];
1768
+ isProcessingQueue = false;
1769
+ async processQueue() {
1770
+ if (this.isProcessingQueue) return;
1771
+ this.isProcessingQueue = true;
1772
+ while (this.writeQueue.length > 0) {
1773
+ const op = this.writeQueue.shift();
1774
+ if (op) {
1775
+ let attempts = 0;
1776
+ while (attempts < 5) {
1777
+ try {
1778
+ op();
1779
+ break;
1780
+ } catch (err) {
1781
+ if (err instanceof Error && err.code === "SQLITE_BUSY") {
1782
+ attempts++;
1783
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, attempts)));
1784
+ } else {
1785
+ console.error("Cascade AI: DB Write Error:", err);
1786
+ break;
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ }
1792
+ this.isProcessingQueue = false;
1793
+ }
1794
+ enqueueWrite(op) {
1795
+ this.writeQueue.push(op);
1796
+ this.processQueue().catch(console.error);
1797
+ }
1634
1798
  // ── Sessions ──────────────────────────────────
1635
1799
  createSession(session) {
1636
1800
  this.db.prepare(`
@@ -1717,26 +1881,28 @@ Original error: ${err.message}`
1717
1881
  }
1718
1882
  // ── Runtime Sessions / Nodes ─────────────────
1719
1883
  upsertRuntimeSession(session) {
1720
- this.db.prepare(`
1721
- INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1722
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1723
- ON CONFLICT(session_id) DO UPDATE SET
1724
- title = excluded.title,
1725
- workspace_path = excluded.workspace_path,
1726
- status = excluded.status,
1727
- updated_at = excluded.updated_at,
1728
- latest_prompt = excluded.latest_prompt,
1729
- is_global = excluded.is_global
1730
- `).run(
1731
- session.sessionId,
1732
- session.title,
1733
- session.workspacePath,
1734
- session.status,
1735
- session.startedAt,
1736
- session.updatedAt,
1737
- session.latestPrompt ?? null,
1738
- session.isGlobal ? 1 : 0
1739
- );
1884
+ this.enqueueWrite(() => {
1885
+ this.db.prepare(`
1886
+ INSERT INTO runtime_sessions (session_id, title, workspace_path, status, started_at, updated_at, latest_prompt, is_global)
1887
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1888
+ ON CONFLICT(session_id) DO UPDATE SET
1889
+ title = excluded.title,
1890
+ workspace_path = excluded.workspace_path,
1891
+ status = excluded.status,
1892
+ updated_at = excluded.updated_at,
1893
+ latest_prompt = excluded.latest_prompt,
1894
+ is_global = excluded.is_global
1895
+ `).run(
1896
+ session.sessionId,
1897
+ session.title,
1898
+ session.workspacePath,
1899
+ session.status,
1900
+ session.startedAt,
1901
+ session.updatedAt,
1902
+ session.latestPrompt ?? null,
1903
+ session.isGlobal ? 1 : 0
1904
+ );
1905
+ });
1740
1906
  }
1741
1907
  listRuntimeSessions(limit = 100) {
1742
1908
  const rows = this.db.prepare(`
@@ -1754,33 +1920,35 @@ Original error: ${err.message}`
1754
1920
  }));
1755
1921
  }
1756
1922
  upsertRuntimeNode(node) {
1757
- this.db.prepare(`
1758
- INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1759
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1760
- ON CONFLICT(tier_id) DO UPDATE SET
1761
- session_id = excluded.session_id,
1762
- parent_id = excluded.parent_id,
1763
- role = excluded.role,
1764
- label = excluded.label,
1765
- status = excluded.status,
1766
- current_action = excluded.current_action,
1767
- progress_pct = excluded.progress_pct,
1768
- updated_at = excluded.updated_at,
1769
- workspace_path = excluded.workspace_path,
1770
- is_global = excluded.is_global
1771
- `).run(
1772
- node.tierId,
1773
- node.sessionId,
1774
- node.parentId ?? null,
1775
- node.role,
1776
- node.label,
1777
- node.status,
1778
- node.currentAction ?? null,
1779
- node.progressPct ?? null,
1780
- node.updatedAt,
1781
- node.workspacePath ?? null,
1782
- node.isGlobal ? 1 : 0
1783
- );
1923
+ this.enqueueWrite(() => {
1924
+ this.db.prepare(`
1925
+ INSERT INTO runtime_nodes (tier_id, session_id, parent_id, role, label, status, current_action, progress_pct, updated_at, workspace_path, is_global)
1926
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1927
+ ON CONFLICT(tier_id) DO UPDATE SET
1928
+ session_id = excluded.session_id,
1929
+ parent_id = excluded.parent_id,
1930
+ role = excluded.role,
1931
+ label = excluded.label,
1932
+ status = excluded.status,
1933
+ current_action = excluded.current_action,
1934
+ progress_pct = excluded.progress_pct,
1935
+ updated_at = excluded.updated_at,
1936
+ workspace_path = excluded.workspace_path,
1937
+ is_global = excluded.is_global
1938
+ `).run(
1939
+ node.tierId,
1940
+ node.sessionId,
1941
+ node.parentId ?? null,
1942
+ node.role,
1943
+ node.label,
1944
+ node.status,
1945
+ node.currentAction ?? null,
1946
+ node.progressPct ?? null,
1947
+ node.updatedAt,
1948
+ node.workspacePath ?? null,
1949
+ node.isGlobal ? 1 : 0
1950
+ );
1951
+ });
1784
1952
  }
1785
1953
  listRuntimeNodes(sessionId, limit = 500) {
1786
1954
  const rows = sessionId ? this.db.prepare(`
@@ -1803,30 +1971,32 @@ Original error: ${err.message}`
1803
1971
  }));
1804
1972
  }
1805
1973
  addRuntimeNodeLog(log) {
1806
- this.db.prepare(`
1807
- INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1808
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1809
- `).run(
1810
- log.id,
1811
- log.sessionId,
1812
- log.tierId,
1813
- log.role,
1814
- log.label,
1815
- log.status,
1816
- log.currentAction ?? null,
1817
- log.progressPct ?? null,
1818
- log.timestamp,
1819
- log.workspacePath ?? null,
1820
- log.isGlobal ? 1 : 0
1821
- );
1822
- this.db.prepare(`
1823
- DELETE FROM runtime_node_logs
1824
- WHERE id NOT IN (
1825
- SELECT id FROM runtime_node_logs
1826
- ORDER BY timestamp DESC
1827
- LIMIT 2000
1828
- )
1829
- `).run();
1974
+ this.enqueueWrite(() => {
1975
+ this.db.prepare(`
1976
+ INSERT INTO runtime_node_logs (id, session_id, tier_id, role, label, status, current_action, progress_pct, timestamp, workspace_path, is_global)
1977
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1978
+ `).run(
1979
+ log.id,
1980
+ log.sessionId,
1981
+ log.tierId,
1982
+ log.role,
1983
+ log.label,
1984
+ log.status,
1985
+ log.currentAction ?? null,
1986
+ log.progressPct ?? null,
1987
+ log.timestamp,
1988
+ log.workspacePath ?? null,
1989
+ log.isGlobal ? 1 : 0
1990
+ );
1991
+ this.db.prepare(`
1992
+ DELETE FROM runtime_node_logs
1993
+ WHERE id NOT IN (
1994
+ SELECT id FROM runtime_node_logs
1995
+ ORDER BY timestamp DESC
1996
+ LIMIT 2000
1997
+ )
1998
+ `).run();
1999
+ });
1830
2000
  }
1831
2001
  listRuntimeNodeLogs(sessionId, tierId, limit = 200) {
1832
2002
  let rows;
@@ -1864,19 +2034,21 @@ Original error: ${err.message}`
1864
2034
  }
1865
2035
  // ── Messages ──────────────────────────────────
1866
2036
  addMessage(message) {
1867
- this.db.prepare(`
1868
- INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
1869
- VALUES (?, ?, ?, ?, ?, ?, ?)
1870
- `).run(
1871
- message.id,
1872
- message.sessionId,
1873
- message.role,
1874
- typeof message.content === "string" ? message.content : JSON.stringify(message.content),
1875
- message.timestamp,
1876
- message.tokens ? JSON.stringify(message.tokens) : null,
1877
- message.agentMessages ? JSON.stringify(message.agentMessages) : null
1878
- );
1879
- this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
2037
+ this.enqueueWrite(() => {
2038
+ this.db.prepare(`
2039
+ INSERT INTO messages (id, session_id, role, content, timestamp, tokens, agent_messages)
2040
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2041
+ `).run(
2042
+ message.id,
2043
+ message.sessionId,
2044
+ message.role,
2045
+ typeof message.content === "string" ? message.content : JSON.stringify(message.content),
2046
+ message.timestamp,
2047
+ message.tokens ? JSON.stringify(message.tokens) : null,
2048
+ message.agentMessages ? JSON.stringify(message.agentMessages) : null
2049
+ );
2050
+ this.db.prepare("UPDATE sessions SET updated_at = ? WHERE id = ?").run(message.timestamp, message.sessionId);
2051
+ });
1880
2052
  }
1881
2053
  getSessionMessages(sessionId) {
1882
2054
  const rows = this.db.prepare("SELECT * FROM messages WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
@@ -1973,10 +2145,12 @@ Original error: ${err.message}`
1973
2145
  }
1974
2146
  // ── Audit Log ─────────────────────────────────
1975
2147
  addAuditEntry(entry) {
1976
- this.db.prepare(`
1977
- INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
1978
- VALUES (?, ?, ?, ?, ?, ?)
1979
- `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2148
+ this.enqueueWrite(() => {
2149
+ this.db.prepare(`
2150
+ INSERT INTO audit_log (id, session_id, timestamp, tier_id, action, details)
2151
+ VALUES (?, ?, ?, ?, ?, ?)
2152
+ `).run(entry.id, entry.sessionId, entry.timestamp, entry.tierId, entry.action, JSON.stringify(entry.details));
2153
+ });
1980
2154
  }
1981
2155
  getAuditLog(sessionId, limit = 100) {
1982
2156
  const rows = this.db.prepare("SELECT * FROM audit_log WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
@@ -1991,10 +2165,12 @@ Original error: ${err.message}`
1991
2165
  }
1992
2166
  // ── File Snapshots ────────────────────────────
1993
2167
  addFileSnapshot(sessionId, filePath, content) {
1994
- this.db.prepare(`
1995
- INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
1996
- VALUES (?, ?, ?, ?, ?)
1997
- `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2168
+ this.enqueueWrite(() => {
2169
+ this.db.prepare(`
2170
+ INSERT INTO file_snapshots (id, session_id, file_path, content, timestamp)
2171
+ VALUES (?, ?, ?, ?, ?)
2172
+ `).run(crypto.randomUUID(), sessionId, filePath, content, (/* @__PURE__ */ new Date()).toISOString());
2173
+ });
1998
2174
  }
1999
2175
  getLatestFileSnapshots(sessionId) {
2000
2176
  const rows = this.db.prepare(`
@@ -2298,12 +2474,24 @@ var McpServerConfigSchema = zod.z.object({
2298
2474
  args: zod.z.array(zod.z.string()).optional(),
2299
2475
  env: zod.z.record(zod.z.string()).optional()
2300
2476
  });
2477
+ var WebSearchConfigSchema = zod.z.object({
2478
+ /** Base URL of your SearXNG instance (e.g. http://localhost:8080) */
2479
+ searxngUrl: zod.z.string().optional(),
2480
+ /** Brave Search API key — get one at https://api.search.brave.com */
2481
+ braveApiKey: zod.z.string().optional(),
2482
+ /** Tavily API key — get one at https://tavily.com */
2483
+ tavilyApiKey: zod.z.string().optional(),
2484
+ /** Max results per search (default 5) */
2485
+ maxResults: zod.z.number().default(5)
2486
+ });
2301
2487
  var ToolsConfigSchema = zod.z.object({
2302
2488
  shellAllowlist: zod.z.array(zod.z.string()).default([]),
2303
2489
  shellBlocklist: zod.z.array(zod.z.string()).default(["rm -rf", "sudo rm", "format", "mkfs"]),
2304
2490
  requireApprovalFor: zod.z.array(zod.z.string()).default([]),
2305
2491
  browserEnabled: zod.z.boolean().default(false),
2306
- mcpServers: zod.z.array(McpServerConfigSchema).optional()
2492
+ mcpServers: zod.z.array(McpServerConfigSchema).optional(),
2493
+ /** Web search backends — at least one should be configured for best results */
2494
+ webSearch: WebSearchConfigSchema.optional()
2307
2495
  });
2308
2496
  var HookDefinitionSchema = zod.z.object({
2309
2497
  command: zod.z.string(),
@@ -2408,7 +2596,7 @@ var ConfigManager = class {
2408
2596
  globalDir;
2409
2597
  constructor(workspacePath = process.cwd()) {
2410
2598
  this.workspacePath = workspacePath;
2411
- this.globalDir = path17__default.default.join(os__default.default.homedir(), GLOBAL_CONFIG_DIR);
2599
+ this.globalDir = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR);
2412
2600
  }
2413
2601
  async load() {
2414
2602
  this.config = await this.loadConfig();
@@ -2476,6 +2664,7 @@ var ConfigManager = class {
2476
2664
  }
2477
2665
  }
2478
2666
  async injectEnvKeys() {
2667
+ const isFirstRun = this.config.providers.length === 0;
2479
2668
  const envProviders = [
2480
2669
  { env: "ANTHROPIC_API_KEY", type: "anthropic" },
2481
2670
  { env: "OPENAI_API_KEY", type: "openai" },
@@ -2486,10 +2675,13 @@ var ConfigManager = class {
2486
2675
  const key = process.env[env];
2487
2676
  if (!key) continue;
2488
2677
  const existing = this.config.providers.find((p) => p.type === type);
2489
- if (!existing) this.config.providers.push({ type, apiKey: key });
2490
- else if (!existing.apiKey) existing.apiKey = key;
2678
+ if (!existing && isFirstRun) {
2679
+ this.config.providers.push({ type, apiKey: key });
2680
+ } else if (existing && !existing.apiKey) {
2681
+ existing.apiKey = key;
2682
+ }
2491
2683
  }
2492
- if (!this.config.providers.find((p) => p.type === "ollama")) {
2684
+ if (isFirstRun && !this.config.providers.find((p) => p.type === "ollama")) {
2493
2685
  this.config.providers.push({ type: "ollama" });
2494
2686
  }
2495
2687
  }
@@ -3407,6 +3599,16 @@ var CascadeRouter = class _CascadeRouter extends EventEmitter__default.default {
3407
3599
  return /rate.?limit|429|too.?many.?requests|quota/i.test(msg);
3408
3600
  }
3409
3601
  };
3602
+
3603
+ // src/utils/retry.ts
3604
+ var CascadeCancelledError = class extends Error {
3605
+ constructor(reason) {
3606
+ super(reason ?? "Run was cancelled via AbortSignal");
3607
+ this.name = "CascadeCancelledError";
3608
+ }
3609
+ };
3610
+
3611
+ // src/core/tiers/base.ts
3410
3612
  var BaseTier = class extends EventEmitter__default.default {
3411
3613
  id;
3412
3614
  role;
@@ -3416,6 +3618,8 @@ var BaseTier = class extends EventEmitter__default.default {
3416
3618
  label;
3417
3619
  systemPromptOverride = "";
3418
3620
  hierarchyContext = "";
3621
+ /** Propagated AbortSignal — set by the tier's `execute()` before work begins. */
3622
+ signal;
3419
3623
  constructor(role, id, parentId) {
3420
3624
  super();
3421
3625
  this.role = role;
@@ -3478,6 +3682,18 @@ var BaseTier = class extends EventEmitter__default.default {
3478
3682
  log(message, data) {
3479
3683
  this.emit("log", { tierId: this.id, role: this.role, message, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
3480
3684
  }
3685
+ /**
3686
+ * Throws `CascadeCancelledError` if the run's `AbortSignal` has fired.
3687
+ * Call this at safe checkpoints (before LLM calls, between T3 dispatches)
3688
+ * to provide a fast, clean cancellation path.
3689
+ */
3690
+ throwIfCancelled() {
3691
+ if (this.signal?.aborted) {
3692
+ throw new CascadeCancelledError(
3693
+ typeof this.signal.reason === "string" ? this.signal.reason : "Run cancelled by caller"
3694
+ );
3695
+ }
3696
+ }
3481
3697
  };
3482
3698
 
3483
3699
  // src/core/context/manager.ts
@@ -3679,6 +3895,7 @@ Rules:
3679
3895
  - Execute the subtask completely \u2014 do not stop partway through.
3680
3896
  - Use tools when needed. Ask for approval only when the tool registry requires it.
3681
3897
  - 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.
3898
+ - Use the "web_search" tool to find current information, documentation, news, or general web data.
3682
3899
  - Use the "pdf_create" tool for PDF requests.
3683
3900
  - Use the "run_code" tool for any file types (Excel, Zip, csv, etc.) or complex processing not covered by other tools. Always cleanup after code execution.
3684
3901
  - If you are not making meaningful progress, stop and escalate rather than looping or padding the response.
@@ -3722,7 +3939,8 @@ var T3Worker = class extends BaseTier {
3722
3939
  this.store = store;
3723
3940
  this.audit = new AuditLogger(store, sessionId);
3724
3941
  }
3725
- async execute(assignment, taskId) {
3942
+ async execute(assignment, taskId, signal) {
3943
+ this.signal = signal;
3726
3944
  this.assignment = assignment;
3727
3945
  this.taskId = taskId;
3728
3946
  this.setLabel(assignment.subtaskTitle);
@@ -3872,6 +4090,7 @@ Now execute your subtask using this context where relevant.`
3872
4090
  tools = [...tools];
3873
4091
  while (iterations < MAX_ITERATIONS) {
3874
4092
  iterations++;
4093
+ this.throwIfCancelled();
3875
4094
  const options = {
3876
4095
  messages: this.context.getMessages(),
3877
4096
  systemPrompt: this.systemPromptOverride + systemPrompt + (this.hierarchyContext ? `
@@ -3892,21 +4111,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
3892
4111
  if (requiresArtifact) {
3893
4112
  stalledArtifactIterations += 1;
3894
4113
  if (stalledArtifactIterations >= 2) {
3895
- if (this.toolCreator && stalledArtifactIterations === 2) {
3896
- const toolName = await this.toolCreator.createTool(
3897
- `Help complete: ${this.assignment?.subtaskTitle ?? "unknown task"}`,
3898
- this.assignment?.description ?? ""
3899
- );
3900
- if (toolName) {
3901
- tools = this.toolRegistry.getToolDefinitions();
3902
- this.sendStatusUpdate({
3903
- progressPct: 50,
3904
- currentAction: `Dynamic tool created: ${toolName}`,
3905
- status: "IN_PROGRESS"
3906
- });
3907
- this.emit("tool:created", { tierId: this.id, toolName });
3908
- continue;
3909
- }
4114
+ if (stalledArtifactIterations === 2) {
4115
+ throw new Error(`Worker stalled waiting for artifact creation. Requesting dynamic tool generation from T2 Manager for: ${this.assignment?.subtaskTitle ?? "unknown task"}`);
3910
4116
  }
3911
4117
  throw new Error("Artifact-producing task stalled without creating or verifying the required files");
3912
4118
  }
@@ -4066,6 +4272,9 @@ ${assignment.expectedOutput}`;
4066
4272
  const artifactPaths = this.extractArtifactPaths(assignment);
4067
4273
  if (!artifactPaths.length) return { ok: true, issues: [] };
4068
4274
  const issues = [];
4275
+ const { exec: exec4 } = await import('child_process');
4276
+ const { promisify: promisify3 } = await import('util');
4277
+ const execAsync3 = promisify3(exec4);
4069
4278
  for (const artifactPath of artifactPaths) {
4070
4279
  const absolutePath = path17__default.default.resolve(process.cwd(), artifactPath);
4071
4280
  try {
@@ -4082,9 +4291,27 @@ ${assignment.expectedOutput}`;
4082
4291
  const content = await fs7__default.default.readFile(absolutePath, "utf-8");
4083
4292
  if (!content.trim()) {
4084
4293
  issues.push(`Artifact content is empty: ${artifactPath}`);
4294
+ continue;
4085
4295
  }
4086
4296
  } else if (stat.size < 100) {
4087
4297
  issues.push(`PDF artifact looks too small to be valid: ${artifactPath}`);
4298
+ continue;
4299
+ }
4300
+ const ext = path17__default.default.extname(absolutePath).toLowerCase();
4301
+ try {
4302
+ if (ext === ".ts" || ext === ".tsx") {
4303
+ await execAsync3(`npx tsc --noEmit ${absolutePath}`, { timeout: 1e4 });
4304
+ } else if (ext === ".js" || ext === ".jsx") {
4305
+ await execAsync3(`node --check ${absolutePath}`, { timeout: 1e4 });
4306
+ } else if (ext === ".py") {
4307
+ await execAsync3(`python -m py_compile ${absolutePath}`, { timeout: 1e4 });
4308
+ }
4309
+ } catch (err) {
4310
+ const stderr = err?.stderr || String(err);
4311
+ const stdout = err?.stdout || "";
4312
+ issues.push(`Semantic error in ${artifactPath}:
4313
+ ${stderr}
4314
+ ${stdout}`);
4088
4315
  }
4089
4316
  } catch {
4090
4317
  issues.push(`Required artifact was not created: ${artifactPath}`);
@@ -4481,7 +4708,8 @@ var T2Manager = class extends BaseTier {
4481
4708
  });
4482
4709
  this.emit("peer-sync-received", { fromId, content });
4483
4710
  }
4484
- async execute(assignment, taskId) {
4711
+ async execute(assignment, taskId, signal) {
4712
+ this.signal = signal;
4485
4713
  this.assignment = assignment;
4486
4714
  this.taskId = taskId;
4487
4715
  this.setLabel(assignment.sectionTitle);
@@ -4493,12 +4721,14 @@ var T2Manager = class extends BaseTier {
4493
4721
  });
4494
4722
  this.log(`T2 managing section: ${assignment.sectionTitle}`);
4495
4723
  try {
4724
+ this.throwIfCancelled();
4496
4725
  const subtasks = assignment.t3Subtasks.length > 0 ? assignment.t3Subtasks : await this.decomposeSection(assignment);
4497
4726
  this.sendStatusUpdate({
4498
4727
  progressPct: 20,
4499
4728
  currentAction: `Dispatching ${subtasks.length} T3 workers`,
4500
4729
  status: "IN_PROGRESS"
4501
4730
  });
4731
+ this.throwIfCancelled();
4502
4732
  const t3Results = await this.executeSubtasks(subtasks, taskId);
4503
4733
  this.sendStatusUpdate({
4504
4734
  progressPct: 90,
@@ -4665,11 +4895,12 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4665
4895
  ).join(", ")}`,
4666
4896
  status: "IN_PROGRESS"
4667
4897
  });
4898
+ this.throwIfCancelled();
4668
4899
  const waveResults = await Promise.allSettled(
4669
4900
  runnableIds.map(async (id) => {
4670
4901
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4671
4902
  const worker = workerMap.get(id);
4672
- const result = await worker.execute(assignment, taskId);
4903
+ const result = await worker.execute(assignment, taskId, this.signal);
4673
4904
  resultMap.set(id, result);
4674
4905
  return result;
4675
4906
  })
@@ -4683,6 +4914,60 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4683
4914
  const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4684
4915
  const retried = await this.retryT3(assignment, taskId);
4685
4916
  resultMap.set(id, retried);
4917
+ } else if (r.status === "fulfilled" && r.value.status === "ESCALATED" && r.value.issues.some((i2) => i2.includes("dynamic tool generation"))) {
4918
+ const assignment = sanitizedAssignments.find((a) => a.subtaskId === id);
4919
+ if (this.toolCreator) {
4920
+ this.log(`T3 escalated for tool. T2 spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`);
4921
+ this.sendStatusUpdate({
4922
+ progressPct: 50,
4923
+ currentAction: `Spawning Tool-Builder T3 for: ${assignment.subtaskTitle}`,
4924
+ status: "IN_PROGRESS"
4925
+ });
4926
+ const toolName = await this.toolCreator.createTool(
4927
+ `Help complete: ${assignment.subtaskTitle}`,
4928
+ assignment.description
4929
+ );
4930
+ if (toolName) {
4931
+ this.log(`T2 verifying new tool: ${toolName}`);
4932
+ this.sendStatusUpdate({
4933
+ progressPct: 60,
4934
+ currentAction: `T2 Verifying new tool: ${toolName}`,
4935
+ status: "IN_PROGRESS"
4936
+ });
4937
+ try {
4938
+ const verifyResult = await this.router.generate("T2", {
4939
+ 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".` }],
4940
+ systemPrompt: this.systemPromptOverride + "You are T2 Manager verifying a dynamic tool.",
4941
+ maxTokens: 50
4942
+ });
4943
+ if (!verifyResult.content.toUpperCase().includes("REJECTED")) {
4944
+ this.log(`T2 verification passed for ${toolName}. Restarting original T3.`);
4945
+ const retried = await this.retryT3({
4946
+ ...assignment,
4947
+ description: `${assignment.description}
4948
+
4949
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built and verified for you. Use it to complete your task.`
4950
+ }, taskId);
4951
+ resultMap.set(id, retried);
4952
+ } else {
4953
+ this.log(`T2 rejected the dynamic tool: ${toolName}`);
4954
+ resultMap.set(id, r.value);
4955
+ }
4956
+ } catch {
4957
+ const retried = await this.retryT3({
4958
+ ...assignment,
4959
+ description: `${assignment.description}
4960
+
4961
+ [SYSTEM NOTIFICATION]: A new dynamic tool "${toolName}" has been built for you. Use it to complete your task.`
4962
+ }, taskId);
4963
+ resultMap.set(id, retried);
4964
+ }
4965
+ } else {
4966
+ resultMap.set(id, r.value);
4967
+ }
4968
+ } else {
4969
+ resultMap.set(id, r.value);
4970
+ }
4686
4971
  }
4687
4972
  for (const dependent of adj.get(id) ?? []) {
4688
4973
  inDegree.set(dependent, Math.max(0, (inDegree.get(dependent) ?? 0) - 1));
@@ -4746,7 +5031,8 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4746
5031
  }));
4747
5032
  return worker.execute(
4748
5033
  { ...assignment, description: `[RETRY] ${assignment.description}` },
4749
- taskId
5034
+ taskId,
5035
+ this.signal
4750
5036
  );
4751
5037
  }
4752
5038
  publishSectionOutput(result) {
@@ -4760,29 +5046,51 @@ HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4760
5046
  async aggregateResults(assignment, results) {
4761
5047
  const completed = results.filter((r) => r.status === "COMPLETED");
4762
5048
  if (!completed.length) return `Section ${assignment.sectionTitle} failed \u2014 no T3 workers completed.`;
4763
- const outputs = completed.map((r, i) => `[T3-${i + 1}]: ${r.output}`).join("\n\n");
4764
5049
  const peerOutputs = this.peerSyncBuffer.filter((p) => p.content?.type === "T2_SECTION_OUTPUT").map((p) => `[Peer ${p.fromId} Output]: ${p.content.output}`).join("\n\n");
4765
- const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences:
4766
-
4767
- ${outputs}
4768
- ${peerOutputs ? `
5050
+ const peerContext = peerOutputs ? `
4769
5051
 
4770
5052
  Context from sibling T2 completed sections (use this to ensure your summary aligns with the overall state):
4771
- ${peerOutputs}` : ""}`;
4772
- const messages = [{ role: "user", content: prompt }];
4773
- try {
4774
- const result = await this.router.generate("T2", {
4775
- messages,
4776
- systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
5053
+ ${peerOutputs}` : "";
5054
+ const MAX_CHUNK_LENGTH = 15e3;
5055
+ let currentSummary = "";
5056
+ let i = 0;
5057
+ while (i < completed.length) {
5058
+ let chunkText = "";
5059
+ let chunkEnd = i;
5060
+ while (chunkEnd < completed.length) {
5061
+ const nextOutput = `[T3-${chunkEnd + 1}]: ${completed[chunkEnd].output}
5062
+
5063
+ `;
5064
+ if (chunkText.length + nextOutput.length > MAX_CHUNK_LENGTH && chunkEnd > i) {
5065
+ break;
5066
+ }
5067
+ chunkText += nextOutput;
5068
+ chunkEnd++;
5069
+ }
5070
+ i = chunkEnd;
5071
+ const prompt = `Summarize these T3 worker outputs for section "${assignment.sectionTitle}" in 2-3 sentences.
5072
+ ${currentSummary ? `
5073
+ PREVIOUS SUMMARY SO FAR:
5074
+ ${currentSummary}
5075
+
5076
+ NEW OUTPUTS TO INTEGRATE:
5077
+ ` : "\nOUTPUTS:\n"}${chunkText}${peerContext}`;
5078
+ const messages = [{ role: "user", content: prompt }];
5079
+ try {
5080
+ const result = await this.router.generate("T2", {
5081
+ messages,
5082
+ systemPrompt: this.systemPromptOverride + "You are a T2 Manager. Summarize the work of your T3 workers succinctly." + (this.hierarchyContext ? `
4777
5083
 
4778
5084
  HIERARCHY CONTEXT: ${this.hierarchyContext}` : ""),
4779
- maxTokens: 300
4780
- });
4781
- return result.content;
4782
- } catch (err) {
4783
- this.log(`aggregateResults: LLM summarization failed \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
4784
- return outputs;
5085
+ maxTokens: 500
5086
+ });
5087
+ currentSummary = result.content;
5088
+ } catch (err) {
5089
+ this.log(`aggregateResults: LLM summarization failed at chunk \u2014 returning raw T3 outputs. Error: ${err instanceof Error ? err.message : String(err)}`);
5090
+ return currentSummary + "\n\n" + chunkText;
5091
+ }
4785
5092
  }
5093
+ return currentSummary;
4786
5094
  }
4787
5095
  determineStatus(results) {
4788
5096
  if (results.every((r) => r.status === "COMPLETED")) return "COMPLETED";
@@ -4910,10 +5218,10 @@ Rules:
4910
5218
  - If the user asks for Excel/Zip/complex processing, use "run_code" with Python or Node.js
4911
5219
  - Ensure every plan includes explicit creation and verification steps for requested artifacts
4912
5220
 
4913
- EXECUTION MODE GUIDANCE:
4914
- - Use "parallel" for sections that are independent (e.g. writing different files, researching different topics).
4915
- - Use "sequential" ONLY when a later section strictly depends on the output of an earlier one (e.g. write code \u2192 then test it).
4916
- - Prefer parallel execution: it is significantly faster and reduces total wall-clock time.
5221
+ DEPENDENCY GUIDANCE:
5222
+ - Leave "dependsOn" empty [] for sections that are independent (e.g. writing different files, researching different topics).
5223
+ - 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).
5224
+ - Prefer empty dependencies (parallel execution): it is significantly faster and reduces total wall-clock time.
4917
5225
  - Within a sequential section, mark T3 subtasks with "dependsOn" only when they truly block each other.
4918
5226
 
4919
5227
  QUALITY RULES:
@@ -4952,7 +5260,8 @@ var T1Administrator = class extends BaseTier {
4952
5260
  setToolCreator(creator) {
4953
5261
  this.toolCreator = creator;
4954
5262
  }
4955
- async execute(userPrompt, images, systemContext) {
5263
+ async execute(userPrompt, images, systemContext, signal) {
5264
+ this.signal = signal;
4956
5265
  this.taskId = crypto.randomUUID();
4957
5266
  this.setLabel("Administrator");
4958
5267
  this.setStatus("ACTIVE");
@@ -4963,10 +5272,12 @@ var T1Administrator = class extends BaseTier {
4963
5272
  status: "IN_PROGRESS"
4964
5273
  });
4965
5274
  this.log(`T1 received task: ${userPrompt.slice(0, 100)}...`);
5275
+ this.throwIfCancelled();
4966
5276
  let enrichedPrompt = userPrompt;
4967
5277
  if (images?.length) {
4968
5278
  enrichedPrompt = await this.analyzeImages(userPrompt, images);
4969
5279
  }
5280
+ this.throwIfCancelled();
4970
5281
  const plan = await this.decomposeTask(enrichedPrompt, systemContext);
4971
5282
  this.sendStatusUpdate({
4972
5283
  progressPct: 10,
@@ -4974,21 +5285,83 @@ var T1Administrator = class extends BaseTier {
4974
5285
  status: "IN_PROGRESS"
4975
5286
  });
4976
5287
  this.emit("plan", { taskId: this.taskId, plan });
4977
- const t2Results = await this.dispatchT2Managers(plan.sections);
5288
+ this.throwIfCancelled();
5289
+ let allT2Results = await this.dispatchT2Managers(plan.sections);
5290
+ let pass = 1;
5291
+ const MAX_REPLAN_PASSES = 2;
5292
+ while (pass <= MAX_REPLAN_PASSES) {
5293
+ const reviewResult = await this.reviewT2Outputs(enrichedPrompt, plan, allT2Results);
5294
+ if (reviewResult.approved) {
5295
+ this.log("T1 Review passed.");
5296
+ break;
5297
+ }
5298
+ this.log(`T1 Review rejected outputs. Replanning (Pass ${pass}). Reason: ${reviewResult.reason}`);
5299
+ this.sendStatusUpdate({
5300
+ progressPct: 80 + pass * 5,
5301
+ currentAction: `Review failed: ${reviewResult.reason}. Replanning...`,
5302
+ status: "IN_PROGRESS"
5303
+ });
5304
+ const correctionPlan = await this.decomposeTask(`The previous execution plan failed to fully satisfy the original goal or encountered errors.
5305
+ Review reason: ${reviewResult.reason}
5306
+
5307
+ Original goal: ${enrichedPrompt}
5308
+
5309
+ Create a CORRECTION PLAN that contains only the new sections needed to fix the issues. Do not repeat successful sections.`);
5310
+ const correctionResults = await this.dispatchT2Managers(correctionPlan.sections);
5311
+ allT2Results = [...allT2Results, ...correctionResults];
5312
+ pass++;
5313
+ }
4978
5314
  this.sendStatusUpdate({
4979
5315
  progressPct: 95,
4980
5316
  currentAction: "Compiling final output",
4981
5317
  status: "IN_PROGRESS"
4982
5318
  });
4983
- const output = await this.compileFinalOutput(userPrompt, plan, t2Results);
5319
+ const output = await this.compileFinalOutput(userPrompt, plan, allT2Results);
4984
5320
  this.setStatus("COMPLETED");
4985
5321
  this.sendStatusUpdate({ progressPct: 100, currentAction: "Task complete", status: "IN_PROGRESS" });
4986
- return { output, t2Results, taskId: this.taskId, complexity: plan.complexity };
5322
+ return { output, t2Results: allT2Results, taskId: this.taskId, complexity: plan.complexity };
4987
5323
  }
4988
5324
  getEscalations() {
4989
5325
  return [...this.escalations];
4990
5326
  }
4991
5327
  // ── Private ──────────────────────────────────
5328
+ async reviewT2Outputs(originalPrompt, plan, t2Results) {
5329
+ const failedSections = t2Results.filter((r) => r.status === "FAILED");
5330
+ if (failedSections.length > 0) {
5331
+ return {
5332
+ approved: false,
5333
+ reason: `Some T2 managers failed entirely: ${failedSections.map((s) => s.sectionTitle).join(", ")}. Errors: ${failedSections.flatMap((s) => s.issues).join("; ")}`
5334
+ };
5335
+ }
5336
+ const sectionsText = t2Results.map((r) => `**${r.sectionTitle}**
5337
+ ${r.sectionSummary}`).join("\n\n");
5338
+ const prompt = `You are a strict QA Reviewer for the Cascade AI system.
5339
+ Review the following execution outputs against the original user prompt.
5340
+
5341
+ Original Request: ${originalPrompt}
5342
+
5343
+ T2 Manager Summaries:
5344
+ ${sectionsText}
5345
+
5346
+ Does the current state of the workspace and the outputs fully satisfy the user's request?
5347
+ If yes, reply with exactly: "APPROVED".
5348
+ If no, reply with "REJECTED: [Detailed reason explaining exactly what is missing or incorrect]".`;
5349
+ try {
5350
+ const result = await this.router.generate("T1", {
5351
+ messages: [{ role: "user", content: prompt }],
5352
+ systemPrompt: this.systemPromptOverride + "You are a QA Reviewer.",
5353
+ maxTokens: 500,
5354
+ temperature: 0
5355
+ });
5356
+ const response = result.content.trim();
5357
+ if (response.toUpperCase().startsWith("APPROVED")) {
5358
+ return { approved: true };
5359
+ }
5360
+ return { approved: false, reason: response.replace(/^REJECTED:\s*/i, "") };
5361
+ } catch {
5362
+ return { approved: true };
5363
+ }
5364
+ }
4992
5365
  async analyzeImages(prompt, images) {
4993
5366
  const visionModel = this.router.getModelForTier("T1");
4994
5367
  if (!visionModel?.isVisionCapable) return prompt;
@@ -5017,29 +5390,35 @@ ${systemContext}` : "";
5017
5390
  Example: if asked to create files "inside python_exclusive", every subtask that
5018
5391
  creates a file must use "python_exclusive/filename.ext" as the path.
5019
5392
 
5020
- Return JSON where subtasks can declare dependencies:
5393
+ Return JSON where SECTIONS can declare dependencies on other SECTIONS:
5021
5394
  {
5022
5395
  "sections": [{
5396
+ "sectionId": "s1",
5397
+ "sectionTitle": "Setup Project",
5398
+ "description": "Initialize the project",
5399
+ "expectedOutput": "Basic structure created",
5400
+ "constraints": [],
5401
+ "dependsOn": [], // \u2190 empty = runs immediately
5023
5402
  "t3Subtasks": [{
5024
5403
  "subtaskId": "t1",
5025
- "subtaskTitle": "Generate Source Code",
5026
- "dependsOn": [], // \u2190 empty = runs immediately
5027
- "executionMode": "parallel"
5028
- }, {
5029
- "subtaskId": "t2",
5030
- "subtaskTitle": "Save Code to File",
5031
- "dependsOn": ["t1"], // \u2190 waits for t1 to complete first
5032
- "executionMode": "parallel"
5033
- }, {
5034
- "subtaskId": "t3",
5035
- "subtaskTitle": "Execute and Verify",
5036
- "dependsOn": ["t2"], // \u2190 waits for t2
5037
- "executionMode": "parallel"
5404
+ "subtaskTitle": "Init NPM",
5405
+ "description": "Run npm init",
5406
+ "expectedOutput": "package.json created",
5407
+ "constraints": [],
5408
+ "dependsOn": []
5038
5409
  }]
5410
+ }, {
5411
+ "sectionId": "s2",
5412
+ "sectionTitle": "Write Tests",
5413
+ "description": "Write tests for the project",
5414
+ "expectedOutput": "Tests passing",
5415
+ "constraints": [],
5416
+ "dependsOn": ["s1"], // \u2190 waits for section s1 to complete first
5417
+ "t3Subtasks": [...]
5039
5418
  }]
5040
5419
  }
5041
- Use dependsOn when a subtask needs the output of a previous one.
5042
- Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5420
+ Use dependsOn at the SECTION level when a whole T2 Manager needs the output of a previous T2 Manager.
5421
+ Leave dependsOn empty for sections that can run immediately in parallel.`;
5043
5422
  const messages = [{ role: "user", content: decompositionPrompt }];
5044
5423
  const result = await this.router.generate("T1", {
5045
5424
  messages,
@@ -5167,92 +5546,127 @@ Leave dependsOn empty for subtasks that can run immediately in parallel.`;
5167
5546
  ].filter(Boolean).join(" ");
5168
5547
  m.setHierarchyContext(context);
5169
5548
  });
5170
- if (overlapSections.size > 0 && !sections.some((s) => s.executionMode === "sequential")) {
5171
- this.log("Overlap detected \u2014 switching to sequential execution for conflicting sections");
5172
- for (const section of sections) {
5173
- if (overlapSections.has(section.sectionId)) {
5174
- section.executionMode = "sequential";
5549
+ if (overlapSections.size > 0) {
5550
+ this.log("Overlap detected \u2014 adding sequential dependencies for conflicting sections to prevent race conditions");
5551
+ const overlapArray = Array.from(overlapSections);
5552
+ for (let i = 1; i < overlapArray.length; i++) {
5553
+ const section = sections.find((s) => s.sectionId === overlapArray[i]);
5554
+ if (section) {
5555
+ section.dependsOn = [...section.dependsOn || [], overlapArray[i - 1]];
5175
5556
  }
5176
5557
  }
5177
5558
  }
5178
- const pct = (i) => 10 + Math.floor(i / sections.length * 85);
5179
- const isSequential = sections.some((s) => s.executionMode === "sequential");
5180
5559
  const t2Results = [];
5181
5560
  try {
5182
- if (isSequential) {
5183
- this.log("Dispatching T2 managers sequentially");
5184
- for (let i = 0; i < managers.length; i++) {
5185
- const m = managers[i];
5186
- this.sendStatusUpdate({
5187
- progressPct: pct(i),
5188
- currentAction: `T2 working on: ${sections[i].sectionTitle} (Sequential)`,
5189
- status: "IN_PROGRESS"
5190
- });
5191
- try {
5192
- const result = await m.execute(sections[i], this.taskId);
5193
- t2Results.push(result);
5194
- m.shareCompletedOutput(sections[i].sectionId, result.sectionSummary);
5195
- if (result.status === "ESCALATED") {
5196
- this.escalations.push({
5197
- raisedBy: `T2_${sections[i].sectionId}`,
5198
- sectionId: sections[i].sectionId,
5199
- attempted: result.issues,
5200
- blocker: result.issues.join("; "),
5201
- needs: "Human review required"
5202
- });
5203
- }
5204
- } catch (err) {
5205
- t2Results.push({
5206
- sectionId: sections[i].sectionId,
5207
- sectionTitle: sections[i].sectionTitle,
5208
- status: "FAILED",
5209
- t3Results: [],
5210
- sectionSummary: "",
5211
- issues: [err instanceof Error ? err.message : String(err)]
5212
- });
5561
+ t2Results.push(...await this.runT2sWithDependencies(sections, managers, this.taskId));
5562
+ } finally {
5563
+ cleanup();
5564
+ }
5565
+ return t2Results;
5566
+ }
5567
+ /**
5568
+ * Runs T2 managers respecting dependsOn declarations using Kahn's algorithm.
5569
+ */
5570
+ async runT2sWithDependencies(sections, managers, taskId) {
5571
+ const adj = /* @__PURE__ */ new Map();
5572
+ const inDegree = /* @__PURE__ */ new Map();
5573
+ const resultMap = /* @__PURE__ */ new Map();
5574
+ const allKeys = new Set(sections.map((s) => s.sectionId));
5575
+ for (const s of sections) {
5576
+ if (!adj.has(s.sectionId)) adj.set(s.sectionId, /* @__PURE__ */ new Set());
5577
+ inDegree.set(s.sectionId, 0);
5578
+ s.dependsOn = (s.dependsOn ?? []).filter((d) => allKeys.has(d));
5579
+ }
5580
+ for (const s of sections) {
5581
+ for (const dep of s.dependsOn ?? []) {
5582
+ adj.get(dep).add(s.sectionId);
5583
+ inDegree.set(s.sectionId, (inDegree.get(s.sectionId) ?? 0) + 1);
5584
+ }
5585
+ }
5586
+ const queue = [];
5587
+ const degree = new Map(inDegree);
5588
+ for (const [id, deg] of degree.entries()) if (deg === 0) queue.push(id);
5589
+ const visited = /* @__PURE__ */ new Set();
5590
+ while (queue.length > 0) {
5591
+ const u = queue.shift();
5592
+ visited.add(u);
5593
+ for (const v of adj.get(u) ?? /* @__PURE__ */ new Set()) {
5594
+ const newDeg = (degree.get(v) ?? 1) - 1;
5595
+ degree.set(v, newDeg);
5596
+ if (newDeg === 0) queue.push(v);
5597
+ }
5598
+ }
5599
+ const cycleNodes = [...inDegree.keys()].filter((id) => !visited.has(id));
5600
+ if (cycleNodes.length > 0) {
5601
+ this.log(`\u26A0 Circular dependency detected among sections: [${cycleNodes.join(", ")}]. Breaking cycles.`);
5602
+ for (const s of sections) {
5603
+ if (cycleNodes.includes(s.sectionId)) {
5604
+ const safeDeps = (s.dependsOn ?? []).filter((d) => !cycleNodes.includes(d));
5605
+ for (const removed of (s.dependsOn ?? []).filter((d) => cycleNodes.includes(d))) {
5606
+ inDegree.set(s.sectionId, Math.max(0, (inDegree.get(s.sectionId) ?? 1) - 1));
5607
+ adj.get(removed)?.delete(s.sectionId);
5213
5608
  }
5609
+ s.dependsOn = safeDeps;
5214
5610
  }
5215
- } else {
5216
- const results = await Promise.allSettled(
5217
- managers.map((m, i) => {
5218
- this.sendStatusUpdate({
5219
- progressPct: pct(i),
5220
- currentAction: `T2 working on: ${sections[i].sectionTitle}`,
5221
- status: "IN_PROGRESS"
5222
- });
5223
- return m.execute(sections[i], this.taskId);
5224
- })
5225
- );
5226
- for (let i = 0; i < results.length; i++) {
5227
- const r = results[i];
5228
- if (r.status === "fulfilled") {
5229
- t2Results.push(r.value);
5230
- managers[i].shareCompletedOutput(sections[i].sectionId, r.value.sectionSummary);
5231
- if (r.value.status === "ESCALATED") {
5232
- this.escalations.push({
5233
- raisedBy: `T2_${sections[i].sectionId}`,
5234
- sectionId: sections[i].sectionId,
5235
- attempted: r.value.issues,
5236
- blocker: r.value.issues.join("; "),
5237
- needs: "Human review required"
5238
- });
5239
- }
5240
- } else {
5241
- t2Results.push({
5242
- sectionId: sections[i].sectionId,
5243
- sectionTitle: sections[i].sectionTitle,
5244
- status: "FAILED",
5245
- t3Results: [],
5246
- sectionSummary: "",
5247
- issues: [r.reason instanceof Error ? r.reason.message : String(r.reason)]
5611
+ }
5612
+ }
5613
+ const totalSections = sections.length;
5614
+ let completedSections = 0;
5615
+ const executeWave = async () => {
5616
+ const readyIds = [];
5617
+ for (const [id, deg] of inDegree.entries()) {
5618
+ if (deg === 0 && !resultMap.has(id)) {
5619
+ readyIds.push(id);
5620
+ }
5621
+ }
5622
+ if (readyIds.length === 0) return;
5623
+ await Promise.all(readyIds.map(async (id) => {
5624
+ resultMap.set(id, null);
5625
+ const index = sections.findIndex((s) => s.sectionId === id);
5626
+ const section = sections[index];
5627
+ const manager = managers[index];
5628
+ const progressPct = 10 + Math.floor(completedSections / totalSections * 85);
5629
+ this.sendStatusUpdate({
5630
+ progressPct,
5631
+ currentAction: `T2 working on: ${section.sectionTitle}`,
5632
+ status: "IN_PROGRESS"
5633
+ });
5634
+ this.throwIfCancelled();
5635
+ let result;
5636
+ try {
5637
+ result = await manager.execute(section, taskId, this.signal);
5638
+ manager.shareCompletedOutput(section.sectionId, result.sectionSummary);
5639
+ if (result.status === "ESCALATED") {
5640
+ this.escalations.push({
5641
+ raisedBy: `T2_${section.sectionId}`,
5642
+ sectionId: section.sectionId,
5643
+ attempted: result.issues,
5644
+ blocker: result.issues.join("; "),
5645
+ needs: "Human review required"
5248
5646
  });
5249
5647
  }
5648
+ } catch (err) {
5649
+ result = {
5650
+ sectionId: section.sectionId,
5651
+ sectionTitle: section.sectionTitle,
5652
+ status: "FAILED",
5653
+ t3Results: [],
5654
+ sectionSummary: "",
5655
+ issues: [err instanceof Error ? err.message : String(err)]
5656
+ };
5657
+ }
5658
+ resultMap.set(id, result);
5659
+ completedSections++;
5660
+ for (const dependentId of adj.get(id) ?? /* @__PURE__ */ new Set()) {
5661
+ inDegree.set(dependentId, Math.max(0, (inDegree.get(dependentId) ?? 1) - 1));
5250
5662
  }
5663
+ }));
5664
+ if (Array.from(inDegree.values()).some((deg) => deg === 0) && resultMap.size < totalSections) {
5665
+ await executeWave();
5251
5666
  }
5252
- } finally {
5253
- cleanup();
5254
- }
5255
- return t2Results;
5667
+ };
5668
+ await executeWave();
5669
+ return sections.map((s) => resultMap.get(s.sectionId)).filter(Boolean);
5256
5670
  }
5257
5671
  async compileFinalOutput(originalPrompt, plan, t2Results) {
5258
5672
  const completedSections = t2Results.filter((r) => r.status !== "FAILED");
@@ -5702,13 +6116,47 @@ var GitHubTool = class extends BaseTool {
5702
6116
  }
5703
6117
  async execute(input, _options) {
5704
6118
  const platform = input["platform"] ?? "github";
5705
- const token = input["token"] ?? process.env["GITHUB_TOKEN"] ?? process.env["GITLAB_TOKEN"] ?? "";
5706
6119
  const operation = input["operation"];
5707
6120
  const repo = input["repo"];
5708
- if (platform === "github") {
5709
- return this.executeGitHub(operation, repo, token, input);
6121
+ let token = input["token"];
6122
+ if (!token) {
6123
+ if (platform === "github") {
6124
+ token = process.env["GITHUB_TOKEN"];
6125
+ } else {
6126
+ token = process.env["GITLAB_TOKEN"];
6127
+ }
6128
+ }
6129
+ if (!token) {
6130
+ const envName = platform === "github" ? "GITHUB_TOKEN" : "GITLAB_TOKEN";
6131
+ return `Error: No ${platform} token provided. Set the ${envName} environment variable or pass a "token" field in the input.`;
6132
+ }
6133
+ try {
6134
+ if (platform === "github") {
6135
+ return await this.executeGitHub(operation, repo, token, input);
6136
+ }
6137
+ return await this.executeGitLab(operation, repo, token, input);
6138
+ } catch (err) {
6139
+ const axiosErr = err;
6140
+ if (axiosErr?.response?.status) {
6141
+ const status = axiosErr.response.status;
6142
+ const msg = axiosErr.response.data?.message ?? "";
6143
+ switch (status) {
6144
+ case 401:
6145
+ return `Authentication failed: Your ${platform} token is invalid or expired. Check your token and try again.`;
6146
+ case 403:
6147
+ return `Permission denied: Your ${platform} token lacks the required scopes for this operation. Needed: repo or workflow.`;
6148
+ case 404:
6149
+ return `Not found: Repository "${repo}" does not exist, or your token cannot access it.`;
6150
+ case 422:
6151
+ return `Validation error from ${platform}: ${msg || "Check your input parameters (branch names, base/head refs, etc.)."}`;
6152
+ case 429:
6153
+ return `Rate limited by ${platform}. Please wait a moment before trying again.`;
6154
+ default:
6155
+ return `${platform} API error (${status}): ${msg || (axiosErr.message ?? "Unknown error")}`;
6156
+ }
6157
+ }
6158
+ return `${platform} request failed: ${axiosErr.message ?? String(err)}`;
5710
6159
  }
5711
- return this.executeGitLab(operation, repo, token, input);
5712
6160
  }
5713
6161
  async executeGitHub(operation, repo, token, input) {
5714
6162
  const headers = {
@@ -5795,6 +6243,7 @@ ${response.data.description}`;
5795
6243
  };
5796
6244
 
5797
6245
  // src/tools/browser.ts
6246
+ var BROWSER_LAUNCH_TIMEOUT_MS = 15e3;
5798
6247
  var BrowserTool = class extends BaseTool {
5799
6248
  name = "browser";
5800
6249
  description = "Control a browser: navigate to URLs, click elements, fill forms, take screenshots. Only available with multimodal models.";
@@ -5803,7 +6252,7 @@ var BrowserTool = class extends BaseTool {
5803
6252
  properties: {
5804
6253
  action: {
5805
6254
  type: "string",
5806
- enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait"]
6255
+ enum: ["navigate", "click", "fill", "screenshot", "evaluate", "extract_text", "wait", "close"]
5807
6256
  },
5808
6257
  url: { type: "string", description: "URL to navigate to" },
5809
6258
  selector: { type: "string", description: "CSS selector for click/fill" },
@@ -5823,53 +6272,86 @@ var BrowserTool = class extends BaseTool {
5823
6272
  try {
5824
6273
  playwright = await import('playwright');
5825
6274
  } catch {
5826
- throw new Error("Playwright is not installed. Run: npm install playwright && npx playwright install chromium");
6275
+ return "Error: Playwright is not installed. Run: npm install playwright && npx playwright install chromium";
5827
6276
  }
5828
- if (!this.browser) {
5829
- const pw = playwright;
5830
- this.browser = await pw.chromium.launch({ headless: true });
5831
- const b = this.browser;
5832
- this.page = await b.newPage();
5833
- }
5834
- const page = this.page;
5835
6277
  const action = input["action"];
5836
6278
  const timeout = input["timeout"] ?? 1e4;
5837
- switch (action) {
5838
- case "navigate": {
5839
- await page.goto(input["url"], { timeout });
5840
- return `Navigated to ${input["url"]}`;
5841
- }
5842
- case "click": {
5843
- await page.click(input["selector"], { timeout });
5844
- return `Clicked ${input["selector"]}`;
5845
- }
5846
- case "fill": {
5847
- await page.fill(input["selector"], input["value"]);
5848
- return `Filled ${input["selector"]} with value`;
5849
- }
5850
- case "screenshot": {
5851
- const buf = await page.screenshot({ type: "png" });
5852
- return `data:image/png;base64,${buf.toString("base64")}`;
5853
- }
5854
- case "evaluate": {
5855
- const result = await page.evaluate(input["script"]);
5856
- return JSON.stringify(result);
6279
+ if (action === "close") {
6280
+ await this.close();
6281
+ return "Browser closed.";
6282
+ }
6283
+ if (!this.browser || !this.page) {
6284
+ await this.close();
6285
+ const launchPromise = playwright.chromium.launch({ headless: true });
6286
+ const timeoutPromise = new Promise(
6287
+ (_, 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)
6288
+ );
6289
+ try {
6290
+ this.browser = await Promise.race([launchPromise, timeoutPromise]);
6291
+ this.page = await this.browser.newPage();
6292
+ } catch (err) {
6293
+ this.browser = null;
6294
+ this.page = null;
6295
+ return `Browser launch failed: ${err instanceof Error ? err.message : String(err)}`;
5857
6296
  }
5858
- case "extract_text": {
5859
- const text = await page.locator("body").innerText();
5860
- return text.slice(0, 1e4);
6297
+ }
6298
+ const page = this.page;
6299
+ try {
6300
+ switch (action) {
6301
+ case "navigate": {
6302
+ await page.goto(input["url"], { timeout });
6303
+ const title = await page.title();
6304
+ return `Navigated to ${input["url"]} (title: "${title}")`;
6305
+ }
6306
+ case "click": {
6307
+ await page.click(input["selector"], { timeout });
6308
+ return `Clicked ${input["selector"]}`;
6309
+ }
6310
+ case "fill": {
6311
+ await page.fill(input["selector"], input["value"]);
6312
+ return `Filled ${input["selector"]} with value`;
6313
+ }
6314
+ case "screenshot": {
6315
+ const buf = await page.screenshot({ type: "png" });
6316
+ return `data:image/png;base64,${buf.toString("base64")}`;
6317
+ }
6318
+ case "evaluate": {
6319
+ const result = await page.evaluate(input["script"]);
6320
+ return JSON.stringify(result);
6321
+ }
6322
+ case "extract_text": {
6323
+ const text = await page.locator("body").innerText();
6324
+ return text.slice(0, 1e4);
6325
+ }
6326
+ case "wait": {
6327
+ await page.waitForTimeout(timeout);
6328
+ return `Waited ${timeout}ms`;
6329
+ }
6330
+ default:
6331
+ return `Unknown browser action: ${action}. Supported: navigate, click, fill, screenshot, evaluate, extract_text, wait, close`;
5861
6332
  }
5862
- case "wait": {
5863
- await page.waitForTimeout(timeout);
5864
- return `Waited ${timeout}ms`;
6333
+ } catch (err) {
6334
+ const errMsg = err instanceof Error ? err.message : String(err);
6335
+ if (/Target closed|Page crashed|Navigation failed/i.test(errMsg)) {
6336
+ await this.close();
6337
+ return `Browser error (page reset): ${errMsg}`;
5865
6338
  }
5866
- default:
5867
- throw new Error(`Unknown browser action: ${action}`);
6339
+ return `Browser action "${action}" failed: ${errMsg}`;
5868
6340
  }
5869
6341
  }
5870
6342
  async close() {
5871
- if (this.browser) {
5872
- await this.browser.close();
6343
+ try {
6344
+ if (this.page) {
6345
+ await this.page.close().catch(() => {
6346
+ });
6347
+ this.page = null;
6348
+ }
6349
+ if (this.browser) {
6350
+ await this.browser.close().catch(() => {
6351
+ });
6352
+ this.browser = null;
6353
+ }
6354
+ } catch {
5873
6355
  this.browser = null;
5874
6356
  this.page = null;
5875
6357
  }
@@ -5966,6 +6448,19 @@ var PDFCreateTool = class extends BaseTool {
5966
6448
  });
5967
6449
  }
5968
6450
  };
6451
+ function detectCommand(candidates2) {
6452
+ for (const cmd of candidates2) {
6453
+ try {
6454
+ const which = process.platform === "win32" ? "where" : "which";
6455
+ child_process.execSync(`${which} ${cmd}`, { stdio: "ignore" });
6456
+ return cmd;
6457
+ } catch {
6458
+ }
6459
+ }
6460
+ return null;
6461
+ }
6462
+ var PYTHON_CMD = detectCommand(["python3", "python"]);
6463
+ var NODE_CMD = detectCommand(["node"]);
5969
6464
  var CodeInterpreterTool = class extends BaseTool {
5970
6465
  name = "run_code";
5971
6466
  description = "Execute a Python or Node.js script to perform complex tasks (data processing, file conversion, etc.). The script is automatically cleaned up after execution.";
@@ -5981,10 +6476,30 @@ var CodeInterpreterTool = class extends BaseTool {
5981
6476
  isDangerous() {
5982
6477
  return true;
5983
6478
  }
5984
- async execute(input, options) {
6479
+ async execute(input, _options) {
5985
6480
  const language = input["language"];
5986
6481
  const code = input["code"];
5987
6482
  const args = input["args"] ?? [];
6483
+ let cmdPrefix;
6484
+ if (language === "python") {
6485
+ if (!PYTHON_CMD) {
6486
+ return [
6487
+ "Error: Python interpreter not found.",
6488
+ "Please install Python and ensure it is in your PATH.",
6489
+ "Tried: python3, python"
6490
+ ].join("\n");
6491
+ }
6492
+ cmdPrefix = PYTHON_CMD;
6493
+ } else {
6494
+ if (!NODE_CMD) {
6495
+ return [
6496
+ "Error: Node.js interpreter not found.",
6497
+ "Please install Node.js and ensure it is in your PATH.",
6498
+ "Tried: node"
6499
+ ].join("\n");
6500
+ }
6501
+ cmdPrefix = NODE_CMD;
6502
+ }
5988
6503
  const tmpDir = path17__default.default.join(process.cwd(), ".cascade", "tmp");
5989
6504
  if (!fs14__default.default.existsSync(tmpDir)) {
5990
6505
  fs14__default.default.mkdirSync(tmpDir, { recursive: true });
@@ -5993,8 +6508,9 @@ var CodeInterpreterTool = class extends BaseTool {
5993
6508
  const fileName = `intp_${crypto.randomUUID().slice(0, 8)}.${extension}`;
5994
6509
  const filePath = path17__default.default.join(tmpDir, fileName);
5995
6510
  fs14__default.default.writeFileSync(filePath, code, "utf-8");
5996
- const cmdPrefix = language === "python" ? "python3" : "node";
5997
- const fullCmd = `${cmdPrefix} "${filePath}" ${args.map((a) => `"${a}"`).join(" ")}`;
6511
+ const quotedPath = `"${filePath}"`;
6512
+ const quotedArgs = args.map((a) => `"${a}"`).join(" ");
6513
+ const fullCmd = `${cmdPrefix} ${quotedPath}${quotedArgs ? " " + quotedArgs : ""}`;
5998
6514
  return new Promise((resolve) => {
5999
6515
  const startMs = Date.now();
6000
6516
  child_process.exec(fullCmd, { cwd: process.cwd(), timeout: 3e4 }, (error, stdout, stderr) => {
@@ -6007,10 +6523,17 @@ var CodeInterpreterTool = class extends BaseTool {
6007
6523
  console.error(`Failed to cleanup interpreter script ${filePath}:`, cleanupErr);
6008
6524
  }
6009
6525
  if (error) {
6010
- resolve(`Execution failed (${duration}ms):
6526
+ const timedOut = error.killed && duration >= 3e4;
6527
+ if (timedOut) {
6528
+ resolve(`Execution timed out after 30s. Consider breaking the task into smaller pieces.
6529
+ Partial stdout: ${stdout}
6530
+ Stderr: ${stderr}`);
6531
+ } else {
6532
+ resolve(`Execution failed (${duration}ms):
6011
6533
  Error: ${error.message}
6012
6534
  Stderr: ${stderr}
6013
6535
  Stdout: ${stdout}`);
6536
+ }
6014
6537
  } else {
6015
6538
  resolve(`Execution successful (${duration}ms):
6016
6539
  Stdout: ${stdout}
@@ -6075,6 +6598,186 @@ ${formatted}`;
6075
6598
  }
6076
6599
  };
6077
6600
 
6601
+ // src/tools/web-search.ts
6602
+ async function searchSearXNG(query, baseUrl, maxResults) {
6603
+ const url = new URL("/search", baseUrl);
6604
+ url.searchParams.set("q", query);
6605
+ url.searchParams.set("format", "json");
6606
+ url.searchParams.set("categories", "general");
6607
+ url.searchParams.set("engines", "google,bing,duckduckgo");
6608
+ const resp = await fetch(url.toString(), {
6609
+ headers: { "User-Agent": "Cascade-AI/1.0 WebSearchTool" },
6610
+ signal: AbortSignal.timeout(1e4)
6611
+ });
6612
+ if (!resp.ok) {
6613
+ throw new Error(`SearXNG returned HTTP ${resp.status}`);
6614
+ }
6615
+ const data = await resp.json();
6616
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6617
+ title: r.title ?? "",
6618
+ url: r.url ?? "",
6619
+ snippet: r.content ?? "",
6620
+ engine: `searxng(${r.engine ?? "unknown"})`
6621
+ }));
6622
+ }
6623
+ async function searchBrave(query, apiKey, maxResults) {
6624
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}&safesearch=off`;
6625
+ const resp = await fetch(url, {
6626
+ headers: {
6627
+ "Accept": "application/json",
6628
+ "Accept-Encoding": "gzip",
6629
+ "X-Subscription-Token": apiKey
6630
+ },
6631
+ signal: AbortSignal.timeout(1e4)
6632
+ });
6633
+ if (!resp.ok) {
6634
+ throw new Error(`Brave Search returned HTTP ${resp.status}`);
6635
+ }
6636
+ const data = await resp.json();
6637
+ return (data.web?.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6638
+ title: r.title ?? "",
6639
+ url: r.url ?? "",
6640
+ snippet: r.description ?? "",
6641
+ engine: "brave"
6642
+ }));
6643
+ }
6644
+ async function searchTavily(query, apiKey, maxResults) {
6645
+ const resp = await fetch("https://api.tavily.com/search", {
6646
+ method: "POST",
6647
+ headers: {
6648
+ "Content-Type": "application/json",
6649
+ "Authorization": `Bearer ${apiKey}`
6650
+ },
6651
+ body: JSON.stringify({
6652
+ query,
6653
+ max_results: maxResults,
6654
+ search_depth: "basic",
6655
+ include_answer: false,
6656
+ include_raw_content: false
6657
+ }),
6658
+ signal: AbortSignal.timeout(15e3)
6659
+ });
6660
+ if (!resp.ok) {
6661
+ throw new Error(`Tavily returned HTTP ${resp.status}`);
6662
+ }
6663
+ const data = await resp.json();
6664
+ return (data.results ?? []).filter((r) => r.url && r.title).slice(0, maxResults).map((r) => ({
6665
+ title: r.title ?? "",
6666
+ url: r.url ?? "",
6667
+ snippet: r.content ?? "",
6668
+ engine: "tavily"
6669
+ }));
6670
+ }
6671
+ async function searchDuckDuckGoLite(query, maxResults) {
6672
+ const resp = await fetch(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, {
6673
+ headers: { "User-Agent": "Mozilla/5.0 (compatible; Cascade-AI/1.0)" },
6674
+ signal: AbortSignal.timeout(1e4)
6675
+ });
6676
+ if (!resp.ok) throw new Error(`DuckDuckGo Lite returned HTTP ${resp.status}`);
6677
+ const html = await resp.text();
6678
+ const linkPattern = /<a[^>]+class="result-link"[^>]+href="([^"]+)"[^>]*>([^<]+)<\/a>/g;
6679
+ const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/g;
6680
+ const links = [];
6681
+ const snippets = [];
6682
+ let m;
6683
+ while ((m = linkPattern.exec(html)) !== null) {
6684
+ links.push({ url: m[1], title: m[2].trim() });
6685
+ }
6686
+ while ((m = snippetPattern.exec(html)) !== null) {
6687
+ snippets.push(m[1].replace(/<[^>]+>/g, "").trim());
6688
+ }
6689
+ return links.slice(0, maxResults).map((link, i) => ({
6690
+ title: link.title,
6691
+ url: link.url,
6692
+ snippet: snippets[i] ?? "",
6693
+ engine: "duckduckgo-lite"
6694
+ }));
6695
+ }
6696
+ var WebSearchTool = class extends BaseTool {
6697
+ name = "web_search";
6698
+ description = "Search the web for current information, news, documentation, or any topic. Returns a list of relevant results with titles, URLs, and snippets.";
6699
+ inputSchema = {
6700
+ type: "object",
6701
+ properties: {
6702
+ query: { type: "string", description: "The search query" },
6703
+ maxResults: { type: "number", description: "Number of results to return (default: 5, max: 10)" }
6704
+ },
6705
+ required: ["query"]
6706
+ };
6707
+ config;
6708
+ constructor(config = {}) {
6709
+ super();
6710
+ this.config = {
6711
+ searxngUrl: config.searxngUrl ?? process.env["SEARXNG_URL"],
6712
+ braveApiKey: config.braveApiKey ?? process.env["BRAVE_SEARCH_API_KEY"],
6713
+ tavilyApiKey: config.tavilyApiKey ?? process.env["TAVILY_API_KEY"],
6714
+ maxResults: config.maxResults ?? 5
6715
+ };
6716
+ }
6717
+ async execute(input, _options) {
6718
+ const query = input["query"];
6719
+ if (!query?.trim()) return "Error: query is required and must be non-empty.";
6720
+ const maxResults = Math.min(
6721
+ input["maxResults"] ?? this.config.maxResults ?? 5,
6722
+ 10
6723
+ );
6724
+ const errors = [];
6725
+ let results = [];
6726
+ if (this.config.searxngUrl) {
6727
+ try {
6728
+ results = await searchSearXNG(query, this.config.searxngUrl, maxResults);
6729
+ if (results.length > 0) return this.formatResults(query, results);
6730
+ errors.push("SearXNG: returned 0 results");
6731
+ } catch (err) {
6732
+ errors.push(`SearXNG: ${err instanceof Error ? err.message : String(err)}`);
6733
+ }
6734
+ }
6735
+ if (this.config.braveApiKey) {
6736
+ try {
6737
+ results = await searchBrave(query, this.config.braveApiKey, maxResults);
6738
+ if (results.length > 0) return this.formatResults(query, results);
6739
+ errors.push("Brave: returned 0 results");
6740
+ } catch (err) {
6741
+ errors.push(`Brave: ${err instanceof Error ? err.message : String(err)}`);
6742
+ }
6743
+ }
6744
+ if (this.config.tavilyApiKey) {
6745
+ try {
6746
+ results = await searchTavily(query, this.config.tavilyApiKey, maxResults);
6747
+ if (results.length > 0) return this.formatResults(query, results);
6748
+ errors.push("Tavily: returned 0 results");
6749
+ } catch (err) {
6750
+ errors.push(`Tavily: ${err instanceof Error ? err.message : String(err)}`);
6751
+ }
6752
+ }
6753
+ try {
6754
+ results = await searchDuckDuckGoLite(query, maxResults);
6755
+ if (results.length > 0) return this.formatResults(query, results);
6756
+ errors.push("DuckDuckGo Lite: returned 0 results");
6757
+ } catch (err) {
6758
+ errors.push(`DuckDuckGo Lite: ${err instanceof Error ? err.message : String(err)}`);
6759
+ }
6760
+ 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" : "";
6761
+ return [
6762
+ `Web search for "${query}" failed across all backends:`,
6763
+ ...errors.map((e) => ` \u2022 ${e}`),
6764
+ configHint
6765
+ ].join("\n");
6766
+ }
6767
+ formatResults(query, results) {
6768
+ const lines = [`Web search results for: "${query}"`, ""];
6769
+ for (let i = 0; i < results.length; i++) {
6770
+ const r = results[i];
6771
+ lines.push(`[${i + 1}] ${r.title}`);
6772
+ lines.push(` URL: ${r.url}`);
6773
+ if (r.snippet) lines.push(` ${r.snippet.slice(0, 300)}`);
6774
+ if (r.engine) lines.push(` Source: ${r.engine}`);
6775
+ lines.push("");
6776
+ }
6777
+ return lines.join("\n");
6778
+ }
6779
+ };
6780
+
6078
6781
  // src/tools/mcp.ts
6079
6782
  var McpToolWrapper = class extends BaseTool {
6080
6783
  name;
@@ -6196,7 +6899,8 @@ var ToolRegistry = class {
6196
6899
  new ImageAnalyzeTool(),
6197
6900
  new PDFCreateTool(),
6198
6901
  new CodeInterpreterTool(),
6199
- new PeerCommunicationTool()
6902
+ new PeerCommunicationTool(),
6903
+ new WebSearchTool(this.config.webSearch)
6200
6904
  ];
6201
6905
  for (const tool of tools) {
6202
6906
  tool.setWorkspaceRoot(this.workspaceRoot);
@@ -6220,8 +6924,23 @@ var ToolRegistry = class {
6220
6924
  return this.ignoreMatcher.ignores(posixRel);
6221
6925
  }
6222
6926
  };
6223
- var McpClient = class {
6927
+ var McpClient = class _McpClient {
6928
+ static activeProcessPids = /* @__PURE__ */ new Set();
6929
+ /**
6930
+ * Forcefully kills all known MCP child processes.
6931
+ * Call this from global process exit handlers to prevent zombie processes.
6932
+ */
6933
+ static killAllProcesses() {
6934
+ for (const pid of _McpClient.activeProcessPids) {
6935
+ try {
6936
+ process.kill(pid, "SIGKILL");
6937
+ } catch {
6938
+ }
6939
+ }
6940
+ _McpClient.activeProcessPids.clear();
6941
+ }
6224
6942
  clients = /* @__PURE__ */ new Map();
6943
+ transports = /* @__PURE__ */ new Map();
6225
6944
  tools = /* @__PURE__ */ new Map();
6226
6945
  trustedServers;
6227
6946
  approvalCallback;
@@ -6250,6 +6969,8 @@ var McpClient = class {
6250
6969
  );
6251
6970
  await client.connect(transport);
6252
6971
  this.clients.set(server.name, client);
6972
+ this.transports.set(server.name, transport);
6973
+ if (transport.pid) _McpClient.activeProcessPids.add(transport.pid);
6253
6974
  const toolsResult = await client.listTools();
6254
6975
  for (const tool of toolsResult.tools) {
6255
6976
  for (const existing of this.tools.values()) {
@@ -6271,8 +6992,11 @@ var McpClient = class {
6271
6992
  async disconnect(serverName) {
6272
6993
  const client = this.clients.get(serverName);
6273
6994
  if (client) {
6995
+ const transport = this.transports.get(serverName);
6996
+ if (transport?.pid) _McpClient.activeProcessPids.delete(transport.pid);
6274
6997
  await client.close();
6275
6998
  this.clients.delete(serverName);
6999
+ this.transports.delete(serverName);
6276
7000
  for (const key of this.tools.keys()) {
6277
7001
  if (key.startsWith(`${serverName}::`)) this.tools.delete(key);
6278
7002
  }
@@ -6300,6 +7024,13 @@ var McpClient = class {
6300
7024
  getConnectedServers() {
6301
7025
  return Array.from(this.clients.keys());
6302
7026
  }
7027
+ getActivePids() {
7028
+ const pids = [];
7029
+ for (const transport of this.transports.values()) {
7030
+ if (transport.pid) pids.push(transport.pid);
7031
+ }
7032
+ return pids;
7033
+ }
6303
7034
  isConnected(serverName) {
6304
7035
  return this.clients.has(serverName);
6305
7036
  }
@@ -6868,12 +7599,25 @@ var Cascade = class extends EventEmitter__default.default {
6868
7599
  looksLikeSimpleArtifactTask(prompt) {
6869
7600
  return /create .*\.(txt|md|json|csv)\b/i.test(prompt) && !/(research|compare|thorough|pdf|report|analy[sz]e|architecture|multi-agent)/i.test(prompt);
6870
7601
  }
6871
- async determineComplexity(prompt, conversationHistory = []) {
7602
+ async determineComplexity(prompt, workspacePath, conversationHistory = []) {
6872
7603
  if (this.looksLikeSimpleArtifactTask(prompt)) {
6873
7604
  return "Simple";
6874
7605
  }
7606
+ let workspaceContext = "";
7607
+ try {
7608
+ const files = await glob.glob("**/*.*", {
7609
+ cwd: workspacePath,
7610
+ ignore: ["node_modules/**", ".git/**", "dist/**", "build/**"],
7611
+ nodir: true
7612
+ });
7613
+ workspaceContext = `Workspace Scout: Found ~${files.length} source files in the project.`;
7614
+ } catch {
7615
+ workspaceContext = "Workspace Scout: Could not scan workspace.";
7616
+ }
6875
7617
  const sysPrompt = `You are a routing classifier for a hierarchical AI system. Determine task complexity using BOTH the latest user message and the recent conversation context.
6876
7618
 
7619
+ ${workspaceContext}
7620
+
6877
7621
  Classification:
6878
7622
  - "Simple": basic conversation, direct single-step work, or small troubleshooting
6879
7623
  - "Moderate": requires a few steps, some tool use, or a manager coordinating workers
@@ -6948,7 +7692,7 @@ ${prompt}` : prompt;
6948
7692
  }
6949
7693
  escalator.resolveUserDecision(req.id, approved, always);
6950
7694
  });
6951
- const complexity = await this.determineComplexity(options.prompt, options.conversationHistory);
7695
+ const complexity = await this.determineComplexity(options.prompt, options.workspacePath || process.cwd(), options.conversationHistory);
6952
7696
  this.telemetry.capture("cascade:session_start", {
6953
7697
  complexity,
6954
7698
  providerCount: this.config.providers.length,
@@ -7028,7 +7772,7 @@ ${prompt}` : prompt;
7028
7772
  peerT3Ids: [],
7029
7773
  parentT2: "root"
7030
7774
  };
7031
- const t3Result = await t3.execute(assignment, taskId);
7775
+ const t3Result = await t3.execute(assignment, taskId, options.signal);
7032
7776
  finalOutput = typeof t3Result.output === "string" ? t3Result.output : JSON.stringify(t3Result.output);
7033
7777
  this.emit("tier:status", { tierId: "t3-root", status: "COMPLETED", role: "T3" });
7034
7778
  } else if (complexity === "Moderate") {
@@ -7051,7 +7795,7 @@ ${prompt}` : prompt;
7051
7795
  constraints: [],
7052
7796
  t3Subtasks: []
7053
7797
  };
7054
- const t2Result = await t2.execute(assignment, taskId);
7798
+ const t2Result = await t2.execute(assignment, taskId, options.signal);
7055
7799
  this.emit("tier:status", { tierId: "t2-root", status: "COMPLETED", role: "T2" });
7056
7800
  t2Results = [t2Result];
7057
7801
  const completed = t2Result.t3Results.filter((r) => r.status === "COMPLETED");
@@ -7073,13 +7817,22 @@ ${prompt}` : prompt;
7073
7817
  if (toolCreator) t1.setToolCreator(toolCreator);
7074
7818
  bindTierEvents(t1);
7075
7819
  t1.on("plan", (e) => this.emit("plan", e));
7076
- const result = await t1.execute(options.prompt, options.images);
7820
+ const result = await t1.execute(options.prompt, options.images, void 0, options.signal);
7077
7821
  finalOutput = result.output;
7078
7822
  t2Results = result.t2Results;
7079
7823
  }
7080
7824
  } catch (err) {
7081
- runError = err;
7082
- throw err;
7825
+ if (err instanceof CascadeCancelledError) {
7826
+ this.emit("run:cancelled", {
7827
+ taskId,
7828
+ reason: err.message,
7829
+ partialOutput: finalOutput || ""
7830
+ });
7831
+ runError = null;
7832
+ } else {
7833
+ runError = err;
7834
+ throw err;
7835
+ }
7083
7836
  } finally {
7084
7837
  try {
7085
7838
  escalator.cancelAllPending();
@@ -7394,11 +8147,6 @@ var SlashCommandRegistry = class {
7394
8147
  description: "Show active models per tier",
7395
8148
  handler: (_args, ctx) => ({ output: ctx.onModelInfo(), handled: true })
7396
8149
  });
7397
- this.register({
7398
- command: "/models",
7399
- description: "Browse available models by provider",
7400
- handler: (_args, ctx) => ({ output: ctx.onModelsInfo(), handled: true })
7401
- });
7402
8150
  this.register({
7403
8151
  command: "/providers",
7404
8152
  description: "Show configured providers",
@@ -7814,6 +8562,9 @@ var ModelsDisplay = ({
7814
8562
  });
7815
8563
  const title = step === "PROVIDER" ? "\u25C8 SELECT PROVIDER" : step === "TIER" ? `\u25C8 APPLY ${picked.provider === "auto" ? "AUTO" : String(picked.provider).toUpperCase()} TO WHICH TIER?` : `\u25C8 ${String(picked.provider).toUpperCase()} \u2192 SELECT MODEL FOR ${picked.tier}`;
7816
8564
  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}`;
8565
+ const PAGE_SIZE = 8;
8566
+ const viewStart = Math.max(0, Math.min(cursor - Math.floor(PAGE_SIZE / 2), currentItems.length - PAGE_SIZE));
8567
+ const visibleItems = currentItems.slice(viewStart, viewStart + PAGE_SIZE);
7817
8568
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
7818
8569
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { justifyContent: "space-between", children: [
7819
8570
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, color: "cyan", children: title }),
@@ -7823,16 +8574,29 @@ var ModelsDisplay = ({
7823
8574
  breadcrumb,
7824
8575
  " \xB7 \u2191/\u2193 navigate \xB7 1\u20139 jump"
7825
8576
  ] }) }),
7826
- currentItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", children: currentItems.map((item, i) => {
7827
- const focused = i === cursor;
7828
- return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
7829
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
7830
- /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
7831
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
7832
- item.sublabel && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
7833
- ] })
7834
- ] }, `${step}-${item.value}-${i}`);
7835
- }) })
8577
+ currentItems.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { italic: true, color: "yellow", children: "No items to show." }) : /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
8578
+ viewStart > 0 && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "gray", dimColor: true, children: [
8579
+ " \u2191 ",
8580
+ viewStart,
8581
+ " more above"
8582
+ ] }),
8583
+ visibleItems.map((item, i) => {
8584
+ const globalIdx = viewStart + i;
8585
+ const focused = globalIdx === cursor;
8586
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "row", children: [
8587
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "green" : "gray", children: focused ? "\u276F " : " " }),
8588
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
8589
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: focused ? "white" : "gray", bold: focused, children: item.label }),
8590
+ item.sublabel && /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "gray", dimColor: true, children: ` ${item.sublabel}` })
8591
+ ] })
8592
+ ] }, `${step}-${item.value}-${globalIdx}`);
8593
+ }),
8594
+ viewStart + PAGE_SIZE < currentItems.length && /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "gray", dimColor: true, children: [
8595
+ " \u2193 ",
8596
+ currentItems.length - viewStart - PAGE_SIZE,
8597
+ " more below"
8598
+ ] })
8599
+ ] })
7836
8600
  ] });
7837
8601
  };
7838
8602
  function CostTracker({
@@ -8026,13 +8790,18 @@ function replReducer(state, action) {
8026
8790
  async function refreshModelCache(store, providers) {
8027
8791
  for (const provider of providers) {
8028
8792
  try {
8029
- const dummyModel = { id: "dummy", name: "dummy", provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8793
+ const dummyId = provider.type === "azure" ? provider.deploymentName || "azure-model" : "dummy";
8794
+ const dummyModel = { id: dummyId, name: dummyId, provider: provider.type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
8030
8795
  let instance;
8031
8796
  if (provider.type === "openai") instance = new OpenAIProvider(provider, dummyModel);
8032
8797
  else if (provider.type === "gemini") instance = new GeminiProvider(provider, dummyModel);
8033
8798
  else if (provider.type === "anthropic") instance = new AnthropicProvider(provider, dummyModel);
8034
8799
  else if (provider.type === "ollama") instance = new OllamaProvider(provider, dummyModel);
8035
8800
  else if (provider.type === "openai-compatible") instance = new OpenAICompatibleProvider(provider, dummyModel);
8801
+ else if (provider.type === "azure") {
8802
+ const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
8803
+ instance = new AzureOpenAIProvider2(provider, dummyModel);
8804
+ }
8036
8805
  if (instance) {
8037
8806
  const fetched = await instance.listModels();
8038
8807
  for (const m of fetched) store.upsertCachedModel(m);
@@ -8170,7 +8939,7 @@ function Repl({ config, workspacePath, themeName, initialPrompt, identityName })
8170
8939
  };
8171
8940
  const store = new MemoryStore(path17__default.default.join(workspacePath, CASCADE_DB_FILE));
8172
8941
  storeRef.current = store;
8173
- globalStoreRef.current = new MemoryStore(path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8942
+ globalStoreRef.current = new MemoryStore(path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE));
8174
8943
  const identityRows = store.listIdentities().map((i) => ({ id: i.id, name: i.name, isDefault: i.isDefault }));
8175
8944
  setIdentities(identityRows);
8176
8945
  let initialIdentityId = config.defaultIdentityId ?? identityRows.find((i) => i.isDefault)?.id ?? identityRows[0]?.id;
@@ -8655,14 +9424,29 @@ Use /identity <name|id> to switch.`;
8655
9424
  const isAutoScrollingRef = React5.useRef(true);
8656
9425
  const width = stdout?.columns ?? 100;
8657
9426
  const height = stdout?.rows ?? 24;
8658
- const statusHeight = state.showDetails ? state.agentTree ? 10 : 4 : 3;
9427
+ const hasActiveOrFailed2 = (node) => {
9428
+ if (node.status === "ACTIVE" || node.status === "FAILED") return true;
9429
+ return node.children?.some(hasActiveOrFailed2) ?? false;
9430
+ };
9431
+ let agentTreeHeight = 0;
9432
+ if (state.agentTree && hasActiveOrFailed2(state.agentTree)) {
9433
+ agentTreeHeight = 1;
9434
+ const childrenCount = state.agentTree.children?.length ?? 0;
9435
+ agentTreeHeight += Math.min(childrenCount, 6);
9436
+ if (childrenCount > 6) agentTreeHeight += 1;
9437
+ }
9438
+ let timelineHeight = 0;
9439
+ if (state.showDetails && treeNodesRef.current.size > 0) {
9440
+ timelineHeight = 1;
9441
+ timelineHeight += Math.min(3, treeNodesRef.current.size);
9442
+ }
9443
+ const statusHeight = agentTreeHeight + timelineHeight;
8659
9444
  const costHeight = state.showCost ? 6 : 0;
8660
9445
  const approvalHeight = state.approvalRequest ? 12 : 0;
8661
- const slashVisibleCount = Math.min(SLASH_PAGE_SIZE, slashEntries.length);
8662
- const slashHeight = slashVisibleCount > 0 ? slashVisibleCount + 2 : 0;
9446
+ Math.min(SLASH_PAGE_SIZE, slashEntries.length);
9447
+ const slashHeight = isTypingCommand ? SLASH_PAGE_SIZE + 2 : 0;
8663
9448
  const chromeHeight = statusHeight + costHeight + approvalHeight + slashHeight + 7;
8664
- const totalCap = Math.floor(height * 0.7);
8665
- const availableHeight = Math.max(4, totalCap - chromeHeight);
9449
+ const availableHeight = Math.max(4, height - chromeHeight);
8666
9450
  const allLines = formatToLines(
8667
9451
  state.isStreaming ? [...state.messages, { id: "stream", role: "assistant", content: state.streamBuffer, timestamp: (/* @__PURE__ */ new Date()).toISOString() }] : state.messages,
8668
9452
  width - 4,
@@ -9043,10 +9827,14 @@ function wizardReducer(state, action) {
9043
9827
  return { ...state, currentEntryIdx: next };
9044
9828
  }
9045
9829
  case "ADD_AZURE": {
9830
+ const prevAzure = state.entries.find((e) => e.type === "azure");
9046
9831
  const newEntry = {
9047
9832
  id: crypto.randomUUID(),
9048
9833
  type: "azure",
9049
- label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`
9834
+ label: `Azure deployment ${state.entries.filter((e) => e.type === "azure").length + 1}`,
9835
+ baseUrl: prevAzure?.baseUrl,
9836
+ apiKey: prevAzure?.apiKey,
9837
+ apiVersion: prevAzure?.apiVersion
9050
9838
  };
9051
9839
  return {
9052
9840
  ...state,
@@ -9068,8 +9856,21 @@ function wizardReducer(state, action) {
9068
9856
  addingAnotherCompat: false
9069
9857
  };
9070
9858
  }
9859
+ case "ADD_OLLAMA": {
9860
+ const newEntry = {
9861
+ id: crypto.randomUUID(),
9862
+ type: "ollama",
9863
+ label: `Ollama endpoint ${state.entries.filter((e) => e.type === "ollama").length + 1}`
9864
+ };
9865
+ return {
9866
+ ...state,
9867
+ entries: [...state.entries, newEntry],
9868
+ currentEntryIdx: state.entries.length,
9869
+ addingAnotherOllama: false
9870
+ };
9871
+ }
9071
9872
  case "SKIP_MORE":
9072
- return { ...state, addingAnotherAzure: false, addingAnotherCompat: false, step: "FETCH_MODELS", currentEntryIdx: 0 };
9873
+ return { ...state, addingAnotherAzure: false, addingAnotherCompat: false, addingAnotherOllama: false, step: "FETCH_MODELS", currentEntryIdx: 0 };
9073
9874
  case "GO_FETCH":
9074
9875
  return { ...state, step: "FETCH_MODELS", fetchLog: [], fetchedModels: [] };
9075
9876
  case "SET_FETCH_LOG":
@@ -9102,6 +9903,7 @@ function SetupWizard({ workspacePath, onComplete }) {
9102
9903
  currentEntryIdx: 0,
9103
9904
  addingAnotherAzure: false,
9104
9905
  addingAnotherCompat: false,
9906
+ addingAnotherOllama: false,
9105
9907
  fetchedModels: [],
9106
9908
  fetchLog: [],
9107
9909
  tierT1: "auto",
@@ -9155,8 +9957,9 @@ function SetupWizard({ workspacePath, onComplete }) {
9155
9957
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
9156
9958
  } else if (type === "azure") {
9157
9959
  const { AzureOpenAIProvider: AzureOpenAIProvider2 } = await Promise.resolve().then(() => (init_azure(), azure_exports));
9158
- const dummyModel = { id: "dummy", name: "dummy", provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9159
- const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName }, dummyModel);
9960
+ const actualModelId = deploymentName || `azure-${entry.id}`;
9961
+ const dummyModel = { id: actualModelId, name: actualModelId, provider: type, contextWindow: 0, isVisionCapable: false, inputCostPer1kTokens: 0, outputCostPer1kTokens: 0, maxOutputTokens: 0, supportsStreaming: false, isLocal: false };
9962
+ const p = new AzureOpenAIProvider2({ type, apiKey, baseUrl, deploymentName, apiVersion: entry.apiVersion }, dummyModel);
9160
9963
  const fetched = await p.listModels();
9161
9964
  fetched.forEach((m) => models.push({ id: m.id, name: m.name, providerLabel: entry.label }));
9162
9965
  dispatchRef.current({ type: "SET_FETCH_LOG", line: ` \u2714 ${entry.label} \u2014 ${fetched.length} models` });
@@ -9177,7 +9980,8 @@ function SetupWizard({ workspacePath, onComplete }) {
9177
9980
  type: e.type,
9178
9981
  ...e.apiKey ? { apiKey: e.apiKey } : {},
9179
9982
  ...e.baseUrl ? { baseUrl: e.baseUrl } : {},
9180
- ...e.deploymentName ? { deploymentName: e.deploymentName } : {}
9983
+ ...e.deploymentName ? { deploymentName: e.deploymentName } : {},
9984
+ ...e.apiVersion ? { apiVersion: e.apiVersion } : {}
9181
9985
  }));
9182
9986
  const models = {};
9183
9987
  if (state.tierT1 !== "auto") models["t1"] = state.tierT1;
@@ -9199,25 +10003,28 @@ function SetupWizard({ workspacePath, onComplete }) {
9199
10003
  }, [state.step, state.entries, state.tierT1, state.tierT2, state.tierT3, workspacePath, onComplete, exit]);
9200
10004
  ink.useInput((_input, key) => {
9201
10005
  if (state.step === "PROVIDER_SELECT") {
9202
- if (key.upArrow) setProviderCursor((p) => Math.max(0, p - 1));
9203
- if (key.downArrow) setProviderCursor((p) => Math.min(providerOrder.length - 1, p + 1));
10006
+ if (key.upArrow) setProviderCursor((p) => p <= 0 ? providerOrder.length - 1 : p - 1);
10007
+ if (key.downArrow) setProviderCursor((p) => p >= providerOrder.length - 1 ? 0 : p + 1);
9204
10008
  if (_input === " ") dispatch({ type: "TOGGLE_PROVIDER", provider: providerOrder[providerCursor] });
9205
10009
  if (_input === "a") dispatch({ type: "TOGGLE_ALL" });
9206
10010
  if (_input === "i") dispatch({ type: "INVERT_SELECTION" });
9207
10011
  if (key.return) {
9208
- if (state.selectedTypes.size === 0) return;
10012
+ if (state.selectedTypes.size === 0) {
10013
+ dispatch({ type: "SET_ERROR", error: "Please select at least one provider using <space> before pressing <enter>." });
10014
+ return;
10015
+ }
9209
10016
  dispatch({ type: "CONFIRM_PROVIDERS" });
9210
- setFieldStage("apiKey");
10017
+ const firstType = [...state.selectedTypes][0];
10018
+ setFieldStage(firstType === "azure" ? "deploymentName" : firstType === "openai-compatible" ? "label" : firstType === "ollama" ? "baseUrl" : "apiKey");
9211
10019
  setFieldBuffer("");
9212
10020
  }
9213
10021
  }
9214
10022
  if (state.step === "TIER_ASSIGN") {
9215
- if (key.tab || key.downArrow) {
10023
+ if (key.tab || key.rightArrow) {
9216
10024
  const order = ["T1", "T2", "T3"];
9217
10025
  const idx = order.indexOf(state.tierSelectFocus);
9218
10026
  dispatch({ type: "SET_TIER_FOCUS", tier: order[(idx + 1) % 3] });
9219
10027
  }
9220
- if (key.return) dispatch({ type: "GO_SAVE" });
9221
10028
  }
9222
10029
  });
9223
10030
  const currentEntry = state.entries[state.currentEntryIdx];
@@ -9227,7 +10034,11 @@ function SetupWizard({ workspacePath, onComplete }) {
9227
10034
  if (fieldStage === "deploymentName") {
9228
10035
  dispatch({ type: "SET_ENTRY_FIELD", field: "deploymentName", value: val });
9229
10036
  setFieldBuffer("");
9230
- setFieldStage("baseUrl");
10037
+ if (currentEntry.baseUrl && currentEntry.apiKey && currentEntry.apiVersion) {
10038
+ setFieldStage("askMore");
10039
+ } else {
10040
+ setFieldStage("baseUrl");
10041
+ }
9231
10042
  } else if (fieldStage === "baseUrl") {
9232
10043
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val });
9233
10044
  setFieldBuffer("");
@@ -9235,6 +10046,10 @@ function SetupWizard({ workspacePath, onComplete }) {
9235
10046
  } else if (fieldStage === "apiKey") {
9236
10047
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9237
10048
  setFieldBuffer("");
10049
+ setFieldStage("apiVersion");
10050
+ } else if (fieldStage === "apiVersion") {
10051
+ dispatch({ type: "SET_ENTRY_FIELD", field: "apiVersion", value: val || "2024-08-01-preview" });
10052
+ setFieldBuffer("");
9238
10053
  setFieldStage("askMore");
9239
10054
  }
9240
10055
  } else if (currentEntry.type === "openai-compatible") {
@@ -9254,13 +10069,15 @@ function SetupWizard({ workspacePath, onComplete }) {
9254
10069
  } else if (currentEntry.type === "ollama") {
9255
10070
  dispatch({ type: "SET_ENTRY_FIELD", field: "baseUrl", value: val || "http://localhost:11434" });
9256
10071
  setFieldBuffer("");
9257
- dispatch({ type: "NEXT_ENTRY" });
9258
- setFieldStage("apiKey");
10072
+ setFieldStage("askMore");
9259
10073
  } else {
9260
10074
  dispatch({ type: "SET_ENTRY_FIELD", field: "apiKey", value: val });
9261
10075
  setFieldBuffer("");
10076
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10077
+ if (nextEntry) {
10078
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10079
+ }
9262
10080
  dispatch({ type: "NEXT_ENTRY" });
9263
- setFieldStage("apiKey");
9264
10081
  }
9265
10082
  }, [currentEntry, fieldStage]);
9266
10083
  if (state.step === "PROVIDER_SELECT") {
@@ -9293,7 +10110,7 @@ function SetupWizard({ workspacePath, onComplete }) {
9293
10110
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9294
10111
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, children: [
9295
10112
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "magenta", bold: true, children: "? " }),
9296
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: isAzure ? "Add another Azure deployment? (y/n)" : "Add another custom endpoint? (y/n)" })
10113
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, children: isAzure ? "Add another Azure deployment? (y/n)" : isOllama ? "Add another Ollama endpoint? (y/n)" : "Add another custom endpoint? (y/n)" })
9297
10114
  ] }),
9298
10115
  /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { children: /* @__PURE__ */ jsxRuntime.jsx(
9299
10116
  SelectInput__default.default,
@@ -9307,12 +10124,16 @@ function SetupWizard({ workspacePath, onComplete }) {
9307
10124
  onSelect: (item) => {
9308
10125
  if (item.value === "yes") {
9309
10126
  if (isAzure) dispatch({ type: "ADD_AZURE" });
10127
+ else if (isOllama) dispatch({ type: "ADD_OLLAMA" });
9310
10128
  else dispatch({ type: "ADD_COMPAT" });
9311
- setFieldStage(isAzure ? "deploymentName" : "label");
10129
+ setFieldStage(isAzure ? "deploymentName" : isOllama ? "baseUrl" : "label");
9312
10130
  setFieldBuffer("");
9313
10131
  } else {
10132
+ const nextEntry = state.entries[state.currentEntryIdx + 1];
10133
+ if (nextEntry) {
10134
+ setFieldStage(nextEntry.type === "azure" ? "deploymentName" : nextEntry.type === "openai-compatible" ? "label" : nextEntry.type === "ollama" ? "baseUrl" : "apiKey");
10135
+ }
9314
10136
  dispatch({ type: "NEXT_ENTRY" });
9315
- setFieldStage("apiKey");
9316
10137
  setFieldBuffer("");
9317
10138
  }
9318
10139
  }
@@ -9321,7 +10142,7 @@ function SetupWizard({ workspacePath, onComplete }) {
9321
10142
  ] });
9322
10143
  }
9323
10144
  const prompt = isAzure && fieldStage === "deploymentName" ? `Azure deployment name (${currentEntry.label})` : isAzure && fieldStage === "baseUrl" ? `Azure endpoint URL` : isCompat && fieldStage === "label" ? `Name for this endpoint (e.g. Groq)` : isCompat && fieldStage === "baseUrl" ? `Base URL (e.g. https://api.groq.com/openai/v1)` : isOllama ? `Ollama URL (Enter for http://localhost:11434)` : `${currentEntry.label} API Key`;
9324
- const isMasked = fieldStage === "apiKey";
10145
+ const isMasked = fieldStage === "apiKey" && !isOllama;
9325
10146
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
9326
10147
  /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { marginBottom: 1, children: [
9327
10148
  /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "magenta", bold: true, children: "? " }),
@@ -9393,7 +10214,16 @@ function SetupWizard({ workspacePath, onComplete }) {
9393
10214
  SelectInput__default.default,
9394
10215
  {
9395
10216
  items: modelOptions,
9396
- onSelect: (item) => dispatch({ type: "SET_TIER", tier, value: item.value }),
10217
+ onSelect: (item) => {
10218
+ dispatch({ type: "SET_TIER", tier, value: item.value });
10219
+ const order = ["T1", "T2", "T3"];
10220
+ const idx = order.indexOf(tier);
10221
+ if (idx < 2) {
10222
+ dispatch({ type: "SET_TIER_FOCUS", tier: order[idx + 1] });
10223
+ } else {
10224
+ dispatch({ type: "GO_SAVE" });
10225
+ }
10226
+ },
9397
10227
  indicatorComponent: ({ isSelected }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "magenta", children: isSelected ? "\u276F " : " " }),
9398
10228
  itemComponent: ({ isSelected, label }) => /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: isSelected ? "magenta" : "white", children: label })
9399
10229
  }
@@ -9871,7 +10701,7 @@ var DashboardServer = class {
9871
10701
  // ── Setup ─────────────────────────────────────
9872
10702
  getGlobalStore() {
9873
10703
  if (!this.globalStore) {
9874
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10704
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9875
10705
  this.globalStore = new MemoryStore(globalDbPath);
9876
10706
  }
9877
10707
  return this.globalStore;
@@ -9933,7 +10763,7 @@ var DashboardServer = class {
9933
10763
  }
9934
10764
  watchRuntimeChanges() {
9935
10765
  const workspaceDbPath = path17__default.default.join(this.workspacePath, CASCADE_DB_FILE);
9936
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10766
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
9937
10767
  const watchPaths = [workspaceDbPath, globalDbPath].filter((p, index, arr) => arr.indexOf(p) === index);
9938
10768
  for (const watchPath of watchPaths) {
9939
10769
  if (!fs14__default.default.existsSync(watchPath)) continue;
@@ -10041,7 +10871,7 @@ var DashboardServer = class {
10041
10871
  const sessionId = req.params.id;
10042
10872
  this.store.deleteSession(sessionId);
10043
10873
  this.store.deleteRuntimeSession(sessionId);
10044
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10874
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10045
10875
  const globalStore = new MemoryStore(globalDbPath);
10046
10876
  try {
10047
10877
  globalStore.deleteRuntimeSession(sessionId);
@@ -10055,7 +10885,7 @@ var DashboardServer = class {
10055
10885
  });
10056
10886
  this.app.delete("/api/sessions", auth, (req, res) => {
10057
10887
  const body = req.body;
10058
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10888
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10059
10889
  if (body?.ids && Array.isArray(body.ids) && body.ids.length > 0) {
10060
10890
  const globalStore = new MemoryStore(globalDbPath);
10061
10891
  try {
@@ -10078,7 +10908,7 @@ var DashboardServer = class {
10078
10908
  });
10079
10909
  this.app.delete("/api/runtime", auth, (_req, res) => {
10080
10910
  this.store.deleteAllRuntimeNodes();
10081
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10911
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10082
10912
  const globalStore = new MemoryStore(globalDbPath);
10083
10913
  try {
10084
10914
  globalStore.deleteAllRuntimeNodes();
@@ -10174,7 +11004,7 @@ var DashboardServer = class {
10174
11004
  this.app.get("/api/runtime", auth, (req, res) => {
10175
11005
  const scope = req.query["scope"] ?? "workspace";
10176
11006
  if (scope === "global") {
10177
- const globalDbPath = path17__default.default.join(process.env["HOME"] ?? process.cwd(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
11007
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_RUNTIME_DB_FILE);
10178
11008
  const globalStore = new MemoryStore(globalDbPath);
10179
11009
  try {
10180
11010
  res.json({
@@ -10424,7 +11254,15 @@ async function exportCommand(options = {}) {
10424
11254
  const spin = ora__default.default({ text: "Loading sessions\u2026", color: "magenta" }).start();
10425
11255
  let store;
10426
11256
  try {
10427
- const dbPath = path17__default.default.join(process.env["HOME"] ?? "~", GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11257
+ const workspacePath = options.workspacePath ?? process.cwd();
11258
+ const workspaceDbPath = path17__default.default.join(workspacePath, CASCADE_DB_FILE);
11259
+ const globalDbPath = path17__default.default.join(os3__default.default.homedir(), GLOBAL_CONFIG_DIR, GLOBAL_DB_FILE);
11260
+ let dbPath = globalDbPath;
11261
+ try {
11262
+ await fs7__default.default.access(workspaceDbPath);
11263
+ dbPath = workspaceDbPath;
11264
+ } catch {
11265
+ }
10428
11266
  store = new MemoryStore(dbPath);
10429
11267
  } catch (err) {
10430
11268
  spin.fail(chalk8__default.default.red(`Cannot open memory store: ${err instanceof Error ? err.message : String(err)}`));
@@ -10563,6 +11401,15 @@ async function telemetryCommand(action) {
10563
11401
 
10564
11402
  // src/cli/index.ts
10565
11403
  dotenv__default.default.config();
11404
+ process.on("exit", () => McpClient.killAllProcesses());
11405
+ process.on("SIGINT", () => {
11406
+ McpClient.killAllProcesses();
11407
+ process.exit(0);
11408
+ });
11409
+ process.on("SIGTERM", () => {
11410
+ McpClient.killAllProcesses();
11411
+ process.exit(0);
11412
+ });
10566
11413
  var program = new commander.Command();
10567
11414
  program.name("cascade").description("Multi-tier AI orchestration CLI").version(CASCADE_VERSION, "-v, --version").option("-p, --prompt <text>", "Run a single prompt non-interactively").option("-t, --theme <name>", "Color theme", DEFAULT_THEME).option("-w, --workspace <path>", "Workspace path", process.cwd()).option("-i, --identity <name>", "Identity name or ID").option("--no-color", "Disable colors").action(async (options) => {
10568
11415
  await startRepl(options);